Add support for standard webpush (#33528)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
ee4edbb94f
commit
4a2813158d
12 changed files with 167 additions and 46 deletions
|
@ -21,6 +21,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
|||
endpoint: subscription_params[:endpoint],
|
||||
key_p256dh: subscription_params[:keys][:p256dh],
|
||||
key_auth: subscription_params[:keys][:auth],
|
||||
standard: subscription_params[:standard] || false,
|
||||
data: data_params,
|
||||
user_id: current_user.id,
|
||||
access_token_id: doorkeeper_token.id
|
||||
|
@ -55,7 +56,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
|||
end
|
||||
|
||||
def subscription_params
|
||||
params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
|
||||
params.require(:subscription).permit(:endpoint, :standard, keys: [:auth, :p256dh])
|
||||
end
|
||||
|
||||
def data_params
|
||||
|
|
|
@ -66,7 +66,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||
end
|
||||
|
||||
def subscription_params
|
||||
@subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
|
||||
@subscription_params ||= params.require(:subscription).permit(:standard, :endpoint, keys: [:auth, :p256dh])
|
||||
end
|
||||
|
||||
def web_push_subscription_params
|
||||
|
@ -76,6 +76,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||
endpoint: subscription_params[:endpoint],
|
||||
key_auth: subscription_params[:keys][:auth],
|
||||
key_p256dh: subscription_params[:keys][:p256dh],
|
||||
standard: subscription_params[:standard] || false,
|
||||
user_id: active_session.user_id,
|
||||
}
|
||||
end
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
class WebPushRequest
|
||||
SIGNATURE_ALGORITHM = 'p256ecdsa'
|
||||
AUTH_HEADER = 'WebPush'
|
||||
LEGACY_AUTH_HEADER = 'WebPush'
|
||||
STANDARD_AUTH_HEADER = 'vapid'
|
||||
PAYLOAD_EXPIRATION = 24.hours
|
||||
JWT_ALGORITHM = 'ES256'
|
||||
JWT_TYPE = 'JWT'
|
||||
|
@ -10,6 +11,7 @@ class WebPushRequest
|
|||
attr_reader :web_push_subscription
|
||||
|
||||
delegate(
|
||||
:standard,
|
||||
:endpoint,
|
||||
:key_auth,
|
||||
:key_p256dh,
|
||||
|
@ -24,20 +26,36 @@ class WebPushRequest
|
|||
@audience ||= Addressable::URI.parse(endpoint).normalized_site
|
||||
end
|
||||
|
||||
def authorization_header
|
||||
[AUTH_HEADER, encoded_json_web_token].join(' ')
|
||||
def legacy_authorization_header
|
||||
[LEGACY_AUTH_HEADER, encoded_json_web_token].join(' ')
|
||||
end
|
||||
|
||||
def crypto_key_header
|
||||
[SIGNATURE_ALGORITHM, vapid_key.public_key_for_push_header].join('=')
|
||||
end
|
||||
|
||||
def encrypt(payload)
|
||||
def legacy_encrypt(payload)
|
||||
Webpush::Legacy::Encryption.encrypt(payload, key_p256dh, key_auth)
|
||||
end
|
||||
|
||||
def standard_authorization_header
|
||||
[STANDARD_AUTH_HEADER, standard_vapid_value].join(' ')
|
||||
end
|
||||
|
||||
def standard_encrypt(payload)
|
||||
Webpush::Encryption.encrypt(payload, key_p256dh, key_auth)
|
||||
end
|
||||
|
||||
def legacy
|
||||
!standard
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def standard_vapid_value
|
||||
"t=#{encoded_json_web_token},k=#{vapid_key.public_key_for_push_header}"
|
||||
end
|
||||
|
||||
def encoded_json_web_token
|
||||
JWT.encode(
|
||||
web_token_payload,
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
# Table name: web_push_subscriptions
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# endpoint :string not null
|
||||
# key_p256dh :string not null
|
||||
# key_auth :string not null
|
||||
# data :json
|
||||
# endpoint :string not null
|
||||
# key_auth :string not null
|
||||
# key_p256dh :string not null
|
||||
# standard :boolean default(FALSE), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# access_token_id :bigint(8)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::WebPushSubscriptionSerializer < ActiveModel::Serializer
|
||||
attributes :id, :endpoint, :alerts, :server_key, :policy
|
||||
attributes :id, :endpoint, :standard, :alerts, :server_key, :policy
|
||||
|
||||
delegate :standard, to: :object
|
||||
|
||||
def alerts
|
||||
(object.data&.dig('alerts') || {}).each_with_object({}) { |(k, v), h| h[k] = ActiveModel::Type::Boolean.new.cast(v) }
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class WebPushKeyValidator < ActiveModel::Validator
|
||||
def validate(subscription)
|
||||
begin
|
||||
Webpush::Legacy::Encryption.encrypt('validation_test', subscription.key_p256dh, subscription.key_auth)
|
||||
Webpush::Encryption.encrypt('validation_test', subscription.key_p256dh, subscription.key_auth)
|
||||
rescue ArgumentError, OpenSSL::PKey::EC::Point::Error
|
||||
subscription.errors.add(:base, I18n.t('crypto.errors.invalid_key'))
|
||||
end
|
||||
|
|
|
@ -19,7 +19,19 @@ class Web::PushNotificationWorker
|
|||
# in the meantime, so we have to double-check before proceeding
|
||||
return unless @notification.activity.present? && @subscription.pushable?(@notification)
|
||||
|
||||
payload = web_push_request.encrypt(push_notification_json)
|
||||
if web_push_request.legacy
|
||||
perform_legacy_request
|
||||
else
|
||||
perform_standard_request
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def perform_legacy_request
|
||||
payload = web_push_request.legacy_encrypt(push_notification_json)
|
||||
|
||||
request_pool.with(web_push_request.audience) do |http_client|
|
||||
request = Request.new(:post, web_push_request.endpoint, body: payload.fetch(:ciphertext), http_client: http_client)
|
||||
|
@ -31,28 +43,48 @@ class Web::PushNotificationWorker
|
|||
'Content-Encoding' => 'aesgcm',
|
||||
'Encryption' => "salt=#{Webpush.encode64(payload.fetch(:salt)).delete('=')}",
|
||||
'Crypto-Key' => "dh=#{Webpush.encode64(payload.fetch(:server_public_key)).delete('=')};#{web_push_request.crypto_key_header}",
|
||||
'Authorization' => web_push_request.authorization_header,
|
||||
'Authorization' => web_push_request.legacy_authorization_header,
|
||||
'Unsubscribe-URL' => subscription_url
|
||||
)
|
||||
|
||||
request.perform do |response|
|
||||
# If the server responds with an error in the 4xx range
|
||||
# that isn't about rate-limiting or timeouts, we can
|
||||
# assume that the subscription is invalid or expired
|
||||
# and must be removed
|
||||
|
||||
if (400..499).cover?(response.code) && ![408, 429].include?(response.code)
|
||||
@subscription.destroy!
|
||||
elsif !(200...300).cover?(response.code)
|
||||
raise Mastodon::UnexpectedResponseError, response
|
||||
end
|
||||
end
|
||||
send(request)
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
def perform_standard_request
|
||||
payload = web_push_request.standard_encrypt(push_notification_json)
|
||||
|
||||
request_pool.with(web_push_request.audience) do |http_client|
|
||||
request = Request.new(:post, web_push_request.endpoint, body: payload, http_client: http_client)
|
||||
|
||||
request.add_headers(
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Ttl' => TTL.to_s,
|
||||
'Urgency' => URGENCY,
|
||||
'Content-Encoding' => 'aes128gcm',
|
||||
'Authorization' => web_push_request.standard_authorization_header,
|
||||
'Unsubscribe-URL' => subscription_url,
|
||||
'Content-Length' => payload.length.to_s
|
||||
)
|
||||
|
||||
send(request)
|
||||
end
|
||||
end
|
||||
|
||||
def send(request)
|
||||
request.perform do |response|
|
||||
# If the server responds with an error in the 4xx range
|
||||
# that isn't about rate-limiting or timeouts, we can
|
||||
# assume that the subscription is invalid or expired
|
||||
# and must be removed
|
||||
|
||||
if (400..499).cover?(response.code) && ![408, 429].include?(response.code)
|
||||
@subscription.destroy!
|
||||
elsif !(200...300).cover?(response.code)
|
||||
raise Mastodon::UnexpectedResponseError, response
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def web_push_request
|
||||
@web_push_request || WebPushRequest.new(@subscription)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue