4d41c91335
* Fix Delete and Create-related locks expiring too fast Fixes #16238 By default, RedisLock expires after 10 seconds, which may not be enough to process statuses, especially when those have attached media files. This commit extends those 10 seconds to 15 minutes, which should be plenty enough to handle any status, while being short enough to not waste many sidekiq job retries in the exceedingly rare case in which a sidekiq process would crash when processing a `Create` or `Delete`. * Fix other RedisLock autorelease durations Fixes #15645 - things that only perform a few simple database queries (e.g. finding and saving a record) have been left unchanged, so they'll still use the default 10s duration - things that perform significantly more complex database queries have been changed to a 5 minutes timeout - things that perform multiple HTTP queries have been changed to a 15 minutes timeout
150 lines
4.4 KiB
Ruby
150 lines
4.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class ResolveAccountService < BaseService
|
|
include JsonLdHelper
|
|
include DomainControlHelper
|
|
include WebfingerHelper
|
|
|
|
# Find or create an account record for a remote user. When creating,
|
|
# look up the user's webfinger and fetch ActivityPub data
|
|
# @param [String, Account] uri URI in the username@domain format or account record
|
|
# @param [Hash] options
|
|
# @option options [Boolean] :redirected Do not follow further Webfinger redirects
|
|
# @option options [Boolean] :skip_webfinger Do not attempt to refresh account data
|
|
# @return [Account]
|
|
def call(uri, options = {})
|
|
return if uri.blank?
|
|
|
|
process_options!(uri, options)
|
|
|
|
# First of all we want to check if we've got the account
|
|
# record with the URI already, and if so, we can exit early
|
|
|
|
return if domain_not_allowed?(@domain)
|
|
|
|
@account ||= Account.find_remote(@username, @domain)
|
|
|
|
return @account if @account&.local? || @domain.nil? || !webfinger_update_due?
|
|
|
|
# At this point we are in need of a Webfinger query, which may
|
|
# yield us a different username/domain through a redirect
|
|
process_webfinger!(@uri)
|
|
@domain = nil if TagManager.instance.local_domain?(@domain)
|
|
|
|
# Because the username/domain pair may be different than what
|
|
# we already checked, we need to check if we've already got
|
|
# the record with that URI, again
|
|
|
|
return if domain_not_allowed?(@domain)
|
|
|
|
@account ||= Account.find_remote(@username, @domain)
|
|
|
|
if gone_from_origin? && not_yet_deleted?
|
|
queue_deletion!
|
|
return
|
|
end
|
|
|
|
return @account if @account&.local? || gone_from_origin? || !webfinger_update_due?
|
|
|
|
# Now it is certain, it is definitely a remote account, and it
|
|
# either needs to be created, or updated from fresh data
|
|
|
|
fetch_account!
|
|
rescue Webfinger::Error, Oj::ParseError => e
|
|
Rails.logger.debug "Webfinger query for #{@uri} failed: #{e}"
|
|
nil
|
|
end
|
|
|
|
private
|
|
|
|
def process_options!(uri, options)
|
|
@options = options
|
|
|
|
if uri.is_a?(Account)
|
|
@account = uri
|
|
@username = @account.username
|
|
@domain = @account.domain
|
|
else
|
|
@username, @domain = uri.split('@')
|
|
end
|
|
|
|
@domain = begin
|
|
if TagManager.instance.local_domain?(@domain)
|
|
nil
|
|
else
|
|
TagManager.instance.normalize_domain(@domain)
|
|
end
|
|
end
|
|
|
|
@uri = [@username, @domain].compact.join('@')
|
|
end
|
|
|
|
def process_webfinger!(uri)
|
|
@webfinger = webfinger!("acct:#{uri}")
|
|
confirmed_username, confirmed_domain = split_acct(@webfinger.subject)
|
|
|
|
if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
|
|
@username = confirmed_username
|
|
@domain = confirmed_domain
|
|
return
|
|
end
|
|
|
|
# Account doesn't match, so it may have been redirected
|
|
@webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
|
|
@username, @domain = split_acct(@webfinger.subject)
|
|
|
|
unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
|
|
raise Webfinger::RedirectError, "The URI #{uri} tries to hijack #{@username}@#{@domain}"
|
|
end
|
|
rescue Webfinger::GoneError
|
|
@gone = true
|
|
end
|
|
|
|
def split_acct(acct)
|
|
acct.gsub(/\Aacct:/, '').split('@')
|
|
end
|
|
|
|
def fetch_account!
|
|
return unless activitypub_ready?
|
|
|
|
RedisLock.acquire(lock_options) do |lock|
|
|
if lock.acquired?
|
|
@account = ActivityPub::FetchRemoteAccountService.new.call(actor_url)
|
|
else
|
|
raise Mastodon::RaceConditionError
|
|
end
|
|
end
|
|
|
|
@account
|
|
end
|
|
|
|
def webfinger_update_due?
|
|
return false if @options[:check_delivery_availability] && !DeliveryFailureTracker.available?(@domain)
|
|
|
|
@account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?)
|
|
end
|
|
|
|
def activitypub_ready?
|
|
['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self', 'type'))
|
|
end
|
|
|
|
def actor_url
|
|
@actor_url ||= @webfinger.link('self', 'href')
|
|
end
|
|
|
|
def gone_from_origin?
|
|
@gone
|
|
end
|
|
|
|
def not_yet_deleted?
|
|
@account.present? && !@account.local?
|
|
end
|
|
|
|
def queue_deletion!
|
|
AccountDeletionWorker.perform_async(@account.id, reserve_username: false, skip_activitypub: true)
|
|
end
|
|
|
|
def lock_options
|
|
{ redis: Redis.current, key: "resolve:#{@username}@#{@domain}", autorelease: 15.minutes.seconds }
|
|
end
|
|
end
|