# TODO check POST to outbox with mastinator.com require 'json' require 'net/http' require 'uri' require 'base64' USER = "pdp8" WWW_DOMAIN = "pdp8.info" WWW_URL = "https://#{WWW_DOMAIN}" SOCIAL_DOMAIN = "social.#{WWW_DOMAIN}" ACCOUNT = "#{USER}@#{SOCIAL_DOMAIN}" SOCIAL_URL = "https://#{SOCIAL_DOMAIN}" ACTOR = URI.join(SOCIAL_URL, USER) MATRIX = "@#{USER}:matrix.#{WWW_DOMAIN}" class Application def call(env) code = 404 type = "application/activity+json" case env['REQUEST_METHOD'] when 'POST' case env["REQUEST_URI"] when "/inbox" # receive from server if verify(env) save JSON.parse(env["rack.input"].gets), "inbox" code = 200 response = "OK" else code = 401 response = "not verified" end 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 when 'GET' case env["REQUEST_URI"] when "/.well-known/webfinger?resource=acct:#{ACCOUNT}" type = "application/jrd+json" response = File.read("webfinger") code = 200 when "/#{USER}" response = File.read(USER) code = 200 when %r{/[inbox|outbox|following|followers|likes|shares]} response = ordered_collection env["REQUEST_URI"] code = 200 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 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" => "#{USER} #{collection}", "type" => "OrderedCollection", "totalItems" => posts.size, "orderedItems" => posts, }.to_json end def verify env 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) key = OpenSSL::PKey::RSA.new(actor['publicKey']['publicKeyPem']) 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").encode("ascii-8bit") key.verify(OpenSSL::Digest::SHA256.new, signature, comparison) rescue false end end def auth true end end