Mastodonサーバがリモートからポストを受け取ったらやること (書きかけ)
ActivityPubでフォロワーの多いアカウントがポストをブーストした時にポスト元のサーバにどのようなリクエストが送られるのか議論している時に、僕はポストを受け取ったサーバがポストを作成したアカウントのrel="me"リンクの検証をしないと思い込んでいました。実際には、ポストを受け取ったサーバがポストを作成したアカウントのプロファイルを表示するので、rel="me"リンクの検証が必要そうです。この他、プレビューカードを作成するために元のポストの内容を取得する必要もありそうです。コードを読んで、ブーストされたポストを受け取ったMastodonサーバがどのような処理をおこなうのか想像してみます。参照する対象のコードは4.2.0ごろのものです。
著作権表示とライセンス
参照対象のコードのCopyright (C) 2016-2023 Eugen Rochko & other Mastodon contributors (see AUTHORS.md)
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
ポストの到着
手元のMastodonサーバへのポストは、/inbox
へのPOST
リクエストとして届くようです。Railsのログにはcontroller=ActivityPub::InboxesController action=create
と一緒にkey
が記録されていて、202
を返却するようです。
def create
upgrade_account
process_collection_synchronization
process_payload
head 202
end
ActivityPub::InboxesController#upgrade_account
OStatusからの移行パスかな?
def upgrade_account
if signed_request_account&.ostatus?
signed_request_account.update(last_webfingered_at: nil)
ResolveAccountWorker.perform_async(signed_request_account.acct)
end
DeliveryFailureTracker.reset!(signed_request_actor.inbox_url)
end
DeliveryFailureTracker.reset!
はローカルで保持してる情報の更新しかしないようだ。
class DeliveryFailureTracker
:
def track_success!
redis.del(exhausted_deliveries_key)
UnavailableDomain.find_by(domain: @host)&.destroy
end
:
alias reset! track_success!
:
ActivityPub::InboxesController#process_collection_synchronization
Collection-Synchronization
リクエストヘッダが来ていた場合の処理。https://socialhub.activitypub.rocks/t/fep-8fcf-followers-collection-synchronization-across-servers/1172 かな?
def process_collection_synchronization
raw_params = request.headers['Collection-Synchronization']
return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true' || signed_request_account.nil?
# Re-using the syntax for signature parameters
tree = SignatureParamsParser.new.parse(raw_params)
params = SignatureParamsTransformer.new.apply(tree)
ActivityPub::PrepareFollowersSynchronizationService.new.call(signed_request_account, params)
rescue Parslet::ParseFailed
Rails.logger.warn 'Error parsing Collection-Synchronization header'
end
ActivityPub::InboxesController#process_payload
ここを追っていかないといけなさそう。
signed_request_actor
が呼ばれるのはperform_async
よりperform
が実行された時ではなく、process_payload
が実行された時だろう。このメソッドで例外が起きた場合にはレスポンスコードが202ではない適切なものになるのかもしれない。
def process_payload
ActivityPub::ProcessingWorker.perform_async(signed_request_actor.id, body, @account&.id, signed_request_actor.class.name)
end
class ActivityPub::ProcessingWorker
include Sidekiq::Worker
sidekiq_options queue: 'ingress', backtrace: true, retry: 8
def perform(actor_id, body, delivered_to_account_id = nil, actor_type = 'Account')
case actor_type
when 'Account'
actor = Account.find_by(id: actor_id)
end
return if actor.nil?
ActivityPub::ProcessCollectionService.new.call(body, actor, override_timestamps: true, delivered_to_account_id: delivered_to_account_id, delivery: true)
rescue ActiveRecord::RecordInvalid => e
Rails.logger.debug { "Error processing incoming ActivityPub object: #{e}" }
end
end
Account.find_by
知らないアカウントだった場合にcreateするのかな?いや、ActiveRecord.find_byには見つからないレコードを暗黙のうちに作る機能は無さそう。
新規にリモートアカウントを作る可能性があるのはupgrade_account
から呼ばれたDeliveryFailureTracker.reset!(signed_request_actor.inbox_url)
ということだろうか?いや、signed_request_actor
だろう。
ActivityPub::ProcessCollectionService.new.call
# frozen_string_literal: true
class ActivityPub::ProcessCollectionService < BaseService
include JsonLdHelper
def call(body, actor, **options)
@account = actor
@json = original_json = Oj.load(body, mode: :strict)
@options = options
return unless @json.is_a?(Hash)
begin
@json = compact(@json) if @json['signature'].is_a?(Hash)
rescue JSON::LD::JsonLdError => e
Rails.logger.debug { "Error when compacting JSON-LD document for #{value_or_id(@json['actor'])}: #{e.message}" }
@json = original_json.without('signature')
end
return if !supported_context? || (different_actor? && verify_account!.nil?) || suspended_actor? || @account.local?
return unless @account.is_a?(Account)
if @json['signature'].present?
# We have verified the signature, but in the compaction step above, might
# have introduced incompatibilities with other servers that do not
# normalize the JSON-LD documents (for instance, previous Mastodon
# versions), so skip redistribution if we can't get a safe document.
patch_for_forwarding!(original_json, @json)
@json.delete('signature') unless safe_for_forwarding?(original_json, @json)
end
case @json['type']
when 'Collection', 'CollectionPage'
process_items @json['items']
when 'OrderedCollection', 'OrderedCollectionPage'
process_items @json['orderedItems']
else
process_items [@json]
end
rescue Oj::ParseError
nil
end
private
def different_actor?
@json['actor'].present? && value_or_id(@json['actor']) != @account.uri
end
def suspended_actor?
@account.suspended? && !activity_allowed_while_suspended?
end
def activity_allowed_while_suspended?
%w(Delete Reject Undo Update).include?(@json['type'])
end
def process_items(items)
items.reverse_each.filter_map { |item| process_item(item) }
end
def supported_context?
super(@json)
end
def process_item(item)
activity = ActivityPub::Activity.factory(item, @account, **@options)
activity&.perform
end
def verify_account!
@options[:relayed_through_actor] = @account
@account = ActivityPub::LinkedDataSignature.new(@json).verify_actor!
@account = nil unless @account.is_a?(Account)
@account
rescue JSON::LD::JsonLdError, RDF::WriterError => e
Rails.logger.debug { "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}" }
nil
end
end
# frozen_string_literal: true
module JsonLdHelper
include ContextHelper
def equals_or_includes?(haystack, needle)
haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle
end
def equals_or_includes_any?(haystack, needles)
needles.any? { |needle| equals_or_includes?(haystack, needle) }
end
def first_of_value(value)
value.is_a?(Array) ? value.first : value
end
def uri_from_bearcap(str)
if str&.start_with?('bear:')
Addressable::URI.parse(str).query_values['u']
else
str
end
end
# The url attribute can be a string, an array of strings, or an array of objects.
# The objects could include a mimeType. Not-included mimeType means it's text/html.
def url_to_href(value, preferred_type = nil)
single_value = if value.is_a?(Array) && !value.first.is_a?(String)
value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) }
elsif value.is_a?(Array)
value.first
else
value
end
if single_value.nil? || single_value.is_a?(String)
single_value
else
single_value['href']
end
end
def as_array(value)
if value.nil?
[]
elsif value.is_a?(Array)
value
else
[value]
end
end
def value_or_id(value)
value.is_a?(String) || value.nil? ? value : value['id']
end
def supported_context?(json)
!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT)
end
def unsupported_uri_scheme?(uri)
uri.nil? || !uri.start_with?('http://', 'https://')
end
def non_matching_uri_hosts?(base_url, comparison_url)
return true if unsupported_uri_scheme?(comparison_url)
needle = Addressable::URI.parse(comparison_url).host
haystack = Addressable::URI.parse(base_url).host
!haystack.casecmp(needle).zero?
end
def canonicalize(json)
graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context))
graph.dump(:normalize)
end
def compact(json)
compacted = JSON::LD::API.compact(json.without('signature'), full_context, documentLoader: method(:load_jsonld_context))
compacted['signature'] = json['signature']
compacted
end
# Patches a JSON-LD document to avoid compatibility issues on redistribution
#
# Since compacting a JSON-LD document against Mastodon's built-in vocabulary
# means other extension namespaces will be expanded, malformed JSON-LD
# attributes lost, and some values “unexpectedly” compacted this method
# patches the following likely sources of incompatibility:
# - 'https://www.w3.org/ns/activitystreams#Public' being compacted to
# 'as:Public' (for instance, pre-3.4.0 Mastodon does not understand
# 'as:Public')
# - single-item arrays being compacted to the item itself (`[foo]` being
# compacted to `foo`)
#
# It is not always possible for `patch_for_forwarding!` to produce a document
# deemed safe for forwarding. Use `safe_for_forwarding?` to check the status
# of the output document.
#
# @param original [Hash] The original JSON-LD document used as reference
# @param compacted [Hash] The compacted JSON-LD document to be patched
# @return [void]
def patch_for_forwarding!(original, compacted)
original.without('@context', 'signature').each do |key, value|
next if value.nil? || !compacted.key?(key)
compacted_value = compacted[key]
if value.is_a?(Hash) && compacted_value.is_a?(Hash)
patch_for_forwarding!(value, compacted_value)
elsif value.is_a?(Array)
compacted_value = [compacted_value] unless compacted_value.is_a?(Array)
return if value.size != compacted_value.size
compacted[key] = value.zip(compacted_value).map do |v, vc|
if v.is_a?(Hash) && vc.is_a?(Hash)
patch_for_forwarding!(v, vc)
vc
elsif v == 'https://www.w3.org/ns/activitystreams#Public' && vc == 'as:Public'
v
else
vc
end
end
elsif value == 'https://www.w3.org/ns/activitystreams#Public' && compacted_value == 'as:Public'
compacted[key] = value
end
end
end
# Tests whether a JSON-LD compaction is deemed safe for redistribution,
# that is, if it doesn't change its meaning to consumers that do not actually
# handle JSON-LD, but rely on values being serialized in a certain way.
#
# See `patch_for_forwarding!` for details.
#
# @param original [Hash] The original JSON-LD document used as reference
# @param compacted [Hash] The compacted JSON-LD document to be patched
# @return [Boolean] Whether the patched document is deemed safe
def safe_for_forwarding?(original, compacted)
original.without('@context', 'signature').all? do |key, value|
compacted_value = compacted[key]
return false unless value.class == compacted_value.class
if value.is_a?(Hash)
safe_for_forwarding?(value, compacted_value)
elsif value.is_a?(Array)
value.zip(compacted_value).all? do |v, vc|
v.is_a?(Hash) ? (vc.is_a?(Hash) && safe_for_forwarding?(v, vc)) : v == vc
end
else
value == compacted_value
end
end
end
def fetch_resource(uri, id, on_behalf_of = nil)
unless id
json = fetch_resource_without_id_validation(uri, on_behalf_of)
return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
uri = json['id']
end
json = fetch_resource_without_id_validation(uri, on_behalf_of)
json.present? && json['id'] == uri ? json : nil
end
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false)
on_behalf_of ||= Account.representative
build_request(uri, on_behalf_of).perform do |response|
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
body_to_json(response.body_with_limit) if response.code == 200
end
end
def body_to_json(body, compare_id: nil)
json = body.is_a?(String) ? Oj.load(body, mode: :strict) : body
return if compare_id.present? && json['id'] != compare_id
json
rescue Oj::ParseError
nil
end
def merge_context(context, new_context)
if context.is_a?(Array)
context << new_context
else
[context, new_context]
end
end
def response_successful?(response)
(200...300).cover?(response.code)
end
def response_error_unsalvageable?(response)
response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code))
end
def build_request(uri, on_behalf_of = nil)
Request.new(:get, uri).tap do |request|
request.on_behalf_of(on_behalf_of) if on_behalf_of
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
end
end
def load_jsonld_context(url, _options = {}, &block)
json = Rails.cache.fetch("jsonld:context:#{url}", expires_in: 30.days, raw: true) do
request = Request.new(:get, url)
request.add_headers('Accept' => 'application/ld+json')
request.perform do |res|
raise JSON::LD::JsonLdError::LoadingDocumentFailed unless res.code == 200 && res.mime_type == 'application/ld+json'
res.body_with_limit
end
end
doc = JSON::LD::API::RemoteDocument.new(json, documentUrl: url)
block ? yield(doc) : doc
end
end
signed_request_actor
署名が有効なのは、
-
Signature
リクエストヘッダが存在し、 -
Signature
リクエストヘッダにkeyId
とsignature
パラメータが存在し、 - 署名アルゴリズムがサポートされたもので、かつ、
- 署名時刻がある程度の範囲内にある場合
で、その場合、署名の強度を確認して、ダイジェストの照合をおこない、actor_from_key_id(signature_params['keyId'])
からResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false)
を呼ぶようだ。
この段階でローカルに持っている公開鍵で署名の検証をおこない、検証が失敗した場合にはactor_refresh_key!(actor)
からActivityPub::FetchRemoteActorService.new.call(actor.uri, only_key: true, suppress_errors: false)
を呼び公開鍵を取得しなおしてから再度署名を検証する。
def signed_request_actor
return @signed_request_actor if defined?(@signed_request_actor)
raise SignatureVerificationError, 'Request not signed' unless signed_request?
raise SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters?
raise SignatureVerificationError, 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)' unless %w(rsa-sha256 hs2019).include?(signature_algorithm)
raise SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window?
verify_signature_strength!
verify_body_digest!
actor = actor_from_key_id(signature_params['keyId'])
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
signature = Base64.decode64(signature_params['signature'])
compare_signed_string = build_signed_string
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
actor = stoplight_wrap_request { actor_refresh_key!(actor) }
raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature']
rescue SignatureVerificationError => e
fail_with! e.message
rescue HTTP::Error, OpenSSL::SSL::SSLError => e
fail_with! "Failed to fetch remote data: #{e.message}"
rescue Mastodon::UnexpectedResponseError
fail_with! 'Failed to fetch remote data (got unexpected reply from server)'
rescue Stoplight::Error::RedLight
fail_with! 'Fetching attempt skipped because of recent connection failure'
end
def signed_request?
request.headers['Signature'].present?
end
def missing_required_signature_parameters?
signature_params['keyId'].blank? || signature_params['signature'].blank?
end
def signature_params
@signature_params ||= begin
raw_signature = request.headers['Signature']
tree = SignatureParamsParser.new.parse(raw_signature)
SignatureParamsTransformer.new.apply(tree)
end
rescue Parslet::ParseFailed
raise SignatureVerificationError, 'Error parsing signature parameters'
end
def actor_from_key_id(key_id)
domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id
if domain_not_allowed?(domain)
@signature_verification_failure_code = 403
return
end
if key_id.start_with?('acct:')
stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) }
account
end
rescue Mastodon::PrivateNetworkAddressError => e
raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e
raise SignatureVerificationError, e.message
end
def actor_refresh_key!(actor)
return if actor.local? || !actor.activitypub?
return actor.refresh! if actor.respond_to?(:refresh!) && actor.possibly_stale?
ActivityPub::FetchRemoteActorService.new.call(actor.uri, only_key: true, suppress_errors: false)
rescue Mastodon::PrivateNetworkAddressError => e
raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, Webfinger::Error => e
raise SignatureVerificationError, e.message
end
ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false)
-
ここで参照されている
uri
の内容を確認する。acct:zundan@mastodon.zunda.ninja
の形式だろうか。
既知のリモートのアカウントについてはwebfingerの更新時期になっていない限りローカルで知っている内容を返す。取得あるいは更新が必要な場合にはprocess_webfinger!(@uri)
をしたのちに、fetch_account!
からActivityPub::FetchRemoteAccountService.new.call(actor_url, suppress_errors: @options[:suppress_errors])
を呼ぶ。
# 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
# @option options [Boolean] :skip_cache Get the latest data from origin even if cache is not due to update yet
# @option options [Boolean] :suppress_errors When failing, return nil instead of raising an error
# @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 => e
Rails.logger.debug { "Webfinger query for #{@uri} failed: #{e}" }
raise unless @options[:suppress_errors]
end
def fetch_account!
return unless activitypub_ready?
with_redis_lock("resolve:#{@username}@#{@domain}") do
@account = ActivityPub::FetchRemoteAccountService.new.call(actor_url, suppress_errors: @options[:suppress_errors])
end
@account
end
ActivityPub::FetchRemoteActorService.new.call(actor.uri, only_key: true, suppress_errors: false)
ざっくり読んだだけだが、Request.new(:get, uri).perfome do |response|
して得られたJSONをパーズして返すようだ。
# 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, suppress_errors: true, request_id: nil)
return if domain_not_allowed?(uri)
return ActivityPub::TagManager.instance.uri_to_actor(uri) if ActivityPub::TagManager.instance.local_uri?(uri)
@json = begin
if prefetched_body.nil?
fetch_resource(uri, id)
else
body_to_json(prefetched_body, compare_id: id ? uri : nil)
end
rescue Oj::ParseError
raise Error, "Error parsing JSON-LD document #{uri}"
end
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?
raise Error, "Actor #{uri} has no 'preferredUsername', which is a requirement for Mastodon compatibility" if @json['preferredUsername'].blank?
@uri = @json['id']
@username = @json['preferredUsername']
@domain = Addressable::URI.parse(@uri).normalized_host
check_webfinger! unless only_key
ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key, verified_webfinger: !only_key, request_id: request_id)
rescue Error => e
Rails.logger.debug { "Fetching actor #{uri} failed: #{e.message}" }
raise unless suppress_errors
end
def fetch_resource(uri, id, on_behalf_of = nil)
unless id
json = fetch_resource_without_id_validation(uri, on_behalf_of)
return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
uri = json['id']
end
json = fetch_resource_without_id_validation(uri, on_behalf_of)
json.present? && json['id'] == uri ? json : nil
end
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false)
on_behalf_of ||= Account.representative
build_request(uri, on_behalf_of).perform do |response|
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
body_to_json(response.body_with_limit) if response.code == 200
end
end
def build_request(uri, on_behalf_of = nil)
Request.new(:get, uri).tap do |request|
request.on_behalf_of(on_behalf_of) if on_behalf_of
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
end
end
ActivityPub::FetchRemoteAccountService.new.call(actor_url, suppress_errors: @options[:suppress_errors])
class ActivityPub::FetchRemoteAccountService < ActivityPub::FetchRemoteActorService
# 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, suppress_errors: true, request_id: nil)
actor = super
return actor if actor.nil? || actor.is_a?(Account)
Rails.logger.debug { "Fetching account #{uri} failed: Expected Account, got #{actor.class.name}" }
raise Error, "Expected Account, got #{actor.class.name}" unless suppress_errors
end
end
process_webfinger!(@uri)
https://#{@domain}/.well-known/webfinger?resource=#{@uri}
形式のURLからJSONを取得するようだ。
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)
raise Webfinger::RedirectError, "Too many webfinger redirects for URI #{uri} (stopped at #{@username}@#{@domain})" unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
rescue Webfinger::GoneError
@gone = true
end
module WebfingerHelper
def webfinger!(uri)
Webfinger.new(uri).perform
end
end
def initialize(uri)
_, @domain = uri.split('@')
raise ArgumentError, 'Webfinger requested for local account' if @domain.nil?
@uri = uri
end
def perform
Response.new(@uri, body_from_webfinger)
rescue Oj::ParseError
raise Webfinger::Error, "Invalid JSON in response for #{@uri}"
rescue Addressable::URI::InvalidURIError
raise Webfinger::Error, "Invalid URI for #{@uri}"
end
private
def body_from_webfinger(url = standard_url, use_fallback = true)
webfinger_request(url).perform do |res|
if res.code == 200
body = res.body_with_limit
raise Webfinger::Error, "Request for #{@uri} returned empty response" if body.empty?
body
elsif res.code == 404 && use_fallback
body_from_host_meta
elsif res.code == 410
raise Webfinger::GoneError, "#{@uri} is gone from the server"
else
raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}"
end
end
end
def webfinger_request(url)
Request.new(:get, url).add_headers('Accept' => 'application/jrd+json, application/json')
end
def standard_url
if @domain.end_with? '.onion'
"http://#{@domain}/.well-known/webfinger?resource=#{@uri}"
else
"https://#{@domain}/.well-known/webfinger?resource=#{@uri}"
end
end
レスポンスの例
$ curl -s -H 'Accept: applicatio/jrd+json, application/json' https://mastodon.zunda.ninja/.well-known/webfinger?resource=acct:zundan@mastodon.zunda.ninja | jq
{
"subject": "acct:zundan@mastodon.zunda.ninja",
"aliases": [
"https://mastodon.zunda.ninja/@zundan",
"https://mastodon.zunda.ninja/users/zundan"
],
"links": [
{
"rel": "http://webfinger.net/rel/profile-page",
"type": "text/html",
"href": "https://mastodon.zunda.ninja/@zundan"
},
{
"rel": "self",
"type": "application/activity+json",
"href": "https://mastodon.zunda.ninja/users/zundan"
},
{
"rel": "http://ostatus.org/schema/1.0/subscribe",
"template": "https://mastodon.zunda.ninja/authorize_interaction?uri={uri}"
},
{
"rel": "http://webfinger.net/rel/avatar",
"type": "image/gif",
"href": "https://s3.amazonaws.com/zundan-mastodon/accounts/avatars/000/000/001/original/527fd226e51be531.gif"
}
]
}
メソッドの呼び出し順のメモ
- ActivityPub::InboxesControlle#create
- ActivityPub::InboxesController#process_payload
- signed_request_actor (app/controllers/concerns/signature_verification.rb)
- actor = actor_from_key_id(signature_params['keyId'])
- stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
- @account ||= Account.find_remote(@username, @domain)
- return @account if @account&.local? || @domain.nil? || !webfinger_update_due?
- process_webfinger!(@uri)
- webfinger!("acct:#{uri}")
- Webfinger.new(uri).perform
- Response.new(@uri, body_from_webfinger) (app/lib/webfinger)
- webfinger_request(url)
- Request.new(:get, url) 必要な場合にはActivityPubオブジェクトの到着と同期的にActivityPubオブジェクトの送り主にWebFingerが送られるようだ
- webfinger_request(url)
- Response.new(@uri, body_from_webfinger) (app/lib/webfinger)
- Webfinger.new(uri).perform
- webfinger!("acct:#{uri}")
- fetch_account!
- @account = ActivityPub::FetchRemoteAccountService.new.call(actor_url, suppress_errors: @options[:suppress_errors])
- fetch_resource(uri, id)
- json = fetch_resource_without_id_validation(uri, on_behalf_of)
- build_request(uri, on_behalf_of).perform
- Request.new(:get, uri)
- build_request(uri, on_behalf_of).perform
- json = fetch_resource_without_id_validation(uri, on_behalf_of)
- ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key, verified_webfinger: !only_key, request_id: request_id)
- TODO
- fetch_resource(uri, id)
- @account = ActivityPub::FetchRemoteAccountService.new.call(actor_url, suppress_errors: @options[:suppress_errors])
- stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
- actor = stoplight_wrap_request { actor_refresh_key!(actor) }
- actor = actor_from_key_id(signature_params['keyId'])
- ActivityPub::ProcessingWorker.perform_async (以下はActivityPubオブジェクトの到着とは非同期的)
- signed_request_actor (app/controllers/concerns/signature_verification.rb)
- ActivityPub::InboxesController#process_payload