Refactor domain block checks (#11268)
This commit is contained in:
		| @@ -5,6 +5,8 @@ | |||||||
| module SignatureVerification | module SignatureVerification | ||||||
|   extend ActiveSupport::Concern |   extend ActiveSupport::Concern | ||||||
|  |  | ||||||
|  |   include DomainControlHelper | ||||||
|  |  | ||||||
|   def signed_request? |   def signed_request? | ||||||
|     request.headers['Signature'].present? |     request.headers['Signature'].present? | ||||||
|   end |   end | ||||||
| @@ -126,6 +128,8 @@ module SignatureVerification | |||||||
|     if key_id.start_with?('acct:') |     if key_id.start_with?('acct:') | ||||||
|       stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) } |       stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) } | ||||||
|     elsif !ActivityPub::TagManager.instance.local_uri?(key_id) |     elsif !ActivityPub::TagManager.instance.local_uri?(key_id) | ||||||
|  |       return if domain_not_allowed?(key_id) | ||||||
|  |  | ||||||
|       account   = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account) |       account   = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account) | ||||||
|       account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) } |       account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) } | ||||||
|       account |       account | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								app/helpers/domain_control_helper.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/helpers/domain_control_helper.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  |  | ||||||
|  | module DomainControlHelper | ||||||
|  |   def domain_not_allowed?(uri_or_domain) | ||||||
|  |     return if uri_or_domain.blank? | ||||||
|  |  | ||||||
|  |     domain = begin | ||||||
|  |       if uri_or_domain.include?('://') | ||||||
|  |         Addressable::URI.parse(uri_or_domain).domain | ||||||
|  |       else | ||||||
|  |         uri_or_domain | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     DomainBlock.blocked?(domain) | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -24,13 +24,16 @@ class TagManager | |||||||
|  |  | ||||||
|   def same_acct?(canonical, needle) |   def same_acct?(canonical, needle) | ||||||
|     return true if canonical.casecmp(needle).zero? |     return true if canonical.casecmp(needle).zero? | ||||||
|  |  | ||||||
|     username, domain = needle.split('@') |     username, domain = needle.split('@') | ||||||
|  |  | ||||||
|     local_domain?(domain) && canonical.casecmp(username).zero? |     local_domain?(domain) && canonical.casecmp(username).zero? | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def local_url?(url) |   def local_url?(url) | ||||||
|     uri    = Addressable::URI.parse(url).normalize |     uri    = Addressable::URI.parse(url).normalize | ||||||
|     domain = uri.host + (uri.port ? ":#{uri.port}" : '') |     domain = uri.host + (uri.port ? ":#{uri.port}" : '') | ||||||
|  |  | ||||||
|     TagManager.instance.web_domain?(domain) |     TagManager.instance.web_domain?(domain) | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -4,13 +4,12 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService | |||||||
|   include JsonLdHelper |   include JsonLdHelper | ||||||
|  |  | ||||||
|   def call(account) |   def call(account) | ||||||
|     return if account.featured_collection_url.blank? |     return if account.featured_collection_url.blank? || account.suspended? || account.local? | ||||||
|  |  | ||||||
|     @account = account |     @account = account | ||||||
|     @json    = fetch_resource(@account.featured_collection_url, true) |     @json    = fetch_resource(@account.featured_collection_url, true) | ||||||
|  |  | ||||||
|     return unless supported_context? |     return unless supported_context? | ||||||
|     return if @account.suspended? || @account.local? |  | ||||||
|  |  | ||||||
|     case @json['type'] |     case @json['type'] | ||||||
|     when 'Collection', 'CollectionPage' |     when 'Collection', 'CollectionPage' | ||||||
|   | |||||||
| @@ -2,18 +2,22 @@ | |||||||
|  |  | ||||||
| class ActivityPub::FetchRemoteAccountService < BaseService | class ActivityPub::FetchRemoteAccountService < BaseService | ||||||
|   include JsonLdHelper |   include JsonLdHelper | ||||||
|  |   include DomainControlHelper | ||||||
|  |  | ||||||
|   SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze |   SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze | ||||||
|  |  | ||||||
|   # Does a WebFinger roundtrip on each call, unless `only_key` is true |   # 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) | ||||||
|  |     return if domain_not_allowed?(uri) | ||||||
|     return ActivityPub::TagManager.instance.uri_to_resource(uri, Account) if ActivityPub::TagManager.instance.local_uri?(uri) |     return ActivityPub::TagManager.instance.uri_to_resource(uri, Account) if ActivityPub::TagManager.instance.local_uri?(uri) | ||||||
|  |  | ||||||
|     @json = if prefetched_body.nil? |     @json = begin | ||||||
|               fetch_resource(uri, id) |       if prefetched_body.nil? | ||||||
|             else |         fetch_resource(uri, id) | ||||||
|               body_to_json(prefetched_body, compare_id: id ? uri : nil) |       else | ||||||
|             end |         body_to_json(prefetched_body, compare_id: id ? uri : nil) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|     return if !supported_context? || !expected_type? || (break_on_redirect && @json['movedTo'].present?) |     return if !supported_context? || !expected_type? || (break_on_redirect && @json['movedTo'].present?) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,7 +5,9 @@ class ActivityPub::FetchRemotePollService < BaseService | |||||||
|  |  | ||||||
|   def call(poll, on_behalf_of = nil) |   def call(poll, on_behalf_of = nil) | ||||||
|     json = fetch_resource(poll.status.uri, true, on_behalf_of) |     json = fetch_resource(poll.status.uri, true, on_behalf_of) | ||||||
|  |  | ||||||
|     return unless supported_context?(json) |     return unless supported_context?(json) | ||||||
|  |  | ||||||
|     ActivityPub::ProcessPollService.new.call(poll, json) |     ActivityPub::ProcessPollService.new.call(poll, json) | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -2,11 +2,12 @@ | |||||||
|  |  | ||||||
| class ActivityPub::ProcessAccountService < BaseService | class ActivityPub::ProcessAccountService < BaseService | ||||||
|   include JsonLdHelper |   include JsonLdHelper | ||||||
|  |   include DomainControlHelper | ||||||
|  |  | ||||||
|   # Should be called with confirmed valid JSON |   # Should be called with confirmed valid JSON | ||||||
|   # and WebFinger-resolved username and domain |   # and WebFinger-resolved username and domain | ||||||
|   def call(username, domain, json, options = {}) |   def call(username, domain, json, options = {}) | ||||||
|     return if json['inbox'].blank? || unsupported_uri_scheme?(json['id']) |     return if json['inbox'].blank? || unsupported_uri_scheme?(json['id']) || domain_not_allowed?(domain) | ||||||
|  |  | ||||||
|     @options     = options |     @options     = options | ||||||
|     @json        = json |     @json        = json | ||||||
| @@ -15,8 +16,6 @@ class ActivityPub::ProcessAccountService < BaseService | |||||||
|     @domain      = domain |     @domain      = domain | ||||||
|     @collections = {} |     @collections = {} | ||||||
|  |  | ||||||
|     return if auto_suspend? |  | ||||||
|  |  | ||||||
|     RedisLock.acquire(lock_options) do |lock| |     RedisLock.acquire(lock_options) do |lock| | ||||||
|       if lock.acquired? |       if lock.acquired? | ||||||
|         @account        = Account.find_remote(@username, @domain) |         @account        = Account.find_remote(@username, @domain) | ||||||
|   | |||||||
| @@ -8,9 +8,7 @@ class ActivityPub::ProcessCollectionService < BaseService | |||||||
|     @json    = Oj.load(body, mode: :strict) |     @json    = Oj.load(body, mode: :strict) | ||||||
|     @options = options |     @options = options | ||||||
|  |  | ||||||
|     return unless supported_context? |     return if !supported_context? || (different_actor? && verify_account!.nil?) || @account.suspended? || @account.local? | ||||||
|     return if different_actor? && verify_account!.nil? |  | ||||||
|     return if @account.suspended? || @account.local? |  | ||||||
|  |  | ||||||
|     case @json['type'] |     case @json['type'] | ||||||
|     when 'Collection', 'CollectionPage' |     when 'Collection', 'CollectionPage' | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ class ActivityPub::ProcessPollService < BaseService | |||||||
|  |  | ||||||
|   def call(poll, json) |   def call(poll, json) | ||||||
|     @json = json |     @json = json | ||||||
|  |  | ||||||
|     return unless expected_type? |     return unless expected_type? | ||||||
|  |  | ||||||
|     previous_expires_at = poll.expires_at |     previous_expires_at = poll.expires_at | ||||||
|   | |||||||
| @@ -1,75 +1,108 @@ | |||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
|  |  | ||||||
| require_relative '../models/account' |  | ||||||
|  |  | ||||||
| class ResolveAccountService < BaseService | class ResolveAccountService < BaseService | ||||||
|   include JsonLdHelper |   include JsonLdHelper | ||||||
|  |   include DomainControlHelper | ||||||
|  |  | ||||||
|   # Find or create a local account for a remote user. |   class WebfingerRedirectError < StandardError; end | ||||||
|   # When creating, look up the user's webfinger and fetch all |  | ||||||
|   # important information from their feed |   # Find or create an account record for a remote user. When creating, | ||||||
|   # @param [String, Account] uri User URI in the form of username@domain |   # 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 |   # @param [Hash] options | ||||||
|  |   # @option options [Boolean] :redirected Do not follow further Webfinger redirects | ||||||
|  |   # @option options [Boolean] :skip_webfinger Do not attempt to refresh account data | ||||||
|   # @return [Account] |   # @return [Account] | ||||||
|   def call(uri, options = {}) |   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? || !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! | ||||||
|  |  | ||||||
|  |     # 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) | ||||||
|  |  | ||||||
|  |     return @account if @account&.local? || !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 | ||||||
|  |  | ||||||
|  |     process_account! | ||||||
|  |   rescue Goldfinger::Error, WebfingerRedirectError, Oj::ParseError => e | ||||||
|  |     Rails.logger.debug "Webfinger query for #{@uri} failed: #{e}" | ||||||
|  |     nil | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def process_options!(uri, options) | ||||||
|     @options = options |     @options = options | ||||||
|  |  | ||||||
|     if uri.is_a?(Account) |     if uri.is_a?(Account) | ||||||
|       @account  = uri |       @account  = uri | ||||||
|       @username = @account.username |       @username = @account.username | ||||||
|       @domain   = @account.domain |       @domain   = @account.domain | ||||||
|       uri       = "#{@username}@#{@domain}" |       @uri      = [@username, @domain].compact.join('@') | ||||||
|  |  | ||||||
|       return @account if @account.local? || !webfinger_update_due? |  | ||||||
|     else |     else | ||||||
|  |       @uri               = uri | ||||||
|       @username, @domain = uri.split('@') |       @username, @domain = uri.split('@') | ||||||
|  |  | ||||||
|       return Account.find_local(@username) if TagManager.instance.local_domain?(@domain) |  | ||||||
|  |  | ||||||
|       @account = Account.find_remote(@username, @domain) |  | ||||||
|  |  | ||||||
|       return @account unless webfinger_update_due? |  | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     Rails.logger.debug "Looking up webfinger for #{uri}" |     @domain = nil if TagManager.instance.local_domain?(@domain) | ||||||
|  |   end | ||||||
|     @webfinger = Goldfinger.finger("acct:#{uri}") |  | ||||||
|  |  | ||||||
|  |   def process_webfinger! | ||||||
|  |     @webfinger                           = Goldfinger.finger("acct:#{@uri}") | ||||||
|     confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@') |     confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@') | ||||||
|  |  | ||||||
|     if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero? |     if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero? | ||||||
|       @username = confirmed_username |       @username = confirmed_username | ||||||
|       @domain   = confirmed_domain |       @domain   = confirmed_domain | ||||||
|     elsif options[:redirected].nil? |     elsif @options[:redirected].nil? | ||||||
|       return call("#{confirmed_username}@#{confirmed_domain}", options.merge(redirected: true)) |       @account = ResolveAccountService.new.call("#{confirmed_username}@#{confirmed_domain}", @options.merge(redirected: true)) | ||||||
|     else |     else | ||||||
|       Rails.logger.debug 'Requested and returned acct URIs do not match' |       raise WebfingerRedirectError, "The URI #{uri} tries to hijack #{@username}@#{@domain}" | ||||||
|       return |  | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     return Account.find_local(@username) if TagManager.instance.local_domain?(@domain) |     @domain = nil if TagManager.instance.local_domain?(@domain) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def process_account! | ||||||
|     return unless activitypub_ready? |     return unless activitypub_ready? | ||||||
|  |  | ||||||
|     RedisLock.acquire(lock_options) do |lock| |     RedisLock.acquire(lock_options) do |lock| | ||||||
|       if lock.acquired? |       if lock.acquired? | ||||||
|         @account = Account.find_remote(@username, @domain) |         @account = Account.find_remote(@username, @domain) | ||||||
|  |  | ||||||
|         next unless @account.nil? || @account.activitypub? |         next if (@account.present? && !@account.activitypub?) || actor_json.nil? | ||||||
|  |  | ||||||
|         handle_activitypub |         @account = ActivityPub::ProcessAccountService.new.call(@username, @domain, actor_json) | ||||||
|       else |       else | ||||||
|         raise Mastodon::RaceConditionError |         raise Mastodon::RaceConditionError | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     @account |     @account | ||||||
|   rescue Goldfinger::Error => e |  | ||||||
|     Rails.logger.debug "Webfinger query for #{uri} unsuccessful: #{e}" |  | ||||||
|     nil |  | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   private |  | ||||||
|  |  | ||||||
|   def webfinger_update_due? |   def webfinger_update_due? | ||||||
|     @account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?) |     @account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?) | ||||||
|   end |   end | ||||||
| @@ -78,14 +111,6 @@ class ResolveAccountService < BaseService | |||||||
|     !@webfinger.link('self').nil? && ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type) |     !@webfinger.link('self').nil? && ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def handle_activitypub |  | ||||||
|     return if actor_json.nil? |  | ||||||
|  |  | ||||||
|     @account = ActivityPub::ProcessAccountService.new.call(@username, @domain, actor_json) |  | ||||||
|   rescue Oj::ParseError |  | ||||||
|     nil |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def actor_url |   def actor_url | ||||||
|     @actor_url ||= @webfinger.link('self').href |     @actor_url ||= @webfinger.link('self').href | ||||||
|   end |   end | ||||||
|   | |||||||
| @@ -53,6 +53,11 @@ RSpec.describe ResolveAccountService, type: :service do | |||||||
|     fail_occurred  = false |     fail_occurred  = false | ||||||
|     return_values  = Concurrent::Array.new |     return_values  = Concurrent::Array.new | ||||||
|  |  | ||||||
|  |     # Preload classes that throw circular dependency errors in threads | ||||||
|  |     Account | ||||||
|  |     TagManager | ||||||
|  |     DomainBlock | ||||||
|  |  | ||||||
|     threads = Array.new(5) do |     threads = Array.new(5) do | ||||||
|       Thread.new do |       Thread.new do | ||||||
|         true while wait_for_start |         true while wait_for_start | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user