NAME
VAPID - Voluntary Application Server Identification
VERSION
Version 2.00
SYNOPSIS
use VAPID qw/all/;
my ($public, $private) = generate_vapid_keys();
# Validate keys
validate_public_key($public);
validate_private_key($private);
# Send a push notification
my $subscription = {
endpoint => $endpoint_from_browser,
keys => {
p256dh => $p256dh_from_browser,
auth => $auth_from_browser
}
};
my $result = send_push_notification(
subscription => $subscription,
payload => 'Hello World!',
vapid_public => $public,
vapid_private => $private,
subject => 'mailto:email@lnation.org',
ttl => 60
);
if ($result->{success}) {
print "Notification sent!\n";
}
# Or build the request manually for more control
my $req = build_push_request(
subscription => $subscription,
payload => 'Hello World!',
vapid_public => $public,
vapid_private => $private,
subject => 'mailto:email@lnation.org'
);
# Or just generate headers (legacy)
my $auth_headers = generate_vapid_header(
'https://updates.push.services.mozilla.com',
'mailto:email@lnation.org',
$public,
$private,
time + 60
);
DESCRIPTION
VAPID, which stands for Voluntary Application Server Identity, is a new way to send and receive website push notifications. Your VAPID keys allow you to send web push campaigns without having to send them through a service like Firebase Cloud Messaging (or FCM). Instead, the application server can voluntarily identify itself to your web push provider.
EXPORT
generate_vapid_keys
Generates vapid private and public keys.
generate_vapid_header
Generates the Authorization and Crypto-Key headers that should be passed when making a request to push a notification.
generate_future_expiration_timestamp
Generates a time that is in future based upon the number of seconds if passed, the default is 12 hours.
validate_subject
Validate the subject.
validate_public_key
Validate the public key.
validate_private_key
Validate the private key.
validate_expiration
Validate the expiration key.
validate_subscription
Validate a push subscription object. Expects a hash reference with:
{
endpoint => 'https://fcm.googleapis.com/...',
keys => {
p256dh => '...',
auth => '...'
}
}
encrypt_payload
Encrypt a message payload for web push, as RFC 8291 over the RFC 8188 aes128gcm content encoding. This is what browsers implement.
my $body = encrypt_payload($message, $subscription);
The whole body is returned as one string. RFC 8188 carries the salt and the sender's public key inside the body, as an 86-octet record header, so there is nothing to lift into Encryption: and Crypto-Key: headers. Send it with Content-Encoding: aes128gcm:
POST $subscription->{endpoint}
Authorization: vapid t=..., k=...
TTL: 60
Content-Encoding: aes128gcm
Content-Type: application/octet-stream
RFC 8291 guarantees a push service will accept only 4096 octets of encrypted payload, and the encoding spends 86 bytes on the header, one on the padding delimiter and 16 on the authentication tag before any of your message.
This changed in 2.00. Before it, this function produced Content-Encoding: aesgcm - draft-ietf-webpush-encryption-04, which RFC 8291 replaced - and returned a hash of ciphertext, salt and local_public_key, because that draft carried the last two in HTTP headers. It now returns a single string and the draft is gone. If you were reading those three keys, you no longer need to: pass the string as the body.
salt and local_key may be passed to reproduce the worked example in RFC 8291 section 5, and exist for that. Do not pass them in production: the ephemeral key must be fresh for every message, or the relationship between two messages to the same subscription leaks.
build_push_request
Build a complete HTTP::Request object for sending a push notification.
my $req = build_push_request(
subscription => $subscription,
payload => 'Hello World',
vapid_public => $public,
vapid_private => $private,
subject => 'mailto:email@example.com',
ttl => 60
);
The request carries Content-Encoding: aes128gcm and the RFC 8292 single-header Authorization: vapid t=..., k=... form.
This changed in 2.00. Before it the request was aesgcm, with the salt in an Encryption: header and the sender key in Crypto-Key:. Passing encoding => 'aesgcm' now dies rather than being ignored, because a caller asking for it has expectations about the body that this no longer meets.
send_push_notification
Send a push notification and return the result.
my $result = send_push_notification(
subscription => $subscription,
payload => 'Hello World',
vapid_public => $public,
vapid_private => $private,
subject => 'mailto:email@example.com',
ttl => 60
);
if ($result->{success}) {
print "Notification sent!\n";
}
Example
The following is pseudo code but it should get you started.
STEP 1 - generate private and public keys
my ($public, $private) = generate_vapid_keys()
$c->stash({
VAPID_USER_PUBLIC_KEY => $public
});
STEP 2 - main.js
var publicKey = [% VAPID_USER_PUBLIC_KEY %];
navigator.serviceWorker.getRegistrations().then(function (registrations) {
navigator.serviceWorker.register('/service-worker.js').then(function (worker) {
console.log('Service Worker Registered');
worker.pushManager.getSubscription().then(function(sub) {
if (sub === null) {
// Update UI to ask user to register for Push
subscribeUser();
console.log('Not subscribed to push service!');
} else {
// We have a subscription, update the database
console.log('Subscription object: ', sub);
}
});
});
});
function subscribeUser() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(function(reg) {
reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: publicKey
}).then(function(sub) {
// We have a subscription, update the database
console.log('Endpoint URL: ', sub.endpoint);
}).catch(function(e) {
if (Notification.permission === 'denied') {
console.warn('Permission for notifications was denied');
} else {
console.error('Unable to subscribe to push', e);
}
});
})
}
}
STEP 3 - service-worker.js
self.addEventListener('push', function(e) {
var body;
if (e.data) {
body = e.data.text();
} else {
body = 'Push message no payload';
}
var options = {
body: body,
icon: 'images/notification-flat.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
};
e.waitUntil(
self.registration.showNotification('Push Notification', options)
);
});
STEP 4 - manifest.json
Required for Chrome; Firefox works even without this file:
{
"short_name" : "Push",
"name" : "Push Dashboard",
"icons" : [
{
"src" : "/icon-144x144.png",
"type" : "image/png",
"sizes" : "144x144"
}
],
"display" : "standalone",
"start_url" : "/",
"background_color" : "#fff",
"theme_color" : "#fff",
"scope" : "/"
}
STEP 5 - send the push notification
Using send_push_notification (recommended):
use VAPID qw/all/;
my $subscription = {
endpoint => $subscription_url,
keys => {
p256dh => $user_p256dh_key,
auth => $user_auth_key
}
};
my $result = send_push_notification(
subscription => $subscription,
payload => 'Hello from VAPID!',
vapid_public => $public,
vapid_private => $private,
subject => 'mailto:email@lnation.org',
ttl => 60
);
if ($result->{success}) {
print "Push message sent successfully.\n";
} else {
print "Push message failed: ", $result->{message}, "\n";
}
STEP 5 (alternative) - build request manually
Using build_push_request for more control:
use VAPID qw/all/;
use LWP::UserAgent;
my $subscription = {
endpoint => $subscription_url,
keys => {
p256dh => $user_p256dh_key,
auth => $user_auth_key
}
};
my $req = build_push_request(
subscription => $subscription,
payload => 'Hello from VAPID!',
vapid_public => $public,
vapid_private => $private,
subject => 'mailto:email@lnation.org',
ttl => 60
);
my $ua = LWP::UserAgent->new;
my $resp = $ua->request($req);
if ($resp->is_success) {
print "Push message sent successfully.\n";
} else {
print "Push message failed: ", $resp->as_string, "\n";
}
STEP 5 (legacy) - generate headers only
For backward compatibility or custom implementations:
my $notification_host = URI->new($subscription_url)->host;
my $auth_headers = generate_vapid_header(
"https://$notification_host",
'mailto:email@lnation.org',
$public,
$private,
time + 60
);
# Then manually construct HTTP request with headers
# Note: This does not encrypt the payload
Curl from the command line (no payload):
curl "{SUBSCRIPTION_URL}" --request POST --header "TTL: 60" --header "Content-Length: 0" --header "Authorization: {AUTHORIZATION_HEADER}" --header "Crypto-Key: {CRYPTO_KEY_HEADER}"
AUTHOR
LNATION, <email at lnation.org>
BUGS
Please report any bugs or feature requests to bug-vapid at rt.cpan.org, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=VAPID. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
SUPPORT
You can find documentation for this module with the perldoc command.
perldoc VAPID
You can also look for information at:
RT: CPAN's request tracker (report bugs here)
CPAN Ratings
Search CPAN
ACKNOWLEDGEMENTS
LICENSE AND COPYRIGHT
This software is Copyright (c) 2020 by LNATION.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)