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 --- helpers.rb | 177 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 helpers.rb (limited to 'helpers.rb') diff --git a/helpers.rb b/helpers.rb new file mode 100644 index 0000000..b8f65de --- /dev/null +++ b/helpers.rb @@ -0,0 +1,177 @@ + +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 items + 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 unless @items.empty? + end + + def threads + items + @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 } + if object['inReplyTo'] + @dir = 'inbox' + items + if @items.select{|it| it[:id] == object['inReplyTo'] }.empty? + download object['inReplyTo'] + end + end + end + end + + def download object_url + create fetch(object_url) + end + + def people + File.read('cache/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('cache/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('cache/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 + host = URI.parse(url).host + inbox = fetch(url)["inbox"] + + sha256 = OpenSSL::Digest::SHA256.new + body = object.to_json + digest = "SHA-256=" + sha256.base64digest(body) + + signed_string = "(request-target): post #{inbox}\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 + '"' + + puts `/run/current-system/sw/bin/curl -i -X POST -H 'Content-Type: application/activity+json' -H 'Host: #{host}' -H 'Date: #{date}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' -d '#{body}' #{inbox}` + end + +end -- cgit v1.2.3