You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

137 lines
4.2 KiB

8 years ago
8 years ago
  1. # frozen_string_literal: true
  2. class ResolveAccountService < BaseService
  3. include JsonLdHelper
  4. include DomainControlHelper
  5. include WebfingerHelper
  6. class WebfingerRedirectError < StandardError; end
  7. # Find or create an account record for a remote user. When creating,
  8. # look up the user's webfinger and fetch ActivityPub data
  9. # @param [String, Account] uri URI in the username@domain format or account record
  10. # @param [Hash] options
  11. # @option options [Boolean] :redirected Do not follow further Webfinger redirects
  12. # @option options [Boolean] :skip_webfinger Do not attempt to refresh account data
  13. # @return [Account]
  14. def call(uri, options = {})
  15. return if uri.blank?
  16. process_options!(uri, options)
  17. # First of all we want to check if we've got the account
  18. # record with the URI already, and if so, we can exit early
  19. return if domain_not_allowed?(@domain)
  20. @account ||= Account.find_remote(@username, @domain)
  21. return @account if @account&.local? || @domain.nil? || !webfinger_update_due?
  22. # At this point we are in need of a Webfinger query, which may
  23. # yield us a different username/domain through a redirect
  24. process_webfinger!(@uri)
  25. # Because the username/domain pair may be different than what
  26. # we already checked, we need to check if we've already got
  27. # the record with that URI, again
  28. return if domain_not_allowed?(@domain)
  29. @account ||= Account.find_remote(@username, @domain)
  30. return @account if @account&.local? || !webfinger_update_due?
  31. # Now it is certain, it is definitely a remote account, and it
  32. # either needs to be created, or updated from fresh data
  33. process_account!
  34. rescue Webfinger::Error, WebfingerRedirectError, Oj::ParseError => e
  35. Rails.logger.debug "Webfinger query for #{@uri} failed: #{e}"
  36. nil
  37. end
  38. private
  39. def process_options!(uri, options)
  40. @options = options
  41. if uri.is_a?(Account)
  42. @account = uri
  43. @username = @account.username
  44. @domain = @account.domain
  45. else
  46. @username, @domain = uri.split('@')
  47. end
  48. @domain = begin
  49. if TagManager.instance.local_domain?(@domain)
  50. nil
  51. else
  52. TagManager.instance.normalize_domain(@domain)
  53. end
  54. end
  55. @uri = [@username, @domain].compact.join('@')
  56. end
  57. def process_webfinger!(uri, redirected = false)
  58. @webfinger = webfinger!("acct:#{uri}")
  59. confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@')
  60. if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
  61. @username = confirmed_username
  62. @domain = confirmed_domain
  63. @uri = uri
  64. elsif !redirected
  65. return process_webfinger!("#{confirmed_username}@#{confirmed_domain}", true)
  66. else
  67. raise WebfingerRedirectError, "The URI #{uri} tries to hijack #{@username}@#{@domain}"
  68. end
  69. @domain = nil if TagManager.instance.local_domain?(@domain)
  70. end
  71. def process_account!
  72. return unless activitypub_ready?
  73. RedisLock.acquire(lock_options) do |lock|
  74. if lock.acquired?
  75. @account = Account.find_remote(@username, @domain)
  76. next if actor_json.nil?
  77. @account = ActivityPub::ProcessAccountService.new.call(@username, @domain, actor_json)
  78. else
  79. raise Mastodon::RaceConditionError
  80. end
  81. end
  82. @account
  83. end
  84. def webfinger_update_due?
  85. return false if @options[:check_delivery_availability] && !DeliveryFailureTracker.available?(@domain)
  86. @account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?)
  87. end
  88. def activitypub_ready?
  89. ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self', 'type'))
  90. end
  91. def actor_url
  92. @actor_url ||= @webfinger.link('self', 'href')
  93. end
  94. def actor_json
  95. return @actor_json if defined?(@actor_json)
  96. json = fetch_resource(actor_url, false)
  97. @actor_json = supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) ? json : nil
  98. end
  99. def lock_options
  100. { redis: Redis.current, key: "resolve:#{@username}@#{@domain}" }
  101. end
  102. end