about summary refs log tree commit diff
path: root/app/services/keys/claim_service.rb
blob: 0451c3cb1dd7a46e95d330d5f4f11fac5c2d3973 (plain) (blame)
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
# frozen_string_literal: true

class Keys::ClaimService < BaseService
  HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze

  class Result < ActiveModelSerializers::Model
    attributes :account, :device_id, :key_id,
               :key, :signature

    def initialize(account, device_id, key_attributes = {})
      super(
        account:   account,
        device_id: device_id,
        key_id:    key_attributes[:key_id],
        key:       key_attributes[:key],
        signature: key_attributes[:signature],
      )
    end
  end

  def call(source_account, target_account_id, device_id)
    @source_account = source_account
    @target_account = Account.find(target_account_id)
    @device_id      = device_id

    if @target_account.local?
      claim_local_key!
    else
      claim_remote_key!
    end
  rescue ActiveRecord::RecordNotFound
    nil
  end

  private

  def claim_local_key!
    device = @target_account.devices.find_by(device_id: @device_id)
    key    = nil

    ApplicationRecord.transaction do
      key = device.one_time_keys.order(Arel.sql('random()')).first!
      key.destroy!
    end

    @result = Result.new(@target_account, @device_id, key)
  end

  def claim_remote_key!
    query_result = QueryService.new.call(@target_account)
    device       = query_result.find(@device_id)

    return unless device.present? && device.valid_claim_url?

    json = fetch_resource_with_post(device.claim_url)

    return unless json.present? && json['publicKeyBase64'].present?

    @result = Result.new(@target_account, @device_id, key_id: json['id'], key: json['publicKeyBase64'], signature: json.dig('signature', 'signatureValue'))
  rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e
    Rails.logger.debug { "Claiming one-time key for #{@target_account.acct}:#{@device_id} failed: #{e}" }
    nil
  end

  def fetch_resource_with_post(uri)
    build_post_request(uri).perform do |response|
      raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response)

      body_to_json(response.body_with_limit) if response.code == 200
    end
  end

  def build_post_request(uri)
    Request.new(:post, uri).tap do |request|
      request.on_behalf_of(@source_account)
      request.add_headers(HEADERS)
    end
  end
end