catstodon/app/services/resolve_account_service.rb
Claire 9a19227f17
Fix some RedisLocks auto-releasing too fast (#16276)
* 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
2021-05-19 23:52:08 +02:00

151 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 any webfinger query or refreshing 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)
return false if @options[:skip_webfinger]
@account.nil? || @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