From e5a682e2ae5a5ceecdf0d05ff0f831535370f7d3 Mon Sep 17 00:00:00 2001 From: pdp8 Date: Sun, 23 Apr 2023 23:33:46 +0200 Subject: functions separated from routes --- application.rb | 135 +++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 98 insertions(+), 37 deletions(-) diff --git a/application.rb b/application.rb index 6e700fd..cd8c2ea 100644 --- a/application.rb +++ b/application.rb @@ -1,3 +1,4 @@ +# TODO check POST to outbox with mastinator.com require 'json' require 'net/http' require 'uri' @@ -14,9 +15,6 @@ ACTOR = URI.join(SOCIAL_URL, USER) MATRIX = "@#{USER}:matrix.#{WWW_DOMAIN}" -OUTBOX = File.join(File.dirname(__FILE__), "outbox") -INBOX = File.join(File.dirname(__FILE__), "inbox") - class Application def call(env) code = 404 @@ -29,8 +27,7 @@ class Application when "/inbox" # receive from server if verify(env) - input = JSON.parse(env["rack.input"].gets) - # p input["type"] + save JSON.parse(env["rack.input"].gets), "inbox" code = 200 response = "OK" else @@ -38,10 +35,28 @@ class Application response = "not verified" end - when "/outbox" - input = JSON.parse(env["rack.input"].gets) - case input["type"] - when "Create" + when "/outbox" # receive from client + # 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 + code = 200 + response = "OK" + else + code = 403 + response = "forbidden" end end @@ -56,67 +71,113 @@ class Application code = 200 when "/#{USER}" - response = File.read("pdp8") + response = File.read(USER) code = 200 - when "/outbox" - response = ordered_collection OUTBOX + when %r{/[inbox|outbox|following|followers|likes|shares]} + response = ordered_collection env["REQUEST_URI"] code = 200 - when "/inbox" - type = "application/activity+json" - response = ordered_collection INBOX - code = 200 - - # when "/following" - # when "/followers" - # when "/liked" end end [code, { "Content-Type" => type }, [response]] end + def activity object + date = Time.now.strftime("%Y-%m-%dT%H:%M:%S.%3N") + object["id"] = "TODO" + object["attributedTo"] = ACTOR + object["published"] = date + { + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "id" => "https://example.net/~mallory/87374", + "actor" => ACTOR, + "object" => object, + "published" => date, + "to" => object["to"], + "cc" => object["cc"] + } + 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 } + end + + def deliver addr + p addr + keypair = OpenSSL::PKey::RSA.new(File.read('private.pem')) + addr.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) + 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, + 'Date' => date, + 'Signature' => signed_header, + } + request = Net::HTTP::Post.new(inbox.request_uri, header) + request.body = document.to_json + + response = http.request(request) + puts(response.body, response.code) + end + end + def ordered_collection dir - posts = Dir[File.join(dir, "*.json")].sort.reverse.collect { |f| p f; JSON.parse(File.read f) } + collection = dir.sub(/^\//, "") + posts = Dir[File.join(collection, "*.json")].sort.reverse.collect { |f| p f; JSON.parse(File.read f) } { "@context" => "https://www.w3.org/ns/activitystreams", - "summary" => "pdp8 outbox", + "summary" => "#{USER} #{collection}", "type" => "OrderedCollection", "totalItems" => posts.size, "orderedItems" => posts, }.to_json end - def verify(env) + def verify env begin - signature_header = {} - env["HTTP_SIGNATURE"].split(',').each do |pair| - k, v = pair.split('=') - signature_header[k] = v.gsub('"', '') - end - key_id = signature_header['keyId'] - headers = signature_header['headers'] + 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) key = OpenSSL::PKey::RSA.new(actor['publicKey']['publicKeyPem']) - comparison_string = headers.split(' ').map do |signed_header_name| + comparison = headers.split(' ').map do |signed_header_name| if signed_header_name == '(request-target)' '(request-target): post /inbox' else "#{signed_header_name}: #{env["HTTP_" + signed_header_name.upcase]}" end - end.join("\n") - if key.verify(OpenSSL::Digest::SHA256.new, signature, comparison_string.encode("ascii-8bit")) - true - else - false - end + end.join("\n").encode("ascii-8bit") + + key.verify(OpenSSL::Digest::SHA256.new, signature, comparison) rescue false end end + + def auth + true + end end -- cgit v1.2.3