diff options
Diffstat (limited to 'application.rb')
-rw-r--r-- | application.rb | 415 |
1 files changed, 0 insertions, 415 deletions
diff --git a/application.rb b/application.rb deleted file mode 100644 index c8a657c..0000000 --- a/application.rb +++ /dev/null @@ -1,415 +0,0 @@ -# TODO -# unwrap and save object from create -# boost -# archive -# threads -# federation -# client post media -# test with pleroma etc -require 'json' -require 'net/http' -require 'uri' -require 'base64' -require 'securerandom' -require 'fileutils' -require 'digest/sha2' -require 'nokogiri' - -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 = File.join(SOCIAL_URL, USER) - -class Application - def call(env) - code = 404 - type = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' - response = "Not found." - - case env['REQUEST_METHOD'] - - when 'POST' - input = env["rack.input"].read - case env["REQUEST_PATH"] - - when "/inbox" # receive from server - if verify(env) - begin - object = JSON.parse(input) - case object["type"] - when "Create" - File.open(File.join("inbox", SecureRandom.uuid + ".json"), "w+") { |f| f.puts input } - # File.open(File.join("inbox", input["published"] + ".json"), "w+") { |f| f.puts input["object"] } - when "Delete" - puts input - when "Follow" - File.open(File.join("followers", SecureRandom.uuid + ".json"), "w+") { |f| f.puts input } - accept = { "@context" => "https://www.w3.org/ns/activitystreams", - "id" => File.join(SOCIAL_URL + "#accepts", SecureRandom.uuid), - "type" => "Accept", - "actor" => ACTOR, - "object" => JSON.parse(input) } - send accept, [accept["object"]["actor"]] - when "Undo" - o = object["object"] - case o["type"] - when "Follow" - Dir["followers/*.json"].each do |follower| - if JSON.parse(File.read(follower))["actor"] == o["actor"] - FileUtils.rm follower - end - end - else - puts input - end - else - puts input - end - code = 200 - response = "OK" - rescue => e - puts input, e.to_s - response = "Request body contains invalid json." - end - else - code = 403 - response = "Key verification failed for POST to #{env["REQUEST_URI"]}." - end - - when %r{/delete} # receive from client - if auth(env) - FileUtils.rm env["REQUEST_URI"].sub("/delete/", "") - return [302, { "Location" => "/inbox" }, []] - end - - when "/outbox" # receive from client - if auth(env) - code, response = parse input - else - code = 403 - response = "You are not allowed to POST to #{env["REQUEST_URI"]}." - end - - when "/follow" # receive from client - if auth(env) - input.split.each do |mention| - actor = actor(mention) - follow = { "@context" => "https://www.w3.org/ns/activitystreams", - "id" => File.join(SOCIAL_URL, "following", SecureRandom.uuid + ".json"), - "type" => "Follow", - "actor" => ACTOR, - "object" => actor } - save follow - puts(send follow, [actor]) - code = 200 - response = "OK" - end - else - code = 403 - response = "You are not allowed to POST to #{env["REQUEST_URI"]}." - end - - when "/unfollow" # receive from client - if auth(env) - input.split.each do |mention| - actor = actor(mention) - Dir["following/*.json"].each do |f| - follow = JSON.parse(File.read(f)) - puts follow - if follow["object"] == actor - undo = { "@context" => "https://www.w3.org/ns/activitystreams", - "id" => File.join(SOCIAL_URL + "#undo", SecureRandom.uuid), - "type" => "Undo", - "actor" => ACTOR, - "object" => follow } - send undo, [actor] - FileUtils.rm f - end - end - end - end - end - - when 'GET' - - case env["REQUEST_URI"] # REQUEST_PATH does not contain queries - - when "/.well-known/webfinger?resource=acct:#{ACCOUNT}" - type = "application/jrd+json" - response = File.read("webfinger") - code = 200 - - when "/#{USER}" - # TODO serve html - response = File.read(USER) - code = 200 - - when "/inbox" - if auth(env) - case env["HTTP_ACCEPT"] - when /json/ - response = ordered_collection(env["REQUEST_PATH"]).to_json - else - type = "text/html" - response = html env["REQUEST_PATH"] - end - code = 200 - else - code = 403 - response = "You are not allowed to GET #{env["REQUEST_URI"]}." - end - - when %r{/[outbox|following|followers|likes|shares]} - response = ordered_collection(env["REQUEST_PATH"]).to_json - code = 200 - end - - end - [code, { "Content-Type" => type }, [response]] - end - - def html path - html = "<!DOCTYPE html>\n<html lang='en'>\n\t<body>" - Dir[File.join(path.sub(/^\//, ''), "*")].sort_by { |f| File.stat(f).ctime }.each do |file| - item = JSON.parse(File.read(file)) - html << "\n\t\t<b>#{mention item["actor"]}</b> <i>#{item["object"]["published"].sub("T", - " ")}</i><p>#{item["object"]["content"]}" - if item["object"]["attachment"] - item["object"]["attachment"].each do |att| - case att["mediaType"] - when /audio/ - html << "\n<br><audio controls=''><source src='#{att["url"]}' type='#{att["mediaType"]}'></audio>" - when /image/ - html << "\n<br><a href='#{att["url"]}'><img src='#{att["url"]}'></a>" - when /video/ - html << "\n<br><video controls=''><source src='#{att["url"]}' type='#{att["mediaType"]}'></video>" - else - html << att + "<br>" - html << "\n<a href='#{att["url"]}'>#{att["url"]}</a>" - end - end - end - html << "<p> - <form action='#{File.join "delete", file}' method='post'> - <button>Delete</button> - </form> - <form action='#{File.join "boost", file}' method='post'> - <button>Boost</button> - </form> - <form action='#{File.join "archive", file}' method='post'> - <button>Archive</button> - </form> - <form action='#{File.join "reply", file}' method='post'> - <button>Reply</button> - </form> - <hr>" - end - html << "\n\t</body>\n</html>" - end -=begin -=end - -=begin - def html o - html = "<!DOCTYPE html> -<html lang='en'> - <body> - <b>#{mention o["actor"]}</b> <i>#{o["object"]["published"]}</i> - <p>#{o["object"]["content"]} - " - if o["object"]["attachment"] - o["object"]["attachment"].each do |att| - case att["mediaType"] - when /audio/ - html<< "\n<br><audio controls=''><source src='#{att["url"]}' type='#{att["mediaType"]}'></audio>" - when /image/ - html << "\n<br><a href='#{att["url"]}'><img src='#{att["url"]}'></a>" - when /video/ - html<< "\n<br><video controls=''><source src='#{att["url"]}' type='#{att["mediaType"]}'></video>" - else - html<< att + "<br>" - html << "\n<a href='#{att["url"]}'>#{att["url"]}</a>" - end - end - end - end - html << "\n\t</body>\n</html>" - html - end -=end - - def parse input - date = Time.now.strftime("%Y-%m-%dT%H:%M:%S") - # TODO media attachments, hashtags - note = { - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => File.join(SOCIAL_URL, "note", SecureRandom.uuid + ".json"), - "type" => "Note", - "attributedTo" => ACTOR, - "published" => date, - "content" => "", - "to" => ["https://www.w3.org/ns/activitystreams#Public"] - } - create = { - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => File.join(SOCIAL_URL, "create", SecureRandom.uuid + ".json"), - "type" => "Create", - "actor" => ACTOR, - "object" => note, - "published" => date, - "to" => ["https://www.w3.org/ns/activitystreams#Public"] - } - recipients = [] - if /^@/.match input - mentions, input = input.split("\n", 2) - mentions.split(/, */).each do |m| - recipients << actor(m.chomp) - end - end - note["content"] = input.lines.select { |l| !l.empty? }.join("<br>") - recipients += Dir[File.join("followers", "*.json")].collect { |f| JSON.parse(File.read(f))["actor"] } - recipients.delete ACTOR - recipients.uniq! - note["to"] += recipients - create["to"] += recipients - - save create - save note - FileUtils.ln_s File.join('..', path(create)), "outbox" - - responses = send create, recipients - if responses.collect { |r| r.code.to_i }.uniq.max < 400 - code = 200 - response = "OK" - else - code = 502 - response = responses.select { |r| r.code.to_i >= 400 }.collect { |r| r.body }.uniq - end - [code, response] - end - - def actor mention - mention = mention.sub(/^@/, '').chomp - user, server = mention.split("@") - get("https://#{server}/.well-known/webfinger?resource=acct:#{mention}", - "application/jrd+json")["links"].select { |l| - l["rel"] == "self" - }[0]["href"] - end - - def mention actor - "#{get(actor)["preferredUsername"]}@#{URI(actor).host}" - end - - def send object, urls - # https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb - keypair = OpenSSL::PKey::RSA.new(File.read('private.pem')) - responses = [] - urls.each do |url| - 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 #{inbox uri}\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 + '"' - - uri = URI.parse(get(url)["inbox"]) - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true - header = { - 'Content-Type' => 'application/activity+json', - 'Host' => uri.host, - 'Date' => date, - 'Digest' => digest, - 'Signature' => signed_header, - } - request = Net::HTTP::Post.new(uri.request_uri, header) - request.body = body - - responses << http.request(request) - end - # puts responses - responses - end - - def ordered_collection dir - collection = dir.sub(/^\//, "") - posts = Dir[File.join(collection, "*.json")].collect { |f| JSON.parse(File.read f) }.sort_by { |o| o["published"] } - { - "@context" => "https://www.w3.org/ns/activitystreams", - "summary" => "#{USER} #{collection}", - "type" => "OrderedCollection", - "totalItems" => posts.size, - "orderedItems" => posts, - } - end - - def inbox uri - URI(get(uri)["inbox"]).request_uri - end - - def get url, accept = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' - uri = URI(url) - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true - header = { 'Accept' => accept } - request = Net::HTTP::Get.new(uri.request_uri, header) - response = http.request(request) - JSON.parse(response.body) - end - - def path object - object["id"].sub(SOCIAL_URL, '').sub('/', '') - end - - def save object - path = path object - FileUtils.mkdir_p File.dirname(path) - File.open(path, "w+") { |f| f.puts object.to_json } - end - - def verify env - # https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/signature_verification.rb - # TODO verify digest - signature_params = {} - 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 = get 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}: #{env["CONTENT_TYPE"]}" - else - "#{signed_params_name}: #{env["HTTP_" + signed_params_name.upcase]}" - end - end.join("\n") - - key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison) - end - - def auth env - auth = Rack::Auth::Basic::Request.new(env) - usr = File.read(".usr").chomp - pwd = File.read(".pwd").chomp - auth.provided? && auth.basic? && auth.credentials && auth.credentials == [usr, pwd] - true - end -end |