Merge remote-tracking branch 'parent/main' into upstream-20240128

This commit is contained in:
KMY 2025-01-31 08:52:03 +09:00
commit bd5b417d2b
107 changed files with 795 additions and 246 deletions

View file

@ -338,6 +338,38 @@ class FeedManager
end
end
# Populate list feed of account from scratch
# @param [List] list
# @return [void]
def populate_list(list)
limit = FeedManager::MAX_ITEMS / 2
aggregate = list.account.user&.aggregates_reblogs?
timeline_key = key(:list, list.id)
list.active_accounts.includes(:account_stat).reorder(nil).find_each do |target_account|
if redis.zcard(timeline_key) >= limit
oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i
last_status_score = Mastodon::Snowflake.id_at(target_account.last_status_at)
# If the feed is full and this account has not posted more recently
# than the last item on the feed, then we can skip the whole account
# because none of its statuses would stay on the feed anyway
next if last_status_score < oldest_home_score
end
statuses = target_account.statuses.list_eligible_visibility.includes(:preloadable_poll, :media_attachments, :account, reblog: :account).limit(limit)
crutches = build_crutches(list.account_id, statuses)
statuses.each do |status|
next if filter_from_home(status, list.account_id, crutches) || filter_from_list?(status, list)
add_to_feed(:list, list.id, status, aggregate_reblogs: aggregate)
end
trim(:list, list.id)
end
end
# Completely clear multiple feeds at once
# @param [Symbol] type
# @param [Array<Integer>] ids

View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
# This implements an older draft of HTTP Signatures:
# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures
class HttpSignatureDraft
REQUEST_TARGET = '(request-target)'
def initialize(keypair, key_id, full_path: true)
@keypair = keypair
@key_id = key_id
@full_path = full_path
end
def request_target(verb, url)
if url.query.nil? || !@full_path
"#{verb} #{url.path}"
else
"#{verb} #{url.path}?#{url.query}"
end
end
def sign(signed_headers, verb, url)
signed_headers = signed_headers.merge(REQUEST_TARGET => request_target(verb, url))
signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
algorithm = 'rsa-sha256'
signature = Base64.strict_encode64(@keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
"keyId=\"#{@key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""
end
end

View file

@ -61,8 +61,6 @@ class PerOperationWithDeadline < HTTP::Timeout::PerOperation
end
class Request
REQUEST_TARGET = '(request-target)'
# We enforce a 5s timeout on DNS resolving, 5s timeout on socket opening
# and 5s timeout on the TLS handshake, meaning the worst case should take
# about 15s in total
@ -78,11 +76,21 @@ class Request
@http_client = options.delete(:http_client)
@allow_local = options.delete(:allow_local)
@full_path = !options.delete(:omit_query_string)
@options = options.merge(socket_class: use_proxy? || @allow_local ? ProxySocket : Socket)
@options = @options.merge(timeout_class: PerOperationWithDeadline, timeout_options: TIMEOUT)
@options = {
follow: {
max_hops: 3,
on_redirect: ->(response, request) { re_sign_on_redirect(response, request) },
},
}.merge(options).merge(
socket_class: use_proxy? || @allow_local ? ProxySocket : Socket,
timeout_class: PerOperationWithDeadline,
timeout_options: TIMEOUT
)
@options = @options.merge(proxy_url) if use_proxy?
@headers = {}
@signing = nil
raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if block_hidden_service?
set_common_headers!
@ -92,8 +100,9 @@ class Request
def on_behalf_of(actor, sign_with: nil)
raise ArgumentError, 'actor must not be nil' if actor.nil?
@actor = actor
@keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @actor.keypair
key_id = ActivityPub::TagManager.instance.key_uri_for(actor)
keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : actor.keypair
@signing = HttpSignatureDraft.new(keypair, key_id, full_path: @full_path)
self
end
@ -119,7 +128,7 @@ class Request
end
def headers
(@actor ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
(@signing ? @headers.merge('Signature' => signature) : @headers)
end
class << self
@ -134,14 +143,13 @@ class Request
end
def http_client
HTTP.use(:auto_inflate).follow(max_hops: 3)
HTTP.use(:auto_inflate)
end
end
private
def set_common_headers!
@headers[REQUEST_TARGET] = request_target
@headers['User-Agent'] = Mastodon::Version.user_agent
@headers['Host'] = @url.host
@headers['Date'] = Time.now.utc.httpdate
@ -152,31 +160,28 @@ class Request
@headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(@options[:body])}"
end
def request_target
if @url.query.nil? || !@full_path
"#{@verb} #{@url.path}"
else
"#{@verb} #{@url.path}?#{@url.query}"
end
end
def signature
algorithm = 'rsa-sha256'
signature = Base64.strict_encode64(@keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
"keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""
@signing.sign(@headers.without('User-Agent', 'Accept-Encoding'), @verb, @url)
end
def signed_string
signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
end
def re_sign_on_redirect(_response, request)
# Delete existing signature if there is one, since it will be invalid
request.headers.delete('Signature')
def signed_headers
@headers.without('User-Agent', 'Accept-Encoding')
end
return unless @signing.present? && @verb == :get
def key_id
ActivityPub::TagManager.instance.key_uri_for(@actor)
signed_headers = request.headers.to_h.slice(*@headers.keys)
unless @headers.keys.all? { |key| signed_headers.key?(key) }
# We have lost some headers in the process, so don't sign the new
# request, in order to avoid issuing a valid signature with fewer
# conditions than expected.
Rails.logger.warn { "Some headers (#{@headers.keys - signed_headers.keys}) have been lost on redirect from {@uri} to #{request.uri}, this should not happen. Skipping signatures" }
return
end
signature_value = @signing.sign(signed_headers.without('User-Agent', 'Accept-Encoding'), @verb, Addressable::URI.parse(request.uri))
request.headers['Signature'] = signature_value
end
def http_client