From 720214feb01bbd8d28359810242c4f41a2dfc82e Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 24 Aug 2020 14:11:47 +0200 Subject: Add support for inlined objects in activity audience (#14514) * Add support for inlined objects in activity audience * Add tests --- app/lib/activitypub/activity.rb | 2 +- app/lib/activitypub/activity/announce.rb | 14 +++++++++++--- app/lib/activitypub/activity/create.rb | 16 ++++++++-------- 3 files changed, 20 insertions(+), 12 deletions(-) (limited to 'app/lib/activitypub') diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index ab946470b..f0ef4d553 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -172,7 +172,7 @@ class ActivityPub::Activity end def first_mentioned_local_account - audience = (as_array(@json['to']) + as_array(@json['cc'])).uniq + audience = (as_array(@json['to']) + as_array(@json['cc'])).map { |x| value_or_id(x) }.uniq local_usernames = audience.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) } .map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) } diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb index 9e108985a..349e8f77e 100644 --- a/app/lib/activitypub/activity/announce.rb +++ b/app/lib/activitypub/activity/announce.rb @@ -34,12 +34,20 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity private + def audience_to + as_array(@json['to']).map { |x| value_or_id(x) } + end + + def audience_cc + as_array(@json['cc']).map { |x| value_or_id(x) } + end + def visibility_from_audience - if equals_or_includes?(@json['to'], ActivityPub::TagManager::COLLECTIONS[:public]) + if audience_to.include?(ActivityPub::TagManager::COLLECTIONS[:public]) :public - elsif equals_or_includes?(@json['cc'], ActivityPub::TagManager::COLLECTIONS[:public]) + elsif audience_cc.include?(ActivityPub::TagManager::COLLECTIONS[:public]) :unlisted - elsif equals_or_includes?(@json['to'], @account.followers_url) + elsif audience_to.include?(@account.followers_url) :private else :direct diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 08dd98e94..a60b79d15 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -65,11 +65,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def audience_to - @object['to'] || @json['to'] + as_array(@object['to'] || @json['to']).map { |x| value_or_id(x) } end def audience_cc - @object['cc'] || @json['cc'] + as_array(@object['cc'] || @json['cc']).map { |x| value_or_id(x) } end def process_status @@ -122,7 +122,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def process_audience - (as_array(audience_to) + as_array(audience_cc)).uniq.each do |audience| + (audience_to + audience_cc).uniq.each do |audience| next if audience == ActivityPub::TagManager::COLLECTIONS[:public] # Unlike with tags, there is no point in resolving accounts we don't already @@ -352,11 +352,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def visibility_from_audience - if equals_or_includes?(audience_to, ActivityPub::TagManager::COLLECTIONS[:public]) + if audience_to.include?(ActivityPub::TagManager::COLLECTIONS[:public]) :public - elsif equals_or_includes?(audience_cc, ActivityPub::TagManager::COLLECTIONS[:public]) + elsif audience_cc.include?(ActivityPub::TagManager::COLLECTIONS[:public]) :unlisted - elsif equals_or_includes?(audience_to, @account.followers_url) + elsif audience_to.include?(@account.followers_url) :private else :direct @@ -365,7 +365,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def audience_includes?(account) uri = ActivityPub::TagManager.instance.uri_for(account) - equals_or_includes?(audience_to, uri) || equals_or_includes?(audience_cc, uri) + audience_to.include?(uri) || audience_cc.include?(uri) end def replied_to_status @@ -477,7 +477,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def addresses_local_accounts? return true if @options[:delivered_to_account_id] - local_usernames = (as_array(audience_to) + as_array(audience_cc)).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) } + local_usernames = (audience_to + audience_cc).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) } return false if local_usernames.empty? -- cgit From 92319d73409a02689e05ae491a15ce81003a7b7e Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 24 Aug 2020 16:56:21 +0200 Subject: Fix dereferencing remote statuses not using the correct account (#14656) Follow-up to #14359 In the case of limited toots, the receiver may not be explicitly part of the audience. If a specific user's inbox URI was specified, it makes sense to dereference the toot from the corresponding user, instead of trying to find someone in the explicit audience. --- app/lib/activitypub/activity.rb | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app/lib/activitypub') diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index f0ef4d553..a379a7ef4 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -168,6 +168,8 @@ class ActivityPub::Activity end def signed_fetch_account + return Account.find(@options[:delivered_to_account_id]) if @options[:delivered_to_account_id].present? + first_mentioned_local_account || first_local_follower end -- cgit From 52157fdcba0837c782edbfd240be07cabc551de9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 30 Aug 2020 12:34:20 +0200 Subject: Add support for dereferencing objects through bearcaps (#14683) --- app/lib/activitypub/activity.rb | 16 ++++--- app/lib/activitypub/activity/create.rb | 22 ++++++---- app/lib/activitypub/dereferencer.rb | 69 +++++++++++++++++++++++++++++ spec/lib/activitypub/dereferencer_spec.rb | 73 +++++++++++++++++++++++++++++++ 4 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 app/lib/activitypub/dereferencer.rb create mode 100644 spec/lib/activitypub/dereferencer_spec.rb (limited to 'app/lib/activitypub') diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index a379a7ef4..94aee7939 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -71,7 +71,15 @@ class ActivityPub::Activity 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? @@ -159,12 +167,10 @@ class ActivityPub::Activity 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 diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index a60b79d15..f275feefc 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -15,7 +15,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity 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')) @@ -43,7 +43,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity 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? @@ -90,7 +90,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity fetch_replies(@status) check_for_spam distribute(@status) - forward_for_reply if @status.distributable? + forward_for_reply end def find_existing_status @@ -102,8 +102,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity 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, @@ -313,7 +313,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity 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 @@ -385,7 +385,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity 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'] @@ -484,12 +484,16 @@ class ActivityPub::Activity::Create < ActivityPub::Activity 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 @@ -507,7 +511,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def lock_options - { redis: Redis.current, key: "create:#{@object['id']}" } + { redis: Redis.current, key: "create:#{object_uri}" } end def poll_lock_options diff --git a/app/lib/activitypub/dereferencer.rb b/app/lib/activitypub/dereferencer.rb new file mode 100644 index 000000000..bea69608f --- /dev/null +++ b/app/lib/activitypub/dereferencer.rb @@ -0,0 +1,69 @@ +# 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 diff --git a/spec/lib/activitypub/dereferencer_spec.rb b/spec/lib/activitypub/dereferencer_spec.rb new file mode 100644 index 000000000..ce30513d7 --- /dev/null +++ b/spec/lib/activitypub/dereferencer_spec.rb @@ -0,0 +1,73 @@ +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 -- cgit