Skip to content

Commit 14c7697

Browse files
authored
Merge pull request #245 from gauntface/requestDetails
Request details
2 parents 4efbc22 + d82d6b4 commit 14c7697

File tree

5 files changed

+416
-46
lines changed

5 files changed

+416
-46
lines changed

README.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,131 @@ encryption.
276276
- *salt*: A string representing the salt used to encrypt the payload.
277277
- *cipherText*: The encrypted payload as a Buffer.
278278

279+
<hr />
280+
281+
## getVapidHeaders(audience, subject, publicKey, privateKey, expiration)
282+
283+
```javascript
284+
const parsedUrl = url.parse(subscription.endpoint);
285+
const audience = parsedUrl.protocol + '//' +
286+
parsedUrl.hostname;
287+
288+
const vapidHeaders = vapidHelper.getVapidHeaders(
289+
audience,
290+
'mailto: [email protected]',
291+
vapidDetails.publicKey,
292+
vapidDetails.privateKey
293+
);
294+
```
295+
296+
The *getVapidHeaders()* method will take in the values needed to create
297+
an Authorization and Crypto-Key header.
298+
299+
### Input
300+
301+
The `getVapidHeaders()` method expects the following input:
302+
303+
- *audience*: the origin of the **push service**.
304+
- *subject*: the mailto or URL for your application.
305+
- *publicKey*: the VAPID public key.
306+
- *privateKey*: the VAPID private key.
307+
308+
### Returns
309+
310+
This method returns an object with the following fields:
311+
312+
- *localPublicKey*: The public key matched the private key used during
313+
encryption.
314+
- *salt*: A string representing the salt used to encrypt the payload.
315+
- *cipherText*: The encrypted payload as a Buffer.
316+
317+
<hr />
318+
319+
## generateRequestDetails(pushSubscription, payload, options)
320+
321+
```javascript
322+
const pushSubscription = {
323+
endpoint: '< Push Subscription URL >';
324+
keys: {
325+
p256dh: '< User Public Encryption Key >',
326+
auth: '< User Auth Secret >'
327+
}
328+
};
329+
330+
const payload = '< Push Payload String >';
331+
332+
const options = {
333+
gcmAPIKey: '< GCM API Key >',
334+
vapidDetails: {
335+
subject: '< \'mailto\' Address or URL >',
336+
publicKey: '< URL Safe Base64 Encoded Public Key >',
337+
privateKey: '< URL Safe Base64 Encoded Private Key >',
338+
}
339+
TTL: <Number>
340+
}
341+
342+
try {
343+
const details = webpush.generateRequestDetails(
344+
pushSubscription,
345+
payload,
346+
options
347+
);
348+
} catch (err) {
349+
console.error(err);
350+
}
351+
```
352+
353+
> **Note:** When calling `generateRequestDetails()` the payload argument
354+
does not *need* to be defined, passing in null will return no body and
355+
> exclude any unnecessary headers.
356+
> Headers related to the GCM API Key and / or VAPID keys will be included
357+
> if supplied and required.
358+
359+
### Input
360+
361+
**Push Subscription**
362+
363+
The first argument must be an object containing the details for a push
364+
subscription.
365+
366+
The expected format is the same output as JSON.stringify'ing a PushSubscription
367+
in the browser.
368+
369+
**Payload**
370+
371+
The payload is optional, but if set, will be encrypted and a [*Buffer*](https://nodejs.org/api/buffer.html)
372+
will be returned via the `payload` parameter.
373+
374+
This argument must be either a *string* or a node
375+
[*Buffer*](https://nodejs.org/api/buffer.html).
376+
377+
> **Note:** In order to encrypt the *payload*, the *pushSubscription* **must**
378+
have a *keys* object with *p256dh* and *auth* values.
379+
380+
**Options**
381+
382+
Options is an optional argument that if defined should be an object containing
383+
any of the following values defined, although none of them are required.
384+
385+
- **gcmAPIKey** can be a GCM API key to be used for this request and this
386+
request only. This overrides any API key set via `setGCMAPIKey()`.
387+
- **vapidDetails** should be an object with *subject*, *publicKey* and
388+
*privateKey* values defined. These values should follow the [VAPID Spec](https://tools.ietf.org/html/draft-thomson-webpush-vapid).
389+
- **TTL** is a value in seconds that describes how long a push message is
390+
retained by the push service (by default, four weeks);
391+
392+
### Returns
393+
394+
An object containing all the details needed to make a network request, the
395+
object will contain:
396+
397+
- *endpoint*, the URL to send the request to;
398+
- *method*, this will be 'POST';
399+
- *headers*, the headers to add to the request;
400+
- *body*, the body of the request (As a Node Buffer).
401+
402+
<hr />
403+
279404
# Browser Support
280405

281406
<table>

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ const webPush = new WebPushLib();
88

99
module.exports = {
1010
encrypt: encryptionHelper.encrypt,
11+
getVapidHeaders: vapidHelper.getVapidHeaders,
1112
generateVAPIDKeys: vapidHelper.generateVAPIDKeys,
1213
setGCMAPIKey: webPush.setGCMAPIKey,
1314
setVapidDetails: webPush.setVapidDetails,
15+
generateRequestDetails: webPush.generateRequestDetails,
1416
sendNotification: webPush.sendNotification
1517
};

src/web-push-lib.js

Lines changed: 77 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -66,38 +66,39 @@ WebPushLib.prototype.setVapidDetails =
6666
};
6767
};
6868

69-
/**
70-
* To send a push notification call this method with a subscription, optional
71-
* payload and any options.
72-
* @param {PushSubscription} subscription The PushSubscription you wish to
73-
* send the notification to.
74-
* @param {string} [payload] The payload you wish to send to the
75-
* the user.
76-
* @param {Object} [options] Options for the GCM API key and
77-
* vapid keys can be passed in if they are unique for each notification you
78-
* wish to send.
79-
* @return {Promise} This method returns a Promise which
80-
* resolves if the sending of the notification was successful, otherwise it
81-
* rejects.
82-
*/
83-
WebPushLib.prototype.sendNotification =
69+
/**
70+
* To get the details of a request to trigger a push message, without sending
71+
* a push notification call this method.
72+
*
73+
* This method will throw an error if there is an issue with the input.
74+
* @param {PushSubscription} subscription The PushSubscription you wish to
75+
* send the notification to.
76+
* @param {string} [payload] The payload you wish to send to the
77+
* the user.
78+
* @param {Object} [options] Options for the GCM API key and
79+
* vapid keys can be passed in if they are unique for each notification you
80+
* wish to send.
81+
* @return {Object} This method returns an Object which
82+
* contains 'endpoint', 'method', 'headers' and 'payload'.
83+
*/
84+
WebPushLib.prototype.generateRequestDetails =
8485
function(subscription, payload, options) {
8586
if (!subscription || !subscription.endpoint) {
86-
return Promise.reject('You must pass in a subscription with at least ' +
87+
throw new Error('You must pass in a subscription with at least ' +
8788
'an endpoint.');
8889
}
8990

9091
if (typeof subscription.endpoint !== 'string' ||
9192
subscription.endpoint.length === 0) {
92-
return Promise.reject('The subscription endpoint must be a string with ' +
93+
throw new Error('The subscription endpoint must be a string with ' +
9394
'a valid URL.');
9495
}
9596

9697
if (payload) {
9798
// Validate the subscription keys
9899
if (!subscription.keys || !subscription.keys.p256dh ||
99100
!subscription.keys.auth) {
100-
return Promise.reject('To send a message with a payload, the ' +
101+
throw new Error('To send a message with a payload, the ' +
101102
'subscription must have \'auth\' and \'p256dh\' keys.');
102103
}
103104
}
@@ -116,7 +117,7 @@ WebPushLib.prototype.sendNotification =
116117
for (let i = 0; i < optionKeys.length; i += 1) {
117118
const optionKey = optionKeys[i];
118119
if (validOptionKeys.indexOf(optionKey) === -1) {
119-
return Promise.reject('\'' + optionKey + '\' is an invalid option. ' +
120+
throw new Error('\'' + optionKey + '\' is an invalid option. ' +
120121
'The valid options are [\'' + validOptionKeys.join('\', \'') +
121122
'\'].');
122123
}
@@ -139,7 +140,7 @@ WebPushLib.prototype.sendNotification =
139140
timeToLive = DEFAULT_TTL;
140141
}
141142

142-
const requestOptions = {
143+
const requestDetails = {
143144
method: 'POST',
144145
headers: {
145146
TTL: timeToLive
@@ -152,27 +153,23 @@ WebPushLib.prototype.sendNotification =
152153
typeof subscription !== 'object' ||
153154
!subscription.keys.p256dh ||
154155
!subscription.keys.auth) {
155-
return Promise.reject(new Error('Unable to send a message with ' +
156+
throw new Error(new Error('Unable to send a message with ' +
156157
'payload to this subscription since it doesn\'t have the ' +
157158
'required encryption keys'));
158159
}
159160

160-
try {
161-
const encrypted = encryptionHelper.encrypt(
162-
subscription.keys.p256dh, subscription.keys.auth, payload);
161+
const encrypted = encryptionHelper.encrypt(
162+
subscription.keys.p256dh, subscription.keys.auth, payload);
163163

164-
requestOptions.headers['Content-Length'] = encrypted.cipherText.length;
165-
requestOptions.headers['Content-Type'] = 'application/octet-stream';
166-
requestOptions.headers['Content-Encoding'] = 'aesgcm';
167-
requestOptions.headers.Encryption = 'salt=' + encrypted.salt;
168-
requestOptions.headers['Crypto-Key'] = 'dh=' + urlBase64.encode(encrypted.localPublicKey);
164+
requestDetails.headers['Content-Length'] = encrypted.cipherText.length;
165+
requestDetails.headers['Content-Type'] = 'application/octet-stream';
166+
requestDetails.headers['Content-Encoding'] = 'aesgcm';
167+
requestDetails.headers.Encryption = 'salt=' + encrypted.salt;
168+
requestDetails.headers['Crypto-Key'] = 'dh=' + urlBase64.encode(encrypted.localPublicKey);
169169

170-
requestPayload = encrypted.cipherText;
171-
} catch (err) {
172-
return Promise.reject(err);
173-
}
170+
requestPayload = encrypted.cipherText;
174171
} else {
175-
requestOptions.headers['Content-Length'] = 0;
172+
requestDetails.headers['Content-Length'] = 0;
176173
}
177174

178175
const isGCM = subscription.endpoint.indexOf(
@@ -183,7 +180,7 @@ WebPushLib.prototype.sendNotification =
183180
console.warn('Attempt to send push notification to GCM endpoint, ' +
184181
'but no GCM key is defined'.bold.red);
185182
} else {
186-
requestOptions.headers.Authorization = 'key=' + currentGCMAPIKey;
183+
requestDetails.headers.Authorization = 'key=' + currentGCMAPIKey;
187184
}
188185
} else if (currentVapidDetails) {
189186
const parsedUrl = url.parse(subscription.endpoint);
@@ -197,22 +194,56 @@ WebPushLib.prototype.sendNotification =
197194
currentVapidDetails.privateKey
198195
);
199196

200-
requestOptions.headers.Authorization = vapidHeaders.Authorization;
201-
if (requestOptions.headers['Crypto-Key']) {
202-
requestOptions.headers['Crypto-Key'] += ';' +
197+
requestDetails.headers.Authorization = vapidHeaders.Authorization;
198+
if (requestDetails.headers['Crypto-Key']) {
199+
requestDetails.headers['Crypto-Key'] += ';' +
203200
vapidHeaders['Crypto-Key'];
204201
} else {
205-
requestOptions.headers['Crypto-Key'] = vapidHeaders['Crypto-Key'];
202+
requestDetails.headers['Crypto-Key'] = vapidHeaders['Crypto-Key'];
206203
}
207204
}
208205

206+
requestDetails.body = requestPayload;
207+
requestDetails.endpoint = subscription.endpoint;
208+
209+
return requestDetails;
210+
};
211+
212+
/**
213+
* To send a push notification call this method with a subscription, optional
214+
* payload and any options.
215+
* @param {PushSubscription} subscription The PushSubscription you wish to
216+
* send the notification to.
217+
* @param {string} [payload] The payload you wish to send to the
218+
* the user.
219+
* @param {Object} [options] Options for the GCM API key and
220+
* vapid keys can be passed in if they are unique for each notification you
221+
* wish to send.
222+
* @return {Promise} This method returns a Promise which
223+
* resolves if the sending of the notification was successful, otherwise it
224+
* rejects.
225+
*/
226+
WebPushLib.prototype.sendNotification =
227+
function(subscription, payload, options) {
228+
let requestDetails;
229+
try {
230+
requestDetails = this.generateRequestDetails(
231+
subscription, payload, options);
232+
} catch (err) {
233+
return Promise.reject(err);
234+
}
235+
209236
return new Promise(function(resolve, reject) {
210-
const urlParts = url.parse(subscription.endpoint);
211-
requestOptions.hostname = urlParts.hostname;
212-
requestOptions.port = urlParts.port;
213-
requestOptions.path = urlParts.path;
237+
const httpsOptions = {};
238+
const urlParts = url.parse(requestDetails.endpoint);
239+
httpsOptions.hostname = urlParts.hostname;
240+
httpsOptions.port = urlParts.port;
241+
httpsOptions.path = urlParts.path;
242+
243+
httpsOptions.headers = requestDetails.headers;
244+
httpsOptions.method = requestDetails.method;
214245

215-
const pushRequest = https.request(requestOptions, function(pushResponse) {
246+
const pushRequest = https.request(httpsOptions, function(pushResponse) {
216247
let responseText = '';
217248

218249
pushResponse.on('data', function(chunk) {
@@ -237,8 +268,8 @@ WebPushLib.prototype.sendNotification =
237268
reject(e);
238269
});
239270

240-
if (requestPayload) {
241-
pushRequest.write(requestPayload);
271+
if (requestDetails.body) {
272+
pushRequest.write(requestDetails.body);
242273
}
243274

244275
pushRequest.end();

0 commit comments

Comments
 (0)