NAME

VAPID - Voluntary Application Server Identification

VERSION

Version 1.05

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 using ECDH key agreement and AES-GCM.

my $encrypted = encrypt_payload($message, $subscription);

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
);

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:

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)