about summary refs log tree commit diff
path: root/app/services/activitypub
diff options
context:
space:
mode:
authorDavid Yip <yipdw@member.fsf.org>2017-09-09 14:27:47 -0500
committerDavid Yip <yipdw@member.fsf.org>2017-09-09 14:27:47 -0500
commitb9f7bc149b2a6abfbdaee83e6992b617b8bdb18e (patch)
tree355225f4424a6ea1b40c66c5540ccab42096e3bf /app/services/activitypub
parente18ed4bbc7ab4e258d05a3e2a5db0790f67a8f37 (diff)
parent5d170587e3b6c1a3b3ebe0910b62a4c526e2900d (diff)
Merge branch 'origin/master' into sync/upstream
 Conflicts:
	app/javascript/mastodon/components/status_list.js
	app/javascript/mastodon/features/notifications/index.js
	app/javascript/mastodon/features/ui/components/modal_root.js
	app/javascript/mastodon/features/ui/components/onboarding_modal.js
	app/javascript/mastodon/features/ui/index.js
	app/javascript/styles/about.scss
	app/javascript/styles/accounts.scss
	app/javascript/styles/components.scss
	app/presenters/instance_presenter.rb
	app/services/post_status_service.rb
	app/services/reblog_service.rb
	app/views/about/more.html.haml
	app/views/about/show.html.haml
	app/views/accounts/_header.html.haml
	config/webpack/loaders/babel.js
	spec/controllers/api/v1/accounts/credentials_controller_spec.rb
Diffstat (limited to 'app/services/activitypub')
-rw-r--r--app/services/activitypub/fetch_remote_account_service.rb57
-rw-r--r--app/services/activitypub/fetch_remote_key_service.rb47
-rw-r--r--app/services/activitypub/fetch_remote_status_service.rb48
-rw-r--r--app/services/activitypub/process_account_service.rb93
-rw-r--r--app/services/activitypub/process_collection_service.rb48
5 files changed, 293 insertions, 0 deletions
diff --git a/app/services/activitypub/fetch_remote_account_service.rb b/app/services/activitypub/fetch_remote_account_service.rb
new file mode 100644
index 000000000..3eeca585e
--- /dev/null
+++ b/app/services/activitypub/fetch_remote_account_service.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+class ActivityPub::FetchRemoteAccountService < BaseService
+  include JsonLdHelper
+
+  # Should be called when uri has already been checked for locality
+  # Does a WebFinger roundtrip on each call
+  def call(uri, prefetched_json = nil)
+    @json = body_to_json(prefetched_json) || fetch_resource(uri)
+
+    return unless supported_context? && expected_type?
+
+    @uri      = @json['id']
+    @username = @json['preferredUsername']
+    @domain   = Addressable::URI.parse(uri).normalized_host
+
+    return unless verified_webfinger?
+
+    ActivityPub::ProcessAccountService.new.call(@username, @domain, @json)
+  rescue Oj::ParseError
+    nil
+  end
+
+  private
+
+  def verified_webfinger?
+    webfinger                            = Goldfinger.finger("acct:#{@username}@#{@domain}")
+    confirmed_username, confirmed_domain = split_acct(webfinger.subject)
+
+    return true if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
+
+    webfinger                            = Goldfinger.finger("acct:#{confirmed_username}@#{confirmed_domain}")
+    confirmed_username, confirmed_domain = split_acct(webfinger.subject)
+    self_reference                       = webfinger.link('self')
+
+    return false if self_reference&.href != @uri
+
+    @username = confirmed_username
+    @domain   = confirmed_domain
+
+    true
+  rescue Goldfinger::Error
+    false
+  end
+
+  def split_acct(acct)
+    acct.gsub(/\Aacct:/, '').split('@')
+  end
+
+  def supported_context?
+    super(@json)
+  end
+
+  def expected_type?
+    @json['type'] == 'Person'
+  end
+end
diff --git a/app/services/activitypub/fetch_remote_key_service.rb b/app/services/activitypub/fetch_remote_key_service.rb
new file mode 100644
index 000000000..ebd64071e
--- /dev/null
+++ b/app/services/activitypub/fetch_remote_key_service.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+class ActivityPub::FetchRemoteKeyService < BaseService
+  include JsonLdHelper
+
+  # Returns account that owns the key
+  def call(uri, prefetched_json = nil)
+    @json = body_to_json(prefetched_json) || fetch_resource(uri)
+
+    return unless supported_context?(@json) && expected_type?
+    return find_account(uri, @json) if person?
+
+    @owner = fetch_resource(owner_uri)
+
+    return unless supported_context?(@owner) && confirmed_owner?
+
+    find_account(owner_uri, @owner)
+  end
+
+  private
+
+  def find_account(uri, prefetched_json)
+    account   = ActivityPub::TagManager.instance.uri_to_resource(uri, Account)
+    account ||= ActivityPub::FetchRemoteAccountService.new.call(uri, prefetched_json)
+    account
+  end
+
+  def expected_type?
+    person? || public_key?
+  end
+
+  def person?
+    @json['type'] == 'Person'
+  end
+
+  def public_key?
+    @json['publicKeyPem'].present? && @json['owner'].present?
+  end
+
+  def owner_uri
+    @owner_uri ||= value_or_id(@json['owner'])
+  end
+
+  def confirmed_owner?
+    @owner['type'] == 'Person' && value_or_id(@owner['publicKey']) == @json['id']
+  end
+end
diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb
new file mode 100644
index 000000000..68ca58d62
--- /dev/null
+++ b/app/services/activitypub/fetch_remote_status_service.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+class ActivityPub::FetchRemoteStatusService < BaseService
+  include JsonLdHelper
+
+  # Should be called when uri has already been checked for locality
+  def call(uri, prefetched_json = nil)
+    @json = body_to_json(prefetched_json) || fetch_resource(uri)
+
+    return unless supported_context?
+
+    activity = activity_json
+    actor_id = value_or_id(activity['actor'])
+
+    return unless expected_type?(activity) && trustworthy_attribution?(uri, actor_id)
+
+    actor = ActivityPub::TagManager.instance.uri_to_resource(actor_id, Account)
+    actor = ActivityPub::FetchRemoteAccountService.new.call(actor_id) if actor.nil?
+
+    ActivityPub::Activity.factory(activity, actor).perform
+  end
+
+  private
+
+  def activity_json
+    if %w(Note Article).include? @json['type']
+      {
+        'type'   => 'Create',
+        'actor'  => first_of_value(@json['attributedTo']),
+        'object' => @json,
+      }
+    else
+      @json
+    end
+  end
+
+  def trustworthy_attribution?(uri, attributed_to)
+    Addressable::URI.parse(uri).normalized_host.casecmp(Addressable::URI.parse(attributed_to).normalized_host).zero?
+  end
+
+  def supported_context?
+    super(@json)
+  end
+
+  def expected_type?(json)
+    %w(Create Announce).include? json['type']
+  end
+end
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
new file mode 100644
index 000000000..a26b39cb5
--- /dev/null
+++ b/app/services/activitypub/process_account_service.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+class ActivityPub::ProcessAccountService < BaseService
+  include JsonLdHelper
+
+  # Should be called with confirmed valid JSON
+  # and WebFinger-resolved username and domain
+  def call(username, domain, json)
+    return unless json['inbox'].present?
+
+    @json     = json
+    @uri      = @json['id']
+    @username = username
+    @domain   = domain
+    @account  = Account.find_by(uri: @uri)
+
+    create_account  if @account.nil?
+    upgrade_account if @account.ostatus?
+    update_account
+
+    @account
+  rescue Oj::ParseError
+    nil
+  end
+
+  private
+
+  def create_account
+    @account = Account.new
+    @account.protocol    = :activitypub
+    @account.username    = @username
+    @account.domain      = @domain
+    @account.uri         = @uri
+    @account.suspended   = true if auto_suspend?
+    @account.silenced    = true if auto_silence?
+    @account.private_key = nil
+    @account.save!
+  end
+
+  def update_account
+    @account.last_webfingered_at = Time.now.utc
+    @account.protocol            = :activitypub
+    @account.inbox_url           = @json['inbox'] || ''
+    @account.outbox_url          = @json['outbox'] || ''
+    @account.shared_inbox_url    = @json['sharedInbox'] || ''
+    @account.followers_url       = @json['followers'] || ''
+    @account.url                 = @json['url'] || @uri
+    @account.display_name        = @json['name'] || ''
+    @account.note                = @json['summary'] || ''
+    @account.avatar_remote_url   = image_url('icon')
+    @account.header_remote_url   = image_url('image')
+    @account.public_key          = public_key || ''
+    @account.locked              = @json['manuallyApprovesFollowers'] || false
+    @account.save!
+  end
+
+  def upgrade_account
+    ActivityPub::PostUpgradeWorker.perform_async(@account.domain)
+  end
+
+  def image_url(key)
+    value = first_of_value(@json[key])
+
+    return if value.nil?
+    return @json[key]['url'] if @json[key].is_a?(Hash)
+
+    image = fetch_resource(value)
+    image['url'] if image
+  end
+
+  def public_key
+    value = first_of_value(@json['publicKey'])
+
+    return if value.nil?
+    return value['publicKeyPem'] if value.is_a?(Hash)
+
+    key = fetch_resource(value)
+    key['publicKeyPem'] if key
+  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
+end
diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb
new file mode 100644
index 000000000..bc04c50ba
--- /dev/null
+++ b/app/services/activitypub/process_collection_service.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+class ActivityPub::ProcessCollectionService < BaseService
+  include JsonLdHelper
+
+  def call(body, account)
+    @account = account
+    @json    = Oj.load(body, mode: :strict)
+
+    return if @account.suspended? || !supported_context?
+
+    return if different_actor? && verify_account!.nil?
+
+    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 && @json['signature'].present?
+  end
+
+  def process_items(items)
+    items.reverse_each.map { |item| process_item(item) }.compact
+  end
+
+  def supported_context?
+    super(@json)
+  end
+
+  def process_item(item)
+    activity = ActivityPub::Activity.factory(item, @account)
+    activity&.perform
+  end
+
+  def verify_account!
+    @account = ActivityPub::LinkedDataSignature.new(@json).verify_account!
+  end
+end