UNPKG

web-push

Version:
300 lines (256 loc) 9.66 kB
'use strict'; const urlBase64 = require('urlsafe-base64'); const url = require('url'); const https = require('https'); const WebPushError = require('./web-push-error.js'); const vapidHelper = require('./vapid-helper.js'); const encryptionHelper = require('./encryption-helper.js'); // Default TTL is four weeks. const DEFAULT_TTL = 2419200; let gcmAPIKey = ''; let vapidDetails; function WebPushLib() { } /** * When sending messages to a GCM endpoint you need to set the GCM API key * by either calling setGMAPIKey() or passing in the API key as an option * to sendNotification(). * @param {string} apiKey The API key to send with the GCM request. */ WebPushLib.prototype.setGCMAPIKey = function(apiKey) { if (apiKey === null) { gcmAPIKey = null; return; } if (typeof apiKey === 'undefined' || typeof apiKey !== 'string' || apiKey.length === 0) { throw new Error('The GCM API Key should be a non-empty string or null.'); } gcmAPIKey = apiKey; }; /** * When making requests where you want to define VAPID details, call this * method before sendNotification() or pass in the details and options to * sendNotification. * @param {string} subject This must be either a URL or a 'mailto:' * address. For example: 'https://my-site.com/contact' or * 'mailto: contact@my-site.com' * @param {Buffer} publicKey The public VAPID key. * @param {Buffer} privateKey The private VAPID key. */ WebPushLib.prototype.setVapidDetails = function(subject, publicKey, privateKey) { if (arguments.length === 1 && arguments[0] === null) { vapidDetails = null; return; } vapidHelper.validateSubject(subject); vapidHelper.validatePublicKey(publicKey); vapidHelper.validatePrivateKey(privateKey); vapidDetails = { subject: subject, publicKey: publicKey, privateKey: privateKey }; }; /** * To get the details of a request to trigger a push message, without sending * a push notification call this method. * * This method will throw an error if there is an issue with the input. * @param {PushSubscription} subscription The PushSubscription you wish to * send the notification to. * @param {string} [payload] The payload you wish to send to the * the user. * @param {Object} [options] Options for the GCM API key and * vapid keys can be passed in if they are unique for each notification you * wish to send. * @return {Object} This method returns an Object which * contains 'endpoint', 'method', 'headers' and 'payload'. */ WebPushLib.prototype.generateRequestDetails = function(subscription, payload, options) { if (!subscription || !subscription.endpoint) { throw new Error('You must pass in a subscription with at least ' + 'an endpoint.'); } if (typeof subscription.endpoint !== 'string' || subscription.endpoint.length === 0) { throw new Error('The subscription endpoint must be a string with ' + 'a valid URL.'); } if (payload) { // Validate the subscription keys if (!subscription.keys || !subscription.keys.p256dh || !subscription.keys.auth) { throw new Error('To send a message with a payload, the ' + 'subscription must have \'auth\' and \'p256dh\' keys.'); } } let currentGCMAPIKey = gcmAPIKey; let currentVapidDetails = vapidDetails; let timeToLive = DEFAULT_TTL; let extraHeaders = {}; if (options) { const validOptionKeys = [ 'headers', 'gcmAPIKey', 'vapidDetails', 'TTL' ]; const optionKeys = Object.keys(options); for (let i = 0; i < optionKeys.length; i += 1) { const optionKey = optionKeys[i]; if (validOptionKeys.indexOf(optionKey) === -1) { throw new Error('\'' + optionKey + '\' is an invalid option. ' + 'The valid options are [\'' + validOptionKeys.join('\', \'') + '\'].'); } } if (options.headers) { extraHeaders = options.headers; let duplicates = Object.keys(extraHeaders) .filter(function (header) { return typeof options[header] !== 'undefined'; }); if (duplicates.length > 0) { throw new Error('Duplicated headers defined [' + duplicates.join(',') + ']. Please either define the header in the' + 'top level options OR in the \'headers\' key.'); } } if (options.gcmAPIKey) { currentGCMAPIKey = options.gcmAPIKey; } if (options.vapidDetails) { currentVapidDetails = options.vapidDetails; } if (options.TTL) { timeToLive = options.TTL; } } if (typeof timeToLive === 'undefined') { timeToLive = DEFAULT_TTL; } const requestDetails = { method: 'POST', headers: { TTL: timeToLive } }; Object.keys(extraHeaders).forEach(function (header) { requestDetails.headers[header] = extraHeaders[header]; }); let requestPayload = null; if (payload) { if (!subscription.keys || typeof subscription !== 'object' || !subscription.keys.p256dh || !subscription.keys.auth) { throw new Error(new Error('Unable to send a message with ' + 'payload to this subscription since it doesn\'t have the ' + 'required encryption keys')); } const encrypted = encryptionHelper.encrypt( subscription.keys.p256dh, subscription.keys.auth, payload); requestDetails.headers['Content-Length'] = encrypted.cipherText.length; requestDetails.headers['Content-Type'] = 'application/octet-stream'; requestDetails.headers['Content-Encoding'] = 'aesgcm'; requestDetails.headers.Encryption = 'salt=' + encrypted.salt; requestDetails.headers['Crypto-Key'] = 'dh=' + urlBase64.encode(encrypted.localPublicKey); requestPayload = encrypted.cipherText; } else { requestDetails.headers['Content-Length'] = 0; } const isGCM = subscription.endpoint.indexOf( 'https://android.googleapis.com/gcm/send') === 0; // VAPID isn't supported by GCM hence the if, else if. if (isGCM) { if (!currentGCMAPIKey) { console.warn('Attempt to send push notification to GCM endpoint, ' + 'but no GCM key is defined. Please use setGCMApiKey() or add ' + '\'gcmAPIKey\' as an option.'); } else { requestDetails.headers.Authorization = 'key=' + currentGCMAPIKey; } } else if (currentVapidDetails) { const parsedUrl = url.parse(subscription.endpoint); const audience = parsedUrl.protocol + '//' + parsedUrl.hostname; const vapidHeaders = vapidHelper.getVapidHeaders( audience, currentVapidDetails.subject, currentVapidDetails.publicKey, currentVapidDetails.privateKey ); requestDetails.headers.Authorization = vapidHeaders.Authorization; if (requestDetails.headers['Crypto-Key']) { requestDetails.headers['Crypto-Key'] += ';' + vapidHeaders['Crypto-Key']; } else { requestDetails.headers['Crypto-Key'] = vapidHeaders['Crypto-Key']; } } requestDetails.body = requestPayload; requestDetails.endpoint = subscription.endpoint; return requestDetails; }; /** * To send a push notification call this method with a subscription, optional * payload and any options. * @param {PushSubscription} subscription The PushSubscription you wish to * send the notification to. * @param {string} [payload] The payload you wish to send to the * the user. * @param {Object} [options] Options for the GCM API key and * vapid keys can be passed in if they are unique for each notification you * wish to send. * @return {Promise} This method returns a Promise which * resolves if the sending of the notification was successful, otherwise it * rejects. */ WebPushLib.prototype.sendNotification = function(subscription, payload, options) { let requestDetails; try { requestDetails = this.generateRequestDetails( subscription, payload, options); } catch (err) { return Promise.reject(err); } return new Promise(function(resolve, reject) { const httpsOptions = {}; const urlParts = url.parse(requestDetails.endpoint); httpsOptions.hostname = urlParts.hostname; httpsOptions.port = urlParts.port; httpsOptions.path = urlParts.path; httpsOptions.headers = requestDetails.headers; httpsOptions.method = requestDetails.method; const pushRequest = https.request(httpsOptions, function(pushResponse) { let responseText = ''; pushResponse.on('data', function(chunk) { responseText += chunk; }); pushResponse.on('end', function() { if (pushResponse.statusCode !== 201) { reject(new WebPushError('Received unexpected response code', pushResponse.statusCode, pushResponse.headers, responseText, subscription.endpoint)); } else { resolve({ statusCode: pushResponse.statusCode, body: responseText, headers: pushResponse.headers }); } }); }); pushRequest.on('error', function(e) { reject(e); }); if (requestDetails.body) { pushRequest.write(requestDetails.body); } pushRequest.end(); }); }; module.exports = WebPushLib;