From e1a5a8283ff69eb906d4c93b31d8cec1fae1b6c5 Mon Sep 17 00:00:00 2001 From: pdp8 Date: Wed, 21 Jun 2023 12:51:05 +0200 Subject: server, client, helpers separated --- activitypub.rb | 359 +-------------------------------------------------------- 1 file changed, 3 insertions(+), 356 deletions(-) (limited to 'activitypub.rb') diff --git a/activitypub.rb b/activitypub.rb index e250fe1..fd8583e 100644 --- a/activitypub.rb +++ b/activitypub.rb @@ -16,6 +16,7 @@ require 'uri' require 'base64' require 'digest/sha2' require 'sinatra' +require_relative 'helpers.rb' USER = "pdp8" WWW_DOMAIN = "pdp8.info" @@ -31,359 +32,5 @@ set :session_secret, File.read(".secret").chomp set :default_content_type, 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' set :port, 9292 -# server-server -post "/inbox" do - verify! - request.body.rewind # in case someone already read it - body = request.body.read - action = JSON.parse body - - case action["type"] - - when "Create" - create action["object"] - - when "Delete" - delete action["object"] - - when "Update" - delete action["object"] - create action["object"] - - when "Follow" - File.open(File.join("public", "followers", mention(action["actor"]) + ".json"), "w+") { |f| f.puts body } - accept = { "@context" => "https://www.w3.org/ns/activitystreams", - "id" => File.join(SOCIAL_URL + "#accepts", SecureRandom.uuid), - "type" => "Accept", - "actor" => ACTOR, - "object" => action } - send_signed accept, action["actor"] - - when "Undo" - o = action["object"] - case o["type"] - when "Follow" - Dir["public/followers/*.json"].each do |follower| - FileUtils.rm follower if JSON.parse(File.read(follower))["actor"] == o["actor"] - end - end - - when "Accept" - o = action["object"] - case o["type"] - when "Follow" - File.open(File.join("public","following",mention(o['object'])+".json"),"w+"){|f| f.puts o.to_json} - end - - #when "Announce" - #when "Move" - #when "Add" - #when "Remove" - #when "Like" - #when "Block" - - else - p "Unknown action: #{action['type']}" - p body - end -end - -# client-server -post "/outbox" do - protected! - request.body.rewind # in case someone already read it - body = request.body.read - date = Time.now.strftime("%Y-%m-%dT%H:%M:%S") - # TODO hashtags, replys - 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 - -post "/archive" do - protected! - FileUtils.mv params['file'], "archive/" - redirect to(params['redirect']) -end - -post "/delete" do - protected! - FileUtils.rm params['file'] - redirect to(params['redirect']) -end - -post "/delete_all" do - protected! - FileUtils.rm Dir["inbox/*.json"] - redirect to("/") -end - -post "/follow/*/*" do - protected! - mention = params['splat'][0] - actor = actor(mention) - follow = { "@context" => "https://www.w3.org/ns/activitystreams", - "id" => File.join(SOCIAL_URL, "following", mention + ".json"), - "type" => "Follow", - "actor" => ACTOR, - "object" => actor } - send_signed follow, actor - redirect to("/") -end - -post "/unfollow/*" do - protected! - mention = params['splat'][0] - actor = actor(mention) - following_path = File.join("public", "following", mention + ".json") - if File.exists?(following_path) - undo = { "@context" => "https://www.w3.org/ns/activitystreams", - "id" => File.join(SOCIAL_URL + "#undo", SecureRandom.uuid), - "type" => "Undo", - "actor" => ACTOR, - "object" => JSON.parse(File.read(following_path)) } - send_signed undo, actor - FileUtils.rm following_path - redirect to("/") - end -end - -post "/login" do - session["client"] = true if params["secret"] == File.read(".pwd").chomp - redirect to("/") -end - -# public -get "/.well-known/webfinger" do - if request["resource"] == "acct:#{ACCOUNT}" - send_file "./public/webfinger", :type => "application/jrd+json" - else - halt 404 - end -end - -get "/pdp8", :provides => 'html' do - redirect 'https://pdp8.info' -end - -get "/pdp8" do - send_file "pdp8.json", :type => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' -end - -["/outbox","/following","/followers"].each do |path| - get path do - ordered_collection(path).to_json - end -end - -# private -["/", "/archive"].each do |path| - get path, :provides => 'html' do - protected! - if path == '/' - @dir = 'inbox' - @alt_dir = '/archive' - @alt_name = 'archive' - else - @dir = path.sub('/','') - @alt_dir = '/' - @alt_name = 'inbox' - end - threads - erb :index - end -end - -helpers do - - def protected! - redirect("/login.html") unless session['client'] - end - - def verify! - # https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/signature_verification.rb - # TODO verify digest - begin - signature_params = {} - request.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 = fetch 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}: #{request.env["CONTENT_TYPE"]}" - else - "#{signed_params_name}: #{request.env["HTTP_" + signed_params_name.upcase]}" - end - end.join("\n") - - halt 400 unless key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison) - rescue => e - p request.env["HTTP_SIGNATURE"], e - halt 400 - end - end - - def threads - nr = 0 - items = Dir[File.join(@dir, '*.json')].sort.collect do |file| - item = JSON.parse(File.read(file)) - mention = mention(item['attributedTo']) - following_path = File.join('public', 'following', mention + '.json') - File.exists?(following_path) ? follow = 'unfollow' : follow = 'follow' - nr += 1 - { :id => item['id'], - :nr => nr, - :parent => item['inReplyTo'], - :file => file, - :actor_url => item['attributedTo'], - :mention => mention, - :follow => follow, - :content => item['content'], - :attachment => item['attachment'], - :indent => 2, - :replies => [] - } - end.compact - items.last[:nr] = items.last[:nr] - 2 - @threads = [] - items.each do |i| - if i[:parent].nil? or items.select{|it| it[:id] == i[:parent] }.empty? - @threads << i - else - items.select{|it| it[:id] == i[:parent] }.each do |it| - i[:indent] = it[:indent] + 2 - it[:replies] << i - end - end - end - end - - def html item - @item = item - erb :item - end - - def delete object - Dir["inbox/*.json"].each do |doc| - FileUtils.rm doc if JSON.parse(File.read(doc))["id"] == object["id"] - end - end - - def create object - unless object['type'] == 'Person' - 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 - end - - def people - File.read('inbox/people.tsv').split("\n").collect {|l| l.chomp.split("\t")} - end - - def mention actor - person = people.select{|p| p[1] == actor} - if person.empty? - mention = "#{fetch(actor)["preferredUsername"]}@#{URI(actor).host}" - File.open('inbox/people.tsv','a'){|f| f.puts "#{mention}\t#{actor}"} - mention - else - person[0][0] - end - end - - def actor mention - mention = mention.sub(/^@/, '').chomp - actors = people.select{|p| p[0] == mention} - if actors.empty? - user, server = mention.split("@") - actor = fetch("https://#{server}/.well-known/webfinger?resource=acct:#{mention}", - "application/jrd+json")["links"].select { |l| - l["rel"] == "self" - }[0]["href"] - File.open('inbox/people.tsv','a'){|f| f.puts "#{mention}\t#{actor}"} - actor - else - actors[0][0] - end - end - - def fetch url, accept = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' - p url - response = `/run/current-system/sw/bin/curl --fail-with-body -sSL -H 'Accept: #{accept}' #{url}` - halt 400 unless $?.success? - JSON.parse(response) - end - - def ordered_collection dir - posts = Dir[File.join("public",dir, "*.json")].collect { |f| JSON.parse(File.read f) }.sort_by { |o| o["published"] } - { - "@context" => "https://www.w3.org/ns/activitystreams", - "summary" => "#{USER} #{dir}", - "type" => "OrderedCollection", - "totalItems" => posts.size, - "orderedItems" => posts, - } - end - - def send_signed object, url - # https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb - keypair = OpenSSL::PKey::RSA.new(File.read('private.pem')) - date = Time.now.utc.httpdate - uri = URI.parse(url) - - sha256 = OpenSSL::Digest::SHA256.new - body = object.to_json - digest = "SHA-256=" + sha256.base64digest(body) - - signed_string = "(request-target): post #{fetch(uri)["inbox"]}\nhost: #{uri.host}\ndate: #{date}\ndigest: #{digest}\ncontent-type: application/activity+json" - 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 content-type",signature="' + signature + '"' - - puts `/run/current-system/sw/bin/curl -i -X POST -H 'Content-Type: application/activity+json' -H 'Host: #{uri.host}' -H 'Date: #{date}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' -d '#{body}' #{fetch(url)['inbox']}` - end - -end +require_relative 'server.rb' +require_relative 'client.rb' -- cgit v1.2.3