Fix followers synchronization mechanism not working when URI has empty path (#16510)

* Fix followers synchronization mechanism not working when URI has empty path

To my knowledge, there is no current implementation on the fediverse
that can use bare domains (e.g., actor is at https://example.org instead of
something like https://example.org/actor) that also plans to support the
followers synchronization mechanism. However, Mastodon's current implementation
would exclude such accounts from followers list.

Also adds tests and rename them to reflect the proper method names.

* Move url prefix regexp to its own constant
This commit is contained in:
Claire 2021-08-11 17:48:42 +02:00 committed by GitHub
parent 13b08610a0
commit 5efb1ff337
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 49 additions and 38 deletions

View file

@ -60,6 +60,7 @@ class Account < ApplicationRecord
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[a-z0-9]+)?)/i MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[a-z0-9]+)?)/i
URL_PREFIX_RE = /\Ahttp(s?):\/\/[^\/]+/
include AccountAssociations include AccountAssociations
include AccountAvatar include AccountAvatar
@ -379,7 +380,7 @@ class Account < ApplicationRecord
def synchronization_uri_prefix def synchronization_uri_prefix
return 'local' if local? return 'local' if local?
@synchronization_uri_prefix ||= uri[/http(s?):\/\/[^\/]+\//] @synchronization_uri_prefix ||= "#{uri[URL_PREFIX_RE]}/"
end end
class Field < ActiveModelSerializers::Model class Field < ActiveModelSerializers::Model

View file

@ -254,10 +254,13 @@ module AccountInteractions
.where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago) .where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
end end
def remote_followers_hash(url_prefix) def remote_followers_hash(url)
Rails.cache.fetch("followers_hash:#{id}:#{url_prefix}") do url_prefix = url[Account::URL_PREFIX_RE]
return if url_prefix.blank?
Rails.cache.fetch("followers_hash:#{id}:#{url_prefix}/") do
digest = "\x00" * 32 digest = "\x00" * 32
followers.where(Account.arel_table[:uri].matches(url_prefix + '%', false, true)).pluck_each(:uri) do |uri| followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(url_prefix)}/%", false, true)).or(followers.where(uri: url_prefix)).pluck_each(:uri) do |uri|
Xorcist.xor!(digest, Digest::SHA256.digest(uri)) Xorcist.xor!(digest, Digest::SHA256.digest(uri))
end end
digest.unpack('H*')[0] digest.unpack('H*')[0]

View file

@ -44,11 +44,7 @@ class ActivityPub::DeliveryWorker
end end
def synchronization_header def synchronization_header
"collectionId=\"#{account_followers_url(@source_account)}\", digest=\"#{@source_account.remote_followers_hash(inbox_url_prefix)}\", url=\"#{account_followers_synchronization_url(@source_account)}\"" "collectionId=\"#{account_followers_url(@source_account)}\", digest=\"#{@source_account.remote_followers_hash(@inbox_url)}\", url=\"#{account_followers_synchronization_url(@source_account)}\""
end
def inbox_url_prefix
@inbox_url[/http(s?):\/\/[^\/]+\//]
end end
def perform_request def perform_request

View file

@ -539,26 +539,31 @@ describe AccountInteractions do
end end
end end
describe '#followers_hash' do describe '#remote_followers_hash' do
let(:me) { Fabricate(:account, username: 'Me') } let(:me) { Fabricate(:account, username: 'Me') }
let(:remote_1) { Fabricate(:account, username: 'alice', domain: 'example.org', uri: 'https://example.org/users/alice') } let(:remote_1) { Fabricate(:account, username: 'alice', domain: 'example.org', uri: 'https://example.org/users/alice') }
let(:remote_2) { Fabricate(:account, username: 'bob', domain: 'example.org', uri: 'https://example.org/users/bob') } let(:remote_2) { Fabricate(:account, username: 'bob', domain: 'example.org', uri: 'https://example.org/users/bob') }
let(:remote_3) { Fabricate(:account, username: 'eve', domain: 'foo.org', uri: 'https://foo.org/users/eve') } let(:remote_3) { Fabricate(:account, username: 'instance-actor', domain: 'example.org', uri: 'https://example.org') }
let(:remote_4) { Fabricate(:account, username: 'eve', domain: 'foo.org', uri: 'https://foo.org/users/eve') }
before do before do
remote_1.follow!(me) remote_1.follow!(me)
remote_2.follow!(me) remote_2.follow!(me)
remote_3.follow!(me) remote_3.follow!(me)
remote_4.follow!(me)
me.follow!(remote_1) me.follow!(remote_1)
end end
context 'on a local user' do
it 'returns correct hash for remote domains' do it 'returns correct hash for remote domains' do
expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec' expect(me.remote_followers_hash('https://example.org/')).to eq '20aecbe774b3d61c25094370baf370012b9271c5b172ecedb05caff8d79ef0c7'
expect(me.remote_followers_hash('https://foo.org/')).to eq 'ccb9c18a67134cfff9d62c7f7e7eb88e6b803446c244b84265565f4eba29df0e' expect(me.remote_followers_hash('https://foo.org/')).to eq 'ccb9c18a67134cfff9d62c7f7e7eb88e6b803446c244b84265565f4eba29df0e'
expect(me.remote_followers_hash('https://foo.org.evil.com/')).to eq '0000000000000000000000000000000000000000000000000000000000000000'
expect(me.remote_followers_hash('https://foo')).to eq '0000000000000000000000000000000000000000000000000000000000000000'
end end
it 'invalidates cache as needed when removing or adding followers' do it 'invalidates cache as needed when removing or adding followers' do
expect(me.remote_followers_hash('https://example.org/')).to eq '20aecbe774b3d61c25094370baf370012b9271c5b172ecedb05caff8d79ef0c7'
remote_3.unfollow!(me)
expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec' expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
remote_1.unfollow!(me) remote_1.unfollow!(me)
expect(me.remote_followers_hash('https://example.org/')).to eq '241b00794ce9b46aa864f3220afadef128318da2659782985bac5ed5bd436bff' expect(me.remote_followers_hash('https://example.org/')).to eq '241b00794ce9b46aa864f3220afadef128318da2659782985bac5ed5bd436bff'
@ -567,8 +572,15 @@ describe AccountInteractions do
end end
end end
context 'on a remote user' do describe '#local_followers_hash' do
it 'returns correct hash for remote domains' do let(:me) { Fabricate(:account, username: 'Me') }
let(:remote_1) { Fabricate(:account, username: 'alice', domain: 'example.org', uri: 'https://example.org/users/alice') }
before do
me.follow!(remote_1)
end
it 'returns correct hash for local users' do
expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me)) expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
end end
@ -580,7 +592,6 @@ describe AccountInteractions do
expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me)) expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
end end
end end
end
describe 'muting an account' do describe 'muting an account' do
let(:me) { Fabricate(:account, username: 'Me') } let(:me) { Fabricate(:account, username: 'Me') }

View file

@ -11,7 +11,7 @@ describe ActivityPub::DeliveryWorker do
let(:payload) { 'test' } let(:payload) { 'test' }
before do before do
allow_any_instance_of(Account).to receive(:remote_followers_hash).with('https://example.com/').and_return('somehash') allow_any_instance_of(Account).to receive(:remote_followers_hash).with('https://example.com/api').and_return('somehash')
end end
describe 'perform' do describe 'perform' do