|
1 | 1 | 'use strict';
|
2 | 2 |
|
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. |
12 | 4 | require('./shim');
|
13 | 5 |
|
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'); |
253 | 9 |
|
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(); |
263 | 11 |
|
264 | 12 | 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 |
270 | 18 | };
|
0 commit comments