Merge commit 'd27eb181f6ab419d1745a1fe9b94094be17a618f' into glitch-soc/merge-upstream

Conflicts:
- `spec/requests/api/v2/instance_spec.rb`:
  Conflict due to glitch-soc having a different default site name.
  Updated the tests as upstream did, keeping glitch-soc's default name.
main-rebase-security-fix
Claire 2024-05-01 17:22:02 +02:00
commit 15f6d2d038
19 changed files with 88 additions and 54 deletions

View File

@ -11,6 +11,6 @@ linters:
MiddleDot: MiddleDot:
enabled: true enabled: true
LineLength: LineLength:
max: 320 max: 300
ViewLength: ViewLength:
max: 200 # Override default value of 100 inherited from rubocop max: 200 # Override default value of 100 inherited from rubocop

View File

@ -39,7 +39,7 @@ Layout/FirstHashElementIndentation:
# Reason: Currently disabled in .rubocop_todo.yml # Reason: Currently disabled in .rubocop_todo.yml
# https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength # https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength
Layout/LineLength: Layout/LineLength:
Max: 320 # Default of 120 causes a duplicate entry in generated todo file Max: 300 # Default of 120 causes a duplicate entry in generated todo file
## Disable most Metrics/*Length cops ## Disable most Metrics/*Length cops
# Reason: those are often triggered and force significant refactors when this happend # Reason: those are often triggered and force significant refactors when this happend

View File

@ -69,7 +69,6 @@ gem 'nsa'
gem 'oj', '~> 3.14' gem 'oj', '~> 3.14'
gem 'ox', '~> 2.14' gem 'ox', '~> 2.14'
gem 'parslet' gem 'parslet'
gem 'posix-spawn'
gem 'public_suffix', '~> 5.0' gem 'public_suffix', '~> 5.0'
gem 'pundit', '~> 2.3' gem 'pundit', '~> 2.3'
gem 'premailer-rails' gem 'premailer-rails'
@ -89,7 +88,7 @@ gem 'sidekiq-unique-jobs', '~> 7.1'
gem 'sidekiq-bulk', '~> 0.2.0' gem 'sidekiq-bulk', '~> 0.2.0'
gem 'simple-navigation', '~> 4.4' gem 'simple-navigation', '~> 4.4'
gem 'simple_form', '~> 5.2' gem 'simple_form', '~> 5.2'
gem 'stoplight', '~> 3.0.1' gem 'stoplight', '~> 4.1'
gem 'strong_migrations', '1.8.0' gem 'strong_migrations', '1.8.0'
gem 'tty-prompt', '~> 0.23', require: false gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0' gem 'twitter-text', '~> 3.1.0'

View File

@ -245,7 +245,7 @@ GEM
tzinfo tzinfo
excon (0.110.0) excon (0.110.0)
fabrication (2.31.0) fabrication (2.31.0)
faker (3.3.0) faker (3.3.1)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (1.10.3) faraday (1.10.3)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
@ -509,7 +509,6 @@ GEM
pg (1.5.6) pg (1.5.6)
pghero (3.4.1) pghero (3.4.1)
activerecord (>= 6) activerecord (>= 6)
posix-spawn (0.3.15)
premailer (1.23.0) premailer (1.23.0)
addressable addressable
css_parser (>= 1.12.0) css_parser (>= 1.12.0)
@ -733,7 +732,7 @@ GEM
smart_properties (1.17.0) smart_properties (1.17.0)
stackprof (0.2.26) stackprof (0.2.26)
statsd-ruby (1.5.0) statsd-ruby (1.5.0)
stoplight (3.0.2) stoplight (4.1.0)
redlock (~> 1.0) redlock (~> 1.0)
stringio (3.1.0) stringio (3.1.0)
strong_migrations (1.8.0) strong_migrations (1.8.0)
@ -899,7 +898,6 @@ DEPENDENCIES
parslet parslet
pg (~> 1.5) pg (~> 1.5)
pghero pghero
posix-spawn
premailer-rails premailer-rails
private_address_check (~> 0.5) private_address_check (~> 0.5)
propshaft propshaft
@ -941,7 +939,7 @@ DEPENDENCIES
simplecov (~> 0.22) simplecov (~> 0.22)
simplecov-lcov (~> 0.8) simplecov-lcov (~> 0.8)
stackprof stackprof
stoplight (~> 3.0.1) stoplight (~> 4.1)
strong_migrations (= 1.8.0) strong_migrations (= 1.8.0)
test-prof test-prof
thor (~> 1.2) thor (~> 1.2)

View File

@ -66,7 +66,7 @@ module SignatureVerification
compare_signed_string = build_signed_string(include_query_string: false) compare_signed_string = build_signed_string(include_query_string: false)
return actor unless verify_signature(actor, signature, compare_signed_string).nil? return actor unless verify_signature(actor, signature, compare_signed_string).nil?
actor = stoplight_wrap_request { actor_refresh_key!(actor) } actor = stoplight_wrapper.run { actor_refresh_key!(actor) }
raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil? raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
@ -226,10 +226,10 @@ module SignatureVerification
end end
if key_id.start_with?('acct:') if key_id.start_with?('acct:')
stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) } stoplight_wrapper.run { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id) elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
account = ActivityPub::TagManager.instance.uri_to_actor(key_id) account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) } account ||= stoplight_wrapper.run { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) }
account account
end end
rescue Mastodon::PrivateNetworkAddressError => e rescue Mastodon::PrivateNetworkAddressError => e
@ -238,12 +238,11 @@ module SignatureVerification
raise SignatureVerificationError, e.message raise SignatureVerificationError, e.message
end end
def stoplight_wrap_request(&block) def stoplight_wrapper
Stoplight("source:#{request.remote_ip}", &block) Stoplight("source:#{request.remote_ip}")
.with_threshold(1) .with_threshold(1)
.with_cool_off_time(5.minutes.seconds) .with_cool_off_time(5.minutes.seconds)
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) } .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
.run
end end
def actor_refresh_key!(actor) def actor_refresh_key!(actor)

View File

@ -10104,9 +10104,10 @@ noscript {
} }
.filtered-notifications-banner__badge { .filtered-notifications-banner__badge {
background-color: $highlight-text-color; background: $ui-button-background-color;
border-radius: 4px; border-radius: 4px;
padding: 1px 6px; padding: 1px 6px;
color: $white;
} }
} }

View File

@ -491,10 +491,10 @@ class FeedManager
# @param [List] list # @param [List] list
# @return [Boolean] # @return [Boolean]
def filter_from_list?(status, list) def filter_from_list?(status, list)
if status.reply? && status.in_reply_to_account_id != status.account_id if status.reply? && status.in_reply_to_account_id != status.account_id # Status is a reply to account other than status account
should_filter = status.in_reply_to_account_id != list.account_id should_filter = status.in_reply_to_account_id != list.account_id # Status replies to account id other than list account
should_filter &&= !list.show_followed? should_filter &&= !list.show_followed? # List show_followed? is false
should_filter &&= !(list.show_list? && ListAccount.exists?(list_id: list.id, account_id: status.in_reply_to_account_id)) should_filter &&= !(list.show_list? && ListAccount.exists?(list_id: list.id, account_id: status.in_reply_to_account_id)) # If show_list? true, check for a ListAccount with list and reply to account
return !!should_filter return !!should_filter
end end
@ -509,7 +509,11 @@ class FeedManager
# @param [Hash] crutches # @param [Hash] crutches
# @return [Boolean] # @return [Boolean]
def filter_from_tags?(status, receiver_id, crutches) def filter_from_tags?(status, receiver_id, crutches)
receiver_id == status.account_id || ((crutches[:active_mentions][status.id] || []) + [status.account_id]).any? { |target_account_id| crutches[:blocking][target_account_id] || crutches[:muting][target_account_id] } || crutches[:blocked_by][status.account_id] || crutches[:domain_blocking][status.account.domain] receiver_id == status.account_id || # Receiver is status account?
((crutches[:active_mentions][status.id] || []) + [status.account_id]) # For mentioned accounts or status account:
.any? { |target_account_id| crutches[:blocking][target_account_id] || crutches[:muting][target_account_id] } || # - Target account is muted or blocked?
crutches[:blocked_by][status.account_id] || # Blocked by status account?
crutches[:domain_blocking][status.account.domain] # Blocking domain of status account?
end end
# Adds a status to an account's feed, returning true if a status was # Adds a status to an account's feed, returning true if a status was

View File

@ -15,4 +15,6 @@ class UserIp < ApplicationRecord
self.primary_key = :user_id self.primary_key = :user_id
belongs_to :user belongs_to :user
scope :by_latest_used, -> { order(used_at: :desc) }
end end

View File

@ -54,6 +54,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
accounts: { accounts: {
max_featured_tags: FeaturedTag::LIMIT, max_featured_tags: FeaturedTag::LIMIT,
max_pinned_statuses: StatusPinValidator::PIN_LIMIT,
}, },
statuses: { statuses: {

View File

@ -10,7 +10,7 @@ class BulkImportRowService
when :following, :blocking, :muting, :lists when :following, :blocking, :muting, :lists
target_acct = @data['acct'] target_acct = @data['acct']
target_domain = domain(target_acct) target_domain = domain(target_acct)
@target_account = stoplight_wrap_request(target_domain) { ResolveAccountService.new.call(target_acct, { check_delivery_availability: true }) } @target_account = stoplight_wrapper(target_domain).run { ResolveAccountService.new.call(target_acct, { check_delivery_availability: true }) }
return false if @target_account.nil? return false if @target_account.nil?
when :bookmarks when :bookmarks
target_uri = @data['uri'] target_uri = @data['uri']
@ -18,7 +18,7 @@ class BulkImportRowService
@target_status = ActivityPub::TagManager.instance.uri_to_resource(target_uri, Status) @target_status = ActivityPub::TagManager.instance.uri_to_resource(target_uri, Status)
return false if @target_status.nil? && ActivityPub::TagManager.instance.local_uri?(target_uri) return false if @target_status.nil? && ActivityPub::TagManager.instance.local_uri?(target_uri)
@target_status ||= stoplight_wrap_request(target_domain) { ActivityPub::FetchRemoteStatusService.new.call(target_uri) } @target_status ||= stoplight_wrapper(target_domain).run { ActivityPub::FetchRemoteStatusService.new.call(target_uri) }
return false if @target_status.nil? return false if @target_status.nil?
end end
@ -51,16 +51,15 @@ class BulkImportRowService
TagManager.instance.local_domain?(domain) ? nil : TagManager.instance.normalize_domain(domain) TagManager.instance.local_domain?(domain) ? nil : TagManager.instance.normalize_domain(domain)
end end
def stoplight_wrap_request(domain, &block) def stoplight_wrapper(domain)
if domain.present? if domain.present?
Stoplight("source:#{domain}", &block) Stoplight("source:#{domain}")
.with_fallback { nil } .with_fallback { nil }
.with_threshold(1) .with_threshold(1)
.with_cool_off_time(5.minutes.seconds) .with_cool_off_time(5.minutes.seconds)
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) } .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
.run
else else
yield Stoplight('domain-blank')
end end
end end
end end

View File

@ -62,13 +62,7 @@
%td %td
%time.formatted{ datetime: account.created_at.iso8601, title: l(account.created_at) }= l account.created_at %time.formatted{ datetime: account.created_at.iso8601, title: l(account.created_at) }= l account.created_at
%td %td
- recent_ips = account.user.ips.order(used_at: :desc).to_a = render partial: 'admin/accounts/user_ip', collection: account.user.ips.by_latest_used
- recent_ips.each_with_index do |recent_ip, i|
%tr
- if i.zero?
%th{ rowspan: recent_ips.size }= t('admin.accounts.most_recent_ip')
%td= recent_ip.ip
%td= table_link_to 'search', t('admin.accounts.search_same_ip'), admin_accounts_path(ip: recent_ip.ip)
%tr %tr
%th= t('admin.accounts.most_recent_activity') %th= t('admin.accounts.most_recent_activity')
%td %td

View File

@ -0,0 +1,5 @@
%tr
- if user_ip_iteration.first?
%th{ rowspan: user_ip_iteration.size }= t('admin.accounts.most_recent_ip')
%td= user_ip.ip
%td= table_link_to 'search', t('admin.accounts.search_same_ip'), admin_accounts_path(ip: user_ip.ip)

View File

@ -59,7 +59,7 @@ class ActivityPub::DeliveryWorker
end end
def perform_request def perform_request
light = Stoplight(@inbox_url) do stoplight_wrapper.run do
request_pool.with(@host) do |http_client| request_pool.with(@host) do |http_client|
build_request(http_client).perform do |response| build_request(http_client).perform do |response|
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response)
@ -68,10 +68,12 @@ class ActivityPub::DeliveryWorker
end end
end end
end end
end
light.with_threshold(STOPLIGHT_FAILURE_THRESHOLD) def stoplight_wrapper
Stoplight(@inbox_url)
.with_threshold(STOPLIGHT_FAILURE_THRESHOLD)
.with_cool_off_time(STOPLIGHT_COOLDOWN) .with_cool_off_time(STOPLIGHT_COOLDOWN)
.run
end end
def failure_tracker def failure_tracker

View File

@ -11,7 +11,7 @@ class Import::RelationshipWorker
def perform(account_id, target_account_uri, relationship, options) def perform(account_id, target_account_uri, relationship, options)
from_account = Account.find(account_id) from_account = Account.find(account_id)
target_domain = domain(target_account_uri) target_domain = domain(target_account_uri)
target_account = stoplight_wrap_request(target_domain) { ResolveAccountService.new.call(target_account_uri, { check_delivery_availability: true }) } target_account = stoplight_wrapper(target_domain).run { ResolveAccountService.new.call(target_account_uri, { check_delivery_availability: true }) }
options.symbolize_keys! options.symbolize_keys!
return if target_account.nil? return if target_account.nil?
@ -43,16 +43,15 @@ class Import::RelationshipWorker
TagManager.instance.local_domain?(domain) ? nil : TagManager.instance.normalize_domain(domain) TagManager.instance.local_domain?(domain) ? nil : TagManager.instance.normalize_domain(domain)
end end
def stoplight_wrap_request(domain, &block) def stoplight_wrapper(domain)
if domain.present? if domain.present?
Stoplight("source:#{domain}", &block) Stoplight("source:#{domain}")
.with_fallback { nil } .with_fallback { nil }
.with_threshold(1) .with_threshold(1)
.with_cool_off_time(5.minutes.seconds) .with_cool_off_time(5.minutes.seconds)
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) } .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
.run
else else
yield Stoplight('domain-blank')
end end
end end
end end

View File

@ -3,6 +3,6 @@
require 'stoplight' require 'stoplight'
Rails.application.reloader.to_prepare do Rails.application.reloader.to_prepare do
Stoplight::Light.default_data_store = Stoplight::DataStore::Redis.new(RedisConfiguration.new.connection) Stoplight.default_data_store = Stoplight::DataStore::Redis.new(RedisConfiguration.new.connection)
Stoplight::Light.default_notifiers = [Stoplight::Notifier::Logger.new(Rails.logger)] Stoplight.default_notifiers = [Stoplight::Notifier::Logger.new(Rails.logger)]
end end

View File

@ -84,13 +84,11 @@ module Paperclip
# Don't go through Stoplight if we don't have anything object-storage-oriented to do # Don't go through Stoplight if we don't have anything object-storage-oriented to do
return super if @queued_for_delete.empty? && @queued_for_write.empty? && !dirty? return super if @queued_for_delete.empty? && @queued_for_write.empty? && !dirty?
Stoplight('object-storage') { super }.with_threshold(STOPLIGHT_THRESHOLD).with_cool_off_time(STOPLIGHT_COOLDOWN).with_error_handler do |error, handle| Stoplight('object-storage')
if error.is_a?(Seahorse::Client::NetworkingError) .with_threshold(STOPLIGHT_THRESHOLD)
handle.call(error) .with_cool_off_time(STOPLIGHT_COOLDOWN)
else .with_error_handler { |error, handle| error.is_a?(Seahorse::Client::NetworkingError) ? handle.call(error) : raise(error) }
raise error .run { super }
end
end.run
end end
end end
end end

View File

@ -22,13 +22,15 @@ describe 'Public' do
get '/api/v1/timelines/public', headers: headers, params: params get '/api/v1/timelines/public', headers: headers, params: params
end end
let!(:private_status) { Fabricate(:status, visibility: :private) } # rubocop:disable RSpec/LetSetup
let!(:local_status) { Fabricate(:status, account: Fabricate.build(:account, domain: nil)) } let!(:local_status) { Fabricate(:status, account: Fabricate.build(:account, domain: nil)) }
let!(:remote_status) { Fabricate(:status, account: Fabricate.build(:account, domain: 'example.com')) } let!(:remote_status) { Fabricate(:status, account: Fabricate.build(:account, domain: 'example.com')) }
let!(:media_status) { Fabricate(:status, media_attachments: [Fabricate.build(:media_attachment)]) } let!(:media_status) { Fabricate(:status, media_attachments: [Fabricate.build(:media_attachment)]) }
let(:params) { {} } let(:params) { {} }
before do
Fabricate(:status, visibility: :private)
end
context 'when the instance allows public preview' do context 'when the instance allows public preview' do
let(:expected_statuses) { [local_status, remote_status, media_status] } let(:expected_statuses) { [local_status, remote_status, media_status] }

View File

@ -18,6 +18,7 @@ describe 'Instances' do
expect(body_as_json) expect(body_as_json)
.to be_present .to be_present
.and include(title: 'Mastodon Glitch Edition') .and include(title: 'Mastodon Glitch Edition')
.and include_configuration_limits
end end
end end
@ -31,7 +32,26 @@ describe 'Instances' do
expect(body_as_json) expect(body_as_json)
.to be_present .to be_present
.and include(title: 'Mastodon Glitch Edition') .and include(title: 'Mastodon Glitch Edition')
.and include_configuration_limits
end end
end end
def include_configuration_limits
include(
configuration: include(
accounts: include(
max_featured_tags: FeaturedTag::LIMIT,
max_pinned_statuses: StatusPinValidator::PIN_LIMIT
),
statuses: include(
max_characters: StatusLengthValidator::MAX_CHARS,
max_media_attachments: 4 # TODO, move to constant somewhere
),
polls: include(
max_options: PollValidator::MAX_OPTIONS
)
)
)
end
end end
end end

View File

@ -10,11 +10,22 @@ describe REST::InstanceSerializer do
it 'returns recent usage data' do it 'returns recent usage data' do
expect(serialization['usage']).to eq({ 'users' => { 'active_month' => 0 } }) expect(serialization['usage']).to eq({ 'users' => { 'active_month' => 0 } })
end end
end
describe 'configuration' do
it 'returns the VAPID public key' do it 'returns the VAPID public key' do
expect(serialization['configuration']['vapid']).to eq({ expect(serialization['configuration']['vapid']).to eq({
'public_key' => Rails.configuration.x.vapid_public_key, 'public_key' => Rails.configuration.x.vapid_public_key,
}) })
end end
it 'returns the max pinned statuses limit' do
expect(serialization.deep_symbolize_keys)
.to include(
configuration: include(
accounts: include(max_pinned_statuses: StatusPinValidator::PIN_LIMIT)
)
)
end
end end
end end