Initial commit
This commit is contained in:
5
node_modules/web-push/LICENSE
generated
vendored
Normal file
5
node_modules/web-push/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
Copyright 2015 Marco Castelluccio
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
599
node_modules/web-push/README.md
generated
vendored
Normal file
599
node_modules/web-push/README.md
generated
vendored
Normal file
@@ -0,0 +1,599 @@
|
||||
<h1 align="center">web-push</h1>
|
||||
|
||||
[](https://github.com/web-push-libs/web-push/actions/workflows/ci.yml)
|
||||
|
||||
# Why
|
||||
|
||||
Web push requires that push messages triggered from a backend be done via the
|
||||
[Web Push Protocol](https://tools.ietf.org/html/draft-ietf-webpush-protocol)
|
||||
and if you want to send data with your push message, you must also encrypt
|
||||
that data according to the [Message Encryption for Web Push spec](https://tools.ietf.org/html/draft-ietf-webpush-encryption).
|
||||
|
||||
This module makes it easy to send messages and will also handle legacy support
|
||||
for browsers relying on GCM for message sending / delivery.
|
||||
|
||||
# Install
|
||||
|
||||
Installation is simple, just install via npm.
|
||||
|
||||
npm install web-push --save
|
||||
|
||||
# Usage
|
||||
|
||||
The common use case for this library is an application server using
|
||||
a GCM API key and VAPID keys.
|
||||
|
||||
```javascript
|
||||
const webpush = require('web-push');
|
||||
|
||||
// VAPID keys should be generated only once.
|
||||
const vapidKeys = webpush.generateVAPIDKeys();
|
||||
|
||||
webpush.setGCMAPIKey('<Your GCM API Key Here>');
|
||||
webpush.setVapidDetails(
|
||||
'mailto:example@yourdomain.org',
|
||||
vapidKeys.publicKey,
|
||||
vapidKeys.privateKey
|
||||
);
|
||||
|
||||
// This is the same output of calling JSON.stringify on a PushSubscription
|
||||
const pushSubscription = {
|
||||
endpoint: '.....',
|
||||
keys: {
|
||||
auth: '.....',
|
||||
p256dh: '.....'
|
||||
}
|
||||
};
|
||||
|
||||
webpush.sendNotification(pushSubscription, 'Your Push Payload Text');
|
||||
```
|
||||
|
||||
## Using VAPID Key for applicationServerKey
|
||||
|
||||
When subscribing to push messages, you'll need to pass your VAPID key,
|
||||
which you can do like so:
|
||||
|
||||
```javascript
|
||||
registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: '<Your Public Key from generateVAPIDKeys()>'
|
||||
});
|
||||
```
|
||||
|
||||
## Command Line
|
||||
|
||||
You can install `web-push` globally and use it for sending notifications
|
||||
and / or generating VAPID keys.
|
||||
|
||||
Install like so:
|
||||
|
||||
npm install web-push -g
|
||||
|
||||
Then you can run the following commands:
|
||||
|
||||
Usage:
|
||||
|
||||
web-push send-notification --endpoint=<url> [--key=<browser key>] [--auth=<auth secret>] [--payload=<message>] [--encoding=<aesgcm | aes128gcm>] [--ttl=<seconds>] [--vapid-subject=<vapid subject>] [--vapid-pubkey=<public key url base64>] [--vapid-pvtkey=<private key url base64>] [--proxy=<http proxy uri>] [--gcm-api-key=<api key>]
|
||||
|
||||
web-push generate-vapid-keys [--json]
|
||||
|
||||
Example of send notification:
|
||||
```shell
|
||||
> web-push generate-vapid-keys --json
|
||||
> {"publicKey":"BGtkbcjrO12YMoDuq2sCQeHlu47uPx3SHTgFKZFYiBW8Qr0D9vgyZSZPdw6_4ZFEI9Snk1VEAj2qTYI1I1YxBXE","privateKey":"I0_d0vnesxbBSUmlDdOKibGo6vEXRO-Vu88QlSlm5j0"}
|
||||
```
|
||||
The subscription value:
|
||||
```javascript
|
||||
{
|
||||
"endpoint": "https://fcm.googleapis.com/fcm/send/d61c5u920dw:APA91bEmnw8utjDYCqSRplFMVCzQMg9e5XxpYajvh37mv2QIlISdasBFLbFca9ZZ4Uqcya0ck-SP84YJUEnWsVr3mwYfaDB7vGtsDQuEpfDdcIqOX_wrCRkBW2NDWRZ9qUz9hSgtI3sY",
|
||||
"expirationTime": null,
|
||||
"keys": {
|
||||
"p256dh": "BL7ELU24fJTAlH5Kyl8N6BDCac8u8li_U5PIwG963MOvdYs9s7LSzj8x_7v7RFdLZ9Eap50PiiyF5K0TDAis7t0",
|
||||
"auth": "juarI8x__VnHvsOgfeAPHg"
|
||||
}
|
||||
}
|
||||
```
|
||||
The command example:
|
||||
```shell
|
||||
web-push send-notification \
|
||||
--endpoint=https://fcm.googleapis.com/fcm/send/d61c5u920dw:APA91bEmnw8utjDYCqSRplFMVCzQMg9e5XxpYajvh37mv2QIlISdasBFLbFca9ZZ4Uqcya0ck-SP84YJUEnWsVr3mwYfaDB7vGtsDQuEpfDdcIqOX_wrCRkBW2NDWRZ9qUz9hSgtI3sY \
|
||||
--key=BL7ELU24fJTAlH5Kyl8N6BDCac8u8li_U5PIwG963MOvdYs9s7LSzj8x_7v7RFdLZ9Eap50PiiyF5K0TDAis7t0 \
|
||||
--auth=juarI8x__VnHvsOgfeAPHg \
|
||||
--vapid-subject=mailto:example@qq.com \
|
||||
--vapid-pubkey=BGtkbcjrO12YMoDuq2sCQeHlu47uPx3SHTgFKZFYiBW8Qr0D9vgyZSZPdw6_4ZFEI9Snk1VEAj2qTYI1I1YxBXE \
|
||||
--vapid-pvtkey=I0_d0vnesxbBSUmlDdOKibGo6vEXRO-Vu88QlSlm5j0 \
|
||||
--payload=Hello
|
||||
```
|
||||
|
||||
# API Reference
|
||||
|
||||
## sendNotification(pushSubscription, payload, options)
|
||||
|
||||
```javascript
|
||||
const pushSubscription = {
|
||||
endpoint: '< Push Subscription URL >',
|
||||
keys: {
|
||||
p256dh: '< User Public Encryption Key >',
|
||||
auth: '< User Auth Secret >'
|
||||
}
|
||||
};
|
||||
|
||||
const payload = '< Push Payload String >';
|
||||
|
||||
const options = {
|
||||
gcmAPIKey: '< GCM API Key >',
|
||||
vapidDetails: {
|
||||
subject: '< \'mailto\' Address or URL >',
|
||||
publicKey: '< URL Safe Base64 Encoded Public Key >',
|
||||
privateKey: '< URL Safe Base64 Encoded Private Key >'
|
||||
},
|
||||
timeout: <Number>
|
||||
TTL: <Number>,
|
||||
headers: {
|
||||
'< header name >': '< header value >'
|
||||
},
|
||||
contentEncoding: '< Encoding type, e.g.: aesgcm or aes128gcm >',
|
||||
urgency:'< Default is "normal" >',
|
||||
topic:'< Use a maximum of 32 characters from the URL or filename-safe Base64 characters sets. >',
|
||||
|
||||
proxy: '< proxy server options >',
|
||||
agent: '< https.Agent instance >'
|
||||
}
|
||||
|
||||
webpush.sendNotification(
|
||||
pushSubscription,
|
||||
payload,
|
||||
options
|
||||
);
|
||||
```
|
||||
|
||||
> **Note:** `sendNotification()` you don't need to define a payload, and this
|
||||
method will work without a GCM API Key and / or VAPID keys if the push service
|
||||
supports it.
|
||||
|
||||
### Input
|
||||
|
||||
**Push Subscription**
|
||||
|
||||
The first argument must be an object containing the details for a push
|
||||
subscription.
|
||||
|
||||
The expected format is the same output as JSON.stringify'ing a PushSubscription
|
||||
in the browser.
|
||||
|
||||
**Payload**
|
||||
|
||||
The payload is optional, but if set, will be the data sent with a push
|
||||
message.
|
||||
|
||||
This must be either a *string* or a node
|
||||
[*Buffer*](https://nodejs.org/api/buffer.html).
|
||||
|
||||
> **Note:** In order to encrypt the *payload*, the *pushSubscription* **must**
|
||||
have a *keys* object with *p256dh* and *auth* values.
|
||||
|
||||
**Options**
|
||||
|
||||
Options is an optional argument that if defined should be an object containing
|
||||
any of the following values defined, although none of them are required.
|
||||
|
||||
- **gcmAPIKey** can be a GCM API key to be used for this request and this
|
||||
request only. This overrides any API key set via `setGCMAPIKey()`.
|
||||
- **vapidDetails** should be an object with *subject*, *publicKey* and
|
||||
*privateKey* values defined. These values should follow the [VAPID Spec](https://tools.ietf.org/html/draft-thomson-webpush-vapid).
|
||||
- **timeout** is a value in milliseconds that specifies the request's socket timeout. On timeout, the request will be destroyed and the promise will be rejected with a meaningful error. It's a common misconception that a socket timeout is the timeout to receive the full response. So if you have a socket timeout of 1 second, and a response comprised of 3 TCP packets, where each response packet takes 0.9 seconds to arrive, for a total response time of 2.7 seconds, then there will be no timeout. Once a socket 'timeout' triggers the request will be aborted by the library (by default undefined).
|
||||
- **TTL** is a value in seconds that describes how long a push message is
|
||||
retained by the push service (by default, four weeks).
|
||||
- **headers** is an object with all the extra headers you want to add to the request.
|
||||
- **contentEncoding** is the type of push encoding to use (e.g. 'aes128gcm', by default, or 'aesgcm').
|
||||
- **urgency** is to indicate to the push service whether to send the notification immediately or prioritize the recipient’s device power considerations for delivery. Provide one of the following values: very-low, low, normal, or high. To attempt to deliver the notification immediately, specify high.
|
||||
- **topic** optionally provide an identifier that the push service uses to coalesce notifications. Use a maximum of 32 characters from the URL or filename-safe Base64 characters sets.
|
||||
- **proxy** is the [HttpsProxyAgent's constructor argument](https://github.com/TooTallNate/node-https-proxy-agent#new-httpsproxyagentobject-options)
|
||||
that may either be a string URI of the proxy server (eg. http://< hostname >:< port >)
|
||||
or an "options" object with more specific properties.
|
||||
- **agent** is the [HTTPS Agent instance](https://nodejs.org/dist/latest/docs/api/https.html#https_class_https_agent) which will be used in the `https.request` method. If the `proxy` options defined, `agent` will be ignored!
|
||||
|
||||
> **Note:** As of this writing, if a push notification request contains a VAPID `subject` referencing an `https://localhost` URI (set either using the `options` argument or via the global `setVapidDetails()` method), Safari's push notification endpoint rejects the request with a `BadJwtToken` error.
|
||||
|
||||
### Returns
|
||||
|
||||
A promise that resolves if the notification was sent successfully
|
||||
with details of the request, otherwise it rejects.
|
||||
|
||||
In both cases, resolving or rejecting, you'll be able to access the following
|
||||
values on the returned object or error.
|
||||
|
||||
- *statusCode*, the status code of the response from the push service;
|
||||
- *headers*, the headers of the response from the push service;
|
||||
- *body*, the body of the response from the push service.
|
||||
|
||||
<hr />
|
||||
|
||||
## generateVAPIDKeys()
|
||||
|
||||
```javascript
|
||||
const vapidKeys = webpush.generateVAPIDKeys();
|
||||
|
||||
// Prints 2 URL Safe Base64 Encoded Strings
|
||||
console.log(vapidKeys.publicKey, vapidKeys.privateKey);
|
||||
```
|
||||
|
||||
### Input
|
||||
|
||||
None.
|
||||
|
||||
### Returns
|
||||
|
||||
Returns an object with **publicKey** and **privateKey** values which are
|
||||
URL Safe Base64 encoded strings.
|
||||
|
||||
> **Note:** You should create these keys once, store them and use them for all
|
||||
> future messages you send.
|
||||
|
||||
<hr />
|
||||
|
||||
## setGCMAPIKey(apiKey)
|
||||
|
||||
```javascript
|
||||
webpush.setGCMAPIKey('Your GCM API Key');
|
||||
```
|
||||
|
||||
### Input
|
||||
|
||||
This method expects the GCM API key that is linked to the `gcm_sender_id ` in
|
||||
your web app manifest.
|
||||
|
||||
You can use a GCM API Key from the Google Developer Console or the
|
||||
*Cloud Messaging* tab under a Firebase Project.
|
||||
|
||||
### Returns
|
||||
|
||||
None.
|
||||
|
||||
<hr />
|
||||
|
||||
## setVapidDetails(subject, publicKey, privateKey)
|
||||
|
||||
```javascript
|
||||
webpush.setVapidDetails(
|
||||
'mailto:user@example.org',
|
||||
process.env.VAPID_PUBLIC_KEY,
|
||||
process.env.VAPID_PRIVATE_KEY
|
||||
);
|
||||
```
|
||||
|
||||
Globally sets the application's VAPID subject, public key, and private key, to be used in subsequent calls to `sendNotification()` and `generateRequestDetails()` that don't specifically override them in their `options` argument.
|
||||
|
||||
### Input
|
||||
|
||||
The `setVapidDetails` method expects the following input:
|
||||
|
||||
- *subject*: the VAPID server contact information, as either an `https:` or `mailto:` URI ([as per the VAPID spec](https://datatracker.ietf.org/doc/html/draft-thomson-webpush-vapid#section-2.1)).
|
||||
- *publicKey*: the VAPID public key.
|
||||
- *privateKey*: the VAPID private key.
|
||||
|
||||
### Returns
|
||||
|
||||
None.
|
||||
|
||||
<hr />
|
||||
|
||||
## encrypt(userPublicKey, userAuth, payload, contentEncoding)
|
||||
|
||||
```javascript
|
||||
const pushSubscription = {
|
||||
endpoint: 'https://....',
|
||||
keys: {
|
||||
p256dh: '.....',
|
||||
auth: '.....'
|
||||
}
|
||||
};
|
||||
webPush.encrypt(
|
||||
pushSubscription.keys.p256dh,
|
||||
pushSubscription.keys.auth,
|
||||
'My Payload',
|
||||
'aes128gcm'
|
||||
)
|
||||
.then(encryptionDetails => {
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
Encrypts the payload according to the [Message Encryption for Web
|
||||
Push](https://webpush-wg.github.io/webpush-encryption/) standard.
|
||||
|
||||
> (*sendNotification* will automatically encrypt the payload for you, so if
|
||||
> you use *sendNotification* you don't need to worry about it).
|
||||
|
||||
### Input
|
||||
|
||||
The `encrypt()` method expects the following input:
|
||||
|
||||
- *userPublicKey*: the public key of the receiver (from the browser).
|
||||
- *userAuth*: the auth secret of the receiver (from the browser).
|
||||
- *payload*: the message to attach to the notification.
|
||||
- *contentEncoding*: the type of content encoding to use (e.g. aesgcm or aes128gcm).
|
||||
|
||||
### Returns
|
||||
|
||||
This method returns an object with the following fields:
|
||||
|
||||
- *localPublicKey*: The public key matched the private key used during
|
||||
encryption.
|
||||
- *salt*: A string representing the salt used to encrypt the payload.
|
||||
- *cipherText*: The encrypted payload as a Buffer.
|
||||
|
||||
<hr />
|
||||
|
||||
## getVapidHeaders(audience, subject, publicKey, privateKey, contentEncoding, expiration)
|
||||
|
||||
```javascript
|
||||
const parsedUrl = url.parse(subscription.endpoint);
|
||||
const audience = parsedUrl.protocol + '//' +
|
||||
parsedUrl.hostname;
|
||||
|
||||
const vapidHeaders = vapidHelper.getVapidHeaders(
|
||||
audience,
|
||||
'mailto: example@web-push-node.org',
|
||||
vapidDetails.publicKey,
|
||||
vapidDetails.privateKey,
|
||||
'aes128gcm'
|
||||
);
|
||||
```
|
||||
|
||||
The *getVapidHeaders()* method will take in the values needed to create
|
||||
an Authorization and Crypto-Key header.
|
||||
|
||||
### Input
|
||||
|
||||
The `getVapidHeaders()` method expects the following input:
|
||||
|
||||
- *audience*: the origin of the **push service**.
|
||||
- *subject*: the mailto or URL for your application.
|
||||
- *publicKey*: the VAPID public key.
|
||||
- *privateKey*: the VAPID private key.
|
||||
- *contentEncoding*: the type of content encoding to use (e.g. aesgcm or aes128gcm).
|
||||
|
||||
### Returns
|
||||
|
||||
This method returns an object with the following fields:
|
||||
|
||||
- *localPublicKey*: The public key matched the private key used during
|
||||
encryption.
|
||||
- *salt*: A string representing the salt used to encrypt the payload.
|
||||
- *cipherText*: The encrypted payload as a Buffer.
|
||||
|
||||
<hr />
|
||||
|
||||
## generateRequestDetails(pushSubscription, payload, options)
|
||||
|
||||
```javascript
|
||||
const pushSubscription = {
|
||||
endpoint: '< Push Subscription URL >';
|
||||
keys: {
|
||||
p256dh: '< User Public Encryption Key >',
|
||||
auth: '< User Auth Secret >'
|
||||
}
|
||||
};
|
||||
|
||||
const payload = '< Push Payload String >';
|
||||
|
||||
const options = {
|
||||
gcmAPIKey: '< GCM API Key >',
|
||||
vapidDetails: {
|
||||
subject: '< \'mailto\' Address or URL >',
|
||||
publicKey: '< URL Safe Base64 Encoded Public Key >',
|
||||
privateKey: '< URL Safe Base64 Encoded Private Key >',
|
||||
}
|
||||
TTL: <Number>,
|
||||
headers: {
|
||||
'< header name >': '< header value >'
|
||||
},
|
||||
contentEncoding: '< Encoding type, e.g.: aesgcm or aes128gcm >',
|
||||
urgency:'< Default is normal "Defult" >',
|
||||
topic:'< Use a maximum of 32 characters from the URL or filename-safe Base64 characters sets. >',
|
||||
proxy: '< proxy server options >'
|
||||
}
|
||||
|
||||
try {
|
||||
const details = webpush.generateRequestDetails(
|
||||
pushSubscription,
|
||||
payload,
|
||||
options
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** When calling `generateRequestDetails()` the payload argument
|
||||
does not *need* to be defined, passing in null will return no body and
|
||||
> exclude any unnecessary headers.
|
||||
> Headers related to the GCM API Key and / or VAPID keys will be included
|
||||
> if supplied and required.
|
||||
|
||||
### Input
|
||||
|
||||
**Push Subscription**
|
||||
|
||||
The first argument must be an object containing the details for a push
|
||||
subscription.
|
||||
|
||||
The expected format is the same output as JSON.stringify'ing a PushSubscription
|
||||
in the browser.
|
||||
|
||||
**Payload**
|
||||
|
||||
The payload is optional, but if set, will be encrypted and a [*Buffer*](https://nodejs.org/api/buffer.html)
|
||||
will be returned via the `payload` parameter.
|
||||
|
||||
This argument must be either a *string* or a node
|
||||
[*Buffer*](https://nodejs.org/api/buffer.html).
|
||||
|
||||
> **Note:** In order to encrypt the *payload*, the *pushSubscription* **must**
|
||||
have a *keys* object with *p256dh* and *auth* values.
|
||||
|
||||
**Options**
|
||||
|
||||
Options is an optional argument that if defined should be an object containing
|
||||
any of the following values defined, although none of them are required.
|
||||
|
||||
- **gcmAPIKey** can be a GCM API key to be used for this request and this
|
||||
request only. This overrides any API key set via `setGCMAPIKey()`.
|
||||
- **vapidDetails** should be an object with *subject*, *publicKey* and
|
||||
*privateKey* values defined. These values should follow the [VAPID Spec](https://tools.ietf.org/html/draft-thomson-webpush-vapid).
|
||||
- **TTL** is a value in seconds that describes how long a push message is
|
||||
retained by the push service (by default, four weeks).
|
||||
- **headers** is an object with all the extra headers you want to add to the request.
|
||||
- **contentEncoding** is the type of push encoding to use (e.g. 'aesgcm', by default, or 'aes128gcm').
|
||||
- **urgency** is to indicate to the push service whether to send the notification immediately or prioritize the recipient’s device power considerations for delivery. Provide one of the following values: very-low, low, normal, or high. To attempt to deliver the notification immediately, specify high.
|
||||
- **topic** optionally provide an identifier that the push service uses to coalesce notifications. Use a maximum of 32 characters from the URL or filename-safe Base64 characters sets.
|
||||
- **proxy** is the [HttpsProxyAgent's constructor argument](https://github.com/TooTallNate/node-https-proxy-agent#new-httpsproxyagentobject-options)
|
||||
that may either be a string URI of the proxy server (eg. http://< hostname >:< port >)
|
||||
or an "options" object with more specific properties.
|
||||
|
||||
### Returns
|
||||
|
||||
An object containing all the details needed to make a network request, the
|
||||
object will contain:
|
||||
|
||||
- *endpoint*, the URL to send the request to;
|
||||
- *method*, this will be 'POST';
|
||||
- *headers*, the headers to add to the request;
|
||||
- *body*, the body of the request (As a Node Buffer).
|
||||
|
||||
<hr />
|
||||
|
||||
# Browser Support
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><strong>Browser</strong></th>
|
||||
<th width="130px"><strong>Push without Payload</strong></th>
|
||||
<th width="130px"><strong>Push with Payload</strong></th>
|
||||
<th width="130px"><strong>VAPID</strong></th>
|
||||
<th><strong>Notes</strong></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Chrome</td>
|
||||
|
||||
<!-- Push without payloads support-->
|
||||
<td>✓ v42+</td>
|
||||
|
||||
<!-- Push with payload support -->
|
||||
<td>✓ v50+</td>
|
||||
|
||||
<!-- VAPID Support -->
|
||||
<td>✓ v52+</td>
|
||||
|
||||
<td>In v51 and less, the `gcm_sender_id` is needed to get a push subscription.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Edge</td>
|
||||
|
||||
<!-- Push without payloads support-->
|
||||
<td>✓ v17+ (April 2018)</td>
|
||||
|
||||
<!-- Push with payload support -->
|
||||
<td>✓ v17+ (April 2018)</td>
|
||||
|
||||
<!-- VAPID Support -->
|
||||
<td>✓ v17+ (April 2018)</td>
|
||||
|
||||
<td></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Firefox</td>
|
||||
|
||||
<!-- Push without payloads support-->
|
||||
<td>✓ v44+</td>
|
||||
|
||||
<!-- Push with payload support -->
|
||||
<td>✓ v44+</td>
|
||||
|
||||
<!-- VAPID Support -->
|
||||
<td>✓ v46+</td>
|
||||
|
||||
<td></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Opera</td>
|
||||
|
||||
<!-- Push without payloads support-->
|
||||
<td>✓ v39+ <strong>*</strong></td>
|
||||
|
||||
<!-- Push with payload support -->
|
||||
<td>✓ v39+ <strong>*</strong></td>
|
||||
|
||||
<!-- VAPID Support -->
|
||||
<td>✗</td>
|
||||
|
||||
<td>
|
||||
<strong>*</strong> Opera supports push on Android but not on desktop.
|
||||
<br />
|
||||
<br />
|
||||
The `gcm_sender_id` is needed to get a push subscription.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Safari</td>
|
||||
|
||||
<!-- Push without payloads support-->
|
||||
<td>✓ v16+ </td>
|
||||
|
||||
<!-- Push with payload support -->
|
||||
<td>✓ v16+</td>
|
||||
|
||||
<!-- VAPID Support -->
|
||||
<td>✓ v16+</td>
|
||||
|
||||
<td>Safari 16 in macOS 13 or later</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Samsung Internet Browser</td>
|
||||
|
||||
<!-- Push without payloads support-->
|
||||
<td>✓ v4.0.10-53+</td>
|
||||
|
||||
<!-- Push with payload support -->
|
||||
<td>✓ v5.0.30-40+</td>
|
||||
|
||||
<!-- VAPID Support -->
|
||||
<td>✗</td>
|
||||
|
||||
<td>The `gcm_sender_id` is needed to get a push subscription.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
# Help
|
||||
|
||||
**MDN**
|
||||
|
||||
There's an example on [MDN](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Tutorials/js13kGames/Re-engageable_Notifications_Push).
|
||||
|
||||
Also, the [Service Worker Cookbook](https://github.com/mdn/serviceworker-cookbook) is full of Web Push
|
||||
examples using this library.
|
||||
|
||||
# Running tests
|
||||
|
||||
> Prerequisites:
|
||||
> * Java JDK or JRE (http://www.oracle.com/technetwork/java/javase/downloads/index.html)
|
||||
|
||||
To run tests:
|
||||
|
||||
npm test
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/web-push">
|
||||
<img src="https://nodei.co/npm/web-push.svg?downloads=true" />
|
||||
</a>
|
||||
</p>
|
||||
17
node_modules/web-push/node_modules/jwa/LICENSE
generated
vendored
Normal file
17
node_modules/web-push/node_modules/jwa/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
Copyright (c) 2013 Brian J. Brennan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
150
node_modules/web-push/node_modules/jwa/README.md
generated
vendored
Normal file
150
node_modules/web-push/node_modules/jwa/README.md
generated
vendored
Normal file
@@ -0,0 +1,150 @@
|
||||
# node-jwa [](https://travis-ci.org/brianloveswords/node-jwa)
|
||||
|
||||
A
|
||||
[JSON Web Algorithms](http://tools.ietf.org/id/draft-ietf-jose-json-web-algorithms-08.html)
|
||||
implementation focusing (exclusively, at this point) on the algorithms necessary for
|
||||
[JSON Web Signatures](http://self-issued.info/docs/draft-ietf-jose-json-web-signature.html).
|
||||
|
||||
This library supports all of the required, recommended and optional cryptographic algorithms for JWS:
|
||||
|
||||
alg Parameter Value | Digital Signature or MAC Algorithm
|
||||
----------------|----------------------------
|
||||
HS256 | HMAC using SHA-256 hash algorithm
|
||||
HS384 | HMAC using SHA-384 hash algorithm
|
||||
HS512 | HMAC using SHA-512 hash algorithm
|
||||
RS256 | RSASSA using SHA-256 hash algorithm
|
||||
RS384 | RSASSA using SHA-384 hash algorithm
|
||||
RS512 | RSASSA using SHA-512 hash algorithm
|
||||
PS256 | RSASSA-PSS using SHA-256 hash algorithm
|
||||
PS384 | RSASSA-PSS using SHA-384 hash algorithm
|
||||
PS512 | RSASSA-PSS using SHA-512 hash algorithm
|
||||
ES256 | ECDSA using P-256 curve and SHA-256 hash algorithm
|
||||
ES384 | ECDSA using P-384 curve and SHA-384 hash algorithm
|
||||
ES512 | ECDSA using P-521 curve and SHA-512 hash algorithm
|
||||
none | No digital signature or MAC value included
|
||||
|
||||
Please note that PS* only works on Node 6.12+ (excluding 7.x).
|
||||
|
||||
# Requirements
|
||||
|
||||
In order to run the tests, a recent version of OpenSSL is
|
||||
required. **The version that comes with OS X (OpenSSL 0.9.8r 8 Feb
|
||||
2011) is not recent enough**, as it does not fully support ECDSA
|
||||
keys. You'll need to use a version > 1.0.0; I tested with OpenSSL 1.0.1c 10 May 2012.
|
||||
|
||||
# Testing
|
||||
|
||||
To run the tests, do
|
||||
|
||||
```bash
|
||||
$ npm test
|
||||
```
|
||||
|
||||
This will generate a bunch of keypairs to use in testing. If you want to
|
||||
generate new keypairs, do `make clean` before running `npm test` again.
|
||||
|
||||
## Methodology
|
||||
|
||||
I spawn `openssl dgst -sign` to test OpenSSL sign → JS verify and
|
||||
`openssl dgst -verify` to test JS sign → OpenSSL verify for each of the
|
||||
RSA and ECDSA algorithms.
|
||||
|
||||
# Usage
|
||||
|
||||
## jwa(algorithm)
|
||||
|
||||
Creates a new `jwa` object with `sign` and `verify` methods for the
|
||||
algorithm. Valid values for algorithm can be found in the table above
|
||||
(`'HS256'`, `'HS384'`, etc) and are case-sensitive. Passing an invalid
|
||||
algorithm value will throw a `TypeError`.
|
||||
|
||||
|
||||
## jwa#sign(input, secretOrPrivateKey)
|
||||
|
||||
Sign some input with either a secret for HMAC algorithms, or a private
|
||||
key for RSA and ECDSA algorithms.
|
||||
|
||||
If input is not already a string or buffer, `JSON.stringify` will be
|
||||
called on it to attempt to coerce it.
|
||||
|
||||
For the HMAC algorithm, `secretOrPrivateKey` should be a string or a
|
||||
buffer. For ECDSA and RSA, the value should be a string representing a
|
||||
PEM encoded **private** key.
|
||||
|
||||
Output [base64url](http://en.wikipedia.org/wiki/Base64#URL_applications)
|
||||
formatted. This is for convenience as JWS expects the signature in this
|
||||
format. If your application needs the output in a different format,
|
||||
[please open an issue](https://github.com/brianloveswords/node-jwa/issues). In
|
||||
the meantime, you can use
|
||||
[brianloveswords/base64url](https://github.com/brianloveswords/base64url)
|
||||
to decode the signature.
|
||||
|
||||
As of nodejs *v0.11.8*, SPKAC support was introduce. If your nodeJs
|
||||
version satisfies, then you can pass an object `{ key: '..', passphrase: '...' }`
|
||||
|
||||
|
||||
## jwa#verify(input, signature, secretOrPublicKey)
|
||||
|
||||
Verify a signature. Returns `true` or `false`.
|
||||
|
||||
`signature` should be a base64url encoded string.
|
||||
|
||||
For the HMAC algorithm, `secretOrPublicKey` should be a string or a
|
||||
buffer. For ECDSA and RSA, the value should be a string represented a
|
||||
PEM encoded **public** key.
|
||||
|
||||
|
||||
# Example
|
||||
|
||||
HMAC
|
||||
```js
|
||||
const jwa = require('jwa');
|
||||
|
||||
const hmac = jwa('HS256');
|
||||
const input = 'super important stuff';
|
||||
const secret = 'shhhhhh';
|
||||
|
||||
const signature = hmac.sign(input, secret);
|
||||
hmac.verify(input, signature, secret) // === true
|
||||
hmac.verify(input, signature, 'trickery!') // === false
|
||||
```
|
||||
|
||||
With keys
|
||||
```js
|
||||
const fs = require('fs');
|
||||
const jwa = require('jwa');
|
||||
const privateKey = fs.readFileSync(__dirname + '/ecdsa-p521-private.pem');
|
||||
const publicKey = fs.readFileSync(__dirname + '/ecdsa-p521-public.pem');
|
||||
|
||||
const ecdsa = jwa('ES512');
|
||||
const input = 'very important stuff';
|
||||
|
||||
const signature = ecdsa.sign(input, privateKey);
|
||||
ecdsa.verify(input, signature, publicKey) // === true
|
||||
```
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
```
|
||||
Copyright (c) 2013 Brian J. Brennan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
```
|
||||
266
node_modules/web-push/node_modules/jwa/index.js
generated
vendored
Normal file
266
node_modules/web-push/node_modules/jwa/index.js
generated
vendored
Normal file
@@ -0,0 +1,266 @@
|
||||
var Buffer = require('safe-buffer').Buffer;
|
||||
var crypto = require('crypto');
|
||||
var formatEcdsa = require('ecdsa-sig-formatter');
|
||||
var util = require('util');
|
||||
|
||||
var MSG_INVALID_ALGORITHM = '"%s" is not a valid algorithm.\n Supported algorithms are:\n "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512" and "none".'
|
||||
var MSG_INVALID_SECRET = 'secret must be a string or buffer';
|
||||
var MSG_INVALID_VERIFIER_KEY = 'key must be a string or a buffer';
|
||||
var MSG_INVALID_SIGNER_KEY = 'key must be a string, a buffer or an object';
|
||||
|
||||
var supportsKeyObjects = typeof crypto.createPublicKey === 'function';
|
||||
if (supportsKeyObjects) {
|
||||
MSG_INVALID_VERIFIER_KEY += ' or a KeyObject';
|
||||
MSG_INVALID_SECRET += 'or a KeyObject';
|
||||
}
|
||||
|
||||
function checkIsPublicKey(key) {
|
||||
if (Buffer.isBuffer(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof key === 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!supportsKeyObjects) {
|
||||
throw typeError(MSG_INVALID_VERIFIER_KEY);
|
||||
}
|
||||
|
||||
if (typeof key !== 'object') {
|
||||
throw typeError(MSG_INVALID_VERIFIER_KEY);
|
||||
}
|
||||
|
||||
if (typeof key.type !== 'string') {
|
||||
throw typeError(MSG_INVALID_VERIFIER_KEY);
|
||||
}
|
||||
|
||||
if (typeof key.asymmetricKeyType !== 'string') {
|
||||
throw typeError(MSG_INVALID_VERIFIER_KEY);
|
||||
}
|
||||
|
||||
if (typeof key.export !== 'function') {
|
||||
throw typeError(MSG_INVALID_VERIFIER_KEY);
|
||||
}
|
||||
};
|
||||
|
||||
function checkIsPrivateKey(key) {
|
||||
if (Buffer.isBuffer(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof key === 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof key === 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
throw typeError(MSG_INVALID_SIGNER_KEY);
|
||||
};
|
||||
|
||||
function checkIsSecretKey(key) {
|
||||
if (Buffer.isBuffer(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof key === 'string') {
|
||||
return key;
|
||||
}
|
||||
|
||||
if (!supportsKeyObjects) {
|
||||
throw typeError(MSG_INVALID_SECRET);
|
||||
}
|
||||
|
||||
if (typeof key !== 'object') {
|
||||
throw typeError(MSG_INVALID_SECRET);
|
||||
}
|
||||
|
||||
if (key.type !== 'secret') {
|
||||
throw typeError(MSG_INVALID_SECRET);
|
||||
}
|
||||
|
||||
if (typeof key.export !== 'function') {
|
||||
throw typeError(MSG_INVALID_SECRET);
|
||||
}
|
||||
}
|
||||
|
||||
function fromBase64(base64) {
|
||||
return base64
|
||||
.replace(/=/g, '')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
}
|
||||
|
||||
function toBase64(base64url) {
|
||||
base64url = base64url.toString();
|
||||
|
||||
var padding = 4 - base64url.length % 4;
|
||||
if (padding !== 4) {
|
||||
for (var i = 0; i < padding; ++i) {
|
||||
base64url += '=';
|
||||
}
|
||||
}
|
||||
|
||||
return base64url
|
||||
.replace(/\-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
}
|
||||
|
||||
function typeError(template) {
|
||||
var args = [].slice.call(arguments, 1);
|
||||
var errMsg = util.format.bind(util, template).apply(null, args);
|
||||
return new TypeError(errMsg);
|
||||
}
|
||||
|
||||
function bufferOrString(obj) {
|
||||
return Buffer.isBuffer(obj) || typeof obj === 'string';
|
||||
}
|
||||
|
||||
function normalizeInput(thing) {
|
||||
if (!bufferOrString(thing))
|
||||
thing = JSON.stringify(thing);
|
||||
return thing;
|
||||
}
|
||||
|
||||
function createHmacSigner(bits) {
|
||||
return function sign(thing, secret) {
|
||||
checkIsSecretKey(secret);
|
||||
thing = normalizeInput(thing);
|
||||
var hmac = crypto.createHmac('sha' + bits, secret);
|
||||
var sig = (hmac.update(thing), hmac.digest('base64'))
|
||||
return fromBase64(sig);
|
||||
}
|
||||
}
|
||||
|
||||
var bufferEqual;
|
||||
var timingSafeEqual = 'timingSafeEqual' in crypto ? function timingSafeEqual(a, b) {
|
||||
if (a.byteLength !== b.byteLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return crypto.timingSafeEqual(a, b)
|
||||
} : function timingSafeEqual(a, b) {
|
||||
if (!bufferEqual) {
|
||||
bufferEqual = require('buffer-equal-constant-time');
|
||||
}
|
||||
|
||||
return bufferEqual(a, b)
|
||||
}
|
||||
|
||||
function createHmacVerifier(bits) {
|
||||
return function verify(thing, signature, secret) {
|
||||
var computedSig = createHmacSigner(bits)(thing, secret);
|
||||
return timingSafeEqual(Buffer.from(signature), Buffer.from(computedSig));
|
||||
}
|
||||
}
|
||||
|
||||
function createKeySigner(bits) {
|
||||
return function sign(thing, privateKey) {
|
||||
checkIsPrivateKey(privateKey);
|
||||
thing = normalizeInput(thing);
|
||||
// Even though we are specifying "RSA" here, this works with ECDSA
|
||||
// keys as well.
|
||||
var signer = crypto.createSign('RSA-SHA' + bits);
|
||||
var sig = (signer.update(thing), signer.sign(privateKey, 'base64'));
|
||||
return fromBase64(sig);
|
||||
}
|
||||
}
|
||||
|
||||
function createKeyVerifier(bits) {
|
||||
return function verify(thing, signature, publicKey) {
|
||||
checkIsPublicKey(publicKey);
|
||||
thing = normalizeInput(thing);
|
||||
signature = toBase64(signature);
|
||||
var verifier = crypto.createVerify('RSA-SHA' + bits);
|
||||
verifier.update(thing);
|
||||
return verifier.verify(publicKey, signature, 'base64');
|
||||
}
|
||||
}
|
||||
|
||||
function createPSSKeySigner(bits) {
|
||||
return function sign(thing, privateKey) {
|
||||
checkIsPrivateKey(privateKey);
|
||||
thing = normalizeInput(thing);
|
||||
var signer = crypto.createSign('RSA-SHA' + bits);
|
||||
var sig = (signer.update(thing), signer.sign({
|
||||
key: privateKey,
|
||||
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||
saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST
|
||||
}, 'base64'));
|
||||
return fromBase64(sig);
|
||||
}
|
||||
}
|
||||
|
||||
function createPSSKeyVerifier(bits) {
|
||||
return function verify(thing, signature, publicKey) {
|
||||
checkIsPublicKey(publicKey);
|
||||
thing = normalizeInput(thing);
|
||||
signature = toBase64(signature);
|
||||
var verifier = crypto.createVerify('RSA-SHA' + bits);
|
||||
verifier.update(thing);
|
||||
return verifier.verify({
|
||||
key: publicKey,
|
||||
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||
saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST
|
||||
}, signature, 'base64');
|
||||
}
|
||||
}
|
||||
|
||||
function createECDSASigner(bits) {
|
||||
var inner = createKeySigner(bits);
|
||||
return function sign() {
|
||||
var signature = inner.apply(null, arguments);
|
||||
signature = formatEcdsa.derToJose(signature, 'ES' + bits);
|
||||
return signature;
|
||||
};
|
||||
}
|
||||
|
||||
function createECDSAVerifer(bits) {
|
||||
var inner = createKeyVerifier(bits);
|
||||
return function verify(thing, signature, publicKey) {
|
||||
signature = formatEcdsa.joseToDer(signature, 'ES' + bits).toString('base64');
|
||||
var result = inner(thing, signature, publicKey);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
function createNoneSigner() {
|
||||
return function sign() {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function createNoneVerifier() {
|
||||
return function verify(thing, signature) {
|
||||
return signature === '';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function jwa(algorithm) {
|
||||
var signerFactories = {
|
||||
hs: createHmacSigner,
|
||||
rs: createKeySigner,
|
||||
ps: createPSSKeySigner,
|
||||
es: createECDSASigner,
|
||||
none: createNoneSigner,
|
||||
}
|
||||
var verifierFactories = {
|
||||
hs: createHmacVerifier,
|
||||
rs: createKeyVerifier,
|
||||
ps: createPSSKeyVerifier,
|
||||
es: createECDSAVerifer,
|
||||
none: createNoneVerifier,
|
||||
}
|
||||
var match = algorithm.match(/^(RS|PS|ES|HS)(256|384|512)$|^(none)$/);
|
||||
if (!match)
|
||||
throw typeError(MSG_INVALID_ALGORITHM, algorithm);
|
||||
var algo = (match[1] || match[3]).toLowerCase();
|
||||
var bits = match[2];
|
||||
|
||||
return {
|
||||
sign: signerFactories[algo](bits),
|
||||
verify: verifierFactories[algo](bits),
|
||||
}
|
||||
};
|
||||
6
node_modules/web-push/node_modules/jwa/opslevel.yml
generated
vendored
Normal file
6
node_modules/web-push/node_modules/jwa/opslevel.yml
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
version: 1
|
||||
repository:
|
||||
owner: iam_protocols
|
||||
tier:
|
||||
tags:
|
||||
37
node_modules/web-push/node_modules/jwa/package.json
generated
vendored
Normal file
37
node_modules/web-push/node_modules/jwa/package.json
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "jwa",
|
||||
"version": "2.0.1",
|
||||
"description": "JWA implementation (supports all JWS algorithms)",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"base64url": "^2.0.0",
|
||||
"jwk-to-pem": "^2.0.1",
|
||||
"semver": "4.3.6",
|
||||
"tap": "6.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "make test"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/brianloveswords/node-jwa.git"
|
||||
},
|
||||
"keywords": [
|
||||
"jwa",
|
||||
"jws",
|
||||
"jwt",
|
||||
"rsa",
|
||||
"ecdsa",
|
||||
"hmac"
|
||||
],
|
||||
"author": "Brian J. Brennan <brianloveswords@gmail.com>",
|
||||
"license": "MIT"
|
||||
}
|
||||
34
node_modules/web-push/node_modules/jws/CHANGELOG.md
generated
vendored
Normal file
34
node_modules/web-push/node_modules/jws/CHANGELOG.md
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# Change Log
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [3.0.0]
|
||||
### Changed
|
||||
- **BREAKING**: `jwt.verify` now requires an `algorithm` parameter, and
|
||||
`jws.createVerify` requires an `algorithm` option. The `"alg"` field
|
||||
signature headers is ignored. This mitigates a critical security flaw
|
||||
in the library which would allow an attacker to generate signatures with
|
||||
arbitrary contents that would be accepted by `jwt.verify`. See
|
||||
https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/
|
||||
for details.
|
||||
|
||||
## [2.0.0] - 2015-01-30
|
||||
### Changed
|
||||
- **BREAKING**: Default payload encoding changed from `binary` to
|
||||
`utf8`. `utf8` is a is a more sensible default than `binary` because
|
||||
many payloads, as far as I can tell, will contain user-facing
|
||||
strings that could be in any language. (<code>[6b6de48]</code>)
|
||||
|
||||
- Code reorganization, thanks [@fearphage]! (<code>[7880050]</code>)
|
||||
|
||||
### Added
|
||||
- Option in all relevant methods for `encoding`. For those few users
|
||||
that might be depending on a `binary` encoding of the messages, this
|
||||
is for them. (<code>[6b6de48]</code>)
|
||||
|
||||
[unreleased]: https://github.com/brianloveswords/node-jws/compare/v2.0.0...HEAD
|
||||
[2.0.0]: https://github.com/brianloveswords/node-jws/compare/v1.0.1...v2.0.0
|
||||
|
||||
[7880050]: https://github.com/brianloveswords/node-jws/commit/7880050
|
||||
[6b6de48]: https://github.com/brianloveswords/node-jws/commit/6b6de48
|
||||
|
||||
[@fearphage]: https://github.com/fearphage
|
||||
17
node_modules/web-push/node_modules/jws/LICENSE
generated
vendored
Normal file
17
node_modules/web-push/node_modules/jws/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
Copyright (c) 2013 Brian J. Brennan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
22
node_modules/web-push/node_modules/jws/index.js
generated
vendored
Normal file
22
node_modules/web-push/node_modules/jws/index.js
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
/*global exports*/
|
||||
var SignStream = require('./lib/sign-stream');
|
||||
var VerifyStream = require('./lib/verify-stream');
|
||||
|
||||
var ALGORITHMS = [
|
||||
'HS256', 'HS384', 'HS512',
|
||||
'RS256', 'RS384', 'RS512',
|
||||
'PS256', 'PS384', 'PS512',
|
||||
'ES256', 'ES384', 'ES512'
|
||||
];
|
||||
|
||||
exports.ALGORITHMS = ALGORITHMS;
|
||||
exports.sign = SignStream.sign;
|
||||
exports.verify = VerifyStream.verify;
|
||||
exports.decode = VerifyStream.decode;
|
||||
exports.isValid = VerifyStream.isValid;
|
||||
exports.createSign = function createSign(opts) {
|
||||
return new SignStream(opts);
|
||||
};
|
||||
exports.createVerify = function createVerify(opts) {
|
||||
return new VerifyStream(opts);
|
||||
};
|
||||
55
node_modules/web-push/node_modules/jws/lib/data-stream.js
generated
vendored
Normal file
55
node_modules/web-push/node_modules/jws/lib/data-stream.js
generated
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
/*global module, process*/
|
||||
var Buffer = require('safe-buffer').Buffer;
|
||||
var Stream = require('stream');
|
||||
var util = require('util');
|
||||
|
||||
function DataStream(data) {
|
||||
this.buffer = null;
|
||||
this.writable = true;
|
||||
this.readable = true;
|
||||
|
||||
// No input
|
||||
if (!data) {
|
||||
this.buffer = Buffer.alloc(0);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Stream
|
||||
if (typeof data.pipe === 'function') {
|
||||
this.buffer = Buffer.alloc(0);
|
||||
data.pipe(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Buffer or String
|
||||
// or Object (assumedly a passworded key)
|
||||
if (data.length || typeof data === 'object') {
|
||||
this.buffer = data;
|
||||
this.writable = false;
|
||||
process.nextTick(function () {
|
||||
this.emit('end', data);
|
||||
this.readable = false;
|
||||
this.emit('close');
|
||||
}.bind(this));
|
||||
return this;
|
||||
}
|
||||
|
||||
throw new TypeError('Unexpected data type ('+ typeof data + ')');
|
||||
}
|
||||
util.inherits(DataStream, Stream);
|
||||
|
||||
DataStream.prototype.write = function write(data) {
|
||||
this.buffer = Buffer.concat([this.buffer, Buffer.from(data)]);
|
||||
this.emit('data', data);
|
||||
};
|
||||
|
||||
DataStream.prototype.end = function end(data) {
|
||||
if (data)
|
||||
this.write(data);
|
||||
this.emit('end', data);
|
||||
this.emit('close');
|
||||
this.writable = false;
|
||||
this.readable = false;
|
||||
};
|
||||
|
||||
module.exports = DataStream;
|
||||
78
node_modules/web-push/node_modules/jws/lib/sign-stream.js
generated
vendored
Normal file
78
node_modules/web-push/node_modules/jws/lib/sign-stream.js
generated
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
/*global module*/
|
||||
var Buffer = require('safe-buffer').Buffer;
|
||||
var DataStream = require('./data-stream');
|
||||
var jwa = require('jwa');
|
||||
var Stream = require('stream');
|
||||
var toString = require('./tostring');
|
||||
var util = require('util');
|
||||
|
||||
function base64url(string, encoding) {
|
||||
return Buffer
|
||||
.from(string, encoding)
|
||||
.toString('base64')
|
||||
.replace(/=/g, '')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
}
|
||||
|
||||
function jwsSecuredInput(header, payload, encoding) {
|
||||
encoding = encoding || 'utf8';
|
||||
var encodedHeader = base64url(toString(header), 'binary');
|
||||
var encodedPayload = base64url(toString(payload), encoding);
|
||||
return util.format('%s.%s', encodedHeader, encodedPayload);
|
||||
}
|
||||
|
||||
function jwsSign(opts) {
|
||||
var header = opts.header;
|
||||
var payload = opts.payload;
|
||||
var secretOrKey = opts.secret || opts.privateKey;
|
||||
var encoding = opts.encoding;
|
||||
var algo = jwa(header.alg);
|
||||
var securedInput = jwsSecuredInput(header, payload, encoding);
|
||||
var signature = algo.sign(securedInput, secretOrKey);
|
||||
return util.format('%s.%s', securedInput, signature);
|
||||
}
|
||||
|
||||
function SignStream(opts) {
|
||||
var secret = opts.secret||opts.privateKey||opts.key;
|
||||
var secretStream = new DataStream(secret);
|
||||
this.readable = true;
|
||||
this.header = opts.header;
|
||||
this.encoding = opts.encoding;
|
||||
this.secret = this.privateKey = this.key = secretStream;
|
||||
this.payload = new DataStream(opts.payload);
|
||||
this.secret.once('close', function () {
|
||||
if (!this.payload.writable && this.readable)
|
||||
this.sign();
|
||||
}.bind(this));
|
||||
|
||||
this.payload.once('close', function () {
|
||||
if (!this.secret.writable && this.readable)
|
||||
this.sign();
|
||||
}.bind(this));
|
||||
}
|
||||
util.inherits(SignStream, Stream);
|
||||
|
||||
SignStream.prototype.sign = function sign() {
|
||||
try {
|
||||
var signature = jwsSign({
|
||||
header: this.header,
|
||||
payload: this.payload.buffer,
|
||||
secret: this.secret.buffer,
|
||||
encoding: this.encoding
|
||||
});
|
||||
this.emit('done', signature);
|
||||
this.emit('data', signature);
|
||||
this.emit('end');
|
||||
this.readable = false;
|
||||
return signature;
|
||||
} catch (e) {
|
||||
this.readable = false;
|
||||
this.emit('error', e);
|
||||
this.emit('close');
|
||||
}
|
||||
};
|
||||
|
||||
SignStream.sign = jwsSign;
|
||||
|
||||
module.exports = SignStream;
|
||||
10
node_modules/web-push/node_modules/jws/lib/tostring.js
generated
vendored
Normal file
10
node_modules/web-push/node_modules/jws/lib/tostring.js
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/*global module*/
|
||||
var Buffer = require('buffer').Buffer;
|
||||
|
||||
module.exports = function toString(obj) {
|
||||
if (typeof obj === 'string')
|
||||
return obj;
|
||||
if (typeof obj === 'number' || Buffer.isBuffer(obj))
|
||||
return obj.toString();
|
||||
return JSON.stringify(obj);
|
||||
};
|
||||
120
node_modules/web-push/node_modules/jws/lib/verify-stream.js
generated
vendored
Normal file
120
node_modules/web-push/node_modules/jws/lib/verify-stream.js
generated
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
/*global module*/
|
||||
var Buffer = require('safe-buffer').Buffer;
|
||||
var DataStream = require('./data-stream');
|
||||
var jwa = require('jwa');
|
||||
var Stream = require('stream');
|
||||
var toString = require('./tostring');
|
||||
var util = require('util');
|
||||
var JWS_REGEX = /^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/;
|
||||
|
||||
function isObject(thing) {
|
||||
return Object.prototype.toString.call(thing) === '[object Object]';
|
||||
}
|
||||
|
||||
function safeJsonParse(thing) {
|
||||
if (isObject(thing))
|
||||
return thing;
|
||||
try { return JSON.parse(thing); }
|
||||
catch (e) { return undefined; }
|
||||
}
|
||||
|
||||
function headerFromJWS(jwsSig) {
|
||||
var encodedHeader = jwsSig.split('.', 1)[0];
|
||||
return safeJsonParse(Buffer.from(encodedHeader, 'base64').toString('binary'));
|
||||
}
|
||||
|
||||
function securedInputFromJWS(jwsSig) {
|
||||
return jwsSig.split('.', 2).join('.');
|
||||
}
|
||||
|
||||
function signatureFromJWS(jwsSig) {
|
||||
return jwsSig.split('.')[2];
|
||||
}
|
||||
|
||||
function payloadFromJWS(jwsSig, encoding) {
|
||||
encoding = encoding || 'utf8';
|
||||
var payload = jwsSig.split('.')[1];
|
||||
return Buffer.from(payload, 'base64').toString(encoding);
|
||||
}
|
||||
|
||||
function isValidJws(string) {
|
||||
return JWS_REGEX.test(string) && !!headerFromJWS(string);
|
||||
}
|
||||
|
||||
function jwsVerify(jwsSig, algorithm, secretOrKey) {
|
||||
if (!algorithm) {
|
||||
var err = new Error("Missing algorithm parameter for jws.verify");
|
||||
err.code = "MISSING_ALGORITHM";
|
||||
throw err;
|
||||
}
|
||||
jwsSig = toString(jwsSig);
|
||||
var signature = signatureFromJWS(jwsSig);
|
||||
var securedInput = securedInputFromJWS(jwsSig);
|
||||
var algo = jwa(algorithm);
|
||||
return algo.verify(securedInput, signature, secretOrKey);
|
||||
}
|
||||
|
||||
function jwsDecode(jwsSig, opts) {
|
||||
opts = opts || {};
|
||||
jwsSig = toString(jwsSig);
|
||||
|
||||
if (!isValidJws(jwsSig))
|
||||
return null;
|
||||
|
||||
var header = headerFromJWS(jwsSig);
|
||||
|
||||
if (!header)
|
||||
return null;
|
||||
|
||||
var payload = payloadFromJWS(jwsSig);
|
||||
if (header.typ === 'JWT' || opts.json)
|
||||
payload = JSON.parse(payload, opts.encoding);
|
||||
|
||||
return {
|
||||
header: header,
|
||||
payload: payload,
|
||||
signature: signatureFromJWS(jwsSig)
|
||||
};
|
||||
}
|
||||
|
||||
function VerifyStream(opts) {
|
||||
opts = opts || {};
|
||||
var secretOrKey = opts.secret||opts.publicKey||opts.key;
|
||||
var secretStream = new DataStream(secretOrKey);
|
||||
this.readable = true;
|
||||
this.algorithm = opts.algorithm;
|
||||
this.encoding = opts.encoding;
|
||||
this.secret = this.publicKey = this.key = secretStream;
|
||||
this.signature = new DataStream(opts.signature);
|
||||
this.secret.once('close', function () {
|
||||
if (!this.signature.writable && this.readable)
|
||||
this.verify();
|
||||
}.bind(this));
|
||||
|
||||
this.signature.once('close', function () {
|
||||
if (!this.secret.writable && this.readable)
|
||||
this.verify();
|
||||
}.bind(this));
|
||||
}
|
||||
util.inherits(VerifyStream, Stream);
|
||||
VerifyStream.prototype.verify = function verify() {
|
||||
try {
|
||||
var valid = jwsVerify(this.signature.buffer, this.algorithm, this.key.buffer);
|
||||
var obj = jwsDecode(this.signature.buffer, this.encoding);
|
||||
this.emit('done', valid, obj);
|
||||
this.emit('data', valid);
|
||||
this.emit('end');
|
||||
this.readable = false;
|
||||
return valid;
|
||||
} catch (e) {
|
||||
this.readable = false;
|
||||
this.emit('error', e);
|
||||
this.emit('close');
|
||||
}
|
||||
};
|
||||
|
||||
VerifyStream.decode = jwsDecode;
|
||||
VerifyStream.isValid = isValidJws;
|
||||
VerifyStream.verify = jwsVerify;
|
||||
|
||||
module.exports = VerifyStream;
|
||||
34
node_modules/web-push/node_modules/jws/package.json
generated
vendored
Normal file
34
node_modules/web-push/node_modules/jws/package.json
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "jws",
|
||||
"version": "4.0.0",
|
||||
"description": "Implementation of JSON Web Signatures",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "make test"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/brianloveswords/node-jws.git"
|
||||
},
|
||||
"keywords": [
|
||||
"jws",
|
||||
"json",
|
||||
"web",
|
||||
"signatures"
|
||||
],
|
||||
"author": "Brian J Brennan",
|
||||
"license": "MIT",
|
||||
"readmeFilename": "readme.md",
|
||||
"gitHead": "c0f6b27bcea5a2ad2e304d91c2e842e4076a6b03",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.0",
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"semver": "^5.1.0",
|
||||
"tape": "~2.14.0"
|
||||
}
|
||||
}
|
||||
255
node_modules/web-push/node_modules/jws/readme.md
generated
vendored
Normal file
255
node_modules/web-push/node_modules/jws/readme.md
generated
vendored
Normal file
@@ -0,0 +1,255 @@
|
||||
# node-jws [](http://travis-ci.org/brianloveswords/node-jws)
|
||||
|
||||
An implementation of [JSON Web Signatures](http://self-issued.info/docs/draft-ietf-jose-json-web-signature.html).
|
||||
|
||||
This was developed against `draft-ietf-jose-json-web-signature-08` and
|
||||
implements the entire spec **except** X.509 Certificate Chain
|
||||
signing/verifying (patches welcome).
|
||||
|
||||
There are both synchronous (`jws.sign`, `jws.verify`) and streaming
|
||||
(`jws.createSign`, `jws.createVerify`) APIs.
|
||||
|
||||
# Install
|
||||
|
||||
```bash
|
||||
$ npm install jws
|
||||
```
|
||||
|
||||
# Usage
|
||||
|
||||
## jws.ALGORITHMS
|
||||
|
||||
Array of supported algorithms. The following algorithms are currently supported.
|
||||
|
||||
alg Parameter Value | Digital Signature or MAC Algorithm
|
||||
----------------|----------------------------
|
||||
HS256 | HMAC using SHA-256 hash algorithm
|
||||
HS384 | HMAC using SHA-384 hash algorithm
|
||||
HS512 | HMAC using SHA-512 hash algorithm
|
||||
RS256 | RSASSA using SHA-256 hash algorithm
|
||||
RS384 | RSASSA using SHA-384 hash algorithm
|
||||
RS512 | RSASSA using SHA-512 hash algorithm
|
||||
PS256 | RSASSA-PSS using SHA-256 hash algorithm
|
||||
PS384 | RSASSA-PSS using SHA-384 hash algorithm
|
||||
PS512 | RSASSA-PSS using SHA-512 hash algorithm
|
||||
ES256 | ECDSA using P-256 curve and SHA-256 hash algorithm
|
||||
ES384 | ECDSA using P-384 curve and SHA-384 hash algorithm
|
||||
ES512 | ECDSA using P-521 curve and SHA-512 hash algorithm
|
||||
none | No digital signature or MAC value included
|
||||
|
||||
## jws.sign(options)
|
||||
|
||||
(Synchronous) Return a JSON Web Signature for a header and a payload.
|
||||
|
||||
Options:
|
||||
|
||||
* `header`
|
||||
* `payload`
|
||||
* `secret` or `privateKey`
|
||||
* `encoding` (Optional, defaults to 'utf8')
|
||||
|
||||
`header` must be an object with an `alg` property. `header.alg` must be
|
||||
one a value found in `jws.ALGORITHMS`. See above for a table of
|
||||
supported algorithms.
|
||||
|
||||
If `payload` is not a buffer or a string, it will be coerced into a string
|
||||
using `JSON.stringify`.
|
||||
|
||||
Example
|
||||
|
||||
```js
|
||||
const signature = jws.sign({
|
||||
header: { alg: 'HS256' },
|
||||
payload: 'h. jon benjamin',
|
||||
secret: 'has a van',
|
||||
});
|
||||
```
|
||||
|
||||
## jws.verify(signature, algorithm, secretOrKey)
|
||||
|
||||
(Synchronous) Returns `true` or `false` for whether a signature matches a
|
||||
secret or key.
|
||||
|
||||
`signature` is a JWS Signature. `header.alg` must be a value found in `jws.ALGORITHMS`.
|
||||
See above for a table of supported algorithms. `secretOrKey` is a string or
|
||||
buffer containing either the secret for HMAC algorithms, or the PEM
|
||||
encoded public key for RSA and ECDSA.
|
||||
|
||||
Note that the `"alg"` value from the signature header is ignored.
|
||||
|
||||
|
||||
## jws.decode(signature)
|
||||
|
||||
(Synchronous) Returns the decoded header, decoded payload, and signature
|
||||
parts of the JWS Signature.
|
||||
|
||||
Returns an object with three properties, e.g.
|
||||
```js
|
||||
{ header: { alg: 'HS256' },
|
||||
payload: 'h. jon benjamin',
|
||||
signature: 'YOWPewyGHKu4Y_0M_vtlEnNlqmFOclqp4Hy6hVHfFT4'
|
||||
}
|
||||
```
|
||||
|
||||
## jws.createSign(options)
|
||||
|
||||
Returns a new SignStream object.
|
||||
|
||||
Options:
|
||||
|
||||
* `header` (required)
|
||||
* `payload`
|
||||
* `key` || `privateKey` || `secret`
|
||||
* `encoding` (Optional, defaults to 'utf8')
|
||||
|
||||
Other than `header`, all options expect a string or a buffer when the
|
||||
value is known ahead of time, or a stream for convenience.
|
||||
`key`/`privateKey`/`secret` may also be an object when using an encrypted
|
||||
private key, see the [crypto documentation][encrypted-key-docs].
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
|
||||
// This...
|
||||
jws.createSign({
|
||||
header: { alg: 'RS256' },
|
||||
privateKey: privateKeyStream,
|
||||
payload: payloadStream,
|
||||
}).on('done', function(signature) {
|
||||
// ...
|
||||
});
|
||||
|
||||
// is equivalent to this:
|
||||
const signer = jws.createSign({
|
||||
header: { alg: 'RS256' },
|
||||
});
|
||||
privateKeyStream.pipe(signer.privateKey);
|
||||
payloadStream.pipe(signer.payload);
|
||||
signer.on('done', function(signature) {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## jws.createVerify(options)
|
||||
|
||||
Returns a new VerifyStream object.
|
||||
|
||||
Options:
|
||||
|
||||
* `signature`
|
||||
* `algorithm`
|
||||
* `key` || `publicKey` || `secret`
|
||||
* `encoding` (Optional, defaults to 'utf8')
|
||||
|
||||
All options expect a string or a buffer when the value is known ahead of
|
||||
time, or a stream for convenience.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
|
||||
// This...
|
||||
jws.createVerify({
|
||||
publicKey: pubKeyStream,
|
||||
signature: sigStream,
|
||||
}).on('done', function(verified, obj) {
|
||||
// ...
|
||||
});
|
||||
|
||||
// is equivilant to this:
|
||||
const verifier = jws.createVerify();
|
||||
pubKeyStream.pipe(verifier.publicKey);
|
||||
sigStream.pipe(verifier.signature);
|
||||
verifier.on('done', function(verified, obj) {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## Class: SignStream
|
||||
|
||||
A `Readable Stream` that emits a single data event (the calculated
|
||||
signature) when done.
|
||||
|
||||
### Event: 'done'
|
||||
`function (signature) { }`
|
||||
|
||||
### signer.payload
|
||||
|
||||
A `Writable Stream` that expects the JWS payload. Do *not* use if you
|
||||
passed a `payload` option to the constructor.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
payloadStream.pipe(signer.payload);
|
||||
```
|
||||
|
||||
### signer.secret<br>signer.key<br>signer.privateKey
|
||||
|
||||
A `Writable Stream`. Expects the JWS secret for HMAC, or the privateKey
|
||||
for ECDSA and RSA. Do *not* use if you passed a `secret` or `key` option
|
||||
to the constructor.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
privateKeyStream.pipe(signer.privateKey);
|
||||
```
|
||||
|
||||
## Class: VerifyStream
|
||||
|
||||
This is a `Readable Stream` that emits a single data event, the result
|
||||
of whether or not that signature was valid.
|
||||
|
||||
### Event: 'done'
|
||||
`function (valid, obj) { }`
|
||||
|
||||
`valid` is a boolean for whether or not the signature is valid.
|
||||
|
||||
### verifier.signature
|
||||
|
||||
A `Writable Stream` that expects a JWS Signature. Do *not* use if you
|
||||
passed a `signature` option to the constructor.
|
||||
|
||||
### verifier.secret<br>verifier.key<br>verifier.publicKey
|
||||
|
||||
A `Writable Stream` that expects a public key or secret. Do *not* use if you
|
||||
passed a `key` or `secret` option to the constructor.
|
||||
|
||||
# TODO
|
||||
|
||||
* It feels like there should be some convenience options/APIs for
|
||||
defining the algorithm rather than having to define a header object
|
||||
with `{ alg: 'ES512' }` or whatever every time.
|
||||
|
||||
* X.509 support, ugh
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
||||
|
||||
```
|
||||
Copyright (c) 2013-2015 Brian J. Brennan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
```
|
||||
|
||||
[encrypted-key-docs]: https://nodejs.org/api/crypto.html#crypto_sign_sign_private_key_output_format
|
||||
55
node_modules/web-push/package.json
generated
vendored
Normal file
55
node_modules/web-push/package.json
generated
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "web-push",
|
||||
"version": "3.6.7",
|
||||
"description": "Web Push library for Node.js",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
"web-push": "src/cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"download-browser": "node --harmony ./test/helpers/download-test-browsers.js",
|
||||
"lint": "node ./node_modules/eslint/bin/eslint --ignore-path .gitignore '.'",
|
||||
"pretest": "npm run lint && npm run download-browser",
|
||||
"test": "nyc --reporter=lcov --reporter=text mocha -- --ui tdd test/test*"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/web-push-libs/web-push.git"
|
||||
},
|
||||
"keywords": [
|
||||
"web push",
|
||||
"push",
|
||||
"notifications",
|
||||
"push notifications"
|
||||
],
|
||||
"author": "Marco Castelluccio",
|
||||
"license": "MPL-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/web-push-libs/web-push/issues"
|
||||
},
|
||||
"homepage": "https://github.com/web-push-libs/web-push#readme",
|
||||
"dependencies": {
|
||||
"asn1.js": "^5.3.0",
|
||||
"http_ece": "1.2.0",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"jws": "^4.0.0",
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chromedriver": "120.0.1",
|
||||
"del": "6.0.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-airbnb": "19.0.4",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"geckodriver": "4.3.0",
|
||||
"nyc": "15.1.0",
|
||||
"mkdirp": "3.0.1",
|
||||
"mocha": "10.2.0",
|
||||
"portfinder": "1.0.32",
|
||||
"selenium-assistant": "5.4.0",
|
||||
"sinon": "17.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
}
|
||||
131
node_modules/web-push/src/cli.js
generated
vendored
Executable file
131
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
node_modules/web-push/src/encryption-helper.js
generated
vendored
Normal file
62
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
node_modules/web-push/src/index.js
generated
vendored
Normal file
21
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
node_modules/web-push/src/urlsafe-base64-helper.js
generated
vendored
Normal file
13
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
node_modules/web-push/src/vapid-helper.js
generated
vendored
Normal file
255
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
node_modules/web-push/src/web-push-constants.js
generated
vendored
Normal file
17
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
node_modules/web-push/src/web-push-error.js
generated
vendored
Normal file
16
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
node_modules/web-push/src/web-push-lib.js
generated
vendored
Normal file
413
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