From 86c8118c9b908af2f0c2a74d2bc9f7af431e4e12 Mon Sep 17 00:00:00 2001 From: pdp8 Date: Mon, 29 May 2023 22:10:37 +0200 Subject: client post to outbox --- activitypub.rb | 187 +++++++++++++++++++++++++++------------------------------ 1 file changed, 88 insertions(+), 99 deletions(-) (limited to 'activitypub.rb') diff --git a/activitypub.rb b/activitypub.rb index c5f5079..c019d6e 100644 --- a/activitypub.rb +++ b/activitypub.rb @@ -5,12 +5,6 @@ # federation # client post media # test with pleroma etc -=begin -require 'json' -require 'securerandom' -require 'fileutils' -require 'nokogiri' -=end require 'uri' require 'base64' require 'digest/sha2' @@ -30,7 +24,13 @@ use Rack::Reloader set :default_content_type, 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' set :port, 9292 -post "/inbox" do +before "/inbox" do + if request.request_method == "POST" + halt 400 unless verify_signature(request.env) + end +end + +post "/inbox" do # server-server request.body.rewind # in case someone already read it body = request.body.read action = JSON.parse body @@ -64,6 +64,49 @@ post "/inbox" do end end +post "/outbox" do # client-server + request.body.rewind # in case someone already read it + body = request.body.read + date = Time.now.strftime("%Y-%m-%dT%H:%M:%S") + # TODO media attachments, hashtags + outbox_path = File.join("public/outbox", date + ".json") + object_path = File.join("public/objects", date + ".json") + create = { + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => File.join(SOCIAL_URL, outbox_path), + "type" => "Create", + "actor" => ACTOR, + "object" => { + "id" => File.join(SOCIAL_URL, object_path), + "type" => "Note", + "attributedTo" => ACTOR, + "published" => date, + "content" => "", + "to" => ["https://www.w3.org/ns/activitystreams#Public"] + }, + "published" => date, + "to" => ["https://www.w3.org/ns/activitystreams#Public"] + } + recipients = [] + if /^@/.match body + mentions, body = body.split("\n", 2) + mentions.split(/, */).each do |m| + recipients << actor(m.chomp) + end + end + create["object"]["content"] = body.lines.select { |l| !l.empty? }.join("
") + recipients += Dir[File.join("public/followers", "*.json")].collect { |f| JSON.parse(File.read(f))["actor"] } + recipients.delete ACTOR + recipients.uniq! + create["object"]["to"] += recipients + create["to"] += recipients + + File.open(outbox_path, "w+") { |f| f.puts create.to_json } + File.open(object_path, "w+") { |f| f.puts create["object"].to_json } + + recipients.each { |r| send_signed create, r } +end + get "/.well-known/webfinger" do if request["resource"] == "acct:#{ACCOUNT}" send_file "./webfinger", :type => "application/jrd+json" @@ -121,7 +164,7 @@ def delete object end def create object - doc = File.join("inbox", "#{object["published"]}-#{mention object["attributedTo"]}.json") + doc = File.join("inbox", "#{Time.now.strftime('%Y-%m-%dT%H:%M:%S.%N')}.json") File.open(doc, "w+") { |f| f.puts object.to_json } end @@ -184,11 +227,46 @@ def inbox uri URI(get(uri)["inbox"]).request_uri end -=begin -post "/outbox" do +def verify_signature env + # https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/signature_verification.rb + # TODO verify digest + signature_params = {} + env["HTTP_SIGNATURE"].split(',').each do |pair| + k, v = pair.split('=') + signature_params[k] = v.gsub('"', '') + end + + 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_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_params_name}: #{env["HTTP_" + signed_params_name.upcase]}" + end + end.join("\n") + key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison) end +def actor mention + mention = mention.sub(/^@/, '').chomp + user, server = mention.split("@") + get("https://#{server}/.well-known/webfinger?resource=acct:#{mention}", + "application/jrd+json")["links"].select { |l| + l["rel"] == "self" + }[0]["href"] +end + +=begin + class Application def call(env) code = 404 @@ -211,7 +289,6 @@ class Application p "outbox" if auth(env) p "OK" - code, response = parse input else code = 403 response = "You are not allowed to POST to #{env["REQUEST_URI"]}." @@ -257,64 +334,6 @@ class Application end end - def parse input - date = Time.now.strftime("%Y-%m-%dT%H:%M:%S") - # TODO media attachments, hashtags - note = { - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => File.join(SOCIAL_URL, "note", SecureRandom.uuid + ".json"), - "type" => "Note", - "attributedTo" => ACTOR, - "published" => date, - "content" => "", - "to" => ["https://www.w3.org/ns/activitystreams#Public"] - } - create = { - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => File.join(SOCIAL_URL, "create", SecureRandom.uuid + ".json"), - "type" => "Create", - "actor" => ACTOR, - "object" => note, - "published" => date, - "to" => ["https://www.w3.org/ns/activitystreams#Public"] - } - recipients = [] - if /^@/.match input - mentions, input = input.split("\n", 2) - mentions.split(/, */).each do |m| - recipients << actor(m.chomp) - end - end - note["content"] = input.lines.select { |l| !l.empty? }.join("
") - recipients += Dir[File.join("followers", "*.json")].collect { |f| JSON.parse(File.read(f))["actor"] } - recipients.delete ACTOR - recipients.uniq! - note["to"] += recipients - create["to"] += recipients - - save create - save note - FileUtils.ln_s File.join('..', path(create)), "outbox" - - responses = send create, recipients - if responses.collect { |r| r.code.to_i }.uniq.max < 400 - code = 200 - response = "OK" - else - code = 502 - response = responses.select { |r| r.code.to_i >= 400 }.collect { |r| r.body }.uniq - end - [code, response] - end - - def actor mention - mention = mention.sub(/^@/, '').chomp - user, server = mention.split("@") - get("https://#{server}/.well-known/webfinger?resource=acct:#{mention}", - "application/jrd+json")["links"].select { |l| - l["rel"] == "self" - }[0]["href"] - end def path object object["id"].sub(SOCIAL_URL, '').sub('/', '') @@ -326,36 +345,6 @@ class Application File.open(path, "w+") { |f| f.puts object.to_json } end - def verify env - # https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/signature_verification.rb - # TODO verify digest - signature_params = {} - env["HTTP_SIGNATURE"].split(',').each do |pair| - k, v = pair.split('=') - signature_params[k] = v.gsub('"', '') - end - - 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_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_params_name}: #{env["HTTP_" + signed_params_name.upcase]}" - end - end.join("\n") - - key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison) - # true - end - def auth env auth = Rack::Auth::Basic::Request.new(env) usr = File.read(".usr").chomp -- cgit v1.2.3