diff options
Diffstat (limited to 'app/controllers')
19 files changed, 140 insertions, 51 deletions
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 620c0ff78..654f2d92c 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -10,10 +10,10 @@ class AboutController < ApplicationController before_action :require_open_federation!, only: [:show, :more] before_action :set_body_classes, only: :show before_action :set_instance_presenter - before_action :set_expires_in, only: [:more, :terms] + before_action :set_expires_in, only: [:more] before_action :set_registration_form_time, only: :show - skip_before_action :require_functional!, only: [:more, :terms] + skip_before_action :require_functional!, only: [:more] def show; end @@ -28,8 +28,6 @@ class AboutController < ApplicationController @blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks? end - def terms; end - helper_method :display_blocks? helper_method :display_blocks_rationale? helper_method :public_fetch_mode? diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 9949206cb..02f3c3dd7 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -7,7 +7,7 @@ class AccountsController < ApplicationController include AccountControllerConcern include SignatureAuthentication - before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers before_action :set_body_classes diff --git a/app/controllers/activitypub/claims_controller.rb b/app/controllers/activitypub/claims_controller.rb index 08ad952df..339333e46 100644 --- a/app/controllers/activitypub/claims_controller.rb +++ b/app/controllers/activitypub/claims_controller.rb @@ -6,7 +6,7 @@ class ActivityPub::ClaimsController < ActivityPub::BaseController skip_before_action :authenticate_user! - before_action :require_signature! + before_action :require_account_signature! before_action :set_claim_result def create diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index ac7ab8a0b..23d874071 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -4,7 +4,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController include SignatureVerification include AccountOwnedConcern - before_action :require_signature!, if: :authorized_fetch_mode? + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_items before_action :set_size before_action :set_type diff --git a/app/controllers/activitypub/followers_synchronizations_controller.rb b/app/controllers/activitypub/followers_synchronizations_controller.rb index 940b77cf0..4e445bcb1 100644 --- a/app/controllers/activitypub/followers_synchronizations_controller.rb +++ b/app/controllers/activitypub/followers_synchronizations_controller.rb @@ -4,7 +4,7 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro include SignatureVerification include AccountOwnedConcern - before_action :require_signature! + before_action :require_account_signature! before_action :set_items before_action :set_cache_headers diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 92dcb5ac7..5ee85474e 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -6,7 +6,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController include AccountOwnedConcern before_action :skip_unknown_actor_activity - before_action :require_signature! + before_action :require_actor_signature! skip_before_action :authenticate_user! def create @@ -49,17 +49,17 @@ class ActivityPub::InboxesController < ActivityPub::BaseController end def upgrade_account - if signed_request_account.ostatus? + if signed_request_account&.ostatus? signed_request_account.update(last_webfingered_at: nil) ResolveAccountWorker.perform_async(signed_request_account.acct) end - DeliveryFailureTracker.reset!(signed_request_account.inbox_url) + DeliveryFailureTracker.reset!(signed_request_actor.inbox_url) end def process_collection_synchronization raw_params = request.headers['Collection-Synchronization'] - return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true' + return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true' || signed_request_account.nil? # Re-using the syntax for signature parameters tree = SignatureParamsParser.new.parse(raw_params) @@ -71,6 +71,6 @@ class ActivityPub::InboxesController < ActivityPub::BaseController end def process_payload - ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id) + ActivityPub::ProcessingWorker.perform_async(signed_request_actor.id, body, @account&.id, signed_request_actor.class.name) end end diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index cd3992502..60d201f76 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -6,7 +6,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController include SignatureVerification include AccountOwnedConcern - before_action :require_signature!, if: :authorized_fetch_mode? + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_statuses before_action :set_cache_headers diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index 4ff7cfa08..8e0f9de2e 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -7,7 +7,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController DESCENDANTS_LIMIT = 60 - before_action :require_signature!, if: :authorized_fetch_mode? + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_status before_action :set_cache_headers before_action :set_replies diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 2e393fbb6..7ce6599c5 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -131,4 +131,10 @@ class Api::BaseController < ApplicationController def disallow_unauthenticated_api_access? authorized_fetch_mode? end + + private + + def respond_with_error(code) + render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code + end end diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index a665863eb..b61de13b9 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Accounts::FollowerAccountsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:accounts' } + before_action -> { authorize_if_got_token! :read, :'read:accounts' } before_action :set_account after_action :insert_pagination_headers diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 7d885a212..37d3c2d78 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Accounts::FollowingAccountsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:accounts' } + before_action -> { authorize_if_got_token! :read, :'read:accounts' } before_action :set_account after_action :insert_pagination_headers diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 5537cc9b0..be84720aa 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -30,12 +30,12 @@ class Api::V1::AccountsController < Api::BaseController self.response_body = Oj.dump(response.body) self.status = response.status rescue ActiveRecord::RecordInvalid => e - render json: ValidationErrorFormatter.new(e, :'account.username' => :username, :'invite_request.text' => :reason).as_json, status: :unprocessable_entity + render json: ValidationErrorFormatter.new(e, 'account.username': :username, 'invite_request.text': :reason).as_json, status: :unprocessable_entity end def follow - follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, with_rate_limit: true) - options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify? } }, requested_map: { @account.id => false } } + follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, languages: params.key?(:languages) ? params[:languages] : nil, with_rate_limit: true) + options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify?, languages: follow.languages } }, requested_map: { @account.id => false } } render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(**options) end diff --git a/app/controllers/api/v1/statuses/translations_controller.rb b/app/controllers/api/v1/statuses/translations_controller.rb new file mode 100644 index 000000000..540b17d00 --- /dev/null +++ b/app/controllers/api/v1/statuses/translations_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class Api::V1::Statuses::TranslationsController < Api::BaseController + include Authorization + + before_action -> { doorkeeper_authorize! :read, :'read:statuses' } + before_action :set_status + before_action :set_translation + + rescue_from TranslationService::NotConfiguredError, with: :not_found + rescue_from TranslationService::UnexpectedResponseError, TranslationService::QuotaExceededError, TranslationService::TooManyRequestsError, with: :service_unavailable + + def create + render json: @translation, serializer: REST::TranslationSerializer + end + + private + + def set_status + @status = Status.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + not_found + end + + def set_translation + @translation = TranslateStatusService.new.call(@status, content_locale) + end +end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 4dd0cac55..2394574b3 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -45,10 +45,14 @@ module SignatureVerification end end - def require_signature! + def require_account_signature! render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account end + def require_actor_signature! + render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_actor + end + def signed_request? request.headers['Signature'].present? end @@ -68,7 +72,11 @@ module SignatureVerification end def signed_request_account - return @signed_request_account if defined?(@signed_request_account) + signed_request_actor.is_a?(Account) ? signed_request_actor : nil + end + + def signed_request_actor + return @signed_request_actor if defined?(@signed_request_actor) raise SignatureVerificationError, 'Request not signed' unless signed_request? raise SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters? @@ -78,26 +86,30 @@ module SignatureVerification verify_signature_strength! verify_body_digest! - account = account_from_key_id(signature_params['keyId']) + actor = actor_from_key_id(signature_params['keyId']) - raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil? + 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 - return account unless verify_signature(account, signature, compare_signed_string).nil? + return actor unless verify_signature(actor, signature, compare_signed_string).nil? - account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) } + actor = stoplight_wrap_request { actor_refresh_key!(actor) } - raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil? + raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil? - return account unless verify_signature(account, signature, compare_signed_string).nil? + return actor unless verify_signature(actor, signature, compare_signed_string).nil? - @signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)" - @signed_request_account = nil + fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)" rescue SignatureVerificationError => e - @signature_verification_failure_reason = e.message - @signed_request_account = nil + fail_with! e.message + rescue HTTP::Error, OpenSSL::SSL::SSLError => e + fail_with! "Failed to fetch remote data: #{e.message}" + rescue Mastodon::UnexpectedResponseError + fail_with! 'Failed to fetch remote data (got unexpected reply from server)' + rescue Stoplight::Error::RedLight + fail_with! 'Fetching attempt skipped because of recent connection failure' end def request_body @@ -106,6 +118,11 @@ module SignatureVerification private + def fail_with!(message) + @signature_verification_failure_reason = message + @signed_request_actor = nil + end + def signature_params @signature_params ||= begin raw_signature = request.headers['Signature'] @@ -138,13 +155,23 @@ module SignatureVerification digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] } sha256 = digests.assoc('sha-256') raise SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil? - raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}" if body_digest != sha256[1] + + return if body_digest == sha256[1] + + digest_size = begin + Base64.strict_decode64(sha256[1].strip).length + rescue ArgumentError + raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a valid base64 string. Given digest: #{sha256[1]}" + end + + raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a SHA-256 digest. Given digest: #{sha256[1]}" if digest_size != 32 + raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}" end - def verify_signature(account, signature, compare_signed_string) - if account.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string) - @signed_request_account = account - @signed_request_account + def verify_signature(actor, signature, compare_signed_string) + if actor.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string) + @signed_request_actor = actor + @signed_request_actor end rescue OpenSSL::PKey::RSAError nil @@ -207,7 +234,7 @@ module SignatureVerification signature_params['keyId'].blank? || signature_params['signature'].blank? end - def account_from_key_id(key_id) + def actor_from_key_id(key_id) domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id if domain_not_allowed?(domain) @@ -216,27 +243,34 @@ module SignatureVerification end if key_id.start_with?('acct:') - stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) } + stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), suppress_errors: false) } elsif !ActivityPub::TagManager.instance.local_uri?(key_id) - account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account) - account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) } + 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 end - rescue Mastodon::HostValidationError - nil + rescue Mastodon::PrivateNetworkAddressError => e + raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})" + rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e + raise SignatureVerificationError, e.message end def stoplight_wrap_request(&block) Stoplight("source:#{request.remote_ip}", &block) - .with_fallback { nil } .with_threshold(1) .with_cool_off_time(5.minutes.seconds) .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) } .run end - def account_refresh_key(account) - return if account.local? || !account.activitypub? - ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true) + def actor_refresh_key!(actor) + return if actor.local? || !actor.activitypub? + return actor.refresh! if actor.respond_to?(:refresh!) && actor.possibly_stale? + + ActivityPub::FetchRemoteActorService.new.call(actor.uri, only_key: true, suppress_errors: false) + rescue Mastodon::PrivateNetworkAddressError => e + raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})" + rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, Webfinger::Error => e + raise SignatureVerificationError, e.message end end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index f898994ac..0d9e624ef 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -4,7 +4,7 @@ class FollowerAccountsController < ApplicationController include AccountControllerConcern include SignatureVerification - before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers skip_around_action :set_locale, if: -> { request.format == :json } diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 11c6b6d50..68e1a79be 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -4,7 +4,7 @@ class FollowingAccountsController < ApplicationController include AccountControllerConcern include SignatureVerification - before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers skip_around_action :set_locale, if: -> { request.format == :json } diff --git a/app/controllers/privacy_controller.rb b/app/controllers/privacy_controller.rb new file mode 100644 index 000000000..ced84dbe5 --- /dev/null +++ b/app/controllers/privacy_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class PrivacyController < ApplicationController + layout 'public' + + before_action :set_instance_presenter + before_action :set_expires_in + + skip_before_action :require_functional! + + def show; end + + private + + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end + + def set_expires_in + expires_in 0, public: true + end +end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 3812f541e..55cc3790f 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -8,7 +8,7 @@ class StatusesController < ApplicationController layout 'public' - before_action :require_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } + before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_status before_action :set_instance_presenter before_action :set_link_headers diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 46821a200..315eabb3d 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -8,7 +8,7 @@ class TagsController < ApplicationController layout 'public' - before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :authenticate_user!, if: :whitelist_mode? before_action :set_local before_action :set_tag |