Improve error reporting and logging when processing remote accounts (#15605)

* Add a more descriptive PrivateNetworkAddressError exception class

* Remove unnecessary exception class to rescue clause

* Remove unnecessary include to JsonLdHelper

* Give more neutral error message when too many webfinger redirects

* Remove unnecessary guard condition

* Rework how “ActivityPub::FetchRemoteAccountService” handles errors

Add “suppress_errors” keyword argument to avoid raising errors in
ActivityPub::FetchRemoteAccountService#call (default/previous behavior).

* Rework how “ActivityPub::FetchRemoteKeyService” handles errors

Add “suppress_errors” keyword argument to avoid raising errors in
ActivityPub::FetchRemoteKeyService#call (default/previous behavior).

* Fix Webfinger::RedirectError not being a subclass of Webfinger::Error

* Add suppress_errors option to ResolveAccountService

Defaults to true (to preserve previous behavior). If set to false,
errors will be raised instead of caught, allowing the caller to be
informed of what went wrong.

* Return more precise error when failing to fetch account signing AP payloads

* Add tests

* Fixes

* Refactor error handling a bit

* Fix various issues

* Add specific error when provided Digest is not 256 bits of base64-encoded data

* Please CodeClimate

* Improve webfinger error reporting
This commit is contained in:
Claire 2022-09-20 23:30:26 +02:00 committed by GitHub
parent 14c7c9e40e
commit 1145dbd327
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 158 additions and 47 deletions

View file

@ -5,10 +5,12 @@ class ActivityPub::FetchRemoteAccountService < BaseService
include DomainControlHelper
include WebfingerHelper
class Error < StandardError; end
SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
# Does a WebFinger roundtrip on each call, unless `only_key` is true
def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false)
def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true)
return if domain_not_allowed?(uri)
return ActivityPub::TagManager.instance.uri_to_resource(uri, Account) if ActivityPub::TagManager.instance.local_uri?(uri)
@ -18,38 +20,50 @@ class ActivityPub::FetchRemoteAccountService < BaseService
else
body_to_json(prefetched_body, compare_id: id ? uri : nil)
end
rescue Oj::ParseError
raise Error, "Error parsing JSON-LD document #{uri}"
end
return if !supported_context? || !expected_type? || (break_on_redirect && @json['movedTo'].present?)
raise Error, "Error fetching actor JSON at #{uri}" if @json.nil?
raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?
raise Error, "Unexpected object type for actor #{uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_type?
raise Error, "Actor #{uri} has moved to #{@json['movedTo']}" if break_on_redirect && @json['movedTo'].present?
@uri = @json['id']
@username = @json['preferredUsername']
@domain = Addressable::URI.parse(@uri).normalized_host
return unless only_key || verified_webfinger?
check_webfinger! unless only_key
ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key, verified_webfinger: !only_key)
rescue Oj::ParseError
nil
rescue Error => e
Rails.logger.debug "Fetching account #{uri} failed: #{e.message}"
raise unless suppress_errors
end
private
def verified_webfinger?
def check_webfinger!
webfinger = webfinger!("acct:#{@username}@#{@domain}")
confirmed_username, confirmed_domain = split_acct(webfinger.subject)
return webfinger.link('self', 'href') == @uri if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
return
end
webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
@username, @domain = split_acct(webfinger.subject)
return false unless @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
return false if webfinger.link('self', 'href') != @uri
unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
raise Webfinger::RedirectError, "Too many webfinger redirects for URI #{uri} (stopped at #{@username}@#{@domain})"
end
true
rescue Webfinger::Error
false
raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
rescue Webfinger::RedirectError => e
raise Error, e.message
rescue Webfinger::Error => e
raise Error, "Webfinger error when resolving #{@username}@#{@domain}: #{e.message}"
end
def split_acct(acct)

View file

@ -3,9 +3,11 @@
class ActivityPub::FetchRemoteKeyService < BaseService
include JsonLdHelper
class Error < StandardError; end
# Returns account that owns the key
def call(uri, id: true, prefetched_body: nil)
return if uri.blank?
def call(uri, id: true, prefetched_body: nil, suppress_errors: true)
raise Error, 'No key URI given' if uri.blank?
if prefetched_body.nil?
if id
@ -13,7 +15,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
if person?
@json = fetch_resource(@json['id'], true)
elsif uri != @json['id']
return
raise Error, "Fetched URI #{uri} has wrong id #{@json['id']}"
end
else
@json = fetch_resource(uri, id)
@ -22,21 +24,29 @@ class ActivityPub::FetchRemoteKeyService < BaseService
@json = body_to_json(prefetched_body, compare_id: id ? uri : nil)
end
return unless supported_context?(@json) && expected_type?
return find_account(@json['id'], @json) if person?
raise Error, "Unable to fetch key JSON at #{uri}" if @json.nil?
raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?(@json)
raise Error, "Unexpected object type for key #{uri}" unless expected_type?
return find_account(@json['id'], @json, suppress_errors) if person?
@owner = fetch_resource(owner_uri, true)
return unless supported_context?(@owner) && confirmed_owner?
raise Error, "Unable to fetch actor JSON #{owner_uri}" if @owner.nil?
raise Error, "Unsupported JSON-LD context for document #{owner_uri}" unless supported_context?(@owner)
raise Error, "Unexpected object type for actor #{owner_uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_owner_type?
raise Error, "publicKey id for #{owner_uri} does not correspond to #{@json['id']}" unless confirmed_owner?
find_account(owner_uri, @owner)
find_account(owner_uri, @owner, suppress_errors)
rescue Error => e
Rails.logger.debug "Fetching key #{uri} failed: #{e.message}"
raise unless suppress_errors
end
private
def find_account(uri, prefetched_body)
def find_account(uri, prefetched_body, suppress_errors)
account = ActivityPub::TagManager.instance.uri_to_resource(uri, Account)
account ||= ActivityPub::FetchRemoteAccountService.new.call(uri, prefetched_body: prefetched_body)
account ||= ActivityPub::FetchRemoteAccountService.new.call(uri, prefetched_body: prefetched_body, suppress_errors: suppress_errors)
account
end
@ -56,7 +66,11 @@ class ActivityPub::FetchRemoteKeyService < BaseService
@owner_uri ||= value_or_id(@json['owner'])
end
def expected_owner_type?
equals_or_includes_any?(@owner['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
end
def confirmed_owner?
equals_or_includes_any?(@owner['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) && value_or_id(@owner['publicKey']) == @json['id']
value_or_id(@owner['publicKey']) == @json['id']
end
end

View file

@ -32,8 +32,6 @@ class ActivityPub::ProcessAccountService < BaseService
process_duplicate_accounts! if @options[:verified_webfinger]
end
return if @account.nil?
after_protocol_change! if protocol_changed?
after_key_change! if key_changed? && !@options[:signed_with_known_key]
clear_tombstones! if key_changed?