Refactor ActivityPub handling to prepare for non-Account actors (#19212)
* Move ActivityPub::FetchRemoteAccountService to ActivityPub::FetchRemoteActorService ActivityPub::FetchRemoteAccountService is kept as a wrapper for when the actor is specifically required to be an Account * Refactor SignatureVerification to allow non-Account actors * fixup! Move ActivityPub::FetchRemoteAccountService to ActivityPub::FetchRemoteActorService * Refactor ActivityPub::FetchRemoteKeyService to potentially return non-Account actors * Refactor inbound ActivityPub payload processing to accept non-Account actors * Refactor inbound ActivityPub processing to accept activities relayed through non-Account * Refactor how Account key URIs are built * Refactor Request and drop unused key_id_format parameter * Rename ActivityPub::Dereferencer `signature_account` to `signature_actor`
This commit is contained in:
parent
84aff598ea
commit
8cf7006d4e
|
@ -7,7 +7,7 @@ class AccountsController < ApplicationController
|
|||
include AccountControllerConcern
|
||||
include SignatureAuthentication
|
||||
|
||||
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :set_cache_headers
|
||||
before_action :set_body_classes
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ class ActivityPub::ClaimsController < ActivityPub::BaseController
|
|||
|
||||
skip_before_action :authenticate_user!
|
||||
|
||||
before_action :require_signature!
|
||||
before_action :require_account_signature!
|
||||
before_action :set_claim_result
|
||||
|
||||
def create
|
||||
|
|
|
@ -4,7 +4,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
|||
include SignatureVerification
|
||||
include AccountOwnedConcern
|
||||
|
||||
before_action :require_signature!, if: :authorized_fetch_mode?
|
||||
before_action :require_account_signature!, if: :authorized_fetch_mode?
|
||||
before_action :set_items
|
||||
before_action :set_size
|
||||
before_action :set_type
|
||||
|
|
|
@ -4,7 +4,7 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro
|
|||
include SignatureVerification
|
||||
include AccountOwnedConcern
|
||||
|
||||
before_action :require_signature!
|
||||
before_action :require_account_signature!
|
||||
before_action :set_items
|
||||
before_action :set_cache_headers
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
|||
include AccountOwnedConcern
|
||||
|
||||
before_action :skip_unknown_actor_activity
|
||||
before_action :require_signature!
|
||||
before_action :require_actor_signature!
|
||||
skip_before_action :authenticate_user!
|
||||
|
||||
def create
|
||||
|
@ -49,17 +49,17 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
|||
end
|
||||
|
||||
def upgrade_account
|
||||
if signed_request_account.ostatus?
|
||||
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_account.inbox_url)
|
||||
DeliveryFailureTracker.reset!(signed_request_actor.inbox_url)
|
||||
end
|
||||
|
||||
def process_collection_synchronization
|
||||
raw_params = request.headers['Collection-Synchronization']
|
||||
return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true'
|
||||
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)
|
||||
|
@ -71,6 +71,6 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
|||
end
|
||||
|
||||
def process_payload
|
||||
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id)
|
||||
ActivityPub::ProcessingWorker.perform_async(signed_request_actor.id, body, @account&.id, signed_request_actor.class.name)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,7 +6,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
|||
include SignatureVerification
|
||||
include AccountOwnedConcern
|
||||
|
||||
before_action :require_signature!, if: :authorized_fetch_mode?
|
||||
before_action :require_account_signature!, if: :authorized_fetch_mode?
|
||||
before_action :set_statuses
|
||||
before_action :set_cache_headers
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
|
|||
|
||||
DESCENDANTS_LIMIT = 60
|
||||
|
||||
before_action :require_signature!, if: :authorized_fetch_mode?
|
||||
before_action :require_account_signature!, if: :authorized_fetch_mode?
|
||||
before_action :set_status
|
||||
before_action :set_cache_headers
|
||||
before_action :set_replies
|
||||
|
|
|
@ -45,10 +45,14 @@ module SignatureVerification
|
|||
end
|
||||
end
|
||||
|
||||
def require_signature!
|
||||
def require_account_signature!
|
||||
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
|
||||
end
|
||||
|
||||
def require_actor_signature!
|
||||
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_actor
|
||||
end
|
||||
|
||||
def signed_request?
|
||||
request.headers['Signature'].present?
|
||||
end
|
||||
|
@ -68,7 +72,11 @@ module SignatureVerification
|
|||
end
|
||||
|
||||
def signed_request_account
|
||||
return @signed_request_account if defined?(@signed_request_account)
|
||||
signed_request_actor.is_a?(Account) ? signed_request_actor : nil
|
||||
end
|
||||
|
||||
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?
|
||||
|
@ -78,22 +86,22 @@ module SignatureVerification
|
|||
verify_signature_strength!
|
||||
verify_body_digest!
|
||||
|
||||
account = account_from_key_id(signature_params['keyId'])
|
||||
actor = actor_from_key_id(signature_params['keyId'])
|
||||
|
||||
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil?
|
||||
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 account unless verify_signature(account, signature, compare_signed_string).nil?
|
||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||
|
||||
account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
|
||||
actor = stoplight_wrap_request { actor_refresh_key!(actor) }
|
||||
|
||||
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil?
|
||||
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
|
||||
|
||||
return account unless verify_signature(account, signature, compare_signed_string).nil?
|
||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||
|
||||
fail_with! "Verification failed for #{account.username}@#{account.domain} #{account.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)"
|
||||
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)"
|
||||
rescue SignatureVerificationError => e
|
||||
fail_with! e.message
|
||||
rescue HTTP::Error, OpenSSL::SSL::SSLError => e
|
||||
|
@ -112,7 +120,7 @@ module SignatureVerification
|
|||
|
||||
def fail_with!(message)
|
||||
@signature_verification_failure_reason = message
|
||||
@signed_request_account = nil
|
||||
@signed_request_actor = nil
|
||||
end
|
||||
|
||||
def signature_params
|
||||
|
@ -160,10 +168,10 @@ module SignatureVerification
|
|||
raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}"
|
||||
end
|
||||
|
||||
def verify_signature(account, signature, compare_signed_string)
|
||||
if account.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string)
|
||||
@signed_request_account = account
|
||||
@signed_request_account
|
||||
def verify_signature(actor, signature, compare_signed_string)
|
||||
if actor.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string)
|
||||
@signed_request_actor = actor
|
||||
@signed_request_actor
|
||||
end
|
||||
rescue OpenSSL::PKey::RSAError
|
||||
nil
|
||||
|
@ -226,7 +234,7 @@ module SignatureVerification
|
|||
signature_params['keyId'].blank? || signature_params['signature'].blank?
|
||||
end
|
||||
|
||||
def account_from_key_id(key_id)
|
||||
def actor_from_key_id(key_id)
|
||||
domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id
|
||||
|
||||
if domain_not_allowed?(domain)
|
||||
|
@ -237,13 +245,13 @@ module SignatureVerification
|
|||
if key_id.start_with?('acct:')
|
||||
stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), suppress_errors: false) }
|
||||
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
||||
account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
|
||||
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::FetchRemoteAccountService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e
|
||||
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e
|
||||
raise SignatureVerificationError, e.message
|
||||
end
|
||||
|
||||
|
@ -255,12 +263,14 @@ module SignatureVerification
|
|||
.run
|
||||
end
|
||||
|
||||
def account_refresh_key(account)
|
||||
return if account.local? || !account.activitypub?
|
||||
ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true, suppress_errors: false)
|
||||
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::FetchRemoteAccountService::Error, Webfinger::Error => e
|
||||
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, Webfinger::Error => e
|
||||
raise SignatureVerificationError, e.message
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ class FollowerAccountsController < ApplicationController
|
|||
include AccountControllerConcern
|
||||
include SignatureVerification
|
||||
|
||||
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :set_cache_headers
|
||||
|
||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||
|
|
|
@ -4,7 +4,7 @@ class FollowingAccountsController < ApplicationController
|
|||
include AccountControllerConcern
|
||||
include SignatureVerification
|
||||
|
||||
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :set_cache_headers
|
||||
|
||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||
|
|
|
@ -8,7 +8,7 @@ class StatusesController < ApplicationController
|
|||
|
||||
layout 'public'
|
||||
|
||||
before_action :require_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :set_status
|
||||
before_action :set_instance_presenter
|
||||
before_action :set_link_headers
|
||||
|
|
|
@ -8,7 +8,7 @@ class TagsController < ApplicationController
|
|||
|
||||
layout 'public'
|
||||
|
||||
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :authenticate_user!, if: :whitelist_mode?
|
||||
before_action :set_local
|
||||
before_action :set_tag
|
||||
|
|
|
@ -116,12 +116,12 @@ class ActivityPub::Activity
|
|||
def dereference_object!
|
||||
return unless @object.is_a?(String)
|
||||
|
||||
dereferencer = ActivityPub::Dereferencer.new(@object, permitted_origin: @account.uri, signature_account: signed_fetch_account)
|
||||
dereferencer = ActivityPub::Dereferencer.new(@object, permitted_origin: @account.uri, signature_actor: signed_fetch_actor)
|
||||
|
||||
@object = dereferencer.object unless dereferencer.object.nil?
|
||||
end
|
||||
|
||||
def signed_fetch_account
|
||||
def signed_fetch_actor
|
||||
return Account.find(@options[:delivered_to_account_id]) if @options[:delivered_to_account_id].present?
|
||||
|
||||
first_mentioned_local_account || first_local_follower
|
||||
|
@ -163,15 +163,15 @@ class ActivityPub::Activity
|
|||
end
|
||||
|
||||
def followed_by_local_accounts?
|
||||
@account.passive_relationships.exists? || @options[:relayed_through_account]&.passive_relationships&.exists?
|
||||
@account.passive_relationships.exists? || (@options[:relayed_through_actor].is_a?(Account) && @options[:relayed_through_actor].passive_relationships&.exists?)
|
||||
end
|
||||
|
||||
def requested_through_relay?
|
||||
@options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled?
|
||||
@options[:relayed_through_actor] && Relay.find_by(inbox_url: @options[:relayed_through_actor].inbox_url)&.enabled?
|
||||
end
|
||||
|
||||
def reject_payload!
|
||||
Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_account] && "via #{@options[:relayed_through_account].uri}"}")
|
||||
Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_actor] && "via #{@options[:relayed_through_actor].uri}"}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
class ActivityPub::Dereferencer
|
||||
include JsonLdHelper
|
||||
|
||||
def initialize(uri, permitted_origin: nil, signature_account: nil)
|
||||
def initialize(uri, permitted_origin: nil, signature_actor: nil)
|
||||
@uri = uri
|
||||
@permitted_origin = permitted_origin
|
||||
@signature_account = signature_account
|
||||
@signature_actor = signature_actor
|
||||
end
|
||||
|
||||
def object
|
||||
|
@ -46,7 +46,7 @@ class ActivityPub::Dereferencer
|
|||
|
||||
req.add_headers('Accept' => 'application/activity+json, application/ld+json')
|
||||
req.add_headers(headers) if headers
|
||||
req.on_behalf_of(@signature_account) if @signature_account
|
||||
req.on_behalf_of(@signature_actor) if @signature_actor
|
||||
|
||||
req.perform do |res|
|
||||
if res.code == 200
|
||||
|
|
|
@ -9,7 +9,7 @@ class ActivityPub::LinkedDataSignature
|
|||
@json = json.with_indifferent_access
|
||||
end
|
||||
|
||||
def verify_account!
|
||||
def verify_actor!
|
||||
return unless @json['signature'].is_a?(Hash)
|
||||
|
||||
type = @json['signature']['type']
|
||||
|
@ -18,7 +18,7 @@ class ActivityPub::LinkedDataSignature
|
|||
|
||||
return unless type == 'RsaSignature2017'
|
||||
|
||||
creator = ActivityPub::TagManager.instance.uri_to_resource(creator_uri, Account)
|
||||
creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
|
||||
creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false)
|
||||
|
||||
return if creator.nil?
|
||||
|
@ -35,7 +35,7 @@ class ActivityPub::LinkedDataSignature
|
|||
def sign!(creator, sign_with: nil)
|
||||
options = {
|
||||
'type' => 'RsaSignature2017',
|
||||
'creator' => [ActivityPub::TagManager.instance.uri_for(creator), '#main-key'].join,
|
||||
'creator' => ActivityPub::TagManager.instance.key_uri_for(creator),
|
||||
'created' => Time.now.utc.iso8601,
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,10 @@ class ActivityPub::TagManager
|
|||
end
|
||||
end
|
||||
|
||||
def key_uri_for(target)
|
||||
[uri_for(target), '#main-key'].join
|
||||
end
|
||||
|
||||
def uri_for_username(username)
|
||||
account_url(username: username)
|
||||
end
|
||||
|
@ -155,6 +159,10 @@ class ActivityPub::TagManager
|
|||
path_params[param]
|
||||
end
|
||||
|
||||
def uri_to_actor(uri)
|
||||
uri_to_resource(uri, Account)
|
||||
end
|
||||
|
||||
def uri_to_resource(uri, klass)
|
||||
return if uri.nil?
|
||||
|
||||
|
|
|
@ -40,12 +40,11 @@ class Request
|
|||
set_digest! if options.key?(:body)
|
||||
end
|
||||
|
||||
def on_behalf_of(account, key_id_format = :uri, sign_with: nil)
|
||||
raise ArgumentError, 'account must not be nil' if account.nil?
|
||||
def on_behalf_of(actor, sign_with: nil)
|
||||
raise ArgumentError, 'actor must not be nil' if actor.nil?
|
||||
|
||||
@account = account
|
||||
@keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @account.keypair
|
||||
@key_id_format = key_id_format
|
||||
@actor = actor
|
||||
@keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @actor.keypair
|
||||
|
||||
self
|
||||
end
|
||||
|
@ -79,7 +78,7 @@ class Request
|
|||
end
|
||||
|
||||
def headers
|
||||
(@account ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
|
||||
(@actor ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
|
||||
end
|
||||
|
||||
class << self
|
||||
|
@ -128,12 +127,7 @@ class Request
|
|||
end
|
||||
|
||||
def key_id
|
||||
case @key_id_format
|
||||
when :acct
|
||||
@account.to_webfinger_s
|
||||
when :uri
|
||||
[ActivityPub::TagManager.instance.uri_for(@account), '#main-key'].join
|
||||
end
|
||||
ActivityPub::TagManager.instance.key_uri_for(@actor)
|
||||
end
|
||||
|
||||
def http_client
|
||||
|
|
|
@ -6,7 +6,7 @@ class ActivityPub::PublicKeySerializer < ActivityPub::Serializer
|
|||
attributes :id, :owner, :public_key_pem
|
||||
|
||||
def id
|
||||
[ActivityPub::TagManager.instance.uri_for(object), '#main-key'].join
|
||||
ActivityPub::TagManager.instance.key_uri_for(object)
|
||||
end
|
||||
|
||||
def owner
|
||||
|
|
|
@ -1,80 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::FetchRemoteAccountService < BaseService
|
||||
include JsonLdHelper
|
||||
include DomainControlHelper
|
||||
include WebfingerHelper
|
||||
|
||||
class Error < StandardError; end
|
||||
|
||||
SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
|
||||
|
||||
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)
|
||||
return if domain_not_allowed?(uri)
|
||||
return ActivityPub::TagManager.instance.uri_to_resource(uri, Account) if ActivityPub::TagManager.instance.local_uri?(uri)
|
||||
actor = super
|
||||
return actor if actor.nil? || actor.is_a?(Account)
|
||||
|
||||
@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?
|
||||
|
||||
@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)
|
||||
rescue Error => e
|
||||
Rails.logger.debug "Fetching account #{uri} failed: #{e.message}"
|
||||
raise unless suppress_errors
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_webfinger!
|
||||
webfinger = webfinger!("acct:#{@username}@#{@domain}")
|
||||
confirmed_username, confirmed_domain = split_acct(webfinger.subject)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
acct.gsub(/\Aacct:/, '').split('@')
|
||||
end
|
||||
|
||||
def supported_context?
|
||||
super(@json)
|
||||
end
|
||||
|
||||
def expected_type?
|
||||
equals_or_includes_any?(@json['type'], SUPPORTED_TYPES)
|
||||
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
|
||||
|
|
80
app/services/activitypub/fetch_remote_actor_service.rb
Normal file
80
app/services/activitypub/fetch_remote_actor_service.rb
Normal file
|
@ -0,0 +1,80 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::FetchRemoteActorService < BaseService
|
||||
include JsonLdHelper
|
||||
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, suppress_errors: true)
|
||||
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?
|
||||
|
||||
@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)
|
||||
rescue Error => e
|
||||
Rails.logger.debug "Fetching actor #{uri} failed: #{e.message}"
|
||||
raise unless suppress_errors
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_webfinger!
|
||||
webfinger = webfinger!("acct:#{@username}@#{@domain}")
|
||||
confirmed_username, confirmed_domain = split_acct(webfinger.subject)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
acct.gsub(/\Aacct:/, '').split('@')
|
||||
end
|
||||
|
||||
def supported_context?
|
||||
super(@json)
|
||||
end
|
||||
|
||||
def expected_type?
|
||||
equals_or_includes_any?(@json['type'], SUPPORTED_TYPES)
|
||||
end
|
||||
end
|
|
@ -5,7 +5,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
|
|||
|
||||
class Error < StandardError; end
|
||||
|
||||
# Returns account that owns the key
|
||||
# Returns actor that owns the key
|
||||
def call(uri, id: true, prefetched_body: nil, suppress_errors: true)
|
||||
raise Error, 'No key URI given' if uri.blank?
|
||||
|
||||
|
@ -27,7 +27,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
|
|||
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?
|
||||
return find_actor(@json['id'], @json, suppress_errors) if person?
|
||||
|
||||
@owner = fetch_resource(owner_uri, true)
|
||||
|
||||
|
@ -36,7 +36,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
|
|||
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, suppress_errors)
|
||||
find_actor(owner_uri, @owner, suppress_errors)
|
||||
rescue Error => e
|
||||
Rails.logger.debug "Fetching key #{uri} failed: #{e.message}"
|
||||
raise unless suppress_errors
|
||||
|
@ -44,18 +44,18 @@ class ActivityPub::FetchRemoteKeyService < BaseService
|
|||
|
||||
private
|
||||
|
||||
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, suppress_errors: suppress_errors)
|
||||
account
|
||||
def find_actor(uri, prefetched_body, suppress_errors)
|
||||
actor = ActivityPub::TagManager.instance.uri_to_actor(uri)
|
||||
actor ||= ActivityPub::FetchRemoteActorService.new.call(uri, prefetched_body: prefetched_body, suppress_errors: suppress_errors)
|
||||
actor
|
||||
end
|
||||
|
||||
def expected_type?
|
||||
person? || public_key?
|
||||
actor? || public_key?
|
||||
end
|
||||
|
||||
def person?
|
||||
equals_or_includes_any?(@json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
|
||||
def actor?
|
||||
equals_or_includes_any?(@json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES)
|
||||
end
|
||||
|
||||
def public_key?
|
||||
|
@ -67,7 +67,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
|
|||
end
|
||||
|
||||
def expected_owner_type?
|
||||
equals_or_includes_any?(@owner['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
|
||||
equals_or_includes_any?(@owner['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES)
|
||||
end
|
||||
|
||||
def confirmed_owner?
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
class ActivityPub::ProcessCollectionService < BaseService
|
||||
include JsonLdHelper
|
||||
|
||||
def call(body, account, **options)
|
||||
@account = account
|
||||
def call(body, actor, **options)
|
||||
@account = actor
|
||||
@json = original_json = Oj.load(body, mode: :strict)
|
||||
@options = options
|
||||
|
||||
|
@ -16,6 +16,7 @@ class ActivityPub::ProcessCollectionService < BaseService
|
|||
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
|
||||
|
@ -66,8 +67,10 @@ class ActivityPub::ProcessCollectionService < BaseService
|
|||
end
|
||||
|
||||
def verify_account!
|
||||
@options[:relayed_through_account] = @account
|
||||
@account = ActivityPub::LinkedDataSignature.new(@json).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 => e
|
||||
Rails.logger.debug "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}"
|
||||
nil
|
||||
|
|
|
@ -47,7 +47,7 @@ class FetchResourceService < BaseService
|
|||
body = response.body_with_limit
|
||||
json = body_to_json(body)
|
||||
|
||||
[json['id'], { prefetched_body: body, id: true }] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) || expected_type?(json))
|
||||
[json['id'], { prefetched_body: body, id: true }] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES) || expected_type?(json))
|
||||
elsif !terminal
|
||||
link_header = response['Link'] && parse_link_header(response)
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ class Keys::ClaimService < BaseService
|
|||
|
||||
def build_post_request(uri)
|
||||
Request.new(:post, uri).tap do |request|
|
||||
request.on_behalf_of(@source_account, :uri)
|
||||
request.on_behalf_of(@source_account)
|
||||
request.add_headers(HEADERS)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,8 +20,8 @@ class ResolveURLService < BaseService
|
|||
private
|
||||
|
||||
def process_url
|
||||
if equals_or_includes_any?(type, ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
|
||||
ActivityPub::FetchRemoteAccountService.new.call(resource_url, prefetched_body: body)
|
||||
if equals_or_includes_any?(type, ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES)
|
||||
ActivityPub::FetchRemoteActorService.new.call(resource_url, prefetched_body: body)
|
||||
elsif equals_or_includes_any?(type, ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES)
|
||||
status = FetchRemoteStatusService.new.call(resource_url, body)
|
||||
authorize_with @on_behalf_of, status, :show? unless status.nil?
|
||||
|
|
|
@ -37,7 +37,7 @@ class ActivityPub::DeliveryWorker
|
|||
|
||||
def build_request(http_client)
|
||||
Request.new(:post, @inbox_url, body: @json, http_client: http_client).tap do |request|
|
||||
request.on_behalf_of(@source_account, :uri, sign_with: @options[:sign_with])
|
||||
request.on_behalf_of(@source_account, sign_with: @options[:sign_with])
|
||||
request.add_headers(HEADERS)
|
||||
request.add_headers({ 'Collection-Synchronization' => synchronization_header }) if ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] != 'true' && @options[:synchronize_followers]
|
||||
end
|
||||
|
|
|
@ -5,11 +5,15 @@ class ActivityPub::ProcessingWorker
|
|||
|
||||
sidekiq_options backtrace: true, retry: 8
|
||||
|
||||
def perform(account_id, body, delivered_to_account_id = nil)
|
||||
account = Account.find_by(id: account_id)
|
||||
return if account.nil?
|
||||
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
|
||||
|
||||
ActivityPub::ProcessCollectionService.new.call(body, account, override_timestamps: true, delivered_to_account_id: delivered_to_account_id, delivery: true)
|
||||
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
|
||||
|
|
|
@ -420,7 +420,7 @@ RSpec.describe AccountsController, type: :controller do
|
|||
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:signed_request_account).and_return(remote_account)
|
||||
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
|
||||
get :show, params: { username: account.username, format: format }
|
||||
end
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
|
|||
end
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:signed_request_account).and_return(remote_account)
|
||||
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
|
||||
|
||||
Fabricate(:status_pin, account: account)
|
||||
Fabricate(:status_pin, account: account)
|
||||
|
|
|
@ -15,7 +15,7 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController, type: :controll
|
|||
end
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:signed_request_account).and_return(remote_account)
|
||||
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
|
|
|
@ -6,7 +6,7 @@ RSpec.describe ActivityPub::InboxesController, type: :controller do
|
|||
let(:remote_account) { nil }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:signed_request_account).and_return(remote_account)
|
||||
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
|
|
|
@ -28,7 +28,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
|
|||
end
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:signed_request_account).and_return(remote_account)
|
||||
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
|
|
|
@ -168,7 +168,7 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do
|
|||
|
||||
before do
|
||||
stub_const 'ActivityPub::RepliesController::DESCENDANTS_LIMIT', 5
|
||||
allow(controller).to receive(:signed_request_account).and_return(remote_querier)
|
||||
allow(controller).to receive(:signed_request_actor).and_return(remote_querier)
|
||||
|
||||
Fabricate(:status, thread: status, visibility: :public)
|
||||
Fabricate(:status, thread: status, visibility: :public)
|
||||
|
|
|
@ -3,6 +3,16 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ApplicationController, type: :controller do
|
||||
class WrappedActor
|
||||
attr_reader :wrapped_account
|
||||
|
||||
def initialize(wrapped_account)
|
||||
@wrapped_account = wrapped_account
|
||||
end
|
||||
|
||||
delegate :uri, :keypair, to: :wrapped_account
|
||||
end
|
||||
|
||||
controller do
|
||||
include SignatureVerification
|
||||
|
||||
|
@ -73,6 +83,41 @@ describe ApplicationController, type: :controller do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with a valid actor that is not an Account' do
|
||||
let(:actor) { WrappedActor.new(author) }
|
||||
|
||||
before do
|
||||
get :success
|
||||
|
||||
fake_request = Request.new(:get, request.url)
|
||||
fake_request.on_behalf_of(author)
|
||||
|
||||
request.headers.merge!(fake_request.headers)
|
||||
|
||||
allow(ActivityPub::TagManager.instance).to receive(:uri_to_actor).with(anything) do
|
||||
actor
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signed_request?' do
|
||||
it 'returns true' do
|
||||
expect(controller.signed_request?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signed_request_account' do
|
||||
it 'returns nil' do
|
||||
expect(controller.signed_request_account).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signed_request_actor' do
|
||||
it 'returns the expected actor' do
|
||||
expect(controller.signed_request_actor).to eq actor
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with request older than a day' do
|
||||
before do
|
||||
get :success
|
||||
|
|
|
@ -426,7 +426,7 @@ describe StatusesController do
|
|||
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:signed_request_account).and_return(remote_account)
|
||||
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
|
||||
end
|
||||
|
||||
context 'when account blocks account' do
|
||||
|
|
|
@ -115,7 +115,7 @@ RSpec.describe ActivityPub::Activity::Announce do
|
|||
|
||||
let(:object_json) { 'https://example.com/actor/hello-world' }
|
||||
|
||||
subject { described_class.new(json, sender, relayed_through_account: relay_account) }
|
||||
subject { described_class.new(json, sender, relayed_through_actor: relay_account) }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json))
|
||||
|
|
|
@ -4,10 +4,10 @@ RSpec.describe ActivityPub::Dereferencer do
|
|||
describe '#object' do
|
||||
let(:object) { { '@context': 'https://www.w3.org/ns/activitystreams', id: 'https://example.com/foo', type: 'Note', content: 'Hoge' } }
|
||||
let(:permitted_origin) { 'https://example.com' }
|
||||
let(:signature_account) { nil }
|
||||
let(:signature_actor) { nil }
|
||||
let(:uri) { nil }
|
||||
|
||||
subject { described_class.new(uri, permitted_origin: permitted_origin, signature_account: signature_account).object }
|
||||
subject { described_class.new(uri, permitted_origin: permitted_origin, signature_actor: signature_actor).object }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/foo').to_return(body: Oj.dump(object), headers: { 'Content-Type' => 'application/activity+json' })
|
||||
|
@ -21,7 +21,7 @@ RSpec.describe ActivityPub::Dereferencer do
|
|||
end
|
||||
|
||||
context 'with signature account' do
|
||||
let(:signature_account) { Fabricate(:account) }
|
||||
let(:signature_actor) { Fabricate(:account) }
|
||||
|
||||
it 'makes signed request' do
|
||||
subject
|
||||
|
@ -52,7 +52,7 @@ RSpec.describe ActivityPub::Dereferencer do
|
|||
end
|
||||
|
||||
context 'with signature account' do
|
||||
let(:signature_account) { Fabricate(:account) }
|
||||
let(:signature_actor) { Fabricate(:account) }
|
||||
|
||||
it 'makes signed request' do
|
||||
subject
|
||||
|
|
|
@ -20,7 +20,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
|
|||
stub_jsonld_contexts!
|
||||
end
|
||||
|
||||
describe '#verify_account!' do
|
||||
describe '#verify_actor!' do
|
||||
context 'when signature matches' do
|
||||
let(:raw_signature) do
|
||||
{
|
||||
|
@ -32,7 +32,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
|
|||
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
|
||||
|
||||
it 'returns creator' do
|
||||
expect(subject.verify_account!).to eq sender
|
||||
expect(subject.verify_actor!).to eq sender
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -40,7 +40,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
|
|||
let(:signature) { nil }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.verify_account!).to be_nil
|
||||
expect(subject.verify_actor!).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -55,7 +55,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
|
|||
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => 's69F3mfddd99dGjmvjdjjs81e12jn121Gkm1') }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.verify_account!).to be_nil
|
||||
expect(subject.verify_actor!).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -73,14 +73,14 @@ RSpec.describe ActivityPub::LinkedDataSignature do
|
|||
end
|
||||
|
||||
it 'can be verified again' do
|
||||
expect(described_class.new(subject).verify_account!).to eq sender
|
||||
expect(described_class.new(subject).verify_actor!).to eq sender
|
||||
end
|
||||
end
|
||||
|
||||
def sign(from_account, options, document)
|
||||
def sign(from_actor, options, document)
|
||||
options_hash = Digest::SHA256.hexdigest(canonicalize(options.merge('@context' => ActivityPub::LinkedDataSignature::CONTEXT)))
|
||||
document_hash = Digest::SHA256.hexdigest(canonicalize(document))
|
||||
to_be_verified = options_hash + document_hash
|
||||
Base64.strict_encode64(from_account.keypair.sign(OpenSSL::Digest.new('SHA256'), to_be_verified))
|
||||
Base64.strict_encode64(from_actor.keypair.sign(OpenSSL::Digest.new('SHA256'), to_be_verified))
|
||||
end
|
||||
end
|
||||
|
|
180
spec/services/activitypub/fetch_remote_actor_service_spec.rb
Normal file
180
spec/services/activitypub/fetch_remote_actor_service_spec.rb
Normal file
|
@ -0,0 +1,180 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
|
||||
subject { ActivityPub::FetchRemoteActorService.new }
|
||||
|
||||
let!(:actor) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'https://example.com/alice',
|
||||
type: 'Person',
|
||||
preferredUsername: 'alice',
|
||||
name: 'Alice',
|
||||
summary: 'Foo bar',
|
||||
inbox: 'http://example.com/alice/inbox',
|
||||
}
|
||||
end
|
||||
|
||||
describe '#call' do
|
||||
let(:account) { subject.call('https://example.com/alice', id: true) }
|
||||
|
||||
shared_examples 'sets profile data' do
|
||||
it 'returns an account' do
|
||||
expect(account).to be_an Account
|
||||
end
|
||||
|
||||
it 'sets display name' do
|
||||
expect(account.display_name).to eq 'Alice'
|
||||
end
|
||||
|
||||
it 'sets note' do
|
||||
expect(account.note).to eq 'Foo bar'
|
||||
end
|
||||
|
||||
it 'sets URL' do
|
||||
expect(account.url).to eq 'https://example.com/alice'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the account does not have a inbox' do
|
||||
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
||||
|
||||
before do
|
||||
actor[:inbox] = nil
|
||||
|
||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
end
|
||||
|
||||
it 'fetches resource' do
|
||||
account
|
||||
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'looks up webfinger' do
|
||||
account
|
||||
expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(account).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when URI and WebFinger share the same host' do
|
||||
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
end
|
||||
|
||||
it 'fetches resource' do
|
||||
account
|
||||
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'looks up webfinger' do
|
||||
account
|
||||
expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'sets username and domain from webfinger' do
|
||||
expect(account.username).to eq 'alice'
|
||||
expect(account.domain).to eq 'example.com'
|
||||
end
|
||||
|
||||
include_examples 'sets profile data'
|
||||
end
|
||||
|
||||
context 'when WebFinger presents different domain than URI' do
|
||||
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
end
|
||||
|
||||
it 'fetches resource' do
|
||||
account
|
||||
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'looks up webfinger' do
|
||||
account
|
||||
expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'looks up "redirected" webfinger' do
|
||||
account
|
||||
expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'sets username and domain from final webfinger' do
|
||||
expect(account.username).to eq 'alice'
|
||||
expect(account.domain).to eq 'iscool.af'
|
||||
end
|
||||
|
||||
include_examples 'sets profile data'
|
||||
end
|
||||
|
||||
context 'when WebFinger returns a different URI' do
|
||||
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
end
|
||||
|
||||
it 'fetches resource' do
|
||||
account
|
||||
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'looks up webfinger' do
|
||||
account
|
||||
expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'does not create account' do
|
||||
expect(account).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when WebFinger returns a different URI after a redirection' do
|
||||
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
end
|
||||
|
||||
it 'fetches resource' do
|
||||
account
|
||||
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'looks up webfinger' do
|
||||
account
|
||||
expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'looks up "redirected" webfinger' do
|
||||
account
|
||||
expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'does not create account' do
|
||||
expect(account).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with wrong id' do
|
||||
it 'does not create account' do
|
||||
expect(subject.call('https://fake.address/@foo', prefetched_body: Oj.dump(actor))).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -68,7 +68,7 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
|
|||
let(:forwarder) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/other_account') }
|
||||
|
||||
it 'does not process payload if no signature exists' do
|
||||
expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(nil)
|
||||
expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(nil)
|
||||
expect(ActivityPub::Activity).not_to receive(:factory)
|
||||
|
||||
subject.call(json, forwarder)
|
||||
|
@ -77,7 +77,7 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
|
|||
it 'processes payload with actor if valid signature exists' do
|
||||
payload['signature'] = { 'type' => 'RsaSignature2017' }
|
||||
|
||||
expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(actor)
|
||||
expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(actor)
|
||||
expect(ActivityPub::Activity).to receive(:factory).with(instance_of(Hash), actor, instance_of(Hash))
|
||||
|
||||
subject.call(json, forwarder)
|
||||
|
@ -86,7 +86,7 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
|
|||
it 'does not process payload if invalid signature exists' do
|
||||
payload['signature'] = { 'type' => 'RsaSignature2017' }
|
||||
|
||||
expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(nil)
|
||||
expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(nil)
|
||||
expect(ActivityPub::Activity).not_to receive(:factory)
|
||||
|
||||
subject.call(json, forwarder)
|
||||
|
|
|
@ -66,7 +66,7 @@ RSpec.describe FetchResourceService, type: :service do
|
|||
|
||||
it 'signs request' do
|
||||
subject
|
||||
expect(a_request(:get, url).with(headers: { 'Signature' => /keyId="#{Regexp.escape(ActivityPub::TagManager.instance.uri_for(Account.representative) + '#main-key')}"/ })).to have_been_made
|
||||
expect(a_request(:get, url).with(headers: { 'Signature' => /keyId="#{Regexp.escape(ActivityPub::TagManager.instance.key_uri_for(Account.representative))}"/ })).to have_been_made
|
||||
end
|
||||
|
||||
context 'when content type is application/atom+xml' do
|
||||
|
|
Loading…
Reference in a new issue