Skip to content

Commit

Permalink
Merge commit '5b291fcbe41564264954618fb1f4086a3be1c600' into glitch-s…
Browse files Browse the repository at this point in the history
…oc/merge-upstream

Conflicts:
- `app/validators/poll_options_validator.rb`:
  Upstream split `PollValidator` in two, and glitch-soc had local changes to
  make the options configurable.
  Refactored as upstream did, keeping glitch-soc's configurable limits.
  • Loading branch information
ClearlyClaire committed Jan 28, 2025
2 parents e736363 + 5b291fc commit f5262f5
Show file tree
Hide file tree
Showing 25 changed files with 213 additions and 134 deletions.
8 changes: 4 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ GEM
marcel (~> 1.0.1)
mime-types
terrapin (>= 0.6.0, < 2.0)
language_server-protocol (3.17.0.3)
language_server-protocol (3.17.0.4)
launchy (3.0.1)
addressable (~> 2.8)
childprocess (~> 5.0)
Expand Down Expand Up @@ -716,7 +716,7 @@ GEM
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 8)
rspec-support (3.13.2)
rubocop (1.70.0)
rubocop (1.71.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
Expand All @@ -726,14 +726,14 @@ GEM
rubocop-ast (>= 1.36.2, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.37.0)
rubocop-ast (1.38.0)
parser (>= 3.3.1.0)
rubocop-capybara (2.21.0)
rubocop (~> 1.41)
rubocop-performance (1.23.1)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.28.0)
rubocop-rails (2.29.1)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0)
Expand Down
8 changes: 4 additions & 4 deletions app/controllers/concerns/signature_verification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def signed_headers

def verify_signature_strength!
raise SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)')
raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(Request::REQUEST_TARGET) || signed_headers.include?('digest')
raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(HttpSignatureDraft::REQUEST_TARGET) || signed_headers.include?('digest')
raise SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host')
raise SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest')
end
Expand Down Expand Up @@ -155,14 +155,14 @@ def verify_signature(actor, signature, compare_signed_string)
def build_signed_string(include_query_string: true)
signed_headers.map do |signed_header|
case signed_header
when Request::REQUEST_TARGET
when HttpSignatureDraft::REQUEST_TARGET
if include_query_string
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}"
"#{HttpSignatureDraft::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}"
"#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
end
when '(created)'
raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
Expand Down
9 changes: 9 additions & 0 deletions app/javascript/mastodon/locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@
"alert.unexpected.message": "Objevila se neočekávaná chyba.",
"alert.unexpected.title": "Jejda!",
"alt_text_badge.title": "Popisek",
"alt_text_modal.add_alt_text": "Přidat alt text",
"alt_text_modal.add_text_from_image": "Přidat text z obrázku",
"alt_text_modal.cancel": "Zrušit",
"alt_text_modal.change_thumbnail": "Změnit miniaturu",
"alt_text_modal.describe_for_people_with_hearing_impairments": "Popište to pro osoby se sluchovým postižením…",
"alt_text_modal.describe_for_people_with_visual_impairments": "Popište to pro osoby se zrakovým postižením…",
"alt_text_modal.done": "Hotovo",
"announcement.announcement": "Oznámení",
"annual_report.summary.archetype.booster": "Lovec obsahu",
"annual_report.summary.archetype.lurker": "Špión",
Expand Down Expand Up @@ -407,6 +414,8 @@
"ignore_notifications_modal.not_followers_title": "Ignorovat oznámení od lidí, kteří vás nesledují?",
"ignore_notifications_modal.not_following_title": "Ignorovat oznámení od lidí, které nesledujete?",
"ignore_notifications_modal.private_mentions_title": "Ignorovat oznámení z nevyžádaných soukromých zmínek?",
"info_button.label": "Nápověda",
"info_button.what_is_alt_text": "<h1>Co je to alt text?</h1> <p>Alt text poskytuje popis obrázků pro lidi se zrakovými postižením, špatným připojením něbo těm, kteří potřebují více kontextu.</p> <p>Můžete zlepšit přístupnost a porozumění napsáním jasného, stručného a objektivního alt textu.</p> <ul> <li>Zachyťte důležité prvky</li> <li>Shrňte text v obrázku</li> <li>Použijte pravidelnou větnou skladbu</li> <li>Vyhněte se nadbytečným informacím</li> <li>U komplexních vizualizací (diagramy, mapy...) se zaměřte na trendy a klíčová zjištění</li> </ul>",
"interaction_modal.action.favourite": "Chcete-li pokračovat, musíte oblíbit z vašeho účtu.",
"interaction_modal.action.follow": "Chcete-li pokračovat, musíte sledovat z vašeho účtu.",
"interaction_modal.action.reblog": "Chcete-li pokračovat, musíte dát boost z vašeho účtu.",
Expand Down
4 changes: 2 additions & 2 deletions app/javascript/mastodon/locales/es-MX.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"account.enable_notifications": "Notificarme cuando @{name} publique algo",
"account.endorse": "Destacar en mi perfil",
"account.featured_tags.last_status_at": "Última publicación el {date}",
"account.featured_tags.last_status_never": "No hay publicaciones",
"account.featured_tags.last_status_never": "Sin publicaciones",
"account.featured_tags.title": "Etiquetas destacadas de {name}",
"account.follow": "Seguir",
"account.follow_back": "Seguir también",
Expand Down Expand Up @@ -146,7 +146,7 @@
"column.about": "Acerca de",
"column.blocks": "Usuarios bloqueados",
"column.bookmarks": "Marcadores",
"column.community": "Línea de tiempo local",
"column.community": "Cronología local",
"column.create_list": "Crear lista",
"column.direct": "Menciones privadas",
"column.directory": "Buscar perfiles",
Expand Down
1 change: 1 addition & 0 deletions app/javascript/mastodon/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@
"ignore_notifications_modal.not_followers_title": "나를 팔로우하지 않는 사람들의 알림을 무시할까요?",
"ignore_notifications_modal.not_following_title": "내가 팔로우하지 않는 사람들의 알림을 무시할까요?",
"ignore_notifications_modal.private_mentions_title": "요청하지 않은 개인 멘션 알림을 무시할까요?",
"info_button.label": "도움말",
"interaction_modal.action.favourite": "계속하려면 내 계정으로 즐겨찾기해야 합니다.",
"interaction_modal.action.follow": "계속하려면 내 계정으로 팔로우해야 합니다.",
"interaction_modal.action.reblog": "계속하려면 내 계정으로 리블로그해야 합니다.",
Expand Down
31 changes: 31 additions & 0 deletions app/lib/http_signature_draft.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

# This implements an older draft of HTTP Signatures:
# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures

class HttpSignatureDraft
REQUEST_TARGET = '(request-target)'

def initialize(keypair, key_id, full_path: true)
@keypair = keypair
@key_id = key_id
@full_path = full_path
end

def request_target(verb, url)
if url.query.nil? || !@full_path
"#{verb} #{url.path}"
else
"#{verb} #{url.path}?#{url.query}"
end
end

def sign(signed_headers, verb, url)
signed_headers = signed_headers.merge(REQUEST_TARGET => request_target(verb, url))
signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
algorithm = 'rsa-sha256'
signature = Base64.strict_encode64(@keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))

"keyId=\"#{@key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""
end
end
58 changes: 30 additions & 28 deletions app/lib/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ def readpartial(size, buffer = nil)
end

class Request
REQUEST_TARGET = '(request-target)'

# We enforce a 5s timeout on DNS resolving, 5s timeout on socket opening
# and 5s timeout on the TLS handshake, meaning the worst case should take
# about 15s in total
Expand All @@ -78,11 +76,18 @@ def initialize(verb, url, **options)
@http_client = options.delete(:http_client)
@allow_local = options.delete(:allow_local)
@full_path = !options.delete(:omit_query_string)
@options = options.merge(socket_class: use_proxy? || @allow_local ? ProxySocket : Socket)
@options = @options.merge(timeout_class: PerOperationWithDeadline, timeout_options: TIMEOUT)
@options = {
follow: {
max_hops: 3,
on_redirect: ->(response, request) { re_sign_on_redirect(response, request) },
},
socket_class: use_proxy? || @allow_local ? ProxySocket : Socket,
}.merge(options)
@options = @options.merge(proxy_url) if use_proxy?
@headers = {}

@signing = nil

raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if block_hidden_service?

set_common_headers!
Expand All @@ -92,8 +97,9 @@ def initialize(verb, url, **options)
def on_behalf_of(actor, sign_with: nil)
raise ArgumentError, 'actor must not be nil' if actor.nil?

@actor = actor
@keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @actor.keypair
key_id = ActivityPub::TagManager.instance.key_uri_for(actor)
keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : actor.keypair
@signing = HttpSignatureDraft.new(keypair, key_id, full_path: @full_path)

self
end
Expand All @@ -119,7 +125,7 @@ def perform
end

def headers
(@actor ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
(@signing ? @headers.merge('Signature' => signature) : @headers)
end

class << self
Expand All @@ -134,14 +140,13 @@ def valid_url?(url)
end

def http_client
HTTP.use(:auto_inflate).follow(max_hops: 3)
HTTP.use(:auto_inflate)
end
end

private

def set_common_headers!
@headers[REQUEST_TARGET] = request_target
@headers['User-Agent'] = Mastodon::Version.user_agent
@headers['Host'] = @url.host
@headers['Date'] = Time.now.utc.httpdate
Expand All @@ -152,31 +157,28 @@ def set_digest!
@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
def signature
@signing.sign(@headers.without('User-Agent', 'Accept-Encoding'), @verb, @url)
end

def signature
algorithm = 'rsa-sha256'
signature = Base64.strict_encode64(@keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
def re_sign_on_redirect(_response, request)
# Delete existing signature if there is one, since it will be invalid
request.headers.delete('Signature')

"keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""
end
return unless @signing.present? && @verb == :get

def signed_string
signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
end
signed_headers = request.headers.to_h.slice(*@headers.keys)
unless @headers.keys.all? { |key| signed_headers.key?(key) }
# We have lost some headers in the process, so don't sign the new
# request, in order to avoid issuing a valid signature with fewer
# conditions than expected.

def signed_headers
@headers.without('User-Agent', 'Accept-Encoding')
end
Rails.logger.warn { "Some headers (#{@headers.keys - signed_headers.keys}) have been lost on redirect from {@uri} to #{request.uri}, this should not happen. Skipping signatures" }
return
end

def key_id
ActivityPub::TagManager.instance.key_uri_for(@actor)
signature_value = @signing.sign(signed_headers.without('User-Agent', 'Accept-Encoding'), @verb, Addressable::URI.parse(request.uri))
request.headers['Signature'] = signature_value
end

def http_client
Expand Down
3 changes: 2 additions & 1 deletion app/models/poll.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ class Poll < ApplicationRecord

validates :options, presence: true
validates :expires_at, presence: true, if: :local?
validates_with PollValidator, on: :create, if: :local?
validates_with PollOptionsValidator, if: :local?
validates_with PollExpirationValidator, if: -> { local? && expires_at_changed? }

before_validation :prepare_options, if: :local?
before_validation :prepare_votes_count
Expand Down
8 changes: 4 additions & 4 deletions app/serializers/rest/instance_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,10 @@ def configuration
},

polls: {
max_options: PollValidator::MAX_OPTIONS,
max_characters_per_option: PollValidator::MAX_OPTION_CHARS,
min_expiration: PollValidator::MIN_EXPIRATION,
max_expiration: PollValidator::MAX_EXPIRATION,
max_options: PollOptionsValidator::MAX_OPTIONS,
max_characters_per_option: PollOptionsValidator::MAX_OPTION_CHARS,
min_expiration: PollExpirationValidator::MIN_EXPIRATION,
max_expiration: PollExpirationValidator::MAX_EXPIRATION,
},

translation: {
Expand Down
8 changes: 4 additions & 4 deletions app/serializers/rest/v1/instance_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ def configuration
},

polls: {
max_options: PollValidator::MAX_OPTIONS,
max_characters_per_option: PollValidator::MAX_OPTION_CHARS,
min_expiration: PollValidator::MIN_EXPIRATION,
max_expiration: PollValidator::MAX_EXPIRATION,
max_options: PollOptionsValidator::MAX_OPTIONS,
max_characters_per_option: PollOptionsValidator::MAX_OPTION_CHARS,
min_expiration: PollExpirationValidator::MIN_EXPIRATION,
max_expiration: PollExpirationValidator::MAX_EXPIRATION,
},
}
end
Expand Down
13 changes: 13 additions & 0 deletions app/validators/poll_expiration_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

class PollExpirationValidator < ActiveModel::Validator
MAX_EXPIRATION = 1.month.freeze
MIN_EXPIRATION = 5.minutes.freeze

def validate(poll)
current_time = Time.now.utc

poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_long')) if poll.expires_at.nil? || poll.expires_at - current_time > MAX_EXPIRATION
poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_short')) if poll.expires_at.present? && (poll.expires_at - current_time).ceil < MIN_EXPIRATION
end
end
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
# frozen_string_literal: true

class PollValidator < ActiveModel::Validator
class PollOptionsValidator < ActiveModel::Validator
MAX_OPTIONS = (ENV['MAX_POLL_OPTIONS'] || 5).to_i
MAX_OPTION_CHARS = (ENV['MAX_POLL_OPTION_CHARS'] || 100).to_i
MAX_EXPIRATION = 1.month.freeze
MIN_EXPIRATION = 5.minutes.freeze

def validate(poll)
current_time = Time.now.utc

poll.errors.add(:options, I18n.t('polls.errors.too_few_options')) unless poll.options.size > 1
poll.errors.add(:options, I18n.t('polls.errors.too_many_options', max: MAX_OPTIONS)) if poll.options.size > MAX_OPTIONS
poll.errors.add(:options, I18n.t('polls.errors.over_character_limit', max: MAX_OPTION_CHARS)) if poll.options.any? { |option| option.mb_chars.grapheme_length > MAX_OPTION_CHARS }
poll.errors.add(:options, I18n.t('polls.errors.duplicate_options')) unless poll.options.uniq.size == poll.options.size
poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_long')) if poll.expires_at.nil? || poll.expires_at - current_time > MAX_EXPIRATION
poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_short')) if poll.expires_at.present? && (poll.expires_at - current_time).ceil < MIN_EXPIRATION
end
end
1 change: 1 addition & 0 deletions config/locales/cs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,7 @@ cs:
too_fast: Formulář byl odeslán příliš rychle, zkuste to znovu.
use_security_key: Použít bezpečnostní klíč
user_agreement_html: Přečetl jsem si a souhlasím s <a href="%{terms_of_service_path}" target="_blank">podmínkami služby</a> a <a href="%{privacy_policy_path}" target="_blank">ochranou osobních údajů</a>
user_privacy_agreement_html: Četl jsem a souhlasím se zásadami <a href="%{privacy_policy_path}" target="_blank">ochrany osobních údajů</a>
author_attribution:
example_title: Ukázkový text
hint_html: Píšete novinové články nebo blog mimo Mastodon? Kontrolujte, jak Vám bude připisováno autorství, když jsou sdíleny na Mastodonu.
Expand Down
3 changes: 2 additions & 1 deletion config/locales/ko.yml
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ ko:
deleted_account: 계정을 삭제했습니다
empty: 로그를 찾을 수 없습니다
filter_by_action: 동작 별 필터
filter_by_user: 사용자로 거르기
filter_by_user: 사용자 기준으로 필터
title: 감사 로그
unavailable_instance: "(도메인네임 사용불가)"
announcements:
Expand Down Expand Up @@ -1192,6 +1192,7 @@ ko:
too_fast: 너무 빠르게 양식이 제출되었습니다, 다시 시도하세요.
use_security_key: 보안 키 사용
user_agreement_html: <a href="%{terms_of_service_path}" target="_blank">이용 약관</a>과 <a href="%{privacy_policy_path}" target="_blank">개인정보처리방침</a>을 읽었고 동의합니다
user_privacy_agreement_html: <a href="%{privacy_policy_path}" target="_blank">개인정보처리방침</a>을 읽었고 동의합니다
author_attribution:
example_title: 예시 텍스트
hint_html: 마스토돈 밖에서 뉴스나 블로그 글을 쓰시나요? 마스토돈에 공유되었을 때 어떻게 표시될지를 제어하세요.
Expand Down
2 changes: 2 additions & 0 deletions config/locales/simple_form.cs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ cs:
simple_form:
hints:
account:
attribution_domains: Jeden na řádek. Chrání před falešným připisování autorství.
discoverable: Vaše veřejné příspěvky a profil mohou být zobrazeny nebo doporučeny v různých oblastech Mastodonu a váš profil může být navrhován ostatním uživatelům.
display_name: Vaše celé jméno nebo přezdívka.
fields: Vaše domovská stránka, zájmena, věk, cokoliv chcete.
Expand Down Expand Up @@ -155,6 +156,7 @@ cs:
url: Kam budou události odesílány
labels:
account:
attribution_domains: Webové stránky s povolením Vám připsat autorství
discoverable: Zobrazovat profil a příspěvky ve vyhledávacích algoritmech
fields:
name: Označení
Expand Down
2 changes: 1 addition & 1 deletion lib/devise/strategies/two_factor_ldap_authenticatable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def authenticate!
protected

def valid_params?
params[scope] && params[scope][:password].present?
params[scope].is_a?(Hash) && params[scope][:password].present?
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/devise/strategies/two_factor_pam_authenticatable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def authenticate!
protected

def valid_params?
params[scope].respond_to?(:[]) && params[scope][:password].present?
params[scope].is_a?(Hash) && params[scope][:password].present?
end
end
end
Expand Down
Loading

0 comments on commit f5262f5

Please sign in to comment.