|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
class ResolveRemoteAccountService < BaseService
|
|
|
|
include OStatus2::MagicKey
|
|
|
|
include JsonLdHelper
|
|
|
|
|
|
|
|
DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0'
|
|
|
|
|
|
|
|
# Find or create a local account for a remote user.
|
|
|
|
# When creating, look up the user's webfinger and fetch all
|
|
|
|
# important information from their feed
|
|
|
|
# @param [String] uri User URI in the form of username@domain
|
|
|
|
# @return [Account]
|
|
|
|
def call(uri, update_profile = true, redirected = nil)
|
|
|
|
@username, @domain = uri.split('@')
|
|
|
|
@update_profile = update_profile
|
|
|
|
|
|
|
|
return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
|
|
|
|
|
|
|
|
@account = Account.find_remote(@username, @domain)
|
|
|
|
|
|
|
|
return @account unless webfinger_update_due?
|
|
|
|
|
|
|
|
Rails.logger.debug "Looking up webfinger for #{uri}"
|
|
|
|
|
|
|
|
@webfinger = Goldfinger.finger("acct:#{uri}")
|
|
|
|
|
|
|
|
confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@')
|
|
|
|
|
|
|
|
if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
|
|
|
|
@username = confirmed_username
|
|
|
|
@domain = confirmed_domain
|
|
|
|
elsif redirected.nil?
|
|
|
|
return call("#{confirmed_username}@#{confirmed_domain}", update_profile, true)
|
|
|
|
else
|
|
|
|
Rails.logger.debug 'Requested and returned acct URIs do not match'
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
return if links_missing?
|
|
|
|
return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
|
|
|
|
|
|
|
|
RedisLock.acquire(lock_options) do |lock|
|
|
|
|
if lock.acquired?
|
|
|
|
@account = Account.find_remote(@username, @domain)
|
|
|
|
|
|
|
|
if activitypub_ready?
|
|
|
|
handle_activitypub
|
|
|
|
else
|
|
|
|
handle_ostatus
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
@account
|
|
|
|
rescue Goldfinger::Error => e
|
|
|
|
Rails.logger.debug "Webfinger query for #{uri} unsuccessful: #{e}"
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def links_missing?
|
|
|
|
!(activitypub_ready? || ostatus_ready?)
|
|
|
|
end
|
|
|
|
|
|
|
|
def ostatus_ready?
|
|
|
|
!(@webfinger.link('http://schemas.google.com/g/2010#updates-from').nil? ||
|
|
|
|
@webfinger.link('salmon').nil? ||
|
|
|
|
@webfinger.link('http://webfinger.net/rel/profile-page').nil? ||
|
|
|
|
@webfinger.link('magic-public-key').nil? ||
|
|
|
|
canonical_uri.nil? ||
|
|
|
|
hub_url.nil?)
|
|
|
|
end
|
|
|
|
|
|
|
|
def webfinger_update_due?
|
|
|
|
@account.nil? || @account.last_webfingered_at.nil? || @account.last_webfingered_at <= 1.day.ago
|
|
|
|
end
|
|
|
|
|
|
|
|
def activitypub_ready?
|
|
|
|
!@webfinger.link('self').nil? &&
|
|
|
|
['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type) &&
|
|
|
|
actor_json['inbox'].present?
|
|
|
|
end
|
|
|
|
|
|
|
|
def handle_ostatus
|
|
|
|
create_account if @account.nil?
|
[WiP] Whenever a remote keypair changes, unfollow them and re-subscribe to … (#4907)
* Whenever a remote keypair changes, unfollow them and re-subscribe to them
In Mastodon (it could be different for other OStatus or AP-enabled software),
a keypair change is indicative of whole user (or instance) data loss. In this
situation, the “new” user might be different, and almost certainly has an empty
followers list. In this case, Mastodon instances will disagree on follower
lists, leading to unreliable delivery and “shadow followers”, that is users
believed by a remote instance to be followers, without the affected user
knowing.
Drawbacks of this change are:
1. If an user legitimately changes public key for some reason without losing
data (not possible in Mastodon at the moment), they will have their remote
followers unsubscribed/re-subscribed needlessly.
2. Depending of the number of remote followers, this may generate quite some
traffic.
3. If the user change is an attempt at usurpation, the remote followers will
unknowingly follow the usurper. Note that this is *not* a change of
behavior, Mastodon already behaves like that, although delivery might be
unreliable, and the usurper would not have known the former user's
followers.
* Rename ResubscribeWorker to RefollowWorker
* Process followers in batches
7 years ago
|
|
|
old_public_key = @account.public_key
|
|
|
|
update_account
|
|
|
|
update_account_profile if update_profile?
|
[WiP] Whenever a remote keypair changes, unfollow them and re-subscribe to … (#4907)
* Whenever a remote keypair changes, unfollow them and re-subscribe to them
In Mastodon (it could be different for other OStatus or AP-enabled software),
a keypair change is indicative of whole user (or instance) data loss. In this
situation, the “new” user might be different, and almost certainly has an empty
followers list. In this case, Mastodon instances will disagree on follower
lists, leading to unreliable delivery and “shadow followers”, that is users
believed by a remote instance to be followers, without the affected user
knowing.
Drawbacks of this change are:
1. If an user legitimately changes public key for some reason without losing
data (not possible in Mastodon at the moment), they will have their remote
followers unsubscribed/re-subscribed needlessly.
2. Depending of the number of remote followers, this may generate quite some
traffic.
3. If the user change is an attempt at usurpation, the remote followers will
unknowingly follow the usurper. Note that this is *not* a change of
behavior, Mastodon already behaves like that, although delivery might be
unreliable, and the usurper would not have known the former user's
followers.
* Rename ResubscribeWorker to RefollowWorker
* Process followers in batches
7 years ago
|
|
|
RefollowWorker.perform_async(@account.id) if old_public_key != @account.public_key
|
|
|
|
end
|
|
|
|
|
|
|
|
def update_profile?
|
|
|
|
@update_profile
|
|
|
|
end
|
|
|
|
|
|
|
|
def handle_activitypub
|
|
|
|
return if actor_json.nil?
|
|
|
|
|
|
|
|
@account = ActivityPub::ProcessAccountService.new.call(@username, @domain, actor_json)
|
|
|
|
rescue Oj::ParseError
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
|
|
|
def create_account
|
|
|
|
Rails.logger.debug "Creating new remote account for #{@username}@#{@domain}"
|
|
|
|
|
|
|
|
@account = Account.new(username: @username, domain: @domain)
|
|
|
|
@account.suspended = true if auto_suspend?
|
|
|
|
@account.silenced = true if auto_silence?
|
|
|
|
@account.private_key = nil
|
|
|
|
end
|
|
|
|
|
|
|
|
def update_account
|
|
|
|
@account.last_webfingered_at = Time.now.utc
|
|
|
|
@account.protocol = :ostatus
|
|
|
|
@account.remote_url = atom_url
|
|
|
|
@account.salmon_url = salmon_url
|
|
|
|
@account.url = url
|
|
|
|
@account.public_key = public_key
|
|
|
|
@account.uri = canonical_uri
|
|
|
|
@account.hub_url = hub_url
|
|
|
|
@account.save!
|
|
|
|
end
|
|
|
|
|
|
|
|
def auto_suspend?
|
|
|
|
domain_block && domain_block.suspend?
|
|
|
|
end
|
|
|
|
|
|
|
|
def auto_silence?
|
|
|
|
domain_block && domain_block.silence?
|
|
|
|
end
|
|
|
|
|
|
|
|
def domain_block
|
|
|
|
return @domain_block if defined?(@domain_block)
|
|
|
|
@domain_block = DomainBlock.find_by(domain: @domain)
|
|
|
|
end
|
|
|
|
|
|
|
|
def atom_url
|
|
|
|
@atom_url ||= @webfinger.link('http://schemas.google.com/g/2010#updates-from').href
|
|
|
|
end
|
|
|
|
|
|
|
|
def salmon_url
|
|
|
|
@salmon_url ||= @webfinger.link('salmon').href
|
|
|
|
end
|
|
|
|
|
|
|
|
def actor_url
|
|
|
|
@actor_url ||= @webfinger.link('self').href
|
|
|
|
end
|
|
|
|
|
|
|
|
def url
|
|
|
|
@url ||= @webfinger.link('http://webfinger.net/rel/profile-page').href
|
|
|
|
end
|
|
|
|
|
|
|
|
def public_key
|
|
|
|
@public_key ||= magic_key_to_pem(@webfinger.link('magic-public-key').href)
|
|
|
|
end
|
|
|
|
|
|
|
|
def canonical_uri
|
|
|
|
return @canonical_uri if defined?(@canonical_uri)
|
|
|
|
|
|
|
|
author_uri = atom.at_xpath('/xmlns:feed/xmlns:author/xmlns:uri')
|
|
|
|
|
|
|
|
if author_uri.nil?
|
|
|
|
owner = atom.at_xpath('/xmlns:feed').at_xpath('./dfrn:owner', dfrn: DFRN_NS)
|
|
|
|
author_uri = owner.at_xpath('./xmlns:uri') unless owner.nil?
|
|
|
|
end
|
|
|
|
|
|
|
|
@canonical_uri = author_uri.nil? ? nil : author_uri.content
|
|
|
|
end
|
|
|
|
|
|
|
|
def hub_url
|
|
|
|
return @hub_url if defined?(@hub_url)
|
|
|
|
|
|
|
|
hubs = atom.xpath('//xmlns:link[@rel="hub"]')
|
|
|
|
@hub_url = hubs.empty? || hubs.first['href'].nil? ? nil : hubs.first['href']
|
|
|
|
end
|
|
|
|
|
|
|
|
def atom_body
|
|
|
|
return @atom_body if defined?(@atom_body)
|
|
|
|
|
|
|
|
response = Request.new(:get, atom_url).perform
|
|
|
|
|
|
|
|
raise Mastodon::UnexpectedResponseError, response unless response.code == 200
|
|
|
|
|
|
|
|
@atom_body = response.to_s
|
|
|
|
end
|
|
|
|
|
|
|
|
def actor_json
|
|
|
|
return @actor_json if defined?(@actor_json)
|
|
|
|
|
|
|
|
json = fetch_resource(actor_url)
|
|
|
|
@actor_json = supported_context?(json) && json['type'] == 'Person' ? json : nil
|
|
|
|
end
|
|
|
|
|
|
|
|
def atom
|
|
|
|
return @atom if defined?(@atom)
|
|
|
|
@atom = Nokogiri::XML(atom_body)
|
|
|
|
end
|
|
|
|
|
|
|
|
def update_account_profile
|
|
|
|
RemoteProfileUpdateWorker.perform_async(@account.id, atom_body.force_encoding('UTF-8'), false)
|
|
|
|
end
|
|
|
|
|
|
|
|
def lock_options
|
|
|
|
{ redis: Redis.current, key: "resolve:#{@username}@#{@domain}" }
|
|
|
|
end
|
|
|
|
end
|