From eea2654236966fee69c4f18217a6a62b1c66d2da Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 13 Nov 2023 17:58:00 +0100 Subject: [PATCH 001/117] Fix format-dependent redirects being cached regardless of requested format (#27634) --- config/routes.rb | 23 +++++++++++++++++++---- spec/requests/cache_spec.rb | 16 +++++++++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 5de8562a8..7e2f1aabe 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,18 @@ require 'sidekiq_unique_jobs/web' require 'sidekiq-scheduler/web' +class RedirectWithVary < ActionDispatch::Routing::PathRedirect + def serve(...) + super.tap do |_, headers, _| + headers['Vary'] = 'Origin, Accept' + end + end +end + +def redirect_with_vary(path) + RedirectWithVary.new(301, path) +end + Rails.application.routes.draw do # Paths of routes on the web app that to not require to be indexed or # have alternative format representations requiring separate controllers @@ -90,10 +102,13 @@ Rails.application.routes.draw do confirmations: 'auth/confirmations', } - get '/users/:username', to: redirect('/@%{username}'), constraints: lambda { |req| req.format.nil? || req.format.html? } - get '/users/:username/following', to: redirect('/@%{username}/following'), constraints: lambda { |req| req.format.nil? || req.format.html? } - get '/users/:username/followers', to: redirect('/@%{username}/followers'), constraints: lambda { |req| req.format.nil? || req.format.html? } - get '/users/:username/statuses/:id', to: redirect('/@%{username}/%{id}'), constraints: lambda { |req| req.format.nil? || req.format.html? } + # rubocop:disable Style/FormatStringToken - those do not go through the usual formatting functions and are not safe to correct + get '/users/:username', to: redirect_with_vary('/@%{username}'), constraints: lambda { |req| req.format.nil? || req.format.html? } + get '/users/:username/following', to: redirect_with_vary('/@%{username}/following'), constraints: lambda { |req| req.format.nil? || req.format.html? } + get '/users/:username/followers', to: redirect_with_vary('/@%{username}/followers'), constraints: lambda { |req| req.format.nil? || req.format.html? } + get '/users/:username/statuses/:id', to: redirect_with_vary('/@%{username}/%{id}'), constraints: lambda { |req| req.format.nil? || req.format.html? } + # rubocop:enable Style/FormatStringToken + get '/authorize_follow', to: redirect { |_, request| "/authorize_interaction?#{request.params.to_query}" } resources :accounts, path: 'users', only: [:show], param: :username do diff --git a/spec/requests/cache_spec.rb b/spec/requests/cache_spec.rb index c391c8b3d..d40895fc3 100644 --- a/spec/requests/cache_spec.rb +++ b/spec/requests/cache_spec.rb @@ -124,7 +124,7 @@ describe 'Caching behavior' do expect(response.cookies).to be_empty end - it 'sets public cache control' do + it 'sets public cache control', :aggregate_failures do # expect(response.cache_control[:max_age]&.to_i).to be_positive expect(response.cache_control[:public]).to be_truthy expect(response.cache_control[:private]).to be_falsy @@ -141,11 +141,8 @@ describe 'Caching behavior' do end shared_examples 'non-cacheable error' do - it 'does not return HTTP success' do + it 'does not return HTTP success and does not have cache headers', :aggregate_failures do expect(response).to_not have_http_status(200) - end - - it 'does not have cache headers' do expect(response.cache_control[:public]).to be_falsy end end @@ -182,6 +179,15 @@ describe 'Caching behavior' do end context 'when anonymously accessed' do + describe '/users/alice' do + it 'redirects with proper cache header', :aggregate_failures do + get '/users/alice' + + expect(response).to redirect_to('/@alice') + expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('accept') + end + end + TestEndpoints::ALWAYS_CACHED.each do |endpoint| describe endpoint do before { get endpoint } From ef149674f093e97ffbeb12910e2ebdd668bed7c2 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 23 Oct 2023 14:27:07 +0200 Subject: [PATCH 002/117] Change Content-Security-Policy to be tighter on media paths (#26889) --- config/initializers/content_security_policy.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index 6ce84a6e4..85a328b44 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -5,7 +5,11 @@ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy def host_to_url(str) - "http#{Rails.configuration.x.use_https ? 's' : ''}://#{str.split('/').first}" if str.present? + return if str.blank? + + uri = Addressable::URI.parse("http#{Rails.configuration.x.use_https ? 's' : ''}://#{str}") + uri.path += '/' unless uri.path.blank? || uri.path.end_with?('/') + uri.to_s end base_host = Rails.configuration.x.web_domain From 156d32689b3d084a7fe9f0aa6a8788cd4e831f17 Mon Sep 17 00:00:00 2001 From: gunchleoc Date: Thu, 28 Sep 2023 09:13:44 +0100 Subject: [PATCH 003/117] Only strip country code when language not listed in SUPPORTED_LOCALES (#27099) --- app/helpers/languages_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index a8c66552c..c42c4c23e 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -254,6 +254,7 @@ module LanguagesHelper def valid_locale_or_nil(str) return if str.blank? + return str if valid_locale?(str) code, = str.to_s.split(/[_-]/) # Strip out the region from e.g. en_US or ja-JP From aa69ca74ed113e4f0526522931f24db55de90b9a Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 6 Oct 2023 17:46:04 +0200 Subject: [PATCH 004/117] Fix incorrect serialization of regional languages in `contentMap` (#27207) --- app/lib/activitypub/case_transform.rb | 2 ++ .../serializers/activitypub/note_serializer_spec.rb | 13 ++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/lib/activitypub/case_transform.rb b/app/lib/activitypub/case_transform.rb index da2c5eb8b..bf5de7221 100644 --- a/app/lib/activitypub/case_transform.rb +++ b/app/lib/activitypub/case_transform.rb @@ -14,6 +14,8 @@ module ActivityPub::CaseTransform when String camel_lower_cache[value] ||= if value.start_with?('_:') "_:#{value.delete_prefix('_:').underscore.camelize(:lower)}" + elsif LanguagesHelper::ISO_639_1_REGIONAL.key?(value.to_sym) + value else value.underscore.camelize(:lower) end diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb index 4b2b8ec87..31ee31f13 100644 --- a/spec/serializers/activitypub/note_serializer_spec.rb +++ b/spec/serializers/activitypub/note_serializer_spec.rb @@ -7,7 +7,7 @@ describe ActivityPub::NoteSerializer do let!(:account) { Fabricate(:account) } let!(:other) { Fabricate(:account) } - let!(:parent) { Fabricate(:status, account: account, visibility: :public) } + let!(:parent) { Fabricate(:status, account: account, visibility: :public, language: 'zh-TW') } let!(:reply_by_account_first) { Fabricate(:status, account: account, thread: parent, visibility: :public) } let!(:reply_by_account_next) { Fabricate(:status, account: account, thread: parent, visibility: :public) } let!(:reply_by_other_first) { Fabricate(:status, account: other, thread: parent, visibility: :public) } @@ -18,8 +18,15 @@ describe ActivityPub::NoteSerializer do @serialization = ActiveModelSerializers::SerializableResource.new(parent, serializer: described_class, adapter: ActivityPub::Adapter) end - it 'has a Note type' do - expect(subject['type']).to eql('Note') + it 'has the expected shape' do + expect(subject).to include({ + '@context' => include('https://www.w3.org/ns/activitystreams'), + 'type' => 'Note', + 'attributedTo' => ActivityPub::TagManager.instance.uri_for(account), + 'contentMap' => include({ + 'zh-TW' => a_kind_of(String), + }), + }) end it 'has a replies collection' do From cdedae6d63ff6ac21dd8810896d39f016fd8ec68 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 23 Oct 2023 14:19:38 +0200 Subject: [PATCH 005/117] Fix some link anchors being recognized as hashtags (#27271) --- app/models/tag.rb | 2 +- spec/models/tag_spec.rb | 26 +++++++++++++++----------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/models/tag.rb b/app/models/tag.rb index 672d80c8b..413f6f500 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -35,7 +35,7 @@ class Tag < ApplicationRecord HASHTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)' HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASHTAG_LAST_SEQUENCE}" - HASHTAG_RE = %r{(?:^|[^/)\w])#(#{HASHTAG_NAME_PAT})}i + HASHTAG_RE = %r{(? Date: Fri, 13 Oct 2023 18:15:47 +0900 Subject: [PATCH 006/117] Fix when unfollow a tag, my post also disappears from the home timeline (#27391) --- app/lib/feed_manager.rb | 1 + spec/lib/feed_manager_spec.rb | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 26e5b78e8..8b7f20811 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -192,6 +192,7 @@ class FeedManager # also tagged with another followed hashtag or from a followed user scope = from_tag.statuses .where(id: timeline_status_ids) + .where.not(account: into_account) .where.not(account: into_account.following) .tagged_with_none(TagFollow.where(account: into_account).pluck(:tag_id)) diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 25edaada6..f4dd42f84 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -525,6 +525,44 @@ RSpec.describe FeedManager do end end + describe '#unmerge_tag_from_home' do + let(:receiver) { Fabricate(:account) } + let(:tag) { Fabricate(:tag) } + + it 'leaves a tagged status' do + status = Fabricate(:status) + status.tags << tag + described_class.instance.push_to_home(receiver, status) + + described_class.instance.unmerge_tag_from_home(tag, receiver) + + expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s) + end + + it 'remains a tagged status written by receiver\'s followee' do + followee = Fabricate(:account) + receiver.follow!(followee) + + status = Fabricate(:status, account: followee) + status.tags << tag + described_class.instance.push_to_home(receiver, status) + + described_class.instance.unmerge_tag_from_home(tag, receiver) + + expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s) + end + + it 'remains a tagged status written by receiver' do + status = Fabricate(:status, account: receiver) + status.tags << tag + described_class.instance.push_to_home(receiver, status) + + described_class.instance.unmerge_tag_from_home(tag, receiver) + + expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s) + end + end + describe '#clear_from_home' do let(:account) { Fabricate(:account) } let(:followed_account) { Fabricate(:account) } From 13205b54fd69f8cce1975aff8e784aa2f24e82ff Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 16 Oct 2023 15:24:14 +0200 Subject: [PATCH 007/117] Fix handling of `inLanguage` attribute in preview card processing (#27423) --- app/lib/link_details_extractor.rb | 3 ++- spec/lib/link_details_extractor_spec.rb | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/lib/link_details_extractor.rb b/app/lib/link_details_extractor.rb index b95ec8051..a96612cab 100644 --- a/app/lib/link_details_extractor.rb +++ b/app/lib/link_details_extractor.rb @@ -36,7 +36,8 @@ class LinkDetailsExtractor end def language - json['inLanguage'] + lang = json['inLanguage'] + lang.is_a?(Hash) ? (lang['alternateName'] || lang['name']) : lang end def type diff --git a/spec/lib/link_details_extractor_spec.rb b/spec/lib/link_details_extractor_spec.rb index 599bc4e6d..8c485cef2 100644 --- a/spec/lib/link_details_extractor_spec.rb +++ b/spec/lib/link_details_extractor_spec.rb @@ -82,6 +82,10 @@ RSpec.describe LinkDetailsExtractor do 'name' => 'Pet News', 'url' => 'https://example.com', }, + 'inLanguage' => { + name: 'English', + alternateName: 'en', + }, }.to_json end @@ -115,6 +119,12 @@ RSpec.describe LinkDetailsExtractor do expect(subject.provider_name).to eq 'Pet News' end end + + describe '#language' do + it 'returns the language from structured data' do + expect(subject.language).to eq 'en' + end + end end context 'when is wrapped in CDATA tags' do From 700ae1f9183a01afe574687a91afb6e7a99825fc Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 26 Oct 2023 19:03:31 +0200 Subject: [PATCH 008/117] Fix report processing notice not mentioning the report number when performing a custom action (#27442) --- app/controllers/admin/account_actions_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/admin/account_actions_controller.rb b/app/controllers/admin/account_actions_controller.rb index e89404b60..e674bf55a 100644 --- a/app/controllers/admin/account_actions_controller.rb +++ b/app/controllers/admin/account_actions_controller.rb @@ -21,7 +21,7 @@ module Admin account_action.save! if account_action.with_report? - redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: params[:report_id]) + redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: resource_params[:report_id]) else redirect_to admin_account_path(@account.id) end From bece853e3cc45a7b8b8b19b78b6f772c65e80b2a Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 27 Oct 2023 10:35:21 +0200 Subject: [PATCH 009/117] Fix error and incorrect URLs in `/api/v1/accounts/:id/featured_tags` for remote accounts (#27459) --- .../rest/featured_tag_serializer.rb | 4 +- config/routes.rb | 2 +- .../accounts/featured_tags_controller_spec.rb | 23 --------- .../api/v1/accounts/featured_tags_spec.rb | 50 +++++++++++++++++++ 4 files changed, 54 insertions(+), 25 deletions(-) delete mode 100644 spec/controllers/api/v1/accounts/featured_tags_controller_spec.rb create mode 100644 spec/requests/api/v1/accounts/featured_tags_spec.rb diff --git a/app/serializers/rest/featured_tag_serializer.rb b/app/serializers/rest/featured_tag_serializer.rb index c4b35ab03..c1ff4602a 100644 --- a/app/serializers/rest/featured_tag_serializer.rb +++ b/app/serializers/rest/featured_tag_serializer.rb @@ -10,7 +10,9 @@ class REST::FeaturedTagSerializer < ActiveModel::Serializer end def url - short_account_tag_url(object.account, object.tag) + # The path is hardcoded because we have to deal with both local and + # remote users, which are different routes + account_with_domain_url(object.account, "tagged/#{object.tag.to_param}") end def name diff --git a/config/routes.rb b/config/routes.rb index 7e2f1aabe..379262f4c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -149,7 +149,7 @@ Rails.application.routes.draw do get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status end - get '/@:username_with_domain/(*any)', to: 'home#index', constraints: { username_with_domain: %r{([^/])+?} }, format: false + get '/@:username_with_domain/(*any)', to: 'home#index', constraints: { username_with_domain: %r{([^/])+?} }, as: :account_with_domain, format: false get '/settings', to: redirect('/settings/profile') draw(:settings) diff --git a/spec/controllers/api/v1/accounts/featured_tags_controller_spec.rb b/spec/controllers/api/v1/accounts/featured_tags_controller_spec.rb deleted file mode 100644 index 53ac1e2a7..000000000 --- a/spec/controllers/api/v1/accounts/featured_tags_controller_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Api::V1::Accounts::FeaturedTagsController do - render_views - - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') } - let(:account) { Fabricate(:account) } - - before do - allow(controller).to receive(:doorkeeper_token) { token } - end - - describe 'GET #index' do - it 'returns http success' do - get :index, params: { account_id: account.id, limit: 2 } - - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/requests/api/v1/accounts/featured_tags_spec.rb b/spec/requests/api/v1/accounts/featured_tags_spec.rb new file mode 100644 index 000000000..bae7d448b --- /dev/null +++ b/spec/requests/api/v1/accounts/featured_tags_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'account featured tags API' do + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:scopes) { 'read:accounts' } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } + let(:account) { Fabricate(:account) } + + describe 'GET /api/v1/accounts/:id/featured_tags' do + subject do + get "/api/v1/accounts/#{account.id}/featured_tags", headers: headers + end + + before do + account.featured_tags.create!(name: 'foo') + account.featured_tags.create!(name: 'bar') + end + + it 'returns the expected tags', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(body_as_json).to contain_exactly(a_hash_including({ + name: 'bar', + url: "https://cb6e6126.ngrok.io/@#{account.username}/tagged/bar", + }), a_hash_including({ + name: 'foo', + url: "https://cb6e6126.ngrok.io/@#{account.username}/tagged/foo", + })) + end + + context 'when the account is remote' do + it 'returns the expected tags', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(body_as_json).to contain_exactly(a_hash_including({ + name: 'bar', + url: "https://cb6e6126.ngrok.io/@#{account.pretty_acct}/tagged/bar", + }), a_hash_including({ + name: 'foo', + url: "https://cb6e6126.ngrok.io/@#{account.pretty_acct}/tagged/foo", + })) + end + end + end +end From c66ade7de80e72d1dfdd1833c04cb3be47754f71 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 20 Oct 2023 10:45:46 +0200 Subject: [PATCH 010/117] Fix processing LDSigned activities from actors with unknown public keys (#27474) --- app/lib/activitypub/linked_data_signature.rb | 6 ++-- .../activitypub/linked_data_signature_spec.rb | 34 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app/lib/activitypub/linked_data_signature.rb b/app/lib/activitypub/linked_data_signature.rb index ea59879f3..faea63e8f 100644 --- a/app/lib/activitypub/linked_data_signature.rb +++ b/app/lib/activitypub/linked_data_signature.rb @@ -18,8 +18,8 @@ class ActivityPub::LinkedDataSignature return unless type == 'RsaSignature2017' - creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri) - creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) + creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri) + creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) if creator&.public_key.blank? return if creator.nil? @@ -28,6 +28,8 @@ class ActivityPub::LinkedDataSignature to_be_verified = options_hash + document_hash creator if creator.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), to_be_verified) + rescue OpenSSL::PKey::RSAError + false end def sign!(creator, sign_with: nil) diff --git a/spec/lib/activitypub/linked_data_signature_spec.rb b/spec/lib/activitypub/linked_data_signature_spec.rb index 6a6ad1a70..03d0c5a87 100644 --- a/spec/lib/activitypub/linked_data_signature_spec.rb +++ b/spec/lib/activitypub/linked_data_signature_spec.rb @@ -38,6 +38,40 @@ RSpec.describe ActivityPub::LinkedDataSignature do end end + context 'when local account record is missing a public key' do + let(:raw_signature) do + { + 'creator' => 'http://example.com/alice', + 'created' => '2017-09-23T20:21:34Z', + } + end + + let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) } + + let(:service_stub) { instance_double(ActivityPub::FetchRemoteKeyService) } + + before do + # Ensure signature is computed with the old key + signature + + # Unset key + old_key = sender.public_key + sender.update!(private_key: '', public_key: '') + + allow(ActivityPub::FetchRemoteKeyService).to receive(:new).and_return(service_stub) + + allow(service_stub).to receive(:call).with('http://example.com/alice', id: false) 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 + end + end + context 'when signature is missing' do let(:signature) { nil } From d5bc10b711dede8c84087de9240eea85c14ab7b0 Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Thu, 19 Oct 2023 13:22:44 +0200 Subject: [PATCH 011/117] The `class` props should be `className` (#27462) --- .../mastodon/features/ui/components/link_footer.jsx | 2 +- .../mastodon/features/ui/components/navigation_panel.jsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/features/ui/components/link_footer.jsx b/app/javascript/mastodon/features/ui/components/link_footer.jsx index 9585df2ec..6b1555243 100644 --- a/app/javascript/mastodon/features/ui/components/link_footer.jsx +++ b/app/javascript/mastodon/features/ui/components/link_footer.jsx @@ -100,7 +100,7 @@ class LinkFooter extends PureComponent { {DividingCircle} {DividingCircle} - v{version} + v{version}

); diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx index 8006ca89a..22eee79c0 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx @@ -59,10 +59,10 @@ class NavigationPanel extends Component { {transientSingleColumn ? ( -
+
{intl.formatMessage(messages.openedInClassicInterface)} {" "} - + {intl.formatMessage(messages.advancedInterface)}
From 4fc252354624d5b0fbafd86251204cc19edc525d Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Thu, 19 Oct 2023 19:36:08 +0200 Subject: [PATCH 012/117] Do not display the navigation banner in the logo container (#27476) --- .../ui/components/navigation_panel.jsx | 30 +++++++++++-------- .../styles/mastodon/components.scss | 1 + 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx index 22eee79c0..b16eb5e17 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx @@ -53,24 +53,30 @@ class NavigationPanel extends Component { const { intl } = this.props; const { signedIn, disabledAccountId } = this.context.identity; + let banner = undefined; + + if(transientSingleColumn) + banner = (
+ {intl.formatMessage(messages.openedInClassicInterface)} + {" "} + + {intl.formatMessage(messages.advancedInterface)} + +
); + return (
- - {transientSingleColumn ? ( -
- {intl.formatMessage(messages.openedInClassicInterface)} - {" "} - - {intl.formatMessage(messages.advancedInterface)} - -
- ) : ( -
- )} + {!banner &&
}
+ {banner && + + } + {signedIn && ( <> diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 567397a07..8c88b7dd6 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2466,6 +2466,7 @@ $ui-header-height: 55px; .navigation-panel__sign-in-banner, .navigation-panel__logo, + .navigation-panel__banner, .getting-started__trends { display: none; } From 4c3870647414f4cbd05373b0767d79e89a0db9a2 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 25 Oct 2023 15:55:57 +0200 Subject: [PATCH 013/117] Fix batch attachment deletion when using OpenStack Swift (#27554) --- app/lib/attachment_batch.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/attachment_batch.rb b/app/lib/attachment_batch.rb index b75938bdd..78bd59316 100644 --- a/app/lib/attachment_batch.rb +++ b/app/lib/attachment_batch.rb @@ -75,7 +75,7 @@ class AttachmentBatch end when :fog logger.debug { "Deleting #{attachment.path(style)}" } - attachment.directory.files.new(key: attachment.path(style)).destroy + attachment.send(:directory).files.new(key: attachment.path(style)).destroy when :azure logger.debug { "Deleting #{attachment.path(style)}" } attachment.destroy From de86e822f4f51d7e20a59d48b2c7e66431e374fe Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 26 Oct 2023 15:09:48 +0200 Subject: [PATCH 014/117] Fix error when trying to delete already-deleted file with OpenStack Swift (#27569) --- app/lib/attachment_batch.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/lib/attachment_batch.rb b/app/lib/attachment_batch.rb index 78bd59316..13a9da828 100644 --- a/app/lib/attachment_batch.rb +++ b/app/lib/attachment_batch.rb @@ -75,7 +75,12 @@ class AttachmentBatch end when :fog logger.debug { "Deleting #{attachment.path(style)}" } - attachment.send(:directory).files.new(key: attachment.path(style)).destroy + + begin + attachment.send(:directory).files.new(key: attachment.path(style)).destroy + rescue Fog::Storage::OpenStack::NotFound + # Ignore failure to delete a file that has already been deleted + end when :azure logger.debug { "Deleting #{attachment.path(style)}" } attachment.destroy From e6f4c91c5c7a28b5185f31c0dc5b58c242f4270f Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 27 Oct 2023 16:04:51 +0200 Subject: [PATCH 015/117] Fix hashtag matching pattern matching some URLs (#27584) --- app/models/tag.rb | 2 +- spec/models/tag_spec.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/tag.rb b/app/models/tag.rb index 413f6f500..8fab98fb5 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -35,7 +35,7 @@ class Tag < ApplicationRecord HASHTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)' HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASHTAG_LAST_SEQUENCE}" - HASHTAG_RE = %r{(? Date: Mon, 30 Oct 2023 23:32:25 +0100 Subject: [PATCH 016/117] Fix posts from force-sensitized accounts being able to trend (#27620) --- app/models/trends/statuses.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/trends/statuses.rb b/app/models/trends/statuses.rb index 427fa8e79..c47fb8427 100644 --- a/app/models/trends/statuses.rb +++ b/app/models/trends/statuses.rb @@ -106,7 +106,7 @@ class Trends::Statuses < Trends::Base private def eligible?(status) - status.public_visibility? && status.account.discoverable? && !status.account.silenced? && status.spoiler_text.blank? && !status.sensitive? && !status.reply? && valid_locale?(status.language) + status.public_visibility? && status.account.discoverable? && !status.account.silenced? && !status.account.sensitized? && status.spoiler_text.blank? && !status.sensitive? && !status.reply? && valid_locale?(status.language) end def calculate_scores(statuses, at_time) From 54a07731d14ed3809375b31e2ce86e347afed271 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 2 Nov 2023 15:58:37 +0100 Subject: [PATCH 017/117] Fix posts from threads received out-of-order sometimes not being inserted into timelines (#27653) --- app/services/fan_out_on_write_service.rb | 8 +- app/workers/thread_resolve_worker.rb | 9 +- spec/lib/activitypub/activity/create_spec.rb | 103 +++++++++++++++++++ 3 files changed, 116 insertions(+), 4 deletions(-) diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 2554756a5..f2a79c9fc 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -8,6 +8,7 @@ class FanOutOnWriteService < BaseService # @param [Hash] options # @option options [Boolean] update # @option options [Array] silenced_account_ids + # @option options [Boolean] skip_notifications def call(status, options = {}) @status = status @account = status.account @@ -37,8 +38,11 @@ class FanOutOnWriteService < BaseService def fan_out_to_local_recipients! deliver_to_self! - notify_mentioned_accounts! - notify_about_update! if update? + + unless @options[:skip_notifications] + notify_mentioned_accounts! + notify_about_update! if update? + end case @status.visibility.to_sym when :public, :unlisted, :private diff --git a/app/workers/thread_resolve_worker.rb b/app/workers/thread_resolve_worker.rb index 3206c45f6..d4cefb3fd 100644 --- a/app/workers/thread_resolve_worker.rb +++ b/app/workers/thread_resolve_worker.rb @@ -7,13 +7,18 @@ class ThreadResolveWorker sidekiq_options queue: 'pull', retry: 3 def perform(child_status_id, parent_url, options = {}) - child_status = Status.find(child_status_id) - parent_status = FetchRemoteStatusService.new.call(parent_url, **options.deep_symbolize_keys) + child_status = Status.find(child_status_id) + return if child_status.in_reply_to_id.present? + + parent_status = ActivityPub::TagManager.instance.uri_to_resource(parent_url, Status) + parent_status ||= FetchRemoteStatusService.new.call(parent_url, **options.deep_symbolize_keys) return if parent_status.nil? child_status.thread = parent_status child_status.save! + + DistributionWorker.perform_async(child_status_id, { 'skip_notifications' => true }) if child_status.within_realtime_window? rescue ActiveRecord::RecordNotFound true end diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index f6c24754c..8425f2127 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -23,6 +23,109 @@ RSpec.describe ActivityPub::Activity::Create do stub_request(:get, 'http://example.com/emojib.png').to_return(body: attachment_fixture('emojo.png'), headers: { 'Content-Type' => 'application/octet-stream' }) end + describe 'processing posts received out of order' do + let(:follower) { Fabricate(:account, username: 'bob') } + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'), + type: 'Note', + to: [ + 'https://www.w3.org/ns/activitystreams#Public', + ActivityPub::TagManager.instance.uri_for(follower), + ], + content: '@bob lorem ipsum', + published: 1.hour.ago.utc.iso8601, + updated: 1.hour.ago.utc.iso8601, + tag: { + type: 'Mention', + href: ActivityPub::TagManager.instance.uri_for(follower), + }, + } + end + + let(:reply_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), 'reply'].join('/'), + type: 'Note', + inReplyTo: object_json[:id], + to: [ + 'https://www.w3.org/ns/activitystreams#Public', + ActivityPub::TagManager.instance.uri_for(follower), + ], + content: '@bob lorem ipsum', + published: Time.now.utc.iso8601, + updated: Time.now.utc.iso8601, + tag: { + type: 'Mention', + href: ActivityPub::TagManager.instance.uri_for(follower), + }, + } + end + + def activity_for_object(json) + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: [json[:id], 'activity'].join('/'), + type: 'Create', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: json, + }.with_indifferent_access + end + + before do + follower.follow!(sender) + end + + around do |example| + Sidekiq::Testing.fake! do + example.run + Sidekiq::Worker.clear_all + end + end + + it 'correctly processes posts and inserts them in timelines', :aggregate_failures do + # Simulate a temporary failure preventing from fetching the parent post + stub_request(:get, object_json[:id]).to_return(status: 500) + + # When receiving the reply… + described_class.new(activity_for_object(reply_json), sender, delivery: true).perform + + # NOTE: Refering explicitly to the workers is a bit awkward + DistributionWorker.drain + FeedInsertWorker.drain + + # …it creates a status with an unknown parent + reply = Status.find_by(uri: reply_json[:id]) + expect(reply.reply?).to be true + expect(reply.in_reply_to_id).to be_nil + + # …and creates a notification + expect(LocalNotificationWorker.jobs.size).to eq 1 + + # …but does not insert it into timelines + expect(redis.zscore(FeedManager.instance.key(:home, follower.id), reply.id)).to be_nil + + # When receiving the parent… + described_class.new(activity_for_object(object_json), sender, delivery: true).perform + + Sidekiq::Worker.drain_all + + # …it creates a status and insert it into timelines + parent = Status.find_by(uri: object_json[:id]) + expect(parent.reply?).to be false + expect(parent.in_reply_to_id).to be_nil + expect(reply.reload.in_reply_to_id).to eq parent.id + + # Check that the both statuses have been inserted into the home feed + expect(redis.zscore(FeedManager.instance.key(:home, follower.id), parent.id)).to be_within(0.1).of(parent.id.to_f) + expect(redis.zscore(FeedManager.instance.key(:home, follower.id), reply.id)).to be_within(0.1).of(reply.id.to_f) + + # Creates two notifications + expect(Notification.count).to eq 2 + end + end + describe '#perform' do context 'when fetching' do subject { described_class.new(json, sender) } From 1076a6cd62a80f1e331980e7c81174fa1a67f435 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 6 Nov 2023 10:28:14 +0100 Subject: [PATCH 018/117] Fix incoming status creation date not being restricted to standard ISO8601 (#27655) --- app/lib/activitypub/parser/status_parser.rb | 3 +- spec/lib/activitypub/activity/create_spec.rb | 48 ++++++++++++++++++-- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb index 3ba154d01..45f5fc5bf 100644 --- a/app/lib/activitypub/parser/status_parser.rb +++ b/app/lib/activitypub/parser/status_parser.rb @@ -53,7 +53,8 @@ class ActivityPub::Parser::StatusParser end def created_at - @object['published']&.to_datetime + datetime = @object['published']&.to_datetime + datetime if datetime.present? && (0..9999).cover?(datetime.year) rescue ArgumentError nil end diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 8425f2127..7594efd59 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -134,6 +134,46 @@ RSpec.describe ActivityPub::Activity::Create do subject.perform end + context 'when object publication date is below ISO8601 range' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + published: '-0977-11-03T08:31:22Z', + } + end + + it 'creates status with a valid creation date', :aggregate_failures do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.text).to eq 'Lorem ipsum' + + expect(status.created_at).to be_within(30).of(Time.now.utc) + end + end + + context 'when object publication date is above ISO8601 range' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + published: '10000-11-03T08:31:22Z', + } + end + + it 'creates status with a valid creation date', :aggregate_failures do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.text).to eq 'Lorem ipsum' + + expect(status.created_at).to be_within(30).of(Time.now.utc) + end + end + context 'when object has been edited' do let(:object_json) do { @@ -145,18 +185,16 @@ RSpec.describe ActivityPub::Activity::Create do } end - it 'creates status' do + it 'creates status with appropriate creation and edition dates', :aggregate_failures do status = sender.statuses.first expect(status).to_not be_nil expect(status.text).to eq 'Lorem ipsum' - end - it 'marks status as edited' do - status = sender.statuses.first + expect(status.created_at).to eq '2022-01-22T15:00:00Z'.to_datetime - expect(status).to_not be_nil expect(status.edited?).to be true + expect(status.edited_at).to eq '2022-01-22T16:00:00Z'.to_datetime end end From 8d02e58ff42d28f62a354c1ce8282924d54c455f Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 16 Nov 2023 14:43:02 +0100 Subject: [PATCH 019/117] Fix upper border radius of onboarding columns (#27890) --- app/javascript/styles/mastodon/components.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 8c88b7dd6..fbad5e2db 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2239,8 +2239,7 @@ $ui-header-height: 55px; > .scrollable { background: $ui-base-color; - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; + border-radius: 0 0 4px 4px; } } From 252ea2fc67a8e95532c1887fc9f2842ddae189f3 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 27 Oct 2023 16:55:00 +0200 Subject: [PATCH 020/117] Have `Follow` activities bypass availability (#27586) Co-authored-by: Claire --- app/services/follow_service.rb | 2 +- app/workers/activitypub/delivery_worker.rb | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index feea40e3c..1aa0241fe 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -71,7 +71,7 @@ class FollowService < BaseService if @target_account.local? LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request') elsif @target_account.activitypub? - ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url) + ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url, { 'bypass_availability' => true }) end follow_request diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb index 7c1c14766..376c237a9 100644 --- a/app/workers/activitypub/delivery_worker.rb +++ b/app/workers/activitypub/delivery_worker.rb @@ -23,9 +23,10 @@ class ActivityPub::DeliveryWorker HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze def perform(json, source_account_id, inbox_url, options = {}) - return unless DeliveryFailureTracker.available?(inbox_url) - @options = options.with_indifferent_access + + return unless @options[:bypass_availability] || DeliveryFailureTracker.available?(inbox_url) + @json = json @source_account = Account.find(source_account_id) @inbox_url = inbox_url From e11100d7824258cafbd08b56945f340720a8029a Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 27 Nov 2023 14:25:54 +0100 Subject: [PATCH 021/117] Clamp dates when serializing to Elasticsearch API (#28081) --- app/chewy/accounts_index.rb | 4 +++- app/chewy/concerns/datetime_clamping_concern.rb | 14 ++++++++++++++ app/chewy/public_statuses_index.rb | 4 +++- app/chewy/statuses_index.rb | 4 +++- app/chewy/tags_index.rb | 4 +++- 5 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 app/chewy/concerns/datetime_clamping_concern.rb diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb index 00db257ac..59f2f991f 100644 --- a/app/chewy/accounts_index.rb +++ b/app/chewy/accounts_index.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class AccountsIndex < Chewy::Index + include DatetimeClampingConcern + settings index: index_preset(refresh_interval: '30s'), analysis: { filter: { english_stop: { @@ -60,7 +62,7 @@ class AccountsIndex < Chewy::Index field(:following_count, type: 'long') field(:followers_count, type: 'long') field(:properties, type: 'keyword', value: ->(account) { account.searchable_properties }) - field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }) + field(:last_status_at, type: 'date', value: ->(account) { clamp_date(account.last_status_at || account.created_at) }) field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' } field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' } field(:text, type: 'text', analyzer: 'verbatim', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' } diff --git a/app/chewy/concerns/datetime_clamping_concern.rb b/app/chewy/concerns/datetime_clamping_concern.rb new file mode 100644 index 000000000..7f176b6e5 --- /dev/null +++ b/app/chewy/concerns/datetime_clamping_concern.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module DatetimeClampingConcern + extend ActiveSupport::Concern + + MIN_ISO8601_DATETIME = '0000-01-01T00:00:00Z'.to_datetime.freeze + MAX_ISO8601_DATETIME = '9999-12-31T23:59:59Z'.to_datetime.freeze + + class_methods do + def clamp_date(datetime) + datetime.clamp(MIN_ISO8601_DATETIME, MAX_ISO8601_DATETIME) + end + end +end diff --git a/app/chewy/public_statuses_index.rb b/app/chewy/public_statuses_index.rb index 4be204d4a..076f72e52 100644 --- a/app/chewy/public_statuses_index.rb +++ b/app/chewy/public_statuses_index.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class PublicStatusesIndex < Chewy::Index + include DatetimeClampingConcern + settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: { filter: { english_stop: { @@ -62,6 +64,6 @@ class PublicStatusesIndex < Chewy::Index field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) }) field(:language, type: 'keyword') field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties }) - field(:created_at, type: 'date') + field(:created_at, type: 'date', value: ->(status) { clamp_date(status.created_at) }) end end diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb index 6b25dc9df..c717de66f 100644 --- a/app/chewy/statuses_index.rb +++ b/app/chewy/statuses_index.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class StatusesIndex < Chewy::Index + include DatetimeClampingConcern + settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: { filter: { english_stop: { @@ -60,6 +62,6 @@ class StatusesIndex < Chewy::Index field(:searchable_by, type: 'long', value: ->(status) { status.searchable_by }) field(:language, type: 'keyword') field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties }) - field(:created_at, type: 'date') + field(:created_at, type: 'date', value: ->(status) { clamp_date(status.created_at) }) end end diff --git a/app/chewy/tags_index.rb b/app/chewy/tags_index.rb index 5b6349a96..c99218a47 100644 --- a/app/chewy/tags_index.rb +++ b/app/chewy/tags_index.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class TagsIndex < Chewy::Index + include DatetimeClampingConcern + settings index: index_preset(refresh_interval: '30s'), analysis: { analyzer: { content: { @@ -42,6 +44,6 @@ class TagsIndex < Chewy::Index field(:name, type: 'text', analyzer: 'content', value: :display_name) { field(:edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content') } field(:reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }) field(:usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts }) - field(:last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }) + field(:last_status_at, type: 'date', value: ->(tag) { clamp_date(tag.last_status_at || tag.created_at) }) end end From 09115731d617c556135ac465421645ed54e92ca9 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 24 Nov 2023 10:31:28 +0100 Subject: [PATCH 022/117] Change GIF max matrix size error to explicitly mention GIF files (#27927) --- app/models/concerns/attachmentable.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/models/concerns/attachmentable.rb b/app/models/concerns/attachmentable.rb index c0ee1bdce..4cdbdeb47 100644 --- a/app/models/concerns/attachmentable.rb +++ b/app/models/concerns/attachmentable.rb @@ -52,9 +52,13 @@ module Attachmentable return if attachment.blank? || !/image.*/.match?(attachment.content_type) || attachment.queued_for_write[:original].blank? width, height = FastImage.size(attachment.queued_for_write[:original].path) - matrix_limit = attachment.content_type == 'image/gif' ? GIF_MATRIX_LIMIT : MAX_MATRIX_LIMIT + return unless width.present? && height.present? - raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported" if width.present? && height.present? && (width * height > matrix_limit) + if attachment.content_type == 'image/gif' && width * height > GIF_MATRIX_LIMIT + raise Mastodon::DimensionsValidationError, "#{width}x#{height} GIF files are not supported" + elsif width * height > MAX_MATRIX_LIMIT + raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported" + end end def appropriate_extension(attachment) From 7b9496322fd726a70c6fec1eee77c8cd04f0fd9e Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 30 Nov 2023 12:45:54 +0100 Subject: [PATCH 023/117] Change dismissed banners to be stored server-side (#27055) --- .../components/dismissable_banner.tsx | 25 ++++++++++++++++--- app/javascript/mastodon/reducers/settings.js | 9 +++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/components/dismissable_banner.tsx b/app/javascript/mastodon/components/dismissable_banner.tsx index 04a28e3cb..698469140 100644 --- a/app/javascript/mastodon/components/dismissable_banner.tsx +++ b/app/javascript/mastodon/components/dismissable_banner.tsx @@ -1,9 +1,16 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call, + @typescript-eslint/no-unsafe-return, + @typescript-eslint/no-unsafe-assignment, + @typescript-eslint/no-unsafe-member-access + -- the settings store is not yet typed */ import type { PropsWithChildren } from 'react'; -import { useCallback, useState } from 'react'; +import { useCallback, useState, useEffect } from 'react'; import { defineMessages, useIntl } from 'react-intl'; +import { changeSetting } from 'mastodon/actions/settings'; import { bannerSettings } from 'mastodon/settings'; +import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { IconButton } from './icon_button'; @@ -19,13 +26,25 @@ export const DismissableBanner: React.FC> = ({ id, children, }) => { - const [visible, setVisible] = useState(!bannerSettings.get(id)); + const dismissed = useAppSelector((state) => + state.settings.getIn(['dismissed_banners', id], false), + ); + const dispatch = useAppDispatch(); + + const [visible, setVisible] = useState(!bannerSettings.get(id) && !dismissed); const intl = useIntl(); const handleDismiss = useCallback(() => { setVisible(false); bannerSettings.set(id, true); - }, [id]); + dispatch(changeSetting(['dismissed_banners', id], true)); + }, [id, dispatch]); + + useEffect(() => { + if (!visible && !dismissed) { + dispatch(changeSetting(['dismissed_banners', id], true)); + } + }, [id, dispatch, visible, dismissed]); if (!visible) { return null; diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 07d1bda0f..a605ecbb8 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -100,6 +100,15 @@ const initialState = ImmutableMap({ body: '', }), }), + + dismissed_banners: ImmutableMap({ + 'public_timeline': false, + 'community_timeline': false, + 'home.explore_prompt': false, + 'explore/links': false, + 'explore/statuses': false, + 'explore/tags': false, + }), }); const defaultColumns = fromJS([ From 4b8fe9df73cf27db68f3c92178e37eff9deb3bd3 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 27 Nov 2023 15:00:52 +0100 Subject: [PATCH 024/117] Bump version to v4.2.2 --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ SECURITY.md | 2 +- docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 2 +- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9303f011..54bc2b403 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ All notable changes to this project will be documented in this file. +## [4.2.2] - 2023-12-04 + +### Changed + +- Change dismissed banners to be stored server-side ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27055)) +- Change GIF max matrix size error to explicitly mention GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27927)) +- Change `Follow` activities delivery to bypass availability check ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/27586)) +- Change single-column navigation notice to be displayed outside of the logo container ([renchap](https://github.com/mastodon/mastodon/pull/27462), [renchap](https://github.com/mastodon/mastodon/pull/27476)) +- Change Content-Security-Policy to be tighter on media paths ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26889)) +- Change post language code to include country code when relevant ([gunchleoc](https://github.com/mastodon/mastodon/pull/27099), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27207)) + +### Fixed + +- Fix upper border radius of onboarding columns ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27890)) +- Fix incoming status creation date not being restricted to standard ISO8601 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27655), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28081)) +- Fix some posts from threads received out-of-order sometimes not being inserted into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27653)) +- Fix posts from force-sensitized accounts being able to trend ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27620)) +- Fix error when trying to delete already-deleted file with OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27569)) +- Fix batch attachment deletion when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27554)) +- Fix processing LDSigned activities from actors with unknown public keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27474)) +- Fix error and incorrect URLs in `/api/v1/accounts/:id/featured_tags` for remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27459)) +- Fix report processing notice not mentioning the report number when performing a custom action ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27442)) +- Fix handling of `inLanguage` attribute in preview card processing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27423)) +- Fix own posts being removed from home timeline when unfollowing a used hashtag ([kmycode](https://github.com/mastodon/mastodon/pull/27391)) +- Fix some link anchors being recognized as hashtags ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27271), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27584)) +- Fix format-dependent redirects being cached regardless of requested format ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27634)) + ## [4.2.1] - 2023-10-10 ### Added diff --git a/SECURITY.md b/SECURITY.md index 3e13377db..954ff73a2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -17,6 +17,6 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through | ------- | ---------------- | | 4.2.x | Yes | | 4.1.x | Yes | -| 4.0.x | Until 2023-10-31 | +| 4.0.x | No | | 3.5.x | Until 2023-12-31 | | < 3.5 | No | diff --git a/docker-compose.yml b/docker-compose.yml index 1a180b089..efb8e528e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,7 @@ services: web: build: . - image: ghcr.io/mastodon/mastodon:v4.2.1 + image: ghcr.io/mastodon/mastodon:v4.2.2 restart: always env_file: .env.production command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" @@ -77,7 +77,7 @@ services: streaming: build: . - image: ghcr.io/mastodon/mastodon:v4.2.1 + image: ghcr.io/mastodon/mastodon:v4.2.2 restart: always env_file: .env.production command: node ./streaming @@ -95,7 +95,7 @@ services: sidekiq: build: . - image: ghcr.io/mastodon/mastodon:v4.2.1 + image: ghcr.io/mastodon/mastodon:v4.2.2 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 00538d07c..7d5578d82 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 1 + 2 end def default_prerelease From 71b60b09f494ddd4670128cf252b6372a293a51e Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 5 Dec 2023 14:15:33 +0100 Subject: [PATCH 025/117] Update dependency json-ld to v3.3.1 --- Gemfile.lock | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a003cd18d..76925a2bf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -148,6 +148,7 @@ GEM net-http-persistent (~> 4.0) nokogiri (~> 1, >= 1.10.8) base64 (0.1.1) + bcp47_spec (0.2.1) bcrypt (3.1.18) better_errors (2.10.1) erubi (>= 1.0.0) @@ -377,19 +378,19 @@ GEM ipaddress (0.8.3) jmespath (1.6.2) json (2.6.3) - json-canonicalization (0.3.2) + json-canonicalization (1.0.0) json-jwt (1.15.3) activesupport (>= 4.2) aes_key_wrap bindata httpclient - json-ld (3.2.5) + json-ld (3.3.1) htmlentities (~> 4.3) - json-canonicalization (~> 0.3, >= 0.3.2) + json-canonicalization (~> 1.0) link_header (~> 0.0, >= 0.0.8) multi_json (~> 1.15) rack (>= 2.2, < 4) - rdf (~> 3.2, >= 3.2.10) + rdf (~> 3.3) json-ld-preloaded (3.2.2) json-ld (~> 3.2) rdf (~> 3.2) @@ -593,7 +594,8 @@ GEM zeitwerk (~> 2.5) rainbow (3.1.1) rake (13.0.6) - rdf (3.2.11) + rdf (3.3.1) + bcp47_spec (~> 0.2) link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.6.1) rdf (~> 3.2) From 90371a4fc4da90c692e632b3231d804e310de916 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 5 Dec 2023 14:21:18 +0100 Subject: [PATCH 026/117] Bump version to v4.2.3 --- CHANGELOG.md | 6 ++++++ docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54bc2b403..5f970519e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## [4.2.3] - 2023-12-05 + +### Fixed + +- Fix dependency on `json-canonicalization` version that has been made unavailable since last release + ## [4.2.2] - 2023-12-04 ### Changed diff --git a/docker-compose.yml b/docker-compose.yml index efb8e528e..dda71cb09 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,7 @@ services: web: build: . - image: ghcr.io/mastodon/mastodon:v4.2.2 + image: ghcr.io/mastodon/mastodon:v4.2.3 restart: always env_file: .env.production command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" @@ -77,7 +77,7 @@ services: streaming: build: . - image: ghcr.io/mastodon/mastodon:v4.2.2 + image: ghcr.io/mastodon/mastodon:v4.2.3 restart: always env_file: .env.production command: node ./streaming @@ -95,7 +95,7 @@ services: sidekiq: build: . - image: ghcr.io/mastodon/mastodon:v4.2.2 + image: ghcr.io/mastodon/mastodon:v4.2.3 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 7d5578d82..2e68df474 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 2 + 3 end def default_prerelease From d7875adad242881876e316477b0610cbad466893 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 19 Dec 2023 11:27:37 +0100 Subject: [PATCH 027/117] Fix call to inefficient `delete_matched` cache method in domain blocks (#28367) --- .../api/v1/accounts/notes_controller.rb | 2 +- .../api/v1/accounts/pins_controller.rb | 2 +- .../v1/accounts/relationships_controller.rb | 5 +- app/controllers/api/v1/accounts_controller.rb | 2 +- .../api/v1/follow_requests_controller.rb | 4 +- app/controllers/relationships_controller.rb | 2 +- app/models/account_domain_block.rb | 10 +-- app/models/concerns/account_interactions.rb | 6 -- app/models/concerns/relationship_cacheable.rb | 4 +- .../account_relationships_presenter.rb | 63 ++++++++++++++----- .../account_relationships_presenter_spec.rb | 61 +++++++++++++----- 11 files changed, 104 insertions(+), 57 deletions(-) diff --git a/app/controllers/api/v1/accounts/notes_controller.rb b/app/controllers/api/v1/accounts/notes_controller.rb index 032e807d1..6d115631a 100644 --- a/app/controllers/api/v1/accounts/notes_controller.rb +++ b/app/controllers/api/v1/accounts/notes_controller.rb @@ -25,6 +25,6 @@ class Api::V1::Accounts::NotesController < Api::BaseController end def relationships_presenter - AccountRelationshipsPresenter.new([@account.id], current_user.account_id) + AccountRelationshipsPresenter.new([@account], current_user.account_id) end end diff --git a/app/controllers/api/v1/accounts/pins_controller.rb b/app/controllers/api/v1/accounts/pins_controller.rb index 73f845c61..0eb13c048 100644 --- a/app/controllers/api/v1/accounts/pins_controller.rb +++ b/app/controllers/api/v1/accounts/pins_controller.rb @@ -25,6 +25,6 @@ class Api::V1::Accounts::PinsController < Api::BaseController end def relationships_presenter - AccountRelationshipsPresenter.new([@account.id], current_user.account_id) + AccountRelationshipsPresenter.new([@account], current_user.account_id) end end diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index 503f85c97..038d6700f 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -5,11 +5,10 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController before_action :require_user! def index - accounts = Account.without_suspended.where(id: account_ids).select('id') + @accounts = Account.without_suspended.where(id: account_ids).select(:id, :domain).to_a # .where doesn't guarantee that our results are in the same order # we requested them, so return the "right" order to the requestor. - @accounts = accounts.index_by(&:id).values_at(*account_ids).compact - render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships + render json: @accounts.index_by(&:id).values_at(*account_ids).compact, each_serializer: REST::RelationshipSerializer, relationships: relationships end private diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index ddb94d5ca..321a57ac5 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -86,7 +86,7 @@ class Api::V1::AccountsController < Api::BaseController end def relationships(**options) - AccountRelationshipsPresenter.new([@account.id], current_user.account_id, **options) + AccountRelationshipsPresenter.new([@account], current_user.account_id, **options) end def account_params diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index 7c197ce6b..ee717ebbc 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -25,11 +25,11 @@ class Api::V1::FollowRequestsController < Api::BaseController private def account - Account.find(params[:id]) + @account ||= Account.find(params[:id]) end def relationships(**options) - AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, **options) + AccountRelationshipsPresenter.new([account], current_user.account_id, **options) end def load_accounts diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index e87b5a656..dd794f319 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -33,7 +33,7 @@ class RelationshipsController < ApplicationController end def set_relationships - @relationships = AccountRelationshipsPresenter.new(@accounts.pluck(:id), current_user.account_id) + @relationships = AccountRelationshipsPresenter.new(@accounts, current_user.account_id) end def form_account_batch_params diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb index af1e6a68d..db2e37184 100644 --- a/app/models/account_domain_block.rb +++ b/app/models/account_domain_block.rb @@ -18,16 +18,12 @@ class AccountDomainBlock < ApplicationRecord belongs_to :account validates :domain, presence: true, uniqueness: { scope: :account_id }, domain: true - after_commit :remove_blocking_cache - after_commit :remove_relationship_cache + after_commit :invalidate_domain_blocking_cache private - def remove_blocking_cache + def invalidate_domain_blocking_cache Rails.cache.delete("exclude_domains_for:#{account_id}") - end - - def remove_relationship_cache - Rails.cache.delete_matched("relationship:#{account_id}:*") + Rails.cache.delete(['exclude_domains', account_id, domain]) end end diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 3c64ebd9f..2de15031f 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -60,12 +60,6 @@ module AccountInteractions end end - def domain_blocking_map(target_account_ids, account_id) - accounts_map = Account.where(id: target_account_ids).select('id, domain').each_with_object({}) { |a, h| h[a.id] = a.domain } - blocked_domains = domain_blocking_map_by_domain(accounts_map.values.compact, account_id) - accounts_map.reduce({}) { |h, (id, domain)| h.merge(id => blocked_domains[domain]) } - end - def domain_blocking_map_by_domain(target_domains, account_id) follow_mapping(AccountDomainBlock.where(account_id: account_id, domain: target_domains), :domain) end diff --git a/app/models/concerns/relationship_cacheable.rb b/app/models/concerns/relationship_cacheable.rb index 0d9359f7e..c32a8d62c 100644 --- a/app/models/concerns/relationship_cacheable.rb +++ b/app/models/concerns/relationship_cacheable.rb @@ -10,7 +10,7 @@ module RelationshipCacheable private def remove_relationship_cache - Rails.cache.delete("relationship:#{account_id}:#{target_account_id}") - Rails.cache.delete("relationship:#{target_account_id}:#{account_id}") + Rails.cache.delete(['relationship', account_id, target_account_id]) + Rails.cache.delete(['relationship', target_account_id, account_id]) end end diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb index 5d2b5435d..8482ef54d 100644 --- a/app/presenters/account_relationships_presenter.rb +++ b/app/presenters/account_relationships_presenter.rb @@ -5,8 +5,9 @@ class AccountRelationshipsPresenter :muting, :requested, :requested_by, :domain_blocking, :endorsed, :account_note - def initialize(account_ids, current_account_id, **options) - @account_ids = account_ids.map { |a| a.is_a?(Account) ? a.id : a.to_i } + def initialize(accounts, current_account_id, **options) + @accounts = accounts.to_a + @account_ids = @accounts.pluck(:id) @current_account_id = current_account_id @following = cached[:following].merge(Account.following_map(@uncached_account_ids, @current_account_id)) @@ -16,10 +17,11 @@ class AccountRelationshipsPresenter @muting = cached[:muting].merge(Account.muting_map(@uncached_account_ids, @current_account_id)) @requested = cached[:requested].merge(Account.requested_map(@uncached_account_ids, @current_account_id)) @requested_by = cached[:requested_by].merge(Account.requested_by_map(@uncached_account_ids, @current_account_id)) - @domain_blocking = cached[:domain_blocking].merge(Account.domain_blocking_map(@uncached_account_ids, @current_account_id)) @endorsed = cached[:endorsed].merge(Account.endorsed_map(@uncached_account_ids, @current_account_id)) @account_note = cached[:account_note].merge(Account.account_note_map(@uncached_account_ids, @current_account_id)) + @domain_blocking = domain_blocking_map + cache_uncached! @following.merge!(options[:following_map] || {}) @@ -36,6 +38,31 @@ class AccountRelationshipsPresenter private + def domain_blocking_map + target_domains = @accounts.pluck(:domain).compact.uniq + blocks_by_domain = {} + + # Fetch from cache + cache_keys = target_domains.map { |domain| domain_cache_key(domain) } + Rails.cache.read_multi(*cache_keys).each do |key, blocking| + blocks_by_domain[key.last] = blocking + end + + uncached_domains = target_domains - blocks_by_domain.keys + + # Read uncached values from database + AccountDomainBlock.where(account_id: @current_account_id, domain: uncached_domains).pluck(:domain).each do |domain| + blocks_by_domain[domain] = true + end + + # Write database reads to cache + to_cache = uncached_domains.to_h { |domain| [domain_cache_key(domain), blocks_by_domain[domain]] } + Rails.cache.write_multi(to_cache, expires_in: 1.day) + + # Return formatted value + @accounts.each_with_object({}) { |account, h| h[account.id] = blocks_by_domain[account.domain] } + end + def cached return @cached if defined?(@cached) @@ -47,28 +74,23 @@ class AccountRelationshipsPresenter muting: {}, requested: {}, requested_by: {}, - domain_blocking: {}, endorsed: {}, account_note: {}, } - @uncached_account_ids = [] + @uncached_account_ids = @account_ids.uniq - @account_ids.each do |account_id| - maps_for_account = Rails.cache.read("relationship:#{@current_account_id}:#{account_id}") - - if maps_for_account.is_a?(Hash) - @cached.deep_merge!(maps_for_account) - else - @uncached_account_ids << account_id - end + cache_ids = @account_ids.map { |account_id| relationship_cache_key(account_id) } + Rails.cache.read_multi(*cache_ids).each do |key, maps_for_account| + @cached.deep_merge!(maps_for_account) + @uncached_account_ids.delete(key.last) end @cached end def cache_uncached! - @uncached_account_ids.each do |account_id| + to_cache = @uncached_account_ids.to_h do |account_id| maps_for_account = { following: { account_id => following[account_id] }, followed_by: { account_id => followed_by[account_id] }, @@ -77,12 +99,21 @@ class AccountRelationshipsPresenter muting: { account_id => muting[account_id] }, requested: { account_id => requested[account_id] }, requested_by: { account_id => requested_by[account_id] }, - domain_blocking: { account_id => domain_blocking[account_id] }, endorsed: { account_id => endorsed[account_id] }, account_note: { account_id => account_note[account_id] }, } - Rails.cache.write("relationship:#{@current_account_id}:#{account_id}", maps_for_account, expires_in: 1.day) + [relationship_cache_key(account_id), maps_for_account] end + + Rails.cache.write_multi(to_cache, expires_in: 1.day) + end + + def domain_cache_key(domain) + ['exclude_domains', @current_account_id, domain] + end + + def relationship_cache_key(account_id) + ['relationship', @current_account_id, account_id] end end diff --git a/spec/presenters/account_relationships_presenter_spec.rb b/spec/presenters/account_relationships_presenter_spec.rb index 5c2ba54e0..282cae4f0 100644 --- a/spec/presenters/account_relationships_presenter_spec.rb +++ b/spec/presenters/account_relationships_presenter_spec.rb @@ -5,30 +5,57 @@ require 'rails_helper' RSpec.describe AccountRelationshipsPresenter do describe '.initialize' do before do - allow(Account).to receive(:following_map).with(account_ids, current_account_id).and_return(default_map) - allow(Account).to receive(:followed_by_map).with(account_ids, current_account_id).and_return(default_map) - allow(Account).to receive(:blocking_map).with(account_ids, current_account_id).and_return(default_map) - allow(Account).to receive(:muting_map).with(account_ids, current_account_id).and_return(default_map) - allow(Account).to receive(:requested_map).with(account_ids, current_account_id).and_return(default_map) - allow(Account).to receive(:requested_by_map).with(account_ids, current_account_id).and_return(default_map) - allow(Account).to receive(:domain_blocking_map).with(account_ids, current_account_id).and_return(default_map) + allow(Account).to receive(:following_map).with(accounts.pluck(:id), current_account_id).and_return(default_map) + allow(Account).to receive(:followed_by_map).with(accounts.pluck(:id), current_account_id).and_return(default_map) + allow(Account).to receive(:blocking_map).with(accounts.pluck(:id), current_account_id).and_return(default_map) + allow(Account).to receive(:muting_map).with(accounts.pluck(:id), current_account_id).and_return(default_map) + allow(Account).to receive(:requested_map).with(accounts.pluck(:id), current_account_id).and_return(default_map) + allow(Account).to receive(:requested_by_map).with(accounts.pluck(:id), current_account_id).and_return(default_map) end - let(:presenter) { described_class.new(account_ids, current_account_id, **options) } + let(:presenter) { described_class.new(accounts, current_account_id, **options) } let(:current_account_id) { Fabricate(:account).id } - let(:account_ids) { [Fabricate(:account).id] } - let(:default_map) { { 1 => true } } + let(:accounts) { [Fabricate(:account)] } + let(:default_map) { { accounts[0].id => true } } context 'when options are not set' do let(:options) { {} } it 'sets default maps' do - expect(presenter.following).to eq default_map - expect(presenter.followed_by).to eq default_map - expect(presenter.blocking).to eq default_map - expect(presenter.muting).to eq default_map - expect(presenter.requested).to eq default_map - expect(presenter.domain_blocking).to eq default_map + expect(presenter).to have_attributes( + following: default_map, + followed_by: default_map, + blocking: default_map, + muting: default_map, + requested: default_map, + domain_blocking: { accounts[0].id => nil } + ) + end + end + + context 'with a warm cache' do + let(:options) { {} } + + before do + described_class.new(accounts, current_account_id, **options) + + allow(Account).to receive(:following_map).with([], current_account_id).and_return({}) + allow(Account).to receive(:followed_by_map).with([], current_account_id).and_return({}) + allow(Account).to receive(:blocking_map).with([], current_account_id).and_return({}) + allow(Account).to receive(:muting_map).with([], current_account_id).and_return({}) + allow(Account).to receive(:requested_map).with([], current_account_id).and_return({}) + allow(Account).to receive(:requested_by_map).with([], current_account_id).and_return({}) + end + + it 'sets returns expected values' do + expect(presenter).to have_attributes( + following: default_map, + followed_by: default_map, + blocking: default_map, + muting: default_map, + requested: default_map, + domain_blocking: { accounts[0].id => nil } + ) end end @@ -84,7 +111,7 @@ RSpec.describe AccountRelationshipsPresenter do let(:options) { { domain_blocking_map: { 7 => true } } } it 'sets @domain_blocking merged with default_map and options[:domain_blocking_map]' do - expect(presenter.domain_blocking).to eq default_map.merge(options[:domain_blocking_map]) + expect(presenter.domain_blocking).to eq({ accounts[0].id => nil }.merge(options[:domain_blocking_map])) end end end From 279be076794c21304aee2a1c1469a6f39b8fbe50 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 12 Dec 2023 09:29:46 +0100 Subject: [PATCH 028/117] Fix `LinkCrawlWorker` error when encountering empty OEmbed response (#28268) --- app/services/fetch_oembed_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/fetch_oembed_service.rb b/app/services/fetch_oembed_service.rb index 1ae592238..dc84b16b6 100644 --- a/app/services/fetch_oembed_service.rb +++ b/app/services/fetch_oembed_service.rb @@ -100,7 +100,7 @@ class FetchOEmbedService end def validate(oembed) - oembed if oembed[:version].to_s == '1.0' && oembed[:type].present? + oembed if oembed.present? && oembed[:version].to_s == '1.0' && oembed[:type].present? end def html From 7abc61887f30d7f1d1145e5be104a098b4b14f87 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 12 Dec 2023 15:31:37 +0100 Subject: [PATCH 029/117] Fix empty column explainer getting cropped under certain conditions (#28337) --- app/javascript/styles/mastodon/components.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index fbad5e2db..7d152fbf7 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -4493,11 +4493,6 @@ a.status-card { align-items: center; justify-content: center; - @supports (display: grid) { - // hack to fix Chrome <57 - contain: strict; - } - & > span { max-width: 500px; } From a12b7551cfa4a95ea8ca7ab1ae1a3d96d1a3ad24 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 13 Dec 2023 08:47:32 +0100 Subject: [PATCH 030/117] Fix N+1s because of association preloaders not actually getting called (#28339) --- app/lib/inline_renderer.rb | 4 ++-- app/models/announcement.rb | 4 ++-- app/models/concerns/account_search.rb | 4 ++-- app/models/notification.rb | 2 +- app/serializers/initial_state_serializer.rb | 4 ++-- app/services/account_search_service.rb | 2 +- app/services/batched_remove_status_service.rb | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/lib/inline_renderer.rb b/app/lib/inline_renderer.rb index eda3da2c2..0aebb13fc 100644 --- a/app/lib/inline_renderer.rb +++ b/app/lib/inline_renderer.rb @@ -37,13 +37,13 @@ class InlineRenderer private def preload_associations_for_status - ActiveRecord::Associations::Preloader.new(records: @object, associations: { + ActiveRecord::Associations::Preloader.new(records: [@object], associations: { active_mentions: :account, reblog: { active_mentions: :account, }, - }) + }).call end def current_user diff --git a/app/models/announcement.rb b/app/models/announcement.rb index c5d6dd62e..2cd7c1d5e 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -78,9 +78,9 @@ class Announcement < ApplicationRecord else scope.select("name, custom_emoji_id, count(*) as count, exists(select 1 from announcement_reactions r where r.account_id = #{account.id} and r.announcement_id = announcement_reactions.announcement_id and r.name = announcement_reactions.name) as me") end - end + end.to_a - ActiveRecord::Associations::Preloader.new(records: records, associations: :custom_emoji) + ActiveRecord::Associations::Preloader.new(records: records, associations: :custom_emoji).call records end diff --git a/app/models/concerns/account_search.rb b/app/models/concerns/account_search.rb index 9f7720f11..5bd624025 100644 --- a/app/models/concerns/account_search.rb +++ b/app/models/concerns/account_search.rb @@ -122,7 +122,7 @@ module AccountSearch tsquery = generate_query_for_search(terms) find_by_sql([BASIC_SEARCH_SQL, { limit: limit, offset: offset, tsquery: tsquery }]).tap do |records| - ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat) + ActiveRecord::Associations::Preloader.new(records: records, associations: [:account_stat, { user: :role }]).call end end @@ -131,7 +131,7 @@ module AccountSearch sql_template = following ? ADVANCED_SEARCH_WITH_FOLLOWING : ADVANCED_SEARCH_WITHOUT_FOLLOWING find_by_sql([sql_template, { id: account.id, limit: limit, offset: offset, tsquery: tsquery }]).tap do |records| - ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat) + ActiveRecord::Associations::Preloader.new(records: records, associations: [:account_stat, { user: :role }]).call end end diff --git a/app/models/notification.rb b/app/models/notification.rb index 60f834a63..54212d675 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -111,7 +111,7 @@ class Notification < ApplicationRecord # Instead of using the usual `includes`, manually preload each type. # If polymorphic associations are loaded with the usual `includes`, other types of associations will be loaded more. - ActiveRecord::Associations::Preloader.new(records: grouped_notifications, associations: associations) + ActiveRecord::Associations::Preloader.new(records: grouped_notifications, associations: associations).call end unique_target_statuses = notifications.filter_map(&:target_status).uniq diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index b707d6fcb..d1c03a413 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -86,8 +86,8 @@ class InitialStateSerializer < ActiveModel::Serializer ActiveRecord::Associations::Preloader.new( records: [object.current_account, object.admin, object.owner, object.disabled_account, object.moved_to_account].compact, - associations: [:account_stat, :user, { moved_to_account: [:account_stat, :user] }] - ) + associations: [:account_stat, { user: :role, moved_to_account: [:account_stat, { user: :role }] }] + ).call store[object.current_account.id.to_s] = ActiveModelSerializers::SerializableResource.new(object.current_account, serializer: REST::AccountSerializer) if object.current_account store[object.admin.id.to_s] = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) if object.admin diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb index b437ff475..7b85b09d8 100644 --- a/app/services/account_search_service.rb +++ b/app/services/account_search_service.rb @@ -218,7 +218,7 @@ class AccountSearchService < BaseService records = query_builder.build.limit(limit_for_non_exact_results).offset(offset).objects.compact - ActiveRecord::Associations::Preloader.new(records: records, associations: :account_stat) + ActiveRecord::Associations::Preloader.new(records: records, associations: [:account_stat, { user: :role }]).call records rescue Faraday::ConnectionFailed, Parslet::ParseFailed diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index c54cc1d35..de4ee16e9 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -11,7 +11,7 @@ class BatchedRemoveStatusService < BaseService ActiveRecord::Associations::Preloader.new( records: statuses, associations: options[:skip_side_effects] ? :reblogs : [:account, :tags, reblogs: :account] - ) + ).call statuses_and_reblogs = statuses.flat_map { |status| [status] + status.reblogs } @@ -23,7 +23,7 @@ class BatchedRemoveStatusService < BaseService ActiveRecord::Associations::Preloader.new( records: statuses_with_account_conversations, associations: [mentions: :account] - ) + ).call statuses_with_account_conversations.each(&:unlink_from_conversations!) From 0a01bc01d2fc8967f75cd24ab3347d13a9d7f211 Mon Sep 17 00:00:00 2001 From: MitarashiDango Date: Sat, 13 Jan 2024 00:58:28 +0900 Subject: [PATCH 031/117] Fix Undo Announce activity is not sent, when not followed by the reblogged post author (#18482) Co-authored-by: Claire --- app/lib/status_reach_finder.rb | 34 ++++++++++----------- app/services/reblog_service.rb | 6 +--- spec/lib/activitypub/tag_manager_spec.rb | 8 +++++ spec/services/reblog_service_spec.rb | 4 --- spec/services/remove_status_service_spec.rb | 18 +++++++++++ 5 files changed, 44 insertions(+), 26 deletions(-) diff --git a/app/lib/status_reach_finder.rb b/app/lib/status_reach_finder.rb index 36fb0e80f..17e42e3ec 100644 --- a/app/lib/status_reach_finder.rb +++ b/app/lib/status_reach_finder.rb @@ -16,28 +16,28 @@ class StatusReachFinder private def reached_account_inboxes + Account.where(id: reached_account_ids).inboxes + end + + def reached_account_ids # When the status is a reblog, there are no interactions with it # directly, we assume all interactions are with the original one if @status.reblog? - [] + [reblog_of_account_id] else - Account.where(id: reached_account_ids).inboxes - end - end - - def reached_account_ids - [ - replied_to_account_id, - reblog_of_account_id, - mentioned_account_ids, - reblogs_account_ids, - favourites_account_ids, - replies_account_ids, - ].tap do |arr| - arr.flatten! - arr.compact! - arr.uniq! + [ + replied_to_account_id, + reblog_of_account_id, + mentioned_account_ids, + reblogs_account_ids, + favourites_account_ids, + replies_account_ids, + ].tap do |arr| + arr.flatten! + arr.compact! + arr.uniq! + end end end diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index 6ec094474..c02674612 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -43,11 +43,7 @@ class ReblogService < BaseService def create_notification(reblog) reblogged_status = reblog.reblog - if reblogged_status.account.local? - LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name, 'reblog') - elsif reblogged_status.account.activitypub? && !reblogged_status.account.following?(reblog.account) - ActivityPub::DeliveryWorker.perform_async(build_json(reblog), reblog.account_id, reblogged_status.account.inbox_url) - end + LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name, 'reblog') if reblogged_status.account.local? end def bump_potential_friendship(account, reblog) diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb index 2bff125a6..55e9b4bb5 100644 --- a/spec/lib/activitypub/tag_manager_spec.rb +++ b/spec/lib/activitypub/tag_manager_spec.rb @@ -112,6 +112,14 @@ RSpec.describe ActivityPub::TagManager do expect(subject.cc(status)).to include(subject.uri_for(foo)) expect(subject.cc(status)).to_not include(subject.uri_for(alice)) end + + it 'returns poster of reblogged post, if reblog' do + bob = Fabricate(:account, username: 'bob', domain: 'example.com', inbox_url: 'http://example.com/bob') + alice = Fabricate(:account, username: 'alice') + status = Fabricate(:status, visibility: :public, account: bob) + reblog = Fabricate(:status, visibility: :public, account: alice, reblog: status) + expect(subject.cc(reblog)).to include(subject.uri_for(bob)) + end end describe '#local_uri?' do diff --git a/spec/services/reblog_service_spec.rb b/spec/services/reblog_service_spec.rb index 7b85e37ed..357b315af 100644 --- a/spec/services/reblog_service_spec.rb +++ b/spec/services/reblog_service_spec.rb @@ -86,9 +86,5 @@ RSpec.describe ReblogService, type: :service do it 'distributes to followers' do expect(ActivityPub::DistributionWorker).to have_received(:perform_async) end - - it 'sends an announce activity to the author' do - expect(a_request(:post, bob.inbox_url)).to have_been_made.once - end end end diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb index c19b4fac1..7e81f3f6a 100644 --- a/spec/services/remove_status_service_spec.rb +++ b/spec/services/remove_status_service_spec.rb @@ -110,4 +110,22 @@ RSpec.describe RemoveStatusService, type: :service do )).to have_been_made.once end end + + context 'when removed status is a reblog of a non-follower' do + let!(:original_status) { Fabricate(:status, account: bill, text: 'Hello ThisIsASecret', visibility: :public) } + let!(:status) { ReblogService.new.call(alice, original_status) } + + it 'sends Undo activity to followers' do + subject.call(status) + expect(a_request(:post, bill.inbox_url).with( + body: hash_including({ + 'type' => 'Undo', + 'object' => hash_including({ + 'type' => 'Announce', + 'object' => ActivityPub::TagManager.instance.uri_for(original_status), + }), + }) + )).to have_been_made.once + end + end end From e8c575414233df9d8c1a44140f91aa8e03bdc161 Mon Sep 17 00:00:00 2001 From: gunchleoc Date: Mon, 25 Sep 2023 12:59:37 +0200 Subject: [PATCH 032/117] Fix line wrapping of language selection button with long locale codes (#27100) --- app/javascript/styles/mastodon/components.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 7d152fbf7..5f773c1cf 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -284,6 +284,7 @@ font-size: 11px; padding: 0 3px; line-height: 27px; + white-space: nowrap; &:hover, &:active, From dfc8fcc6f0a266c72c1e37ee7b2982a5b9ee57f0 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 25 Sep 2023 15:06:57 +0200 Subject: [PATCH 033/117] Fix width of large text icon buttons (#27127) --- .../mastodon/features/compose/components/text_icon_button.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/compose/components/text_icon_button.jsx b/app/javascript/mastodon/features/compose/components/text_icon_button.jsx index 46b5d7fad..166d022b8 100644 --- a/app/javascript/mastodon/features/compose/components/text_icon_button.jsx +++ b/app/javascript/mastodon/features/compose/components/text_icon_button.jsx @@ -4,7 +4,7 @@ import { PureComponent } from 'react'; const iconStyle = { height: null, lineHeight: '27px', - width: `${18 * 1.28571429}px`, + minWidth: `${18 * 1.28571429}px`, }; export default class TextIconButton extends PureComponent { From 8f2dac05672094f2b76944ec1935fd0338333131 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 20 Oct 2023 04:08:13 +0200 Subject: [PATCH 034/117] Fix missing background behind dismissable banner in web UI (#27479) --- .../mastodon/features/explore/statuses.jsx | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/app/javascript/mastodon/features/explore/statuses.jsx b/app/javascript/mastodon/features/explore/statuses.jsx index f32a4a536..0d8d212b2 100644 --- a/app/javascript/mastodon/features/explore/statuses.jsx +++ b/app/javascript/mastodon/features/explore/statuses.jsx @@ -45,24 +45,20 @@ class Statuses extends PureComponent { const emptyMessage = ; return ( - <> - - - - - - + } + alwaysPrepend + timelineId='explore' + statusIds={statusIds} + scrollKey='explore-statuses' + hasMore={hasMore} + isLoading={isLoading} + onLoadMore={this.handleLoadMore} + emptyMessage={emptyMessage} + bindToDocument={!multiColumn} + withCounters + /> ); } From 3ecc991f6328170a452c69839719bfe92911cbc4 Mon Sep 17 00:00:00 2001 From: Brian Holley Date: Mon, 13 Nov 2023 16:39:54 -0800 Subject: [PATCH 035/117] Fix "Hide these posts from home" list setting not refreshing when switching lists (#27763) --- app/javascript/mastodon/features/list_timeline/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/list_timeline/index.jsx b/app/javascript/mastodon/features/list_timeline/index.jsx index a5acc416e..472c5ee8a 100644 --- a/app/javascript/mastodon/features/list_timeline/index.jsx +++ b/app/javascript/mastodon/features/list_timeline/index.jsx @@ -201,7 +201,7 @@ class ListTimeline extends PureComponent {
- + From 4d96d716c449d81eb1835b94329e00f316a48fa3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 22 Nov 2023 12:38:07 +0100 Subject: [PATCH 036/117] Fix unsupported time zone or locale preventing sign-up (#28035) Co-authored-by: Claire --- app/models/user.rb | 14 ++++++++++---- .../preferences/appearance_controller_spec.rb | 6 ------ spec/models/user_spec.rb | 18 ++++++++++++------ 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index fa445af81..309a00d1c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -96,11 +96,9 @@ class User < ApplicationRecord accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? && !Setting.require_invite_text } validates :invite_request, presence: true, on: :create, if: :invite_text_required? - validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale? validates_with BlacklistedEmailValidator, if: -> { ENV['EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION'] == 'true' || !confirmed? } validates_with EmailMxValidator, if: :validate_email_dns? validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create - validates :time_zone, inclusion: { in: ActiveSupport::TimeZone.all.map { |tz| tz.tzinfo.name } }, allow_blank: true # Honeypot/anti-spam fields attr_accessor :registration_form_time, :website, :confirm_password @@ -124,6 +122,8 @@ class User < ApplicationRecord before_validation :sanitize_languages before_validation :sanitize_role + before_validation :sanitize_time_zone + before_validation :sanitize_locale before_create :set_approved after_commit :send_pending_devise_notifications after_create_commit :trigger_webhooks @@ -451,9 +451,15 @@ class User < ApplicationRecord end def sanitize_role - return if role.nil? + self.role = nil if role.present? && role.everyone? + end - self.role = nil if role.everyone? + def sanitize_time_zone + self.time_zone = nil if time_zone.present? && ActiveSupport::TimeZone[time_zone].nil? + end + + def sanitize_locale + self.locale = nil if locale.present? && I18n.available_locales.exclude?(locale.to_sym) end def prepare_new_user! diff --git a/spec/controllers/settings/preferences/appearance_controller_spec.rb b/spec/controllers/settings/preferences/appearance_controller_spec.rb index 9a98a4188..083bf4954 100644 --- a/spec/controllers/settings/preferences/appearance_controller_spec.rb +++ b/spec/controllers/settings/preferences/appearance_controller_spec.rb @@ -31,11 +31,5 @@ describe Settings::Preferences::AppearanceController do expect(response).to redirect_to(settings_preferences_appearance_path) end - - it 'renders show on failure' do - put :update, params: { user: { locale: 'fake option' } } - - expect(response).to render_template('preferences/appearance/show') - end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index bb61c02a6..0dad96cc8 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -27,12 +27,6 @@ RSpec.describe User do expect(user).to model_have_error_on_field(:account) end - it 'is invalid without a valid locale' do - user = Fabricate.build(:user, locale: 'toto') - user.valid? - expect(user).to model_have_error_on_field(:locale) - end - it 'is invalid without a valid email' do user = Fabricate.build(:user, email: 'john@') user.valid? @@ -45,6 +39,18 @@ RSpec.describe User do expect(user.valid?).to be true end + it 'cleans out invalid locale' do + user = Fabricate.build(:user, locale: 'toto') + expect(user.valid?).to be true + expect(user.locale).to be_nil + end + + it 'cleans out invalid timezone' do + user = Fabricate.build(:user, time_zone: 'toto') + expect(user.valid?).to be true + expect(user.time_zone).to be_nil + end + it 'cleans out empty string from languages' do user = Fabricate.build(:user, chosen_languages: ['']) user.valid? From c609b726cba4874597d203c80cce647cd0932fbc Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 7 Dec 2023 10:05:08 +0100 Subject: [PATCH 037/117] Fix error when processing link preview with an array as `inLanguage` (#28252) --- app/lib/link_details_extractor.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/lib/link_details_extractor.rb b/app/lib/link_details_extractor.rb index a96612cab..bb031986d 100644 --- a/app/lib/link_details_extractor.rb +++ b/app/lib/link_details_extractor.rb @@ -37,6 +37,7 @@ class LinkDetailsExtractor def language lang = json['inLanguage'] + lang = lang.first if lang.is_a?(Array) lang.is_a?(Hash) ? (lang['alternateName'] || lang['name']) : lang end From 01caa18e5ba6eea8f102f31916ed47e5c781e862 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 2 Jan 2024 13:27:51 +0100 Subject: [PATCH 038/117] Fix streaming API redirection ignoring the port of `streaming_api_base_url` (#28558) --- app/controllers/api/v1/streaming_controller.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v1/streaming_controller.rb b/app/controllers/api/v1/streaming_controller.rb index 0cdd00d62..0cb6e856f 100644 --- a/app/controllers/api/v1/streaming_controller.rb +++ b/app/controllers/api/v1/streaming_controller.rb @@ -13,7 +13,9 @@ class Api::V1::StreamingController < Api::BaseController def streaming_api_url Addressable::URI.parse(request.url).tap do |uri| - uri.host = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url).host + base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url) + uri.host = base_url.host + uri.port = base_url.port end.to_s end end From c0a9db3611b3cda655409bbf28ec5531194cd20e Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 10 Jan 2024 16:05:46 +0100 Subject: [PATCH 039/117] Fix potential redirection loop of streaming endpoint (#28665) --- app/controllers/api/v1/streaming_controller.rb | 7 ++++++- spec/controllers/api/v1/streaming_controller_spec.rb | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/streaming_controller.rb b/app/controllers/api/v1/streaming_controller.rb index 0cb6e856f..adb14676e 100644 --- a/app/controllers/api/v1/streaming_controller.rb +++ b/app/controllers/api/v1/streaming_controller.rb @@ -2,7 +2,7 @@ class Api::V1::StreamingController < Api::BaseController def index - if Rails.configuration.x.streaming_api_base_url == request.host + if same_host? not_found else redirect_to streaming_api_url, status: 301, allow_other_host: true @@ -11,6 +11,11 @@ class Api::V1::StreamingController < Api::BaseController private + def same_host? + base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url) + request.host == base_url.host && request.port == (base_url.port || 80) + end + def streaming_api_url Addressable::URI.parse(request.url).tap do |uri| base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url) diff --git a/spec/controllers/api/v1/streaming_controller_spec.rb b/spec/controllers/api/v1/streaming_controller_spec.rb index 7014ed9b2..825bb1197 100644 --- a/spec/controllers/api/v1/streaming_controller_spec.rb +++ b/spec/controllers/api/v1/streaming_controller_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' describe Api::V1::StreamingController do around(:each) do |example| before = Rails.configuration.x.streaming_api_base_url - Rails.configuration.x.streaming_api_base_url = Rails.configuration.x.web_domain + Rails.configuration.x.streaming_api_base_url = "wss://#{Rails.configuration.x.web_domain}" example.run Rails.configuration.x.streaming_api_base_url = before end From 1998c561b2ffe7200781778f281fcc0890ebf6df Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 22 Dec 2023 19:56:22 +0100 Subject: [PATCH 040/117] Convert signature verification specs to request specs (#28443) --- .../concerns/signature_verification_spec.rb | 305 ---------------- spec/requests/signature_verification_spec.rb | 332 ++++++++++++++++++ 2 files changed, 332 insertions(+), 305 deletions(-) delete mode 100644 spec/controllers/concerns/signature_verification_spec.rb create mode 100644 spec/requests/signature_verification_spec.rb diff --git a/spec/controllers/concerns/signature_verification_spec.rb b/spec/controllers/concerns/signature_verification_spec.rb deleted file mode 100644 index 650cd21ea..000000000 --- a/spec/controllers/concerns/signature_verification_spec.rb +++ /dev/null @@ -1,305 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe SignatureVerification do - let(:wrapped_actor_class) do - Class.new do - attr_reader :wrapped_account - - def initialize(wrapped_account) - @wrapped_account = wrapped_account - end - - delegate :uri, :keypair, to: :wrapped_account - end - end - - controller(ApplicationController) do - include SignatureVerification - - before_action :require_actor_signature!, only: [:signature_required] - - def success - head 200 - end - - def alternative_success - head 200 - end - - def signature_required - head 200 - end - end - - before do - routes.draw do - match :via => [:get, :post], 'success' => 'anonymous#success' - match :via => [:get, :post], 'signature_required' => 'anonymous#signature_required' - end - end - - context 'without signature header' do - before do - get :success - end - - describe '#signed_request?' do - it 'returns false' do - expect(controller.signed_request?).to be false - end - end - - describe '#signed_request_account' do - it 'returns nil' do - expect(controller.signed_request_account).to be_nil - end - end - end - - context 'with signature header' do - let!(:author) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/actor') } - - context 'without body' do - before do - get :success - - fake_request = Request.new(:get, request.url) - fake_request.on_behalf_of(author) - - request.headers.merge!(fake_request.headers) - end - - describe '#signed_request?' do - it 'returns true' do - expect(controller.signed_request?).to be true - end - end - - describe '#signed_request_account' do - it 'returns an account' do - expect(controller.signed_request_account).to eq author - end - - it 'returns nil when path does not match' do - request.path = '/alternative-path' - expect(controller.signed_request_account).to be_nil - end - - it 'returns nil when method does not match' do - post :success - expect(controller.signed_request_account).to be_nil - end - end - end - - context 'with a valid actor that is not an Account' do - let(:actor) { wrapped_actor_class.new(author) } - - before do - get :success - - fake_request = Request.new(:get, request.url) - fake_request.on_behalf_of(author) - - request.headers.merge!(fake_request.headers) - - allow(ActivityPub::TagManager.instance).to receive(:uri_to_actor).with(anything) do - actor - end - end - - describe '#signed_request?' do - it 'returns true' do - expect(controller.signed_request?).to be true - end - end - - describe '#signed_request_account' do - it 'returns nil' do - expect(controller.signed_request_account).to be_nil - end - end - - describe '#signed_request_actor' do - it 'returns the expected actor' do - expect(controller.signed_request_actor).to eq actor - end - end - end - - context 'with request with unparsable Date header' do - before do - get :success - - fake_request = Request.new(:get, request.url) - fake_request.add_headers({ 'Date' => 'wrong date' }) - fake_request.on_behalf_of(author) - - request.headers.merge!(fake_request.headers) - end - - describe '#signed_request?' do - it 'returns true' do - expect(controller.signed_request?).to be true - end - end - - describe '#signed_request_account' do - it 'returns nil' do - expect(controller.signed_request_account).to be_nil - end - end - - describe '#signature_verification_failure_reason' do - it 'contains an error description' do - controller.signed_request_account - expect(controller.signature_verification_failure_reason[:error]).to eq 'Invalid Date header: not RFC 2616 compliant date: "wrong date"' - end - end - end - - context 'with request older than a day' do - before do - get :success - - fake_request = Request.new(:get, request.url) - fake_request.add_headers({ 'Date' => 2.days.ago.utc.httpdate }) - fake_request.on_behalf_of(author) - - request.headers.merge!(fake_request.headers) - end - - describe '#signed_request?' do - it 'returns true' do - expect(controller.signed_request?).to be true - end - end - - describe '#signed_request_account' do - it 'returns nil' do - expect(controller.signed_request_account).to be_nil - end - end - - describe '#signature_verification_failure_reason' do - it 'contains an error description' do - controller.signed_request_account - expect(controller.signature_verification_failure_reason[:error]).to eq 'Signed request date outside acceptable time window' - end - end - end - - context 'with inaccessible key' do - before do - get :success - - author = Fabricate(:account, domain: 'localhost:5000', uri: 'http://localhost:5000/actor') - fake_request = Request.new(:get, request.url) - fake_request.on_behalf_of(author) - author.destroy - - request.headers.merge!(fake_request.headers) - - stub_request(:get, 'http://localhost:5000/actor#main-key').to_raise(Mastodon::HostValidationError) - end - - describe '#signed_request?' do - it 'returns true' do - expect(controller.signed_request?).to be true - end - end - - describe '#signed_request_account' do - it 'returns nil' do - expect(controller.signed_request_account).to be_nil - end - end - end - - context 'with body' do - before do - allow(controller).to receive(:actor_refresh_key!).and_return(author) - post :success, body: 'Hello world' - - fake_request = Request.new(:post, request.url, body: 'Hello world') - fake_request.on_behalf_of(author) - - request.headers.merge!(fake_request.headers) - end - - describe '#signed_request?' do - it 'returns true' do - expect(controller.signed_request?).to be true - end - end - - describe '#signed_request_account' do - it 'returns an account' do - expect(controller.signed_request_account).to eq author - end - end - - context 'when path does not match' do - before do - request.path = '/alternative-path' - end - - describe '#signed_request_account' do - it 'returns nil' do - expect(controller.signed_request_account).to be_nil - end - end - - describe '#signature_verification_failure_reason' do - it 'contains an error description' do - controller.signed_request_account - expect(controller.signature_verification_failure_reason[:error]).to include('using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)') - expect(controller.signature_verification_failure_reason[:signed_string]).to include("(request-target): post /alternative-path\n") - end - end - end - - context 'when method does not match' do - before do - get :success - end - - describe '#signed_request_account' do - it 'returns nil' do - expect(controller.signed_request_account).to be_nil - end - end - end - - context 'when body has been tampered' do - before do - post :success, body: 'doo doo doo' - end - - describe '#signed_request_account' do - it 'returns nil when body has been tampered' do - expect(controller.signed_request_account).to be_nil - end - end - end - end - end - - context 'when a signature is required' do - before do - get :signature_required - end - - context 'without signature header' do - it 'returns HTTP 401' do - expect(response).to have_http_status(401) - end - - it 'returns an error' do - expect(Oj.load(response.body)['error']).to eq 'Request not signed' - end - end - end -end diff --git a/spec/requests/signature_verification_spec.rb b/spec/requests/signature_verification_spec.rb new file mode 100644 index 000000000..b753750b8 --- /dev/null +++ b/spec/requests/signature_verification_spec.rb @@ -0,0 +1,332 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'signature verification concern' do + before do + stub_tests_controller + + # Signature checking is time-dependent, so travel to a fixed date + travel_to '2023-12-20T10:00:00Z' + end + + after { Rails.application.reload_routes! } + + # Include the private key so the tests can be easily adjusted and reviewed + let(:actor_keypair) do + OpenSSL::PKey.read(<<~PEM_TEXT) + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEAqIAYvNFGbZ5g4iiK6feSdXD4bDStFM58A7tHycYXaYtzZQpI + eHXAmaXuZzXIwtrP4N0gIk8JNwZvXj2UPS+S07t0V9wNK94he01LV5EMz/GN4eNn + FmDL64HIEuKLvV8TvgjbUPRD6Y5X0UpKi2ZIFLSb96Q5w0Z/k7ntpVKV52y8kz5F + jr/O/0JuHryZe0yItzJh8kzFfeMf0EXzfSnaKvT7P9jhgC6uTre+jXyvVZjiHDrn + qvvucdI3I7DRfXo1OqARBrLjy+TdseUAjNYJ+OuPRI1URIWQI01DCHqcohVu9+Ar + +BiCjFp3ua+XMuJvrvbD61d1Fvig/9nbBRR+8QIDAQABAoIBAAgySHnFWI6gItR3 + fkfiqIm80cHCN3Xk1C6iiVu+3oBOZbHpW9R7vl9e/WOA/9O+LPjiSsQOegtWnVvd + RRjrl7Hj20VDlZKv5Mssm6zOGAxksrcVbqwdj+fUJaNJCL0AyyseH0x/IE9T8rDC + I1GH+3tB3JkhkIN/qjipdX5ab8MswEPu8IC4ViTpdBgWYY/xBcAHPw4xuL0tcwzh + FBlf4DqoEVQo8GdK5GAJ2Ny0S4xbXHUURzx/R4y4CCts7niAiLGqd9jmLU1kUTMk + QcXfQYK6l+unLc7wDYAz7sFEHh04M48VjWwiIZJnlCqmQbLda7uhhu8zkF1DqZTu + ulWDGQECgYEA0TIAc8BQBVab979DHEEmMdgqBwxLY3OIAk0b+r50h7VBGWCDPRsC + STD73fQY3lNet/7/jgSGwwAlAJ5PpMXxXiZAE3bUwPmHzgF7pvIOOLhA8O07tHSO + L2mvQe6NPzjZ+6iAO2U9PkClxcvGvPx2OBvisfHqZLmxC9PIVxzruQECgYEAzjM6 + BTUXa6T/qHvLFbN699BXsUOGmHBGaLRapFDBfVvgZrwqYQcZpBBhesLdGTGSqwE7 + gWsITPIJ+Ldo+38oGYyVys+w/V67q6ud7hgSDTW3hSvm+GboCjk6gzxlt9hQ0t9X + 8vfDOYhEXvVUJNv3mYO60ENqQhILO4bQ0zi+VfECgYBb/nUccfG+pzunU0Cb6Dp3 + qOuydcGhVmj1OhuXxLFSDG84Tazo7juvHA9mp7VX76mzmDuhpHPuxN2AzB2SBEoE + cSW0aYld413JRfWukLuYTc6hJHIhBTCRwRQFFnae2s1hUdQySm8INT2xIc+fxBXo + zrp+Ljg5Wz90SAnN5TX0AQKBgDaatDOq0o/r+tPYLHiLtfWoE4Dau+rkWJDjqdk3 + lXWn/e3WyHY3Vh/vQpEqxzgju45TXjmwaVtPATr+/usSykCxzP0PMPR3wMT+Rm1F + rIoY/odij+CaB7qlWwxj0x/zRbwB7x1lZSp4HnrzBpxYL+JUUwVRxPLIKndSBTza + GvVRAoGBAIVBcNcRQYF4fvZjDKAb4fdBsEuHmycqtRCsnkGOz6ebbEQznSaZ0tZE + +JuouZaGjyp8uPjNGD5D7mIGbyoZ3KyG4mTXNxDAGBso1hrNDKGBOrGaPhZx8LgO + 4VXJ+ybXrATf4jr8ccZYsZdFpOphPzz+j55Mqg5vac5P1XjmsGTb + -----END RSA PRIVATE KEY----- + PEM_TEXT + end + + context 'without a Signature header' do + it 'does not treat the request as signed' do + get '/activitypub/success' + + expect(response).to have_http_status(200) + expect(body_as_json).to match( + signed_request: false, + signature_actor_id: nil, + error: 'Request not signed' + ) + end + + context 'when a signature is required' do + it 'returns http unauthorized with appropriate error' do + get '/activitypub/signature_required' + + expect(response).to have_http_status(401) + expect(body_as_json).to match( + error: 'Request not signed' + ) + end + end + end + + context 'with an HTTP Signature from a known account' do + let!(:actor) { Fabricate(:account, domain: 'remote.domain', uri: 'https://remote.domain/users/bob', private_key: nil, public_key: actor_keypair.public_key.to_pem) } + + context 'with a valid signature on a GET request' do + let(:signature_header) do + 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="' # rubocop:disable Layout/LineLength + end + + it 'successfuly verifies signature', :aggregate_failures do + expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success', { 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' }) + + get '/activitypub/success', headers: { + 'Host' => 'www.example.com', + 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', + 'Signature' => signature_header, + } + + expect(response).to have_http_status(200) + expect(body_as_json).to match( + signed_request: true, + signature_actor_id: actor.id.to_s + ) + end + end + + context 'with a mismatching path' do + it 'fails to verify signature', :aggregate_failures do + get '/activitypub/alternative-path', headers: { + 'Host' => 'www.example.com', + 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', + 'Signature' => 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="', # rubocop:disable Layout/LineLength + } + + expect(body_as_json).to match( + signed_request: true, + signature_actor_id: nil, + error: anything + ) + end + end + + context 'with a mismatching method' do + it 'fails to verify signature', :aggregate_failures do + post '/activitypub/success', headers: { + 'Host' => 'www.example.com', + 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', + 'Signature' => 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="', # rubocop:disable Layout/LineLength + } + + expect(body_as_json).to match( + signed_request: true, + signature_actor_id: nil, + error: anything + ) + end + end + + context 'with an unparsable date' do + let(:signature_header) do + 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="d4B7nfx8RJcfdJDu1J//5WzPzK/hgtPkdzZx49lu5QhnE7qdV3lgyVimmhCFrO16bwvzIp9iRMyRLkNFxLiEeVaa1gqeKbldGSnU0B0OMjx7rFBa65vLuzWQOATDitVGiBEYqoK4v0DMuFCz2DtFaA/DIUZ3sty8bZ/Ea3U1nByLOO6MacARA3zhMSI0GNxGqsSmZmG0hPLavB3jIXoE3IDoQabMnC39jrlcO/a8h1iaxBm2WD8TejrImJullgqlJIFpKhIHI3ipQkvTGPlm9dx0y+beM06qBvWaWQcmT09eRIUefVsOAzIhUtS/7FVb/URhZvircIJDa7vtiFcmZQ=="' # rubocop:disable Layout/LineLength + end + + it 'fails to verify signature', :aggregate_failures do + expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success', { 'Date' => 'wrong date', 'Host' => 'www.example.com' }) + + get '/activitypub/success', headers: { + 'Host' => 'www.example.com', + 'Date' => 'wrong date', + 'Signature' => signature_header, + } + + expect(body_as_json).to match( + signed_request: true, + signature_actor_id: nil, + error: 'Invalid Date header: not RFC 2616 compliant date: "wrong date"' + ) + end + end + + context 'with a request older than a day' do + let(:signature_header) do + 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="G1NuJv4zgoZ3B/ZIjzDWZHK4RC+5pYee74q8/LJEMCWXhcnAomcb9YHaqk1QYfQvcBUIXw3UZ3Q9xO8F9y0i8G5mzJHfQ+OgHqCoJk8EmGwsUXJMh5s1S5YFCRt8TT12TmJZz0VMqLq85ubueSYBM7QtUE/FzFIVLvz4RysgXxaXQKzdnM6+gbUEEKdCURpXdQt2NXQhp4MAmZH3+0lQoR6VxdsK0hx0Ji2PNp1nuqFTlYqNWZazVdLBN+9rETLRmvGXknvg9jOxTTppBVWnkAIl26HtLS3wwFVvz4pJzi9OQDOvLziehVyLNbU61hky+oJ215e2HuKSe2hxHNl1MA=="' # rubocop:disable Layout/LineLength + end + + it 'fails to verify signature', :aggregate_failures do + expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success', { 'Date' => 'Wed, 18 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' }) + + get '/activitypub/success', headers: { + 'Host' => 'www.example.com', + 'Date' => 'Wed, 18 Dec 2023 10:00:00 GMT', + 'Signature' => signature_header, + } + + expect(body_as_json).to match( + signed_request: true, + signature_actor_id: nil, + error: 'Signed request date outside acceptable time window' + ) + end + end + + context 'with a valid signature on a POST request' do + let(:digest_header) { 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=' } + let(:signature_header) do + 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="host date digest (request-target)",signature="gmhMjgMROGElJU3fpehV2acD5kMHeELi8EFP2UPHOdQ54H0r55AxIpji+J3lPe+N2qSb/4H1KXIh6f0lRu8TGSsu12OQmg5hiO8VA9flcA/mh9Lpk+qwlQZIPRqKP9xUEfqD+Z7ti5wPzDKrWAUK/7FIqWgcT/mlqB1R1MGkpMFc/q4CIs2OSNiWgA4K+Kp21oQxzC2kUuYob04gAZ7cyE/FTia5t08uv6lVYFdRsn4XNPn1MsHgFBwBMRG79ng3SyhoG4PrqBEi5q2IdLq3zfre/M6He3wlCpyO2VJNdGVoTIzeZ0Zz8jUscPV3XtWUchpGclLGSaKaq/JyNZeiYQ=="' # rubocop:disable Layout/LineLength + end + + it 'successfuly verifies signature', :aggregate_failures do + expect(digest_header).to eq digest_value('Hello world') + expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'post /activitypub/success', { 'Host' => 'www.example.com', 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Digest' => digest_header }) + + post '/activitypub/success', params: 'Hello world', headers: { + 'Host' => 'www.example.com', + 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', + 'Digest' => digest_header, + 'Signature' => signature_header, + } + + expect(response).to have_http_status(200) + expect(body_as_json).to match( + signed_request: true, + signature_actor_id: actor.id.to_s + ) + end + end + + context 'when the Digest of a POST request is not signed' do + let(:digest_header) { 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=' } + let(:signature_header) do + 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="host date (request-target)",signature="CPD704CG8aCm8X8qIP8kkkiGp1qwFLk/wMVQHOGP0Txxan8c2DZtg/KK7eN8RG8tHx8br/yS2hJs51x4kXImYukGzNJd7ihE3T8lp+9RI1tCcdobTzr/VcVJHDFySdQkg266GCMijRQRZfNvqlJLiisr817PI+gNVBI5qV+vnVd1XhWCEZ+YSmMe8UqYARXAYNqMykTheojqGpTeTFGPUpTQA2Fmt2BipwIjcFDm2Hpihl2kB0MUS0x3zPmHDuadvzoBbN6m3usPDLgYrpALlh+wDs1dYMntcwdwawRKY1oE1XNtgOSum12wntDq3uYL4gya2iPdcw3c929b4koUzw=="' # rubocop:disable Layout/LineLength + end + + it 'fails to verify signature', :aggregate_failures do + expect(digest_header).to eq digest_value('Hello world') + expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'post /activitypub/success', { 'Host' => 'www.example.com', 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT' }) + + post '/activitypub/success', params: 'Hello world', headers: { + 'Host' => 'www.example.com', + 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', + 'Digest' => digest_header, + 'Signature' => signature_header, + } + + expect(body_as_json).to match( + signed_request: true, + signature_actor_id: nil, + error: 'Mastodon requires the Digest header to be signed when doing a POST request' + ) + end + end + + context 'with a tampered body on a POST request' do + let(:digest_header) { 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=' } + let(:signature_header) do + 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="host date digest (request-target)",signature="gmhMjgMROGElJU3fpehV2acD5kMHeELi8EFP2UPHOdQ54H0r55AxIpji+J3lPe+N2qSb/4H1KXIh6f0lRu8TGSsu12OQmg5hiO8VA9flcA/mh9Lpk+qwlQZIPRqKP9xUEfqD+Z7ti5wPzDKrWAUK/7FIqWgcT/mlqB1R1MGkpMFc/q4CIs2OSNiWgA4K+Kp21oQxzC2kUuYob04gAZ7cyE/FTia5t08uv6lVYFdRsn4XNPn1MsHgFBwBMRG79ng3SyhoG4PrqBEi5q2IdLq3zfre/M6He3wlCpyO2VJNdGVoTIzeZ0Zz8jUscPV3XtWUchpGclLGSaKaq/JyNZeiYQ=="' # rubocop:disable Layout/LineLength + end + + it 'fails to verify signature', :aggregate_failures do + expect(digest_header).to_not eq digest_value('Hello world!') + expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'post /activitypub/success', { 'Host' => 'www.example.com', 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Digest' => digest_header }) + + post '/activitypub/success', params: 'Hello world!', headers: { + 'Host' => 'www.example.com', + 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', + 'Digest' => 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=', + 'Signature' => signature_header, + } + + expect(body_as_json).to match( + signed_request: true, + signature_actor_id: nil, + error: 'Invalid Digest value. Computed SHA-256 digest: wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro=; given: ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=' + ) + end + end + + context 'with a tampered path in a POST request' do + it 'fails to verify signature', :aggregate_failures do + post '/activitypub/alternative-path', params: 'Hello world', headers: { + 'Host' => 'www.example.com', + 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', + 'Digest' => 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=', + 'Signature' => 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="host date digest (request-target)",signature="gmhMjgMROGElJU3fpehV2acD5kMHeELi8EFP2UPHOdQ54H0r55AxIpji+J3lPe+N2qSb/4H1KXIh6f0lRu8TGSsu12OQmg5hiO8VA9flcA/mh9Lpk+qwlQZIPRqKP9xUEfqD+Z7ti5wPzDKrWAUK/7FIqWgcT/mlqB1R1MGkpMFc/q4CIs2OSNiWgA4K+Kp21oQxzC2kUuYob04gAZ7cyE/FTia5t08uv6lVYFdRsn4XNPn1MsHgFBwBMRG79ng3SyhoG4PrqBEi5q2IdLq3zfre/M6He3wlCpyO2VJNdGVoTIzeZ0Zz8jUscPV3XtWUchpGclLGSaKaq/JyNZeiYQ=="', # rubocop:disable Layout/LineLength + } + + expect(response).to have_http_status(200) + expect(body_as_json).to match( + signed_request: true, + signature_actor_id: nil, + error: anything + ) + end + end + end + + context 'with an inaccessible key' do + before do + stub_request(:get, 'https://remote.domain/users/alice#main-key').to_return(status: 404) + end + + it 'fails to verify signature', :aggregate_failures do + get '/activitypub/success', headers: { + 'Host' => 'www.example.com', + 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', + 'Signature' => 'keyId="https://remote.domain/users/alice#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="', # rubocop:disable Layout/LineLength + } + + expect(body_as_json).to match( + signed_request: true, + signature_actor_id: nil, + error: 'Unable to fetch key JSON at https://remote.domain/users/alice#main-key' + ) + end + end + + private + + def stub_tests_controller + stub_const('ActivityPub::TestsController', activitypub_tests_controller) + + Rails.application.routes.draw do + # NOTE: RouteSet#draw removes all routes, so we need to re-insert one + resource :instance_actor, path: 'actor', only: [:show] + + match :via => [:get, :post], '/activitypub/success' => 'activitypub/tests#success' + match :via => [:get, :post], '/activitypub/alternative-path' => 'activitypub/tests#alternative_success' + match :via => [:get, :post], '/activitypub/signature_required' => 'activitypub/tests#signature_required' + end + end + + def activitypub_tests_controller + Class.new(ApplicationController) do + include SignatureVerification + + before_action :require_actor_signature!, only: [:signature_required] + + def success + render json: { + signed_request: signed_request?, + signature_actor_id: signed_request_actor&.id&.to_s, + }.merge(signature_verification_failure_reason || {}) + end + + alias_method :alternative_success, :success + alias_method :signature_required, :success + end + end + + def digest_value(body) + "SHA-256=#{Digest::SHA256.base64digest(body)}" + end + + def build_signature_string(keypair, key_id, request_target, headers) + algorithm = 'rsa-sha256' + signed_headers = headers.merge({ '(request-target)' => request_target }) + signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n") + 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 From 3837ec2227159d5b05f07d4d9c4a01647f29ad6f Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 3 Jan 2024 12:29:26 +0100 Subject: [PATCH 041/117] Fix Mastodon not correctly processing HTTP Signatures with query strings (#28476) --- .../concerns/signature_verification.rb | 22 ++++++- app/lib/request.rb | 11 +++- spec/requests/signature_verification_spec.rb | 66 +++++++++++++++++++ 3 files changed, 95 insertions(+), 4 deletions(-) diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index f0a344f1c..35391e64c 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -91,14 +91,23 @@ module SignatureVerification raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil? signature = Base64.decode64(signature_params['signature']) - compare_signed_string = build_signed_string + compare_signed_string = build_signed_string(include_query_string: true) return actor unless verify_signature(actor, signature, compare_signed_string).nil? + # Compatibility quirk with older Mastodon versions + compare_signed_string = build_signed_string(include_query_string: false) + return actor unless verify_signature(actor, signature, compare_signed_string).nil? + actor = stoplight_wrap_request { actor_refresh_key!(actor) } raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil? + compare_signed_string = build_signed_string(include_query_string: true) + return actor unless verify_signature(actor, signature, compare_signed_string).nil? + + # Compatibility quirk with older Mastodon versions + compare_signed_string = build_signed_string(include_query_string: false) return actor unless verify_signature(actor, signature, compare_signed_string).nil? fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature'] @@ -180,11 +189,18 @@ module SignatureVerification nil end - def build_signed_string + def build_signed_string(include_query_string: true) signed_headers.map do |signed_header| case signed_header when Request::REQUEST_TARGET - "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" + if include_query_string + "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}" + else + # Current versions of Mastodon incorrectly omit the query string from the (request-target) pseudo-header. + # Therefore, temporarily support such incorrect signatures for compatibility. + # TODO: remove eventually some time after release of the fixed version + "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" + end when '(created)' raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019' raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank? diff --git a/app/lib/request.rb b/app/lib/request.rb index 5f128af73..8d4120868 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -77,6 +77,7 @@ class Request @url = Addressable::URI.parse(url).normalize @http_client = options.delete(:http_client) @allow_local = options.delete(:allow_local) + @full_path = options.delete(:with_query_string) @options = options.merge(socket_class: use_proxy? || @allow_local ? ProxySocket : Socket) @options = @options.merge(timeout_class: PerOperationWithDeadline, timeout_options: TIMEOUT) @options = @options.merge(proxy_url) if use_proxy? @@ -146,7 +147,7 @@ class Request private def set_common_headers! - @headers[REQUEST_TARGET] = "#{@verb} #{@url.path}" + @headers[REQUEST_TARGET] = request_target @headers['User-Agent'] = Mastodon::Version.user_agent @headers['Host'] = @url.host @headers['Date'] = Time.now.utc.httpdate @@ -157,6 +158,14 @@ 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)) diff --git a/spec/requests/signature_verification_spec.rb b/spec/requests/signature_verification_spec.rb index b753750b8..401828c4a 100644 --- a/spec/requests/signature_verification_spec.rb +++ b/spec/requests/signature_verification_spec.rb @@ -94,6 +94,72 @@ describe 'signature verification concern' do end end + context 'with a valid signature on a GET request that has a query string' do + let(:signature_header) do + 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="SDMa4r/DQYMXYxVgYO2yEqGWWUXugKjVuz0I8dniQAk+aunzBaF2aPu+4grBfawAshlx1Xytl8lhb0H2MllEz16/tKY7rUrb70MK0w8ohXgpb0qs3YvQgdj4X24L1x2MnkFfKHR/J+7TBlnivq0HZqXm8EIkPWLv+eQxu8fbowLwHIVvRd/3t6FzvcfsE0UZKkoMEX02542MhwSif6cu7Ec/clsY9qgKahb9JVGOGS1op9Lvg/9y1mc8KCgD83U5IxVygYeYXaVQ6gixA9NgZiTCwEWzHM5ELm7w5hpdLFYxYOHg/3G3fiqJzpzNQAcCD4S4JxfE7hMI0IzVlNLT6A=="' # rubocop:disable Layout/LineLength + end + + it 'successfuly verifies signature', :aggregate_failures do + expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success?foo=42', { 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' }) + + get '/activitypub/success?foo=42', headers: { + 'Host' => 'www.example.com', + 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', + 'Signature' => signature_header, + } + + expect(response).to have_http_status(200) + expect(body_as_json).to match( + signed_request: true, + signature_actor_id: actor.id.to_s + ) + end + end + + context 'when the query string is missing from the signature verification (compatibility quirk)' do + let(:signature_header) do + 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="' # rubocop:disable Layout/LineLength + end + + it 'successfuly verifies signature', :aggregate_failures do + expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success', { 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' }) + + get '/activitypub/success?foo=42', headers: { + 'Host' => 'www.example.com', + 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', + 'Signature' => signature_header, + } + + expect(response).to have_http_status(200) + expect(body_as_json).to match( + signed_request: true, + signature_actor_id: actor.id.to_s + ) + end + end + + context 'with mismatching query string' do + let(:signature_header) do + 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="SDMa4r/DQYMXYxVgYO2yEqGWWUXugKjVuz0I8dniQAk+aunzBaF2aPu+4grBfawAshlx1Xytl8lhb0H2MllEz16/tKY7rUrb70MK0w8ohXgpb0qs3YvQgdj4X24L1x2MnkFfKHR/J+7TBlnivq0HZqXm8EIkPWLv+eQxu8fbowLwHIVvRd/3t6FzvcfsE0UZKkoMEX02542MhwSif6cu7Ec/clsY9qgKahb9JVGOGS1op9Lvg/9y1mc8KCgD83U5IxVygYeYXaVQ6gixA9NgZiTCwEWzHM5ELm7w5hpdLFYxYOHg/3G3fiqJzpzNQAcCD4S4JxfE7hMI0IzVlNLT6A=="' # rubocop:disable Layout/LineLength + end + + it 'fails to verify signature', :aggregate_failures do + expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success?foo=42', { 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' }) + + get '/activitypub/success?foo=43', headers: { + 'Host' => 'www.example.com', + 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', + 'Signature' => signature_header, + } + + expect(body_as_json).to match( + signed_request: true, + signature_actor_id: nil, + error: anything + ) + end + end + context 'with a mismatching path' do it 'fails to verify signature', :aggregate_failures do get '/activitypub/alternative-path', headers: { From 499bc716a5dc5b5ef2ca710ae8dd0af8b0028127 Mon Sep 17 00:00:00 2001 From: Jeong Arm Date: Tue, 16 Jan 2024 17:35:54 +0900 Subject: [PATCH 042/117] Ignore RecordNotUnique errors in LinkCrawlWorker (#28748) --- app/workers/link_crawl_worker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/workers/link_crawl_worker.rb b/app/workers/link_crawl_worker.rb index b3d8aa264..c63af1e43 100644 --- a/app/workers/link_crawl_worker.rb +++ b/app/workers/link_crawl_worker.rb @@ -7,7 +7,7 @@ class LinkCrawlWorker def perform(status_id) FetchLinkCardService.new.call(Status.find(status_id)) - rescue ActiveRecord::RecordNotFound + rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordNotUnique true end end From 2dbf176d239720edfc2efd3ae4082830d7137062 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 19 Jan 2024 10:18:21 +0100 Subject: [PATCH 043/117] Retry 401 errors on replies fetching (#28788) Co-authored-by: Claire --- app/helpers/jsonld_helper.rb | 12 ++++++------ app/services/activitypub/fetch_replies_service.rb | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index ce3ff094f..b3d0d032c 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -155,7 +155,7 @@ module JsonLdHelper end end - def fetch_resource(uri, id, on_behalf_of = nil) + def fetch_resource(uri, id, on_behalf_of = nil, request_options: {}) unless id json = fetch_resource_without_id_validation(uri, on_behalf_of) @@ -164,14 +164,14 @@ module JsonLdHelper uri = json['id'] end - json = fetch_resource_without_id_validation(uri, on_behalf_of) + json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options) json.present? && json['id'] == uri ? json : nil end - def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false) + def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {}) on_behalf_of ||= Account.representative - build_request(uri, on_behalf_of).perform do |response| + build_request(uri, on_behalf_of, options: request_options).perform do |response| raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error body_to_json(response.body_with_limit) if response.code == 200 @@ -204,8 +204,8 @@ module JsonLdHelper response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code)) end - def build_request(uri, on_behalf_of = nil) - Request.new(:get, uri).tap do |request| + def build_request(uri, on_behalf_of = nil, options: {}) + Request.new(:get, uri, **options).tap do |request| request.on_behalf_of(on_behalf_of) if on_behalf_of request.add_headers('Accept' => 'application/activity+json, application/ld+json') end diff --git a/app/services/activitypub/fetch_replies_service.rb b/app/services/activitypub/fetch_replies_service.rb index b5c7759ec..a9dd327e9 100644 --- a/app/services/activitypub/fetch_replies_service.rb +++ b/app/services/activitypub/fetch_replies_service.rb @@ -37,7 +37,20 @@ class ActivityPub::FetchRepliesService < BaseService return unless @allow_synchronous_requests return if non_matching_uri_hosts?(@account.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 def filtered_replies From 6fe2a47357f893096f86866cb37d0bb052573757 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 19 Jan 2024 13:19:49 +0100 Subject: [PATCH 044/117] Add rate-limit of TOTP authentication attempts at controller level (#28801) --- app/controllers/auth/sessions_controller.rb | 22 +++++++++++++++++++ .../two_factor_authentication_concern.rb | 5 +++++ config/locales/en.yml | 1 + .../auth/sessions_controller_spec.rb | 20 +++++++++++++++++ 4 files changed, 48 insertions(+) diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 06a3deee2..8212e2e63 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class Auth::SessionsController < Devise::SessionsController + include Redisable + + MAX_2FA_ATTEMPTS_PER_HOUR = 10 + layout 'auth' skip_before_action :require_no_authentication, only: [:create] @@ -134,9 +138,23 @@ class Auth::SessionsController < Devise::SessionsController session.delete(:attempt_user_updated_at) end + def clear_2fa_attempt_from_user(user) + redis.del(second_factor_attempts_key(user)) + end + + def check_second_factor_rate_limits(user) + attempts, = redis.multi do |multi| + multi.incr(second_factor_attempts_key(user)) + multi.expire(second_factor_attempts_key(user), 1.hour) + end + + attempts >= MAX_2FA_ATTEMPTS_PER_HOUR + end + def on_authentication_success(user, security_measure) @on_authentication_success_called = true + clear_2fa_attempt_from_user(user) clear_attempt_from_session user.update_sign_in!(new_sign_in: true) @@ -168,4 +186,8 @@ class Auth::SessionsController < Devise::SessionsController user_agent: request.user_agent ) end + + def second_factor_attempts_key(user) + "2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}" + end end diff --git a/app/controllers/concerns/two_factor_authentication_concern.rb b/app/controllers/concerns/two_factor_authentication_concern.rb index 9eb45b90d..90fa392a1 100644 --- a/app/controllers/concerns/two_factor_authentication_concern.rb +++ b/app/controllers/concerns/two_factor_authentication_concern.rb @@ -65,6 +65,11 @@ module TwoFactorAuthenticationConcern end def authenticate_with_two_factor_via_otp(user) + if check_second_factor_rate_limits(user) + flash.now[:alert] = I18n.t('users.rate_limited') + return prompt_for_two_factor(user) + end + if valid_otp_attempt?(user) on_authentication_success(user, :otp) else diff --git a/config/locales/en.yml b/config/locales/en.yml index 71b8f27aa..2b3bfb4c7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1826,6 +1826,7 @@ en: go_to_sso_account_settings: Go to your identity provider's account settings invalid_otp_token: Invalid two-factor code otp_lost_help_html: If you lost access to both, you may get in touch with %{email} + rate_limited: Too many authentication attempts, try again later. seamless_external_login: You are logged in via an external service, so password and e-mail settings are not available. signed_in_as: 'Signed in as:' verification: diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb index c727a7633..95055ade4 100644 --- a/spec/controllers/auth/sessions_controller_spec.rb +++ b/spec/controllers/auth/sessions_controller_spec.rb @@ -263,6 +263,26 @@ RSpec.describe Auth::SessionsController do end end + context 'when repeatedly using an invalid TOTP code before using a valid code' do + before do + stub_const('Auth::SessionsController::MAX_2FA_ATTEMPTS_PER_HOUR', 2) + end + + it 'does not log the user in' do + # Travel to the beginning of an hour to avoid crossing rate-limit buckets + travel_to '2023-12-20T10:00:00Z' + + Auth::SessionsController::MAX_2FA_ATTEMPTS_PER_HOUR.times do + post :create, params: { user: { otp_attempt: '1234' } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } + expect(controller.current_user).to be_nil + end + + post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } + expect(controller.current_user).to be_nil + expect(flash[:alert]).to match I18n.t('users.rate_limited') + end + end + context 'when using a valid OTP' do before do post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } From b377f82b1dcc7961f2f5e17b2830c1e61ad7a504 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 19 Jan 2024 13:43:10 +0100 Subject: [PATCH 045/117] Fix processing of compacted single-item JSON-LD collections (#28816) --- .../fetch_featured_collection_service.rb | 4 +-- .../activitypub/fetch_replies_service.rb | 4 +-- .../synchronize_followers_service.rb | 4 +-- app/services/keys/query_service.rb | 2 +- .../fetch_featured_collection_service_spec.rb | 34 +++++++++++++++++-- .../activitypub/fetch_replies_service_spec.rb | 12 +++++++ 6 files changed, 51 insertions(+), 9 deletions(-) diff --git a/app/services/activitypub/fetch_featured_collection_service.rb b/app/services/activitypub/fetch_featured_collection_service.rb index e8a31dade..d5d52c3cd 100644 --- a/app/services/activitypub/fetch_featured_collection_service.rb +++ b/app/services/activitypub/fetch_featured_collection_service.rb @@ -23,9 +23,9 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService case collection['type'] when 'Collection', 'CollectionPage' - collection['items'] + as_array(collection['items']) when 'OrderedCollection', 'OrderedCollectionPage' - collection['orderedItems'] + as_array(collection['orderedItems']) end end diff --git a/app/services/activitypub/fetch_replies_service.rb b/app/services/activitypub/fetch_replies_service.rb index a9dd327e9..e2ecdef16 100644 --- a/app/services/activitypub/fetch_replies_service.rb +++ b/app/services/activitypub/fetch_replies_service.rb @@ -26,9 +26,9 @@ class ActivityPub::FetchRepliesService < BaseService case collection['type'] when 'Collection', 'CollectionPage' - collection['items'] + as_array(collection['items']) when 'OrderedCollection', 'OrderedCollectionPage' - collection['orderedItems'] + as_array(collection['orderedItems']) end end diff --git a/app/services/activitypub/synchronize_followers_service.rb b/app/services/activitypub/synchronize_followers_service.rb index 9bd6034a5..a9aab653c 100644 --- a/app/services/activitypub/synchronize_followers_service.rb +++ b/app/services/activitypub/synchronize_followers_service.rb @@ -59,9 +59,9 @@ class ActivityPub::SynchronizeFollowersService < BaseService case collection['type'] when 'Collection', 'CollectionPage' - collection['items'] + as_array(collection['items']) when 'OrderedCollection', 'OrderedCollectionPage' - collection['orderedItems'] + as_array(collection['orderedItems']) end end diff --git a/app/services/keys/query_service.rb b/app/services/keys/query_service.rb index 14c9d9205..33e13293f 100644 --- a/app/services/keys/query_service.rb +++ b/app/services/keys/query_service.rb @@ -69,7 +69,7 @@ class Keys::QueryService < BaseService return if json['items'].blank? - @devices = json['items'].map do |device| + @devices = as_array(json['items']).map do |device| Device.new(device_id: device['id'], name: device['name'], identity_key: device.dig('identityKey', 'publicKeyBase64'), fingerprint_key: device.dig('fingerprintKey', 'publicKeyBase64'), claim_url: device['claim']) end rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e diff --git a/spec/services/activitypub/fetch_featured_collection_service_spec.rb b/spec/services/activitypub/fetch_featured_collection_service_spec.rb index 5975c81a1..a096296ff 100644 --- a/spec/services/activitypub/fetch_featured_collection_service_spec.rb +++ b/spec/services/activitypub/fetch_featured_collection_service_spec.rb @@ -31,7 +31,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do } end - let(:status_json_pinned_unknown_unreachable) do + let(:status_json_pinned_unknown_reachable) do { '@context': 'https://www.w3.org/ns/activitystreams', type: 'Note', @@ -65,7 +65,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: Oj.dump(status_json_pinned_known)) stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_inlined)) stub_request(:get, 'https://example.com/account/pinned/unknown-unreachable').to_return(status: 404) - stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_unreachable)) + stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable)) subject.call(actor, note: true, hashtag: false) end @@ -103,6 +103,21 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do end it_behaves_like 'sets pinned posts' + + context 'when there is a single item, with the array compacted away' do + let(:items) { 'https://example.com/account/pinned/unknown-reachable' } + + before do + stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable)) + subject.call(actor, note: true, hashtag: false) + end + + it 'sets expected posts as pinned posts' do + expect(actor.pinned_statuses.pluck(:uri)).to contain_exactly( + 'https://example.com/account/pinned/unknown-reachable' + ) + end + end end context 'when the endpoint is a paginated Collection' do @@ -124,6 +139,21 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do end it_behaves_like 'sets pinned posts' + + context 'when there is a single item, with the array compacted away' do + let(:items) { 'https://example.com/account/pinned/unknown-reachable' } + + before do + stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable)) + subject.call(actor, note: true, hashtag: false) + end + + it 'sets expected posts as pinned posts' do + expect(actor.pinned_statuses.pluck(:uri)).to contain_exactly( + 'https://example.com/account/pinned/unknown-reachable' + ) + end + end end end end diff --git a/spec/services/activitypub/fetch_replies_service_spec.rb b/spec/services/activitypub/fetch_replies_service_spec.rb index bf8e29676..62841d7d2 100644 --- a/spec/services/activitypub/fetch_replies_service_spec.rb +++ b/spec/services/activitypub/fetch_replies_service_spec.rb @@ -34,6 +34,18 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do describe '#call' do context 'when the payload is a Collection with inlined replies' do + context 'when there is a single reply, with the array compacted away' do + let(:items) { 'http://example.com/self-reply-1' } + + it 'queues the expected worker' do + allow(FetchReplyWorker).to receive(:push_bulk) + + subject.call(status, payload) + + expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1']) + end + end + context 'when passing the collection itself' do it 'spawns workers for up to 5 replies on the same server' do expect(FetchReplyWorker).to receive(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) From 779237f0546b9675e8a6f0b0d3a2dd37d396270f Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 19 Jan 2024 19:52:59 +0100 Subject: [PATCH 046/117] Fix error when processing remote files with unusually long names (#28823) --- lib/paperclip/response_with_limit_adapter.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/paperclip/response_with_limit_adapter.rb b/lib/paperclip/response_with_limit_adapter.rb index deb89717a..ff7a938ab 100644 --- a/lib/paperclip/response_with_limit_adapter.rb +++ b/lib/paperclip/response_with_limit_adapter.rb @@ -16,7 +16,7 @@ module Paperclip private def cache_current_values - @original_filename = filename_from_content_disposition.presence || filename_from_path.presence || 'data' + @original_filename = truncated_filename @tempfile = copy_to_tempfile(@target) @content_type = ContentTypeDetector.new(@tempfile.path).detect @size = File.size(@tempfile) @@ -43,6 +43,13 @@ module Paperclip source.response.connection.close end + def truncated_filename + filename = filename_from_content_disposition.presence || filename_from_path.presence || 'data' + extension = File.extname(filename) + basename = File.basename(filename, extension) + [basename[...20], extension[..4]].compact_blank.join + end + def filename_from_content_disposition disposition = @target.response.headers['content-disposition'] disposition&.match(/filename="([^"]*)"/)&.captures&.first From c5c464804d7330211709a2be67fd8869000ce635 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 23 Jan 2024 15:02:19 +0100 Subject: [PATCH 047/117] Update dependency puma to v6.4.2 --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 76925a2bf..41045b5e5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -479,7 +479,7 @@ GEM net-smtp (0.3.3) net-protocol net-ssh (7.1.0) - nio4r (2.5.9) + nio4r (2.7.0) nokogiri (1.15.4) mini_portile2 (~> 2.8.2) racc (~> 1.4) @@ -534,7 +534,7 @@ GEM premailer (~> 1.7, >= 1.7.9) private_address_check (0.5.0) public_suffix (5.0.3) - puma (6.3.1) + puma (6.4.2) nio4r (~> 2.0) pundit (2.3.0) activesupport (>= 3.0.0) From 7a22999f925728ab8c51a9e1a262307d73a1c156 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 23 Jan 2024 15:03:45 +0100 Subject: [PATCH 048/117] Bump ruby version to 3.2.3 --- .ruby-version | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.ruby-version b/.ruby-version index be94e6f53..b347b11ea 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.2 +3.2.3 diff --git a/Dockerfile b/Dockerfile index f73bdcf78..980d70509 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # This needs to be bookworm-slim because the Ruby image is built on bookworm-slim ARG NODE_VERSION="20.6-bookworm-slim" -FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby +FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.3-slim as ruby FROM node:${NODE_VERSION} as build COPY --link --from=ruby /opt/ruby /opt/ruby From 4eb98ef7552835d0b9327c50da2d26b54725b5ae Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 15 Jan 2024 11:45:48 +0100 Subject: [PATCH 049/117] Ignore the devise-two-factor advisory as we have rate limits in place (#28733) --- .bundler-audit.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .bundler-audit.yml diff --git a/.bundler-audit.yml b/.bundler-audit.yml new file mode 100644 index 000000000..0671df390 --- /dev/null +++ b/.bundler-audit.yml @@ -0,0 +1,6 @@ +--- +ignore: + # devise-two-factor advisory about brute-forcing TOTP + # We have rate-limits on authentication endpoints in place (including second + # factor verification) since Mastodon v3.2.0 + - CVE-2024-0227 From 1ab050eb5201699e3d5d8c1abf673d7cd97c699b Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 24 Jan 2024 14:59:09 +0100 Subject: [PATCH 050/117] Change PostgreSQL version check to check for PostgreSQL 10+ --- lib/tasks/db.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/db.rake b/lib/tasks/db.rake index e8a64b8fb..f51a2459c 100644 --- a/lib/tasks/db.rake +++ b/lib/tasks/db.rake @@ -17,7 +17,7 @@ namespace :db do task :pre_migration_check do version = ActiveRecord::Base.connection.select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i - abort 'This version of Mastodon requires PostgreSQL 9.5 or newer. Please update PostgreSQL before updating Mastodon' if version < 90_500 + abort 'This version of Mastodon requires PostgreSQL 10.0 or newer. Please update PostgreSQL before updating Mastodon' if version < 100_000 end Rake::Task['db:migrate'].enhance(['db:pre_migration_check']) From 4633bb8ce09f216170c3f8370a8e48f3a41872cb Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 23 Jan 2024 14:58:27 +0100 Subject: [PATCH 051/117] Bump version to v4.2.4 --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 2 +- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f970519e..8c733f9a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ All notable changes to this project will be documented in this file. +## [4.2.4] - 2024-01-24 + +### Fixed + +- Fix error when processing remote files with unusually long names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28823)) +- Fix processing of compacted single-item JSON-LD collections ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28816)) +- Retry 401 errors on replies fetching ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/28788)) +- Fix `RecordNotUnique` errors in LinkCrawlWorker ([tribela](https://github.com/mastodon/mastodon/pull/28748)) +- Fix Mastodon not correctly processing HTTP Signatures with query strings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28443), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28476)) +- Fix potential redirection loop of streaming endpoint ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28665)) +- Fix streaming API redirection ignoring the port of `streaming_api_base_url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28558)) +- Fix error when processing link preview with an array as `inLanguage` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28252)) +- Fix unsupported time zone or locale preventing sign-up ([Gargron](https://github.com/mastodon/mastodon/pull/28035)) +- Fix "Hide these posts from home" list setting not refreshing when switching lists ([brianholley](https://github.com/mastodon/mastodon/pull/27763)) +- Fix missing background behind dismissable banner in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/27479)) +- Fix line wrapping of language selection button with long locale codes ([gunchleoc](https://github.com/mastodon/mastodon/pull/27100), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27127)) +- Fix `Undo Announce` activity not being sent to non-follower authors ([MitarashiDango](https://github.com/mastodon/mastodon/pull/18482)) +- Fix N+1s because of association preloaders not actually getting called ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28339)) +- Fix empty column explainer getting cropped under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28337)) +- Fix `LinkCrawlWorker` error when encountering empty OEmbed response ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28268)) +- Fix call to inefficient `delete_matched` cache method in domain blocks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28367)) + +### Security + +- Add rate-limit of TOTP authentication attempts at controller level ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28801)) + ## [4.2.3] - 2023-12-05 ### Fixed diff --git a/docker-compose.yml b/docker-compose.yml index dda71cb09..b096012de 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,7 @@ services: web: build: . - image: ghcr.io/mastodon/mastodon:v4.2.3 + image: ghcr.io/mastodon/mastodon:v4.2.4 restart: always env_file: .env.production command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" @@ -77,7 +77,7 @@ services: streaming: build: . - image: ghcr.io/mastodon/mastodon:v4.2.3 + image: ghcr.io/mastodon/mastodon:v4.2.4 restart: always env_file: .env.production command: node ./streaming @@ -95,7 +95,7 @@ services: sidekiq: build: . - image: ghcr.io/mastodon/mastodon:v4.2.3 + image: ghcr.io/mastodon/mastodon:v4.2.4 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 2e68df474..6382712cd 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 3 + 4 end def default_prerelease From a6641f828b9e6f5806be01754318279c2532ae82 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 1 Feb 2024 15:56:46 +0100 Subject: [PATCH 052/117] Merge pull request from GHSA-3fjr-858r-92rw * Fix insufficient origin validation * Bump version to v4.2.5 --- CHANGELOG.md | 6 ++++++ SECURITY.md | 4 +--- .../concerns/signature_verification.rb | 2 +- app/helpers/jsonld_helper.rb | 4 ++-- app/lib/activitypub/activity.rb | 2 +- app/lib/activitypub/linked_data_signature.rb | 2 +- .../activitypub/fetch_remote_account_service.rb | 2 +- .../activitypub/fetch_remote_actor_service.rb | 6 +++--- .../activitypub/fetch_remote_key_service.rb | 17 ++--------------- .../activitypub/fetch_remote_status_service.rb | 8 ++++---- .../activitypub/process_account_service.rb | 2 +- app/services/fetch_resource_service.rb | 10 +++++++++- docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 2 +- .../activitypub/linked_data_signature_spec.rb | 4 ++-- .../fetch_remote_account_service_spec.rb | 2 +- .../fetch_remote_actor_service_spec.rb | 2 +- .../fetch_remote_key_service_spec.rb | 2 +- spec/services/fetch_resource_service_spec.rb | 10 +++++----- spec/services/resolve_url_service_spec.rb | 1 + 20 files changed, 47 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c733f9a9..a34545be1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## [4.2.5] - 2024-02-01 + +### Security + +- Fix insufficient origin validation (CVE-2024-23832, [GHSA-3fjr-858r-92rw](https://github.com/mastodon/mastodon/security/advisories/GHSA-3fjr-858r-92rw)) + ## [4.2.4] - 2024-01-24 ### Fixed diff --git a/SECURITY.md b/SECURITY.md index 954ff73a2..667096324 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -17,6 +17,4 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through | ------- | ---------------- | | 4.2.x | Yes | | 4.1.x | Yes | -| 4.0.x | No | -| 3.5.x | Until 2023-12-31 | -| < 3.5 | No | +| < 4.1 | No | diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 35391e64c..92f1eb5a1 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -266,7 +266,7 @@ module SignatureVerification 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 diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index b3d0d032c..cc05b7a40 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -155,8 +155,8 @@ module JsonLdHelper 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']) diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 51384ef98..322f3e27a 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -154,7 +154,7 @@ class ActivityPub::Activity 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 diff --git a/app/lib/activitypub/linked_data_signature.rb b/app/lib/activitypub/linked_data_signature.rb index faea63e8f..9459fdd8b 100644 --- a/app/lib/activitypub/linked_data_signature.rb +++ b/app/lib/activitypub/linked_data_signature.rb @@ -19,7 +19,7 @@ class ActivityPub::LinkedDataSignature 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? diff --git a/app/services/activitypub/fetch_remote_account_service.rb b/app/services/activitypub/fetch_remote_account_service.rb index 567dd8a14..7b083d889 100644 --- a/app/services/activitypub/fetch_remote_account_service.rb +++ b/app/services/activitypub/fetch_remote_account_service.rb @@ -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) diff --git a/app/services/activitypub/fetch_remote_actor_service.rb b/app/services/activitypub/fetch_remote_actor_service.rb index 8df8c7587..86a134bb4 100644 --- a/app/services/activitypub/fetch_remote_actor_service.rb +++ b/app/services/activitypub/fetch_remote_actor_service.rb @@ -10,15 +10,15 @@ class ActivityPub::FetchRemoteActorService < BaseService 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}" diff --git a/app/services/activitypub/fetch_remote_key_service.rb b/app/services/activitypub/fetch_remote_key_service.rb index 8eb97c1e6..e96b5ad3b 100644 --- a/app/services/activitypub/fetch_remote_key_service.rb +++ b/app/services/activitypub/fetch_remote_key_service.rb @@ -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) diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index a491b32b2..5a3eeeaf4 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -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? @@ -65,7 +65,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService 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 diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 1304ca824..dc845ce97 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -277,7 +277,7 @@ class ActivityPub::ProcessAccountService < BaseService 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 diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb index a3406e5a5..71c6cca79 100644 --- a/app/services/fetch_resource_service.rb +++ b/app/services/fetch_resource_service.rb @@ -48,7 +48,15 @@ class FetchResourceService < BaseService 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) diff --git a/docker-compose.yml b/docker-compose.yml index b096012de..8aa93b1c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,7 @@ services: web: build: . - image: ghcr.io/mastodon/mastodon:v4.2.4 + image: ghcr.io/mastodon/mastodon:v4.2.5 restart: always env_file: .env.production command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" @@ -77,7 +77,7 @@ services: streaming: build: . - image: ghcr.io/mastodon/mastodon:v4.2.4 + image: ghcr.io/mastodon/mastodon:v4.2.5 restart: always env_file: .env.production command: node ./streaming @@ -95,7 +95,7 @@ services: sidekiq: build: . - image: ghcr.io/mastodon/mastodon:v4.2.4 + image: ghcr.io/mastodon/mastodon:v4.2.5 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 6382712cd..f9f801c77 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 4 + 5 end def default_prerelease diff --git a/spec/lib/activitypub/linked_data_signature_spec.rb b/spec/lib/activitypub/linked_data_signature_spec.rb index 03d0c5a87..e821cee6b 100644 --- a/spec/lib/activitypub/linked_data_signature_spec.rb +++ b/spec/lib/activitypub/linked_data_signature_spec.rb @@ -60,7 +60,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do 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 @@ -68,7 +68,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do 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 diff --git a/spec/services/activitypub/fetch_remote_account_service_spec.rb b/spec/services/activitypub/fetch_remote_account_service_spec.rb index ac7484d96..f33a928da 100644 --- a/spec/services/activitypub/fetch_remote_account_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_account_service_spec.rb @@ -18,7 +18,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do end describe '#call' do - let(:account) { subject.call('https://example.com/alice', id: true) } + let(:account) { subject.call('https://example.com/alice') } shared_examples 'sets profile data' do it 'returns an account' do diff --git a/spec/services/activitypub/fetch_remote_actor_service_spec.rb b/spec/services/activitypub/fetch_remote_actor_service_spec.rb index 93d31b69d..944a2f8b1 100644 --- a/spec/services/activitypub/fetch_remote_actor_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_actor_service_spec.rb @@ -18,7 +18,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do end describe '#call' do - let(:account) { subject.call('https://example.com/alice', id: true) } + let(:account) { subject.call('https://example.com/alice') } shared_examples 'sets profile data' do it 'returns an account' do diff --git a/spec/services/activitypub/fetch_remote_key_service_spec.rb b/spec/services/activitypub/fetch_remote_key_service_spec.rb index e210d20ec..0b14da4f4 100644 --- a/spec/services/activitypub/fetch_remote_key_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_key_service_spec.rb @@ -55,7 +55,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do end describe '#call' do - let(:account) { subject.call(public_key_id, id: false) } + let(:account) { subject.call(public_key_id) } context 'when the key is a sub-object from the actor' do before do diff --git a/spec/services/fetch_resource_service_spec.rb b/spec/services/fetch_resource_service_spec.rb index 0f1068471..78037a06c 100644 --- a/spec/services/fetch_resource_service_spec.rb +++ b/spec/services/fetch_resource_service_spec.rb @@ -57,7 +57,7 @@ RSpec.describe FetchResourceService, type: :service do let(:json) do { - id: 1, + id: 'http://example.com/foo', '@context': ActivityPub::TagManager::CONTEXT, type: 'Note', }.to_json @@ -83,27 +83,27 @@ RSpec.describe FetchResourceService, type: :service do let(:content_type) { 'application/activity+json; charset=utf-8' } let(:body) { json } - it { is_expected.to eq [1, { prefetched_body: body, id: true }] } + it { is_expected.to eq ['http://example.com/foo', { prefetched_body: body }] } end context 'when content type is ld+json with profile' do let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' } let(:body) { json } - it { is_expected.to eq [1, { prefetched_body: body, id: true }] } + it { is_expected.to eq ['http://example.com/foo', { prefetched_body: body }] } end context 'when link header is present' do let(:headers) { { 'Link' => '; rel="alternate"; type="application/activity+json"' } } - it { is_expected.to eq [1, { prefetched_body: json, id: true }] } + it { is_expected.to eq ['http://example.com/foo', { prefetched_body: json }] } end context 'when content type is text/html' do let(:content_type) { 'text/html' } let(:body) { '' } - it { is_expected.to eq [1, { prefetched_body: json, id: true }] } + it { is_expected.to eq ['http://example.com/foo', { prefetched_body: json }] } end end end diff --git a/spec/services/resolve_url_service_spec.rb b/spec/services/resolve_url_service_spec.rb index 7991aa6ef..38d35a3a1 100644 --- a/spec/services/resolve_url_service_spec.rb +++ b/spec/services/resolve_url_service_spec.rb @@ -139,6 +139,7 @@ describe ResolveURLService, type: :service do stub_request(:get, url).to_return(status: 302, headers: { 'Location' => status_url }) body = ActiveModelSerializers::SerializableResource.new(status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter).to_json stub_request(:get, status_url).to_return(body: body, headers: { 'Content-Type' => 'application/activity+json' }) + stub_request(:get, uri).to_return(body: body, headers: { 'Content-Type' => 'application/activity+json' }) end it 'returns status by url' do From b7230cd759719c3cdb83d7d013ccdc897720f186 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 14 Feb 2024 11:02:11 +0100 Subject: [PATCH 053/117] Update dependency nokogiri to 1.16.2 --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 41045b5e5..c9c9586d8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -457,7 +457,7 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2023.0808) mini_mime (1.1.5) - mini_portile2 (2.8.4) + mini_portile2 (2.8.5) minitest (5.19.0) msgpack (1.7.1) multi_json (1.15.0) @@ -480,7 +480,7 @@ GEM net-protocol net-ssh (7.1.0) nio4r (2.7.0) - nokogiri (1.15.4) + nokogiri (1.16.2) mini_portile2 (~> 2.8.2) racc (~> 1.4) oj (3.16.1) @@ -539,7 +539,7 @@ GEM pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.7.1) + racc (1.7.3) rack (2.2.8) rack-attack (6.7.0) rack (>= 1.0, < 4) From ae2dce813a9fa40ff1c568e8fd7d6f8f71207e3d Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 14 Feb 2024 11:02:55 +0100 Subject: [PATCH 054/117] Update dependency sidekiq-unique-jobs to 7.1.33 --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index c9c9586d8..1c767859b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -212,7 +212,7 @@ GEM climate_control (0.2.0) cocoon (1.2.15) color_diff (0.1) - concurrent-ruby (1.2.2) + concurrent-ruby (1.2.3) connection_pool (2.4.1) cose (1.3.0) cbor (~> 0.5.9) @@ -693,7 +693,7 @@ GEM rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) semantic_range (3.0.0) - sidekiq (6.5.10) + sidekiq (6.5.12) connection_pool (>= 2.2.5, < 3) rack (~> 2.0) redis (>= 4.5.0, < 5) @@ -703,7 +703,7 @@ GEM rufus-scheduler (~> 3.2) sidekiq (>= 6, < 8) tilt (>= 1.4.0) - sidekiq-unique-jobs (7.1.29) + sidekiq-unique-jobs (7.1.33) brpoplpush-redis_script (> 0.1.1, <= 2.0.0) concurrent-ruby (~> 1.0, >= 1.0.5) redis (< 5.0) @@ -748,7 +748,7 @@ GEM terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) test-prof (1.2.3) - thor (1.2.2) + thor (1.3.0) tilt (2.2.0) timeout (0.4.0) tpm-key_attestation (0.12.0) From 6d43b63275017738218e1e98744168d260180d78 Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Tue, 13 Feb 2024 19:11:47 +0100 Subject: [PATCH 055/117] Disable administrative doorkeeper routes (#29187) --- config/initializers/doorkeeper.rb | 9 +- .../requests/disabled_oauth_endpoints_spec.rb | 83 +++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 spec/requests/disabled_oauth_endpoints_spec.rb diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index a78405ace..9f06c1120 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -21,9 +21,14 @@ Doorkeeper.configure do user unless user&.otp_required_for_login? end - # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. + # Doorkeeper provides some administrative interfaces for managing OAuth + # Applications, allowing creation, edit, and deletion of applications from the + # server. At present, these administrative routes are not integrated into + # Mastodon, and as such, we've disabled them by always return a 403 forbidden + # response for them. This does not affect the ability for users to manage + # their own OAuth Applications. admin_authenticator do - current_user&.admin? || redirect_to(new_user_session_url) + head 403 end # Authorization Code expiration time (default 10 minutes). diff --git a/spec/requests/disabled_oauth_endpoints_spec.rb b/spec/requests/disabled_oauth_endpoints_spec.rb new file mode 100644 index 000000000..7c2c09f38 --- /dev/null +++ b/spec/requests/disabled_oauth_endpoints_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Disabled OAuth routes' do + # These routes are disabled via the doorkeeper configuration for + # `admin_authenticator`, as these routes should only be accessible by server + # administrators. For now, these routes are not properly designed and + # integrated into Mastodon, so we're disabling them completely + describe 'GET /oauth/applications' do + it 'returns 403 forbidden' do + get oauth_applications_path + + expect(response).to have_http_status(403) + end + end + + describe 'POST /oauth/applications' do + it 'returns 403 forbidden' do + post oauth_applications_path + + expect(response).to have_http_status(403) + end + end + + describe 'GET /oauth/applications/new' do + it 'returns 403 forbidden' do + get new_oauth_application_path + + expect(response).to have_http_status(403) + end + end + + describe 'GET /oauth/applications/:id' do + let(:application) { Fabricate(:application, scopes: 'read') } + + it 'returns 403 forbidden' do + get oauth_application_path(application) + + expect(response).to have_http_status(403) + end + end + + describe 'PATCH /oauth/applications/:id' do + let(:application) { Fabricate(:application, scopes: 'read') } + + it 'returns 403 forbidden' do + patch oauth_application_path(application) + + expect(response).to have_http_status(403) + end + end + + describe 'PUT /oauth/applications/:id' do + let(:application) { Fabricate(:application, scopes: 'read') } + + it 'returns 403 forbidden' do + put oauth_application_path(application) + + expect(response).to have_http_status(403) + end + end + + describe 'DELETE /oauth/applications/:id' do + let(:application) { Fabricate(:application, scopes: 'read') } + + it 'returns 403 forbidden' do + delete oauth_application_path(application) + + expect(response).to have_http_status(403) + end + end + + describe 'GET /oauth/applications/:id/edit' do + let(:application) { Fabricate(:application, scopes: 'read') } + + it 'returns 403 forbidden' do + get edit_oauth_application_path(application) + + expect(response).to have_http_status(403) + end + end +end From 1a33d348d093bd3d9ae099553f4c5a28e3dd8e29 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 14 Feb 2024 13:12:13 +0100 Subject: [PATCH 056/117] Add `sidekiq_unique_jobs:delete_all_locks` task and disable `sidekiq-unique-jobs` UI by default (#29199) --- config/routes.rb | 2 +- lib/tasks/sidekiq_unique_jobs.rake | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 lib/tasks/sidekiq_unique_jobs.rake diff --git a/config/routes.rb b/config/routes.rb index 379262f4c..f97bd4ef3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'sidekiq_unique_jobs/web' +require 'sidekiq_unique_jobs/web' if ENV['ENABLE_SIDEKIQ_UNIQUE_JOBS_UI'] == true require 'sidekiq-scheduler/web' class RedirectWithVary < ActionDispatch::Routing::PathRedirect diff --git a/lib/tasks/sidekiq_unique_jobs.rake b/lib/tasks/sidekiq_unique_jobs.rake new file mode 100644 index 000000000..bedc8fe4c --- /dev/null +++ b/lib/tasks/sidekiq_unique_jobs.rake @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +namespace :sidekiq_unique_jobs do + task delete_all_locks: :environment do + digests = SidekiqUniqueJobs::Digests.new + digests.delete_by_pattern('*', count: digests.count) + + expiring_digests = SidekiqUniqueJobs::ExpiringDigests.new + expiring_digests.delete_by_pattern('*', count: expiring_digests.count) + end +end From 0b0c7af2c112606c441b6b4083e6638b14b9263c Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 14 Feb 2024 15:15:34 +0100 Subject: [PATCH 057/117] Merge pull request from GHSA-7w3c-p9j8-mq3x * Ensure destruction of OAuth Applications notifies streaming Due to doorkeeper using a dependent: delete_all relationship, the destroy of an OAuth Application bypassed the existing AccessTokenExtension callbacks for announcing destructing of access tokens. * Ensure password resets revoke access to Streaming API * Improve performance of deleting OAuth tokens --------- Co-authored-by: Emelia Smith --- app/lib/application_extension.rb | 20 ++++++++++++++++++++ app/models/user.rb | 10 ++++++++++ spec/models/user_spec.rb | 7 +++++++ 3 files changed, 37 insertions(+) diff --git a/app/lib/application_extension.rb b/app/lib/application_extension.rb index fb442e2c2..400c51a02 100644 --- a/app/lib/application_extension.rb +++ b/app/lib/application_extension.rb @@ -4,14 +4,34 @@ module ApplicationExtension extend ActiveSupport::Concern included do + include Redisable + has_many :created_users, class_name: 'User', foreign_key: 'created_by_application_id', inverse_of: :created_by_application validates :name, length: { maximum: 60 } validates :website, url: true, length: { maximum: 2_000 }, if: :website? validates :redirect_uri, length: { maximum: 2_000 } + + # The relationship used between Applications and AccessTokens is using + # dependent: delete_all, which means the ActiveRecord callback in + # AccessTokenExtension is not run, so instead we manually announce to + # streaming that these tokens are being deleted. + before_destroy :push_to_streaming_api, prepend: true end def confirmation_redirect_uri redirect_uri.lines.first.strip end + + def push_to_streaming_api + # TODO: #28793 Combine into a single topic + payload = Oj.dump(event: :kill) + access_tokens.in_batches do |tokens| + redis.pipelined do |pipeline| + tokens.ids.each do |id| + pipeline.publish("timeline:access_token:#{id}", payload) + end + end + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index 309a00d1c..ee9097927 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -360,6 +360,16 @@ class User < ApplicationRecord Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch| batch.update_all(revoked_at: Time.now.utc) Web::PushSubscription.where(access_token_id: batch).delete_all + + # Revoke each access token for the Streaming API, since `update_all`` + # doesn't trigger ActiveRecord Callbacks: + # TODO: #28793 Combine into a single topic + payload = Oj.dump(event: :kill) + redis.pipelined do |pipeline| + batch.ids.each do |id| + pipeline.publish("timeline:access_token:#{id}", payload) + end + end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0dad96cc8..ffd1889cb 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -438,7 +438,10 @@ RSpec.describe User do let!(:access_token) { Fabricate(:access_token, resource_owner_id: user.id) } let!(:web_push_subscription) { Fabricate(:web_push_subscription, access_token: access_token) } + let(:redis_pipeline_stub) { instance_double(Redis::Namespace, publish: nil) } + before do + allow(redis).to receive(:pipelined).and_yield(redis_pipeline_stub) user.reset_password! end @@ -454,6 +457,10 @@ RSpec.describe User do expect(Doorkeeper::AccessToken.active_for(user).count).to eq 0 end + it 'revokes streaming access for all access tokens' do + expect(redis_pipeline_stub).to have_received(:publish).with("timeline:access_token:#{access_token.id}", Oj.dump(event: :kill)).once + end + it 'removes push subscriptions' do expect(Web::PushSubscription.where(user: user).or(Web::PushSubscription.where(access_token: access_token)).count).to eq 0 end From f1700523f15cd714fbb05b4f277a6feb40ab8e4b Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 14 Feb 2024 15:16:07 +0100 Subject: [PATCH 058/117] Merge pull request from GHSA-vm39-j3vx-pch3 * Prevent different identities from a same SSO provider from accessing a same account * Lock auth provider changes behind `ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH=true` * Rename methods to avoid confusion between OAuth and OmniAuth --- .../auth/omniauth_callbacks_controller.rb | 2 +- app/models/concerns/omniauthable.rb | 52 ++++++++++++++----- app/models/identity.rb | 2 +- spec/models/identity_spec.rb | 6 +-- spec/requests/omniauth_callbacks_spec.rb | 2 +- 5 files changed, 44 insertions(+), 20 deletions(-) diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb index 4723806b9..7bccac7f6 100644 --- a/app/controllers/auth/omniauth_callbacks_controller.rb +++ b/app/controllers/auth/omniauth_callbacks_controller.rb @@ -6,7 +6,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController def self.provides_callback_for(provider) define_method provider do @provider = provider - @user = User.find_for_oauth(request.env['omniauth.auth'], current_user) + @user = User.find_for_omniauth(request.env['omniauth.auth'], current_user) if @user.persisted? record_login_activity diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb index 3983fbcda..9c004a308 100644 --- a/app/models/concerns/omniauthable.rb +++ b/app/models/concerns/omniauthable.rb @@ -19,17 +19,18 @@ module Omniauthable end class_methods do - def find_for_oauth(auth, signed_in_resource = nil) + def find_for_omniauth(auth, signed_in_resource = nil) # EOLE-SSO Patch auth.uid = (auth.uid[0][:uid] || auth.uid[0][:user]) if auth.uid.is_a? Hashie::Array - identity = Identity.find_for_oauth(auth) + identity = Identity.find_for_omniauth(auth) # If a signed_in_resource is provided it always overrides the existing user # to prevent the identity being locked with accidentally created accounts. # Note that this may leave zombie accounts (with no associated identity) which # can be cleaned up at a later date. user = signed_in_resource || identity.user - user ||= create_for_oauth(auth) + user ||= reattach_for_auth(auth) + user ||= create_for_auth(auth) if identity.user.nil? identity.user = user @@ -39,19 +40,35 @@ module Omniauthable user end - def create_for_oauth(auth) - # Check if the user exists with provided email. If no email was provided, + private + + def reattach_for_auth(auth) + # If allowed, check if a user exists with the provided email address, + # and return it if they does not have an associated identity with the + # current authentication provider. + + # This can be used to provide a choice of alternative auth providers + # or provide smooth gradual transition between multiple auth providers, + # but this is discouraged because any insecure provider will put *all* + # local users at risk, regardless of which provider they registered with. + + return unless ENV['ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH'] == 'true' + + email, email_is_verified = email_from_auth(auth) + return unless email_is_verified + + user = User.find_by(email: email) + return if user.nil? || Identity.exists?(provider: auth.provider, user_id: user.id) + + user + end + + def create_for_auth(auth) + # Create a user for the given auth params. If no email was provided, # we assign a temporary email and ask the user to verify it on # the next step via Auth::SetupController.show - strategy = Devise.omniauth_configs[auth.provider.to_sym].strategy - assume_verified = strategy&.security&.assume_email_is_verified - email_is_verified = auth.info.verified || auth.info.verified_email || auth.info.email_verified || assume_verified - email = auth.info.verified_email || auth.info.email - - user = User.find_by(email: email) if email_is_verified - - return user unless user.nil? + email, email_is_verified = email_from_auth(auth) user = User.new(user_params_from_auth(email, auth)) @@ -66,7 +83,14 @@ module Omniauthable user end - private + def email_from_auth(auth) + strategy = Devise.omniauth_configs[auth.provider.to_sym].strategy + assume_verified = strategy&.security&.assume_email_is_verified + email_is_verified = auth.info.verified || auth.info.verified_email || auth.info.email_verified || assume_verified + email = auth.info.verified_email || auth.info.email + + [email, email_is_verified] + end def user_params_from_auth(email, auth) { diff --git a/app/models/identity.rb b/app/models/identity.rb index c95a68a6f..77821b78f 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -17,7 +17,7 @@ class Identity < ApplicationRecord validates :uid, presence: true, uniqueness: { scope: :provider } validates :provider, presence: true - def self.find_for_oauth(auth) + def self.find_for_omniauth(auth) find_or_create_by(uid: auth.uid, provider: auth.provider) end end diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb index 2fca1e1c1..22c8dbf22 100644 --- a/spec/models/identity_spec.rb +++ b/spec/models/identity_spec.rb @@ -3,16 +3,16 @@ require 'rails_helper' RSpec.describe Identity do - describe '.find_for_oauth' do + describe '.find_for_omniauth' do let(:auth) { Fabricate(:identity, user: Fabricate(:user)) } it 'calls .find_or_create_by' do expect(described_class).to receive(:find_or_create_by).with(uid: auth.uid, provider: auth.provider) - described_class.find_for_oauth(auth) + described_class.find_for_omniauth(auth) end it 'returns an instance of Identity' do - expect(described_class.find_for_oauth(auth)).to be_instance_of described_class + expect(described_class.find_for_omniauth(auth)).to be_instance_of described_class end end end diff --git a/spec/requests/omniauth_callbacks_spec.rb b/spec/requests/omniauth_callbacks_spec.rb index 27aa5ec50..c40ef14c9 100644 --- a/spec/requests/omniauth_callbacks_spec.rb +++ b/spec/requests/omniauth_callbacks_spec.rb @@ -96,7 +96,7 @@ describe 'OmniAuth callbacks' do context 'when a user cannot be built' do before do - allow(User).to receive(:find_for_oauth).and_return(User.new) + allow(User).to receive(:find_for_omniauth).and_return(User.new) end it 'redirects to the new user signup page' do From 7c8ca0c6d612329523fc4e468e82fc3e51634c1c Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 14 Feb 2024 13:29:38 +0100 Subject: [PATCH 059/117] Bump version to v4.2.6 --- CHANGELOG.md | 19 +++++++++++++++++++ docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 2 +- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a34545be1..14749caa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project will be documented in this file. +## [4.2.6] - 2024-02-14 + +### Security + +- Update the `sidekiq-unique-jobs` dependency (see [GHSA-cmh9-rx85-xj38](https://github.com/mhenrixon/sidekiq-unique-jobs/security/advisories/GHSA-cmh9-rx85-xj38)) + In addition, we have disabled the web interface for `sidekiq-unique-jobs` out of caution. + If you need it, you can re-enable it by setting `ENABLE_SIDEKIQ_UNIQUE_JOBS_UI=true`. + If you only need to clear all locks, you can now use `bundle exec rake sidekiq_unique_jobs:delete_all_locks`. +- Update the `nokogiri` dependency (see [GHSA-xc9x-jj77-9p9j](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-xc9x-jj77-9p9j)) +- Disable administrative Doorkeeper routes ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29187)) +- Fix ongoing streaming sessions not being invalidated when applications get deleted in some cases ([GHSA-7w3c-p9j8-mq3x](https://github.com/mastodon/mastodon/security/advisories/GHSA-7w3c-p9j8-mq3x)) + In some rare cases, the streaming server was not notified of access tokens revocation on application deletion. +- Change external authentication behavior to never reattach a new identity to an existing user by default ([GHSA-vm39-j3vx-pch3](https://github.com/mastodon/mastodon/security/advisories/GHSA-vm39-j3vx-pch3)) + Up until now, Mastodon has allowed new identities from external authentication providers to attach to an existing local user based on their verified e-mail address. + This allowed upgrading users from a database-stored password to an external authentication provider, or move from one authentication provider to another. + However, this behavior may be unexpected, and means that when multiple authentication providers are configured, the overall security would be that of the least secure authentication provider. + For these reasons, this behavior is now locked under the `ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH` environment variable. + In addition, regardless of this environment variable, Mastodon will refuse to attach two identities from the same authentication provider to the same account. + ## [4.2.5] - 2024-02-01 ### Security diff --git a/docker-compose.yml b/docker-compose.yml index 8aa93b1c0..2f03db04d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,7 @@ services: web: build: . - image: ghcr.io/mastodon/mastodon:v4.2.5 + image: ghcr.io/mastodon/mastodon:v4.2.6 restart: always env_file: .env.production command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" @@ -77,7 +77,7 @@ services: streaming: build: . - image: ghcr.io/mastodon/mastodon:v4.2.5 + image: ghcr.io/mastodon/mastodon:v4.2.6 restart: always env_file: .env.production command: node ./streaming @@ -95,7 +95,7 @@ services: sidekiq: build: . - image: ghcr.io/mastodon/mastodon:v4.2.5 + image: ghcr.io/mastodon/mastodon:v4.2.6 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index f9f801c77..2256b20a6 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 5 + 6 end def default_prerelease From 76a37bd040861549757714d2c1df733feec2f3bc Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 14 Feb 2024 15:57:49 +0100 Subject: [PATCH 060/117] Fix OmniAuth tests (#29201) --- spec/requests/omniauth_callbacks_spec.rb | 35 ++++++++++++++++++------ 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/spec/requests/omniauth_callbacks_spec.rb b/spec/requests/omniauth_callbacks_spec.rb index c40ef14c9..6bbe8c5a7 100644 --- a/spec/requests/omniauth_callbacks_spec.rb +++ b/spec/requests/omniauth_callbacks_spec.rb @@ -39,16 +39,33 @@ describe 'OmniAuth callbacks' do Fabricate(:user, email: 'user@host.example') end - it 'matches the existing user, creates an identity, and redirects to root path' do - expect { subject } - .to not_change(User, :count) - .and change(Identity, :count) - .by(1) - .and change(LoginActivity, :count) - .by(1) + context 'when ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH is set to true' do + around do |example| + ClimateControl.modify ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH: 'true' do + example.run + end + end - expect(Identity.find_by(user: User.last).uid).to eq('123') - expect(response).to redirect_to(root_path) + it 'matches the existing user, creates an identity, and redirects to root path' do + expect { subject } + .to not_change(User, :count) + .and change(Identity, :count) + .by(1) + .and change(LoginActivity, :count) + .by(1) + + expect(Identity.find_by(user: User.last).uid).to eq('123') + expect(response).to redirect_to(root_path) + end + end + + context 'when ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH is not set to true' do + it 'does not match the existing user or create an identity' do + expect { subject } + .to not_change(User, :count) + .and not_change(Identity, :count) + .and not_change(LoginActivity, :count) + end end end From 870ee80fd366c54ee2a7fc1508d7588d3fdeb878 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 14 Feb 2024 22:49:45 +0100 Subject: [PATCH 061/117] Fix user creation failure handling in OAuth paths (#29207) --- app/controllers/auth/omniauth_callbacks_controller.rb | 3 +++ config/locales/devise.en.yml | 1 + spec/requests/omniauth_callbacks_spec.rb | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb index 7bccac7f6..b8570d0bf 100644 --- a/app/controllers/auth/omniauth_callbacks_controller.rb +++ b/app/controllers/auth/omniauth_callbacks_controller.rb @@ -16,6 +16,9 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController session["devise.#{provider}_data"] = request.env['omniauth.auth'] redirect_to new_user_registration_url end + rescue ActiveRecord::RecordInvalid + flash[:alert] = I18n.t('devise.failure.omniauth_user_creation_failure') if is_navigational_format? + redirect_to new_user_session_url end end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index eef821481..b7c060016 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -12,6 +12,7 @@ en: last_attempt: You have one more attempt before your account is locked. locked: Your account is locked. not_found_in_database: Invalid %{authentication_keys} or password. + omniauth_user_creation_failure: Error creating an account for this identity. pending: Your account is still under review. timeout: Your session expired. Please login again to continue. unauthenticated: You need to login or sign up before continuing. diff --git a/spec/requests/omniauth_callbacks_spec.rb b/spec/requests/omniauth_callbacks_spec.rb index 6bbe8c5a7..6381bf066 100644 --- a/spec/requests/omniauth_callbacks_spec.rb +++ b/spec/requests/omniauth_callbacks_spec.rb @@ -60,11 +60,13 @@ describe 'OmniAuth callbacks' do end context 'when ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH is not set to true' do - it 'does not match the existing user or create an identity' do + it 'does not match the existing user or create an identity, and redirects to login page' do expect { subject } .to not_change(User, :count) .and not_change(Identity, :count) .and not_change(LoginActivity, :count) + + expect(response).to redirect_to(new_user_session_url) end end end From e4ec4ce217e4f1c10cbae5df75a8c22d813b9ba5 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 14 Feb 2024 23:27:02 +0100 Subject: [PATCH 062/117] Update `nsa` gem to version 0.3.0 (#29065) (#29206) Co-authored-by: Matt Jankowski --- Gemfile | 2 +- Gemfile.lock | 18 ++++++------------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/Gemfile b/Gemfile index 6f20ff25a..829e7d857 100644 --- a/Gemfile +++ b/Gemfile @@ -61,7 +61,7 @@ gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar' gem 'nokogiri', '~> 1.15' -gem 'nsa', github: 'jhawthorn/nsa', ref: 'e020fcc3a54d993ab45b7194d89ab720296c111b' +gem 'nsa' gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' gem 'parslet' diff --git a/Gemfile.lock b/Gemfile.lock index 1c767859b..37553641c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,17 +7,6 @@ GIT hkdf (~> 0.2) jwt (~> 2.0) -GIT - remote: https://github.com/jhawthorn/nsa.git - revision: e020fcc3a54d993ab45b7194d89ab720296c111b - ref: e020fcc3a54d993ab45b7194d89ab720296c111b - specs: - nsa (0.2.8) - activesupport (>= 4.2, < 7.2) - concurrent-ruby (~> 1.0, >= 1.0.2) - sidekiq (>= 3.5) - statsd-ruby (~> 1.4, >= 1.4.0) - GIT remote: https://github.com/mastodon/rails-settings-cached.git revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab @@ -483,6 +472,11 @@ GEM nokogiri (1.16.2) mini_portile2 (~> 2.8.2) racc (~> 1.4) + nsa (0.3.0) + activesupport (>= 4.2, < 7.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + sidekiq (>= 3.5) + statsd-ruby (~> 1.4, >= 1.4.0) oj (3.16.1) omniauth (2.1.1) hashie (>= 3.4.6) @@ -884,7 +878,7 @@ DEPENDENCIES net-http (~> 0.3.2) net-ldap (~> 0.18) nokogiri (~> 1.15) - nsa! + nsa oj (~> 3.14) omniauth (~> 2.0) omniauth-cas! From 684f99908f8beab6c002aac5871f5165c583a5ac Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 16 Feb 2024 09:19:35 +0100 Subject: [PATCH 063/117] Update dependency pg to 1.5.5 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 37553641c..134e41a7e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -514,7 +514,7 @@ GEM parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.5.4) + pg (1.5.5) pghero (3.3.4) activerecord (>= 6) posix-spawn (0.3.15) From 15de5202012a2f9c4417724987e54bc885051c0d Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 16 Feb 2024 11:56:12 +0100 Subject: [PATCH 064/117] Merge pull request from GHSA-jhrq-qvrm-qr36 * Fix insufficient Content-Type checking of fetched ActivityStreams objects * Allow JSON-LD documents with multiple profiles --- app/helpers/jsonld_helper.rb | 14 +++++++++++++- app/services/fetch_resource_service.rb | 2 +- spec/helpers/jsonld_helper_spec.rb | 14 +++++++------- spec/lib/activitypub/activity/announce_spec.rb | 4 ++-- .../fetch_featured_collection_service_spec.rb | 16 ++++++++-------- ...etch_featured_tags_collection_service_spec.rb | 8 ++++---- .../fetch_remote_account_service_spec.rb | 10 +++++----- .../fetch_remote_actor_service_spec.rb | 10 +++++----- .../activitypub/fetch_remote_key_service_spec.rb | 8 ++++---- .../activitypub/fetch_replies_service_spec.rb | 6 +++--- .../synchronize_followers_service_spec.rb | 6 +++--- .../activitypub/fetch_replies_worker_spec.rb | 2 +- 12 files changed, 56 insertions(+), 44 deletions(-) diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index cc05b7a40..b0f2077db 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -174,7 +174,19 @@ module JsonLdHelper build_request(uri, on_behalf_of, options: request_options).perform do |response| raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error - body_to_json(response.body_with_limit) if response.code == 200 + body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response) + end + end + + def valid_activitypub_content_type?(response) + return true if response.mime_type == 'application/activity+json' + + # When the mime type is `application/ld+json`, we need to check the profile, + # but `http.rb` does not parse it for us. + return false unless response.mime_type == 'application/ld+json' + + response.headers[HTTP::Headers::CONTENT_TYPE]&.split(';')&.map(&:strip)&.any? do |str| + str.start_with?('profile="') && str[9...-1].split.include?('https://www.w3.org/ns/activitystreams') end end diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb index 71c6cca79..84c36f6a1 100644 --- a/app/services/fetch_resource_service.rb +++ b/app/services/fetch_resource_service.rb @@ -44,7 +44,7 @@ class FetchResourceService < BaseService @response_code = response.code return nil if response.code != 200 - if ['application/activity+json', 'application/ld+json'].include?(response.mime_type) + if valid_activitypub_content_type?(response) body = response.body_with_limit json = body_to_json(body) diff --git a/spec/helpers/jsonld_helper_spec.rb b/spec/helpers/jsonld_helper_spec.rb index 3575bba85..6367492b9 100644 --- a/spec/helpers/jsonld_helper_spec.rb +++ b/spec/helpers/jsonld_helper_spec.rb @@ -56,15 +56,15 @@ describe JsonLdHelper do describe '#fetch_resource' do context 'when the second argument is false' do it 'returns resource even if the retrieved ID and the given URI does not match' do - stub_request(:get, 'https://bob.test/').to_return body: '{"id": "https://alice.test/"}' - stub_request(:get, 'https://alice.test/').to_return body: '{"id": "https://alice.test/"}' + stub_request(:get, 'https://bob.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://alice.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' }) expect(fetch_resource('https://bob.test/', false)).to eq({ 'id' => 'https://alice.test/' }) end it 'returns nil if the object identified by the given URI and the object identified by the retrieved ID does not match' do - stub_request(:get, 'https://mallory.test/').to_return body: '{"id": "https://marvin.test/"}' - stub_request(:get, 'https://marvin.test/').to_return body: '{"id": "https://alice.test/"}' + stub_request(:get, 'https://mallory.test/').to_return(body: '{"id": "https://marvin.test/"}', headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://marvin.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' }) expect(fetch_resource('https://mallory.test/', false)).to be_nil end @@ -72,7 +72,7 @@ describe JsonLdHelper do context 'when the second argument is true' do it 'returns nil if the retrieved ID and the given URI does not match' do - stub_request(:get, 'https://mallory.test/').to_return body: '{"id": "https://alice.test/"}' + stub_request(:get, 'https://mallory.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' }) expect(fetch_resource('https://mallory.test/', true)).to be_nil end end @@ -80,12 +80,12 @@ describe JsonLdHelper do describe '#fetch_resource_without_id_validation' do it 'returns nil if the status code is not 200' do - stub_request(:get, 'https://host.test/').to_return status: 400, body: '{}' + stub_request(:get, 'https://host.test/').to_return(status: 400, body: '{}', headers: { 'Content-Type': 'application/activity+json' }) expect(fetch_resource_without_id_validation('https://host.test/')).to be_nil end it 'returns hash' do - stub_request(:get, 'https://host.test/').to_return status: 200, body: '{}' + stub_request(:get, 'https://host.test/').to_return(status: 200, body: '{}', headers: { 'Content-Type': 'application/activity+json' }) expect(fetch_resource_without_id_validation('https://host.test/')).to eq({}) end end diff --git a/spec/lib/activitypub/activity/announce_spec.rb b/spec/lib/activitypub/activity/announce_spec.rb index 8ad892975..b556bfd6c 100644 --- a/spec/lib/activitypub/activity/announce_spec.rb +++ b/spec/lib/activitypub/activity/announce_spec.rb @@ -35,7 +35,7 @@ RSpec.describe ActivityPub::Activity::Announce do context 'when sender is followed by a local account' do before do Fabricate(:account).follow!(sender) - stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json)) + stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' }) subject.perform end @@ -120,7 +120,7 @@ RSpec.describe ActivityPub::Activity::Announce do let(:object_json) { 'https://example.com/actor/hello-world' } before do - stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json)) + stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' }) end context 'when the relay is enabled' do diff --git a/spec/services/activitypub/fetch_featured_collection_service_spec.rb b/spec/services/activitypub/fetch_featured_collection_service_spec.rb index a096296ff..583212c37 100644 --- a/spec/services/activitypub/fetch_featured_collection_service_spec.rb +++ b/spec/services/activitypub/fetch_featured_collection_service_spec.rb @@ -62,10 +62,10 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do shared_examples 'sets pinned posts' do before do - stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: Oj.dump(status_json_pinned_known)) - stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_inlined)) + stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: Oj.dump(status_json_pinned_known), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_inlined), headers: { 'Content-Type': 'application/activity+json' }) stub_request(:get, 'https://example.com/account/pinned/unknown-unreachable').to_return(status: 404) - stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable)) + stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' }) subject.call(actor, note: true, hashtag: false) end @@ -82,7 +82,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do describe '#call' do context 'when the endpoint is a Collection' do before do - stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload)) + stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets pinned posts' @@ -99,7 +99,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do end before do - stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload)) + stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets pinned posts' @@ -108,7 +108,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do let(:items) { 'https://example.com/account/pinned/unknown-reachable' } before do - stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable)) + stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' }) subject.call(actor, note: true, hashtag: false) end @@ -135,7 +135,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do end before do - stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload)) + stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets pinned posts' @@ -144,7 +144,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do let(:items) { 'https://example.com/account/pinned/unknown-reachable' } before do - stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable)) + stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' }) subject.call(actor, note: true, hashtag: false) end diff --git a/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb b/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb index 071e4d92d..638278a10 100644 --- a/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb +++ b/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb @@ -38,7 +38,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService, type: :service d describe '#call' do context 'when the endpoint is a Collection' do before do - stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload)) + stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets featured tags' @@ -46,7 +46,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService, type: :service d context 'when the account already has featured tags' do before do - stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload)) + stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) actor.featured_tags.create!(name: 'FoO') actor.featured_tags.create!(name: 'baz') @@ -67,7 +67,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService, type: :service d end before do - stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload)) + stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets featured tags' @@ -88,7 +88,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService, type: :service d end before do - stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload)) + stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets featured tags' diff --git a/spec/services/activitypub/fetch_remote_account_service_spec.rb b/spec/services/activitypub/fetch_remote_account_service_spec.rb index f33a928da..42badde05 100644 --- a/spec/services/activitypub/fetch_remote_account_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_account_service_spec.rb @@ -44,7 +44,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do before do actor[:inbox] = nil - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) + stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end @@ -67,7 +67,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) + stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end @@ -93,7 +93,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) + stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end @@ -125,7 +125,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) + stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end @@ -148,7 +148,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) + stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end diff --git a/spec/services/activitypub/fetch_remote_actor_service_spec.rb b/spec/services/activitypub/fetch_remote_actor_service_spec.rb index 944a2f8b1..6d264b7b8 100644 --- a/spec/services/activitypub/fetch_remote_actor_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_actor_service_spec.rb @@ -44,7 +44,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do before do actor[:inbox] = nil - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) + stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end @@ -67,7 +67,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) + stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end @@ -93,7 +93,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) + stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end @@ -125,7 +125,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) + stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end @@ -148,7 +148,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) + stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end diff --git a/spec/services/activitypub/fetch_remote_key_service_spec.rb b/spec/services/activitypub/fetch_remote_key_service_spec.rb index 0b14da4f4..478778cc9 100644 --- a/spec/services/activitypub/fetch_remote_key_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_key_service_spec.rb @@ -50,7 +50,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do end before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) + stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end @@ -59,7 +59,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do context 'when the key is a sub-object from the actor' do before do - stub_request(:get, public_key_id).to_return(body: Oj.dump(actor)) + stub_request(:get, public_key_id).to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) end it 'returns the expected account' do @@ -71,7 +71,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do let(:public_key_id) { 'https://example.com/alice-public-key.json' } before do - stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] }))) + stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })), headers: { 'Content-Type': 'application/activity+json' }) end it 'returns the expected account' do @@ -84,7 +84,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do let(:actor_public_key) { 'https://example.com/alice-public-key.json' } before do - stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] }))) + stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })), headers: { 'Content-Type': 'application/activity+json' }) end it 'returns the nil' do diff --git a/spec/services/activitypub/fetch_replies_service_spec.rb b/spec/services/activitypub/fetch_replies_service_spec.rb index 62841d7d2..73c2b4506 100644 --- a/spec/services/activitypub/fetch_replies_service_spec.rb +++ b/spec/services/activitypub/fetch_replies_service_spec.rb @@ -55,7 +55,7 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do context 'when passing the URL to the collection' do before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) + stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) end it 'spawns workers for up to 5 replies on the same server' do @@ -84,7 +84,7 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do context 'when passing the URL to the collection' do before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) + stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) end it 'spawns workers for up to 5 replies on the same server' do @@ -117,7 +117,7 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do context 'when passing the URL to the collection' do before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) + stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) end it 'spawns workers for up to 5 replies on the same server' do diff --git a/spec/services/activitypub/synchronize_followers_service_spec.rb b/spec/services/activitypub/synchronize_followers_service_spec.rb index c9a513e24..f62376ab9 100644 --- a/spec/services/activitypub/synchronize_followers_service_spec.rb +++ b/spec/services/activitypub/synchronize_followers_service_spec.rb @@ -60,7 +60,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService, type: :service do describe '#call' do context 'when the endpoint is a Collection of actor URIs' do before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) + stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'synchronizes followers' @@ -77,7 +77,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService, type: :service do end before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) + stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'synchronizes followers' @@ -98,7 +98,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService, type: :service do end before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) + stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'synchronizes followers' diff --git a/spec/workers/activitypub/fetch_replies_worker_spec.rb b/spec/workers/activitypub/fetch_replies_worker_spec.rb index ff4d049a2..2d080e286 100644 --- a/spec/workers/activitypub/fetch_replies_worker_spec.rb +++ b/spec/workers/activitypub/fetch_replies_worker_spec.rb @@ -21,7 +21,7 @@ describe ActivityPub::FetchRepliesWorker do describe 'perform' do it 'performs a request if the collection URI is from the same host' do - stub_request(:get, 'https://example.com/statuses_replies/1').to_return(status: 200, body: json) + stub_request(:get, 'https://example.com/statuses_replies/1').to_return(status: 200, body: json, headers: { 'Content-Type': 'application/activity+json' }) subject.perform(status.id, 'https://example.com/statuses_replies/1') expect(a_request(:get, 'https://example.com/statuses_replies/1')).to have_been_made.once end From 0e4e98fad1e21e4f356b3a71eef3fca79890105c Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Feb 2024 11:56:35 +0100 Subject: [PATCH 065/117] Bump version to v4.2.7 --- CHANGELOG.md | 11 +++++++++++ docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14749caa3..2a932250b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. +## [4.2.7] - 2024-02-16 + +### Fixed + +- Fix OmniAuth tests and edge cases in error handling ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29201), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/29207)) +- Fix new installs by upgrading to the latest release of the `nsa` gem, instead of a no longer existing commit ([mjankowski](https://github.com/mastodon/mastodon/pull/29065)) + +### Security + +- Fix insufficient checking of remote posts ([GHSA-jhrq-qvrm-qr36](https://github.com/mastodon/mastodon/security/advisories/GHSA-jhrq-qvrm-qr36)) + ## [4.2.6] - 2024-02-14 ### Security diff --git a/docker-compose.yml b/docker-compose.yml index 2f03db04d..4563e2317 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,7 @@ services: web: build: . - image: ghcr.io/mastodon/mastodon:v4.2.6 + image: ghcr.io/mastodon/mastodon:v4.2.7 restart: always env_file: .env.production command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" @@ -77,7 +77,7 @@ services: streaming: build: . - image: ghcr.io/mastodon/mastodon:v4.2.6 + image: ghcr.io/mastodon/mastodon:v4.2.7 restart: always env_file: .env.production command: node ./streaming @@ -95,7 +95,7 @@ services: sidekiq: build: . - image: ghcr.io/mastodon/mastodon:v4.2.6 + image: ghcr.io/mastodon/mastodon:v4.2.7 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 2256b20a6..486dea920 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 6 + 7 end def default_prerelease From c5d56de98d784825edc464b6e5e47884294ea701 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 16 Feb 2024 13:57:04 +0100 Subject: [PATCH 066/117] Fix linting failure --- SECURITY.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 667096324..81472b01b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -13,8 +13,8 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through ## Supported Versions -| Version | Supported | -| ------- | ---------------- | -| 4.2.x | Yes | -| 4.1.x | Yes | -| < 4.1 | No | +| Version | Supported | +| ------- | --------- | +| 4.2.x | Yes | +| 4.1.x | Yes | +| < 4.1 | No | From fbb07893b82002686a144699ded4401ccd5b8ea7 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 22 Feb 2024 13:25:53 +0100 Subject: [PATCH 067/117] Update dependencies (#29346) --- Gemfile.lock | 120 +++++++++++++++++++++++++-------------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 134e41a7e..9290a84ae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -28,47 +28,47 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8) - actionpack (= 7.0.8) - activesupport (= 7.0.8) + actioncable (7.0.8.1) + actionpack (= 7.0.8.1) + activesupport (= 7.0.8.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8) - actionpack (= 7.0.8) - activejob (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + actionmailbox (7.0.8.1) + actionpack (= 7.0.8.1) + activejob (= 7.0.8.1) + activerecord (= 7.0.8.1) + activestorage (= 7.0.8.1) + activesupport (= 7.0.8.1) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8) - actionpack (= 7.0.8) - actionview (= 7.0.8) - activejob (= 7.0.8) - activesupport (= 7.0.8) + actionmailer (7.0.8.1) + actionpack (= 7.0.8.1) + actionview (= 7.0.8.1) + activejob (= 7.0.8.1) + activesupport (= 7.0.8.1) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.8) - actionview (= 7.0.8) - activesupport (= 7.0.8) + actionpack (7.0.8.1) + actionview (= 7.0.8.1) + activesupport (= 7.0.8.1) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8) - actionpack (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + actiontext (7.0.8.1) + actionpack (= 7.0.8.1) + activerecord (= 7.0.8.1) + activestorage (= 7.0.8.1) + activesupport (= 7.0.8.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8) - activesupport (= 7.0.8) + actionview (7.0.8.1) + activesupport (= 7.0.8.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -78,22 +78,22 @@ GEM activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (7.0.8) - activesupport (= 7.0.8) + activejob (7.0.8.1) + activesupport (= 7.0.8.1) globalid (>= 0.3.6) - activemodel (7.0.8) - activesupport (= 7.0.8) - activerecord (7.0.8) - activemodel (= 7.0.8) - activesupport (= 7.0.8) - activestorage (7.0.8) - actionpack (= 7.0.8) - activejob (= 7.0.8) - activerecord (= 7.0.8) - activesupport (= 7.0.8) + activemodel (7.0.8.1) + activesupport (= 7.0.8.1) + activerecord (7.0.8.1) + activemodel (= 7.0.8.1) + activesupport (= 7.0.8.1) + activestorage (7.0.8.1) + actionpack (= 7.0.8.1) + activejob (= 7.0.8.1) + activerecord (= 7.0.8.1) + activesupport (= 7.0.8.1) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.8) + activesupport (7.0.8.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -215,7 +215,7 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.3.3) + date (3.3.4) debug_inspector (1.1.0) devise (4.9.2) bcrypt (~> 3.0) @@ -424,7 +424,7 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.21.3) + loofah (2.21.4) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -461,11 +461,11 @@ GEM net-ldap (0.18.0) net-pop (0.1.2) net-protocol - net-protocol (0.2.1) + net-protocol (0.2.2) timeout net-scp (4.0.0) net-ssh (>= 2.6.5, < 8.0.0) - net-smtp (0.3.3) + net-smtp (0.3.4) net-protocol net-ssh (7.1.0) nio4r (2.7.0) @@ -534,7 +534,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.7.3) - rack (2.2.8) + rack (2.2.8.1) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-cors (2.0.1) @@ -551,20 +551,20 @@ GEM rack rack-test (2.1.0) rack (>= 1.3) - rails (7.0.8) - actioncable (= 7.0.8) - actionmailbox (= 7.0.8) - actionmailer (= 7.0.8) - actionpack (= 7.0.8) - actiontext (= 7.0.8) - actionview (= 7.0.8) - activejob (= 7.0.8) - activemodel (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + rails (7.0.8.1) + actioncable (= 7.0.8.1) + actionmailbox (= 7.0.8.1) + actionmailer (= 7.0.8.1) + actionpack (= 7.0.8.1) + actiontext (= 7.0.8.1) + actionview (= 7.0.8.1) + activejob (= 7.0.8.1) + activemodel (= 7.0.8.1) + activerecord (= 7.0.8.1) + activestorage (= 7.0.8.1) + activesupport (= 7.0.8.1) bundler (>= 1.15.0) - railties (= 7.0.8) + railties (= 7.0.8.1) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -579,9 +579,9 @@ GEM rails-i18n (7.0.7) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.0.8) - actionpack (= 7.0.8) - activesupport (= 7.0.8) + railties (7.0.8.1) + actionpack (= 7.0.8.1) + activesupport (= 7.0.8.1) method_source rake (>= 12.2) thor (~> 1.0) @@ -744,7 +744,7 @@ GEM test-prof (1.2.3) thor (1.3.0) tilt (2.2.0) - timeout (0.4.0) + timeout (0.4.1) tpm-key_attestation (0.12.0) bindata (~> 2.4) openssl (> 2.0) @@ -808,7 +808,7 @@ GEM xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.11) + zeitwerk (2.6.13) PLATFORMS ruby From 28b666b0d522071e38a5a0a3376d4548c36ee3e9 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 22 Feb 2024 14:39:42 +0100 Subject: [PATCH 068/117] Automatically switch from open to approved registrations in absence of moderators (#29337) --- app/mailers/admin_mailer.rb | 6 ++ .../auto_close_registrations.text.erb | 3 + .../auto_close_registrations_scheduler.rb | 33 ++++++++++ config/locales/en.yml | 3 + config/sidekiq.yml | 4 ++ ...auto_close_registrations_scheduler_spec.rb | 60 +++++++++++++++++++ 6 files changed, 109 insertions(+) create mode 100644 app/views/admin_mailer/auto_close_registrations.text.erb create mode 100644 app/workers/scheduler/auto_close_registrations_scheduler.rb create mode 100644 spec/workers/scheduler/auto_close_registrations_scheduler_spec.rb diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index 990b92c33..6b08aa8cc 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -61,6 +61,12 @@ class AdminMailer < ApplicationMailer end end + def auto_close_registrations + locale_for_account(@me) do + mail subject: default_i18n_subject(instance: @instance) + end + end + private def process_params diff --git a/app/views/admin_mailer/auto_close_registrations.text.erb b/app/views/admin_mailer/auto_close_registrations.text.erb new file mode 100644 index 000000000..c0f848692 --- /dev/null +++ b/app/views/admin_mailer/auto_close_registrations.text.erb @@ -0,0 +1,3 @@ +<%= raw t('admin_mailer.auto_close_registrations.body', instance: @instance) %> + +<%= raw t('application_mailer.view')%> <%= admin_settings_registrations_url %> diff --git a/app/workers/scheduler/auto_close_registrations_scheduler.rb b/app/workers/scheduler/auto_close_registrations_scheduler.rb new file mode 100644 index 000000000..17516dd23 --- /dev/null +++ b/app/workers/scheduler/auto_close_registrations_scheduler.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class Scheduler::AutoCloseRegistrationsScheduler + include Sidekiq::Worker + include Redisable + + sidekiq_options retry: 0 + + # Automatically switch away from open registrations if no + # moderator had any activity in that period of time + OPEN_REGISTRATIONS_MODERATOR_THRESHOLD = 1.week + UserTrackingConcern::SIGN_IN_UPDATE_FREQUENCY + + def perform + return if Rails.configuration.x.email_domains_whitelist.present? || ENV['DISABLE_AUTOMATIC_SWITCHING_TO_APPROVED_REGISTRATIONS'] == 'true' + return unless Setting.registrations_mode == 'open' + + switch_to_approval_mode! unless active_moderators? + end + + private + + def active_moderators? + User.those_who_can(:manage_reports).exists?(current_sign_in_at: OPEN_REGISTRATIONS_MODERATOR_THRESHOLD.ago...) + end + + def switch_to_approval_mode! + Setting.registrations_mode = 'approved' + + User.those_who_can(:view_devops).includes(:account).find_each do |user| + AdminMailer.with(recipient: user.account).auto_close_registrations.deliver_later + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 2b3bfb4c7..85a0a9ff9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -963,6 +963,9 @@ en: title: Webhooks webhook: Webhook admin_mailer: + auto_close_registrations: + body: Due to a lack of recent moderator activity, registrations on %{instance} have been automatically switched to requiring manual review, to prevent %{instance} from being used as a platform for potential bad actors. You can switch it back to open registrations at any time. + subject: Registrations for %{instance} have been automatically switched to requiring approval new_appeal: actions: delete_statuses: to delete their posts diff --git a/config/sidekiq.yml b/config/sidekiq.yml index f1ba5651d..8c9481c05 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -62,3 +62,7 @@ interval: 30 minutes class: Scheduler::SoftwareUpdateCheckScheduler queue: scheduler + auto_close_registrations_scheduler: + interval: 1 hour + class: Scheduler::AutoCloseRegistrationsScheduler + queue: scheduler diff --git a/spec/workers/scheduler/auto_close_registrations_scheduler_spec.rb b/spec/workers/scheduler/auto_close_registrations_scheduler_spec.rb new file mode 100644 index 000000000..c0c50b128 --- /dev/null +++ b/spec/workers/scheduler/auto_close_registrations_scheduler_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Scheduler::AutoCloseRegistrationsScheduler do + subject { described_class.new } + + describe '#perform' do + let(:moderator_activity_date) { Time.now.utc } + + before do + Fabricate(:user, role: UserRole.find_by(name: 'Owner'), current_sign_in_at: 10.years.ago) + Fabricate(:user, role: UserRole.find_by(name: 'Moderator'), current_sign_in_at: moderator_activity_date) + end + + context 'when registrations are open' do + before do + Setting.registrations_mode = 'open' + end + + context 'when a moderator has logged in recently' do + let(:moderator_activity_date) { Time.now.utc } + + it 'does not change registrations mode' do + expect { subject.perform }.to_not change(Setting, :registrations_mode) + end + end + + context 'when a moderator has not recently signed in' do + let(:moderator_activity_date) { 1.year.ago } + + it 'changes registrations mode from open to approved' do + expect { subject.perform }.to change(Setting, :registrations_mode).from('open').to('approved') + end + end + end + + context 'when registrations are closed' do + before do + Setting.registrations_mode = 'none' + end + + context 'when a moderator has logged in recently' do + let(:moderator_activity_date) { Time.now.utc } + + it 'does not change registrations mode' do + expect { subject.perform }.to_not change(Setting, :registrations_mode) + end + end + + context 'when a moderator has not recently signed in' do + let(:moderator_activity_date) { 1.year.ago } + + it 'does not change registrations mode' do + expect { subject.perform }.to_not change(Setting, :registrations_mode) + end + end + end + end +end From 4fd22acb4ad625b263794afa6d4383e73ce9e5b1 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 22 Feb 2024 18:15:38 +0100 Subject: [PATCH 069/117] Fix auto-close email being sent to users with devops permissions instead of settings permissions (#29356) --- app/workers/scheduler/auto_close_registrations_scheduler.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/workers/scheduler/auto_close_registrations_scheduler.rb b/app/workers/scheduler/auto_close_registrations_scheduler.rb index 17516dd23..687450291 100644 --- a/app/workers/scheduler/auto_close_registrations_scheduler.rb +++ b/app/workers/scheduler/auto_close_registrations_scheduler.rb @@ -26,7 +26,7 @@ class Scheduler::AutoCloseRegistrationsScheduler def switch_to_approval_mode! Setting.registrations_mode = 'approved' - User.those_who_can(:view_devops).includes(:account).find_each do |user| + User.those_who_can(:manage_settings).includes(:account).find_each do |user| AdminMailer.with(recipient: user.account).auto_close_registrations.deliver_later end end From 328a9b81574baf1644df9b0792f716c64be12a5c Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 22 Feb 2024 18:15:59 +0100 Subject: [PATCH 070/117] Change registrations to be disabled by default for new servers (#29353) --- app/javascript/packs/admin.jsx | 4 ++++ .../settings/registrations/show.html.haml | 4 +++- config/locales/en.yml | 2 ++ config/settings.yml | 2 +- spec/models/setting_spec.rb | 20 +++++-------------- spec/rails_helper.rb | 7 +++++++ spec/spec_helper.rb | 6 ++++++ 7 files changed, 28 insertions(+), 17 deletions(-) diff --git a/app/javascript/packs/admin.jsx b/app/javascript/packs/admin.jsx index ebcc6903f..524b6a5c4 100644 --- a/app/javascript/packs/admin.jsx +++ b/app/javascript/packs/admin.jsx @@ -145,6 +145,10 @@ delegate(document, '#form_admin_settings_enable_bootstrap_timeline_accounts', 'c const onChangeRegistrationMode = (target) => { const enabled = target.value === 'approved'; + [].forEach.call(document.querySelectorAll('.form_admin_settings_registrations_mode .warning-hint'), (warning_hint) => { + warning_hint.style.display = target.value === 'open' ? 'inline' : 'none'; + }); + [].forEach.call(document.querySelectorAll('#form_admin_settings_require_invite_text'), (input) => { input.disabled = !enabled; if (enabled) { diff --git a/app/views/admin/settings/registrations/show.html.haml b/app/views/admin/settings/registrations/show.html.haml index 168f10975..4ece27bf4 100644 --- a/app/views/admin/settings/registrations/show.html.haml +++ b/app/views/admin/settings/registrations/show.html.haml @@ -10,9 +10,11 @@ %p.lead= t('admin.settings.registrations.preamble') + .flash-message= t('admin.settings.registrations.moderation_recommandation') + .fields-row .fields-row__column.fields-row__column-6.fields-group - = f.input :registrations_mode, collection: %w(open approved none), wrapper: :with_label, include_blank: false, label_method: ->(mode) { I18n.t("admin.settings.registrations_mode.modes.#{mode}") } + = f.input :registrations_mode, collection: %w(open approved none), wrapper: :with_label, include_blank: false, label_method: ->(mode) { I18n.t("admin.settings.registrations_mode.modes.#{mode}") }, warning_hint: I18n.t('admin.settings.registrations_mode.warning_hint') .fields-row__column.fields-row__column-6.fields-group = f.input :require_invite_text, as: :boolean, wrapper: :with_label, disabled: !approved_registrations? diff --git a/config/locales/en.yml b/config/locales/en.yml index 85a0a9ff9..7db5e1bad 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -764,6 +764,7 @@ en: disabled: To no one users: To logged-in local users registrations: + moderation_recommandation: Please make sure you have an adequate and reactive moderation team before you open registrations to everyone! preamble: Control who can create an account on your server. title: Registrations registrations_mode: @@ -771,6 +772,7 @@ en: approved: Approval required for sign up none: Nobody can sign up open: Anyone can sign up + warning_hint: We recommend using “Approval required for sign up” unless you are confident your moderation team can handle spam and malicious registrations in a timely fashion. security: authorized_fetch: Require authentication from federated servers authorized_fetch_hint: Requiring authentication from federated servers enables stricter enforcement of both user-level and server-level blocks. However, this comes at the cost of a performance penalty, reduces the reach of your replies, and may introduce compatibility issues with some federated services. In addition, this will not prevent dedicated actors from fetching your public posts and accounts. diff --git a/config/settings.yml b/config/settings.yml index 67297c26c..208c8e376 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -9,7 +9,7 @@ defaults: &defaults site_terms: '' site_contact_username: '' site_contact_email: '' - registrations_mode: 'open' + registrations_mode: 'none' profile_directory: true closed_registrations_message: '' timeline_preview: true diff --git a/spec/models/setting_spec.rb b/spec/models/setting_spec.rb index 5ed5c5d76..97e548e09 100644 --- a/spec/models/setting_spec.rb +++ b/spec/models/setting_spec.rb @@ -142,22 +142,12 @@ RSpec.describe Setting do context 'when records includes nothing' do let(:records) { [] } - context 'when default_value is not a Hash' do - it 'includes Setting with value of default_value' do - setting = described_class.all_as_records[key] + it 'includes Setting with value of default_value' do + setting = described_class.all_as_records[key] - expect(setting).to be_a described_class - expect(setting).to have_attributes(var: key) - expect(setting).to have_attributes(value: 'default_value') - end - end - - context 'when default_value is a Hash' do - let(:default_value) { { 'foo' => 'fuga' } } - - it 'returns {}' do - expect(described_class.all_as_records).to eq({}) - end + expect(setting).to be_a described_class + expect(setting).to have_attributes(var: key) + expect(setting).to have_attributes(value: default_value) end end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 7b8dccb6a..17067c58f 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -152,6 +152,13 @@ RSpec.configure do |config| self.use_transactional_tests = false DatabaseCleaner.cleaning do + # NOTE: we switched registrations mode to closed by default, but the specs + # very heavily rely on having it enabled by default, as it relies on users + # being approved by default except in select cases where explicitly testing + # other registration modes + # Also needs to be set per-example here because of the database cleaner. + Setting.registrations_mode = 'open' + example.run end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b4c20545f..030bc81fb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -31,6 +31,12 @@ RSpec.configure do |config| config.before :suite do Rails.application.load_seed Chewy.strategy(:bypass) + + # NOTE: we switched registrations mode to closed by default, but the specs + # very heavily rely on having it enabled by default, as it relies on users + # being approved by default except in select cases where explicitly testing + # other registration modes + Setting.registrations_mode = 'open' end config.after :suite do From 9a7802655f652524be58fe73efa0e6077ff65ea2 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 22 Feb 2024 19:12:53 +0100 Subject: [PATCH 071/117] Fix link verifications when page size exceeds 1MB (#29361) --- app/services/verify_link_service.rb | 2 +- spec/services/verify_link_service_spec.rb | 27 ++++++++++++----------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/app/services/verify_link_service.rb b/app/services/verify_link_service.rb index 707aeb4e0..b317fc31a 100644 --- a/app/services/verify_link_service.rb +++ b/app/services/verify_link_service.rb @@ -19,7 +19,7 @@ class VerifyLinkService < BaseService def perform_request! @body = Request.new(:get, @url).add_headers('Accept' => 'text/html').perform do |res| - res.code == 200 ? res.body_with_limit : nil + res.code == 200 ? res.truncated_body : nil end end diff --git a/spec/services/verify_link_service_spec.rb b/spec/services/verify_link_service_spec.rb index 415788cb5..d06344f9c 100644 --- a/spec/services/verify_link_service_spec.rb +++ b/spec/services/verify_link_service_spec.rb @@ -76,6 +76,20 @@ RSpec.describe VerifyLinkService, type: :service do end context 'when a document is truncated but the link back is valid' do + let(:html) do + " + + + + " + end + + it 'marks the field as verified' do + expect(field.verified?).to be true + end + end + + context 'when a link tag might be truncated' do let(:html) do " @@ -89,19 +103,6 @@ RSpec.describe VerifyLinkService, type: :service do end end - context 'when a link back might be truncated' do - let(:html) do - " - - - Date: Fri, 23 Feb 2024 09:53:04 +0100 Subject: [PATCH 072/117] Fix processing of `Link` objects in `Image` objects (#29363) --- .../activitypub/process_account_service.rb | 11 ++-- .../process_account_service_spec.rb | 51 +++++++++++++++---- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index dc845ce97..5edf52325 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -201,10 +201,15 @@ class ActivityPub::ProcessAccountService < BaseService value = first_of_value(@json[key]) return if value.nil? - return value['url'] if value.is_a?(Hash) - image = fetch_resource_without_id_validation(value) - image['url'] if image + if value.is_a?(String) + value = fetch_resource_without_id_validation(value) + return if value.nil? + end + + value = first_of_value(value['url']) if value.is_a?(Hash) && value['type'] == 'Image' + value = value['href'] if value.is_a?(Hash) + value if value.is_a?(String) end def public_key diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb index c02a0800a..60214d2ed 100644 --- a/spec/services/activitypub/process_account_service_spec.rb +++ b/spec/services/activitypub/process_account_service_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' RSpec.describe ActivityPub::ProcessAccountService, type: :service do subject { described_class.new } - context 'with property values' do + context 'with property values, an avatar, and a profile header' do let(:payload) do { id: 'https://foo.test', @@ -16,19 +16,50 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do { type: 'PropertyValue', name: 'Occupation', value: 'Unit test' }, { type: 'PropertyValue', name: 'non-string', value: %w(foo bar) }, ], + image: { + type: 'Image', + mediaType: 'image/png', + url: 'https://foo.test/image.png', + }, + icon: { + type: 'Image', + url: [ + { + mediaType: 'image/png', + href: 'https://foo.test/icon.png', + }, + ], + }, }.with_indifferent_access end - it 'parses out of attachment' do + before do + stub_request(:get, 'https://foo.test/image.png').to_return(request_fixture('avatar.txt')) + stub_request(:get, 'https://foo.test/icon.png').to_return(request_fixture('avatar.txt')) + end + + it 'parses property values, avatar and profile header as expected' do account = subject.call('alice', 'example.com', payload) - expect(account.fields).to be_a Array - expect(account.fields.size).to eq 2 - expect(account.fields[0]).to be_a Account::Field - expect(account.fields[0].name).to eq 'Pronouns' - expect(account.fields[0].value).to eq 'They/them' - expect(account.fields[1]).to be_a Account::Field - expect(account.fields[1].name).to eq 'Occupation' - expect(account.fields[1].value).to eq 'Unit test' + + expect(account.fields) + .to be_an(Array) + .and have_attributes(size: 2) + expect(account.fields.first) + .to be_an(Account::Field) + .and have_attributes( + name: eq('Pronouns'), + value: eq('They/them') + ) + expect(account.fields.last) + .to be_an(Account::Field) + .and have_attributes( + name: eq('Occupation'), + value: eq('Unit test') + ) + expect(account).to have_attributes( + avatar_remote_url: 'https://foo.test/icon.png', + header_remote_url: 'https://foo.test/image.png' + ) end end From bdb6650ebc73566a09482521a43b457270b3c44a Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 23 Feb 2024 14:09:41 +0100 Subject: [PATCH 073/117] Bump version to v4.2.8 (#29370) --- CHANGELOG.md | 20 ++++++++++++++++++++ docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 2 +- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a932250b..636b3c161 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ All notable changes to this project will be documented in this file. +## [4.2.8] - 2024-02-23 + +### Added + +- Add hourly task to automatically require approval for new registrations in the absence of moderators ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29318), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/29355)) + In order to prevent future abandoned Mastodon servers from being used for spam, harassment and other malicious activity, Mastodon will now automatically switch new user registrations to require moderator approval whenever they are left open and no activity (including non-moderation actions from apps) from any logged-in user with permission to access moderation reports has been detected in a full week. + When this happens, users with the permission to change server settings will receive an email notification. + This feature is disabled when `EMAIL_DOMAIN_ALLOWLIST` is used, and can also be disabled with `DISABLE_AUTOMATIC_SWITCHING_TO_APPROVED_REGISTRATIONS=true`. + +### Changed + +- Change registrations to be closed by default on new installations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29280)) + If you are running a server and never changed your registrations mode from the default, updating will automatically close your registrations. + Simply re-enable them through the administration interface or using `tootctl settings registrations open` if you want to enable them again. + +### Fixed + +- Fix processing of remote ActivityPub actors making use of `Link` objects as `Image` `url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29335)) +- Fix link verifications when page size exceeds 1MB ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29358)) + ## [4.2.7] - 2024-02-16 ### Fixed diff --git a/docker-compose.yml b/docker-compose.yml index 4563e2317..004f5dbb0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,7 @@ services: web: build: . - image: ghcr.io/mastodon/mastodon:v4.2.7 + image: ghcr.io/mastodon/mastodon:v4.2.8 restart: always env_file: .env.production command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" @@ -77,7 +77,7 @@ services: streaming: build: . - image: ghcr.io/mastodon/mastodon:v4.2.7 + image: ghcr.io/mastodon/mastodon:v4.2.8 restart: always env_file: .env.production command: node ./streaming @@ -95,7 +95,7 @@ services: sidekiq: build: . - image: ghcr.io/mastodon/mastodon:v4.2.7 + image: ghcr.io/mastodon/mastodon:v4.2.8 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 486dea920..b28349dce 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 7 + 8 end def default_prerelease From ed8e4bab4c8232533bd40d36c2ea5524e078f0af Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 14 Dec 2023 09:02:33 -0500 Subject: [PATCH 074/117] Fix reference to non-existent var in CLI maintenance command (#28363) --- lib/mastodon/cli/maintenance.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mastodon/cli/maintenance.rb b/lib/mastodon/cli/maintenance.rb index e73bcbf86..c2a6802e1 100644 --- a/lib/mastodon/cli/maintenance.rb +++ b/lib/mastodon/cli/maintenance.rb @@ -224,7 +224,7 @@ module Mastodon::CLI users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse ref_user = users.shift say "Multiple users registered with e-mail address #{ref_user.email}.", :yellow - say "e-mail will be disabled for the following accounts: #{user.map(&:account).map(&:acct).join(', ')}", :yellow + say "e-mail will be disabled for the following accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow say 'Please reach out to them and set another address with `tootctl account modify` or delete them.', :yellow users.each_with_index do |user, index| From 6536d96d1b211cb4b462010474cb63b241de9998 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 4 Jan 2024 15:14:46 +0100 Subject: [PATCH 075/117] Add fallback redirection when getting a webfinger query `WEB_DOMAIN@WEB_DOMAIN` (#28592) --- app/controllers/well_known/webfinger_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb index 4748940f7..6cf37c2ff 100644 --- a/app/controllers/well_known/webfinger_controller.rb +++ b/app/controllers/well_known/webfinger_controller.rb @@ -21,7 +21,7 @@ module WellKnown username = username_from_resource @account = begin - if username == Rails.configuration.x.local_domain + if username == Rails.configuration.x.local_domain || username == Rails.configuration.x.web_domain Account.representative else Account.find_local!(username) From f784213c6405cbca2f3ff70479aff2e263b28730 Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Tue, 6 Feb 2024 13:38:14 +0100 Subject: [PATCH 076/117] Return domain block digests from admin domain blocks API (#29092) --- app/serializers/rest/admin/domain_block_serializer.rb | 6 +++++- spec/requests/api/v1/admin/domain_blocks_spec.rb | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/serializers/rest/admin/domain_block_serializer.rb b/app/serializers/rest/admin/domain_block_serializer.rb index b955d008a..e94a337cb 100644 --- a/app/serializers/rest/admin/domain_block_serializer.rb +++ b/app/serializers/rest/admin/domain_block_serializer.rb @@ -1,11 +1,15 @@ # frozen_string_literal: true class REST::Admin::DomainBlockSerializer < ActiveModel::Serializer - attributes :id, :domain, :created_at, :severity, + attributes :id, :domain, :digest, :created_at, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate def id object.id.to_s end + + def digest + object.domain_digest + end end diff --git a/spec/requests/api/v1/admin/domain_blocks_spec.rb b/spec/requests/api/v1/admin/domain_blocks_spec.rb index 7a5ac28c5..a6cfdb48d 100644 --- a/spec/requests/api/v1/admin/domain_blocks_spec.rb +++ b/spec/requests/api/v1/admin/domain_blocks_spec.rb @@ -49,6 +49,7 @@ RSpec.describe 'Domain Blocks' do { id: domain_block.id.to_s, domain: domain_block.domain, + digest: domain_block.domain_digest, created_at: domain_block.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), severity: domain_block.severity.to_s, reject_media: domain_block.reject_media, @@ -102,6 +103,7 @@ RSpec.describe 'Domain Blocks' do { id: domain_block.id.to_s, domain: domain_block.domain, + digest: domain_block.domain_digest, created_at: domain_block.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), severity: domain_block.severity.to_s, reject_media: domain_block.reject_media, @@ -212,6 +214,7 @@ RSpec.describe 'Domain Blocks' do { id: domain_block.id.to_s, domain: domain_block.domain, + digest: domain_block.domain_digest, severity: 'suspend', } ) From 7af69f5cf575b1124a78c0c8e5b24ebce8060621 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 23 Feb 2024 20:04:57 +0100 Subject: [PATCH 077/117] Fix admin account created by `mastodon:setup` not being auto-approved (#29379) --- lib/tasks/mastodon.rake | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index f68d1cf1f..8e024077e 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -515,6 +515,7 @@ namespace :mastodon do owner_role = UserRole.find_by(name: 'Owner') user = User.new(email: email, password: password, confirmed_at: Time.now.utc, account_attributes: { username: username }, bypass_invite_request_check: true, role: owner_role) user.save(validate: false) + user.approve! Setting.site_contact_username = username From ab3f9852f23a2180ae77b3a0963fe61e97ab129f Mon Sep 17 00:00:00 2001 From: Jeong Arm Date: Mon, 11 Mar 2024 18:28:08 +0900 Subject: [PATCH 078/117] Normalize idna domain before account unblock domain (#29530) --- app/models/concerns/account_interactions.rb | 6 ++- .../concerns/account_interactions_spec.rb | 40 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 2de15031f..74ba1695f 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -185,7 +185,7 @@ module AccountInteractions end def unblock_domain!(other_domain) - block = domain_blocks.find_by(domain: other_domain) + block = domain_blocks.find_by(domain: normalized_domain(other_domain)) block&.destroy end @@ -313,4 +313,8 @@ module AccountInteractions def remove_potential_friendship(other_account) PotentialFriendshipTracker.remove(id, other_account.id) end + + def normalized_domain(domain) + TagManager.instance.normalize_domain(domain) + end end diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb index 84e2c91a8..64d1d46d3 100644 --- a/spec/models/concerns/account_interactions_spec.rb +++ b/spec/models/concerns/account_interactions_spec.rb @@ -250,6 +250,24 @@ describe AccountInteractions do end end + describe '#block_idna_domain!' do + subject do + [ + account.block_domain!(idna_domain), + account.block_domain!(punycode_domain), + ] + end + + let(:idna_domain) { '대한민국.한국' } + let(:punycode_domain) { 'xn--3e0bs9hfvinn1a.xn--3e0b707e' } + + it 'creates single AccountDomainBlock' do + expect do + expect(subject).to all(be_a AccountDomainBlock) + end.to change { account.domain_blocks.count }.by 1 + end + end + describe '#unfollow!' do subject { account.unfollow!(target_account) } @@ -345,6 +363,28 @@ describe AccountInteractions do end end + describe '#unblock_idna_domain!' do + subject { account.unblock_domain!(punycode_domain) } + + let(:idna_domain) { '대한민국.한국' } + let(:punycode_domain) { 'xn--3e0bs9hfvinn1a.xn--3e0b707e' } + + context 'when blocking the domain' do + it 'returns destroyed AccountDomainBlock' do + account_domain_block = Fabricate(:account_domain_block, domain: idna_domain) + account.domain_blocks << account_domain_block + expect(subject).to be_a AccountDomainBlock + expect(subject).to be_destroyed + end + end + + context 'when unblocking idna domain' do + it 'returns nil' do + expect(subject).to be_nil + end + end + end + describe '#following?' do subject { account.following?(target_account) } From 0143c9d3e1d7ae11b14aff5a0326885cbbd3ed19 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 22 Mar 2024 11:08:27 -0400 Subject: [PATCH 079/117] Fix results/query in `api/v1/featured_tags/suggestions` (#29597) --- .../featured_tags/suggestions_controller.rb | 6 +++- .../suggestions_controller_spec.rb | 28 +++++++++++++++++-- spec/fabricators/featured_tag_fabricator.rb | 2 +- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/v1/featured_tags/suggestions_controller.rb b/app/controllers/api/v1/featured_tags/suggestions_controller.rb index 76633210a..4f732ed2d 100644 --- a/app/controllers/api/v1/featured_tags/suggestions_controller.rb +++ b/app/controllers/api/v1/featured_tags/suggestions_controller.rb @@ -12,6 +12,10 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController private def set_recently_used_tags - @recently_used_tags = Tag.recently_used(current_account).where.not(id: current_account.featured_tags).limit(10) + @recently_used_tags = Tag.recently_used(current_account).where.not(id: featured_tag_ids).limit(10) + end + + def featured_tag_ids + current_account.featured_tags.pluck(:tag_id) end end diff --git a/spec/controllers/api/v1/featured_tags/suggestions_controller_spec.rb b/spec/controllers/api/v1/featured_tags/suggestions_controller_spec.rb index 54c63dcc6..8cb928ea2 100644 --- a/spec/controllers/api/v1/featured_tags/suggestions_controller_spec.rb +++ b/spec/controllers/api/v1/featured_tags/suggestions_controller_spec.rb @@ -7,17 +7,39 @@ describe Api::V1::FeaturedTags::SuggestionsController do let(:user) { Fabricate(:user) } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') } - let(:account) { Fabricate(:account) } + let(:account) { Fabricate(:account, user: user) } before do allow(controller).to receive(:doorkeeper_token) { token } end describe 'GET #index' do - it 'returns http success' do + let!(:unused_featured_tag) { Fabricate(:tag, name: 'unused_featured_tag') } + let!(:used_tag) { Fabricate(:tag, name: 'used_tag') } + let!(:used_featured_tag) { Fabricate(:tag, name: 'used_featured_tag') } + + before do + _unused_tag = Fabricate(:tag, name: 'unused_tag') + + # Make relevant tags used by account + status = Fabricate(:status, account: account) + status.tags << used_tag + status.tags << used_featured_tag + + # Feature the relevant tags + Fabricate :featured_tag, account: account, name: unused_featured_tag.name + Fabricate :featured_tag, account: account, name: used_featured_tag.name + end + + it 'returns http success and recently used but not featured tags', :aggregate_failures do get :index, params: { account_id: account.id, limit: 2 } - expect(response).to have_http_status(200) + expect(response) + .to have_http_status(200) + expect(body_as_json) + .to contain_exactly( + include(name: used_tag.name) + ) end end end diff --git a/spec/fabricators/featured_tag_fabricator.rb b/spec/fabricators/featured_tag_fabricator.rb index 0803dc43a..6003099db 100644 --- a/spec/fabricators/featured_tag_fabricator.rb +++ b/spec/fabricators/featured_tag_fabricator.rb @@ -2,6 +2,6 @@ Fabricator(:featured_tag) do account { Fabricate.build(:account) } - tag { Fabricate.build(:tag) } + tag { nil } name { sequence(:name) { |i| "Tag#{i}" } } end From 86807e4799d21863084d52d4070f3da7c120e260 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 5 Apr 2024 09:48:45 +0200 Subject: [PATCH 080/117] Improve email address validation (#29838) --- Gemfile | 2 ++ Gemfile.lock | 1 + app/models/user.rb | 2 ++ app/validators/email_address_validator.rb | 18 ++++++++++++++++++ spec/models/user_spec.rb | 6 ++++++ 5 files changed, 29 insertions(+) create mode 100644 app/validators/email_address_validator.rb diff --git a/Gemfile b/Gemfile index 829e7d857..1d038ad9f 100644 --- a/Gemfile +++ b/Gemfile @@ -204,3 +204,5 @@ gem 'net-http', '~> 0.3.2' gem 'rubyzip', '~> 2.3' gem 'hcaptcha', '~> 7.1' + +gem 'mail', '~> 2.8' diff --git a/Gemfile.lock b/Gemfile.lock index 9290a84ae..94d2d0ba4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -871,6 +871,7 @@ DEPENDENCIES letter_opener_web (~> 2.0) link_header (~> 0.0) lograge (~> 0.12) + mail (~> 2.8) mario-redis-lock (~> 1.2) md-paperclip-azure (~> 2.2) memory_profiler diff --git a/app/models/user.rb b/app/models/user.rb index ee9097927..0c8d481c4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -96,6 +96,8 @@ class User < ApplicationRecord accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? && !Setting.require_invite_text } validates :invite_request, presence: true, on: :create, if: :invite_text_required? + validates :email, presence: true, email_address: true + validates_with BlacklistedEmailValidator, if: -> { ENV['EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION'] == 'true' || !confirmed? } validates_with EmailMxValidator, if: :validate_email_dns? validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create diff --git a/app/validators/email_address_validator.rb b/app/validators/email_address_validator.rb new file mode 100644 index 000000000..ed0bb1165 --- /dev/null +++ b/app/validators/email_address_validator.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# NOTE: I initially wrote this as `EmailValidator` but it ended up clashing +# with an indirect dependency of ours, `validate_email`, which, turns out, +# has the same approach as we do, but with an extra check disallowing +# single-label domains. Decided to not switch to `validate_email` because +# we do want to allow at least `localhost`. + +class EmailAddressValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + value = value.strip + + address = Mail::Address.new(value) + record.errors.add(attribute, :invalid) if address.address != value + rescue Mail::Field::FieldError + record.errors.add(attribute, :invalid) + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ffd1889cb..f06150f02 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -39,6 +39,12 @@ RSpec.describe User do expect(user.valid?).to be true end + it 'is valid with a localhost e-mail address' do + user = Fabricate.build(:user, email: 'admin@localhost') + user.valid? + expect(user.valid?).to be true + end + it 'cleans out invalid locale' do user = Fabricate.build(:user, locale: 'toto') expect(user.valid?).to be true From c3be5a3d2eff947a04a32d78726ff511d7dec98f Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 8 Apr 2024 15:46:13 +0200 Subject: [PATCH 081/117] Remove caching in `cache_collection` (#29862) --- app/controllers/concerns/cache_concern.rb | 29 ++++----------- app/models/concerns/cacheable.rb | 4 +++ app/models/feed.rb | 2 +- app/models/public_feed.rb | 2 +- app/models/status.rb | 32 ----------------- app/models/tag_feed.rb | 2 +- .../application_controller_spec.rb | 35 ------------------- spec/models/home_feed_spec.rb | 1 - 8 files changed, 14 insertions(+), 93 deletions(-) diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index 55ebe1bd6..c8ed9f9a5 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -198,34 +198,19 @@ module CacheConcern end end + # TODO: Rename this method, as it does not perform any caching anymore. def cache_collection(raw, klass) - return raw unless klass.respond_to?(:with_includes) + return raw unless klass.respond_to?(:preload_cacheable_associations) - raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) - return [] if raw.empty? + records = raw.to_a - cached_keys_with_value = begin - Rails.cache.read_multi(*raw).transform_keys(&:id).transform_values { |r| ActiveRecordCoder.load(r) } - rescue ActiveRecordCoder::Error - {} # The serialization format may have changed, let's pretend it's a cache miss. - end + klass.preload_cacheable_associations(records) - uncached_ids = raw.map(&:id) - cached_keys_with_value.keys - - klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!) - - unless uncached_ids.empty? - uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id) - - uncached.each_value do |item| - Rails.cache.write(item, ActiveRecordCoder.dump(item)) - end - end - - raw.filter_map { |item| cached_keys_with_value[item.id] || uncached[item.id] } + records end + # TODO: Rename this method, as it does not perform any caching anymore. def cache_collection_paginated_by_id(raw, klass, limit, options) - cache_collection raw.cache_ids.to_a_paginated_by_id(limit, options), klass + cache_collection raw.to_a_paginated_by_id(limit, options), klass end end diff --git a/app/models/concerns/cacheable.rb b/app/models/concerns/cacheable.rb index d7524cdfd..0633f20c7 100644 --- a/app/models/concerns/cacheable.rb +++ b/app/models/concerns/cacheable.rb @@ -14,6 +14,10 @@ module Cacheable includes(@cache_associated) end + def preload_cacheable_associations(records) + ActiveRecord::Associations::Preloader.new(records: records, associations: @cache_associated).call + end + def cache_ids select(:id, :updated_at) end diff --git a/app/models/feed.rb b/app/models/feed.rb index f51dcfab1..30073fed4 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -28,7 +28,7 @@ class Feed unhydrated = redis.zrangebyscore(key, "(#{min_id}", "(#{max_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i) end - Status.where(id: unhydrated).cache_ids + Status.where(id: unhydrated) end def key diff --git a/app/models/public_feed.rb b/app/models/public_feed.rb index c208d6f66..df59c9c22 100644 --- a/app/models/public_feed.rb +++ b/app/models/public_feed.rb @@ -29,7 +29,7 @@ class PublicFeed scope.merge!(media_only_scope) if media_only? scope.merge!(language_scope) if account&.chosen_languages.present? - scope.cache_ids.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id) + scope.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id) end private diff --git a/app/models/status.rb b/app/models/status.rb index 1c41ef1d5..990ac8061 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -338,38 +338,6 @@ class Status < ApplicationRecord StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |p, h| h[p.status_id] = true } end - def reload_stale_associations!(cached_items) - account_ids = [] - - cached_items.each do |item| - account_ids << item.account_id - account_ids << item.reblog.account_id if item.reblog? - end - - account_ids.uniq! - - status_ids = cached_items.map { |item| item.reblog? ? item.reblog_of_id : item.id }.uniq - - return if account_ids.empty? - - accounts = Account.where(id: account_ids).includes(:account_stat, :user).index_by(&:id) - - status_stats = StatusStat.where(status_id: status_ids).index_by(&:status_id) - - cached_items.each do |item| - item.account = accounts[item.account_id] - item.reblog.account = accounts[item.reblog.account_id] if item.reblog? - - if item.reblog? - status_stat = status_stats[item.reblog.id] - item.reblog.status_stat = status_stat if status_stat.present? - else - status_stat = status_stats[item.id] - item.status_stat = status_stat if status_stat.present? - end - end - end - def from_text(text) return [] if text.blank? diff --git a/app/models/tag_feed.rb b/app/models/tag_feed.rb index b8cd63557..6b5831d24 100644 --- a/app/models/tag_feed.rb +++ b/app/models/tag_feed.rb @@ -33,7 +33,7 @@ class TagFeed < PublicFeed scope.merge!(account_filters_scope) if account? scope.merge!(media_only_scope) if media_only? - scope.cache_ids.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id) + scope.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id) end private diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index e9d479603..5cd45e991 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -221,39 +221,4 @@ describe ApplicationController do include_examples 'respond_with_error', 422 end - - describe 'cache_collection' do - subject do - Class.new(ApplicationController) do - public :cache_collection - end - end - - shared_examples 'receives :with_includes' do |fabricator, klass| - it 'uses raw if it is not an ActiveRecord::Relation' do - record = Fabricate(fabricator) - expect(subject.new.cache_collection([record], klass)).to eq [record] - end - end - - shared_examples 'cacheable' do |fabricator, klass| - include_examples 'receives :with_includes', fabricator, klass - - it 'calls cache_ids of raw if it is an ActiveRecord::Relation' do - record = Fabricate(fabricator) - relation = klass.none - allow(relation).to receive(:cache_ids).and_return([record]) - expect(subject.new.cache_collection(relation, klass)).to eq [record] - end - end - - it 'returns raw unless class responds to :with_includes' do - raw = Object.new - expect(subject.new.cache_collection(raw, Object)).to eq raw - end - - context 'with a Status' do - include_examples 'cacheable', :status, Status - end - end end diff --git a/spec/models/home_feed_spec.rb b/spec/models/home_feed_spec.rb index bd649d826..06bb63b1a 100644 --- a/spec/models/home_feed_spec.rb +++ b/spec/models/home_feed_spec.rb @@ -27,7 +27,6 @@ RSpec.describe HomeFeed do results = subject.get(3) expect(results.map(&:id)).to eq [3, 2] - expect(results.first.attributes.keys).to eq %w(id updated_at) end end From e69780ec59bbd04a649abc67cc41a824a635fc7b Mon Sep 17 00:00:00 2001 From: Tim Rogers Date: Mon, 22 Apr 2024 04:00:24 -0500 Subject: [PATCH 082/117] Fixed crash when supplying FFMPEG_BINARY environment variable (#30022) --- app/lib/video_metadata_extractor.rb | 2 +- config/initializers/ffmpeg.rb | 5 +++-- lib/paperclip/image_extractor.rb | 2 +- lib/paperclip/transcoder.rb | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/lib/video_metadata_extractor.rb b/app/lib/video_metadata_extractor.rb index f27d34868..df5409375 100644 --- a/app/lib/video_metadata_extractor.rb +++ b/app/lib/video_metadata_extractor.rb @@ -22,7 +22,7 @@ class VideoMetadataExtractor private def ffmpeg_command_output - command = Terrapin::CommandLine.new('ffprobe', '-i :path -print_format :format -show_format -show_streams -show_error -loglevel :loglevel') + command = Terrapin::CommandLine.new(Rails.configuration.x.ffprobe_binary, '-i :path -print_format :format -show_format -show_streams -show_error -loglevel :loglevel') command.run(path: @path, format: 'json', loglevel: 'fatal') end diff --git a/config/initializers/ffmpeg.rb b/config/initializers/ffmpeg.rb index 30ea617fc..87f85eeec 100644 --- a/config/initializers/ffmpeg.rb +++ b/config/initializers/ffmpeg.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true -if ENV['FFMPEG_BINARY'].present? - FFMPEG.ffmpeg_binary = ENV['FFMPEG_BINARY'] +Rails.application.configure do + config.x.ffmpeg_binary = ENV['FFMPEG_BINARY'] || 'ffmpeg' + config.x.ffprobe_binary = ENV['FFPROBE_BINARY'] || 'ffprobe' end diff --git a/lib/paperclip/image_extractor.rb b/lib/paperclip/image_extractor.rb index 17fe4326f..8a565d046 100644 --- a/lib/paperclip/image_extractor.rb +++ b/lib/paperclip/image_extractor.rb @@ -35,7 +35,7 @@ module Paperclip dst.binmode begin - command = Terrapin::CommandLine.new('ffmpeg', '-i :source -loglevel :loglevel -y :destination', logger: Paperclip.logger) + command = Terrapin::CommandLine.new(Rails.configuration.x.ffmpeg_binary, '-i :source -loglevel :loglevel -y :destination', logger: Paperclip.logger) command.run(source: @file.path, destination: dst.path, loglevel: 'fatal') rescue Terrapin::ExitStatusError dst.close(true) diff --git a/lib/paperclip/transcoder.rb b/lib/paperclip/transcoder.rb index d2d946d3a..3efffa355 100644 --- a/lib/paperclip/transcoder.rb +++ b/lib/paperclip/transcoder.rb @@ -61,7 +61,7 @@ module Paperclip command_arguments, interpolations = prepare_command(destination) begin - command = Terrapin::CommandLine.new('ffmpeg', command_arguments.join(' '), logger: Paperclip.logger) + command = Terrapin::CommandLine.new(Rails.configuration.x.ffmpeg_binary, command_arguments.join(' '), logger: Paperclip.logger) command.run(interpolations) rescue Terrapin::ExitStatusError => e raise Paperclip::Error, "Error while transcoding #{@basename}: #{e}" From 51ef619140fc87c23352aced941e313e8aa1c699 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 26 Apr 2024 15:19:02 +0200 Subject: [PATCH 083/117] Fix Idempotency-Key ignored when scheduling a post (#30084) --- app/services/post_status_service.rb | 4 ++-- spec/services/post_status_service_spec.rb | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index ea27f374e..e4dd480f1 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -160,7 +160,7 @@ class PostStatusService < BaseService def idempotency_duplicate if scheduled? - @account.schedule_statuses.find(@idempotency_duplicate) + @account.scheduled_statuses.find(@idempotency_duplicate) else @account.statuses.find(@idempotency_duplicate) end @@ -214,7 +214,7 @@ class PostStatusService < BaseService end def scheduled_options - @options.tap do |options_hash| + @options.dup.tap do |options_hash| options_hash[:in_reply_to_id] = options_hash.delete(:thread)&.id options_hash[:application_id] = options_hash.delete(:application)&.id options_hash[:scheduled_at] = nil diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index 7d7679c88..a74e82626 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -54,6 +54,13 @@ RSpec.describe PostStatusService, type: :service do it 'does not change statuses count' do expect { subject.call(account, text: 'Hi future!', scheduled_at: future, thread: previous_status) }.to_not(change { [account.statuses_count, previous_status.replies_count] }) end + + it 'returns existing status when used twice with idempotency key' do + account = Fabricate(:account) + status1 = subject.call(account, text: 'test', idempotency: 'meepmeep', scheduled_at: future) + status2 = subject.call(account, text: 'test', idempotency: 'meepmeep', scheduled_at: future) + expect(status2.id).to eq status1.id + end end it 'creates response to the original status of boost' do From 56b7d1a7b6c3beaacbcea21a849894d0a575cb13 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 2 May 2024 22:56:21 +0200 Subject: [PATCH 084/117] Fix not being able to block a subdomain of an already-blocked domain through the API (#30119) --- .../api/v1/admin/domain_blocks_controller.rb | 9 +++- .../api/v1/admin/domain_blocks_spec.rb | 45 ++++++++++++++++--- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/app/controllers/api/v1/admin/domain_blocks_controller.rb b/app/controllers/api/v1/admin/domain_blocks_controller.rb index 2538c7c7c..8786431d9 100644 --- a/app/controllers/api/v1/admin/domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/domain_blocks_controller.rb @@ -29,10 +29,11 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController def create authorize :domain_block, :create? + @domain_block = DomainBlock.new(resource_params) existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil - return render json: existing_domain_block, serializer: REST::Admin::ExistingDomainBlockErrorSerializer, status: 422 if existing_domain_block.present? + return render json: existing_domain_block, serializer: REST::Admin::ExistingDomainBlockErrorSerializer, status: 422 if conflicts_with_existing_block?(@domain_block, existing_domain_block) - @domain_block = DomainBlock.create!(resource_params) + @domain_block.save! DomainBlockWorker.perform_async(@domain_block.id) log_action :create, @domain_block render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer @@ -55,6 +56,10 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController private + def conflicts_with_existing_block?(domain_block, existing_domain_block) + existing_domain_block.present? && (existing_domain_block.domain == TagManager.instance.normalize_domain(domain_block.domain) || !domain_block.stricter_than?(existing_domain_block)) + end + def set_domain_blocks @domain_blocks = filtered_domain_blocks.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) end diff --git a/spec/requests/api/v1/admin/domain_blocks_spec.rb b/spec/requests/api/v1/admin/domain_blocks_spec.rb index a6cfdb48d..58641c20d 100644 --- a/spec/requests/api/v1/admin/domain_blocks_spec.rb +++ b/spec/requests/api/v1/admin/domain_blocks_spec.rb @@ -135,14 +135,10 @@ RSpec.describe 'Domain Blocks' do it_behaves_like 'forbidden for wrong role', '' it_behaves_like 'forbidden for wrong role', 'Moderator' - it 'returns http success' do + it 'creates a domain block with the expected domain name and severity', :aggregate_failures do subject expect(response).to have_http_status(200) - end - - it 'returns expected domain name and severity' do - subject body = body_as_json @@ -160,7 +156,44 @@ RSpec.describe 'Domain Blocks' do expect(DomainBlock.find_by(domain: 'foo.bar.com')).to be_present end - context 'when a stricter domain block already exists' do + context 'when a looser domain block already exists on a higher level domain' do + let(:params) { { domain: 'foo.bar.com', severity: :suspend } } + + before do + Fabricate(:domain_block, domain: 'bar.com', severity: :silence) + end + + it 'creates a domain block with the expected domain name and severity', :aggregate_failures do + subject + + body = body_as_json + + expect(response).to have_http_status(200) + expect(body).to match a_hash_including( + { + domain: 'foo.bar.com', + severity: 'suspend', + } + ) + + expect(DomainBlock.find_by(domain: 'foo.bar.com')).to be_present + end + end + + context 'when a domain block already exists on the same domain' do + before do + Fabricate(:domain_block, domain: 'foo.bar.com', severity: :silence) + end + + it 'returns existing domain block in error', :aggregate_failures do + subject + + expect(response).to have_http_status(422) + expect(body_as_json[:existing_domain_block][:domain]).to eq('foo.bar.com') + end + end + + context 'when a stricter domain block already exists on a higher level domain' do before do Fabricate(:domain_block, domain: 'bar.com', severity: :suspend) end From 67b2e62331f8459a3daa8ab06c8d18af4738e526 Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Tue, 30 Apr 2024 10:48:02 +0200 Subject: [PATCH 085/117] Fix missing destory audit logs for Domain Allows (#30125) --- app/controllers/admin/domain_allows_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/admin/domain_allows_controller.rb b/app/controllers/admin/domain_allows_controller.rb index 31be1978b..b0f139e3a 100644 --- a/app/controllers/admin/domain_allows_controller.rb +++ b/app/controllers/admin/domain_allows_controller.rb @@ -25,6 +25,8 @@ class Admin::DomainAllowsController < Admin::BaseController def destroy authorize @domain_allow, :destroy? UnallowDomainService.new.call(@domain_allow) + log_action :destroy, @domain_allow + redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.destroyed_msg') end From 8cf78825a23184333a4d9ac775e81add5bd5f0da Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 15 May 2024 15:11:13 +0200 Subject: [PATCH 086/117] Fix off-by-one in `tootctl media` commands (#30306) --- lib/mastodon/cli/media.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/mastodon/cli/media.rb b/lib/mastodon/cli/media.rb index c90616177..5879c532e 100644 --- a/lib/mastodon/cli/media.rb +++ b/lib/mastodon/cli/media.rb @@ -134,7 +134,7 @@ module Mastodon::CLI model_name = path_segments.first.classify attachment_name = path_segments[1].singularize - record_id = path_segments[2..-2].join.to_i + record_id = path_segments[2...-2].join.to_i file_name = path_segments.last record = record_map.dig(model_name, record_id) attachment = record&.public_send(attachment_name) @@ -180,7 +180,7 @@ module Mastodon::CLI end model_name = path_segments.first.classify - record_id = path_segments[2..-2].join.to_i + record_id = path_segments[2...-2].join.to_i attachment_name = path_segments[1].singularize file_name = path_segments.last @@ -311,7 +311,7 @@ module Mastodon::CLI end model_name = path_segments.first.classify - record_id = path_segments[2..-2].join.to_i + record_id = path_segments[2...-2].join.to_i unless PRELOAD_MODEL_WHITELIST.include?(model_name) say("Cannot find corresponding model: #{model_name}", :red) @@ -361,7 +361,7 @@ module Mastodon::CLI next unless VALID_PATH_SEGMENTS_SIZE.include?(segments.size) model_name = segments.first.classify - record_id = segments[2..-2].join.to_i + record_id = segments[2...-2].join.to_i next unless PRELOAD_MODEL_WHITELIST.include?(model_name) From 8c72e80019ea01d50bd0a868441083688028615b Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 10 May 2024 15:59:50 +0200 Subject: [PATCH 087/117] Update dependency rack-cors to 2.0.2 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 94d2d0ba4..2deab967c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -537,7 +537,7 @@ GEM rack (2.2.8.1) rack-attack (6.7.0) rack (>= 1.0, < 4) - rack-cors (2.0.1) + rack-cors (2.0.2) rack (>= 2.0.0) rack-oauth2 (1.21.3) activesupport From 2865bfadafc1f4472e9012ff4f03329b5b19d321 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 10 May 2024 16:01:07 +0200 Subject: [PATCH 088/117] Update dependency json-jwt to 1.15.3.1 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2deab967c..d08f79ccc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -368,7 +368,7 @@ GEM jmespath (1.6.2) json (2.6.3) json-canonicalization (1.0.0) - json-jwt (1.15.3) + json-jwt (1.15.3.1) activesupport (>= 4.2) aes_key_wrap bindata From 997b021b69e4f8fe8750e27c28bc3ffd5fe6cee8 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 10 May 2024 16:03:48 +0200 Subject: [PATCH 089/117] Update dependency rotp to 6.3.0 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d08f79ccc..bf438bd34 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -606,7 +606,7 @@ GEM actionpack (>= 5.2) railties (>= 5.2) rexml (3.2.6) - rotp (6.2.2) + rotp (6.3.0) rouge (4.1.2) rpam2 (4.0.2) rqrcode (2.2.0) From 6fc07ff31f60a3c90c593c4c19ef9513f82a0124 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 10 May 2024 16:04:19 +0200 Subject: [PATCH 090/117] Update dependency fastimage to 2.3.1 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index bf438bd34..57783a522 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -288,7 +288,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fast_blank (1.0.1) - fastimage (2.2.7) + fastimage (2.3.1) ffi (1.15.5) ffi-compiler (1.0.1) ffi (>= 1.0.0) From a8dd32102fd5827c7a93df9ef6c2db21da43dde5 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 16 May 2024 10:11:21 +0200 Subject: [PATCH 091/117] Update dependency nokogiri to 1.16.5 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 57783a522..d9cb59c03 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -469,7 +469,7 @@ GEM net-protocol net-ssh (7.1.0) nio4r (2.7.0) - nokogiri (1.16.2) + nokogiri (1.16.5) mini_portile2 (~> 2.8.2) racc (~> 1.4) nsa (0.3.0) From 16213a678d391c18f283676648bbe235da3f6835 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 29 May 2024 11:09:54 +0200 Subject: [PATCH 092/117] Update dependency rexml to 3.2.8 --- Gemfile.lock | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d9cb59c03..891566e1a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -605,7 +605,8 @@ GEM responders (3.1.0) actionpack (>= 5.2) railties (>= 5.2) - rexml (3.2.6) + rexml (3.2.8) + strscan (>= 3.0.9) rotp (6.3.0) rouge (4.1.2) rpam2 (4.0.2) @@ -731,6 +732,7 @@ GEM redlock (~> 1.0) strong_migrations (0.8.0) activerecord (>= 5.2) + strscan (3.0.9) swd (1.3.0) activesupport (>= 3) attr_required (>= 0.0.5) From b8edc95e8ad4ac07db311bc931cb09348b44820a Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 29 May 2024 10:15:06 +0200 Subject: [PATCH 093/117] Fix leaking Elasticsearch connections in Sidekiq processes (#30450) --- lib/mastodon/sidekiq_middleware.rb | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/mastodon/sidekiq_middleware.rb b/lib/mastodon/sidekiq_middleware.rb index 3a747afb6..ac63d4d38 100644 --- a/lib/mastodon/sidekiq_middleware.rb +++ b/lib/mastodon/sidekiq_middleware.rb @@ -8,6 +8,7 @@ class Mastodon::SidekiqMiddleware rescue Mastodon::HostValidationError # Do not retry rescue => e + clean_up_elasticsearch_connections! limit_backtrace_and_raise(e) ensure clean_up_sockets! @@ -25,6 +26,32 @@ class Mastodon::SidekiqMiddleware clean_up_statsd_socket! end + # This is a hack to immediately free up unused Elasticsearch connections. + # + # Indeed, Chewy creates one `Elasticsearch::Client` instance per thread, + # and each such client manages its long-lasting connection to + # Elasticsearch. + # + # As far as I know, neither `chewy`, `elasticsearch-transport` or even + # `faraday` provide a reliable way to immediately close a connection, and + # rely on the underlying object to be garbage-collected instead. + # + # Furthermore, `sidekiq` creates a new thread each time a job throws an + # exception, meaning that each failure will create a new connection, and + # the old one will only be closed on full garbage collection. + def clean_up_elasticsearch_connections! + return unless Chewy.enabled? && Chewy.current[:chewy_client].present? + + Chewy.client.transport.connections.each do |connection| + # NOTE: This bit of code is tailored for the HTTPClient Faraday adapter + connection.connection.app.instance_variable_get(:@client)&.reset_all + end + + Chewy.current.delete(:chewy_client) + rescue + nil + end + def clean_up_redis_socket! RedisConfiguration.pool.checkin if Thread.current[:redis] Thread.current[:redis] = nil From f9c41ae43b24b3561f122dcb1e07d4cbd25b1806 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 23 May 2024 19:28:18 +0200 Subject: [PATCH 094/117] Normalize language code of incoming posts (#30403) --- app/lib/activitypub/parser/status_parser.rb | 11 +++- .../activitypub/parser/status_parser_spec.rb | 50 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 spec/lib/activitypub/parser/status_parser_spec.rb diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb index 45f5fc5bf..6699dc21b 100644 --- a/app/lib/activitypub/parser/status_parser.rb +++ b/app/lib/activitypub/parser/status_parser.rb @@ -3,6 +3,8 @@ class ActivityPub::Parser::StatusParser include JsonLdHelper + NORMALIZED_LOCALE_NAMES = LanguagesHelper::SUPPORTED_LOCALES.keys.index_by(&:downcase).freeze + # @param [Hash] json # @param [Hash] magic_values # @option magic_values [String] :followers_collection @@ -86,6 +88,13 @@ class ActivityPub::Parser::StatusParser end def language + lang = raw_language_code + lang.presence && NORMALIZED_LOCALE_NAMES.fetch(lang.downcase.to_sym, lang) + end + + private + + def raw_language_code if content_language_map? @object['contentMap'].keys.first elsif name_language_map? @@ -95,8 +104,6 @@ class ActivityPub::Parser::StatusParser end end - private - def audience_to as_array(@object['to'] || @json['to']).map { |x| value_or_id(x) } end diff --git a/spec/lib/activitypub/parser/status_parser_spec.rb b/spec/lib/activitypub/parser/status_parser_spec.rb new file mode 100644 index 000000000..5d9f008db --- /dev/null +++ b/spec/lib/activitypub/parser/status_parser_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::Parser::StatusParser do + subject { described_class.new(json) } + + let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor') } + let(:follower) { Fabricate(:account, username: 'bob') } + + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join, + type: 'Create', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: object_json, + }.with_indifferent_access + end + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'), + type: 'Note', + to: [ + 'https://www.w3.org/ns/activitystreams#Public', + ActivityPub::TagManager.instance.uri_for(follower), + ], + content: '@bob lorem ipsum', + contentMap: { + EN: '@bob lorem ipsum', + }, + published: 1.hour.ago.utc.iso8601, + updated: 1.hour.ago.utc.iso8601, + tag: { + type: 'Mention', + href: ActivityPub::TagManager.instance.uri_for(follower), + }, + } + end + + it 'correctly parses status' do + expect(subject).to have_attributes( + text: '@bob lorem ipsum', + uri: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'), + reply: false, + language: :en + ) + end +end From 186f9161923f05cbc1999167caa9a2cf168749e9 Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Wed, 29 May 2024 16:00:05 +0200 Subject: [PATCH 095/117] Fix: remove broken OAuth Application vacuuming & throttle OAuth Application registrations (#30316) Co-authored-by: Claire --- app/lib/vacuum/applications_vacuum.rb | 10 ---- app/workers/scheduler/vacuum_scheduler.rb | 5 -- config/initializers/rack_attack.rb | 4 ++ spec/config/initializers/rack_attack_spec.rb | 18 ++++++++ spec/lib/vacuum/applications_vacuum_spec.rb | 48 -------------------- 5 files changed, 22 insertions(+), 63 deletions(-) delete mode 100644 app/lib/vacuum/applications_vacuum.rb delete mode 100644 spec/lib/vacuum/applications_vacuum_spec.rb diff --git a/app/lib/vacuum/applications_vacuum.rb b/app/lib/vacuum/applications_vacuum.rb deleted file mode 100644 index ba88655f1..000000000 --- a/app/lib/vacuum/applications_vacuum.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -class Vacuum::ApplicationsVacuum - def perform - Doorkeeper::Application.where(owner_id: nil) - .where.missing(:created_users, :access_tokens, :access_grants) - .where(created_at: ...1.day.ago) - .in_batches.delete_all - end -end diff --git a/app/workers/scheduler/vacuum_scheduler.rb b/app/workers/scheduler/vacuum_scheduler.rb index 1c9a2aabe..c22d6f5f8 100644 --- a/app/workers/scheduler/vacuum_scheduler.rb +++ b/app/workers/scheduler/vacuum_scheduler.rb @@ -22,7 +22,6 @@ class Scheduler::VacuumScheduler preview_cards_vacuum, backups_vacuum, access_tokens_vacuum, - applications_vacuum, feeds_vacuum, imports_vacuum, ] @@ -56,10 +55,6 @@ class Scheduler::VacuumScheduler Vacuum::ImportsVacuum.new end - def applications_vacuum - Vacuum::ApplicationsVacuum.new - end - def content_retention_policy ContentRetentionPolicy.current end diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index d0af0fe94..482f4b95a 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -105,6 +105,10 @@ class Rack::Attack req.authenticated_user_id if (req.post? && req.path.match?(API_DELETE_REBLOG_REGEX)) || (req.delete? && req.path.match?(API_DELETE_STATUS_REGEX)) end + throttle('throttle_oauth_application_registrations/ip', limit: 5, period: 10.minutes) do |req| + req.throttleable_remote_ip if req.post? && req.path == '/api/v1/apps' + end + throttle('throttle_sign_up_attempts/ip', limit: 25, period: 5.minutes) do |req| req.throttleable_remote_ip if req.post? && req.path_matches?('/auth') end diff --git a/spec/config/initializers/rack_attack_spec.rb b/spec/config/initializers/rack_attack_spec.rb index 7cd4ac76b..ddbd74705 100644 --- a/spec/config/initializers/rack_attack_spec.rb +++ b/spec/config/initializers/rack_attack_spec.rb @@ -103,4 +103,22 @@ describe Rack::Attack, type: :request do it_behaves_like 'throttled endpoint' end end + + describe 'throttle excessive oauth application registration requests by IP address' do + let(:throttle) { 'throttle_oauth_application_registrations/ip' } + let(:limit) { 5 } + let(:period) { 10.minutes } + let(:path) { '/api/v1/apps' } + let(:params) do + { + client_name: 'Throttle Test', + redirect_uris: 'urn:ietf:wg:oauth:2.0:oob', + scopes: 'read', + } + end + + let(:request) { -> { post path, params: params, headers: { 'REMOTE_ADDR' => remote_ip } } } + + it_behaves_like 'throttled endpoint' + end end diff --git a/spec/lib/vacuum/applications_vacuum_spec.rb b/spec/lib/vacuum/applications_vacuum_spec.rb deleted file mode 100644 index 57a222aaf..000000000 --- a/spec/lib/vacuum/applications_vacuum_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Vacuum::ApplicationsVacuum do - subject { described_class.new } - - describe '#perform' do - let!(:app_with_token) { Fabricate(:application, created_at: 1.month.ago) } - let!(:app_with_grant) { Fabricate(:application, created_at: 1.month.ago) } - let!(:app_with_signup) { Fabricate(:application, created_at: 1.month.ago) } - let!(:app_with_owner) { Fabricate(:application, created_at: 1.month.ago, owner: Fabricate(:user)) } - let!(:unused_app) { Fabricate(:application, created_at: 1.month.ago) } - let!(:recent_app) { Fabricate(:application, created_at: 1.hour.ago) } - - let!(:active_access_token) { Fabricate(:access_token, application: app_with_token) } - let!(:active_access_grant) { Fabricate(:access_grant, application: app_with_grant) } - let!(:user) { Fabricate(:user, created_by_application: app_with_signup) } - - before do - subject.perform - end - - it 'does not delete applications with valid access tokens' do - expect { app_with_token.reload }.to_not raise_error - end - - it 'does not delete applications with valid access grants' do - expect { app_with_grant.reload }.to_not raise_error - end - - it 'does not delete applications that were used to create users' do - expect { app_with_signup.reload }.to_not raise_error - end - - it 'does not delete owned applications' do - expect { app_with_owner.reload }.to_not raise_error - end - - it 'does not delete applications registered less than a day ago' do - expect { recent_app.reload }.to_not raise_error - end - - it 'deletes unused applications' do - expect { unused_app.reload }.to raise_error ActiveRecord::RecordNotFound - end - end -end From 943792c187392dc0a0f0b4e34dae5ddbc5c169b3 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 30 May 2024 14:03:13 +0200 Subject: [PATCH 096/117] Merge pull request from GHSA-5fq7-3p3j-9vrf --- app/services/notify_service.rb | 17 +++++++++-------- spec/services/notify_service_spec.rb | 8 ++++---- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 125883b15..759d6e393 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -71,16 +71,17 @@ class NotifyService < BaseService LEFT JOIN mentions m ON m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id WHERE s.id = :id UNION ALL - SELECT s.id, s.in_reply_to_id, m.id, st.path || s.id, st.depth + 1 - FROM ancestors st - JOIN statuses s ON s.id = st.in_reply_to_id - LEFT JOIN mentions m ON m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id - WHERE st.mention_id IS NULL AND NOT s.id = ANY(path) AND st.depth < :depth_limit + SELECT s.id, s.in_reply_to_id, m.id, ancestors.path || s.id, ancestors.depth + 1 + FROM ancestors + JOIN statuses s ON s.id = ancestors.in_reply_to_id + /* early exit if we already have a mention matching our requirements */ + LEFT JOIN mentions m ON m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id AND s.account_id = :recipient_id + WHERE ancestors.mention_id IS NULL AND NOT s.id = ANY(path) AND ancestors.depth < :depth_limit ) SELECT COUNT(*) - FROM ancestors st - JOIN statuses s ON s.id = st.id - WHERE st.mention_id IS NOT NULL AND s.visibility = 3 + FROM ancestors + JOIN statuses s ON s.id = ancestors.id + WHERE ancestors.mention_id IS NOT NULL AND s.account_id = :recipient_id AND s.visibility = 3 SQL end diff --git a/spec/services/notify_service_spec.rb b/spec/services/notify_service_spec.rb index 8fcb58658..c2664e79c 100644 --- a/spec/services/notify_service_spec.rb +++ b/spec/services/notify_service_spec.rb @@ -76,10 +76,10 @@ RSpec.describe NotifyService, type: :service do end context 'when the message chain is initiated by recipient, but without a mention to the sender, even if the sender sends multiple messages in a row' do - let(:reply_to) { Fabricate(:status, account: recipient) } - let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) } - let(:dummy_reply) { Fabricate(:status, account: sender, visibility: :direct, thread: reply_to) } - let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: dummy_reply)) } + let(:public_status) { Fabricate(:status, account: recipient) } + let(:intermediate_reply) { Fabricate(:status, account: sender, thread: public_status, visibility: :direct) } + let!(:intermediate_mention) { Fabricate(:mention, account: sender, status: intermediate_reply) } + let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: intermediate_reply)) } it 'does not notify' do expect { subject }.to_not change(Notification, :count) From 7920aa59e85bf21a4f6e2e9d485cd82dac89554c Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 30 May 2024 14:14:04 +0200 Subject: [PATCH 097/117] Merge pull request from GHSA-q3rg-xx5v-4mxh --- config/initializers/rack_attack.rb | 10 +++++++- spec/config/initializers/rack_attack_spec.rb | 24 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 482f4b95a..426b14b25 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -30,13 +30,17 @@ class Rack::Attack end def authenticated_user_id - authenticated_token&.resource_owner_id + authenticated_token&.resource_owner_id || warden_user_id end def authenticated_token_id authenticated_token&.id end + def warden_user_id + @env['warden']&.user&.id + end + def unauthenticated? !authenticated_user_id end @@ -141,6 +145,10 @@ class Rack::Attack req.session[:attempt_user_id] || req.params.dig('user', 'email').presence if req.post? && req.path_matches?('/auth/sign_in') end + throttle('throttle_password_change/account', limit: 10, period: 10.minutes) do |req| + req.authenticated_user_id if req.put? || (req.patch? && req.path_matches?('/auth')) + end + self.throttled_responder = lambda do |request| now = Time.now.utc match_data = request.env['rack.attack.match_data'] diff --git a/spec/config/initializers/rack_attack_spec.rb b/spec/config/initializers/rack_attack_spec.rb index ddbd74705..78c4bf03a 100644 --- a/spec/config/initializers/rack_attack_spec.rb +++ b/spec/config/initializers/rack_attack_spec.rb @@ -121,4 +121,28 @@ describe Rack::Attack, type: :request do it_behaves_like 'throttled endpoint' end + + describe 'throttle excessive password change requests by account' do + let(:user) { Fabricate(:user, email: 'user@host.example') } + let(:limit) { 10 } + let(:period) { 10.minutes } + let(:request) { -> { put path, headers: { 'REMOTE_ADDR' => remote_ip } } } + let(:path) { '/auth' } + + before do + sign_in user, scope: :user + + # Unfortunately, devise's `sign_in` helper causes the `session` to be + # loaded in the next request regardless of whether it's actually accessed + # by the client code. + # + # So, we make an extra query to clear issue a session cookie instead. + # + # A less resource-intensive way to deal with that would be to generate the + # session cookie manually, but this seems pretty involved. + get '/' + end + + it_behaves_like 'throttled endpoint' + end end From 8ab0ca7d64b9f7ec9d658d9a90eae1120ccef117 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 30 May 2024 14:24:29 +0200 Subject: [PATCH 098/117] Merge pull request from GHSA-c2r5-cfqr-c553 * Add hardening monkey-patch to prevent IP spoofing on misconfigured installations * Remove rack-attack safelist --- config/application.rb | 1 + config/initializers/rack_attack.rb | 4 -- lib/action_dispatch/remote_ip_extensions.rb | 72 +++++++++++++++++++++ 3 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 lib/action_dispatch/remote_ip_extensions.rb diff --git a/config/application.rb b/config/application.rb index 2a62c37e8..c2a63c869 100644 --- a/config/application.rb +++ b/config/application.rb @@ -48,6 +48,7 @@ require_relative '../lib/chewy/strategy/bypass_with_warning' require_relative '../lib/webpacker/manifest_extensions' require_relative '../lib/webpacker/helper_extensions' require_relative '../lib/rails/engine_extensions' +require_relative '../lib/action_dispatch/remote_ip_extensions' require_relative '../lib/active_record/database_tasks_extensions' require_relative '../lib/active_record/batches' require_relative '../lib/simple_navigation/item_extensions' diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 426b14b25..962f02994 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -62,10 +62,6 @@ class Rack::Attack end end - Rack::Attack.safelist('allow from localhost') do |req| - req.remote_ip == '127.0.0.1' || req.remote_ip == '::1' - end - Rack::Attack.blocklist('deny from blocklist') do |req| IpBlock.blocked?(req.remote_ip) end diff --git a/lib/action_dispatch/remote_ip_extensions.rb b/lib/action_dispatch/remote_ip_extensions.rb new file mode 100644 index 000000000..e5c48bf3c --- /dev/null +++ b/lib/action_dispatch/remote_ip_extensions.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +# Mastodon is not made to be directly accessed without a reverse proxy. +# This monkey-patch prevents remote IP address spoofing when being accessed +# directly. +# +# See PR: https://github.com/rails/rails/pull/51610 + +# In addition to the PR above, it also raises an error if a request with +# `X-Forwarded-For` or `Client-Ip` comes directly from a client without +# going through a trusted proxy. + +# rubocop:disable all -- This is a mostly vendored file + +module ActionDispatch + class RemoteIp + module GetIpExtensions + def calculate_ip + # Set by the Rack web server, this is a single value. + remote_addr = ips_from(@req.remote_addr).last + + # Could be a CSV list and/or repeated headers that were concatenated. + client_ips = ips_from(@req.client_ip).reverse! + forwarded_ips = ips_from(@req.x_forwarded_for).reverse! + + # `Client-Ip` and `X-Forwarded-For` should not, generally, both be set. If they + # are both set, it means that either: + # + # 1) This request passed through two proxies with incompatible IP header + # conventions. + # + # 2) The client passed one of `Client-Ip` or `X-Forwarded-For` + # (whichever the proxy servers weren't using) themselves. + # + # Either way, there is no way for us to determine which header is the right one + # after the fact. Since we have no idea, if we are concerned about IP spoofing + # we need to give up and explode. (If you're not concerned about IP spoofing you + # can turn the `ip_spoofing_check` option off.) + should_check_ip = @check_ip && client_ips.last && forwarded_ips.last + if should_check_ip && !forwarded_ips.include?(client_ips.last) + # We don't know which came from the proxy, and which from the user + raise IpSpoofAttackError, "IP spoofing attack?! " \ + "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \ + "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}" + end + + # NOTE: Mastodon addition to make sure we don't get requests from a non-trusted client + if @check_ip && (forwarded_ips.last || client_ips.last) && !@proxies.any? { |proxy| proxy === remote_addr } + raise IpSpoofAttackError, "IP spoofing attack?! client #{remote_addr} is not a trusted proxy " \ + "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \ + "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}" + end + + # We assume these things about the IP headers: + # + # - X-Forwarded-For will be a list of IPs, one per proxy, or blank + # - Client-Ip is propagated from the outermost proxy, or is blank + # - REMOTE_ADDR will be the IP that made the request to Rack + ips = forwarded_ips + client_ips + ips.compact! + + # If every single IP option is in the trusted list, return the IP that's + # furthest away + filter_proxies([remote_addr] + ips).first || ips.last || remote_addr + end + end + end +end + +ActionDispatch::RemoteIp::GetIp.prepend(ActionDispatch::RemoteIp::GetIpExtensions) + +# rubocop:enable all From 9740c7eaea07629a4d6154a3ed26995110737a0c Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 30 May 2024 14:56:18 +0200 Subject: [PATCH 099/117] Fix rate-limiting incorrectly triggering a session cookie on most endpoints (#30483) --- config/initializers/rack_attack.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 962f02994..6d8284e2b 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -30,7 +30,7 @@ class Rack::Attack end def authenticated_user_id - authenticated_token&.resource_owner_id || warden_user_id + authenticated_token&.resource_owner_id end def authenticated_token_id @@ -142,7 +142,7 @@ class Rack::Attack end throttle('throttle_password_change/account', limit: 10, period: 10.minutes) do |req| - req.authenticated_user_id if req.put? || (req.patch? && req.path_matches?('/auth')) + req.warden_user_id if req.put? || (req.patch? && req.path_matches?('/auth')) end self.throttled_responder = lambda do |request| From c93aacafdea188cac791b62a32e3117a7dc3e9cc Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 30 May 2024 15:34:50 +0200 Subject: [PATCH 100/117] Bump version to v4.2.9 (#30470) --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++++ docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 2 +- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 636b3c161..8e579e148 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,41 @@ All notable changes to this project will be documented in this file. +## [4.2.9] - 2024-05-30 + +### Security + +- Update dependencies +- Fix private mention filtering ([GHSA-5fq7-3p3j-9vrf](https://github.com/mastodon/mastodon/security/advisories/GHSA-5fq7-3p3j-9vrf)) +- Fix password change endpoint not being rate-limited ([GHSA-q3rg-xx5v-4mxh](https://github.com/mastodon/mastodon/security/advisories/GHSA-q3rg-xx5v-4mxh)) +- Add hardening around rate-limit bypass ([GHSA-c2r5-cfqr-c553](https://github.com/mastodon/mastodon/security/advisories/GHSA-c2r5-cfqr-c553)) + +### Added + +- Add rate-limit on OAuth application registration ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/30316)) +- Add fallback redirection when getting a webfinger query `WEB_DOMAIN@WEB_DOMAIN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28592)) +- Add `digest` attribute to `Admin::DomainBlock` entity in REST API ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29092)) + +### Removed + +- Remove superfluous application-level caching in some controllers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29862)) +- Remove aggressive OAuth application vacuuming ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/30316)) + +### Fixed + +- Fix leaking Elasticsearch connections in Sidekiq processes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30450)) +- Fix language of remote posts not being recognized when using unusual casing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30403)) +- Fix off-by-one in `tootctl media` commands ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30306)) +- Fix removal of allowed domains (in `LIMITED_FEDERATION_MODE`) not being recorded in the audit log ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/30125)) +- Fix not being able to block a subdomain of an already-blocked domain through the API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30119)) +- Fix `Idempotency-Key` being ignored when scheduling a post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30084)) +- Fix crash when supplying the `FFMPEG_BINARY` environment variable ([timothyjrogers](https://github.com/mastodon/mastodon/pull/30022)) +- Fix improper email address validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29838)) +- Fix results/query in `api/v1/featured_tags/suggestions` ([mjankowski](https://github.com/mastodon/mastodon/pull/29597)) +- Fix unblocking internationalized domain names under certain conditions ([tribela](https://github.com/mastodon/mastodon/pull/29530)) +- Fix admin account created by `mastodon:setup` not being auto-approved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29379)) +- Fix reference to non-existent var in CLI maintenance command ([mjankowski](https://github.com/mastodon/mastodon/pull/28363)) + ## [4.2.8] - 2024-02-23 ### Added diff --git a/docker-compose.yml b/docker-compose.yml index 004f5dbb0..10a57e31e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,7 @@ services: web: build: . - image: ghcr.io/mastodon/mastodon:v4.2.8 + image: ghcr.io/mastodon/mastodon:v4.2.9 restart: always env_file: .env.production command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" @@ -77,7 +77,7 @@ services: streaming: build: . - image: ghcr.io/mastodon/mastodon:v4.2.8 + image: ghcr.io/mastodon/mastodon:v4.2.9 restart: always env_file: .env.production command: node ./streaming @@ -95,7 +95,7 @@ services: sidekiq: build: . - image: ghcr.io/mastodon/mastodon:v4.2.8 + image: ghcr.io/mastodon/mastodon:v4.2.9 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index b28349dce..f9088382f 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 8 + 9 end def default_prerelease From 520b2086af066d6434a6f2652870a6bb620f4e23 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 12 Oct 2023 16:47:18 +0200 Subject: [PATCH 101/117] Change PWA start URL from `/home` to `/` (#27377) --- app/serializers/manifest_serializer.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/serializers/manifest_serializer.rb b/app/serializers/manifest_serializer.rb index 48f3aa7a6..b6533815a 100644 --- a/app/serializers/manifest_serializer.rb +++ b/app/serializers/manifest_serializer.rb @@ -16,11 +16,18 @@ class ManifestSerializer < ActiveModel::Serializer 512 ).freeze - attributes :name, :short_name, + attributes :id, :name, :short_name, :icons, :theme_color, :background_color, :display, :start_url, :scope, :share_target, :shortcuts + def id + # This is set to `/home` because that was the old value of `start_url` and + # thus the fallback ID computed by Chrome: + # https://developer.chrome.com/blog/pwa-manifest-id/ + '/home' + end + def name object.title end @@ -53,7 +60,7 @@ class ManifestSerializer < ActiveModel::Serializer end def start_url - '/home' + '/' end def scope From 19ed22dc582e9fd922b221034953c428dd0de856 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 12 Jun 2024 09:28:28 +0200 Subject: [PATCH 102/117] Fix duplicate `@context` attribute in user export (#30653) --- app/services/backup_service.rb | 4 ++-- spec/services/backup_service_spec.rb | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb index 3ef0366c3..b3282c409 100644 --- a/app/services/backup_service.rb +++ b/app/services/backup_service.rb @@ -19,8 +19,8 @@ class BackupService < BaseService def build_outbox_json!(file) skeleton = serialize(collection_presenter, ActivityPub::CollectionSerializer) - skeleton[:@context] = full_context - skeleton[:orderedItems] = ['!PLACEHOLDER!'] + skeleton['@context'] = full_context + skeleton['orderedItems'] = ['!PLACEHOLDER!'] skeleton = Oj.dump(skeleton) prepend, append = skeleton.split('"!PLACEHOLDER!"') add_comma = false diff --git a/spec/services/backup_service_spec.rb b/spec/services/backup_service_spec.rb index 806ba1832..1eb789d1f 100644 --- a/spec/services/backup_service_spec.rb +++ b/spec/services/backup_service_spec.rb @@ -55,9 +55,11 @@ RSpec.describe BackupService, type: :service do end def expect_outbox_export - json = export_json(:outbox) + body = export_json_raw(:outbox) + json = Oj.load(body) aggregate_failures do + expect(body.scan('@context').count).to eq 1 expect(json['@context']).to_not be_nil expect(json['type']).to eq 'OrderedCollection' expect(json['totalItems']).to eq 2 @@ -85,8 +87,12 @@ RSpec.describe BackupService, type: :service do end end + def export_json_raw(type) + read_zip_file(backup, "#{type}.json") + end + def export_json(type) - Oj.load(read_zip_file(backup, "#{type}.json")) + Oj.load(export_json_raw(type)) end def include_create_item(status) From bfc287fd6b83d9b32d0d7c34ed5d61f63c5bae35 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 20 Jun 2024 08:56:12 +0200 Subject: [PATCH 103/117] Remove dependency on posix-spawn (#18559) --- Gemfile | 1 - Gemfile.lock | 2 -- 2 files changed, 3 deletions(-) diff --git a/Gemfile b/Gemfile index 1d038ad9f..7f7da8c57 100644 --- a/Gemfile +++ b/Gemfile @@ -65,7 +65,6 @@ gem 'nsa' gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' gem 'parslet' -gem 'posix-spawn' gem 'public_suffix', '~> 5.0' gem 'pundit', '~> 2.3' gem 'premailer-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 891566e1a..2ef61e145 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -517,7 +517,6 @@ GEM pg (1.5.5) pghero (3.3.4) activerecord (>= 6) - posix-spawn (0.3.15) premailer (1.21.0) addressable css_parser (>= 1.12.0) @@ -892,7 +891,6 @@ DEPENDENCIES parslet pg (~> 1.5) pghero - posix-spawn premailer-rails private_address_check (~> 0.5) public_suffix (~> 5.0) From 3f75c6f048c75e5edd5749c0dfe4df1b935d0d3a Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 20 Jun 2024 08:59:41 +0200 Subject: [PATCH 104/117] Update dependency rails --- Gemfile.lock | 122 +++++++++++++++++++++++++-------------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2ef61e145..b3e19cd8f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -28,47 +28,47 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8.1) - actionpack (= 7.0.8.1) - activesupport (= 7.0.8.1) + actioncable (7.0.8.4) + actionpack (= 7.0.8.4) + activesupport (= 7.0.8.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.1) - actionpack (= 7.0.8.1) - activejob (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.1) + actionmailbox (7.0.8.4) + actionpack (= 7.0.8.4) + activejob (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8.1) - actionpack (= 7.0.8.1) - actionview (= 7.0.8.1) - activejob (= 7.0.8.1) - activesupport (= 7.0.8.1) + actionmailer (7.0.8.4) + actionpack (= 7.0.8.4) + actionview (= 7.0.8.4) + activejob (= 7.0.8.4) + activesupport (= 7.0.8.4) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.8.1) - actionview (= 7.0.8.1) - activesupport (= 7.0.8.1) + actionpack (7.0.8.4) + actionview (= 7.0.8.4) + activesupport (= 7.0.8.4) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8.1) - actionpack (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.1) + actiontext (7.0.8.4) + actionpack (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.1) - activesupport (= 7.0.8.1) + actionview (7.0.8.4) + activesupport (= 7.0.8.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -78,22 +78,22 @@ GEM activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (7.0.8.1) - activesupport (= 7.0.8.1) + activejob (7.0.8.4) + activesupport (= 7.0.8.4) globalid (>= 0.3.6) - activemodel (7.0.8.1) - activesupport (= 7.0.8.1) - activerecord (7.0.8.1) - activemodel (= 7.0.8.1) - activesupport (= 7.0.8.1) - activestorage (7.0.8.1) - actionpack (= 7.0.8.1) - activejob (= 7.0.8.1) - activerecord (= 7.0.8.1) - activesupport (= 7.0.8.1) + activemodel (7.0.8.4) + activesupport (= 7.0.8.4) + activerecord (7.0.8.4) + activemodel (= 7.0.8.4) + activesupport (= 7.0.8.4) + activestorage (7.0.8.4) + actionpack (= 7.0.8.4) + activejob (= 7.0.8.4) + activerecord (= 7.0.8.4) + activesupport (= 7.0.8.4) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.8.1) + activesupport (7.0.8.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -350,7 +350,7 @@ GEM httplog (1.6.2) rack (>= 2.0) rainbow (>= 2.0.0) - i18n (1.14.1) + i18n (1.14.5) concurrent-ruby (~> 1.0) i18n-tasks (1.0.12) activesupport (>= 4.0.2) @@ -432,7 +432,7 @@ GEM net-imap net-pop net-smtp - marcel (1.0.2) + marcel (1.0.4) mario-redis-lock (1.2.1) redis (>= 3.0.5) matrix (0.4.2) @@ -446,7 +446,7 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2023.0808) mini_mime (1.1.5) - mini_portile2 (2.8.5) + mini_portile2 (2.8.7) minitest (5.19.0) msgpack (1.7.1) multi_json (1.15.0) @@ -468,8 +468,8 @@ GEM net-smtp (0.3.4) net-protocol net-ssh (7.1.0) - nio4r (2.7.0) - nokogiri (1.16.5) + nio4r (2.7.3) + nokogiri (1.16.6) mini_portile2 (~> 2.8.2) racc (~> 1.4) nsa (0.3.0) @@ -533,7 +533,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.7.3) - rack (2.2.8.1) + rack (2.2.9) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-cors (2.0.2) @@ -550,20 +550,20 @@ GEM rack rack-test (2.1.0) rack (>= 1.3) - rails (7.0.8.1) - actioncable (= 7.0.8.1) - actionmailbox (= 7.0.8.1) - actionmailer (= 7.0.8.1) - actionpack (= 7.0.8.1) - actiontext (= 7.0.8.1) - actionview (= 7.0.8.1) - activejob (= 7.0.8.1) - activemodel (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.1) + rails (7.0.8.4) + actioncable (= 7.0.8.4) + actionmailbox (= 7.0.8.4) + actionmailer (= 7.0.8.4) + actionpack (= 7.0.8.4) + actiontext (= 7.0.8.4) + actionview (= 7.0.8.4) + activejob (= 7.0.8.4) + activemodel (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) bundler (>= 1.15.0) - railties (= 7.0.8.1) + railties (= 7.0.8.4) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -578,9 +578,9 @@ GEM rails-i18n (7.0.7) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.0.8.1) - actionpack (= 7.0.8.1) - activesupport (= 7.0.8.1) + railties (7.0.8.4) + actionpack (= 7.0.8.4) + activesupport (= 7.0.8.4) method_source rake (>= 12.2) thor (~> 1.0) @@ -743,7 +743,7 @@ GEM terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) test-prof (1.2.3) - thor (1.3.0) + thor (1.3.1) tilt (2.2.0) timeout (0.4.1) tpm-key_attestation (0.12.0) @@ -809,7 +809,7 @@ GEM xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.13) + zeitwerk (2.6.16) PLATFORMS ruby From 55408f80853b69fb08d3ae8f9a7daeab78b21a54 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 20 Jun 2024 09:00:08 +0200 Subject: [PATCH 105/117] Update dependency cbor --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index b3e19cd8f..edddcbd73 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -191,7 +191,7 @@ GEM xpath (~> 3.2) case_transform (0.2) activesupport - cbor (0.5.9.6) + cbor (0.5.9.8) charlock_holmes (0.7.7) chewy (7.3.4) activesupport (>= 5.2) From fcae9435ec6cb22e536c7b4eef351ce0b402e3ef Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 24 Jun 2024 15:11:10 +0200 Subject: [PATCH 106/117] Fix `/admin/accounts/:account_id/statuses/:id` for edited posts with media attachments (#30819) --- app/models/status_edit.rb | 2 +- spec/controllers/admin/statuses_controller_spec.rb | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/models/status_edit.rb b/app/models/status_edit.rb index 2b3248bb2..7de509dda 100644 --- a/app/models/status_edit.rb +++ b/app/models/status_edit.rb @@ -42,7 +42,7 @@ class StatusEdit < ApplicationRecord default_scope { order(id: :asc) } delegate :local?, :application, :edited?, :edited_at, - :discarded?, :visibility, to: :status + :discarded?, :visibility, :language, to: :status def emojis return @emojis if defined?(@emojis) diff --git a/spec/controllers/admin/statuses_controller_spec.rb b/spec/controllers/admin/statuses_controller_spec.rb index 9befdf978..5c8fc0a63 100644 --- a/spec/controllers/admin/statuses_controller_spec.rb +++ b/spec/controllers/admin/statuses_controller_spec.rb @@ -43,6 +43,11 @@ describe Admin::StatusesController do describe 'GET #show' do before do + status.media_attachments << Fabricate(:media_attachment, type: :image, account: status.account) + status.save! + status.snapshot!(at_time: status.created_at, rate_limit: false) + status.update!(text: 'Hello, this is an edited post') + status.snapshot!(rate_limit: false) get :show, params: { account_id: account.id, id: status.id } end From 5fd7cd79e0734d0f8d7d1250d4c1629def1a1b48 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 24 Jun 2024 15:49:39 +0200 Subject: [PATCH 107/117] Specify yarn version to avoid confusion with main which uses Yarn 4 --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 227399e80..35a236c8c 100644 --- a/package.json +++ b/package.json @@ -226,5 +226,6 @@ "Capfile|Gemfile|*.{rb,ruby,ru,rake}": "bundle exec rubocop --force-exclusion -a", "*.{js,jsx,ts,tsx}": "eslint --fix", "*.{css,scss}": "stylelint --fix" - } + }, + "packageManager": "yarn@1.22.22" } From 1e87634a43b19953f3e2956003d176967f2e7a8d Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 28 Jun 2024 23:23:19 +0200 Subject: [PATCH 108/117] Update dependency charlock_holmes --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index edddcbd73..34958703a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -192,7 +192,7 @@ GEM case_transform (0.2) activesupport cbor (0.5.9.8) - charlock_holmes (0.7.7) + charlock_holmes (0.7.8) chewy (7.3.4) activesupport (>= 5.2) elasticsearch (>= 7.12.0, < 7.14.0) From 17f69c0002e68851293b158e1e7cda685fdfb2dc Mon Sep 17 00:00:00 2001 From: Tim Rogers Date: Mon, 24 Jun 2024 09:41:04 -0500 Subject: [PATCH 109/117] Added check for STATSD_ADDR setting to emit a warning and proceed rather than crashing if the address is unreachable (#30691) --- config/initializers/statsd.rb | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/config/initializers/statsd.rb b/config/initializers/statsd.rb index a655c1071..f1628a9d1 100644 --- a/config/initializers/statsd.rb +++ b/config/initializers/statsd.rb @@ -3,13 +3,17 @@ if ENV['STATSD_ADDR'].present? host, port = ENV['STATSD_ADDR'].split(':') - statsd = Statsd.new(host, port) - statsd.namespace = ENV.fetch('STATSD_NAMESPACE') { ['Mastodon', Rails.env].join('.') } + begin + statsd = Statsd.new(host, port) + statsd.namespace = ENV.fetch('STATSD_NAMESPACE') { ['Mastodon', Rails.env].join('.') } - NSA.inform_statsd(statsd) do |informant| - informant.collect(:action_controller, :web) - informant.collect(:active_record, :db) - informant.collect(:active_support_cache, :cache) - informant.collect(:sidekiq, :sidekiq) if ENV['STATSD_SIDEKIQ'] == 'true' + NSA.inform_statsd(statsd) do |informant| + informant.collect(:action_controller, :web) + informant.collect(:active_record, :db) + informant.collect(:active_support_cache, :cache) + informant.collect(:sidekiq, :sidekiq) if ENV['STATSD_SIDEKIQ'] == 'true' + end + rescue + Rails.logger.warn("statsd address #{ENV['STATSD_ADDR']} not reachable, proceeding without statsd") end end From 846f59c6e95ae0f6ccd5714bd1a57c8dd0fed017 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Thu, 27 Jun 2024 16:40:19 +0200 Subject: [PATCH 110/117] Add size limit for link preview URLs (#30854) --- app/services/fetch_link_card_service.rb | 5 ++++- spec/services/fetch_link_card_service_spec.rb | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 13775e63c..8302a9ef4 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -15,6 +15,9 @@ class FetchLinkCardService < BaseService ) }iox + # URL size limit to safely store in PosgreSQL's unique indexes + BYTESIZE_LIMIT = 2692 + def call(status) @status = status @original_url = parse_urls @@ -85,7 +88,7 @@ class FetchLinkCardService < BaseService def bad_url?(uri) # Avoid local instance URLs and invalid URLs - uri.host.blank? || TagManager.instance.local_url?(uri.to_s) || !%w(http https).include?(uri.scheme) + uri.host.blank? || TagManager.instance.local_url?(uri.to_s) || !%w(http https).include?(uri.scheme) || uri.to_s.bytesize > BYTESIZE_LIMIT end def mention_link?(anchor) diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb index f44cbb750..fa4d7bb36 100644 --- a/spec/services/fetch_link_card_service_spec.rb +++ b/spec/services/fetch_link_card_service_spec.rb @@ -184,6 +184,19 @@ RSpec.describe FetchLinkCardService, type: :service do end end + context 'with an URL too long for PostgreSQL unique indexes' do + let(:url) { "http://example.com/#{'a' * 2674}" } + let(:status) { Fabricate(:status, text: url) } + + it 'does not fetch the URL' do + expect(a_request(:get, url)).to_not have_been_made + end + + it 'does not create a preview card' do + expect(status.preview_card).to be_nil + end + end + context 'with a URL of a page with oEmbed support' do let(:html) { 'Hello world' } let(:status) { Fabricate(:status, text: 'http://example.com/html') } From 88b2d6eca57e4f8d90062d9eb9d511213bb00a1c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 27 Jun 2024 23:34:34 +0200 Subject: [PATCH 111/117] Change search modifiers to be case-insensitive (#30865) --- app/lib/search_query_transformer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb index a45ae3d09..8849acf12 100644 --- a/app/lib/search_query_transformer.rb +++ b/app/lib/search_query_transformer.rb @@ -223,7 +223,7 @@ class SearchQueryTransformer < Parslet::Transform end rule(clause: subtree(:clause)) do - prefix = clause[:prefix][:term].to_s if clause[:prefix] + prefix = clause[:prefix][:term].to_s.downcase if clause[:prefix] operator = clause[:operator]&.to_s term = clause[:phrase] ? clause[:phrase].map { |term| term[:term].to_s }.join(' ') : clause[:term].to_s From 9b6219c48f4fafacc1f31c0f8d89f98948a46aa9 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Fri, 21 Jun 2024 14:51:10 +0200 Subject: [PATCH 112/117] Improve encoding detection for link cards (#30780) --- app/lib/link_details_extractor.rb | 15 ++++++++++----- .../fixtures/requests/low_confidence_latin1.txt | 17 +++++++++++++++++ spec/services/fetch_link_card_service_spec.rb | 9 +++++++++ 3 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 spec/fixtures/requests/low_confidence_latin1.txt diff --git a/app/lib/link_details_extractor.rb b/app/lib/link_details_extractor.rb index bb031986d..48a592a40 100644 --- a/app/lib/link_details_extractor.rb +++ b/app/lib/link_details_extractor.rb @@ -265,16 +265,21 @@ class LinkDetailsExtractor end def document - @document ||= Nokogiri::HTML(@html, nil, encoding) + @document ||= detect_encoding_and_parse_document end - def encoding - @encoding ||= begin - guess = detector.detect(@html, @html_charset) - guess&.fetch(:confidence, 0).to_i > 60 ? guess&.fetch(:encoding, nil) : nil + def detect_encoding_and_parse_document + [detect_encoding, nil, @html_charset, 'UTF-8'].uniq.each do |encoding| + document = Nokogiri::HTML(@html, nil, encoding) + return document if document.to_s.valid_encoding? end end + def detect_encoding + guess = detector.detect(@html, @html_charset) + guess&.fetch(:confidence, 0).to_i > 60 ? guess&.fetch(:encoding, nil) : nil + end + def detector @detector ||= CharlockHolmes::EncodingDetector.new.tap do |detector| detector.strip_tags = true diff --git a/spec/fixtures/requests/low_confidence_latin1.txt b/spec/fixtures/requests/low_confidence_latin1.txt new file mode 100644 index 000000000..39c3e23d6 --- /dev/null +++ b/spec/fixtures/requests/low_confidence_latin1.txt @@ -0,0 +1,17 @@ +HTTP/1.1 200 OK +server: nginx +date: Thu, 13 Jun 2024 14:33:13 GMT +content-type: text/html; charset=ISO-8859-1 +content-length: 158 +accept-ranges: bytes + + + + + + Tofu l'orange + + +

Tofu l'orange

+ + diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb index fa4d7bb36..592dce24d 100644 --- a/spec/services/fetch_link_card_service_spec.rb +++ b/spec/services/fetch_link_card_service_spec.rb @@ -26,6 +26,7 @@ RSpec.describe FetchLinkCardService, type: :service do stub_request(:get, 'http://example.com/sjis_with_wrong_charset').to_return(request_fixture('sjis_with_wrong_charset.txt')) stub_request(:get, 'http://example.com/koi8-r').to_return(request_fixture('koi8-r.txt')) stub_request(:get, 'http://example.com/windows-1251').to_return(request_fixture('windows-1251.txt')) + stub_request(:get, 'http://example.com/low_confidence_latin1').to_return(request_fixture('low_confidence_latin1.txt')) Rails.cache.write('oembed_endpoint:example.com', oembed_cache) if oembed_cache @@ -148,6 +149,14 @@ RSpec.describe FetchLinkCardService, type: :service do end end + context 'with a URL of a page in ISO-8859-1 encoding, that charlock_holmes cannot detect' do + let(:status) { Fabricate(:status, text: 'Check out http://example.com/low_confidence_latin1') } + + it 'decodes the HTML' do + expect(status.preview_card.title).to eq("Tofu á l'orange") + end + end + context 'with a Japanese path URL' do let(:status) { Fabricate(:status, text: 'テストhttp://example.com/日本語') } From 6cd9bd6ae154eee02c42121ffaae280de907edbb Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 3 Jul 2024 09:15:47 +0200 Subject: [PATCH 113/117] fix: Return HTTP 422 when scheduled status time is less than 5 minutes (#30584) --- app/services/post_status_service.rb | 2 +- .../api/v1/statuses_controller_spec.rb | 40 +++++++++++++++++++ spec/services/post_status_service_spec.rb | 10 +++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index e4dd480f1..280666dcf 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -171,7 +171,7 @@ class PostStatusService < BaseService end def scheduled_in_the_past? - @scheduled_at.present? && @scheduled_at <= Time.now.utc + MIN_SCHEDULE_OFFSET + @scheduled_at.present? && @scheduled_at <= Time.now.utc end def bump_potential_friendship! diff --git a/spec/controllers/api/v1/statuses_controller_spec.rb b/spec/controllers/api/v1/statuses_controller_spec.rb index c2bdba9ac..591307fe2 100644 --- a/spec/controllers/api/v1/statuses_controller_spec.rb +++ b/spec/controllers/api/v1/statuses_controller_spec.rb @@ -182,6 +182,46 @@ RSpec.describe Api::V1::StatusesController do expect(response.headers['X-RateLimit-Remaining']).to eq '0' end end + + context 'with missing thread' do + subject { post :create, params: params } + + let(:params) { { status: 'Hello world', in_reply_to_id: 0 } } + + it 'returns http not found' do + subject + + expect(response).to have_http_status(404) + end + end + + context 'when scheduling a status' do + subject { post :create, params: params } + + let(:params) { { status: 'Hello world', scheduled_at: 10.minutes.from_now } } + let(:account) { user.account } + + it 'returns HTTP 200' do + subject + + expect(response).to have_http_status(200) + end + + it 'creates a scheduled status' do + expect { subject }.to change { account.scheduled_statuses.count }.from(0).to(1) + end + + context 'when the scheduling time is less than 5 minutes' do + let(:params) { { status: 'Hello world', scheduled_at: 4.minutes.from_now } } + + it 'does not create a scheduled status', :aggregate_failures do + subject + + expect(response).to have_http_status(422) + expect(account.scheduled_statuses).to be_empty + end + end + end end describe 'DELETE #destroy' do diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index a74e82626..826af5479 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -61,6 +61,16 @@ RSpec.describe PostStatusService, type: :service do status2 = subject.call(account, text: 'test', idempotency: 'meepmeep', scheduled_at: future) expect(status2.id).to eq status1.id end + + context 'when scheduled_at is less than min offset' do + let(:invalid_scheduled_time) { 4.minutes.from_now } + + it 'raises invalid record error' do + expect do + subject.call(account, text: 'Hi future!', scheduled_at: invalid_scheduled_time) + end.to raise_error(ActiveRecord::RecordInvalid) + end + end end it 'creates response to the original status of boost' do From df974a912b59e276ad7bfa413e2b628633a776d4 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 4 Jul 2024 16:11:28 +0200 Subject: [PATCH 114/117] Merge pull request from GHSA-vp5r-5pgw-jwqx * Fix streaming sessions not being closed when revoking access to an app * Add tests for GHSA-7w3c-p9j8-mq3x --- .../oauth/authorized_applications_controller.rb | 1 + app/lib/application_extension.rb | 8 +++++--- .../oauth/authorized_applications_controller_spec.rb | 10 ++++++++++ .../settings/applications_controller_spec.rb | 8 ++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index 350ae2e90..17f1be23d 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -17,6 +17,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio def destroy Web::PushSubscription.unsubscribe_for(params[:id], current_resource_owner) + Doorkeeper::Application.find_by(id: params[:id])&.close_streaming_sessions(current_resource_owner) super end diff --git a/app/lib/application_extension.rb b/app/lib/application_extension.rb index 400c51a02..f226b99cd 100644 --- a/app/lib/application_extension.rb +++ b/app/lib/application_extension.rb @@ -16,17 +16,19 @@ module ApplicationExtension # dependent: delete_all, which means the ActiveRecord callback in # AccessTokenExtension is not run, so instead we manually announce to # streaming that these tokens are being deleted. - before_destroy :push_to_streaming_api, prepend: true + before_destroy :close_streaming_sessions, prepend: true end def confirmation_redirect_uri redirect_uri.lines.first.strip end - def push_to_streaming_api + def close_streaming_sessions(resource_owner = nil) # TODO: #28793 Combine into a single topic payload = Oj.dump(event: :kill) - access_tokens.in_batches do |tokens| + scope = access_tokens + scope = scope.where(resource_owner_id: resource_owner.id) unless resource_owner.nil? + scope.in_batches do |tokens| redis.pipelined do |pipeline| tokens.ids.each do |id| pipeline.publish("timeline:access_token:#{id}", payload) diff --git a/spec/controllers/oauth/authorized_applications_controller_spec.rb b/spec/controllers/oauth/authorized_applications_controller_spec.rb index b54610604..3fd9f9499 100644 --- a/spec/controllers/oauth/authorized_applications_controller_spec.rb +++ b/spec/controllers/oauth/authorized_applications_controller_spec.rb @@ -50,9 +50,11 @@ describe Oauth::AuthorizedApplicationsController do let!(:application) { Fabricate(:application) } let!(:access_token) { Fabricate(:accessible_access_token, application: application, resource_owner_id: user.id) } let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) } + let(:redis_pipeline_stub) { instance_double(Redis::Namespace, publish: nil) } before do sign_in user, scope: :user + allow(redis).to receive(:pipelined).and_yield(redis_pipeline_stub) post :destroy, params: { id: application.id } end @@ -63,5 +65,13 @@ describe Oauth::AuthorizedApplicationsController do it 'removes subscriptions for the application\'s access tokens' do expect(Web::PushSubscription.where(user: user).count).to eq 0 end + + it 'removes the web_push_subscription' do + expect { web_push_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'sends a session kill payload to the streaming server' do + expect(redis_pipeline_stub).to have_received(:publish).with("timeline:access_token:#{access_token.id}", '{"event":"kill"}') + end end end diff --git a/spec/controllers/settings/applications_controller_spec.rb b/spec/controllers/settings/applications_controller_spec.rb index 169304b3e..88974e551 100644 --- a/spec/controllers/settings/applications_controller_spec.rb +++ b/spec/controllers/settings/applications_controller_spec.rb @@ -166,7 +166,11 @@ describe Settings::ApplicationsController do end describe 'destroy' do + let(:redis_pipeline_stub) { instance_double(Redis::Namespace, publish: nil) } + let!(:access_token) { Fabricate(:accessible_access_token, application: app) } + before do + allow(redis).to receive(:pipelined).and_yield(redis_pipeline_stub) post :destroy, params: { id: app.id } end @@ -177,6 +181,10 @@ describe Settings::ApplicationsController do it 'removes the app' do expect(Doorkeeper::Application.find_by(id: app.id)).to be_nil end + + it 'sends a session kill payload to the streaming server' do + expect(redis_pipeline_stub).to have_received(:publish).with("timeline:access_token:#{access_token.id}", '{"event":"kill"}') + end end describe 'regenerate' do From 4fb4721072e552dc2fa7541f7bdeb1737a6c113e Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 4 Jul 2024 16:26:49 +0200 Subject: [PATCH 115/117] Merge pull request from GHSA-58x8-3qxw-6hm7 * Fix insufficient permission checking for public timeline endpoints Note that this changes unauthenticated access failure code from 401 to 422 * Add more tests for public timelines * Require user token in `/api/v1/statuses/:id/translate` and `/api/v1/scheduled_statuses` --- .../api/v1/scheduled_statuses_controller.rb | 1 + .../v1/statuses/translations_controller.rb | 1 + .../api/v1/timelines/public_controller.rb | 1 + .../api/v1/timelines/tag_controller.rb | 3 ++- .../v1/scheduled_statuses_controller_spec.rb | 11 ++++++++ .../statuses/translations_controller_spec.rb | 20 ++++++++++++++ .../api/v1/timelines/tag_controller_spec.rb | 19 +++++++++++--- spec/requests/api/v1/timelines/public_spec.rb | 26 ++++++++++++++----- 8 files changed, 71 insertions(+), 11 deletions(-) diff --git a/app/controllers/api/v1/scheduled_statuses_controller.rb b/app/controllers/api/v1/scheduled_statuses_controller.rb index 2220b6d22..b33b534eb 100644 --- a/app/controllers/api/v1/scheduled_statuses_controller.rb +++ b/app/controllers/api/v1/scheduled_statuses_controller.rb @@ -6,6 +6,7 @@ class Api::V1::ScheduledStatusesController < Api::BaseController before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, except: [:update, :destroy] before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:update, :destroy] + before_action :require_user! before_action :set_statuses, only: :index before_action :set_status, except: :index diff --git a/app/controllers/api/v1/statuses/translations_controller.rb b/app/controllers/api/v1/statuses/translations_controller.rb index ec5ea5b85..5e5ee7d38 100644 --- a/app/controllers/api/v1/statuses/translations_controller.rb +++ b/app/controllers/api/v1/statuses/translations_controller.rb @@ -4,6 +4,7 @@ class Api::V1::Statuses::TranslationsController < Api::BaseController include Authorization before_action -> { doorkeeper_authorize! :read, :'read:statuses' } + before_action :require_user! before_action :set_status before_action :set_translation diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index 5bbd92b9e..0ff2c5aee 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Api::V1::Timelines::PublicController < Api::BaseController + before_action -> { authorize_if_got_token! :read, :'read:statuses' } before_action :require_user!, only: [:show], if: :require_auth? after_action :insert_pagination_headers, unless: -> { @statuses.empty? } diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index a79d65c12..ffea89c25 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true class Api::V1::Timelines::TagController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth? + before_action -> { authorize_if_got_token! :read, :'read:statuses' } + before_action :require_user!, if: :require_auth? before_action :load_tag after_action :insert_pagination_headers, unless: -> { @statuses.empty? } diff --git a/spec/controllers/api/v1/scheduled_statuses_controller_spec.rb b/spec/controllers/api/v1/scheduled_statuses_controller_spec.rb index 256c4b272..cc3b65f37 100644 --- a/spec/controllers/api/v1/scheduled_statuses_controller_spec.rb +++ b/spec/controllers/api/v1/scheduled_statuses_controller_spec.rb @@ -13,6 +13,17 @@ describe Api::V1::ScheduledStatusesController do allow(controller).to receive(:doorkeeper_token) { token } end + context 'with an application token' do + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: 'read:statuses') } + + it 'returns http unprocessable entity' do + get :index + + expect(response) + .to have_http_status(422) + end + end + describe 'GET #index' do it 'returns http success' do get :index diff --git a/spec/controllers/api/v1/statuses/translations_controller_spec.rb b/spec/controllers/api/v1/statuses/translations_controller_spec.rb index 6257494ae..da152843b 100644 --- a/spec/controllers/api/v1/statuses/translations_controller_spec.rb +++ b/spec/controllers/api/v1/statuses/translations_controller_spec.rb @@ -9,6 +9,26 @@ describe Api::V1::Statuses::TranslationsController do let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses', application: app) } + context 'with an application token' do + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: 'read:statuses', application: app) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'POST /api/v1/statuses/:status_id/translate' do + let(:status) { Fabricate(:status, account: user.account, text: 'Hola', language: 'es') } + + before do + post :create, params: { status_id: status.id } + end + + it 'returns http unprocessable entity' do + expect(response).to have_http_status(422) + end + end + end + context 'with an oauth token' do before do allow(controller).to receive(:doorkeeper_token) { token } diff --git a/spec/controllers/api/v1/timelines/tag_controller_spec.rb b/spec/controllers/api/v1/timelines/tag_controller_spec.rb index 1c60798fc..89622a41a 100644 --- a/spec/controllers/api/v1/timelines/tag_controller_spec.rb +++ b/spec/controllers/api/v1/timelines/tag_controller_spec.rb @@ -6,7 +6,8 @@ describe Api::V1::Timelines::TagController do render_views let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses') } + let(:scopes) { 'read:statuses' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } before do allow(controller).to receive(:doorkeeper_token) { token } @@ -48,13 +49,23 @@ describe Api::V1::Timelines::TagController do Form::AdminSettings.new(timeline_preview: false).save end - context 'when the user is not authenticated' do + context 'without an access token' do let(:token) { nil } - it 'returns http unauthorized' do + it 'returns http unprocessable entity' do subject - expect(response).to have_http_status(401) + expect(response).to have_http_status(422) + end + end + + context 'with an application access token, not bound to a user' do + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: scopes) } + + it 'returns http unprocessable entity' do + subject + + expect(response).to have_http_status(422) end end diff --git a/spec/requests/api/v1/timelines/public_spec.rb b/spec/requests/api/v1/timelines/public_spec.rb index c43626240..03bde3da8 100644 --- a/spec/requests/api/v1/timelines/public_spec.rb +++ b/spec/requests/api/v1/timelines/public_spec.rb @@ -32,6 +32,8 @@ describe 'Public' do context 'when the instance allows public preview' do let(:expected_statuses) { [local_status, remote_status, media_status] } + it_behaves_like 'forbidden for wrong scope', 'profile' + context 'with an authorized user' do it_behaves_like 'a successful request to the public timeline' end @@ -96,13 +98,9 @@ describe 'Public' do Form::AdminSettings.new(timeline_preview: false).save end - context 'with an authenticated user' do - let(:expected_statuses) { [local_status, remote_status, media_status] } + it_behaves_like 'forbidden for wrong scope', 'profile' - it_behaves_like 'a successful request to the public timeline' - end - - context 'with an unauthenticated user' do + context 'without an authentication token' do let(:headers) { {} } it 'returns http unprocessable entity' do @@ -111,6 +109,22 @@ describe 'Public' do expect(response).to have_http_status(422) end end + + context 'with an application access token, not bound to a user' do + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: scopes) } + + it 'returns http unprocessable entity' do + subject + + expect(response).to have_http_status(422) + end + end + + context 'with an authenticated user' do + let(:expected_statuses) { [local_status, remote_status, media_status] } + + it_behaves_like 'a successful request to the public timeline' + end end end end From d4bf22b632ea8b1174375c4966a6768ab66393b6 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 4 Jul 2024 16:45:52 +0200 Subject: [PATCH 116/117] Merge pull request from GHSA-xjvf-fm67-4qc3 --- app/lib/activitypub/activity/create.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index fedfa39de..1581555bd 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -104,7 +104,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def find_existing_status status = status_from_uri(object_uri) status ||= Status.find_by(uri: @object['atomUri']) if @object['atomUri'].present? - status + status if status&.account_id == @account.id end def process_status_params From a5b4a2b7e71aedd2f04bc7a90f79dfe234fa7f89 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 4 Jul 2024 16:46:35 +0200 Subject: [PATCH 117/117] Bump version to v4.2.10 (#30910) --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 2 +- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e579e148..e57d5f23b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,37 @@ All notable changes to this project will be documented in this file. +## [4.2.10] - 2024-07-04 + +### Security + +- Fix incorrect permission checking on multiple API endpoints ([GHSA-58x8-3qxw-6hm7](https://github.com/mastodon/mastodon/security/advisories/GHSA-58x8-3qxw-6hm7)) +- Fix incorrect authorship checking when processing some activities (CVE-2024-37903, [GHSA-xjvf-fm67-4qc3](https://github.com/mastodon/mastodon/security/advisories/GHSA-xjvf-fm67-4qc3)) +- Fix ongoing streaming sessions not being invalidated when application tokens get revoked ([GHSA-vp5r-5pgw-jwqx](https://github.com/mastodon/mastodon/security/advisories/GHSA-vp5r-5pgw-jwqx)) +- Update dependencies + +### Added + +- Add yarn version specification to avoid confusion with Yarn 3 and Yarn 4 + +### Changed + +- Change preview cards generation to skip unusually long URLs ([oneiros](https://github.com/mastodon/mastodon/pull/30854)) +- Change search modifiers to be case-insensitive ([Gargron](https://github.com/mastodon/mastodon/pull/30865)) +- Change `STATSD_ADDR` handling to emit a warning rather than crashing if the address is unreachable ([timothyjrogers](https://github.com/mastodon/mastodon/pull/30691)) +- Change PWA start URL from `/home` to `/` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27377)) + +### Removed + +- Removed dependency on `posix-spawn` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18559)) + +### Fixed + +- Fix scheduled statuses scheduled in less than 5 minutes being immediately published ([danielmbrasil](https://github.com/mastodon/mastodon/pull/30584)) +- Fix encoding detection for link cards ([oneiros](https://github.com/mastodon/mastodon/pull/30780)) +- Fix `/admin/accounts/:account_id/statuses/:id` for edited posts with media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30819)) +- Fix duplicate `@context` attribute in user archive export ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30653)) + ## [4.2.9] - 2024-05-30 ### Security diff --git a/docker-compose.yml b/docker-compose.yml index 10a57e31e..d61aa0558 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,7 @@ services: web: build: . - image: ghcr.io/mastodon/mastodon:v4.2.9 + image: ghcr.io/mastodon/mastodon:v4.2.10 restart: always env_file: .env.production command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" @@ -77,7 +77,7 @@ services: streaming: build: . - image: ghcr.io/mastodon/mastodon:v4.2.9 + image: ghcr.io/mastodon/mastodon:v4.2.10 restart: always env_file: .env.production command: node ./streaming @@ -95,7 +95,7 @@ services: sidekiq: build: . - image: ghcr.io/mastodon/mastodon:v4.2.9 + image: ghcr.io/mastodon/mastodon:v4.2.10 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index f9088382f..77e85a165 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 9 + 10 end def default_prerelease