Skip to content

Commit 2fcd717

Browse files
authored
Merge pull request #218 from gauntface/new-send-notification
New send notification
2 parents c069a22 + 3d74337 commit 2fcd717

15 files changed

+1230
-797
lines changed

bin/web-push.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ webPush.setGCMAPIKey(process.env.GCM_API_KEY);
55

66
const argv = require('minimist')(process.argv.slice(2));
77

8-
const usage = 'Use: web-push --endpoint=<url> --key=<browser key> [--auth=<auth secret>] [--ttl=<seconds>] [--payload=<message>] [--vapid-audience] [--vapid-subject] [--vapid-pvtkey] [--vapid-pubkey]';
8+
const usage = 'Use: web-push --endpoint=<url> --key=<browser key> ' +
9+
'[--auth=<auth secret>] [--ttl=<seconds>] [--payload=<message>] ' +
10+
'[--vapid-audience] [--vapid-subject] [--vapid-pvtkey] [--vapid-pubkey]';
911

1012
if (!argv.endpoint || !argv.key) {
1113
console.log(usage);
@@ -17,6 +19,7 @@ const key = argv.key;
1719
const ttl = argv.ttl || 0;
1820
const payload = argv.payload || '';
1921
const auth = argv.auth || null;
22+
2023
const vapidAudience = argv['vapid-audience'] || null;
2124
const vapidSubject = argv['vapid-subject'] || null;
2225
const vapidPubKey = argv['vapid-pubkey'] || null;
@@ -63,4 +66,3 @@ webPush.sendNotification(endpoint, params).then(() => {
6366
}).then(() => {
6467
process.exit(0);
6568
});
66-

package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,24 @@
4242
},
4343
"devDependencies": {
4444
"chalk": "^1.1.3",
45-
"chromedriver": "^2.23.1",
45+
"chromedriver": "^2.24.1",
4646
"del": "^2.2.1",
4747
"dmg": "^0.1.0",
4848
"eslint": "^2.10.2",
4949
"eslint-config-airbnb": "^9.0.1",
5050
"eslint-plugin-import": "^1.11.1",
5151
"fs-extra": "^0.30.0",
52+
"geckodriver": "^1.1.2",
5253
"istanbul": "^0.4.2",
5354
"mkdirp": "^0.5.1",
5455
"mocha": "^2.4.5",
5556
"portfinder": "^1.0.2",
5657
"request": "^2.69.0",
57-
"selenium-assistant": "0.4.0",
58-
"selenium-webdriver": "~2.53.2",
58+
"selenium-assistant": "0.5.3",
59+
"selenium-webdriver": "^3.0.0-beta-2",
5960
"semver": "^5.1.0",
60-
"temp": "^0.8.3"
61+
"temp": "^0.8.3",
62+
"which": "^1.2.11"
6163
},
6264
"engines": {
6365
"node": ">= v0.10.0"

src/encryption-helper.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use strict';
2+
3+
const crypto = require('crypto');
4+
const ece = require('http_ece');
5+
const urlBase64 = require('urlsafe-base64');
6+
7+
const encrypt = function(userPublicKey, userAuth, payload) {
8+
if (!userPublicKey) {
9+
throw new Error('No user public key provided for encryption.');
10+
}
11+
12+
if (typeof userPublicKey !== 'string') {
13+
throw new Error('The subscription p256dh value must be a string.');
14+
}
15+
16+
if (urlBase64.decode(userPublicKey).length !== 65) {
17+
throw new Error('The subscription p256dh value should be 65 bytes long.');
18+
}
19+
20+
if (!userAuth) {
21+
throw new Error('No user auth provided for encryption.');
22+
}
23+
24+
if (typeof userAuth !== 'string') {
25+
throw new Error('The subscription auth key must be a string.');
26+
}
27+
28+
if (urlBase64.decode(userAuth).length < 16) {
29+
throw new Error('The subscription auth key should be at least 16 ' +
30+
'bytes long');
31+
}
32+
33+
if (typeof payload !== 'string' && !Buffer.isBuffer(payload)) {
34+
throw new Error('Payload must be either a string or a Node Buffer.');
35+
}
36+
37+
if (typeof payload === 'string' || payload instanceof String) {
38+
payload = new Buffer(payload);
39+
}
40+
41+
const localCurve = crypto.createECDH('prime256v1');
42+
const localPublicKey = localCurve.generateKeys();
43+
44+
const salt = urlBase64.encode(crypto.randomBytes(16));
45+
46+
ece.saveKey('webpushKey', localCurve, 'P-256');
47+
48+
const cipherText = ece.encrypt(payload, {
49+
keyid: 'webpushKey',
50+
dh: userPublicKey,
51+
salt: salt,
52+
authSecret: userAuth,
53+
padSize: 2
54+
});
55+
56+
return {
57+
localPublicKey: localPublicKey,
58+
salt: salt,
59+
cipherText: cipherText
60+
};
61+
};
62+
63+
module.exports = {
64+
encrypt: encrypt
65+
};

src/index.js

Lines changed: 10 additions & 262 deletions
Original file line numberDiff line numberDiff line change
@@ -1,270 +1,18 @@
11
'use strict';
22

3-
const urlBase64 = require('urlsafe-base64');
4-
const crypto = require('crypto');
5-
const ece = require('http_ece');
6-
const url = require('url');
7-
const https = require('https');
8-
const asn1 = require('asn1.js');
9-
const jws = require('jws');
10-
11-
const WebPushError = require('./web-push-error.js');
3+
// This loads up shims required for older versions of node.
124
require('./shim');
135

14-
const ECPrivateKeyASN = asn1.define('ECPrivateKey', function() {
15-
this.seq().obj(
16-
this.key('version').int(),
17-
this.key('privateKey').octstr(),
18-
this.key('parameters').explicit(0).objid()
19-
.optional(),
20-
this.key('publicKey').explicit(1).bitstr()
21-
.optional()
22-
);
23-
});
24-
25-
function toPEM(key) {
26-
return ECPrivateKeyASN.encode({
27-
version: 1,
28-
privateKey: key,
29-
parameters: [1, 2, 840, 10045, 3, 1, 7] // prime256v1
30-
}, 'pem', {
31-
label: 'EC PRIVATE KEY'
32-
});
33-
}
34-
35-
function generateVAPIDKeys() {
36-
const curve = crypto.createECDH('prime256v1');
37-
curve.generateKeys();
38-
39-
return {
40-
publicKey: curve.getPublicKey(),
41-
privateKey: curve.getPrivateKey()
42-
};
43-
}
44-
45-
function getVapidHeaders(vapid) {
46-
if (!vapid.audience) {
47-
throw new Error('No audience set in vapid.audience');
48-
}
49-
50-
if (!vapid.subject) {
51-
throw new Error('No subject set in vapid.subject');
52-
}
53-
54-
if (!vapid.publicKey) {
55-
throw new Error('No key set vapid.publicKey');
56-
}
57-
58-
if (!vapid.privateKey) {
59-
throw new Error('No key set in vapid.privateKey');
60-
}
61-
62-
const header = {
63-
typ: 'JWT',
64-
alg: 'ES256'
65-
};
66-
67-
const jwtPayload = {
68-
aud: vapid.audience,
69-
exp: Math.floor(Date.now() / 1000) + 86400,
70-
sub: vapid.subject
71-
};
72-
73-
const jwt = jws.sign({
74-
header: header,
75-
payload: jwtPayload,
76-
privateKey: toPEM(vapid.privateKey)
77-
});
78-
79-
return {
80-
Authorization: 'Bearer ' + jwt,
81-
'Crypto-Key': 'p256ecdsa=' + urlBase64.encode(vapid.publicKey)
82-
};
83-
}
84-
85-
let gcmAPIKey = '';
86-
87-
function setGCMAPIKey(apiKey) {
88-
if (!apiKey || typeof apiKey !== 'string') {
89-
throw new Error('The GCM API Key should be a non-emtpy string.');
90-
}
91-
92-
gcmAPIKey = apiKey;
93-
}
94-
95-
// New standard, Firefox 46+ and Chrome 50+.
96-
function encrypt(userPublicKey, userAuth, payload) {
97-
if (typeof payload === 'string' || payload instanceof String) {
98-
payload = new Buffer(payload);
99-
}
100-
const localCurve = crypto.createECDH('prime256v1');
101-
const localPublicKey = localCurve.generateKeys();
102-
103-
const salt = urlBase64.encode(crypto.randomBytes(16));
104-
105-
ece.saveKey('webpushKey', localCurve, 'P-256');
106-
107-
const cipherText = ece.encrypt(payload, {
108-
keyid: 'webpushKey',
109-
dh: userPublicKey,
110-
salt: salt,
111-
authSecret: userAuth,
112-
padSize: 2
113-
});
114-
115-
return {
116-
localPublicKey: localPublicKey,
117-
salt: salt,
118-
cipherText: cipherText
119-
};
120-
}
121-
122-
function sendNotification(endpoint, params) {
123-
const args = arguments;
124-
125-
let curGCMAPIKey;
126-
let TTL;
127-
let userPublicKey;
128-
let userAuth;
129-
let payload;
130-
let vapid;
131-
132-
return new Promise(function(resolve, reject) {
133-
try {
134-
curGCMAPIKey = gcmAPIKey;
135-
136-
if (args.length === 0) {
137-
throw new Error('sendNotification requires at least one argument, ' +
138-
'the endpoint URL.');
139-
} else if (params && typeof params === 'object') {
140-
TTL = params.TTL;
141-
userPublicKey = params.userPublicKey;
142-
userAuth = params.userAuth;
143-
payload = params.payload;
144-
vapid = params.vapid;
145-
146-
if (params.gcmAPIKey) {
147-
curGCMAPIKey = params.gcmAPIKey;
148-
}
149-
} else if (args.length !== 1) {
150-
throw new Error('You are using the old, deprecated, interface of ' +
151-
'the `sendNotification` function.');
152-
}
153-
154-
if (userPublicKey) {
155-
if (typeof userPublicKey !== 'string') {
156-
throw new Error('userPublicKey should be a base64-encoded string.');
157-
} else if (urlBase64.decode(userPublicKey).length !== 65) {
158-
throw new Error('userPublicKey should be 65 bytes long.');
159-
}
160-
}
161-
162-
if (userAuth) {
163-
if (typeof userAuth !== 'string') {
164-
throw new Error('userAuth should be a base64-encoded string.');
165-
} else if (urlBase64.decode(userAuth).length < 16) {
166-
throw new Error('userAuth should be at least 16 bytes long');
167-
}
168-
}
169-
170-
const urlParts = url.parse(endpoint);
171-
const options = {
172-
hostname: urlParts.hostname,
173-
port: urlParts.port,
174-
path: urlParts.pathname,
175-
method: 'POST',
176-
headers: {
177-
'Content-Length': 0
178-
}
179-
};
180-
181-
let requestPayload;
182-
if (typeof payload !== 'undefined') {
183-
const encrypted = encrypt(userPublicKey, userAuth, payload);
184-
185-
options.headers = {
186-
'Content-Type': 'application/octet-stream',
187-
'Content-Encoding': 'aesgcm',
188-
'Encryption': 'keyid=p256dh;salt=' + encrypted.salt
189-
};
190-
191-
options.headers['Crypto-Key'] = 'keyid=p256dh;dh=' +
192-
urlBase64.encode(encrypted.localPublicKey);
193-
194-
requestPayload = encrypted.cipherText;
195-
}
196-
197-
const isGCM = endpoint.indexOf('https://android.googleapis.com/gcm/send') === 0;
198-
if (isGCM) {
199-
if (!curGCMAPIKey) {
200-
console.warn('Attempt to send push notification to GCM endpoint, ' +
201-
'but no GCM key is defined'.bold.red);
202-
}
203-
204-
options.headers.Authorization = 'key=' + curGCMAPIKey;
205-
}
206-
207-
if (vapid && !isGCM) {
208-
// VAPID isn't supported by GCM.
209-
vapid.audience = urlParts.protocol + '//' + urlParts.hostname;
210-
211-
const vapidHeaders = getVapidHeaders(vapid);
212-
213-
options.headers.Authorization = vapidHeaders.Authorization;
214-
if (options.headers['Crypto-Key']) {
215-
options.headers['Crypto-Key'] += ';' + vapidHeaders['Crypto-Key'];
216-
} else {
217-
options.headers['Crypto-Key'] = vapidHeaders['Crypto-Key'];
218-
}
219-
}
220-
221-
if (typeof TTL !== 'undefined') {
222-
options.headers.TTL = TTL;
223-
} else {
224-
options.headers.TTL = 2419200; // Default TTL is four weeks.
225-
}
226-
227-
if (requestPayload) {
228-
options.headers['Content-Length'] = requestPayload.length;
229-
}
230-
231-
const pushRequest = https.request(options, function(pushResponse) {
232-
let body = '';
233-
234-
pushResponse.on('data', function(chunk) {
235-
body += chunk;
236-
});
237-
238-
pushResponse.on('end', function() {
239-
if (pushResponse.statusCode !== 201) {
240-
reject(new WebPushError('Received unexpected response code',
241-
pushResponse.statusCode, pushResponse.headers, body));
242-
} else {
243-
resolve(body);
244-
}
245-
});
246-
});
247-
248-
if (requestPayload) {
249-
pushRequest.write(requestPayload);
250-
}
251-
252-
pushRequest.end();
6+
const vapidHelper = require('./vapid-helper.js');
7+
const encryptionHelper = require('./encryption-helper.js');
8+
const WebPushLib = require('./web-push-lib.js');
2539

254-
pushRequest.on('error', function(e) {
255-
console.error(e);
256-
reject(e);
257-
});
258-
} catch (e) {
259-
reject(e);
260-
}
261-
});
262-
}
10+
const webPush = new WebPushLib();
26311

26412
module.exports = {
265-
encrypt: encrypt,
266-
sendNotification: sendNotification,
267-
setGCMAPIKey: setGCMAPIKey,
268-
WebPushError: WebPushError,
269-
generateVAPIDKeys: generateVAPIDKeys
13+
encrypt: encryptionHelper.encrypt,
14+
generateVAPIDKeys: vapidHelper.generateVAPIDKeys,
15+
setGCMAPIKey: webPush.setGCMAPIKey,
16+
setVapidDetails: webPush.setVapidDetails,
17+
sendNotification: webPush.sendNotification
27018
};

0 commit comments

Comments
 (0)