Skip to content

Commit

Permalink
Merge pull request #514 from kmycode/kb-draft-10.3
Browse files Browse the repository at this point in the history
Release: 10.3
  • Loading branch information
kmycode authored Feb 2, 2024
2 parents e082a8f + 2e7e260 commit 8b0fd35
Show file tree
Hide file tree
Showing 21 changed files with 191 additions and 50 deletions.
10 changes: 6 additions & 4 deletions app/controllers/activitypub/references_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ class ActivityPub::ReferencesController < ActivityPub::BaseController
include Authorization
include AccountOwnedConcern

REFERENCES_LIMIT = 5

before_action :require_signature!, if: :authorized_fetch_mode?
before_action :set_status

Expand Down Expand Up @@ -40,17 +38,21 @@ def results
@results ||= begin
references = @status.reference_objects.order(target_status_id: :asc)
references = references.where('target_status_id > ?', page_params[:min_id]) if page_params[:min_id].present?
references = references.limit(limit_param(REFERENCES_LIMIT))
references = references.limit(limit_param(references_limit))
references.pluck(:target_status_id)
end
end

def references_limit
StatusReference::REFERENCES_LIMIT
end

def pagination_min_id
results.last
end

def records_continue?
results.size == limit_param(REFERENCES_LIMIT)
results.size == limit_param(references_limit)
end

def references_collection_presenter
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/concerns/signature_verification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ def actor_from_key_id(key_id)
stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
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 ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) }
account
end
rescue Mastodon::PrivateNetworkAddressError => e
Expand Down
4 changes: 2 additions & 2 deletions app/helpers/jsonld_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,8 @@ def safe_for_forwarding?(original, compacted)
end
end

def fetch_resource(uri, id, on_behalf_of = nil, request_options: {})
unless id
def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {})
unless id_is_known
json = fetch_resource_without_id_validation(uri, on_behalf_of)

return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
Expand Down
2 changes: 1 addition & 1 deletion app/lib/activitypub/activity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ def fetch_remote_original_status
if object_uri.start_with?('http')
return if ActivityPub::TagManager.instance.local_uri?(object_uri)

ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, on_behalf_of: @account.followers.local.first, request_id: @options[:request_id])
ActivityPub::FetchRemoteStatusService.new.call(object_uri, on_behalf_of: @account.followers.local.first, request_id: @options[:request_id])
elsif @object['url'].present?
::FetchRemoteStatusService.new.call(@object['url'], request_id: @options[:request_id])
end
Expand Down
2 changes: 1 addition & 1 deletion app/lib/activitypub/linked_data_signature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def verify_actor!
return unless type == 'RsaSignature2017'

creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) if creator&.public_key.blank?
creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri) if creator&.public_key.blank?

return if creator.nil?

Expand Down
2 changes: 2 additions & 0 deletions app/models/status_reference.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#

class StatusReference < ApplicationRecord
REFERENCES_LIMIT = 5

belongs_to :status
belongs_to :target_status, class_name: 'Status'

Expand Down
21 changes: 17 additions & 4 deletions app/services/activitypub/fetch_references_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class ActivityPub::FetchReferencesService < BaseService
def call(status, collection_or_uri)
@account = status.account

collection_items(collection_or_uri)&.map { |item| value_or_id(item) }
collection_items(collection_or_uri)&.take(8)&.map { |item| value_or_id(item) }
end

private
Expand All @@ -20,9 +20,9 @@ def collection_items(collection_or_uri)

case collection['type']
when 'Collection', 'CollectionPage'
collection['items']
as_array(collection['items'])
when 'OrderedCollection', 'OrderedCollectionPage'
collection['orderedItems']
as_array(collection['orderedItems'])
end
end

Expand All @@ -31,6 +31,19 @@ def fetch_collection(collection_or_uri)
return if unsupported_uri_scheme?(collection_or_uri)
return if ActivityPub::TagManager.instance.local_uri?(collection_or_uri)

fetch_resource_without_id_validation(collection_or_uri, nil, true)
# NOTE: For backward compatibility reasons, Mastodon signs outgoing
# queries incorrectly by default.
#
# While this is relevant for all URLs with query strings, this is
# the only code path where this happens in practice.
#
# Therefore, retry with correct signatures if this fails.
begin
fetch_resource_without_id_validation(collection_or_uri, nil, true)
rescue Mastodon::UnexpectedResponseError => e
raise unless e.response && e.response.code == 401 && Addressable::URI.parse(collection_or_uri).query.present?

fetch_resource_without_id_validation(collection_or_uri, nil, true, request_options: { with_query_string: true })
end
end
end
2 changes: 1 addition & 1 deletion app/services/activitypub/fetch_remote_account_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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, request_id: nil)
def call(uri, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil)
actor = super
return actor if actor.nil? || actor.is_a?(Account)

Expand Down
6 changes: 3 additions & 3 deletions app/services/activitypub/fetch_remote_actor_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ 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, request_id: nil)
def call(uri, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil)
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)
fetch_resource(uri, true)
else
body_to_json(prefetched_body, compare_id: id ? uri : nil)
body_to_json(prefetched_body, compare_id: uri)
end
rescue Oj::ParseError
raise Error, "Error parsing JSON-LD document #{uri}"
Expand Down
17 changes: 2 additions & 15 deletions app/services/activitypub/fetch_remote_key_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,10 @@ class ActivityPub::FetchRemoteKeyService < BaseService
class Error < StandardError; end

# Returns actor that owns the key
def call(uri, id: true, prefetched_body: nil, suppress_errors: true)
def call(uri, suppress_errors: true)
raise Error, 'No key URI given' if uri.blank?

if prefetched_body.nil?
if id
@json = fetch_resource_without_id_validation(uri)
if actor_type?
@json = fetch_resource(@json['id'], true)
elsif uri != @json['id']
raise Error, "Fetched URI #{uri} has wrong id #{@json['id']}"
end
else
@json = fetch_resource(uri, id)
end
else
@json = body_to_json(prefetched_body, compare_id: id ? uri : nil)
end
@json = fetch_resource(uri, false)

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)
Expand Down
8 changes: 4 additions & 4 deletions app/services/activitypub/fetch_remote_status_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ class ActivityPub::FetchRemoteStatusService < BaseService
DISCOVERIES_PER_REQUEST = 1000

# Should be called when uri has already been checked for locality
def call(uri, id: true, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil)
def call(uri, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil)
return if domain_not_allowed?(uri)

@request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}"
@json = if prefetched_body.nil?
fetch_resource(uri, id, on_behalf_of)
fetch_resource(uri, true, on_behalf_of)
else
body_to_json(prefetched_body, compare_id: id ? uri : nil)
body_to_json(prefetched_body, compare_id: uri)
end

return unless supported_context?
Expand Down Expand Up @@ -65,7 +65,7 @@ def trustworthy_attribution?(uri, attributed_to)

def account_from_uri(uri)
actor = ActivityPub::TagManager.instance.uri_to_resource(uri, Account)
actor = ActivityPub::FetchRemoteAccountService.new.call(uri, id: true, request_id: @request_id) if actor.nil? || actor.possibly_stale?
actor = ActivityPub::FetchRemoteAccountService.new.call(uri, request_id: @request_id) if actor.nil? || actor.possibly_stale?
actor
end

Expand Down
2 changes: 1 addition & 1 deletion app/services/activitypub/process_account_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ def collection_info(type)

def moved_account
account = ActivityPub::TagManager.instance.uri_to_resource(@json['movedTo'], Account)
account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], id: true, break_on_redirect: true, request_id: @options[:request_id])
account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], break_on_redirect: true, request_id: @options[:request_id])
account
end

Expand Down
10 changes: 9 additions & 1 deletion app/services/fetch_resource_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,15 @@ def process_response(response, terminal = false)
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::FetchRemoteActorService::SUPPORTED_TYPES) || expected_type?(json))
return unless supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES) || expected_type?(json))

if json['id'] != @url
return if terminal

return process(json['id'], terminal: true)
end

[@url, { prefetched_body: body }]
elsif !terminal
link_header = response['Link'] && parse_link_header(response)

Expand Down
4 changes: 2 additions & 2 deletions lib/mastodon/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def kmyblue_major
end

def kmyblue_minor
2
3
end

def kmyblue_flag
Expand All @@ -29,7 +29,7 @@ def patch
end

def default_prerelease
'alpha.0'
'alpha.1'
end

def prerelease
Expand Down
4 changes: 2 additions & 2 deletions spec/lib/activitypub/linked_data_signature_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@

allow(ActivityPub::FetchRemoteKeyService).to receive(:new).and_return(service_stub)

allow(service_stub).to receive(:call).with('http://example.com/alice', id: false) do
allow(service_stub).to receive(:call).with('http://example.com/alice') do
sender.update!(public_key: old_key)
sender
end
end

it 'fetches key and returns creator' do
expect(subject.verify_actor!).to eq sender
expect(service_stub).to have_received(:call).with('http://example.com/alice', id: false).once
expect(service_stub).to have_received(:call).with('http://example.com/alice').once
end
end

Expand Down
128 changes: 128 additions & 0 deletions spec/services/activitypub/fetch_references_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ActivityPub::FetchReferencesService, type: :service do
subject { described_class.new.call(status, payload) }

let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') }
let(:status) { Fabricate(:status, account: actor) }
let(:collection_uri) { 'http://example.com/references/1' }

let(:items) do
[
'http://example.com/self-references-1',
'http://example.com/self-references-2',
'http://example.com/self-references-3',
'http://other.com/other-references-1',
'http://other.com/other-references-2',
'http://other.com/other-references-3',
'http://example.com/self-references-4',
'http://example.com/self-references-5',
'http://example.com/self-references-6',
'http://example.com/self-references-7',
'http://example.com/self-references-8',
]
end

let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Collection',
id: collection_uri,
items: items,
}.with_indifferent_access
end

describe '#call' do
context 'when the payload is a Collection with inlined replies' do
context 'when there is a single reference, with the array compacted away' do
let(:items) { 'http://example.com/self-references-1' }

it 'a item is returned' do
expect(subject).to eq ['http://example.com/self-references-1']
end
end

context 'when passing the collection itself' do
it 'first 8 items are returned' do
expect(subject).to eq items.take(8)
end
end

context 'when passing the URL to the collection' do
subject { described_class.new.call(status, collection_uri) }

before do
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
end

it 'first 8 items are returned' do
expect(subject).to eq items.take(8)
end
end
end

context 'when the payload is an OrderedCollection with inlined references' do
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'OrderedCollection',
id: collection_uri,
orderedItems: items,
}.with_indifferent_access
end

context 'when passing the collection itself' do
it 'first 8 items are returned' do
expect(subject).to eq items.take(8)
end
end

context 'when passing the URL to the collection' do
subject { described_class.new.call(status, collection_uri) }

before do
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
end

it 'first 8 items are returned' do
expect(subject).to eq items.take(8)
end
end
end

context 'when the payload is a paginated Collection with inlined references' do
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Collection',
id: collection_uri,
first: {
type: 'CollectionPage',
partOf: collection_uri,
items: items,
},
}.with_indifferent_access
end

context 'when passing the collection itself' do
it 'first 8 items are returned' do
expect(subject).to eq items.take(8)
end
end

context 'when passing the URL to the collection' do
subject { described_class.new.call(status, collection_uri) }

before do
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
end

it 'first 8 items are returned' do
expect(subject).to eq items.take(8)
end
end
end
end
end
Loading

0 comments on commit 8b0fd35

Please sign in to comment.