Merge commit '9c80b16401e7606cbd7eb2bf3af32c18953d1d3f' into glitch-soc/merge-upstream

This commit is contained in:
Claire
2025-06-02 12:16:06 +02:00
26 changed files with 1070 additions and 560 deletions

View File

@@ -345,7 +345,7 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
name: e2e-screenshots name: e2e-screenshots-${{ matrix.ruby-version }}
path: tmp/capybara/ path: tmp/capybara/
test-search: test-search:

View File

@@ -39,15 +39,6 @@ Style/FetchEnvVar:
- 'config/initializers/paperclip.rb' - 'config/initializers/paperclip.rb'
- 'lib/tasks/repo.rake' - 'lib/tasks/repo.rake'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, Mode, AllowedMethods, AllowedPatterns.
# SupportedStyles: annotated, template, unannotated
# AllowedMethods: redirect
Style/FormatStringToken:
Exclude:
- 'config/initializers/devise.rb'
- 'lib/paperclip/color_extractor.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals.
Style/GuardClause: Style/GuardClause:

View File

@@ -168,7 +168,7 @@ GEM
crass (1.0.6) crass (1.0.6)
css_parser (1.21.1) css_parser (1.21.1)
addressable addressable
csv (3.3.4) csv (3.3.5)
database_cleaner-active_record (2.2.1) database_cleaner-active_record (2.2.1)
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0) database_cleaner-core (~> 2.0.0)
@@ -397,7 +397,7 @@ GEM
rexml rexml
link_header (0.0.8) link_header (0.0.8)
lint_roller (1.1.0) lint_roller (1.1.0)
linzer (0.7.2) linzer (0.7.3)
cgi (~> 0.4.2) cgi (~> 0.4.2)
forwardable (~> 1.3, >= 1.3.3) forwardable (~> 1.3, >= 1.3.3)
logger (~> 1.7, >= 1.7.0) logger (~> 1.7, >= 1.7.0)

View File

@@ -22,6 +22,18 @@ module SignatureVerification
request.headers['Signature'].present? request.headers['Signature'].present?
end end
def signature_key_id
signed_request.key_id
end
private
def signed_request
@signed_request ||= SignedRequest.new(request) if signed_request?
rescue SignatureVerificationError
nil
end
def signature_verification_failure_reason def signature_verification_failure_reason
@signature_verification_failure_reason @signature_verification_failure_reason
end end
@@ -30,12 +42,6 @@ module SignatureVerification
@signature_verification_failure_code || 401 @signature_verification_failure_code || 401
end end
def signature_key_id
signature_params['keyId']
rescue Mastodon::SignatureVerificationError
nil
end
def signed_request_account def signed_request_account
signed_request_actor.is_a?(Account) ? signed_request_actor : nil signed_request_actor.is_a?(Account) ? signed_request_actor : nil
end end
@@ -44,38 +50,20 @@ module SignatureVerification
return @signed_request_actor if defined?(@signed_request_actor) return @signed_request_actor if defined?(@signed_request_actor)
raise Mastodon::SignatureVerificationError, 'Request not signed' unless signed_request? raise Mastodon::SignatureVerificationError, 'Request not signed' unless signed_request?
raise Mastodon::SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters?
raise Mastodon::SignatureVerificationError, 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)' unless %w(rsa-sha256 hs2019).include?(signature_algorithm)
raise Mastodon::SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window?
verify_signature_strength! actor = actor_from_key_id
verify_body_digest!
actor = actor_from_key_id(signature_params['keyId']) raise Mastodon::SignatureVerificationError, "Public key not found for key #{signature_key_id}" if actor.nil?
raise Mastodon::SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil? return (@signed_request_actor = actor) if signed_request.verified?(actor)
signature = Base64.decode64(signature_params['signature'])
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_wrapper.run { actor_refresh_key!(actor) } actor = stoplight_wrapper.run { actor_refresh_key!(actor) }
raise Mastodon::SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil? raise Mastodon::SignatureVerificationError, "Could not refresh public key #{signature_key_id}" if actor.nil?
compare_signed_string = build_signed_string(include_query_string: true) return (@signed_request_actor = actor) if signed_request.verified?(actor)
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
# Compatibility quirk with older Mastodon versions fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri}"
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']
rescue Mastodon::SignatureVerificationError => e rescue Mastodon::SignatureVerificationError => e
fail_with! e.message fail_with! e.message
rescue *Mastodon::HTTP_CONNECTION_ERRORS => e rescue *Mastodon::HTTP_CONNECTION_ERRORS => e
@@ -86,12 +74,6 @@ module SignatureVerification
fail_with! 'Fetching attempt skipped because of recent connection failure' fail_with! 'Fetching attempt skipped because of recent connection failure'
end end
def request_body
@request_body ||= request.raw_post
end
private
def fail_with!(message, **options) def fail_with!(message, **options)
Rails.logger.debug { "Signature verification failed: #{message}" } Rails.logger.debug { "Signature verification failed: #{message}" }
@@ -99,123 +81,8 @@ module SignatureVerification
@signed_request_actor = nil @signed_request_actor = nil
end end
def signature_params def actor_from_key_id
@signature_params ||= SignatureParser.parse(request.headers['Signature']) key_id = signature_key_id
rescue SignatureParser::ParsingError
raise Mastodon::SignatureVerificationError, 'Error parsing signature parameters'
end
def signature_algorithm
signature_params.fetch('algorithm', 'hs2019')
end
def signed_headers
signature_params.fetch('headers', signature_algorithm == 'hs2019' ? '(created)' : 'date').downcase.split
end
def verify_signature_strength!
raise Mastodon::SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)')
raise Mastodon::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 Mastodon::SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host')
raise Mastodon::SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest')
end
def verify_body_digest!
return unless signed_headers.include?('digest')
raise Mastodon::SignatureVerificationError, 'Digest header missing' unless request.headers.key?('Digest')
digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] }
sha256 = digests.assoc('sha-256')
raise Mastodon::SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil?
return if body_digest == sha256[1]
digest_size = begin
Base64.strict_decode64(sha256[1].strip).length
rescue ArgumentError
raise Mastodon::SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a valid base64 string. Given digest: #{sha256[1]}"
end
raise Mastodon::SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a SHA-256 digest. Given digest: #{sha256[1]}" if digest_size != 32
raise Mastodon::SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}"
end
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
end
def build_signed_string(include_query_string: true)
signed_headers.map do |signed_header|
case signed_header
when HttpSignatureDraft::REQUEST_TARGET
if include_query_string
"#{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
"#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
end
when '(created)'
raise Mastodon::SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
raise Mastodon::SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?
"(created): #{signature_params['created']}"
when '(expires)'
raise Mastodon::SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019'
raise Mastodon::SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank?
"(expires): #{signature_params['expires']}"
else
"#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
end
end.join("\n")
end
def matches_time_window?
created_time = nil
expires_time = nil
begin
if signature_algorithm == 'hs2019' && signature_params['created'].present?
created_time = Time.at(signature_params['created'].to_i).utc
elsif request.headers['Date'].present?
created_time = Time.httpdate(request.headers['Date']).utc
end
expires_time = Time.at(signature_params['expires'].to_i).utc if signature_params['expires'].present?
rescue ArgumentError => e
raise Mastodon::SignatureVerificationError, "Invalid Date header: #{e.message}"
end
expires_time ||= created_time + 5.minutes unless created_time.nil?
expires_time = [expires_time, created_time + EXPIRATION_WINDOW_LIMIT].min unless created_time.nil?
return false if created_time.present? && created_time > Time.now.utc + CLOCK_SKEW_MARGIN
return false if expires_time.present? && Time.now.utc > expires_time + CLOCK_SKEW_MARGIN
true
end
def body_digest
@body_digest ||= Digest::SHA256.base64digest(request_body)
end
def to_header_name(name)
name.split('-').map(&:capitalize).join('-')
end
def missing_required_signature_parameters?
signature_params['keyId'].blank? || signature_params['signature'].blank?
end
def actor_from_key_id(key_id)
domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id
if domain_not_allowed?(domain) if domain_not_allowed?(domain)

View File

@@ -1,5 +1,5 @@
{ {
"about.blocks": "Gweinyddion a gyfyngir", "about.blocks": "Gweinyddion wedi'u cymedroli",
"about.contact": "Cysylltwch â:", "about.contact": "Cysylltwch â:",
"about.default_locale": "Rhagosodedig", "about.default_locale": "Rhagosodedig",
"about.disclaimer": "Mae Mastodon yn feddalwedd cod agored rhydd ac o dan hawlfraint Mastodon gGmbH.", "about.disclaimer": "Mae Mastodon yn feddalwedd cod agored rhydd ac o dan hawlfraint Mastodon gGmbH.",
@@ -42,7 +42,7 @@
"account.follow_back": "Dilyn nôl", "account.follow_back": "Dilyn nôl",
"account.followers": "Dilynwyr", "account.followers": "Dilynwyr",
"account.followers.empty": "Does neb yn dilyn y defnyddiwr hwn eto.", "account.followers.empty": "Does neb yn dilyn y defnyddiwr hwn eto.",
"account.followers_counter": "{count, plural, one {{counter} dilynwr} two {{counter} ddilynwr} other {{counter} dilynwyr}}", "account.followers_counter": "{count, plural, one {{counter} dilynwr} two {{counter} ddilynwr} other {{counter} dilynwr}}",
"account.followers_you_know_counter": "{counter} rydych chi'n adnabod", "account.followers_you_know_counter": "{counter} rydych chi'n adnabod",
"account.following": "Yn dilyn", "account.following": "Yn dilyn",
"account.following_counter": "{count, plural, one {Yn dilyn {counter}} other {Yn dilyn {counter} arall}}", "account.following_counter": "{count, plural, one {Yn dilyn {counter}} other {Yn dilyn {counter} arall}}",
@@ -288,8 +288,8 @@
"domain_pill.their_username": "Eu dynodwr unigryw ar eu gweinydd. Mae'n bosibl dod o hyd i ddefnyddwyr gyda'r un enw defnyddiwr ar wahanol weinyddion.", "domain_pill.their_username": "Eu dynodwr unigryw ar eu gweinydd. Mae'n bosibl dod o hyd i ddefnyddwyr gyda'r un enw defnyddiwr ar wahanol weinyddion.",
"domain_pill.username": "Enw Defnyddiwr", "domain_pill.username": "Enw Defnyddiwr",
"domain_pill.whats_in_a_handle": "Beth sydd mewn handlen?", "domain_pill.whats_in_a_handle": "Beth sydd mewn handlen?",
"domain_pill.who_they_are": "Gan fod handlen yn dweud pwy yw rhywun a ble maen nhw, gallwch chi ryngweithio â phobl ar draws gwe gymdeithasol <button>llwyfannau wedi'u pweru gan ActivityPub</button> .", "domain_pill.who_they_are": "Gan fod handlen yn dweud pwy yw rhywun a ble maen nhw, gallwch chi ryngweithio â phobl ar draws gwe gymdeithasol <button>llwyfannau wedi'u pweru gan ActivityPub</button>.",
"domain_pill.who_you_are": "Oherwydd bod eich handlen yn dweud pwy ydych chi a ble rydych chi, gall pobl ryngweithio â chi ar draws gwe gymdeithasol <button>llwyfannau wedi'u pweru gan ActivityPub</button> .", "domain_pill.who_you_are": "Oherwydd bod eich handlen yn dweud pwy ydych chi a ble rydych chi, gall pobl ryngweithio â chi ar draws gwe gymdeithasol <button>llwyfannau wedi'u pweru gan ActivityPub</button>.",
"domain_pill.your_handle": "Eich handlen:", "domain_pill.your_handle": "Eich handlen:",
"domain_pill.your_server": "Eich cartref digidol, lle mae'ch holl bostiadau'n byw. Ddim yn hoffi'r un hon? Trosglwyddwch weinyddion ar unrhyw adeg a dewch â'ch dilynwyr hefyd.", "domain_pill.your_server": "Eich cartref digidol, lle mae'ch holl bostiadau'n byw. Ddim yn hoffi'r un hon? Trosglwyddwch weinyddion ar unrhyw adeg a dewch â'ch dilynwyr hefyd.",
"domain_pill.your_username": "Eich dynodwr unigryw ar y gweinydd hwn. Mae'n bosibl dod o hyd i ddefnyddwyr gyda'r un enw defnyddiwr ar wahanol weinyddion.", "domain_pill.your_username": "Eich dynodwr unigryw ar y gweinydd hwn. Mae'n bosibl dod o hyd i ddefnyddwyr gyda'r un enw defnyddiwr ar wahanol weinyddion.",
@@ -684,9 +684,9 @@
"notifications.policy.filter_hint": "Anfon i flwch derbyn hysbysiadau wedi'u hidlo", "notifications.policy.filter_hint": "Anfon i flwch derbyn hysbysiadau wedi'u hidlo",
"notifications.policy.filter_limited_accounts_hint": "Cyfyngwyd gan gymedrolwyr gweinydd", "notifications.policy.filter_limited_accounts_hint": "Cyfyngwyd gan gymedrolwyr gweinydd",
"notifications.policy.filter_limited_accounts_title": "Cyfrifon wedi'u cymedroli", "notifications.policy.filter_limited_accounts_title": "Cyfrifon wedi'u cymedroli",
"notifications.policy.filter_new_accounts.hint": "Crëwyd o fewn {days, lluosog, un {yr un diwrnod} arall {y # diwrnod}} diwethaf", "notifications.policy.filter_new_accounts.hint": "Crëwyd o fewn {days, plural, one {yr un diwrnod} other {y # diwrnod}} diwethaf",
"notifications.policy.filter_new_accounts_title": "Cyfrifon newydd", "notifications.policy.filter_new_accounts_title": "Cyfrifon newydd",
"notifications.policy.filter_not_followers_hint": "Gan gynnwys pobl sydd wedi bod yn eich dilyn am llai {days, plural, un {nag un diwrnod} arall {na # diwrnod}}", "notifications.policy.filter_not_followers_hint": "Gan gynnwys pobl sydd wedi bod yn eich dilyn am llai {days, plural, one {nag un diwrnod} other {na # diwrnod}}",
"notifications.policy.filter_not_followers_title": "Pobl sydd ddim yn eich dilyn", "notifications.policy.filter_not_followers_title": "Pobl sydd ddim yn eich dilyn",
"notifications.policy.filter_not_following_hint": "Hyd nes i chi eu cymeradwyo â llaw", "notifications.policy.filter_not_following_hint": "Hyd nes i chi eu cymeradwyo â llaw",
"notifications.policy.filter_not_following_title": "Pobl nad ydych yn eu dilyn", "notifications.policy.filter_not_following_title": "Pobl nad ydych yn eu dilyn",
@@ -761,7 +761,7 @@
"report.categories.spam": "Sbam", "report.categories.spam": "Sbam",
"report.categories.violation": "Mae cynnwys yn torri un neu fwy o reolau'r gweinydd", "report.categories.violation": "Mae cynnwys yn torri un neu fwy o reolau'r gweinydd",
"report.category.subtitle": "Dewiswch yr ateb gorau", "report.category.subtitle": "Dewiswch yr ateb gorau",
"report.category.title": "Beth sy'n digwydd gyda'r {type} yma?", "report.category.title": "Beth sy'n digwydd gyda'r {type} yma",
"report.category.title_account": "proffil", "report.category.title_account": "proffil",
"report.category.title_status": "post", "report.category.title_status": "post",
"report.close": "Iawn", "report.close": "Iawn",
@@ -853,7 +853,7 @@
"status.direct_indicator": "Crybwyll preifat", "status.direct_indicator": "Crybwyll preifat",
"status.edit": "Golygu", "status.edit": "Golygu",
"status.edited": "Golygwyd ddiwethaf {date}", "status.edited": "Golygwyd ddiwethaf {date}",
"status.edited_x_times": "Golygwyd {count, plural, one {count} two {count} other {{count} gwaith}}", "status.edited_x_times": "Golygwyd {count, plural, one {{count} gwaith} other {{count} gwaith}}",
"status.embed": "Cael y cod mewnblannu", "status.embed": "Cael y cod mewnblannu",
"status.favourite": "Ffafrio", "status.favourite": "Ffafrio",
"status.favourites": "{count, plural, one {ffefryn} other {ffefryn}}", "status.favourites": "{count, plural, one {ffefryn} other {ffefryn}}",
@@ -922,7 +922,7 @@
"units.short.million": "{count}miliwn", "units.short.million": "{count}miliwn",
"units.short.thousand": "{count}mil", "units.short.thousand": "{count}mil",
"upload_area.title": "Llusgwch a gollwng i lwytho", "upload_area.title": "Llusgwch a gollwng i lwytho",
"upload_button.label": "Ychwanegwch gyfryngau (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_button.label": "Ychwanegwch delweddau, fideo neu ffeil sain",
"upload_error.limit": "Wedi mynd heibio'r uchafswm llwytho.", "upload_error.limit": "Wedi mynd heibio'r uchafswm llwytho.",
"upload_error.poll": "Does dim modd llwytho ffeiliau â phleidleisiau.", "upload_error.poll": "Does dim modd llwytho ffeiliau â phleidleisiau.",
"upload_form.drag_and_drop.instructions": "I godi atodiad cyfryngau, pwyswch y space neu enter. Wrth lusgo, defnyddiwch y bysellau saeth i symud yr atodiad cyfryngau i unrhyw gyfeiriad penodol. Pwyswch space neu enter eto i ollwng yr atodiad cyfryngau yn ei safle newydd, neu pwyswch escape i ddiddymu.", "upload_form.drag_and_drop.instructions": "I godi atodiad cyfryngau, pwyswch y space neu enter. Wrth lusgo, defnyddiwch y bysellau saeth i symud yr atodiad cyfryngau i unrhyw gyfeiriad penodol. Pwyswch space neu enter eto i ollwng yr atodiad cyfryngau yn ei safle newydd, neu pwyswch escape i ddiddymu.",

View File

@@ -345,9 +345,9 @@
"explore.trending_links": "Noticias", "explore.trending_links": "Noticias",
"explore.trending_statuses": "Publicaciones", "explore.trending_statuses": "Publicaciones",
"explore.trending_tags": "Etiquetas", "explore.trending_tags": "Etiquetas",
"featured_carousel.header": "{count, plural,one {Publicación fijada} other {Publicaciones fijada}}", "featured_carousel.header": "{count, plural,one {Publicación fijada} other {Publicaciones fijadas}}",
"featured_carousel.next": "Siguiente", "featured_carousel.next": "Siguiente",
"featured_carousel.post": "Publicar", "featured_carousel.post": "Publicación",
"featured_carousel.previous": "Anterior", "featured_carousel.previous": "Anterior",
"featured_carousel.slide": "{index} de {total}", "featured_carousel.slide": "{index} de {total}",
"filter_modal.added.context_mismatch_explanation": "Esta categoría de filtro no se aplica al contexto en el que has accedido a esta publlicación. Si quieres que la publicación sea filtrada también en este contexto, tendrás que editar el filtro.", "filter_modal.added.context_mismatch_explanation": "Esta categoría de filtro no se aplica al contexto en el que has accedido a esta publlicación. Si quieres que la publicación sea filtrada también en este contexto, tendrás que editar el filtro.",

View File

@@ -345,9 +345,9 @@
"explore.trending_links": "Noticias", "explore.trending_links": "Noticias",
"explore.trending_statuses": "Publicaciones", "explore.trending_statuses": "Publicaciones",
"explore.trending_tags": "Etiquetas", "explore.trending_tags": "Etiquetas",
"featured_carousel.header": "{count, plural,one {Publicación fijada} other {Publicaciones fijada}}", "featured_carousel.header": "{count, plural,one {Publicación fijada} other {Publicaciones fijadas}}",
"featured_carousel.next": "Siguiente", "featured_carousel.next": "Siguiente",
"featured_carousel.post": "Publicar", "featured_carousel.post": "Publicación",
"featured_carousel.previous": "Anterior", "featured_carousel.previous": "Anterior",
"featured_carousel.slide": "{index} de {total}", "featured_carousel.slide": "{index} de {total}",
"filter_modal.added.context_mismatch_explanation": "Esta categoría de filtro no se aplica al contexto en el que ha accedido a esta publlicación. Si quieres que la publicación sea filtrada también en este contexto, tendrás que editar el filtro.", "filter_modal.added.context_mismatch_explanation": "Esta categoría de filtro no se aplica al contexto en el que ha accedido a esta publlicación. Si quieres que la publicación sea filtrada también en este contexto, tendrás que editar el filtro.",

View File

@@ -1,6 +1,7 @@
{ {
"about.blocks": "Freastalaithe faoi stiúir", "about.blocks": "Freastalaithe faoi stiúir",
"about.contact": "Teagmháil:", "about.contact": "Teagmháil:",
"about.default_locale": "Réamhshocrú",
"about.disclaimer": "Bogearra foinse oscailte saor in aisce is ea Mastodon, agus is le Mastodon gGmbH an trádmharc.", "about.disclaimer": "Bogearra foinse oscailte saor in aisce is ea Mastodon, agus is le Mastodon gGmbH an trádmharc.",
"about.domain_blocks.no_reason_available": "Níl an fáth ar fáil", "about.domain_blocks.no_reason_available": "Níl an fáth ar fáil",
"about.domain_blocks.preamble": "Go hiondúil, tugann Mastadán cead duit a bheith ag plé le húsáideoirí as freastalaí ar bith eile sa chomhchruinne agus a gcuid inneachair a fheiceáil. Seo iad na heisceachtaí a rinneadh ar an bhfreastalaí áirithe seo.", "about.domain_blocks.preamble": "Go hiondúil, tugann Mastadán cead duit a bheith ag plé le húsáideoirí as freastalaí ar bith eile sa chomhchruinne agus a gcuid inneachair a fheiceáil. Seo iad na heisceachtaí a rinneadh ar an bhfreastalaí áirithe seo.",
@@ -8,6 +9,7 @@
"about.domain_blocks.silenced.title": "Teoranta", "about.domain_blocks.silenced.title": "Teoranta",
"about.domain_blocks.suspended.explanation": "Ní dhéanfar aon sonra ón fhreastalaí seo a phróiseáil, a stóráil ná a mhalartú, rud a fhágann nach féidir aon teagmháil ná aon chumarsáid a dhéanamh le húsáideoirí ón fhreastalaí seo.", "about.domain_blocks.suspended.explanation": "Ní dhéanfar aon sonra ón fhreastalaí seo a phróiseáil, a stóráil ná a mhalartú, rud a fhágann nach féidir aon teagmháil ná aon chumarsáid a dhéanamh le húsáideoirí ón fhreastalaí seo.",
"about.domain_blocks.suspended.title": "Ar fionraí", "about.domain_blocks.suspended.title": "Ar fionraí",
"about.language_label": "Teanga",
"about.not_available": "Níor cuireadh an t-eolas seo ar fáil ar an bhfreastalaí seo.", "about.not_available": "Níor cuireadh an t-eolas seo ar fáil ar an bhfreastalaí seo.",
"about.powered_by": "Meáin shóisialta díláraithe faoi chumhacht {mastodon}", "about.powered_by": "Meáin shóisialta díláraithe faoi chumhacht {mastodon}",
"about.rules": "Rialacha an fhreastalaí", "about.rules": "Rialacha an fhreastalaí",
@@ -308,6 +310,8 @@
"emoji_button.search_results": "Torthaí cuardaigh", "emoji_button.search_results": "Torthaí cuardaigh",
"emoji_button.symbols": "Comharthaí", "emoji_button.symbols": "Comharthaí",
"emoji_button.travel": "Taisteal ⁊ Áiteanna", "emoji_button.travel": "Taisteal ⁊ Áiteanna",
"empty_column.account_featured.me": "Níl aon rud curtha i láthair agat go fóill. An raibh a fhios agat gur féidir leat na haischlibeanna is mó a úsáideann tú, agus fiú cuntais do chairde, a chur i láthair ar do phróifíl?",
"empty_column.account_featured.other": "Níl aon rud feicthe ag {acct} go fóill. An raibh a fhios agat gur féidir leat na hashtags is mó a úsáideann tú, agus fiú cuntais do chairde, a chur ar do phróifíl?",
"empty_column.account_featured_other.unknown": "Níl aon rud le feiceáil sa chuntas seo go fóill.", "empty_column.account_featured_other.unknown": "Níl aon rud le feiceáil sa chuntas seo go fóill.",
"empty_column.account_hides_collections": "Roghnaigh an t-úsáideoir seo gan an fhaisnéis seo a chur ar fáil", "empty_column.account_hides_collections": "Roghnaigh an t-úsáideoir seo gan an fhaisnéis seo a chur ar fáil",
"empty_column.account_suspended": "Cuntas ar fionraí", "empty_column.account_suspended": "Cuntas ar fionraí",
@@ -341,6 +345,11 @@
"explore.trending_links": "Nuacht", "explore.trending_links": "Nuacht",
"explore.trending_statuses": "Postálacha", "explore.trending_statuses": "Postálacha",
"explore.trending_tags": "Haischlibeanna", "explore.trending_tags": "Haischlibeanna",
"featured_carousel.header": "{count, plural, one {Postáil phinnáilte} two {Poist Phionáilte} few {Poist Phionáilte} many {Poist Phionáilte} other {Poist Phionáilte}}",
"featured_carousel.next": "Ar Aghaidh",
"featured_carousel.post": "Post",
"featured_carousel.previous": "Roimhe Seo",
"featured_carousel.slide": "{index} de {total}",
"filter_modal.added.context_mismatch_explanation": "Ní bhaineann an chatagóir scagaire seo leis an gcomhthéacs ina bhfuair tú rochtain ar an bpostáil seo. Más mian leat an postáil a scagadh sa chomhthéacs seo freisin, beidh ort an scagaire a chur in eagar.", "filter_modal.added.context_mismatch_explanation": "Ní bhaineann an chatagóir scagaire seo leis an gcomhthéacs ina bhfuair tú rochtain ar an bpostáil seo. Más mian leat an postáil a scagadh sa chomhthéacs seo freisin, beidh ort an scagaire a chur in eagar.",
"filter_modal.added.context_mismatch_title": "Neamhréir comhthéacs!", "filter_modal.added.context_mismatch_title": "Neamhréir comhthéacs!",
"filter_modal.added.expired_explanation": "Tá an chatagóir scagaire seo imithe in éag, beidh ort an dáta éaga a athrú chun é a chur i bhfeidhm.", "filter_modal.added.expired_explanation": "Tá an chatagóir scagaire seo imithe in éag, beidh ort an dáta éaga a athrú chun é a chur i bhfeidhm.",

View File

@@ -1,6 +1,7 @@
{ {
"about.blocks": "制限中のサーバー", "about.blocks": "制限中のサーバー",
"about.contact": "連絡先", "about.contact": "連絡先",
"about.default_locale": "デフォルト",
"about.disclaimer": "Mastodonは自由なオープンソースソフトウェアであり、Mastodon gGmbHの商標です。", "about.disclaimer": "Mastodonは自由なオープンソースソフトウェアであり、Mastodon gGmbHの商標です。",
"about.domain_blocks.no_reason_available": "理由未記載", "about.domain_blocks.no_reason_available": "理由未記載",
"about.domain_blocks.preamble": "Mastodonでは原則的にあらゆるサーバー同士で交流したり、互いの投稿を読んだりできますが、当サーバーでは例外的に次のような制限を設けています。", "about.domain_blocks.preamble": "Mastodonでは原則的にあらゆるサーバー同士で交流したり、互いの投稿を読んだりできますが、当サーバーでは例外的に次のような制限を設けています。",
@@ -8,6 +9,7 @@
"about.domain_blocks.silenced.title": "制限", "about.domain_blocks.silenced.title": "制限",
"about.domain_blocks.suspended.explanation": "これらのサーバーからのデータは処理されず、保存や変換もされません。該当するユーザーとの交流もできません。", "about.domain_blocks.suspended.explanation": "これらのサーバーからのデータは処理されず、保存や変換もされません。該当するユーザーとの交流もできません。",
"about.domain_blocks.suspended.title": "停止中", "about.domain_blocks.suspended.title": "停止中",
"about.language_label": "言語",
"about.not_available": "この情報はこのサーバーでは利用できません。", "about.not_available": "この情報はこのサーバーでは利用できません。",
"about.powered_by": "{mastodon}による分散型ソーシャルメディア", "about.powered_by": "{mastodon}による分散型ソーシャルメディア",
"about.rules": "サーバーのルール", "about.rules": "サーバーのルール",
@@ -28,7 +30,11 @@
"account.edit_profile": "プロフィール編集", "account.edit_profile": "プロフィール編集",
"account.enable_notifications": "@{name}さんの投稿時に通知", "account.enable_notifications": "@{name}さんの投稿時に通知",
"account.endorse": "プロフィールで紹介する", "account.endorse": "プロフィールで紹介する",
"account.familiar_followers_many": "{name1}、{name2}、他{othersCount, plural, one {one other you know} other {# others you know}}人のユーザーにフォローされています",
"account.familiar_followers_one": "{name1} さんがフォローしています",
"account.familiar_followers_two": "{name1} さんと {name2} さんもフォローしています",
"account.featured": "注目", "account.featured": "注目",
"account.featured.accounts": "プロフィール",
"account.featured.hashtags": "ハッシュタグ", "account.featured.hashtags": "ハッシュタグ",
"account.featured_tags.last_status_at": "最終投稿 {date}", "account.featured_tags.last_status_at": "最終投稿 {date}",
"account.featured_tags.last_status_never": "投稿がありません", "account.featured_tags.last_status_never": "投稿がありません",
@@ -37,6 +43,7 @@
"account.followers": "フォロワー", "account.followers": "フォロワー",
"account.followers.empty": "まだ誰もフォローしていません。", "account.followers.empty": "まだ誰もフォローしていません。",
"account.followers_counter": "{count, plural, other {{counter} フォロワー}}", "account.followers_counter": "{count, plural, other {{counter} フォロワー}}",
"account.followers_you_know_counter": "あなたと知り合いの{counter}人",
"account.following": "フォロー中", "account.following": "フォロー中",
"account.following_counter": "{count, plural, other {{counter} フォロー}}", "account.following_counter": "{count, plural, other {{counter} フォロー}}",
"account.follows.empty": "まだ誰もフォローしていません。", "account.follows.empty": "まだ誰もフォローしていません。",
@@ -303,6 +310,8 @@
"emoji_button.search_results": "検索結果", "emoji_button.search_results": "検索結果",
"emoji_button.symbols": "記号", "emoji_button.symbols": "記号",
"emoji_button.travel": "旅行と場所", "emoji_button.travel": "旅行と場所",
"empty_column.account_featured.me": "まだ何もフィーチャーしていません。最もよく使うハッシュタグや、更には友達のアカウントまでプロフィール上でフィーチャーできると知っていましたか?",
"empty_column.account_featured.other": "{acct}ではまだ何もフィーチャーされていません。最もよく使うハッシュタグや、更には友達のアカウントまでプロフィール上でフィーチャーできると知っていましたか?",
"empty_column.account_featured_other.unknown": "このアカウントにはまだ何も投稿されていません。", "empty_column.account_featured_other.unknown": "このアカウントにはまだ何も投稿されていません。",
"empty_column.account_hides_collections": "このユーザーはこの情報を開示しないことにしています。", "empty_column.account_hides_collections": "このユーザーはこの情報を開示しないことにしています。",
"empty_column.account_suspended": "アカウントは停止されています", "empty_column.account_suspended": "アカウントは停止されています",
@@ -336,6 +345,10 @@
"explore.trending_links": "ニュース", "explore.trending_links": "ニュース",
"explore.trending_statuses": "投稿", "explore.trending_statuses": "投稿",
"explore.trending_tags": "ハッシュタグ", "explore.trending_tags": "ハッシュタグ",
"featured_carousel.next": "次へ",
"featured_carousel.post": "投稿",
"featured_carousel.previous": "前へ",
"featured_carousel.slide": "{index} / {total}",
"filter_modal.added.context_mismatch_explanation": "このフィルターカテゴリーはあなたがアクセスした投稿のコンテキストには適用されません。この投稿のコンテキストでもフィルターを適用するにはフィルターを編集する必要があります。", "filter_modal.added.context_mismatch_explanation": "このフィルターカテゴリーはあなたがアクセスした投稿のコンテキストには適用されません。この投稿のコンテキストでもフィルターを適用するにはフィルターを編集する必要があります。",
"filter_modal.added.context_mismatch_title": "コンテキストが一致しません!", "filter_modal.added.context_mismatch_title": "コンテキストが一致しません!",
"filter_modal.added.expired_explanation": "このフィルターカテゴリーは有効期限が切れています。適用するには有効期限を更新してください。", "filter_modal.added.expired_explanation": "このフィルターカテゴリーは有効期限が切れています。適用するには有効期限を更新してください。",
@@ -402,8 +415,10 @@
"hashtag.counter_by_accounts": "{count, plural, other {{counter}人投稿}}", "hashtag.counter_by_accounts": "{count, plural, other {{counter}人投稿}}",
"hashtag.counter_by_uses": "{count, plural, other {{counter}件}}", "hashtag.counter_by_uses": "{count, plural, other {{counter}件}}",
"hashtag.counter_by_uses_today": "本日{count, plural, other {#件}}", "hashtag.counter_by_uses_today": "本日{count, plural, other {#件}}",
"hashtag.feature": "プロフィールで紹介する",
"hashtag.follow": "ハッシュタグをフォローする", "hashtag.follow": "ハッシュタグをフォローする",
"hashtag.mute": "#{hashtag}をミュート", "hashtag.mute": "#{hashtag}をミュート",
"hashtag.unfeature": "プロフィールから外す",
"hashtag.unfollow": "ハッシュタグのフォローを解除", "hashtag.unfollow": "ハッシュタグのフォローを解除",
"hashtags.and_other": "ほか{count, plural, other {#個}}", "hashtags.and_other": "ほか{count, plural, other {#個}}",
"hints.profiles.followers_may_be_missing": "フォロワーの一覧は不正確な場合があります。", "hints.profiles.followers_may_be_missing": "フォロワーの一覧は不正確な場合があります。",
@@ -854,6 +869,13 @@
"status.mute_conversation": "会話をミュート", "status.mute_conversation": "会話をミュート",
"status.open": "詳細を表示", "status.open": "詳細を表示",
"status.pin": "プロフィールに固定表示", "status.pin": "プロフィールに固定表示",
"status.quote_error.filtered": "あなたのフィルター設定によって非表示になっています",
"status.quote_error.not_found": "この投稿は表示できません。",
"status.quote_error.pending_approval": "この投稿は投稿者の承認待ちです。",
"status.quote_error.rejected": "この投稿は、オリジナルの投稿者が引用することを許可していないため、表示できません。",
"status.quote_error.removed": "この投稿は投稿者によって削除されました。",
"status.quote_error.unauthorized": "この投稿を表示する権限がないため、表示できません。",
"status.quote_post_author": "{name} の投稿",
"status.read_more": "もっと見る", "status.read_more": "もっと見る",
"status.reblog": "ブースト", "status.reblog": "ブースト",
"status.reblog_private": "ブースト", "status.reblog_private": "ブースト",

View File

@@ -1,6 +1,7 @@
{ {
"about.blocks": "Siū 管制 ê 服侍器", "about.blocks": "Siū 管制 ê 服侍器",
"about.contact": "聯絡lâng", "about.contact": "聯絡lâng",
"about.default_locale": "預設",
"about.disclaimer": "Mastodon是自由、開放原始碼ê軟體mā是Mastodon gGmbH ê商標。", "about.disclaimer": "Mastodon是自由、開放原始碼ê軟體mā是Mastodon gGmbH ê商標。",
"about.domain_blocks.no_reason_available": "原因bē-tàng用", "about.domain_blocks.no_reason_available": "原因bē-tàng用",
"about.domain_blocks.preamble": "Mastodon一般ē允准lí看別ê fediverse 服侍器來ê聯絡人kap hām用者交流。Tsiah ê 是本服侍器建立ê例外。", "about.domain_blocks.preamble": "Mastodon一般ē允准lí看別ê fediverse 服侍器來ê聯絡人kap hām用者交流。Tsiah ê 是本服侍器建立ê例外。",
@@ -8,6 +9,7 @@
"about.domain_blocks.silenced.title": "有限制", "about.domain_blocks.silenced.title": "有限制",
"about.domain_blocks.suspended.explanation": "Uì tsit ê服侍器來ê資料lóng bē處理、儲存á是交換無可能kap tsit ê服侍器ê用者互動á是溝通。.", "about.domain_blocks.suspended.explanation": "Uì tsit ê服侍器來ê資料lóng bē處理、儲存á是交換無可能kap tsit ê服侍器ê用者互動á是溝通。.",
"about.domain_blocks.suspended.title": "權限中止", "about.domain_blocks.suspended.title": "權限中止",
"about.language_label": "言語",
"about.not_available": "Tsit ê資訊bē-tàng tī tsit ê服侍器使用。", "about.not_available": "Tsit ê資訊bē-tàng tī tsit ê服侍器使用。",
"about.powered_by": "由 {mastodon} 提供ê非中心化社群媒體", "about.powered_by": "由 {mastodon} 提供ê非中心化社群媒體",
"about.rules": "服侍器ê規則", "about.rules": "服侍器ê規則",
@@ -308,6 +310,8 @@
"emoji_button.search_results": "Tshiau-tshuē ê結果", "emoji_button.search_results": "Tshiau-tshuē ê結果",
"emoji_button.symbols": "符號", "emoji_button.symbols": "符號",
"emoji_button.travel": "旅行kap地點", "emoji_button.travel": "旅行kap地點",
"empty_column.account_featured.me": "Lí iáu無任何ê特色內容。Lí kám知影lí ē當kā lí tsia̍p-tsia̍p用ê hashtag甚至是朋友ê口座揀做特色ê內容khǹg佇lí ê個人資料內底?",
"empty_column.account_featured.other": "{acct} iáu無任何ê特色內容。Lí kám知影lí ē當kā lí tsia̍p-tsia̍p用ê hashtag甚至是朋友ê口座揀做特色ê內容khǹg佇lí ê個人資料內底?",
"empty_column.account_featured_other.unknown": "Tsit ê口座iáu無任何ê特色內容。", "empty_column.account_featured_other.unknown": "Tsit ê口座iáu無任何ê特色內容。",
"empty_column.account_hides_collections": "Tsit位用者選擇無愛公開tsit ê資訊", "empty_column.account_hides_collections": "Tsit位用者選擇無愛公開tsit ê資訊",
"empty_column.account_suspended": "口座已經受停止", "empty_column.account_suspended": "口座已經受停止",
@@ -341,6 +345,11 @@
"explore.trending_links": "新聞", "explore.trending_links": "新聞",
"explore.trending_statuses": "PO文", "explore.trending_statuses": "PO文",
"explore.trending_tags": "Hashtag", "explore.trending_tags": "Hashtag",
"featured_carousel.header": "{count, plural, one {{counter} 篇} other {{counter} 篇}} 釘起來ê PO文",
"featured_carousel.next": "下tsi̍t ê",
"featured_carousel.post": "PO文",
"featured_carousel.previous": "頂tsi̍t ê",
"featured_carousel.slide": "{total} 內底ê {index}",
"filter_modal.added.context_mismatch_explanation": "Tsit ê過濾器類別bē當適用佇lí所接近使用ê PO文ê情境。若是lí mā beh佇tsit ê情境過濾tsit ê PO文lí著編輯過濾器。.", "filter_modal.added.context_mismatch_explanation": "Tsit ê過濾器類別bē當適用佇lí所接近使用ê PO文ê情境。若是lí mā beh佇tsit ê情境過濾tsit ê PO文lí著編輯過濾器。.",
"filter_modal.added.context_mismatch_title": "本文無sio合", "filter_modal.added.context_mismatch_title": "本文無sio合",
"filter_modal.added.expired_explanation": "Tsit ê過濾器類別過期ahlí需要改到期ê日期來繼續用。", "filter_modal.added.expired_explanation": "Tsit ê過濾器類別過期ahlí需要改到期ê日期來繼續用。",
@@ -601,13 +610,13 @@
"notification.moderation_warning.action_none": "Lí ê口座有收著審核ê警告。", "notification.moderation_warning.action_none": "Lí ê口座有收著審核ê警告。",
"notification.moderation_warning.action_sensitive": "Tuì tsit-má開始lí êPO文ē標做敏感ê內容。", "notification.moderation_warning.action_sensitive": "Tuì tsit-má開始lí êPO文ē標做敏感ê內容。",
"notification.moderation_warning.action_silence": "Lí ê口座hōo lâng限制ah。", "notification.moderation_warning.action_silence": "Lí ê口座hōo lâng限制ah。",
"notification.moderation_warning.action_suspend": "Lí ê口座已經受停權。", "notification.moderation_warning.action_suspend": "Lí ê口座ê權限已經停止ah。",
"notification.own_poll": "Lí ê投票結束ah", "notification.own_poll": "Lí ê投票結束ah",
"notification.poll": "Lí bat投ê投票結束ah", "notification.poll": "Lí bat投ê投票結束ah",
"notification.reblog": "{name} 轉送lí ê PO文", "notification.reblog": "{name} 轉送lí ê PO文",
"notification.reblog.name_and_others_with_link": "{name} kap<a>{count, plural, other {另外 # ê lâng}}</a>轉送lí ê PO文", "notification.reblog.name_and_others_with_link": "{name} kap<a>{count, plural, other {另外 # ê lâng}}</a>轉送lí ê PO文",
"notification.relationships_severance_event": "Kap {name} ê結連無去", "notification.relationships_severance_event": "Kap {name} ê結連無去",
"notification.relationships_severance_event.account_suspension": "{from} ê管理員kā {target} 停ah意思是lí bē koh再接受tuì in 來ê更新á是hām in互動。", "notification.relationships_severance_event.account_suspension": "{from} ê管理員kā {target} 停止權限ah意思是lí bē koh再接受tuì in 來ê更新á是hām in互動。",
"notification.relationships_severance_event.domain_block": "{from} ê 管理員kā {target} 封鎖ah包含 {followersCount} 位跟tuè lí ê lângkap {followingCount, plural, other {#}} 位lí跟tuè ê口座。", "notification.relationships_severance_event.domain_block": "{from} ê 管理員kā {target} 封鎖ah包含 {followersCount} 位跟tuè lí ê lângkap {followingCount, plural, other {#}} 位lí跟tuè ê口座。",
"notification.relationships_severance_event.learn_more": "看詳細", "notification.relationships_severance_event.learn_more": "看詳細",
"notification.relationships_severance_event.user_domain_block": "Lí已經kā {target} 封鎖ahē suá走 {followersCount} 位跟tuè lí ê lângkap {followingCount, plural, other {#}} 位lí跟tuè ê口座。", "notification.relationships_severance_event.user_domain_block": "Lí已經kā {target} 封鎖ahē suá走 {followersCount} 位跟tuè lí ê lângkap {followingCount, plural, other {#}} 位lí跟tuè ê口座。",
@@ -891,12 +900,34 @@
"status.translated_from_with": "用 {provider} 翻譯 {lang}", "status.translated_from_with": "用 {provider} 翻譯 {lang}",
"status.uncached_media_warning": "Bē當先看māi", "status.uncached_media_warning": "Bē當先看māi",
"status.unmute_conversation": "Kā對話取消消音", "status.unmute_conversation": "Kā對話取消消音",
"subscribed_languages.lead": "Tī改變了後kan-ta所揀ê語言ê PO文tsiah ē顯示佇lí ê厝ê時間線kap列單。揀「無」來接受所有語言êPO文。",
"subscribed_languages.save": "儲存改變",
"subscribed_languages.target": "改 {target} ê訂ê語言",
"tabs_bar.home": "頭頁", "tabs_bar.home": "頭頁",
"tabs_bar.notifications": "通知", "tabs_bar.notifications": "通知",
"terms_of_service.effective_as_of": "{date} 起實施", "terms_of_service.effective_as_of": "{date} 起實施",
"terms_of_service.title": "服務規定", "terms_of_service.title": "服務規定",
"terms_of_service.upcoming_changes_on": "Ē tī {date} 改變", "terms_of_service.upcoming_changes_on": "Ē tī {date} 改變",
"time_remaining.days": "Tshun {number, plural, other {# kang}}", "time_remaining.days": "Tshun {number, plural, other {# kang}}",
"time_remaining.hours": "Tshun {number, plural, other {# 點鐘}}",
"time_remaining.minutes": "Tshun {number, plural, other {# 分鐘}}",
"time_remaining.moments": "Tshun ê時間",
"time_remaining.seconds": "Tshun {number, plural, other {# 秒}}",
"trends.counter_by_accounts": "{count, plural, one {{counter} ê} other {{counter} ê}} lâng tī過去 {days, plural, one {kang} other {{days} kang}}內底",
"trends.trending_now": "Tsit-má ê趨勢",
"ui.beforeunload": "Nā離開Mastodonlí ê草稿ē無去。",
"units.short.billion": "{count}B",
"units.short.million": "{count}M",
"units.short.thousand": "{count}K",
"upload_area.title": "Giú放來傳起去",
"upload_button.label": "加圖片、影片á是聲音檔",
"upload_error.limit": "超過檔案傳起去ê限制",
"upload_error.poll": "Bô允準佇投票ê時kā檔案傳起去。",
"upload_form.drag_and_drop.instructions": "Nā beh選媒體附件請tshi̍h空白key á是Enter key。Giú ê時請用方向key照指定ê方向suá媒體附件。Beh khǹg媒體附件佇伊ê新位置請koh tshi̍h空白key á是Enter key或者tshi̍h Esc key來取消。",
"upload_form.drag_and_drop.on_drag_cancel": "Suá位取消ah媒體附件 {item} khǹg落來ah。",
"upload_form.drag_and_drop.on_drag_end": "媒體附件 {item} khǹg落來ah。",
"upload_form.drag_and_drop.on_drag_over": "媒體附件 {item} suá tín動ah。",
"upload_form.drag_and_drop.on_drag_start": "媒體附件 {item} 揀起來ah。",
"upload_form.edit": "編輯", "upload_form.edit": "編輯",
"upload_progress.label": "Teh傳起去……", "upload_progress.label": "Teh傳起去……",
"upload_progress.processing": "Teh處理……", "upload_progress.processing": "Teh處理……",

View File

@@ -310,6 +310,8 @@
"emoji_button.search_results": "Søkeresultat", "emoji_button.search_results": "Søkeresultat",
"emoji_button.symbols": "Symbol", "emoji_button.symbols": "Symbol",
"emoji_button.travel": "Reise & stader", "emoji_button.travel": "Reise & stader",
"empty_column.account_featured.me": "Du har ikkje valt ut noko enno. Visste du at du kan velja ut merkelappar du bruker mykje, og til og med venekontoar på profilen din?",
"empty_column.account_featured.other": "{acct} har ikkje valt ut noko enno. Visste du at du kan velja ut merkelappar du bruker mykje, og til og med venekontoar på profilen din?",
"empty_column.account_featured_other.unknown": "Denne kontoen har ikkje valt ut noko enno.", "empty_column.account_featured_other.unknown": "Denne kontoen har ikkje valt ut noko enno.",
"empty_column.account_hides_collections": "Denne brukaren har valt å ikkje gjere denne informasjonen tilgjengeleg", "empty_column.account_hides_collections": "Denne brukaren har valt å ikkje gjere denne informasjonen tilgjengeleg",
"empty_column.account_suspended": "Kontoen er utestengd", "empty_column.account_suspended": "Kontoen er utestengd",
@@ -343,9 +345,10 @@
"explore.trending_links": "Nytt", "explore.trending_links": "Nytt",
"explore.trending_statuses": "Innlegg", "explore.trending_statuses": "Innlegg",
"explore.trending_tags": "Emneknaggar", "explore.trending_tags": "Emneknaggar",
"featured_carousel.header": "{count, plural, one {Festa innlegg} other {Festa innlegg}}",
"featured_carousel.next": "Neste", "featured_carousel.next": "Neste",
"featured_carousel.post": "Innlegg", "featured_carousel.post": "Innlegg",
"featured_carousel.previous": "Forrige", "featured_carousel.previous": "Førre",
"featured_carousel.slide": "{index} av {total}", "featured_carousel.slide": "{index} av {total}",
"filter_modal.added.context_mismatch_explanation": "Denne filterkategorien gjeld ikkje i den samanhengen du har lese dette innlegget. Viss du vil at innlegget skal filtrerast i denne samanhengen òg, må du endra filteret.", "filter_modal.added.context_mismatch_explanation": "Denne filterkategorien gjeld ikkje i den samanhengen du har lese dette innlegget. Viss du vil at innlegget skal filtrerast i denne samanhengen òg, må du endra filteret.",
"filter_modal.added.context_mismatch_title": "Konteksten passar ikkje!", "filter_modal.added.context_mismatch_title": "Konteksten passar ikkje!",

View File

@@ -8,6 +8,7 @@
"about.domain_blocks.silenced.title": "Obmedzený", "about.domain_blocks.silenced.title": "Obmedzený",
"about.domain_blocks.suspended.explanation": "Žiadne údaje z tohto servera nebudú spracovávané, ukladané ani vymieňané, čo znemožní akúkoľvek interakciu alebo komunikáciu s používateľmi z tohto servera.", "about.domain_blocks.suspended.explanation": "Žiadne údaje z tohto servera nebudú spracovávané, ukladané ani vymieňané, čo znemožní akúkoľvek interakciu alebo komunikáciu s používateľmi z tohto servera.",
"about.domain_blocks.suspended.title": "Vylúčený", "about.domain_blocks.suspended.title": "Vylúčený",
"about.language_label": "Jazyk",
"about.not_available": "Tieto informácie neboli sprístupnené na tomto serveri.", "about.not_available": "Tieto informácie neboli sprístupnené na tomto serveri.",
"about.powered_by": "Decentralizovaná sociálna sieť na základe technológie {mastodon}", "about.powered_by": "Decentralizovaná sociálna sieť na základe technológie {mastodon}",
"about.rules": "Pravidlá servera", "about.rules": "Pravidlá servera",
@@ -53,6 +54,7 @@
"account.mute_notifications_short": "Stíšiť upozornenia", "account.mute_notifications_short": "Stíšiť upozornenia",
"account.mute_short": "Stíšiť", "account.mute_short": "Stíšiť",
"account.muted": "Účet stíšený", "account.muted": "Účet stíšený",
"account.mutual": "Nasledujete sa navzájom",
"account.no_bio": "Nie je uvedený žiadny popis.", "account.no_bio": "Nie je uvedený žiadny popis.",
"account.open_original_page": "Otvoriť pôvodnú stránku", "account.open_original_page": "Otvoriť pôvodnú stránku",
"account.posts": "Príspevky", "account.posts": "Príspevky",

View File

@@ -6605,7 +6605,6 @@ a.status-card {
ul { ul {
overflow-y: auto; overflow-y: auto;
flex-shrink: 0;
padding-bottom: 8px; padding-bottom: 8px;
} }

270
app/lib/signed_request.rb Normal file
View File

@@ -0,0 +1,270 @@
# frozen_string_literal: true
class SignedRequest
include DomainControlHelper
EXPIRATION_WINDOW_LIMIT = 12.hours
CLOCK_SKEW_MARGIN = 1.hour
class HttpSignature
REQUIRED_PARAMETERS = %w(keyId signature).freeze
def initialize(request)
@request = request
end
def key_id
signature_params['keyId']
end
def missing_signature_parameters
REQUIRED_PARAMETERS if REQUIRED_PARAMETERS.any? { |p| signature_params[p].blank? }
end
def algorithm_supported?
%w(rsa-sha256 hs2019).include?(signature_algorithm)
end
def verified?(actor)
signature = Base64.decode64(signature_params['signature'])
compare_signed_string = build_signed_string(include_query_string: true)
return true unless verify_signature(actor, signature, compare_signed_string).nil?
compare_signed_string = build_signed_string(include_query_string: false)
return true unless verify_signature(actor, signature, compare_signed_string).nil?
false
end
def created_time
if signature_algorithm == 'hs2019' && signature_params['created'].present?
Time.at(signature_params['created'].to_i).utc
elsif @request.headers['Date'].present?
Time.httpdate(@request.headers['Date']).utc
end
rescue ArgumentError => e
raise Mastodon::SignatureVerificationError, "Invalid Date header: #{e.message}"
end
def expires_time
Time.at(signature_params['expires'].to_i).utc if signature_params['expires'].present?
rescue ArgumentError => e
raise Mastodon::SignatureVerificationError, "Invalid Date header: #{e.message}"
end
def verify_signature_strength!
raise Mastodon::SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)')
raise Mastodon::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 Mastodon::SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if @request.get? && !signed_headers.include?('host')
raise Mastodon::SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if @request.post? && !signed_headers.include?('digest')
end
def verify_body_digest!
return unless signed_headers.include?('digest')
raise Mastodon::SignatureVerificationError, 'Digest header missing' unless @request.headers.key?('Digest')
digests = @request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] }
sha256 = digests.assoc('sha-256')
raise Mastodon::SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil?
return if body_digest == sha256[1]
digest_size = begin
Base64.strict_decode64(sha256[1].strip).length
rescue ArgumentError
raise Mastodon::SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a valid base64 string. Given digest: #{sha256[1]}"
end
raise Mastodon::SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a SHA-256 digest. Given digest: #{sha256[1]}" if digest_size != 32
raise Mastodon::SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}"
end
private
def request_body
@request_body ||= @request.raw_post
end
def signature_params
@signature_params ||= SignatureParser.parse(@request.headers['Signature'])
rescue SignatureParser::ParsingError
raise Mastodon::SignatureVerificationError, 'Error parsing signature parameters'
end
def signature_algorithm
signature_params.fetch('algorithm', 'hs2019')
end
def signed_headers
signature_params.fetch('headers', signature_algorithm == 'hs2019' ? '(created)' : 'date').downcase.split
end
def verify_signature(actor, signature, compare_signed_string)
true if actor.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string)
rescue OpenSSL::PKey::RSAError
nil
end
def build_signed_string(include_query_string: true)
signed_headers.map do |signed_header|
case signed_header
when HttpSignatureDraft::REQUEST_TARGET
if include_query_string
"#{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
"#{HttpSignatureDraft::REQUEST_TARGET}: #{@request.method.downcase} #{@request.path}"
end
when '(created)'
raise Mastodon::SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
raise Mastodon::SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?
"(created): #{signature_params['created']}"
when '(expires)'
raise Mastodon::SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019'
raise Mastodon::SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank?
"(expires): #{signature_params['expires']}"
else
"#{signed_header}: #{@request.headers[to_header_name(signed_header)]}"
end
end.join("\n")
end
def body_digest
@body_digest ||= Digest::SHA256.base64digest(request_body)
end
def to_header_name(name)
name.split('-').map(&:capitalize).join('-')
end
end
class HttpMessageSignature
REQUIRED_PARAMETERS = %w(keyid created).freeze
def initialize(request)
@request = request
@signature = Linzer::Signature.build({
'signature-input' => @request.headers['signature-input'],
'signature' => @request.headers['signature'],
})
end
def key_id
@signature.parameters['keyid']
end
def missing_signature_parameters
REQUIRED_PARAMETERS if REQUIRED_PARAMETERS.any? { |p| @signature.parameters[p].blank? }
end
# This method can lie as we only support one specific algorith for now.
# But HTTP Message Signatures do not need to specify an algorithm (as
# this can be inferred from the key used). Using an unsupported
# algorithm will fail anyway further down the line.
def algorithm_supported?
true
end
def verified?(actor)
key = Linzer.new_rsa_v1_5_sha256_public_key(actor.public_key)
Linzer.verify!(@request.rack_request, key:)
rescue Linzer::VerifyError
false
end
def verify_signature_strength!
raise Mastodon::SignatureVerificationError, 'Mastodon requires the (created) parameter to be signed' if @signature.parameters['created'].blank?
raise Mastodon::SignatureVerificationError, 'Mastodon requires the @method and @target-uri derived components to be signed' unless @signature.components.include?('@method') && @signature.components.include?('@target-uri')
raise Mastodon::SignatureVerificationError, 'Mastodon requires the Content-Digest header to be signed when doing a POST request' if @request.post? && !signed_headers.include?('content-digest')
end
def verify_body_digest!
return unless signed_headers.include?('content-digest')
raise Mastodon::SignatureVerificationError, 'Content-Digest header missing' unless @request.headers.key?('content-digest')
digests = Starry.parse_dictionary(@request.headers['content-digest'])
raise Mastodon::SignatureVerificationError, "Mastodon only supports SHA-256 in Content-Digest header. Offered algorithms: #{digests.keys.join(', ')}" unless digests.key?('sha-256')
received_digest = Base64.strict_encode64(digests['sha-256'].value)
return if body_digest == received_digest
raise Mastodon::SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{received_digest}"
end
def created_time
Time.at(@signature.parameters['created'].to_i).utc
rescue ArgumentError => e
raise Mastodon::SignatureVerificationError, "Invalid Date header: #{e.message}"
end
def expires_time
Time.at(@signature.parameters['expires'].to_i).utc if @signature.parameters['expires'].present?
rescue ArgumentError => e
raise Mastodon::SignatureVerificationError, "Invalid Date header: #{e.message}"
end
private
def request_body
@request_body ||= @request.raw_post
end
def signed_headers
@signed_headers ||= @signature.components.reject { |c| c.start_with?('@') }
end
def body_digest
@body_digest ||= Digest::SHA256.base64digest(request_body)
end
def missing_required_signature_parameters?
@signature.parameters['keyid'].blank?
end
end
attr_reader :signature
delegate :key_id, to: :signature
def initialize(request)
@signature =
if Mastodon::Feature.http_message_signatures_enabled? && request.headers['signature-input'].present?
HttpMessageSignature.new(request)
else
HttpSignature.new(request)
end
end
def verified?(actor)
missing_signature_parameters = @signature.missing_signature_parameters
raise Mastodon::SignatureVerificationError, "Incompatible request signature. #{missing_signature_parameters.to_sentence} are required" if missing_signature_parameters
raise Mastodon::SignatureVerificationError, 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)' unless @signature.algorithm_supported?
raise Mastodon::SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window?
@signature.verify_signature_strength!
@signature.verify_body_digest!
@signature.verified?(actor)
end
private
def matches_time_window?
created_time = @signature.created_time
expires_time = @signature.expires_time
expires_time ||= created_time + 5.minutes unless created_time.nil?
expires_time = [expires_time, created_time + EXPIRATION_WINDOW_LIMIT].min unless created_time.nil?
return false if created_time.present? && created_time > Time.now.utc + CLOCK_SKEW_MARGIN
return false if expires_time.present? && Time.now.utc > expires_time + CLOCK_SKEW_MARGIN
true
end
end

View File

@@ -26,6 +26,8 @@ class FeaturedTag < ApplicationRecord
normalizes :name, with: ->(name) { name.strip.delete_prefix('#') } normalizes :name, with: ->(name) { name.strip.delete_prefix('#') }
scope :by_name, ->(name) { joins(:tag).where(tag: { name: HashtagNormalizer.new.normalize(name) }) }
before_validation :set_tag before_validation :set_tag
before_create :reset_data before_create :reset_data

View File

@@ -394,7 +394,7 @@ Devise.setup do |config|
config.ldap_uid = ENV.fetch('LDAP_UID', 'cn') config.ldap_uid = ENV.fetch('LDAP_UID', 'cn')
config.ldap_mail = ENV.fetch('LDAP_MAIL', 'mail') config.ldap_mail = ENV.fetch('LDAP_MAIL', 'mail')
config.ldap_tls_no_verify = ENV['LDAP_TLS_NO_VERIFY'] == 'true' config.ldap_tls_no_verify = ENV['LDAP_TLS_NO_VERIFY'] == 'true'
config.ldap_search_filter = ENV.fetch('LDAP_SEARCH_FILTER', '(|(%{uid}=%{email})(%{mail}=%{email}))') config.ldap_search_filter = ENV.fetch('LDAP_SEARCH_FILTER', '(|(%<uid>s=%<email>s)(%<mail>s=%<email>s))')
config.ldap_uid_conversion_enabled = ENV['LDAP_UID_CONVERSION_ENABLED'] == 'true' config.ldap_uid_conversion_enabled = ENV['LDAP_UID_CONVERSION_ENABLED'] == 'true'
config.ldap_uid_conversion_search = ENV.fetch('LDAP_UID_CONVERSION_SEARCH', '.,- ') config.ldap_uid_conversion_search = ENV.fetch('LDAP_UID_CONVERSION_SEARCH', '.,- ')
config.ldap_uid_conversion_replace = ENV.fetch('LDAP_UID_CONVERSION_REPLACE', '_') config.ldap_uid_conversion_replace = ENV.fetch('LDAP_UID_CONVERSION_REPLACE', '_')

View File

@@ -332,7 +332,7 @@ cy:
title: Cyhoeddiad newydd title: Cyhoeddiad newydd
preview: preview:
disclaimer: Gan nad oes modd i ddefnyddwyr eu hosgoi, dylai hysbysiadau e-bost gael eu cyfyngu i gyhoeddiadau pwysig fel tor-data personol neu hysbysiadau cau gweinydd. disclaimer: Gan nad oes modd i ddefnyddwyr eu hosgoi, dylai hysbysiadau e-bost gael eu cyfyngu i gyhoeddiadau pwysig fel tor-data personol neu hysbysiadau cau gweinydd.
explanation_html: 'Bydd yr e-bost yn cael ei anfon at <strong>%{display_count} defnyddiwr</strong> . Bydd y testun canlynol yn cael ei gynnwys yn yr e-bost:' explanation_html: 'Bydd yr e-bost yn cael ei anfon at <strong>%{display_count} defnyddiwr</strong>. Bydd y testun canlynol yn cael ei gynnwys yn yr e-bost:'
title: Hysbysiad rhagolwg cyhoeddiad title: Hysbysiad rhagolwg cyhoeddiad
publish: Cyhoeddi publish: Cyhoeddi
published_msg: Cyhoeddiad wedi'i gyhoeddi'n llwyddiannus! published_msg: Cyhoeddiad wedi'i gyhoeddi'n llwyddiannus!
@@ -1352,7 +1352,7 @@ cy:
date: date:
formats: formats:
default: "%b %d %Y" default: "%b %d %Y"
with_month_name: "%b %d %Y" with_month_name: "%B %d, %Y"
datetime: datetime:
distance_in_words: distance_in_words:
about_x_hours: "%{count}a" about_x_hours: "%{count}a"
@@ -1725,7 +1725,7 @@ cy:
unsubscribe: unsubscribe:
action: Iawn, dad-danysgrifio action: Iawn, dad-danysgrifio
complete: Dad-danysgrifiwyd complete: Dad-danysgrifiwyd
confirmation_html: Ydych chi'n siŵr eich bod am ddad-danysgrifio rhag derbyn %{type} Mastodon ar %{domain} i'ch e-bost yn %{email}? Gallwch ail-danysgrifio o'ch <a href="%{settings_path}">gosodiadau hysbysu e-bost</a> rhywbryd eto . confirmation_html: Ydych chi'n siŵr eich bod am ddad-danysgrifio rhag derbyn %{type} Mastodon ar %{domain} i'ch e-bost yn %{email}? Gallwch ail-danysgrifio o'ch <a href="%{settings_path}">gosodiadau hysbysu e-bost</a> rhywbryd eto.
emails: emails:
notification_emails: notification_emails:
favourite: e-bost hysbysu hoffi favourite: e-bost hysbysu hoffi
@@ -2169,7 +2169,7 @@ cy:
agreement: Drwy barhau i ddefnyddio %{domain}, rydych yn cytuno i'r telerau hyn. Os ydych yn anghytuno â'r telerau a ddiweddarwyd, gallwch derfynu eich cytundeb â %{domain} ar unrhyw adeg drwy ddileu eich cyfrif. agreement: Drwy barhau i ddefnyddio %{domain}, rydych yn cytuno i'r telerau hyn. Os ydych yn anghytuno â'r telerau a ddiweddarwyd, gallwch derfynu eich cytundeb â %{domain} ar unrhyw adeg drwy ddileu eich cyfrif.
changelog: 'Yn fyr, dyma beth mae''r diweddariad hwn yn ei olygu i chi:' changelog: 'Yn fyr, dyma beth mae''r diweddariad hwn yn ei olygu i chi:'
description: 'Rydych yn derbyn yr e-bost hwn oherwydd ein bod yn gwneud rhai newidiadau i''n telerau gwasanaeth yn %{domain}. Bydd y diweddariadau hyn yn dod i rym ar %{date}. Rydym yn eich annog i adolygu''r telerau diweddaraf yn llawn yma:' description: 'Rydych yn derbyn yr e-bost hwn oherwydd ein bod yn gwneud rhai newidiadau i''n telerau gwasanaeth yn %{domain}. Bydd y diweddariadau hyn yn dod i rym ar %{date}. Rydym yn eich annog i adolygu''r telerau diweddaraf yn llawn yma:'
description_html: Rydych yn derbyn yr e-bost hwn oherwydd ein bod yn gwneud rhai newidiadau i'n telerau gwasanaeth yn %{domain}. Bydd y diweddariadau hyn yn dod i rym ar <strong>%{date}</strong> . Rydym yn eich annog i adolygu'r <a href="%{path}" target="_blank">telerau diweddaraf yn llawn yma</a> . description_html: Rydych yn derbyn yr e-bost hwn oherwydd ein bod yn gwneud rhai newidiadau i'n telerau gwasanaeth yn %{domain}. Bydd y diweddariadau hyn yn dod i rym ar <strong>%{date}</strong>. Rydym yn eich annog i adolygu'r <a href="%{path}" target="_blank">telerau diweddaraf yn llawn yma</a>.
sign_off: Tîm %{domain} sign_off: Tîm %{domain}
subject: Diweddariadau i'n telerau gwasanaeth subject: Diweddariadau i'n telerau gwasanaeth
subtitle: Mae telerau gwasanaeth %{domain} yn newid subtitle: Mae telerau gwasanaeth %{domain} yn newid

View File

@@ -772,17 +772,26 @@ ja:
title: ロール title: ロール
rules: rules:
add_new: ルールを追加 add_new: ルールを追加
add_translation: 翻訳を追加
delete: 削除 delete: 削除
description_html: たいていの人が利用規約を読んで同意したと言いますが、普通は問題が発生するまで読みません。<strong>箇条書きにして、サーバーのルールが一目で分かるようにしましょう</strong>。個々のルールは短くシンプルなものにし、多くの項目に分割しないようにしましょう。 description_html: たいていの人が利用規約を読んで同意したと言いますが、普通は問題が発生するまで読みません。<strong>箇条書きにして、サーバーのルールが一目で分かるようにしましょう</strong>。個々のルールは短くシンプルなものにし、多くの項目に分割しないようにしましょう。
edit: ルールを編集 edit: ルールを編集
empty: サーバーのルールが定義されていません。 empty: サーバーのルールが定義されていません。
move_down: 下へ移動
move_up: 上へ移動
title: サーバーのルール title: サーバーのルール
translation: 翻訳
translations: 翻訳
translations_explanation: オプションとしてルールに翻訳を追加できます。翻訳されたバージョンがない場合、デフォルトの値が表示されます。表示される翻訳がデフォルトの値と対応していることを常に確認しておいてください。
settings: settings:
about: about:
manage_rules: サーバーのルールを管理 manage_rules: サーバーのルールを管理
preamble: サーバーの運営、管理、資金調達の方法について、詳細な情報を提供します。 preamble: サーバーの運営、管理、資金調達の方法について、詳細な情報を提供します。
rules_hint: ユーザーが守るべきルールのための専用エリアがあります。 rules_hint: ユーザーが守るべきルールのための専用エリアがあります。
title: このサーバーについて title: このサーバーについて
allow_referrer_origin:
desc: ユーザが外部サイトへのリンクをクリックする際、ユーザーのブラウザはあなたのMastodonサーバーのアドレスを紹介者として送信することがあります。 これによりあなたのユーザーが特定されてしまう場合、例えば個人用のMastodonサーバーなどである場合などは無効にしてください。
title: 外部サイトが Mastodon サーバーをトラフィックソースとして表示することを許可する
appearance: appearance:
preamble: ウェブインターフェースをカスタマイズします。 preamble: ウェブインターフェースをカスタマイズします。
title: 外観 title: 外観
@@ -802,6 +811,7 @@ ja:
discovery: discovery:
follow_recommendations: おすすめフォロー follow_recommendations: おすすめフォロー
preamble: Mastodon を知らないユーザーを取り込むには、興味深いコンテンツを浮上させることが重要です。サーバー上で様々なディスカバリー機能がどのように機能するかを制御します。 preamble: Mastodon を知らないユーザーを取り込むには、興味深いコンテンツを浮上させることが重要です。サーバー上で様々なディスカバリー機能がどのように機能するかを制御します。
privacy: プライバシー
profile_directory: ディレクトリ profile_directory: ディレクトリ
public_timelines: 公開タイムライン public_timelines: 公開タイムライン
publish_statistics: 統計情報を公開する publish_statistics: 統計情報を公開する
@@ -888,6 +898,8 @@ ja:
system_checks: system_checks:
database_schema_check: database_schema_check:
message_html: 未実行のデータベースマイグレーションがあります。実行して正常に動作するようにしてください。 message_html: 未実行のデータベースマイグレーションがあります。実行して正常に動作するようにしてください。
elasticsearch_analysis_index_mismatch:
message_html: Elasticsearch インデックスアナライザの設定が古くなっています。 <code>tootctl search deploy --only-mapping --only=%{value}</code>を実行してください
elasticsearch_health_red: elasticsearch_health_red:
message_html: 'Elasticsearchクラスターに異常があります(status: red)。検索機能が利用できなくなっています' message_html: 'Elasticsearchクラスターに異常があります(status: red)。検索機能が利用できなくなっています'
elasticsearch_health_yellow: elasticsearch_health_yellow:
@@ -1812,6 +1824,10 @@ ja:
limit: 固定できる投稿数の上限に達しました limit: 固定できる投稿数の上限に達しました
ownership: 他人の投稿を固定することはできません ownership: 他人の投稿を固定することはできません
reblog: ブーストを固定することはできません reblog: ブーストを固定することはできません
quote_policies:
followers: フォロワーとメンションされたユーザー
nobody: メンションされたユーザーのみ
public: 全員
title: '%{name}: "%{quote}"' title: '%{name}: "%{quote}"'
visibilities: visibilities:
direct: ダイレクト direct: ダイレクト
@@ -1865,6 +1881,11 @@ ja:
does_not_match_previous_name: 以前の名前と一致しません does_not_match_previous_name: 以前の名前と一致しません
terms_of_service: terms_of_service:
title: サービス利用規約 title: サービス利用規約
terms_of_service_interstitial:
future_preamble_html: サービス利用規約にいくつかの変更が加えられています。これは <strong>%{date}</strong>に適用されます。 更新された条件を確認することをお勧めします。
past_preamble_html: 前回の訪問時から利用規約が変更されました。更新された条件を確認することをお勧めします。
review_link: 利用規約を確認する
title: "%{domain} の利用規約が変更されています"
themes: themes:
contrast: Mastodon (ハイコントラスト) contrast: Mastodon (ハイコントラスト)
default: Mastodon (ダーク) default: Mastodon (ダーク)

View File

@@ -10,7 +10,7 @@ nan:
followers: followers:
other: 跟tuè ê other: 跟tuè ê
following: Leh跟tuè following: Leh跟tuè
instance_actor_flash: Tsit ê口座是虛ê用來代表tsit臺服侍器毋是個人用者ê。伊用來做聯邦ê路用毋好kā停權 instance_actor_flash: Tsit ê口座是虛ê用來代表tsit臺服侍器毋是個人用者ê。伊用來做聯邦ê路用毋好kā伊ê權限停止
last_active: 頂kái活動ê時間 last_active: 頂kái活動ê時間
link_verified_on: Tsit ê連結ê所有權佇 %{date} 受檢查 link_verified_on: Tsit ê連結ê所有權佇 %{date} 受檢查
nothing_here: Tsia內底無物件 nothing_here: Tsia內底無物件
@@ -24,7 +24,7 @@ nan:
account_actions: account_actions:
action: 執行動作 action: 執行動作
already_silenced: Tsit ê口座有受著限制。 already_silenced: Tsit ê口座有受著限制。
already_suspended: Tsit ê口座已經受停權 already_suspended: Tsit ê口座ê權限已經hōo lâng停止
title: Kā %{acct} 做審核ê動作 title: Kā %{acct} 做審核ê動作
account_moderation_notes: account_moderation_notes:
create: 留記錄 create: 留記錄
@@ -110,13 +110,30 @@ nan:
previous_strikes_description_html: previous_strikes_description_html:
other: Tsit ê口座有 <strong>%{count}</strong> kái警告。 other: Tsit ê口座有 <strong>%{count}</strong> kái警告。
promote: 權限the̍h懸 promote: 權限the̍h懸
protocol: 協定
public: 公開ê
push_subscription_expires: 訂PuSH ê期間過ah
redownload: 重頭整理個人檔案
redownloaded_msg: Tuì來源站kā %{username} ê個人資料成功重頭整理
reject: 拒絕
rejected_msg: 成功拒絕 %{username} ê註冊申請ah
remote_suspension_irreversible: Tsit ê口座ê資料已經hōo lâng thâi掉bē當復原。
remote_suspension_reversible_hint_html: Tsit ê口座ê權限佇tsit ê服侍器hōo lâng停止ah資料ē佇 %{date} lóng總thâi掉。佇hit日前遠距離ê服侍器ē當復原tsit ê口座無任何pháinn作用。Nā lí想beh liâm-mī thâi掉tsit ê口座ê任何資料,ē當佇下跤操作。
remove_avatar: Thâi掉標頭 remove_avatar: Thâi掉標頭
remove_header: Thâi掉封面ê圖
removed_avatar_msg: 成功thâi掉 %{username} ê 標頭影像 removed_avatar_msg: 成功thâi掉 %{username} ê 標頭影像
removed_header_msg: 成功thâi掉 %{username} ê封面ê圖
resend_confirmation:
already_confirmed: Tsit ê用者有受tio̍h確認
send: 重送確認ê連結
success: 確認連結傳成功ah
reset: 重頭設 reset: 重頭設
reset_password: Kā密碼重頭設 reset_password: Kā密碼重頭設
resubscribe: 重頭訂 resubscribe: 重頭訂
role: 角色 role: 角色
search: Tshiau-tshuē search: Tshiau-tshuē
search_same_email_domain: 其他電子phue域名相kâng ê用者
search_same_ip: 其他IP相kâng ê用者
security: 安全 security: 安全
security_measures: security_measures:
only_password: Kan-ta用密碼 only_password: Kan-ta用密碼
@@ -126,13 +143,21 @@ nan:
shared_inbox_url: 做伙用ê收件kheh-á (Shared Inbox) ê URL shared_inbox_url: 做伙用ê收件kheh-á (Shared Inbox) ê URL
show: show:
created_reports: 檢舉記錄 created_reports: 檢舉記錄
targeted_reports: Hōo別lâng檢舉
silence: 靜音
silenced: 受靜音
statuses: PO文
strikes: Khah早ê處份
subscribe:
suspend: 中止權限
suspended: 權限中止ah
unblocked_email_msg: 成功取消封鎖 %{username} ê電子phue地址 unblocked_email_msg: 成功取消封鎖 %{username} ê電子phue地址
unconfirmed_email: 無驗證ê電子phue unconfirmed_email: 無驗證ê電子phue
undo_sensitized: 取消強制標做敏感ê undo_sensitized: 取消強制標做敏感ê
undo_silenced: 取消限制 undo_silenced: 取消限制
undo_suspension: 取消停 undo_suspension: 取消停止權限
unsubscribe: 取消訂 unsubscribe: 取消訂
unsuspended_msg: 成功kā %{username} ê口座取消停 unsuspended_msg: 成功kā %{username} ê口座取消停止權限
username: 用者ê名 username: 用者ê名
view_domain: 看域名ê摘要 view_domain: 看域名ê摘要
warn: 警告 warn: 警告

View File

@@ -56,6 +56,7 @@ ja:
scopes: アプリの API に許可するアクセス権を選択してください。最上位のスコープを選択する場合、個々のスコープを選択する必要はありません。 scopes: アプリの API に許可するアクセス権を選択してください。最上位のスコープを選択する場合、個々のスコープを選択する必要はありません。
setting_aggregate_reblogs: 最近ブーストされた投稿が新たにブーストされても表示しません (設定後受信したものにのみ影響) setting_aggregate_reblogs: 最近ブーストされた投稿が新たにブーストされても表示しません (設定後受信したものにのみ影響)
setting_always_send_emails: 通常、Mastodon からメール通知は行われません。 setting_always_send_emails: 通常、Mastodon からメール通知は行われません。
setting_default_quote_policy: メンションされたユーザーが常にその投稿を引用できるようになる。 この設定はMastodonの次のバージョンからしか効力を発揮しませんが、現時点で設定を選択しておくことができます
setting_default_sensitive: 閲覧注意状態のメディアはデフォルトでは内容が伏せられ、クリックして初めて閲覧できるようになります setting_default_sensitive: 閲覧注意状態のメディアはデフォルトでは内容が伏せられ、クリックして初めて閲覧できるようになります
setting_display_media_default: 閲覧注意としてマークされたメディアは隠す setting_display_media_default: 閲覧注意としてマークされたメディアは隠す
setting_display_media_hide_all: メディアを常に隠す setting_display_media_hide_all: メディアを常に隠す
@@ -148,6 +149,8 @@ ja:
min_age: お住まいの国や地域の法律によって定められている最低年齢を下回ってはなりません。 min_age: お住まいの国や地域の法律によって定められている最低年齢を下回ってはなりません。
user: user:
chosen_languages: 選択すると、選択した言語の投稿のみが公開タイムラインに表示されるようになります chosen_languages: 選択すると、選択した言語の投稿のみが公開タイムラインに表示されるようになります
date_of_birth:
other: Mastodonを利用するには少なくとも%{count}歳以上であることを確認する必要があります。この情報は保存されません。
role: そのロールは、ユーザーが持つ権限を制御します。 role: そのロールは、ユーザーが持つ権限を制御します。
user_role: user_role:
color: UI 全体でロールの表示に使用される色16進数RGB形式 color: UI 全体でロールの表示に使用される色16進数RGB形式
@@ -228,6 +231,7 @@ ja:
setting_boost_modal: ブーストする前に確認ダイアログを表示する setting_boost_modal: ブーストする前に確認ダイアログを表示する
setting_default_language: 投稿する言語 setting_default_language: 投稿する言語
setting_default_privacy: 投稿の公開範囲 setting_default_privacy: 投稿の公開範囲
setting_default_quote_policy: 引用できるユーザー
setting_default_sensitive: メディアを常に閲覧注意としてマークする setting_default_sensitive: メディアを常に閲覧注意としてマークする
setting_delete_modal: 投稿を削除する前に確認ダイアログを表示する setting_delete_modal: 投稿を削除する前に確認ダイアログを表示する
setting_disable_hover_cards: マウスオーバーでプロフィールをポップアップしない setting_disable_hover_cards: マウスオーバーでプロフィールをポップアップしない

View File

@@ -56,6 +56,7 @@ sv:
scopes: 'Vilka API: er applikationen kommer tillåtas åtkomst till. Om du väljer en omfattning på högstanivån behöver du inte välja individuella sådana.' scopes: 'Vilka API: er applikationen kommer tillåtas åtkomst till. Om du väljer en omfattning på högstanivån behöver du inte välja individuella sådana.'
setting_aggregate_reblogs: Visa inte nya boostar för inlägg som nyligen blivit boostade (påverkar endast nymottagna boostar) setting_aggregate_reblogs: Visa inte nya boostar för inlägg som nyligen blivit boostade (påverkar endast nymottagna boostar)
setting_always_send_emails: E-postnotiser kommer vanligtvis inte skickas när du aktivt använder Mastodon setting_always_send_emails: E-postnotiser kommer vanligtvis inte skickas när du aktivt använder Mastodon
setting_default_quote_policy: Nämnda användare får alltid citeras. Denna inställning kommer att träda i kraft för inlägg som skapats med nästa Mastodon-version, men förbereda dina inställningar för det redan nu
setting_default_sensitive: Känslig media döljs som standard och kan visas med ett klick setting_default_sensitive: Känslig media döljs som standard och kan visas med ett klick
setting_display_media_default: Dölj media markerad som känslig setting_display_media_default: Dölj media markerad som känslig
setting_display_media_hide_all: Dölj alltid all media setting_display_media_hide_all: Dölj alltid all media
@@ -75,6 +76,7 @@ sv:
filters: filters:
action: Välj vilken åtgärd som ska utföras när ett inlägg matchar filtret action: Välj vilken åtgärd som ska utföras när ett inlägg matchar filtret
actions: actions:
blur: Dölj media bakom en varning utan att dölja själva texten
hide: Dölj det filtrerade innehållet helt, beter sig som om det inte fanns hide: Dölj det filtrerade innehållet helt, beter sig som om det inte fanns
warn: Dölj det filtrerade innehållet bakom en varning som visar filtrets rubrik warn: Dölj det filtrerade innehållet bakom en varning som visar filtrets rubrik
form_admin_settings: form_admin_settings:
@@ -88,6 +90,7 @@ sv:
favicon: WEBP, PNG, GIF eller JPG. Används på mobila enheter istället för appens egen ikon. favicon: WEBP, PNG, GIF eller JPG. Används på mobila enheter istället för appens egen ikon.
mascot: Åsidosätter illustrationen i det avancerade webbgränssnittet. mascot: Åsidosätter illustrationen i det avancerade webbgränssnittet.
media_cache_retention_period: Mediafiler från inlägg som gjorts av fjärranvändare cachas på din server. När inställd på ett positivt värde kommer media att raderas efter det angivna antalet dagar. Om mediadatat begärs efter att det har raderats, kommer det att laddas ned igen om källinnehållet fortfarande är tillgängligt. På grund av begränsningar för hur ofta förhandsgranskningskort för länkar hämtas från tredjepartswebbplatser, rekommenderas det att ange detta värde till minst 14 dagar, annars kommer förhandsgranskningskorten inte att uppdateras på begäran före den tiden. media_cache_retention_period: Mediafiler från inlägg som gjorts av fjärranvändare cachas på din server. När inställd på ett positivt värde kommer media att raderas efter det angivna antalet dagar. Om mediadatat begärs efter att det har raderats, kommer det att laddas ned igen om källinnehållet fortfarande är tillgängligt. På grund av begränsningar för hur ofta förhandsgranskningskort för länkar hämtas från tredjepartswebbplatser, rekommenderas det att ange detta värde till minst 14 dagar, annars kommer förhandsgranskningskorten inte att uppdateras på begäran före den tiden.
min_age: Användare kommer att bli ombedda att bekräfta sitt födelsedatum under registreringen
peers_api_enabled: En lista över domänen den här servern har stött på i fediversum. Ingen data inkluderas om du har federerat med servern, bara att din server känner till den. Detta används av tjänster som samlar statistik om federering i allmänhet. peers_api_enabled: En lista över domänen den här servern har stött på i fediversum. Ingen data inkluderas om du har federerat med servern, bara att din server känner till den. Detta används av tjänster som samlar statistik om federering i allmänhet.
profile_directory: Profilkatalogen visar alla användare som har samtyckt till att bli upptäckbara. profile_directory: Profilkatalogen visar alla användare som har samtyckt till att bli upptäckbara.
require_invite_text: Gör fältet "Varför vill du gå med?" obligatoriskt när nyregistreringar kräver manuellt godkännande require_invite_text: Gör fältet "Varför vill du gå med?" obligatoriskt när nyregistreringar kräver manuellt godkännande
@@ -132,11 +135,23 @@ sv:
name: Du kan bara ändra skriftläget av bokstäverna, till exempel, för att göra det mer läsbart name: Du kan bara ändra skriftläget av bokstäverna, till exempel, för att göra det mer läsbart
terms_of_service: terms_of_service:
changelog: Kan struktureras med Markdown syntax. changelog: Kan struktureras med Markdown syntax.
effective_date: En rimlig tidsram kan vara allt från 10 till 30 dagar från det datum du meddelat dina användare.
text: Kan struktureras med Markdown syntax. text: Kan struktureras med Markdown syntax.
terms_of_service_generator: terms_of_service_generator:
admin_email: Juridiska meddelanden inkluderar motanmälningar, domstolsbeslut, begäran om borttagning och begäran från brottsbekämpande myndigheter.
arbitration_address: Ovan kan vara samma som fysisk adress, eller “N/A” om du använder e-post.
arbitration_website: Kan vara ett webbforum, eller ”N/A” om du använder e-post.
choice_of_law: Stad, region, territorium eller stat där det är de interna lagar som ska styra samtliga krav.
dmca_address: För amerikanska operatörer, använd den adress som är registrerad i DMCA:s katalog över utsedda agenter. En P.O. Box-lista finns tillgänglig på begäran; använd DMCA:s begäran om undantag för postbox för att mejla Copyright Office och förklara att du är en hembaserad innehållsmoderator som fruktar hämnd eller vedergällning för dina handlingar och behöver använda en P.O. Box för att ta bort din hemadress från offentlig visning.
dmca_email: Kan vara samma e-postadress som används för “E-postadress för juridiska meddelanden” ovan.
domain: Unik identifiering av onlinetjänsten du tillhandahåller.
jurisdiction: Lista det land där vem som än betalar räkningarna bor. Om det är ett företag eller annan enhet, lista landet där det är inkorporerat, och staden, regionen, territoriet eller staten på lämpligt sätt. jurisdiction: Lista det land där vem som än betalar räkningarna bor. Om det är ett företag eller annan enhet, lista landet där det är inkorporerat, och staden, regionen, territoriet eller staten på lämpligt sätt.
min_age: Bör inte vara lägre än den minimiålder som krävs enligt lagarna i din jurisdiktion.
user: user:
chosen_languages: Vid aktivering visas bara inlägg på dina valda språk i offentliga tidslinjer chosen_languages: Vid aktivering visas bara inlägg på dina valda språk i offentliga tidslinjer
date_of_birth:
one: Vi måste se till att du är minst %{count} för att använda Mastodon. Vi lagrar inte denna information.
other: Vi måste se till att du är minst %{count} för att använda Mastodon. Vi lagrar inte denna information.
role: Rollen styr vilka behörigheter användaren har. role: Rollen styr vilka behörigheter användaren har.
user_role: user_role:
color: Färgen som ska användas för rollen i användargränssnittet, som RGB i hex-format color: Färgen som ska användas för rollen i användargränssnittet, som RGB i hex-format
@@ -150,6 +165,7 @@ sv:
url: Dit händelser kommer skickas url: Dit händelser kommer skickas
labels: labels:
account: account:
attribution_domains: Webbplatser som är tillåtna att kreditera dig
discoverable: Presentera profil och inlägg med upptäcktsalgoritmer discoverable: Presentera profil och inlägg med upptäcktsalgoritmer
fields: fields:
name: Etikett name: Etikett
@@ -216,6 +232,7 @@ sv:
setting_boost_modal: Visa bekräftelsedialog innan boostning setting_boost_modal: Visa bekräftelsedialog innan boostning
setting_default_language: Inläggsspråk setting_default_language: Inläggsspråk
setting_default_privacy: Inläggsintegritet setting_default_privacy: Inläggsintegritet
setting_default_quote_policy: Vem kan citera
setting_default_sensitive: Markera alltid media som känsligt setting_default_sensitive: Markera alltid media som känsligt
setting_delete_modal: Visa bekräftelsedialog innan radering av inlägg setting_delete_modal: Visa bekräftelsedialog innan radering av inlägg
setting_disable_hover_cards: Inaktivera profilförhandsgranskning vid hovring setting_disable_hover_cards: Inaktivera profilförhandsgranskning vid hovring
@@ -226,6 +243,7 @@ sv:
setting_display_media_show_all: Visa alla setting_display_media_show_all: Visa alla
setting_expand_spoilers: Utöka alltid tutningar markerade med innehållsvarningar setting_expand_spoilers: Utöka alltid tutningar markerade med innehållsvarningar
setting_hide_network: Göm ditt nätverk setting_hide_network: Göm ditt nätverk
setting_missing_alt_text_modal: Visa bekräftelsedialog innan du skickar media utan alt-text
setting_reduce_motion: Minska rörelser i animationer setting_reduce_motion: Minska rörelser i animationer
setting_system_font_ui: Använd systemets standardfont setting_system_font_ui: Använd systemets standardfont
setting_system_scrollbars_ui: Använd systemets standardrullningsfält setting_system_scrollbars_ui: Använd systemets standardrullningsfält
@@ -247,6 +265,7 @@ sv:
name: Hashtag name: Hashtag
filters: filters:
actions: actions:
blur: Dölj media med en varning
hide: Dölj helt hide: Dölj helt
warn: Dölj med en varning warn: Dölj med en varning
form_admin_settings: form_admin_settings:
@@ -260,6 +279,7 @@ sv:
favicon: Favicon favicon: Favicon
mascot: Anpassad maskot (tekniskt arv) mascot: Anpassad maskot (tekniskt arv)
media_cache_retention_period: Tid för bibehållande av mediecache media_cache_retention_period: Tid för bibehållande av mediecache
min_age: Åldersgräns
peers_api_enabled: Publicera lista över upptäckta servrar i API:et peers_api_enabled: Publicera lista över upptäckta servrar i API:et
profile_directory: Aktivera profilkatalog profile_directory: Aktivera profilkatalog
registrations_mode: Vem kan registrera sig registrations_mode: Vem kan registrera sig
@@ -325,12 +345,17 @@ sv:
usable: Tillåt inlägg att använda denna fyrkantstagg usable: Tillåt inlägg att använda denna fyrkantstagg
terms_of_service: terms_of_service:
changelog: Vad har ändrats? changelog: Vad har ändrats?
effective_date: Datum för ikraftträdande
text: Användarvillkor text: Användarvillkor
terms_of_service_generator: terms_of_service_generator:
admin_email: E-postadress för juridiska meddelanden admin_email: E-postadress för juridiska meddelanden
arbitration_address: Fysisk adress för meddelanden om skiljeförfarande
arbitration_website: Webbplats för att skicka in skiljedomsmeddelanden
choice_of_law: Lagval
dmca_address: Fysisk adress för meddelanden om DMCA/upphovsrätt dmca_address: Fysisk adress för meddelanden om DMCA/upphovsrätt
dmca_email: Fysisk adress för meddelanden om DMCA/upphovsrätt dmca_email: Fysisk adress för meddelanden om DMCA/upphovsrätt
domain: Domän domain: Domän
jurisdiction: Rättslig jurisdiktion
min_age: Minimiålder min_age: Minimiålder
user: user:
date_of_birth_1i: Dag date_of_birth_1i: Dag

View File

@@ -115,7 +115,7 @@ services:
volumes: volumes:
- ./public/system:/mastodon/public/system - ./public/system:/mastodon/public/system
healthcheck: healthcheck:
test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 6' || false"] test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 7' || false"]
## Uncomment to enable federation with tor instances along with adding the following ENV variables ## Uncomment to enable federation with tor instances along with adding the following ENV variables
## http_hidden_proxy=http://privoxy:8118 ## http_hidden_proxy=http://privoxy:8118

View File

@@ -239,7 +239,7 @@ module Paperclip
end end
def rgb_to_hex(rgb) def rgb_to_hex(rgb)
format('#%02x%02x%02x', rgb.r, rgb.g, rgb.b) format('#%02x%02x%02x', rgb.r, rgb.g, rgb.b) # rubocop:disable Style/FormatStringToken
end end
end end
end end

View File

@@ -4,29 +4,60 @@ require 'rails_helper'
RSpec.describe ActivityPub::Activity::Remove do RSpec.describe ActivityPub::Activity::Remove do
let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured') } let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured') }
let(:status) { Fabricate(:status, account: sender) }
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Add',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: ActivityPub::TagManager.instance.uri_for(status),
target: sender.featured_collection_url,
}.with_indifferent_access
end
describe '#perform' do describe '#perform' do
subject { described_class.new(json, sender) } subject { described_class.new(json, sender) }
before do context 'when removing a pinned status' do
StatusPin.create!(account: sender, status: status) let(:status) { Fabricate(:status, account: sender) }
subject.perform
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Remove',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: ActivityPub::TagManager.instance.uri_for(status),
target: sender.featured_collection_url,
}.deep_stringify_keys
end
before do
StatusPin.create!(account: sender, status: status)
end
it 'removes a pin' do
expect { subject.perform }
.to change { sender.pinned?(status) }.to(false)
end
end end
it 'removes a pin' do context 'when removing a featured tag' do
expect(sender.pinned?(status)).to be false let(:tag) { Fabricate(:tag) }
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Remove',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: {
type: 'Hashtag',
name: "##{tag.display_name}",
href: "https://example.com/tags/#{tag.name}",
},
target: sender.featured_collection_url,
}.deep_stringify_keys
end
before do
sender.featured_tags.find_or_create_by!(tag: tag)
end
it 'removes a pin' do
expect { subject.perform }
.to change { sender.featured_tags.exists?(tag: tag) }.to(false)
end
end end
end end
end end

View File

@@ -69,284 +69,558 @@ RSpec.describe 'signature verification concern' do
end end
end end
context 'with an HTTP Signature from a known account' do context 'with an HTTP Signature (draft version)' 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 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 context 'with a valid signature on a GET request' do
let(:signature_header) 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 '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(response.parsed_body).to match(
signed_request: true,
signature_actor_id: actor.id.to_s
)
end
end end
it 'successfuly verifies signature', :aggregate_failures do context 'with a valid signature on a GET request that has a query string' 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' }) 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(response.parsed_body).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(response.parsed_body).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(response.parsed_body).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: {
'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(response.parsed_body).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(response.parsed_body).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(response.parsed_body).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(response.parsed_body).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(response.parsed_body).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(response.parsed_body).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(response.parsed_body).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(response.parsed_body).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: { get '/activitypub/success', headers: {
'Host' => 'www.example.com', 'Host' => 'www.example.com',
'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
'Signature' => signature_header, '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(response).to have_http_status(200)
expect(response.parsed_body).to match(
signed_request: true,
signature_actor_id: actor.id.to_s
)
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(response.parsed_body).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(response.parsed_body).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(response.parsed_body).to match( expect(response.parsed_body).to match(
signed_request: true, signed_request: true,
signature_actor_id: nil, signature_actor_id: nil,
error: anything error: 'Unable to fetch key JSON at https://remote.domain/users/alice#main-key'
)
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(response.parsed_body).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(response.parsed_body).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(response.parsed_body).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(response.parsed_body).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(response.parsed_body).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(response.parsed_body).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(response.parsed_body).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(response.parsed_body).to match(
signed_request: true,
signature_actor_id: nil,
error: anything
) )
end end
end end
end end
context 'with an inaccessible key' do context 'with an HTTP Message Signature (final RFC version)', feature: :http_message_signatures do
before do context 'with a known account' do
stub_request(:get, 'https://remote.domain/users/alice#main-key').to_return(status: 404) 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_input) do
'sig1=("@method" "@target-uri");created=1703066400;keyid="https://remote.domain/users/bob#main-key"'
end
let(:signature_header) do
'sig1=:WfM6q/qBqhUyqPUDt9metjadJGtLLpmMTBzk/t+R3byKe4/TGAXC6vBB/M6NsD5qv8GCmQGtisCMQxJQO0IGODGzi+Jv+eqDJ50agMVXNV6nUOzY44c4/XTPoI98qyx1oEMa4Hefy3vSYKq96iDVAc+RDLCMTeGP3wn9wizjD1SNmU0RZI1bTB+eCkywMP9mM5zXzUOYF+Qkuf+WdEpPR1XUGPlnqfdvPalcKVfaI/VThBjI91D/lmUGoa69x4EBEHM+aJmW6086e7/dVh+FndKkdGfXslZXFZKi2flTGQZgEWLn948SqAaJQROkJg8B14Sb1NONS1qZBhK3Mum8Pg==:' # rubocop:disable Layout/LineLength
end
it 'successfully verifies signature', :aggregate_failures do
get '/activitypub/success', headers: {
'Host' => 'www.example.com',
'Signature-Input' => signature_input,
'Signature' => signature_header,
}
expect(response).to have_http_status(200)
expect(response.parsed_body).to match(
signed_request: true,
signature_actor_id: actor.id.to_s
)
end
end
context 'with a valid signature on a GET request that has a query string' do
let(:signature_input) do
'sig1=("@method" "@target-uri");created=1703066400;keyid="https://remote.domain/users/bob#main-key"'
end
let(:signature_header) do
'sig1=:JbC0oqoruKkT5p3bTASZZbHQP+EwUmT6+vBjEBw/KSkLPn+tKjEj0HHIMLA2Rw3bshZyzmsVD8+2UkPcwZYnE3gzuX0r0/gC8v4dSBfwGe7EBwpekB2xU8yHW4jawxiof2LmErvEocqcnI2uiA4IlJ09uz2Os/ARmf60lj+0Qf1qqzFeM7KoXJ331BUGMJ4cQ7iS4aO9RG4P8EJ+upe7Ik1LB/q9CZmk/6MFaB2lIemV0pcg2MwctpzMw9GWN1wL10hGxx+BPT2WCXdlPQmetVSoJ89WVV8S/4lQaCA1IucYUVDvBEFgMM//VJBuw7kg8wSTeAg9oKzbR2otLqv8Lg==:' # rubocop:disable Layout/LineLength
end
it 'successfuly verifies signature', :aggregate_failures do
get '/activitypub/success?foo=42', headers: {
'Host' => 'www.example.com',
'Signature-Input' => signature_input,
'Signature' => signature_header,
}
expect(response).to have_http_status(200)
expect(response.parsed_body).to match(
signed_request: true,
signature_actor_id: actor.id.to_s
)
end
end
context 'with mismatching query string' do
let(:signature_input) do
'sig1=("@method" "@target-uri");created=1703066400;keyid="https://remote.domain/users/bob#main-key"'
end
let(:signature_header) do
'sig1=:JbC0oqoruKkT5p3bTASZZbHQP+EwUmT6+vBjEBw/KSkLPn+tKjEj0HHIMLA2Rw3bshZyzmsVD8+2UkPcwZYnE3gzuX0r0/gC8v4dSBfwGe7EBwpekB2xU8yHW4jawxiof2LmErvEocqcnI2uiA4IlJ09uz2Os/ARmf60lj+0Qf1qqzFeM7KoXJ331BUGMJ4cQ7iS4aO9RG4P8EJ+upe7Ik1LB/q9CZmk/6MFaB2lIemV0pcg2MwctpzMw9GWN1wL10hGxx+BPT2WCXdlPQmetVSoJ89WVV8S/4lQaCA1IucYUVDvBEFgMM//VJBuw7kg8wSTeAg9oKzbR2otLqv8Lg==:' # rubocop:disable Layout/LineLength
end
it 'fails to verify signature', :aggregate_failures do
get '/activitypub/success?foo=43', headers: {
'Host' => 'www.example.com',
'Signature-Input' => signature_input,
'Signature' => signature_header,
}
expect(response.parsed_body).to match(
signed_request: true,
signature_actor_id: nil,
error: anything
)
end
end
context 'with a mismatching path' do
let(:signature_input) do
'sig1=("@method" "@target-uri");created=1703066400;keyid="https://remote.domain/users/bob#main-key"'
end
let(:signature_header) do
'sig1=:WfM6q/qBqhUyqPUDt9metjadJGtLLpmMTBzk/t+R3byKe4/TGAXC6vBB/M6NsD5qv8GCmQGtisCMQxJQO0IGODGzi+Jv+eqDJ50agMVXNV6nUOzY44c4/XTPoI98qyx1oEMa4Hefy3vSYKq96iDVAc+RDLCMTeGP3wn9wizjD1SNmU0RZI1bTB+eCkywMP9mM5zXzUOYF+Qkuf+WdEpPR1XUGPlnqfdvPalcKVfaI/VThBjI91D/lmUGoa69x4EBEHM+aJmW6086e7/dVh+FndKkdGfXslZXFZKi2flTGQZgEWLn948SqAaJQROkJg8B14Sb1NONS1qZBhK3Mum8Pg==:' # rubocop:disable Layout/LineLength
end
it 'fails to verify signature', :aggregate_failures do
get '/activitypub/alternative-path', headers: {
'Host' => 'www.example.com',
'Signature-Input' => signature_input,
'Signature' => signature_header,
}
expect(response.parsed_body).to match(
signed_request: true,
signature_actor_id: nil,
error: anything
)
end
end
context 'with a mismatching method' do
let(:signature_input) do
'sig1=("@method" "@target-uri");created=1703066400;keyid="https://remote.domain/users/bob#main-key"'
end
let(:signature_header) do
'sig1=:WfM6q/qBqhUyqPUDt9metjadJGtLLpmMTBzk/t+R3byKe4/TGAXC6vBB/M6NsD5qv8GCmQGtisCMQxJQO0IGODGzi+Jv+eqDJ50agMVXNV6nUOzY44c4/XTPoI98qyx1oEMa4Hefy3vSYKq96iDVAc+RDLCMTeGP3wn9wizjD1SNmU0RZI1bTB+eCkywMP9mM5zXzUOYF+Qkuf+WdEpPR1XUGPlnqfdvPalcKVfaI/VThBjI91D/lmUGoa69x4EBEHM+aJmW6086e7/dVh+FndKkdGfXslZXFZKi2flTGQZgEWLn948SqAaJQROkJg8B14Sb1NONS1qZBhK3Mum8Pg==:' # rubocop:disable Layout/LineLength
end
it 'fails to verify signature', :aggregate_failures do
post '/activitypub/success', headers: {
'Host' => 'www.example.com',
'Signature-Input' => signature_input,
'Signature' => signature_header,
}
expect(response.parsed_body).to match(
signed_request: true,
signature_actor_id: nil,
error: anything
)
end
end
context 'with a request older than a day' do
let(:signature_input) do
'sig1=("@method" "@target-uri");created=1702893600;keyid="https://remote.domain/users/bob#main-key"'
end
let(:signature_header) do
'sig1=:LtvwxwAAyiP7fGzsgKDRUHaZNNclAq6ScmH7RY+KERgaJcrjHFuqaYraQ3d9JVNsDJzZhdtJs+7UDPfIUjYNwWj/5KRQscB2sMQ9+EYR2tBDen+K5TILv/SXoWUdvVU/3vbGMiVIACgynaXokySNrE8AGFWdrzT5NbxE+/pJ0tkB3uWO7LfFpm0ipzo0NN07CGC2AUVl6WxsiTGWtFRqVrrHFmYmRcVYn7NxkKytx8eDg95cyIsB4xAHz8i++NqZHiXaooh79OdhOy10kMWHFDbuy/AijjI3aGtGriAbXdxb8O3nwoSCvfEJ5f7qQ+iMJl/fLvFOUZElZgRo3mk2lA==:' # rubocop:disable Layout/LineLength
end
it 'fails to verify signature', :aggregate_failures do
get '/activitypub/success', headers: {
'Host' => 'www.example.com',
'Signature-Input' => signature_input,
'Signature' => signature_header,
}
expect(response.parsed_body).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_input) do
'sig1=("@method" "@target-uri" "content-digest");created=1703066400;keyid="https://remote.domain/users/bob#main-key"'
end
let(:signature_header) do
'sig1=:c4jGY/PnOV4CwyvNnAmY6NLX0sf6EtbKu7kYseNARRZaq128PrP0GNQ4cd3XsX9cbMfJMw1ntI4zuEC81ncW8g+90OHP02bX0LkT57RweUtN4CSA01hRqSVe/MW32tjGixCiItvWqjNHoIZnZApu1bd+M3zMR+VCEue4/8a0D2eRrvfQxJUUBXZR1ZTRFlf1LNFDW3U7cuTbAKYr2zWVr7on+h2vA+vzEND9WE8z1SHd6SIFFgP0QRqrCXYx+vsTs3aLusTsamRWissoycJGexb64mI9iqiD8SD+uN1xk6iRU3nkUmhUquugjlOFyjxbbLo5ZnYjsECMt/BW+Catxw==:' # rubocop:disable Layout/LineLength
end
it 'successfuly verifies signature', :aggregate_failures do
post '/activitypub/success', params: 'Hello world', headers: {
'Host' => 'www.example.com',
'Content-Digest' => digest_header,
'Signature-Input' => signature_input,
'Signature' => signature_header,
}
expect(response).to have_http_status(200)
expect(response.parsed_body).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_input) do
'sig1=("@method" "@target-uri");created=1703066400;keyid="https://remote.domain/users/bob#main-key"'
end
let(:signature_header) do
'sig1=:Rhc5CzxdUYCzwo3V7y5wjEIN4o2XD90Bhf7lTDg2TIlp33ygl6ufwZpQ156fLJ0aUCkJ4+9KQsHBIkxF4PZJn8d/ZIfz3dpHJAVyMErAToSw+36V61mbnnnJxIPZPvmTT3zYCL7HPv+3GItOA4SqBhjJZRRJwOIW6NmmyrmSpc8xF9klnkeyGbYYRusaG7w6BDzM7ECCttxk120v1rHkGyqVON9fQADqs2LNqPa9WM9kWKiC5LhnZSYgoojhPmhniiA4NpgprncEBo4dOIC8CJihafWVSf+CZp3eogb/hn3Yd0//Pz0ta/lVtLGdb7t9f0rQFiqZfwIcCCz51nDMKw==:' # rubocop:disable Layout/LineLength
end
it 'fails to verify signature', :aggregate_failures do
post '/activitypub/success', params: 'Hello world', headers: {
'Host' => 'www.example.com',
'Content-Digest' => digest_header,
'Signature-Input' => signature_input,
'Signature' => signature_header,
}
expect(response.parsed_body).to match(
signed_request: true,
signature_actor_id: nil,
error: 'Mastodon requires the Content-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_input) do
'sig1=("@method" "@target-uri" "content-digest");created=1703066400;keyid="https://remote.domain/users/bob#main-key"'
end
let(:signature_header) do
'sig1=:c4jGY/PnOV4CwyvNnAmY6NLX0sf6EtbKu7kYseNARRZaq128PrP0GNQ4cd3XsX9cbMfJMw1ntI4zuEC81ncW8g+90OHP02bX0LkT57RweUtN4CSA01hRqSVe/MW32tjGixCiItvWqjNHoIZnZApu1bd+M3zMR+VCEue4/8a0D2eRrvfQxJUUBXZR1ZTRFlf1LNFDW3U7cuTbAKYr2zWVr7on+h2vA+vzEND9WE8z1SHd6SIFFgP0QRqrCXYx+vsTs3aLusTsamRWissoycJGexb64mI9iqiD8SD+uN1xk6iRU3nkUmhUquugjlOFyjxbbLo5ZnYjsECMt/BW+Catxw==:' # rubocop:disable Layout/LineLength
end
it 'fails to verify signature', :aggregate_failures do
post '/activitypub/success', params: 'Hello world!', headers: {
'Host' => 'www.example.com',
'Content-Digest' => digest_header,
'Signature-Input' => signature_input,
'Signature' => signature_header,
}
expect(response.parsed_body).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
let(:digest_header) { 'sha-256=:ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=:' }
let(:signature_input) do
'sig1=("@method" "@target-uri" "content-digest");created=1703066400;keyid="https://remote.domain/users/bob#main-key"'
end
let(:signature_header) do
'sig1=:aJmJgCOuAJD1su2QeeD7Y9wfda8dqReyuau1EBWAz1DYwKWx5kVosONGgJb+XZFugh6CQ15XDFz0vRJRYdt6GyquloOFYLzPYWp3mYRlMhvehR64ALeGIwJbb460/tOeX2PwaFNVBrqLBAHf8PZDAPCxE8Q9cPWhewwQQirBZTm0xhOy8nRkSEfMish87JEQLkEzH+pZQDYIpv+oE+Tz6gow6bllCmjUd8vgLABpc7sZJTz5qklfOMqFczW6HvVxvQK/9G7V509u2z5I2PC/q+XdEs+jC0uzuer5baTJgL2q37gvnKzmz7pB+kbtBz5tmGNEMVLtPYQIEYjVI4Y34Q==:' # rubocop:disable Layout/LineLength
end
it 'fails to verify signature', :aggregate_failures do
post '/activitypub/alternative-path', params: 'Hello world', headers: {
'Host' => 'www.example.com',
'Content-Digest' => digest_header,
'Signature-Input' => signature_input,
'Signature' => signature_header,
}
expect(response).to have_http_status(200)
expect(response.parsed_body).to match(
signed_request: true,
signature_actor_id: nil,
error: anything
)
end
end
end end
it 'fails to verify signature', :aggregate_failures do context 'with an inaccessible key' do
get '/activitypub/success', headers: { let(:signature_input) do
'Host' => 'www.example.com', 'sig1=("@method" "@target-uri");created=1703066400;keyid="https://remote.domain/users/alice#main-key"'
'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', end
'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 let(:signature_header) do
} 'sig1=:WfM6q/qBqhUyqPUDt9metjadJGtLLpmMTBzk/t+R3byKe4/TGAXC6vBB/M6NsD5qv8GCmQGtisCMQxJQO0IGODGzi+Jv+eqDJ50agMVXNV6nUOzY44c4/XTPoI98qyx1oEMa4Hefy3vSYKq96iDVAc+RDLCMTeGP3wn9wizjD1SNmU0RZI1bTB+eCkywMP9mM5zXzUOYF+Qkuf+WdEpPR1XUGPlnqfdvPalcKVfaI/VThBjI91D/lmUGoa69x4EBEHM+aJmW6086e7/dVh+FndKkdGfXslZXFZKi2flTGQZgEWLn948SqAaJQROkJg8B14Sb1NONS1qZBhK3Mum8Pg==:' # rubocop:disable Layout/LineLength
end
expect(response.parsed_body).to match( before do
signed_request: true, stub_request(:get, 'https://remote.domain/users/alice#main-key').to_return(status: 404)
signature_actor_id: nil, end
error: 'Unable to fetch key JSON at https://remote.domain/users/alice#main-key'
) it 'fails to verify signature', :aggregate_failures do
get '/activitypub/success', headers: {
'Host' => 'www.example.com',
'Signature-Input' => signature_input,
'Signature' => signature_header,
}
expect(response.parsed_body).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 end
end end

118
yarn.lock
View File

@@ -3524,11 +3524,11 @@ __metadata:
linkType: hard linkType: hard
"@types/cors@npm:^2.8.16": "@types/cors@npm:^2.8.16":
version: 2.8.17 version: 2.8.18
resolution: "@types/cors@npm:2.8.17" resolution: "@types/cors@npm:2.8.18"
dependencies: dependencies:
"@types/node": "npm:*" "@types/node": "npm:*"
checksum: 10c0/457364c28c89f3d9ed34800e1de5c6eaaf344d1bb39af122f013322a50bc606eb2aa6f63de4e41a7a08ba7ef454473926c94a830636723da45bf786df032696d checksum: 10c0/9dd1075de0e3a40c304826668960c797e67e597a734fb8e8ab404561f31ef2bd553ef5500eb86da7e91a344bee038a59931d2fbf182fbce09f13816f51fdd80e
languageName: node languageName: node
linkType: hard linkType: hard
@@ -3585,14 +3585,14 @@ __metadata:
linkType: hard linkType: hard
"@types/express@npm:^4.17.17": "@types/express@npm:^4.17.17":
version: 4.17.21 version: 4.17.22
resolution: "@types/express@npm:4.17.21" resolution: "@types/express@npm:4.17.22"
dependencies: dependencies:
"@types/body-parser": "npm:*" "@types/body-parser": "npm:*"
"@types/express-serve-static-core": "npm:^4.17.33" "@types/express-serve-static-core": "npm:^4.17.33"
"@types/qs": "npm:*" "@types/qs": "npm:*"
"@types/serve-static": "npm:*" "@types/serve-static": "npm:*"
checksum: 10c0/12e562c4571da50c7d239e117e688dc434db1bac8be55613294762f84fd77fbd0658ccd553c7d3ab02408f385bc93980992369dd30e2ecd2c68c358e6af8fabf checksum: 10c0/15c10a5ebb40a0356baa95ed374a2150d862786c9fccbdd724df12acc9c8cb08fbe1d34b446b1bcef2dbe5305cb3013fb39fba791baa54ef6df8056482776abb
languageName: node languageName: node
linkType: hard linkType: hard
@@ -3665,9 +3665,9 @@ __metadata:
linkType: hard linkType: hard
"@types/lodash@npm:^4.14.195": "@types/lodash@npm:^4.14.195":
version: 4.17.16 version: 4.17.17
resolution: "@types/lodash@npm:4.17.16" resolution: "@types/lodash@npm:4.17.17"
checksum: 10c0/cf017901b8ab1d7aabc86d5189d9288f4f99f19a75caf020c0e2c77b8d4cead4db0d0b842d009b029339f92399f49f34377dd7c2721053388f251778b4c23534 checksum: 10c0/8e75df02a15f04d4322c5a503e4efd0e7a92470570ce80f17e9f11ce2b1f1a7c994009c9bcff39f07e0f9ffd8ccaff09b3598997c404b801abd5a7eee5a639dc
languageName: node languageName: node
linkType: hard linkType: hard
@@ -3709,13 +3709,13 @@ __metadata:
linkType: hard linkType: hard
"@types/pg@npm:^8.6.6": "@types/pg@npm:^8.6.6":
version: 8.11.11 version: 8.15.4
resolution: "@types/pg@npm:8.11.11" resolution: "@types/pg@npm:8.15.4"
dependencies: dependencies:
"@types/node": "npm:*" "@types/node": "npm:*"
pg-protocol: "npm:*" pg-protocol: "npm:*"
pg-types: "npm:^4.0.1" pg-types: "npm:^2.2.0"
checksum: 10c0/18c2585e1ba7a5dd5f849d49410d53fdfe9a6c3cbc4ae46c51fd728264d6ecf9a84a5cd82d89cb1f870a74383bad88effce1eed888f16accbcbde56a53d23a69 checksum: 10c0/7f9295cb2d934681bba84f7caad529c3b100d87e83ad0732c7fe496f4f79e42a795097321db54e010fcff22cb5e410cf683b4c9941907ee4564c822242816e91
languageName: node languageName: node
linkType: hard linkType: hard
@@ -3762,11 +3762,11 @@ __metadata:
linkType: hard linkType: hard
"@types/react-dom@npm:^18.2.4": "@types/react-dom@npm:^18.2.4":
version: 18.3.5 version: 18.3.7
resolution: "@types/react-dom@npm:18.3.5" resolution: "@types/react-dom@npm:18.3.7"
peerDependencies: peerDependencies:
"@types/react": ^18.0.0 "@types/react": ^18.0.0
checksum: 10c0/b163d35a6b32a79f5782574a7aeb12a31a647e248792bf437e6d596e2676961c394c5e3c6e91d1ce44ae90441dbaf93158efb4f051c0d61e2612f1cb04ce4faa checksum: 10c0/8bd309e2c3d1604a28a736a24f96cbadf6c05d5288cfef8883b74f4054c961b6b3a5e997fd5686e492be903c8f3380dba5ec017eff3906b1256529cd2d39603e
languageName: node languageName: node
linkType: hard linkType: hard
@@ -3856,12 +3856,12 @@ __metadata:
linkType: hard linkType: hard
"@types/react@npm:^18.2.7": "@types/react@npm:^18.2.7":
version: 18.3.19 version: 18.3.23
resolution: "@types/react@npm:18.3.19" resolution: "@types/react@npm:18.3.23"
dependencies: dependencies:
"@types/prop-types": "npm:*" "@types/prop-types": "npm:*"
csstype: "npm:^3.0.2" csstype: "npm:^3.0.2"
checksum: 10c0/236bfe0c4748ada1a640f13573eca3e0fc7c9d847b442947adb352b0718d6d285357fd84c33336c8ffb8cbfabc0d58a43a647c7fd79857fecd61fb58ab6f7918 checksum: 10c0/49331800b76572eb2992a5c44801dbf8c612a5f99c8f4e4200f06c7de6f3a6e9455c661784a6c5469df96fa45622cb4a9d0982c44e6a0d5719be5f2ef1f545ed
languageName: node languageName: node
linkType: hard linkType: hard
@@ -3939,11 +3939,11 @@ __metadata:
linkType: hard linkType: hard
"@types/ws@npm:^8.5.9": "@types/ws@npm:^8.5.9":
version: 8.18.0 version: 8.18.1
resolution: "@types/ws@npm:8.18.0" resolution: "@types/ws@npm:8.18.1"
dependencies: dependencies:
"@types/node": "npm:*" "@types/node": "npm:*"
checksum: 10c0/a56d2e0d1da7411a1f3548ce02b51a50cbe9e23f025677d03df48f87e4a3c72e1342fbf1d12e487d7eafa8dc670c605152b61bbf9165891ec0e9694b0d3ea8d4 checksum: 10c0/61aff1129143fcc4312f083bc9e9e168aa3026b7dd6e70796276dcfb2c8211c4292603f9c4864fae702f2ed86e4abd4d38aa421831c2fd7f856c931a481afbab
languageName: node languageName: node
linkType: hard linkType: hard
@@ -8831,13 +8831,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"obuf@npm:~1.1.2":
version: 1.1.2
resolution: "obuf@npm:1.1.2"
checksum: 10c0/520aaac7ea701618eacf000fc96ae458e20e13b0569845800fc582f81b386731ab22d55354b4915d58171db00e79cfcd09c1638c02f89577ef092b38c65b7d81
languageName: node
linkType: hard
"on-exit-leak-free@npm:^2.1.0": "on-exit-leak-free@npm:^2.1.0":
version: 2.1.2 version: 2.1.2
resolution: "on-exit-leak-free@npm:2.1.2" resolution: "on-exit-leak-free@npm:2.1.2"
@@ -9101,13 +9094,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"pg-numeric@npm:1.0.2":
version: 1.0.2
resolution: "pg-numeric@npm:1.0.2"
checksum: 10c0/43dd9884e7b52c79ddc28d2d282d7475fce8bba13452d33c04ceb2e0a65f561edf6699694e8e1c832ff9093770496363183c950dd29608e1bdd98f344b25bca9
languageName: node
linkType: hard
"pg-pool@npm:^3.10.0": "pg-pool@npm:^3.10.0":
version: 3.10.0 version: 3.10.0
resolution: "pg-pool@npm:3.10.0" resolution: "pg-pool@npm:3.10.0"
@@ -9124,7 +9110,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"pg-types@npm:2.2.0": "pg-types@npm:2.2.0, pg-types@npm:^2.2.0":
version: 2.2.0 version: 2.2.0
resolution: "pg-types@npm:2.2.0" resolution: "pg-types@npm:2.2.0"
dependencies: dependencies:
@@ -9137,21 +9123,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"pg-types@npm:^4.0.1":
version: 4.0.1
resolution: "pg-types@npm:4.0.1"
dependencies:
pg-int8: "npm:1.0.1"
pg-numeric: "npm:1.0.2"
postgres-array: "npm:~3.0.1"
postgres-bytea: "npm:~3.0.0"
postgres-date: "npm:~2.0.1"
postgres-interval: "npm:^3.0.0"
postgres-range: "npm:^1.1.1"
checksum: 10c0/e2126b2775554ae8bacb3b104814487c2af2caff44cc52bee786b3887c65fe4c1fe031237e51e30ffed1cbb13b71776bd60cc1e65ac800c9946df4030849a074
languageName: node
linkType: hard
"pg@npm:^8.5.0": "pg@npm:^8.5.0":
version: 8.16.0 version: 8.16.0
resolution: "pg@npm:8.16.0" resolution: "pg@npm:8.16.0"
@@ -9723,13 +9694,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"postgres-array@npm:~3.0.1":
version: 3.0.2
resolution: "postgres-array@npm:3.0.2"
checksum: 10c0/644aa071f67a66a59f641f8e623887d2b915bc102a32643e2aa8b54c11acd343c5ad97831ea444dd37bd4b921ba35add4aa2cb0c6b76700a8252c2324aeba5b4
languageName: node
linkType: hard
"postgres-bytea@npm:~1.0.0": "postgres-bytea@npm:~1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "postgres-bytea@npm:1.0.0" resolution: "postgres-bytea@npm:1.0.0"
@@ -9737,15 +9701,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"postgres-bytea@npm:~3.0.0":
version: 3.0.0
resolution: "postgres-bytea@npm:3.0.0"
dependencies:
obuf: "npm:~1.1.2"
checksum: 10c0/41c79cc48aa730c5ba3eda6ab989a940034f07a1f57b8f2777dce56f1b8cca16c5870582932b5b10cc605048aef9b6157e06253c871b4717cafc6d00f55376aa
languageName: node
linkType: hard
"postgres-date@npm:~1.0.4": "postgres-date@npm:~1.0.4":
version: 1.0.7 version: 1.0.7
resolution: "postgres-date@npm:1.0.7" resolution: "postgres-date@npm:1.0.7"
@@ -9753,13 +9708,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"postgres-date@npm:~2.0.1":
version: 2.0.1
resolution: "postgres-date@npm:2.0.1"
checksum: 10c0/2d3698958f858b7d1df0a3929fb8750ccb43fa2c8ee9fec7a021e7926291f6c85ddd9d94d87cd6529d70bd2444f3e14fb5bb323af19ceaa733542cc05c5c653a
languageName: node
linkType: hard
"postgres-interval@npm:^1.1.0": "postgres-interval@npm:^1.1.0":
version: 1.2.0 version: 1.2.0
resolution: "postgres-interval@npm:1.2.0" resolution: "postgres-interval@npm:1.2.0"
@@ -9769,20 +9717,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"postgres-interval@npm:^3.0.0":
version: 3.0.0
resolution: "postgres-interval@npm:3.0.0"
checksum: 10c0/8b570b30ea37c685e26d136d34460f246f98935a1533defc4b53bb05ee23ae3dc7475b718ec7ea607a57894d8c6b4f1adf67ca9cc83a75bdacffd427d5c68de8
languageName: node
linkType: hard
"postgres-range@npm:^1.1.1":
version: 1.1.3
resolution: "postgres-range@npm:1.1.3"
checksum: 10c0/f46bc379a198a9e3282a222c8e432d77494854bd4fa0706dff01641846db0bf4f09a9723e7fbb202da34ec3b2d88fc50e26e4bbeded7df19646e3acd6a7465ce
languageName: node
linkType: hard
"prelude-ls@npm:^1.2.1": "prelude-ls@npm:^1.2.1":
version: 1.2.1 version: 1.2.1
resolution: "prelude-ls@npm:1.2.1" resolution: "prelude-ls@npm:1.2.1"
@@ -10862,8 +10796,8 @@ __metadata:
linkType: hard linkType: hard
"sass@npm:^1.62.1": "sass@npm:^1.62.1":
version: 1.89.0 version: 1.89.1
resolution: "sass@npm:1.89.0" resolution: "sass@npm:1.89.1"
dependencies: dependencies:
"@parcel/watcher": "npm:^2.4.1" "@parcel/watcher": "npm:^2.4.1"
chokidar: "npm:^4.0.0" chokidar: "npm:^4.0.0"
@@ -10874,7 +10808,7 @@ __metadata:
optional: true optional: true
bin: bin:
sass: sass.js sass: sass.js
checksum: 10c0/8e31b48c5e0abd9d437edb201919a82863f605ace1c1536ccdc6b3133937a70fe29b92ad536af632ecae2f140733931e4ec971e1bfbf8b835c54d392b28331df checksum: 10c0/4406c441f71f5e0ec056345e6091cef51fd1786dac6e5b3eb1e87df464c40b3278219cc51df98309c36794503787ab0fe88fd45f2b0464f22000b68e755883ee
languageName: node languageName: node
linkType: hard linkType: hard