summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--application.rb193
-rw-r--r--outbox/230401T23:00:01.json4
-rw-r--r--outbox/230402T23:00:01.json10
-rw-r--r--pdp84
4 files changed, 133 insertions, 78 deletions
diff --git a/application.rb b/application.rb
index cd8c2ea..ab42c1f 100644
--- a/application.rb
+++ b/application.rb
@@ -1,8 +1,10 @@
-# TODO check POST to outbox with mastinator.com
 require 'json'
 require 'net/http'
 require 'uri'
 require 'base64'
+require 'securerandom'
+require 'fileutils'
+require 'digest/sha2'
 
 USER = "pdp8"
 WWW_DOMAIN = "pdp8.info"
@@ -11,47 +13,64 @@ SOCIAL_DOMAIN = "social.#{WWW_DOMAIN}"
 
 ACCOUNT = "#{USER}@#{SOCIAL_DOMAIN}"
 SOCIAL_URL = "https://#{SOCIAL_DOMAIN}"
-ACTOR = URI.join(SOCIAL_URL, USER)
+ACTOR = File.join(SOCIAL_URL, USER)
 
 MATRIX = "@#{USER}:matrix.#{WWW_DOMAIN}"
 
 class Application
   def call(env)
     code = 404
-    type = "application/activity+json"
+    type = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
+    response = "not allowed"
 
+    # p env["rack.input"].read
+    # if env["CONTENT_TYPE"] =~ /json/
+    # puts env["REMOTE_ADDR"]
     case env['REQUEST_METHOD']
 
     when 'POST'
+      input = env["rack.input"].read
       case env["REQUEST_URI"]
 
       when "/inbox" # receive from server
+        puts "POST INBOX"
         if verify(env)
-          save JSON.parse(env["rack.input"].gets), "inbox"
-          code = 200
-          response = "OK"
+          begin
+            # unless input.match(/<!DOCTYPE html>/)
+            object = JSON.parse(input)
+            # puts object
+            case object["type"]
+            when "Create"
+              File.open(File.join("inbox", SecureRandom.uuid + ".json"), "w+") { |f| f.puts input }
+            else
+              puts input
+            end
+            code = 200
+            response = "OK"
+          # end
+          rescue => e
+            # puts e.to_s
+            puts "Verification ERROR: "
+            # puts input
+            response = "invalid json"
+          end
         else
           code = 401
           response = "not verified"
         end
 
       when "/outbox" # receive from client
+        puts "POST OUTBOX"
         # TODO auth
         if auth(env)
-          input = JSON.parse(env["rack.input"].gets)
-          input["type"] ? activity = input : activity = activity(input) # expand object to create activity
-          save activity, "outbox"
-          deliver ["to", "bto", "cc", "bcc", "audience"].collect { |d| activity[d] }.flatten.uniq
-          case activity["type"]
-          when "Create"
-          when "Update"
-          when "Delete"
-          when "Follow"
-          when "Remove"
-          when "Like"
-          when "Block"
-          when "Undo"
-          end
+          input = JSON.parse(input)
+          input["type"] == "Create" ? activity = input : activity = activity(input) # expand object to create activity
+          add_id activity
+          save activity # , "outbox"
+          FileUtils.ln_s File.join('..', path(activity)), "outbox"
+          code, response = deliver activity, ["to", "bto", "cc", "bcc", "audience"].collect { |d|
+                                               activity[d]
+                                             }.flatten.uniq.compact
           code = 200
           response = "OK"
         else
@@ -71,6 +90,7 @@ class Application
         code = 200
 
       when "/#{USER}"
+        # TODO serve html
         response = File.read(USER)
         code = 200
 
@@ -81,18 +101,25 @@ class Application
       end
 
     end
+    # else
+    # response = "Cannot serve Content-type: " + env["CONTENT_TYPE"]
+    # end
     [code, { "Content-Type" => type }, [response]]
   end
 
+  def add_id object
+    object["id"] = File.join(SOCIAL_URL, object["type"].downcase, SecureRandom.uuid + ".json")
+  end
+
   def activity object
-    date = Time.now.strftime("%Y-%m-%dT%H:%M:%S.%3N")
-    object["id"] = "TODO"
+    date = Time.now.strftime("%Y-%m-%dT%H:%M:%S")
     object["attributedTo"] = ACTOR
     object["published"] = date
+    add_id object
+    save object
     {
       "@context" => "https://www.w3.org/ns/activitystreams",
       "type" => "Create",
-      "id" => "https://example.net/~mallory/87374",
       "actor" => ACTOR,
       "object" => object,
       "published" => date,
@@ -101,40 +128,62 @@ class Application
     }
   end
 
-  def save activity, dir
-    date = Time.now.strftime("%Y-%m-%dT%H:%M:%S.%3N")
-    File.open(File.join(dir, date), "w+") { |f| f.puts activity.to_json }
+  def path object
+    object["id"].sub(SOCIAL_URL, '').sub('/', '')
+  end
+
+  def save object
+    path = path object
+    FileUtils.mkdir_p File.dirname(path)
+    File.open(path, "w+") { |f| f.puts object.to_json }
+  end
+
+  def inbox uri
+    http = Net::HTTP.new(uri.host, uri.port)
+    http.use_ssl = true
+    header = { 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }
+    request = Net::HTTP::Get.new(uri.request_uri, header)
+    response = http.request(request)
+    JSON.parse(response.body)["inbox"]
   end
 
-  def deliver addr
-    p addr
+  def deliver object, urls
+    # https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb
     keypair = OpenSSL::PKey::RSA.new(File.read('private.pem'))
-    addr.each do |url|
+    urls.each do |url|
       date = Time.now.utc.httpdate
       uri = URI.parse(url)
-      signed_string = "(request-target): post /inbox\nhost: #{uri.host}\ndate: #{date}"
-      signed_string = keypair.sign(OpenSSL::Digest::SHA256.new, signed_string)
-      signature = Base64.urlsafe_encode64(signed_string).encode("UTF-8")
-      signed_header = 'keyId="#{url}",headers="(request-target) host date",signature="' + signature + '"'
-
-      inbox_url = JSON.parse(Net::HTTP.get(uri,
-                                           { 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }))["inbox"]
-      inbox = URI.parse(inbox_url)
-      http = Net::HTTP.new(inbox.host, inbox.port)
+
+      sha256 = OpenSSL::Digest::SHA256.new
+      body = object.to_json
+      digest = "SHA-256=" + sha256.base64digest(body)
+
+      signed_string = "(request-target): post /inbox\nhost: #{uri.host}\ndate: #{date}\ndigest: #{digest}"
+      signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
+      signed_header = 'keyId="' + ACTOR + '#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' + signature + '"'
+
+      uri = URI.parse(get(url)["inbox"])
+      http = Net::HTTP.new(uri.host, uri.port)
       http.use_ssl = true
-      # http.verify_mode = OpenSSL::SSL::VERIFY_NONE
       header = {
         'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
-        'Host' => inbox.host,
+        'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
+        'Host' => uri.host,
         'Date' => date,
+        'Digest' => digest,
         'Signature' => signed_header,
       }
-      request = Net::HTTP::Post.new(inbox.request_uri, header)
-      request.body = document.to_json
+      puts signed_header
+      request = Net::HTTP::Post.new(uri.request_uri, header)
+      request.body = body
 
       response = http.request(request)
-      puts(response.body, response.code)
+      # puts(response.body, response.code)
+      puts(response.code)
+      # puts(response.body["signed_string"])
+      # puts(response.body["signature"])
     end
+    # [response.code, response.body]
   end
 
   def ordered_collection dir
@@ -150,34 +199,56 @@ class Application
   end
 
   def verify env
+    # https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/signature_verification.rb
+    puts env
+    # puts env.select { |k, v| k.match(/^HTTP_/) }
+    #  puts env["HTTP_SIGNATURE"] # .split(',').each do |pair|
     begin
-      signature_header = env["HTTP_SIGNATURE"].split(',').each do |pair|
-        pair.gsub('"', '').split('=')
-      end.to_h
-      key_id = signature_header['keyId']
-      headers = signature_header['headers']
-      signature = Base64.urlsafe_decode64(signature_header['signature'].encode("ascii-8bit"))
-
-      uri = URI(key_id)
-      res = Net::HTTP.get_response(uri)
-      actor = JSON.parse(res.body)
+      signature_params = {}
+      env["HTTP_SIGNATURE"].split(',').each do |pair|
+        k, v = pair.split('=')
+        signature_params[k] = v.gsub('"', '')
+      end
+
+      # puts signature_params
+      key_id = signature_params['keyId']
+      headers = signature_params['headers']
+      signature = Base64.decode64(signature_params['signature'])
+
+      actor = get key_id
       key = OpenSSL::PKey::RSA.new(actor['publicKey']['publicKeyPem'])
 
-      comparison = headers.split(' ').map do |signed_header_name|
-        if signed_header_name == '(request-target)'
+      comparison = headers.split(' ').map do |signed_params_name|
+        if signed_params_name == '(request-target)'
           '(request-target): post /inbox'
+        elsif signed_params_name == 'content-type'
+          "#{signed_params_name}: #{env["CONTENT_TYPE"]}"
         else
-          "#{signed_header_name}: #{env["HTTP_" + signed_header_name.upcase]}"
+          "#{signed_params_name}: #{env["HTTP_" + signed_params_name.upcase]}"
         end
-      end.join("\n").encode("ascii-8bit")
-
-      key.verify(OpenSSL::Digest::SHA256.new, signature, comparison)
-    rescue
+      end.join("\n")
+
+      puts comparison
+      # key.verify(OpenSSL::Digest::SHA256.new, signature, comparison)
+      key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison)
+    rescue => e
+      puts e.class
+      # puts e.message
       false
     end
   end
 
-  def auth
+  def get url
+    uri = URI(url)
+    http = Net::HTTP.new(uri.host, uri.port)
+    http.use_ssl = true
+    header = { 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }
+    request = Net::HTTP::Get.new(uri.request_uri, header)
+    response = http.request(request)
+    JSON.parse(response.body)
+  end
+
+  def auth env
     true
   end
 end
diff --git a/outbox/230401T23:00:01.json b/outbox/230401T23:00:01.json
deleted file mode 100644
index baeb7f8..0000000
--- a/outbox/230401T23:00:01.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-  "type": "Note",
-  "name": "A Simple Note"
-}
diff --git a/outbox/230402T23:00:01.json b/outbox/230402T23:00:01.json
deleted file mode 100644
index bd50adb..0000000
--- a/outbox/230402T23:00:01.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "type": "Note",
-  "name": "Another Simple Note",
-  "tag": [
-    {
-      "type": "Hashtag",
-      "name": "#test"
-    }
-  ]
-}
diff --git a/pdp8 b/pdp8
index ceb25c9..7eca15f 100644
--- a/pdp8
+++ b/pdp8
@@ -33,10 +33,8 @@
     }
   ],
   "publicKey": {
-    "@context": "https://w3id.org/security/v1",
-    "@type": "Key",
     "id": "https://social.pdp8.info/pdp8#main-key",
     "owner": "https://social.pdp8.info/pdp8",
-    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4pZMYXoh8G+iEguDpKGD\n+eq+uDdhx/ch2x7rq00aPDDeHp40CG8bW1ZRC4WIOTUOgK4MeMDoaXT9/vWgr7xT\n/Qm95SEyZWBKqasBsp2uGkDxl23C6dB2eeshuAwt308Qzm2DeTrKPAw/XBAyWHDD\nfan2nWrtXcDJaeXhD/QE/w7Qiz5F2GCb/E/o46SwEyOJi13WxI9Jtuzh76xmwNsd\nwVWIBSu4zn0hg/wv+xtq/c/KLO4ZL54YiJXxRwrkDN7Xdnd18FwFuZ7fT8+kfiqF\nBnvle0OTKxumW46U7ivaylnqoSOvsYK6oyop/m2rl9Nh3sGdcmOsLoFVDg4gOjDf\niQIDAQAB\n-----END PUBLIC KEY-----\n"
+    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArDawzSl+XcJ+96sIrx+E\nsDoUQzSvoKazCgw7qOMaOGi7XxJ8riBvdRBlJ4zOEfQaxcaQgGn5JntOofqkeWvk\nIykOAzYfwY6HoUm7i1eZME2quO+CkMMq9SX9/DOqggOYtiVC9DX5FxXe5YHK7Q/n\nbo1iB6rgVS43wT0PnI6uduY4cUlvhRkX4Iht0N1GTrBlGKloRQ96KTzp+U9xF7bp\nKO87Y4yftv+d6L3ZZBfTRgWOtDXG8E4Vdvsq0aPQNBtazq0fwtBbk2G4mZtCMqyT\nvLZh8w+YPn1ICoQsKukU/q7eG29UJCz/QdZndkuv5iIm+H/c8gicGllw9rNQP2G0\nBQIDAQAB\n-----END PUBLIC KEY-----\n"
   }
 }