diff --git a/.dependabot/config.yml b/.dependabot/config.yml deleted file mode 100644 index 06df775c2..000000000 --- a/.dependabot/config.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: 1 - -update_configs: - - package_manager: "ruby:bundler" - directory: "/" - update_schedule: "weekly" - # Supported update schedule: live daily weekly monthly - version_requirement_updates: "auto" - # Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary - allowed_updates: - - match: - dependency_type: "all" - # Supported dependency types: all indirect direct production development - update_type: "all" - # Supported update types: all security - - - package_manager: "javascript" - directory: "/" - update_schedule: "weekly" - # Supported update schedule: live daily weekly monthly - version_requirement_updates: "auto" - # Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary - allowed_updates: - - match: - dependency_type: "all" - # Supported dependency types: all indirect direct production development - update_type: "all" - # Supported update types: all security diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..6b47350a4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: npm + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 99 + allow: + - dependency-type: all + + - package-ecosystem: bundler + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 99 + allow: + - dependency-type: all diff --git a/Gemfile b/Gemfile index 01e35caa7..fb760059e 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ gem 'makara', '~> 0.4' gem 'pghero', '~> 2.5' gem 'dotenv-rails', '~> 2.7' -gem 'aws-sdk-s3', '~> 1.66', require: false +gem 'aws-sdk-s3', '~> 1.67', require: false gem 'fog-core', '<= 2.1.0' gem 'fog-openstack', '~> 0.3', require: false gem 'paperclip', '~> 6.0' @@ -50,6 +50,7 @@ gem 'omniauth', '~> 1.9' gem 'discard', '~> 1.2' gem 'doorkeeper', '~> 5.4' +gem 'ed25519', '~> 1.2' gem 'fast_blank', '~> 1.0' gem 'fastimage' gem 'goldfinger', '~> 2.1' @@ -83,7 +84,7 @@ gem 'redis', '~> 4.1', require: ['redis', 'redis/connection/hiredis'] gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'rqrcode', '~> 1.1' gem 'ruby-progressbar', '~> 1.10' -gem 'sanitize', '~> 5.1' +gem 'sanitize', '~> 5.2' gem 'sidekiq', '~> 6.0' gem 'sidekiq-scheduler', '~> 3.0' gem 'sidekiq-unique-jobs', '~> 6.0' @@ -93,7 +94,6 @@ gem 'simple_form', '~> 5.0' gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'stoplight', '~> 2.2.0' gem 'strong_migrations', '~> 0.6' -gem 'tty-command', '~> 0.9', require: false gem 'tty-prompt', '~> 0.21', require: false gem 'twitter-text', '~> 1.14' gem 'tzinfo-data', '~> 1.2020' @@ -122,7 +122,7 @@ end group :test do gem 'capybara', '~> 3.32' gem 'climate_control', '~> 0.2' - gem 'faker', '~> 2.11' + gem 'faker', '~> 2.12' gem 'microformats', '~> 4.2' gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.0' @@ -141,7 +141,7 @@ group :development do gem 'letter_opener', '~> 1.7' gem 'letter_opener_web', '~> 1.4' gem 'memory_profiler' - gem 'rubocop', '~> 0.84', require: false + gem 'rubocop', '~> 0.85', require: false gem 'rubocop-rails', '~> 2.5', require: false gem 'brakeman', '~> 4.8', require: false gem 'bundler-audit', '~> 0.6', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 111bdabcd..e4900dc68 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -92,20 +92,20 @@ GEM av (0.9.0) cocaine (~> 0.5.3) aws-eventstream (1.1.0) - aws-partitions (1.320.0) - aws-sdk-core (3.96.1) + aws-partitions (1.326.0) + aws-sdk-core (3.98.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.31.0) + aws-sdk-kms (1.33.0) aws-sdk-core (~> 3, >= 3.71.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.66.0) + aws-sdk-s3 (1.67.1) aws-sdk-core (~> 3, >= 3.96.1) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) - aws-sigv4 (1.1.3) + aws-sigv4 (1.1.4) aws-eventstream (~> 1.0, >= 1.0.2) bcrypt (3.1.13) better_errors (2.7.1) @@ -119,7 +119,7 @@ GEM bootsnap (1.4.6) msgpack (~> 1.0) brakeman (4.8.2) - browser (4.1.0) + browser (4.2.0) builder (3.2.4) bullet (6.1.0) activesupport (>= 3.0.0) @@ -164,9 +164,9 @@ GEM climate_control (0.2.0) cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) - coderay (1.1.2) + coderay (1.1.3) concurrent-ruby (1.1.6) - connection_pool (2.2.2) + connection_pool (2.2.3) crack (0.4.3) safe_yaml (~> 1.0.0) crass (1.0.6) @@ -201,6 +201,7 @@ GEM dotenv (= 2.7.5) railties (>= 3.2, < 6.1) e2mmap (0.1.0) + ed25519 (1.2.4) elasticsearch (7.7.0) elasticsearch-api (= 7.7.0) elasticsearch-transport (= 7.7.0) @@ -217,7 +218,7 @@ GEM tzinfo excon (0.73.0) fabrication (2.21.1) - faker (2.11.0) + faker (2.12.0) i18n (>= 1.6, < 2) faraday (1.0.1) multipart-post (>= 1.2, < 3) @@ -235,14 +236,14 @@ GEM fog-json (1.2.0) fog-core multi_json (~> 1.10) - fog-openstack (0.3.7) + fog-openstack (0.3.10) fog-core (>= 1.45, <= 2.1.0) fog-json (>= 1.0) ipaddress (>= 0.8) formatador (0.2.5) - fugit (1.3.5) + fugit (1.3.6) et-orbi (~> 1.1, >= 1.1.8) - raabro (~> 1.1) + raabro (~> 1.3) fuubar (2.5.0) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) @@ -284,7 +285,7 @@ GEM httplog (1.4.2) rack (>= 1.0) rainbow (>= 2.0.0) - i18n (1.8.2) + i18n (1.8.3) concurrent-ruby (~> 1.0) i18n-tasks (0.9.31) activesupport (>= 4.0.2) @@ -309,7 +310,7 @@ GEM multi_json (~> 1.14) rack (~> 2.0) rdf (~> 3.1) - json-ld-preloaded (3.1.2) + json-ld-preloaded (3.1.3) json-ld (~> 3.1) rdf (~> 3.1) jsonapi-renderer (0.2.2) @@ -406,7 +407,7 @@ GEM parallel (1.19.1) parallel_tests (2.32.0) parallel - parser (2.7.1.2) + parser (2.7.1.3) ast (~> 2.4.0) parslet (2.0.0) pastel (0.7.4) @@ -484,7 +485,7 @@ GEM thor (>= 0.19.0, < 2.0) rainbow (3.0.0) rake (13.0.1) - rdf (3.1.1) + rdf (3.1.2) hamster (~> 3.0) link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.4.0) @@ -509,10 +510,10 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.8.2) redis (>= 4, < 5) - regexp_parser (1.7.0) + regexp_parser (1.7.1) request_store (1.5.0) rack (>= 1.4) - responders (3.0.0) + responders (3.0.1) actionpack (>= 5.0) railties (>= 5.0) rexml (3.2.4) @@ -544,10 +545,11 @@ GEM rspec-support (3.9.3) rspec_junit_formatter (0.4.1) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (0.84.0) + rubocop (0.85.1) parallel (~> 1.10) parser (>= 2.7.0.1) rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.7) rexml rubocop-ast (>= 0.0.3) ruby-progressbar (~> 1.7) @@ -564,7 +566,7 @@ GEM rufus-scheduler (3.6.0) fugit (~> 1.1, >= 1.1.6) safe_yaml (1.0.5) - sanitize (5.1.0) + sanitize (5.2.0) crass (~> 1.0.2) nokogiri (>= 1.8.0) nokogumbo (~> 2.0) @@ -623,8 +625,6 @@ GEM thwait (0.1.0) tilt (2.0.10) tty-color (0.5.1) - tty-command (0.9.0) - pastel (~> 0.7.0) tty-cursor (0.7.1) tty-prompt (0.21.0) necromancer (~> 0.5.0) @@ -634,7 +634,7 @@ GEM tty-cursor (~> 0.7) tty-screen (~> 0.7) wisper (~> 2.0.0) - tty-screen (0.7.1) + tty-screen (0.8.0) twitter-text (1.14.7) unf (~> 0.1.0) tzinfo (1.2.7) @@ -662,7 +662,7 @@ GEM jwt (~> 2.0) websocket-driver (0.7.2) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.4) + websocket-extensions (0.1.5) wisper (2.0.1) xpath (3.2.0) nokogiri (~> 1.8) @@ -675,7 +675,7 @@ DEPENDENCIES active_record_query_trace (~> 1.7) addressable (~> 2.7) annotate (~> 3.1) - aws-sdk-s3 (~> 1.66) + aws-sdk-s3 (~> 1.67) better_errors (~> 2.7) binding_of_caller (~> 0.7) blurhash (~> 0.1) @@ -702,8 +702,9 @@ DEPENDENCIES doorkeeper (~> 5.4) dotenv-rails (~> 2.7) e2mmap (~> 0.1.0) + ed25519 (~> 1.2) fabrication (~> 2.21) - faker (~> 2.11) + faker (~> 2.12) fast_blank (~> 1.0) fastimage fog-core (<= 2.1.0) @@ -773,10 +774,10 @@ DEPENDENCIES rspec-rails (~> 4.0) rspec-sidekiq (~> 3.0) rspec_junit_formatter (~> 0.4) - rubocop (~> 0.84) + rubocop (~> 0.85) rubocop-rails (~> 2.5) ruby-progressbar (~> 1.10) - sanitize (~> 5.1) + sanitize (~> 5.2) sidekiq (~> 6.0) sidekiq-bulk (~> 0.2.0) sidekiq-scheduler (~> 3.0) @@ -792,7 +793,6 @@ DEPENDENCIES strong_migrations (~> 0.6) thor (~> 0.20) thwait (~> 0.1.0) - tty-command (~> 0.9) tty-prompt (~> 0.21) twitter-text (~> 1.14) tzinfo-data (~> 1.2020) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..7625597fe --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,12 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 3.1.x | :white_check_mark: | +| < 3.1 | :x: | + +## Reporting a Vulnerability + +hello@joinmastodon.org diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 76703ed05..b8bca580f 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true class AccountsController < ApplicationController - PAGE_SIZE = 20 + PAGE_SIZE = 20 + PAGE_SIZE_MAX = 200 include AccountControllerConcern include SignatureAuthentication @@ -41,7 +42,8 @@ class AccountsController < ApplicationController format.rss do expires_in 1.minute, public: true - @statuses = filtered_statuses.without_reblogs.limit(PAGE_SIZE) + limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE + @statuses = filtered_statuses.without_reblogs.limit(limit) @statuses = cache_collection(@statuses, Status) render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag]) end diff --git a/app/controllers/activitypub/claims_controller.rb b/app/controllers/activitypub/claims_controller.rb new file mode 100644 index 000000000..08ad952df --- /dev/null +++ b/app/controllers/activitypub/claims_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class ActivityPub::ClaimsController < ActivityPub::BaseController + include SignatureVerification + include AccountOwnedConcern + + skip_before_action :authenticate_user! + + before_action :require_signature! + before_action :set_claim_result + + def create + render json: @claim_result, serializer: ActivityPub::OneTimeKeySerializer + end + + private + + def set_claim_result + @claim_result = ::Keys::ClaimService.new.call(@account.id, params[:id]) + end +end diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index 9ca216e4f..e62fba748 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -5,8 +5,9 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController include AccountOwnedConcern before_action :require_signature!, if: :authorized_fetch_mode? + before_action :set_items before_action :set_size - before_action :set_statuses + before_action :set_type before_action :set_cache_headers def show @@ -16,40 +17,53 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController private - def set_statuses - @statuses = scope_for_collection - @statuses = cache_collection(@statuses, Status) - end - - def set_size + def set_items case params[:id] when 'featured' - @size = @account.pinned_statuses.not_local_only.count + @items = begin + # Because in public fetch mode we cache the response, there would be no + # benefit from performing the check below, since a blocked account or domain + # would likely be served the cache from the reverse proxy anyway + + if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain))) + [] + else + cache_collection(@account.pinned_statuses.not_local_only, Status) + end + end + when 'devices' + @items = @account.devices else not_found end end - def scope_for_collection + def set_size + case params[:id] + when 'featured', 'devices' + @size = @items.size + else + not_found + end + end + + def set_type case params[:id] when 'featured' - # Because in public fetch mode we cache the response, there would be no - # benefit from performing the check below, since a blocked account or domain - # would likely be served the cache from the reverse proxy anyway - if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain))) - Status.none - else - @account.pinned_statuses.not_local_only - end + @type = :ordered + when 'devices' + @type = :unordered + else + not_found end end def collection_presenter ActivityPub::CollectionPresenter.new( id: account_collection_url(@account, params[:id]), - type: :ordered, + type: @type, size: @size, - items: @statuses + items: @items ) end end diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index efa8f2950..71efb543e 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -33,6 +33,8 @@ module Admin @form.save rescue ActionController::ParameterMissing flash[:alert] = I18n.t('admin.accounts.no_account_selected') + rescue Mastodon::NotPermittedError + flash[:alert] = I18n.t('admin.custom_emojis.not_permitted') ensure redirect_to admin_custom_emojis_path(filter_params) end diff --git a/app/controllers/api/v1/crypto/deliveries_controller.rb b/app/controllers/api/v1/crypto/deliveries_controller.rb new file mode 100644 index 000000000..aa9df6e03 --- /dev/null +++ b/app/controllers/api/v1/crypto/deliveries_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class Api::V1::Crypto::DeliveriesController < Api::BaseController + before_action -> { doorkeeper_authorize! :crypto } + before_action :require_user! + before_action :set_current_device + + def create + devices.each do |device_params| + DeliverToDeviceService.new.call(current_account, @current_device, device_params) + end + + render_empty + end + + private + + def set_current_device + @current_device = Device.find_by!(access_token: doorkeeper_token) + end + + def resource_params + params.require(:device) + params.permit(device: [:account_id, :device_id, :type, :body, :hmac]) + end + + def devices + Array(resource_params[:device]) + end +end diff --git a/app/controllers/api/v1/crypto/encrypted_messages_controller.rb b/app/controllers/api/v1/crypto/encrypted_messages_controller.rb new file mode 100644 index 000000000..c764915e5 --- /dev/null +++ b/app/controllers/api/v1/crypto/encrypted_messages_controller.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController + LIMIT = 80 + + before_action -> { doorkeeper_authorize! :crypto } + before_action :require_user! + before_action :set_current_device + + before_action :set_encrypted_messages, only: :index + after_action :insert_pagination_headers, only: :index + + def index + render json: @encrypted_messages, each_serializer: REST::EncryptedMessageSerializer + end + + def clear + @current_device.encrypted_messages.up_to(params[:up_to_id]).delete_all + render_empty + end + + private + + def set_current_device + @current_device = Device.find_by!(access_token: doorkeeper_token) + end + + def set_encrypted_messages + @encrypted_messages = @current_device.encrypted_messages.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_crypto_encrypted_messages_url pagination_params(max_id: pagination_max_id) if records_continue? + end + + def prev_path + api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty? + end + + def pagination_max_id + @encrypted_messages.last.id + end + + def pagination_since_id + @encrypted_messages.first.id + end + + def records_continue? + @encrypted_messages.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end +end diff --git a/app/controllers/api/v1/crypto/keys/claims_controller.rb b/app/controllers/api/v1/crypto/keys/claims_controller.rb new file mode 100644 index 000000000..34b21a380 --- /dev/null +++ b/app/controllers/api/v1/crypto/keys/claims_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Api::V1::Crypto::Keys::ClaimsController < Api::BaseController + before_action -> { doorkeeper_authorize! :crypto } + before_action :require_user! + before_action :set_claim_results + + def create + render json: @claim_results, each_serializer: REST::Keys::ClaimResultSerializer + end + + private + + def set_claim_results + @claim_results = devices.map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) }.compact + end + + def resource_params + params.permit(device: [:account_id, :device_id]) + end + + def devices + Array(resource_params[:device]) + end +end diff --git a/app/controllers/api/v1/crypto/keys/counts_controller.rb b/app/controllers/api/v1/crypto/keys/counts_controller.rb new file mode 100644 index 000000000..ffd7151b7 --- /dev/null +++ b/app/controllers/api/v1/crypto/keys/counts_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Api::V1::Crypto::Keys::CountsController < Api::BaseController + before_action -> { doorkeeper_authorize! :crypto } + before_action :require_user! + before_action :set_current_device + + def show + render json: { one_time_keys: @current_device.one_time_keys.count } + end + + private + + def set_current_device + @current_device = Device.find_by!(access_token: doorkeeper_token) + end +end diff --git a/app/controllers/api/v1/crypto/keys/queries_controller.rb b/app/controllers/api/v1/crypto/keys/queries_controller.rb new file mode 100644 index 000000000..0851d797d --- /dev/null +++ b/app/controllers/api/v1/crypto/keys/queries_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class Api::V1::Crypto::Keys::QueriesController < Api::BaseController + before_action -> { doorkeeper_authorize! :crypto } + before_action :require_user! + before_action :set_accounts + before_action :set_query_results + + def create + render json: @query_results, each_serializer: REST::Keys::QueryResultSerializer + end + + private + + def set_accounts + @accounts = Account.where(id: account_ids).includes(:devices) + end + + def set_query_results + @query_results = @accounts.map { |account| ::Keys::QueryService.new.call(account) }.compact + end + + def account_ids + Array(params[:id]).map(&:to_i) + end +end diff --git a/app/controllers/api/v1/crypto/keys/uploads_controller.rb b/app/controllers/api/v1/crypto/keys/uploads_controller.rb new file mode 100644 index 000000000..fc4abf63b --- /dev/null +++ b/app/controllers/api/v1/crypto/keys/uploads_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class Api::V1::Crypto::Keys::UploadsController < Api::BaseController + before_action -> { doorkeeper_authorize! :crypto } + before_action :require_user! + + def create + device = Device.find_or_initialize_by(access_token: doorkeeper_token) + + device.transaction do + device.account = current_account + device.update!(resource_params[:device]) + + if resource_params[:one_time_keys].present? && resource_params[:one_time_keys].is_a?(Enumerable) + resource_params[:one_time_keys].each do |one_time_key_params| + device.one_time_keys.create!(one_time_key_params) + end + end + end + + render json: device, serializer: REST::Keys::DeviceSerializer + end + + private + + def resource_params + params.permit(device: [:device_id, :name, :fingerprint_key, :identity_key], one_time_keys: [:key_id, :key, :signature]) + end +end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index c36561b86..c54f6643a 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -9,7 +9,9 @@ class Auth::SessionsController < Devise::SessionsController skip_before_action :require_functional! prepend_before_action :set_pack - prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create] + + include TwoFactorAuthenticationConcern + include SignInTokenAuthenticationConcern before_action :set_instance_presenter, only: [:new] before_action :set_body_classes @@ -40,8 +42,8 @@ class Auth::SessionsController < Devise::SessionsController protected def find_user - if session[:otp_user_id] - User.find(session[:otp_user_id]) + if session[:attempt_user_id] + User.find(session[:attempt_user_id]) else user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication @@ -50,7 +52,7 @@ class Auth::SessionsController < Devise::SessionsController end def user_params - params.require(:user).permit(:email, :password, :otp_attempt) + params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt) end def after_sign_in_path_for(resource) @@ -71,48 +73,6 @@ class Auth::SessionsController < Devise::SessionsController super end - def two_factor_enabled? - find_user&.otp_required_for_login? - end - - def valid_otp_attempt?(user) - user.validate_and_consume_otp!(user_params[:otp_attempt]) || - user.invalidate_otp_backup_code!(user_params[:otp_attempt]) - rescue OpenSSL::Cipher::CipherError - false - end - - def authenticate_with_two_factor - user = self.resource = find_user - - if user_params[:otp_attempt].present? && session[:otp_user_id] - authenticate_with_two_factor_via_otp(user) - elsif user.present? && (user.encrypted_password.blank? || user.valid_password?(user_params[:password])) - # If encrypted_password is blank, we got the user from LDAP or PAM, - # so credentials are already valid - - prompt_for_two_factor(user) - end - end - - def authenticate_with_two_factor_via_otp(user) - if valid_otp_attempt?(user) - session.delete(:otp_user_id) - remember_me(user) - sign_in(user) - else - flash.now[:alert] = I18n.t('users.invalid_otp_token') - prompt_for_two_factor(user) - end - end - - def prompt_for_two_factor(user) - session[:otp_user_id] = user.id - use_pack 'auth' - @body_classes = 'lighter' - render :two_factor - end - def require_no_authentication super # Delete flash message that isn't entirely useful and may be confusing in diff --git a/app/controllers/concerns/sign_in_token_authentication_concern.rb b/app/controllers/concerns/sign_in_token_authentication_concern.rb new file mode 100644 index 000000000..88c009b19 --- /dev/null +++ b/app/controllers/concerns/sign_in_token_authentication_concern.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module SignInTokenAuthenticationConcern + extend ActiveSupport::Concern + + included do + prepend_before_action :authenticate_with_sign_in_token, if: :sign_in_token_required?, only: [:create] + end + + def sign_in_token_required? + find_user&.suspicious_sign_in?(request.remote_ip) + end + + def valid_sign_in_token_attempt?(user) + Devise.secure_compare(user.sign_in_token, user_params[:sign_in_token_attempt]) + end + + def authenticate_with_sign_in_token + user = self.resource = find_user + + if user_params[:sign_in_token_attempt].present? && session[:attempt_user_id] + authenticate_with_sign_in_token_attempt(user) + elsif user.present? && user.external_or_valid_password?(user_params[:password]) + prompt_for_sign_in_token(user) + end + end + + def authenticate_with_sign_in_token_attempt(user) + if valid_sign_in_token_attempt?(user) + session.delete(:attempt_user_id) + remember_me(user) + sign_in(user) + else + flash.now[:alert] = I18n.t('users.invalid_sign_in_token') + prompt_for_sign_in_token(user) + end + end + + def prompt_for_sign_in_token(user) + if user.sign_in_token_expired? + user.generate_sign_in_token && user.save + UserMailer.sign_in_token(user, request.remote_ip, request.user_agent, Time.now.utc.to_s).deliver_later! + end + + session[:attempt_user_id] = user.id + use_pack 'auth' + @body_classes = 'lighter' + render :sign_in_token + end +end diff --git a/app/controllers/concerns/two_factor_authentication_concern.rb b/app/controllers/concerns/two_factor_authentication_concern.rb new file mode 100644 index 000000000..0d9f87455 --- /dev/null +++ b/app/controllers/concerns/two_factor_authentication_concern.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module TwoFactorAuthenticationConcern + extend ActiveSupport::Concern + + included do + prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create] + end + + def two_factor_enabled? + find_user&.otp_required_for_login? + end + + def valid_otp_attempt?(user) + user.validate_and_consume_otp!(user_params[:otp_attempt]) || + user.invalidate_otp_backup_code!(user_params[:otp_attempt]) + rescue OpenSSL::Cipher::CipherError + false + end + + def authenticate_with_two_factor + user = self.resource = find_user + + if user_params[:otp_attempt].present? && session[:attempt_user_id] + authenticate_with_two_factor_attempt(user) + elsif user.present? && user.external_or_valid_password?(user_params[:password]) + prompt_for_two_factor(user) + end + end + + def authenticate_with_two_factor_attempt(user) + if valid_otp_attempt?(user) + session.delete(:attempt_user_id) + remember_me(user) + sign_in(user) + else + flash.now[:alert] = I18n.t('users.invalid_otp_token') + prompt_for_two_factor(user) + end + end + + def prompt_for_two_factor(user) + session[:attempt_user_id] = user.id + use_pack 'auth' + @body_classes = 'lighter' + render :two_factor + end +end diff --git a/app/controllers/settings/migration/redirects_controller.rb b/app/controllers/settings/migration/redirects_controller.rb index 6e5b72ffb..97193ade0 100644 --- a/app/controllers/settings/migration/redirects_controller.rb +++ b/app/controllers/settings/migration/redirects_controller.rb @@ -18,7 +18,7 @@ class Settings::Migration::RedirectsController < Settings::BaseController if @redirect.valid_with_challenge?(current_user) current_account.update!(moved_to_account: @redirect.target_account) ActivityPub::UpdateDistributionWorker.perform_async(current_account.id) - redirect_to settings_migration_path, notice: I18n.t('migrations.moved_msg', acct: current_account.moved_to_account.acct) + redirect_to settings_migration_path, notice: I18n.t('migrations.redirected_msg', acct: current_account.moved_to_account.acct) else render :new end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index a1b7f4320..b0abad984 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -44,7 +44,7 @@ class StatusesController < ApplicationController def activity expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? - render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter + render_with_cache json: ActivityPub::ActivityPresenter.from_status(@status), content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter end def embed diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 3d12c9eaf..2363cb31b 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -3,7 +3,8 @@ class TagsController < ApplicationController include SignatureVerification - PAGE_SIZE = 20 + PAGE_SIZE = 20 + PAGE_SIZE_MAX = 200 layout 'public' @@ -26,6 +27,7 @@ class TagsController < ApplicationController format.rss do expires_in 0, public: true + limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE @statuses = HashtagQueryService.new.call(@tag, filter_params, nil, @local).limit(PAGE_SIZE) @statuses = cache_collection(@statuses, Status) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 40f914f1e..2f11ccb6f 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -137,6 +137,11 @@ module ApplicationHelper text: [params[:title], params[:text], params[:url]].compact.join(' '), } + permit_visibilities = %w(public unlisted private direct) + default_privacy = current_account&.user&.setting_default_privacy + permit_visibilities.shift(permit_visibilities.index(default_privacy) + 1) if default_privacy.present? + state_params[:visibility] = params[:visibility] if permit_visibilities.include? params[:visibility] + if user_signed_in? state_params[:settings] = state_params[:settings].merge(Web::Setting.find_by(user: current_user)&.data || {}) state_params[:push_subscription] = current_account.user.web_push_subscription(current_session) diff --git a/app/helpers/webfinger_helper.rb b/app/helpers/webfinger_helper.rb index 70c493210..ab7ca4698 100644 --- a/app/helpers/webfinger_helper.rb +++ b/app/helpers/webfinger_helper.rb @@ -1,5 +1,16 @@ # frozen_string_literal: true +# Monkey-patch on monkey-patch. +# Because it conflicts with the request.rb patch. +class HTTP::Timeout::PerOperationOriginal < HTTP::Timeout::PerOperation + def connect(socket_class, host, port, nodelay = false) + ::Timeout.timeout(@connect_timeout, HTTP::TimeoutError) do + @socket = socket_class.open(host, port) + @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay + end + end +end + module WebfingerHelper def webfinger!(uri) hidden_service_uri = /\.(onion|i2p)(:\d+)?$/.match(uri) @@ -12,6 +23,14 @@ module WebfingerHelper headers: { 'User-Agent': Mastodon::Version.user_agent, }, + + timeout_class: HTTP::Timeout::PerOperationOriginal, + + timeout_options: { + write_timeout: 10, + connect_timeout: 5, + read_timeout: 10, + }, } Goldfinger::Client.new(uri, opts.merge(Rails.configuration.x.http_client_proxy)).finger diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index f7cbe4c1c..dca44917a 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -12,7 +12,7 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { export function searchTextFromRawStatus (status) { const spoilerText = status.spoiler_text || ''; - const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); + const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; } diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js index ac2a6366a..58ec4f6eb 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.js +++ b/app/javascript/mastodon/components/autosuggest_textarea.js @@ -208,7 +208,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { {placeholder}