summaryrefslogtreecommitdiff
path: root/server.rb
blob: 49204a3d9ecaa1383d378de0acf20b25d5da77b7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# 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
  type = @activity['type'].downcase.to_sym
  p type
  halt 501 unless respond_to?(type)
  @object = @activity['object']
  @object = fetch(@object) if @object.is_a?(String) && @object.match(/^http/)
  halt 400 unless @object
  verify! unless type == :accept # pixelfed sends unsigned accept activities???
  send(type)
  halt 200
end

# public
get '/.well-known/webfinger' do
  halt 404 unless request['resource'] == "acct:#{MENTION}"
  send_file(WEBFINGER, type: 'application/jrd+json')
end

['/pdp8', '/following', '/followers', '/outbox', '/shared'].each do |path|
  get path do
    send_file(File.join(PUBLIC_DIR, path) + '.json', type: CONTENT_TYPE)
  end
end

helpers do
  def create
    return unless @object

    # return if File.readlines(VISITED).collect { |l| l.chomp }.include? @object['id']

    # File.open(VISITED, 'a+') { |f| f.puts @object['id'] }
    update_collection INBOX, @object
    return unless @object['inReplyTo']

    @object = fetch @object['inReplyTo']
    create if @object
  end

  def announce
    create
  end

  def follow
    update_collection FOLLOWERS, @activity['actor']
    outbox 'Accept', @activity, [@activity['actor']]
  end

  def accept
    halt 501 unless @object['type'] == 'Follow'
    update_collection FOLLOWING, @object['object']
  end

  def undo
    halt 501 unless @object['type'] == 'Follow'
    update_collection FOLLOWERS, @object['actor'], true
  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
end