mirror of
https://github.com/619dev/PaperPhone.git
synced 2026-05-22 12:18:38 +08:00
增加消息通知服务
This commit is contained in:
131
server/node_modules/web-push/src/cli.js
generated
vendored
Executable file
131
server/node_modules/web-push/src/cli.js
generated
vendored
Executable file
@@ -0,0 +1,131 @@
|
||||
#! /usr/bin/env node
|
||||
/* eslint consistent-return:0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
const webPush = require('../src/index.js');
|
||||
|
||||
const printUsageDetails = () => {
|
||||
const actions = [
|
||||
{
|
||||
name: 'send-notification',
|
||||
options: [
|
||||
'--endpoint=<url>',
|
||||
'[--key=<browser key>]',
|
||||
'[--auth=<auth secret>]',
|
||||
'[--payload=<message>]',
|
||||
'[--ttl=<seconds>]',
|
||||
'[--encoding=<encoding type>]',
|
||||
'[--vapid-subject=<vapid subject>]',
|
||||
'[--vapid-pubkey=<public key url base64>]',
|
||||
'[--vapid-pvtkey=<private key url base64>]',
|
||||
'[--proxy=<http proxy uri, e.g: http://127.0.0.1:8889>]',
|
||||
'[--gcm-api-key=<api key>]'
|
||||
]
|
||||
}, {
|
||||
name: 'generate-vapid-keys',
|
||||
options: [
|
||||
'[--json]'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
let usage = '\nUsage: \n\n';
|
||||
actions.forEach(action => {
|
||||
usage += ' web-push ' + action.name;
|
||||
usage += ' ' + action.options.join(' ');
|
||||
usage += '\n\n';
|
||||
});
|
||||
|
||||
console.log(usage);
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
const generateVapidKeys = returnJson => {
|
||||
const vapidKeys = webPush.generateVAPIDKeys();
|
||||
|
||||
let outputText;
|
||||
if (returnJson) {
|
||||
outputText = JSON.stringify(vapidKeys);
|
||||
} else {
|
||||
const outputLine = '\n=======================================\n';
|
||||
outputText = outputLine + '\n'
|
||||
+ 'Public Key:\n' + vapidKeys.publicKey + '\n\n'
|
||||
+ 'Private Key:\n' + vapidKeys.privateKey + '\n'
|
||||
+ outputLine;
|
||||
}
|
||||
|
||||
console.log(outputText);
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
const sendNotification = args => {
|
||||
if (process.env.GCM_API_KEY) {
|
||||
webPush.setGCMAPIKey(process.env.GCM_API_KEY);
|
||||
}
|
||||
|
||||
const subscription = {
|
||||
endpoint: args.endpoint,
|
||||
keys: {
|
||||
p256dh: args.key || null,
|
||||
auth: args.auth || null
|
||||
}
|
||||
};
|
||||
|
||||
const payload = args.payload || null;
|
||||
|
||||
const options = {};
|
||||
|
||||
if (args.ttl) {
|
||||
options.TTL = args.ttl;
|
||||
}
|
||||
|
||||
if (argv['vapid-subject'] || argv['vapid-pubkey'] || argv['vapid-pvtkey']) {
|
||||
options.vapidDetails = {
|
||||
subject: args['vapid-subject'] || null,
|
||||
publicKey: args['vapid-pubkey'] || null,
|
||||
privateKey: args['vapid-pvtkey'] || null
|
||||
};
|
||||
}
|
||||
|
||||
if (args.proxy) {
|
||||
options.proxy = args.proxy;
|
||||
}
|
||||
|
||||
if (args['gcm-api-key']) {
|
||||
options.gcmAPIKey = args['gcm-api-key'];
|
||||
}
|
||||
|
||||
if (args.encoding) {
|
||||
options.contentEncoding = args.encoding;
|
||||
}
|
||||
|
||||
webPush.sendNotification(subscription, payload, options)
|
||||
.then(() => {
|
||||
console.log('Push message sent.');
|
||||
}, err => {
|
||||
console.log('Error sending push message: ');
|
||||
console.log(err);
|
||||
})
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
||||
const action = process.argv[2];
|
||||
const argv = require('minimist')(process.argv.slice(3));
|
||||
switch (action) {
|
||||
case 'send-notification':
|
||||
if (!argv.endpoint) {
|
||||
return printUsageDetails();
|
||||
}
|
||||
|
||||
sendNotification(argv);
|
||||
break;
|
||||
case 'generate-vapid-keys':
|
||||
generateVapidKeys(argv.json || false);
|
||||
break;
|
||||
default:
|
||||
printUsageDetails();
|
||||
break;
|
||||
}
|
||||
62
server/node_modules/web-push/src/encryption-helper.js
generated
vendored
Normal file
62
server/node_modules/web-push/src/encryption-helper.js
generated
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const ece = require('http_ece');
|
||||
|
||||
const encrypt = function(userPublicKey, userAuth, payload, contentEncoding) {
|
||||
if (!userPublicKey) {
|
||||
throw new Error('No user public key provided for encryption.');
|
||||
}
|
||||
|
||||
if (typeof userPublicKey !== 'string') {
|
||||
throw new Error('The subscription p256dh value must be a string.');
|
||||
}
|
||||
|
||||
if (Buffer.from(userPublicKey, 'base64url').length !== 65) {
|
||||
throw new Error('The subscription p256dh value should be 65 bytes long.');
|
||||
}
|
||||
|
||||
if (!userAuth) {
|
||||
throw new Error('No user auth provided for encryption.');
|
||||
}
|
||||
|
||||
if (typeof userAuth !== 'string') {
|
||||
throw new Error('The subscription auth key must be a string.');
|
||||
}
|
||||
|
||||
if (Buffer.from(userAuth, 'base64url').length < 16) {
|
||||
throw new Error('The subscription auth key should be at least 16 '
|
||||
+ 'bytes long');
|
||||
}
|
||||
|
||||
if (typeof payload !== 'string' && !Buffer.isBuffer(payload)) {
|
||||
throw new Error('Payload must be either a string or a Node Buffer.');
|
||||
}
|
||||
|
||||
if (typeof payload === 'string' || payload instanceof String) {
|
||||
payload = Buffer.from(payload);
|
||||
}
|
||||
|
||||
const localCurve = crypto.createECDH('prime256v1');
|
||||
const localPublicKey = localCurve.generateKeys();
|
||||
|
||||
const salt = crypto.randomBytes(16).toString('base64url');
|
||||
|
||||
const cipherText = ece.encrypt(payload, {
|
||||
version: contentEncoding,
|
||||
dh: userPublicKey,
|
||||
privateKey: localCurve,
|
||||
salt: salt,
|
||||
authSecret: userAuth
|
||||
});
|
||||
|
||||
return {
|
||||
localPublicKey: localPublicKey,
|
||||
salt: salt,
|
||||
cipherText: cipherText
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
encrypt: encrypt
|
||||
};
|
||||
21
server/node_modules/web-push/src/index.js
generated
vendored
Normal file
21
server/node_modules/web-push/src/index.js
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
const vapidHelper = require('./vapid-helper.js');
|
||||
const encryptionHelper = require('./encryption-helper.js');
|
||||
const WebPushLib = require('./web-push-lib.js');
|
||||
const WebPushError = require('./web-push-error.js');
|
||||
const WebPushConstants = require('./web-push-constants.js');
|
||||
|
||||
const webPush = new WebPushLib();
|
||||
|
||||
module.exports = {
|
||||
WebPushError: WebPushError,
|
||||
supportedContentEncodings: WebPushConstants.supportedContentEncodings,
|
||||
encrypt: encryptionHelper.encrypt,
|
||||
getVapidHeaders: vapidHelper.getVapidHeaders,
|
||||
generateVAPIDKeys: vapidHelper.generateVAPIDKeys,
|
||||
setGCMAPIKey: webPush.setGCMAPIKey,
|
||||
setVapidDetails: webPush.setVapidDetails,
|
||||
generateRequestDetails: webPush.generateRequestDetails,
|
||||
sendNotification: webPush.sendNotification.bind(webPush)
|
||||
};
|
||||
13
server/node_modules/web-push/src/urlsafe-base64-helper.js
generated
vendored
Normal file
13
server/node_modules/web-push/src/urlsafe-base64-helper.js
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @param {string} base64
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function validate(base64) {
|
||||
return /^[A-Za-z0-9\-_]+$/.test(base64);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validate: validate
|
||||
};
|
||||
255
server/node_modules/web-push/src/vapid-helper.js
generated
vendored
Normal file
255
server/node_modules/web-push/src/vapid-helper.js
generated
vendored
Normal file
@@ -0,0 +1,255 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const asn1 = require('asn1.js');
|
||||
const jws = require('jws');
|
||||
const { URL } = require('url');
|
||||
|
||||
const WebPushConstants = require('./web-push-constants.js');
|
||||
const urlBase64Helper = require('./urlsafe-base64-helper');
|
||||
|
||||
/**
|
||||
* DEFAULT_EXPIRATION is set to seconds in 12 hours
|
||||
*/
|
||||
const DEFAULT_EXPIRATION_SECONDS = 12 * 60 * 60;
|
||||
|
||||
// Maximum expiration is 24 hours according. (See VAPID spec)
|
||||
const MAX_EXPIRATION_SECONDS = 24 * 60 * 60;
|
||||
|
||||
const ECPrivateKeyASN = asn1.define('ECPrivateKey', function() {
|
||||
this.seq().obj(
|
||||
this.key('version').int(),
|
||||
this.key('privateKey').octstr(),
|
||||
this.key('parameters').explicit(0).objid()
|
||||
.optional(),
|
||||
this.key('publicKey').explicit(1).bitstr()
|
||||
.optional()
|
||||
);
|
||||
});
|
||||
|
||||
function toPEM(key) {
|
||||
return ECPrivateKeyASN.encode({
|
||||
version: 1,
|
||||
privateKey: key,
|
||||
parameters: [1, 2, 840, 10045, 3, 1, 7] // prime256v1
|
||||
}, 'pem', {
|
||||
label: 'EC PRIVATE KEY'
|
||||
});
|
||||
}
|
||||
|
||||
function generateVAPIDKeys() {
|
||||
const curve = crypto.createECDH('prime256v1');
|
||||
curve.generateKeys();
|
||||
|
||||
let publicKeyBuffer = curve.getPublicKey();
|
||||
let privateKeyBuffer = curve.getPrivateKey();
|
||||
|
||||
// Occassionally the keys will not be padded to the correct lengh resulting
|
||||
// in errors, hence this padding.
|
||||
// See https://github.com/web-push-libs/web-push/issues/295 for history.
|
||||
if (privateKeyBuffer.length < 32) {
|
||||
const padding = Buffer.alloc(32 - privateKeyBuffer.length);
|
||||
padding.fill(0);
|
||||
privateKeyBuffer = Buffer.concat([padding, privateKeyBuffer]);
|
||||
}
|
||||
|
||||
if (publicKeyBuffer.length < 65) {
|
||||
const padding = Buffer.alloc(65 - publicKeyBuffer.length);
|
||||
padding.fill(0);
|
||||
publicKeyBuffer = Buffer.concat([padding, publicKeyBuffer]);
|
||||
}
|
||||
|
||||
return {
|
||||
publicKey: publicKeyBuffer.toString('base64url'),
|
||||
privateKey: privateKeyBuffer.toString('base64url')
|
||||
};
|
||||
}
|
||||
|
||||
function validateSubject(subject) {
|
||||
if (!subject) {
|
||||
throw new Error('No subject set in vapidDetails.subject.');
|
||||
}
|
||||
|
||||
if (typeof subject !== 'string' || subject.length === 0) {
|
||||
throw new Error('The subject value must be a string containing an https: URL or '
|
||||
+ 'mailto: address. ' + subject);
|
||||
}
|
||||
|
||||
let subjectParseResult = null;
|
||||
try {
|
||||
subjectParseResult = new URL(subject);
|
||||
} catch (err) {
|
||||
throw new Error('Vapid subject is not a valid URL. ' + subject);
|
||||
}
|
||||
if (!['https:', 'mailto:'].includes(subjectParseResult.protocol)) {
|
||||
throw new Error('Vapid subject is not an https: or mailto: URL. ' + subject);
|
||||
}
|
||||
if (subjectParseResult.hostname === 'localhost') {
|
||||
console.warn('Vapid subject points to a localhost web URI, which is unsupported by '
|
||||
+ 'Apple\'s push notification server and will result in a BadJwtToken error when '
|
||||
+ 'sending notifications.');
|
||||
}
|
||||
}
|
||||
|
||||
function validatePublicKey(publicKey) {
|
||||
if (!publicKey) {
|
||||
throw new Error('No key set vapidDetails.publicKey');
|
||||
}
|
||||
|
||||
if (typeof publicKey !== 'string') {
|
||||
throw new Error('Vapid public key is must be a URL safe Base 64 '
|
||||
+ 'encoded string.');
|
||||
}
|
||||
|
||||
if (!urlBase64Helper.validate(publicKey)) {
|
||||
throw new Error('Vapid public key must be a URL safe Base 64 (without "=")');
|
||||
}
|
||||
|
||||
publicKey = Buffer.from(publicKey, 'base64url');
|
||||
|
||||
if (publicKey.length !== 65) {
|
||||
throw new Error('Vapid public key should be 65 bytes long when decoded.');
|
||||
}
|
||||
}
|
||||
|
||||
function validatePrivateKey(privateKey) {
|
||||
if (!privateKey) {
|
||||
throw new Error('No key set in vapidDetails.privateKey');
|
||||
}
|
||||
|
||||
if (typeof privateKey !== 'string') {
|
||||
throw new Error('Vapid private key must be a URL safe Base 64 '
|
||||
+ 'encoded string.');
|
||||
}
|
||||
|
||||
if (!urlBase64Helper.validate(privateKey)) {
|
||||
throw new Error('Vapid private key must be a URL safe Base 64 (without "=")');
|
||||
}
|
||||
|
||||
privateKey = Buffer.from(privateKey, 'base64url');
|
||||
|
||||
if (privateKey.length !== 32) {
|
||||
throw new Error('Vapid private key should be 32 bytes long when decoded.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the number of seconds calculates
|
||||
* the expiration in the future by adding the passed `numSeconds`
|
||||
* with the current seconds from Unix Epoch
|
||||
*
|
||||
* @param {Number} numSeconds Number of seconds to be added
|
||||
* @return {Number} Future expiration in seconds
|
||||
*/
|
||||
function getFutureExpirationTimestamp(numSeconds) {
|
||||
const futureExp = new Date();
|
||||
futureExp.setSeconds(futureExp.getSeconds() + numSeconds);
|
||||
return Math.floor(futureExp.getTime() / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the Expiration Header based on the VAPID Spec
|
||||
* Throws error of type `Error` if the expiration is not validated
|
||||
*
|
||||
* @param {Number} expiration Expiration seconds from Epoch to be validated
|
||||
*/
|
||||
function validateExpiration(expiration) {
|
||||
if (!Number.isInteger(expiration)) {
|
||||
throw new Error('`expiration` value must be a number');
|
||||
}
|
||||
|
||||
if (expiration < 0) {
|
||||
throw new Error('`expiration` must be a positive integer');
|
||||
}
|
||||
|
||||
// Roughly checks the time of expiration, since the max expiration can be ahead
|
||||
// of the time than at the moment the expiration was generated
|
||||
const maxExpirationTimestamp = getFutureExpirationTimestamp(MAX_EXPIRATION_SECONDS);
|
||||
|
||||
if (expiration >= maxExpirationTimestamp) {
|
||||
throw new Error('`expiration` value is greater than maximum of 24 hours');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method takes the required VAPID parameters and returns the required
|
||||
* header to be added to a Web Push Protocol Request.
|
||||
* @param {string} audience This must be the origin of the push service.
|
||||
* @param {string} subject This should be a URL or a 'mailto:' email
|
||||
* address.
|
||||
* @param {string} publicKey The VAPID public key.
|
||||
* @param {string} privateKey The VAPID private key.
|
||||
* @param {string} contentEncoding The contentEncoding type.
|
||||
* @param {integer} [expiration] The expiration of the VAPID JWT.
|
||||
* @return {Object} Returns an Object with the Authorization and
|
||||
* 'Crypto-Key' values to be used as headers.
|
||||
*/
|
||||
function getVapidHeaders(audience, subject, publicKey, privateKey, contentEncoding, expiration) {
|
||||
if (!audience) {
|
||||
throw new Error('No audience could be generated for VAPID.');
|
||||
}
|
||||
|
||||
if (typeof audience !== 'string' || audience.length === 0) {
|
||||
throw new Error('The audience value must be a string containing the '
|
||||
+ 'origin of a push service. ' + audience);
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(audience); // eslint-disable-line no-new
|
||||
} catch (err) {
|
||||
throw new Error('VAPID audience is not a url. ' + audience);
|
||||
}
|
||||
|
||||
validateSubject(subject);
|
||||
validatePublicKey(publicKey);
|
||||
validatePrivateKey(privateKey);
|
||||
|
||||
privateKey = Buffer.from(privateKey, 'base64url');
|
||||
|
||||
if (expiration) {
|
||||
validateExpiration(expiration);
|
||||
} else {
|
||||
expiration = getFutureExpirationTimestamp(DEFAULT_EXPIRATION_SECONDS);
|
||||
}
|
||||
|
||||
const header = {
|
||||
typ: 'JWT',
|
||||
alg: 'ES256'
|
||||
};
|
||||
|
||||
const jwtPayload = {
|
||||
aud: audience,
|
||||
exp: expiration,
|
||||
sub: subject
|
||||
};
|
||||
|
||||
const jwt = jws.sign({
|
||||
header: header,
|
||||
payload: jwtPayload,
|
||||
privateKey: toPEM(privateKey)
|
||||
});
|
||||
|
||||
if (contentEncoding === WebPushConstants.supportedContentEncodings.AES_128_GCM) {
|
||||
return {
|
||||
Authorization: 'vapid t=' + jwt + ', k=' + publicKey
|
||||
};
|
||||
}
|
||||
if (contentEncoding === WebPushConstants.supportedContentEncodings.AES_GCM) {
|
||||
return {
|
||||
Authorization: 'WebPush ' + jwt,
|
||||
'Crypto-Key': 'p256ecdsa=' + publicKey
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Unsupported encoding type specified.');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateVAPIDKeys: generateVAPIDKeys,
|
||||
getFutureExpirationTimestamp: getFutureExpirationTimestamp,
|
||||
getVapidHeaders: getVapidHeaders,
|
||||
validateSubject: validateSubject,
|
||||
validatePublicKey: validatePublicKey,
|
||||
validatePrivateKey: validatePrivateKey,
|
||||
validateExpiration: validateExpiration
|
||||
};
|
||||
17
server/node_modules/web-push/src/web-push-constants.js
generated
vendored
Normal file
17
server/node_modules/web-push/src/web-push-constants.js
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
const WebPushConstants = {};
|
||||
|
||||
WebPushConstants.supportedContentEncodings = {
|
||||
AES_GCM: 'aesgcm',
|
||||
AES_128_GCM: 'aes128gcm'
|
||||
};
|
||||
|
||||
WebPushConstants.supportedUrgency = {
|
||||
VERY_LOW: 'very-low',
|
||||
LOW: 'low',
|
||||
NORMAL: 'normal',
|
||||
HIGH: 'high'
|
||||
};
|
||||
|
||||
module.exports = WebPushConstants;
|
||||
16
server/node_modules/web-push/src/web-push-error.js
generated
vendored
Normal file
16
server/node_modules/web-push/src/web-push-error.js
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
function WebPushError(message, statusCode, headers, body, endpoint) {
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.message = message;
|
||||
this.statusCode = statusCode;
|
||||
this.headers = headers;
|
||||
this.body = body;
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
require('util').inherits(WebPushError, Error);
|
||||
|
||||
module.exports = WebPushError;
|
||||
413
server/node_modules/web-push/src/web-push-lib.js
generated
vendored
Normal file
413
server/node_modules/web-push/src/web-push-lib.js
generated
vendored
Normal file
@@ -0,0 +1,413 @@
|
||||
'use strict';
|
||||
|
||||
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');
|
||||
const webPushConstants = require('./web-push-constants.js');
|
||||
const urlBase64Helper = require('./urlsafe-base64-helper');
|
||||
|
||||
// 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 {string} publicKey The public VAPID key, a URL safe, base64 encoded string
|
||||
* @param {string} privateKey The private VAPID key, a URL safe, base64 encoded string.
|
||||
*/
|
||||
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|Buffer} [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 (typeof subscription !== 'object' || !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 = {};
|
||||
let contentEncoding = webPushConstants.supportedContentEncodings.AES_128_GCM;
|
||||
let urgency = webPushConstants.supportedUrgency.NORMAL;
|
||||
let topic;
|
||||
let proxy;
|
||||
let agent;
|
||||
let timeout;
|
||||
|
||||
if (options) {
|
||||
const validOptionKeys = [
|
||||
'headers',
|
||||
'gcmAPIKey',
|
||||
'vapidDetails',
|
||||
'TTL',
|
||||
'contentEncoding',
|
||||
'urgency',
|
||||
'topic',
|
||||
'proxy',
|
||||
'agent',
|
||||
'timeout'
|
||||
];
|
||||
const optionKeys = Object.keys(options);
|
||||
for (let i = 0; i < optionKeys.length; i += 1) {
|
||||
const optionKey = optionKeys[i];
|
||||
if (!validOptionKeys.includes(optionKey)) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Falsy values are allowed here so one can skip Vapid `else if` below and use FCM
|
||||
if (options.vapidDetails !== undefined) {
|
||||
currentVapidDetails = options.vapidDetails;
|
||||
}
|
||||
|
||||
if (options.TTL !== undefined) {
|
||||
timeToLive = Number(options.TTL);
|
||||
if (timeToLive < 0) {
|
||||
throw new Error('TTL should be a number and should be at least 0');
|
||||
}
|
||||
}
|
||||
|
||||
if (options.contentEncoding) {
|
||||
if ((options.contentEncoding === webPushConstants.supportedContentEncodings.AES_128_GCM
|
||||
|| options.contentEncoding === webPushConstants.supportedContentEncodings.AES_GCM)) {
|
||||
contentEncoding = options.contentEncoding;
|
||||
} else {
|
||||
throw new Error('Unsupported content encoding specified.');
|
||||
}
|
||||
}
|
||||
|
||||
if (options.urgency) {
|
||||
if ((options.urgency === webPushConstants.supportedUrgency.VERY_LOW
|
||||
|| options.urgency === webPushConstants.supportedUrgency.LOW
|
||||
|| options.urgency === webPushConstants.supportedUrgency.NORMAL
|
||||
|| options.urgency === webPushConstants.supportedUrgency.HIGH)) {
|
||||
urgency = options.urgency;
|
||||
} else {
|
||||
throw new Error('Unsupported urgency specified.');
|
||||
}
|
||||
}
|
||||
|
||||
if (options.topic) {
|
||||
if (!urlBase64Helper.validate(options.topic)) {
|
||||
throw new Error('Unsupported characters set use the URL or filename-safe Base64 characters set');
|
||||
}
|
||||
if (options.topic.length > 32) {
|
||||
throw new Error('use maximum of 32 characters from the URL or filename-safe Base64 characters set');
|
||||
}
|
||||
topic = options.topic;
|
||||
}
|
||||
|
||||
if (options.proxy) {
|
||||
if (typeof options.proxy === 'string'
|
||||
|| typeof options.proxy.host === 'string') {
|
||||
proxy = options.proxy;
|
||||
} else {
|
||||
console.warn('Attempt to use proxy option, but invalid type it should be a string or proxy options object.');
|
||||
}
|
||||
}
|
||||
|
||||
if (options.agent) {
|
||||
if (options.agent instanceof https.Agent) {
|
||||
if (proxy) {
|
||||
console.warn('Agent option will be ignored because proxy option is defined.');
|
||||
}
|
||||
|
||||
agent = options.agent;
|
||||
} else {
|
||||
console.warn('Wrong type for the agent option, it should be an instance of https.Agent.');
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof options.timeout === 'number') {
|
||||
timeout = options.timeout;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
const encrypted = encryptionHelper
|
||||
.encrypt(subscription.keys.p256dh, subscription.keys.auth, payload, contentEncoding);
|
||||
|
||||
requestDetails.headers['Content-Length'] = encrypted.cipherText.length;
|
||||
requestDetails.headers['Content-Type'] = 'application/octet-stream';
|
||||
|
||||
if (contentEncoding === webPushConstants.supportedContentEncodings.AES_128_GCM) {
|
||||
requestDetails.headers['Content-Encoding'] = webPushConstants.supportedContentEncodings.AES_128_GCM;
|
||||
} else if (contentEncoding === webPushConstants.supportedContentEncodings.AES_GCM) {
|
||||
requestDetails.headers['Content-Encoding'] = webPushConstants.supportedContentEncodings.AES_GCM;
|
||||
requestDetails.headers.Encryption = 'salt=' + encrypted.salt;
|
||||
requestDetails.headers['Crypto-Key'] = 'dh=' + encrypted.localPublicKey.toString('base64url');
|
||||
}
|
||||
|
||||
requestPayload = encrypted.cipherText;
|
||||
} else {
|
||||
requestDetails.headers['Content-Length'] = 0;
|
||||
}
|
||||
|
||||
const isGCM = subscription.endpoint.startsWith('https://android.googleapis.com/gcm/send');
|
||||
const isFCM = subscription.endpoint.startsWith('https://fcm.googleapis.com/fcm/send');
|
||||
// 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.host;
|
||||
|
||||
const vapidHeaders = vapidHelper.getVapidHeaders(
|
||||
audience,
|
||||
currentVapidDetails.subject,
|
||||
currentVapidDetails.publicKey,
|
||||
currentVapidDetails.privateKey,
|
||||
contentEncoding
|
||||
);
|
||||
|
||||
requestDetails.headers.Authorization = vapidHeaders.Authorization;
|
||||
|
||||
if (contentEncoding === webPushConstants.supportedContentEncodings.AES_GCM) {
|
||||
if (requestDetails.headers['Crypto-Key']) {
|
||||
requestDetails.headers['Crypto-Key'] += ';'
|
||||
+ vapidHeaders['Crypto-Key'];
|
||||
} else {
|
||||
requestDetails.headers['Crypto-Key'] = vapidHeaders['Crypto-Key'];
|
||||
}
|
||||
}
|
||||
} else if (isFCM && currentGCMAPIKey) {
|
||||
requestDetails.headers.Authorization = 'key=' + currentGCMAPIKey;
|
||||
}
|
||||
|
||||
requestDetails.headers.Urgency = urgency;
|
||||
|
||||
if (topic) {
|
||||
requestDetails.headers.Topic = topic;
|
||||
}
|
||||
|
||||
requestDetails.body = requestPayload;
|
||||
requestDetails.endpoint = subscription.endpoint;
|
||||
|
||||
if (proxy) {
|
||||
requestDetails.proxy = proxy;
|
||||
}
|
||||
|
||||
if (agent) {
|
||||
requestDetails.agent = agent;
|
||||
}
|
||||
|
||||
if (timeout) {
|
||||
requestDetails.timeout = timeout;
|
||||
}
|
||||
|
||||
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|Buffer} [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;
|
||||
|
||||
if (requestDetails.timeout) {
|
||||
httpsOptions.timeout = requestDetails.timeout;
|
||||
}
|
||||
|
||||
if (requestDetails.agent) {
|
||||
httpsOptions.agent = requestDetails.agent;
|
||||
}
|
||||
|
||||
if (requestDetails.proxy) {
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent'); // eslint-disable-line global-require
|
||||
httpsOptions.agent = new HttpsProxyAgent(requestDetails.proxy);
|
||||
}
|
||||
|
||||
const pushRequest = https.request(httpsOptions, function(pushResponse) {
|
||||
let responseText = '';
|
||||
|
||||
pushResponse.on('data', function(chunk) {
|
||||
responseText += chunk;
|
||||
});
|
||||
|
||||
pushResponse.on('end', function() {
|
||||
if (pushResponse.statusCode < 200 || pushResponse.statusCode > 299) {
|
||||
reject(new WebPushError(
|
||||
'Received unexpected response code',
|
||||
pushResponse.statusCode,
|
||||
pushResponse.headers,
|
||||
responseText,
|
||||
requestDetails.endpoint
|
||||
));
|
||||
} else {
|
||||
resolve({
|
||||
statusCode: pushResponse.statusCode,
|
||||
body: responseText,
|
||||
headers: pushResponse.headers
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (requestDetails.timeout) {
|
||||
pushRequest.on('timeout', function() {
|
||||
pushRequest.destroy(new Error('Socket timeout'));
|
||||
});
|
||||
}
|
||||
|
||||
pushRequest.on('error', function(e) {
|
||||
reject(e);
|
||||
});
|
||||
|
||||
if (requestDetails.body) {
|
||||
pushRequest.write(requestDetails.body);
|
||||
}
|
||||
|
||||
pushRequest.end();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = WebPushLib;
|
||||
Reference in New Issue
Block a user