summaryrefslogtreecommitdiff
path: root/helpers.rb
diff options
context:
space:
mode:
authorpdp8 <pdp8@pdp8.info>2023-06-21 12:51:05 +0200
committerpdp8 <pdp8@pdp8.info>2023-06-21 12:51:05 +0200
commite1a5a8283ff69eb906d4c93b31d8cec1fae1b6c5 (patch)
tree57fe14ad7ae3e26c17c64133158917163d42db22 /helpers.rb
parentb837b19b1950c7bc14a38aa5ea917e91b6f081dd (diff)
server, client, helpers separated
Diffstat (limited to 'helpers.rb')
-rw-r--r--helpers.rb177
1 files changed, 177 insertions, 0 deletions
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