# frozen_string_literal: true # server-server post '/inbox' do request.body.rewind # in case someone already read it @body = request.body.read halt 400 if @body.empty? begin @activity = JSON.parse @body rescue StandardError p @body halt 400 end halt 501 if @activity['actor'] and @activity['type'] == 'Delete' # deleted actors return 403 => verification error # verify! # pixelfed sends unsigned activities??? type = @activity['type'].downcase.to_sym save_activity(@activity, INBOX) unless %i[create announce].include? type send(type) if %i[create announce follow accept undo].include? type halt 200 end # public get '/' do redirect 'https://pdp8.info' end get '/outbox' do files = Dir[File.join('outbox', 'create', '*.json')] + Dir[File.join('outbox', 'announce', '*.json')] activities = files.collect { |f| JSON.parse(File.read(f)) } ids = activities.sort_by { |a| a['published'] }.collect { |a| a['id'] } { '@context' => 'https://www.w3.org/ns/activitystreams', 'id' => 'https://social.pdp8.info/outbox', 'type' => 'OrderedCollection', 'totalItems' => ids.size, 'orderedItems' => ids }.to_json end get '/pdp8', provides: 'html' do redirect 'https://pdp8.info' end get '/pdp8' do send_file(File.join(PUBLIC_DIR, 'pdp8.json'), type: CONTENT_TYPE) end get '/.well-known/webfinger' do halt 404 unless request['resource'] == "acct:#{MENTION}" send_file(WEBFINGER, type: 'application/jrd+json') end ['/following', '/followers'].each do |path| get path do send_file(File.join(PUBLIC_DIR, path) + '.json', type: CONTENT_TYPE) end end get '/tags/:tag' do |tag| send_file(File.join(PUBLIC_DIR, 'tags', tag) + '.json', type: CONTENT_TYPE) end helpers do def create @object ||= @activity['object'] @object = save_object @object, INBOX return unless @object and @object['inReplyTo'] @object = @object['inReplyTo'] create end def announce create end def follow update_collection FOLLOWERS, @activity['actor'] outbox 'Accept', @activity, [@activity['actor']] end def accept halt 501 unless @activity['object']['type'] == 'Follow' update_collection FOLLOWING, @activity['object']['object'] end def undo case @activity['object']['type'] when 'Follow' update_collection FOLLOWERS, @activity['object']['actor'], true when 'Create', 'Announce' file = find_file @activity['object']['id'] FileUtils.rm(file) if File.exist? file else halt 501 end end def update FileUtils.rm(find_file(@activity['object']['id'])) create end # https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/signature_verification.rb def verify! # digest sha256 = OpenSSL::Digest.new('SHA256') digest = "SHA-256=#{sha256.base64digest(@body)}" halt 403 unless digest == request.env['HTTP_DIGEST'] # signature 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 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 403 unless key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison) end def actor_inbox(url) actor = fetch url return unless actor if actor['endpoints'] and actor['endpoints']['sharedInbox'] actor['endpoints']['sharedInbox'] elsif actor['inbox'] actor['inbox'] end end def outbox(type, object, to) # send ## https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb to = [to] if to.is_a?(String) inboxes = [] to.uniq.each do |url| next if [ACTOR, 'https://www.w3.org/ns/activitystreams#Public'].include? url if url == FOLLOWERS_URL JSON.parse(File.read(FOLLOWERS))['orderedItems'].each do |follower| inboxes << actor_inbox(follower) end next end inboxes << actor_inbox(url) end # add date and id, save activity = save_activity({ '@context' => 'https://www.w3.org/ns/activitystreams', 'type' => type, 'actor' => ACTOR, 'object' => object, 'to' => to }, OUTBOX) body = activity.to_json sha256 = OpenSSL::Digest.new('SHA256') digest = "SHA-256=#{sha256.base64digest(body)}" keypair = OpenSSL::PKey::RSA.new(File.read('private.pem')) inboxes.compact.uniq.each do |inbox| uri = URI(inbox) httpdate = Time.now.utc.httpdate string = "(request-target): post #{uri.request_uri}\nhost: #{uri.host}\ndate: #{httpdate}\ndigest: #{digest}\ncontent-type: #{CONTENT_TYPE}" signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), string)) signed_header = "keyId=\"#{ACTOR}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"#{signature}\"" # Net::HTTP fails with OpenSSL error curl( "-X POST -H 'Host: #{uri.host}' -H 'Date: #{httpdate}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' --data-raw '#{body}'", inbox ) end activity end end