end
def object_uri
- @object_uri ||= value_or_id(@object)
+ @object_uri ||= begin
+ str = value_or_id(@object)
+
+ if str.start_with?('bear:')
+ Addressable::URI.parse(str).query_values['u']
+ else
+ str
+ end
+ end
end
def unsupported_object_type?
def dereference_object!
return unless @object.is_a?(String)
- return if invalid_origin?(@object)
- object = fetch_resource(@object, true, signed_fetch_account)
- return unless object.present? && object.is_a?(Hash) && supported_context?(object)
+ dereferencer = ActivityPub::Dereferencer.new(@object, permitted_origin: @account.uri, signature_account: signed_fetch_account)
- @object = object
+ @object = dereferencer.object unless dereferencer.object.nil?
end
def signed_fetch_account
private
def create_encrypted_message
- return reject_payload! if invalid_origin?(@object['id']) || @options[:delivered_to_account_id].blank?
+ return reject_payload! if invalid_origin?(object_uri) || @options[:delivered_to_account_id].blank?
target_account = Account.find(@options[:delivered_to_account_id])
target_device = target_account.devices.find_by(device_id: @object.dig('to', 'deviceId'))
end
def create_status
- return reject_payload! if unsupported_object_type? || invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity?
+ return reject_payload! if unsupported_object_type? || invalid_origin?(object_uri) || tombstone_exists? || !related_to_local_activity?
RedisLock.acquire(lock_options) do |lock|
if lock.acquired?
fetch_replies(@status)
check_for_spam
distribute(@status)
- forward_for_reply if @status.distributable?
+ forward_for_reply
end
def find_existing_status
def process_status_params
@params = begin
{
- uri: @object['id'],
- url: object_url || @object['id'],
+ uri: object_uri,
+ url: object_url || object_uri,
account: @account,
text: text_from_content || '',
language: detected_language,
RedisLock.acquire(poll_lock_options) do |lock|
if lock.acquired?
already_voted = poll.votes.where(account: @account).exists?
- poll.votes.create!(account: @account, choice: poll.options.index(@object['name']), uri: @object['id'])
+ poll.votes.create!(account: @account, choice: poll.options.index(@object['name']), uri: object_uri)
else
raise Mastodon::RaceConditionError
end
end
def text_from_content
- return Formatter.instance.linkify([[text_from_name, text_from_summary.presence].compact.join("\n\n"), object_url || @object['id']].join(' ')) if converted_object_type?
+ return Formatter.instance.linkify([[text_from_name, text_from_summary.presence].compact.join("\n\n"), object_url || object_uri].join(' ')) if converted_object_type?
if @object['content'].present?
@object['content']
Account.local.where(username: local_usernames).exists?
end
+ def tombstone_exists?
+ Tombstone.exists?(uri: object_uri)
+ end
+
def check_for_spam
SpamCheck.perform(@status)
end
def forward_for_reply
- return unless @json['signature'].present? && reply_to_local?
+ return unless @status.distributable? && @json['signature'].present? && reply_to_local?
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
end
end
def lock_options
- { redis: Redis.current, key: "create:#{@object['id']}" }
+ { redis: Redis.current, key: "create:#{object_uri}" }
end
def poll_lock_options
--- /dev/null
+# frozen_string_literal: true
+
+class ActivityPub::Dereferencer
+ include JsonLdHelper
+
+ def initialize(uri, permitted_origin: nil, signature_account: nil)
+ @uri = uri
+ @permitted_origin = permitted_origin
+ @signature_account = signature_account
+ end
+
+ def object
+ @object ||= fetch_object!
+ end
+
+ private
+
+ def bear_cap?
+ @uri.start_with?('bear:')
+ end
+
+ def fetch_object!
+ if bear_cap?
+ fetch_with_token!
+ else
+ fetch_with_signature!
+ end
+ end
+
+ def fetch_with_token!
+ perform_request(bear_cap['u'], headers: { 'Authorization' => "Bearer #{bear_cap['t']}" })
+ end
+
+ def fetch_with_signature!
+ perform_request(@uri)
+ end
+
+ def bear_cap
+ @bear_cap ||= Addressable::URI.parse(@uri).query_values
+ end
+
+ def perform_request(uri, headers: nil)
+ return if invalid_origin?(uri)
+
+ req = Request.new(:get, uri)
+
+ req.add_headers('Accept' => 'application/activity+json, application/ld+json')
+ req.add_headers(headers) if headers
+ req.on_behalf_of(@signature_account) if @signature_account
+
+ req.perform do |res|
+ if res.code == 200
+ json = body_to_json(res.body_with_limit)
+ json if json.present? && json['id'] == uri
+ else
+ raise Mastodon::UnexpectedResponseError, res unless response_successful?(res) || response_error_unsalvageable?(res)
+ end
+ end
+ end
+
+ def invalid_origin?(uri)
+ return true if unsupported_uri_scheme?(uri)
+
+ needle = Addressable::URI.parse(uri).host
+ haystack = Addressable::URI.parse(@permitted_origin).host
+
+ !haystack.casecmp(needle).zero?
+ end
+end
--- /dev/null
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Dereferencer do
+ describe '#object' do
+ let(:object) { { '@context': 'https://www.w3.org/ns/activitystreams', id: 'https://example.com/foo', type: 'Note', content: 'Hoge' } }
+ let(:permitted_origin) { 'https://example.com' }
+ let(:signature_account) { nil }
+ let(:uri) { nil }
+
+ subject { described_class.new(uri, permitted_origin: permitted_origin, signature_account: signature_account).object }
+
+ before do
+ stub_request(:get, 'https://example.com/foo').to_return(body: Oj.dump(object), headers: { 'Content-Type' => 'application/activity+json' })
+ end
+
+ context 'with a URI' do
+ let(:uri) { 'https://example.com/foo' }
+
+ it 'returns object' do
+ expect(subject.with_indifferent_access).to eq object.with_indifferent_access
+ end
+
+ context 'with signature account' do
+ let(:signature_account) { Fabricate(:account) }
+
+ it 'makes signed request' do
+ subject
+ expect(a_request(:get, 'https://example.com/foo').with { |req| req.headers['Signature'].present? }).to have_been_made
+ end
+ end
+
+ context 'with different origin' do
+ let(:uri) { 'https://other-example.com/foo' }
+
+ it 'does not make request' do
+ subject
+ expect(a_request(:get, 'https://other-example.com/foo')).to_not have_been_made
+ end
+ end
+ end
+
+ context 'with a bearcap' do
+ let(:uri) { 'bear:?t=hoge&u=https://example.com/foo' }
+
+ it 'makes request with Authorization header' do
+ subject
+ expect(a_request(:get, 'https://example.com/foo').with(headers: { 'Authorization' => 'Bearer hoge' })).to have_been_made
+ end
+
+ it 'returns object' do
+ expect(subject.with_indifferent_access).to eq object.with_indifferent_access
+ end
+
+ context 'with signature account' do
+ let(:signature_account) { Fabricate(:account) }
+
+ it 'makes signed request' do
+ subject
+ expect(a_request(:get, 'https://example.com/foo').with { |req| req.headers['Signature'].present? && req.headers['Authorization'] == 'Bearer hoge' }).to have_been_made
+ end
+ end
+
+ context 'with different origin' do
+ let(:uri) { 'bear:?t=hoge&u=https://other-example.com/foo' }
+
+ it 'does not make request' do
+ subject
+ expect(a_request(:get, 'https://other-example.com/foo')).to_not have_been_made
+ end
+ end
+ end
+ end
+end