diff options
-rw-r--r-- | client.rb | 18 | ||||
-rw-r--r-- | helpers.rb | 20 | ||||
-rw-r--r-- | server.rb | 57 |
3 files changed, 54 insertions, 41 deletions
@@ -66,7 +66,8 @@ post "/outbox" do File.open(outbox_path, "w+") { |f| f.puts create.to_json } File.open(notes_path, "w+") { |f| f.puts create["object"].to_json } - create["to"].each { |r| send_signed create, r } + recipients.delete "https://www.w3.org/ns/activitystreams#Public" + recipients.each { |r| send_signed create, r } redirect to(params['redirect']) end @@ -92,6 +93,7 @@ post "/follow/*" do protected! mention = params['splat'][0] actor = actor(mention) + return 502 unless actor follow = { "@context" => "https://www.w3.org/ns/activitystreams", "id" => File.join(SOCIAL_URL, "following", mention + ".json"), "type" => "Follow", @@ -105,6 +107,7 @@ post "/unfollow/*" do protected! mention = params['splat'][0] actor = actor(mention) + return 502 unless actor following_path = File.join("public", "following", mention + ".json") if File.exists?(following_path) undo = { "@context" => "https://www.w3.org/ns/activitystreams", @@ -144,12 +147,12 @@ end helpers do def protected! - redirect("/login.html") unless session['client'] + halt 403 unless session['client'] end def items nr = 0 - files = Dir[File.join(@dir, '*.json')] + Dir['public/objects/*.json'] + files = Dir[File.join(@dir, '*.json')] + Dir['public/notes/*.json'] @items = files.sort.collect do |file| item = JSON.parse(File.read(file)) mention = mention(item['attributedTo']) @@ -199,7 +202,9 @@ helpers do def mention actor person = people.select{|p| p[1] == actor} if person.empty? - mention = "#{fetch(actor)["preferredUsername"]}@#{URI(actor).host}" + a = fetch(actor) + return nil unless a + mention = "#{a["preferredUsername"]}@#{URI(actor).host}" File.open('cache/people.tsv','a'){|f| f.puts "#{mention}\t#{actor}"} mention else @@ -212,8 +217,9 @@ helpers do 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| + a = fetch("https://#{server}/.well-known/webfinger?resource=acct:#{mention}", "application/jrd+json") + return nil unless a + actor = a["links"].select { |l| l["rel"] == "self" }[0]["href"] File.open('cache/people.tsv','a'){|f| f.puts "#{mention}\t#{actor}"} @@ -1,10 +1,13 @@ helpers do + def curl ext, url + response = `/run/current-system/sw/bin/curl -ifsSL #{ext} #{url}` + $?.success? ? response : nil + 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) + response = curl("-H 'Accept: #{accept}'", url) + response ? JSON.parse(response) : nil end # https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb @@ -12,20 +15,21 @@ helpers do keypair = OpenSSL::PKey::RSA.new(File.read('private.pem')) date = Time.now.utc.httpdate - sha256 = OpenSSL::Digest::SHA256.new body = object.to_json + sha256 = OpenSSL::Digest::SHA256.new digest = "SHA-256=" + sha256.base64digest(body) host = URI.parse(url).host inbox = fetch(url)["inbox"] + return false unless inbox request_uri = URI(inbox).request_uri signed_string = "(request-target): post #{request_uri}\nhost: #{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 + '"' - #p url - puts `/run/current-system/sw/bin/curl --fail-with-body -sSL -X POST -H 'Content-Type: application/activity+json' -H 'Host: #{host}' -H 'Date: #{date}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' -d '#{body}' #{inbox}` - #puts `/run/current-system/sw/bin/curl -iL -X POST -H 'Content-Type: application/activity+json' -H 'Host: #{host}' -H 'Date: #{date}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' -d '#{body}' #{inbox}` + curl "-X POST -H 'Content-Type: application/activity+json' -H 'Host: #{host}' -H 'Date: #{date}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' -d '#{body}'", inbox + $?.success? + end end @@ -79,36 +79,39 @@ helpers do # https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/signature_verification.rb def verify! - # 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']) + # verify digest + request.body.rewind # in case someone already read it + body = request.body.read + sha256 = OpenSSL::Digest::SHA256.new + digest = "SHA-256=" + sha256.base64digest(body) + halt 403 unless digest == request.env["HTTP_DIGEST"] + + signature_params = {} + request.env["HTTP_SIGNATURE"].split(',').each do |pair| + k, v = pair.split('=') + signature_params[k] = v.gsub('"', '') + end - 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") + key_id = signature_params['keyId'] + headers = signature_params['headers'] + signature = Base64.decode64(signature_params['signature']) + + actor = fetch key_id + halt 403 unless actor + 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 + halt 403 unless key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison) end def create object |