Added dependencies for API and VAPID implementation
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
*~
|
||||
node_modules
|
||||
3613
package-lock.json
generated
3613
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -7,8 +7,11 @@
|
||||
"node": ">= 18.12 <19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/body-parser": "^1.19.2",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "~29.5",
|
||||
"@types/node": "~18",
|
||||
"@types/sqlite3": "^3.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "~6.2",
|
||||
"@typescript-eslint/parser": "~6.2",
|
||||
"eslint": "~8.46",
|
||||
@@ -36,7 +39,12 @@
|
||||
"author": "Jakub Synowiec <jsynowiec@users.noreply.github.com>",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "~2.6"
|
||||
"body-parser": "^1.20.2",
|
||||
"express": "^4.18.2",
|
||||
"sqlite": "^5.0.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"tslib": "~2.6",
|
||||
"web-push": "^3.6.4"
|
||||
},
|
||||
"volta": {
|
||||
"node": "18.12.1"
|
||||
|
||||
81
src/WebPushSender.ts
Normal file
81
src/WebPushSender.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import fetch from 'node-fetch';
|
||||
import * as crypto from 'crypto';
|
||||
import { ec as EC } from 'elliptic';
|
||||
|
||||
class WebPushSender {
|
||||
private ec = new EC('prime256v1');
|
||||
private vapidPrivateKey: string;
|
||||
|
||||
constructor(vapidPrivateKey: string) {
|
||||
this.vapidPrivateKey = vapidPrivateKey;
|
||||
}
|
||||
|
||||
private generateECDHKeyPair() {
|
||||
return this.ec.genKeyPair();
|
||||
}
|
||||
|
||||
private deriveSharedSecret(senderKeys, receiverPublicKeyBase64: string) {
|
||||
const receiverPublicKey = Buffer.from(receiverPublicKeyBase64, 'base64');
|
||||
return senderKeys.derive(this.ec.keyFromPublic(receiverPublicKey).getPublic());
|
||||
}
|
||||
|
||||
private generateContentEncryptionKeys(sharedSecret, authSecretBase64: string) {
|
||||
const authSecret = Buffer.from(authSecretBase64, 'base64');
|
||||
const info = Buffer.from('Content-Encoding: auth\0', 'utf8');
|
||||
const prk = crypto.createHmac('sha256', authSecret).update(sharedSecret.toArray()).digest();
|
||||
const context = crypto.createHmac('sha256', prk).update(info).digest();
|
||||
const keyInfo = Buffer.from('Content-Encoding: aes128gcm\0', 'utf8');
|
||||
return crypto.createHmac('sha256', context).update(keyInfo).digest().slice(0, 16);
|
||||
}
|
||||
|
||||
private encryptPayload(contentEncryptionKey, payload: string) {
|
||||
const nonce = Buffer.alloc(12);
|
||||
const cipher = crypto.createCipheriv('aes-128-gcm', contentEncryptionKey, nonce);
|
||||
return Buffer.concat([cipher.update(payload, 'utf8'), cipher.final()]);
|
||||
}
|
||||
|
||||
private generateVapidToken(senderKeys, endpoint: string) {
|
||||
const vapidPublicKey = senderKeys.getPublic().encode('hex');
|
||||
const tokenPayload = {
|
||||
aud: endpoint,
|
||||
exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60), // 24 hours
|
||||
sub: 'mailto:your-email@example.com',
|
||||
};
|
||||
const encodedHeader = Buffer.from(JSON.stringify({ alg: 'ES256', typ: 'JWT' })).toString('base64');
|
||||
const encodedPayload = Buffer.from(JSON.stringify(tokenPayload)).toString('base64');
|
||||
const signature = this.ec.sign(encodedHeader + '.' + encodedPayload, this.vapidPrivateKey, 'base64');
|
||||
const encodedSignature = signature.toDER('hex');
|
||||
return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
|
||||
}
|
||||
|
||||
public sendPushNotification(subscription, payload: string) {
|
||||
const senderKeys = this.generateECDHKeyPair();
|
||||
const sharedSecret = this.deriveSharedSecret(senderKeys, subscription.keys.p256dh);
|
||||
const contentEncryptionKey = this.generateContentEncryptionKeys(sharedSecret, subscription.keys.auth);
|
||||
const encryptedPayload = this.encryptPayload(contentEncryptionKey, payload);
|
||||
const vapidToken = this.generateVapidToken(senderKeys, subscription.endpoint);
|
||||
|
||||
const headers = {
|
||||
'Content-Encoding': 'aes128gcm',
|
||||
'Authorization': `vapid t=${vapidToken}`,
|
||||
'TTL': '60',
|
||||
};
|
||||
|
||||
return fetch(subscription.endpoint, { method: 'POST', body: encryptedPayload, headers });
|
||||
}
|
||||
}
|
||||
|
||||
// Usage example
|
||||
const vapidPrivateKey = 'YOUR_PRIVATE_VAPID_KEY';
|
||||
const sender = new WebPushSender(vapidPrivateKey);
|
||||
const subscription = {
|
||||
endpoint: 'https://example.pushservice.com/some-id',
|
||||
keys: {
|
||||
p256dh: 'BIPULBIpUL...',
|
||||
auth: 'eHl6...',
|
||||
},
|
||||
};
|
||||
|
||||
sender.sendPushNotification(subscription, 'Your Push Payload Here')
|
||||
.then(response => console.log('Push message sent successfully:', response))
|
||||
.catch(error => console.error('Failed to send push message:', error));
|
||||
100
src/main.ts
100
src/main.ts
@@ -1,34 +1,72 @@
|
||||
/**
|
||||
* Some predefined delay values (in milliseconds).
|
||||
*/
|
||||
export enum Delays {
|
||||
Short = 500,
|
||||
Medium = 2000,
|
||||
Long = 5000,
|
||||
|
||||
import express from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
import { Request, Response } from 'express';
|
||||
import sqlite3 from 'sqlite3';
|
||||
import { open } from 'sqlite';
|
||||
import { WebPushSender } from './WebPushSender'; // Import from previous example
|
||||
|
||||
class PushService {
|
||||
private app = express();
|
||||
private db: any;
|
||||
private sender: WebPushSender;
|
||||
|
||||
constructor(private port: number, private vapidPrivateKey: string) {
|
||||
this.sender = new WebPushSender(vapidPrivateKey);
|
||||
this.app.use(bodyParser.json());
|
||||
|
||||
open({ filename: './subscriptions.db', driver: sqlite3.Database }).then((db) => {
|
||||
this.db = db;
|
||||
this.db.run('CREATE TABLE IF NOT EXISTS subscriptions (p256dh TEXT PRIMARY KEY, endpoint TEXT, auth TEXT)');
|
||||
this.setupRoutes();
|
||||
});
|
||||
}
|
||||
|
||||
private setupRoutes() {
|
||||
this.app.post('/subscribe', this.handleSubscribe.bind(this));
|
||||
}
|
||||
|
||||
private async handleSubscribe(req: Request, res: Response) {
|
||||
const { endpoint, keys } = req.body;
|
||||
const { p256dh, auth } = keys;
|
||||
|
||||
try {
|
||||
await this.db.run('INSERT INTO subscriptions (p256dh, endpoint, auth) VALUES (?, ?, ?)', [p256dh, endpoint, auth]);
|
||||
res.status(201).send('Subscription created successfully');
|
||||
} catch (err) {
|
||||
res.status(400).send('Failed to create subscription');
|
||||
}
|
||||
}
|
||||
|
||||
public async sendNotification(p256dh: string, payload: string) {
|
||||
const row = await this.db.get('SELECT * FROM subscriptions WHERE p256dh = ?', [p256dh]);
|
||||
if (row) {
|
||||
const subscription = {
|
||||
endpoint: row.endpoint,
|
||||
keys: {
|
||||
p256dh: row.p256dh,
|
||||
auth: row.auth,
|
||||
},
|
||||
};
|
||||
|
||||
return this.sender.sendPushNotification(subscription, payload);
|
||||
} else {
|
||||
throw new Error('Subscription not found');
|
||||
}
|
||||
}
|
||||
|
||||
public start() {
|
||||
this.app.listen(this.port, () => {
|
||||
console.log(`Server running on http://localhost:${this.port}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Promise<string> that resolves after a given time.
|
||||
*
|
||||
* @param {string} name - A name.
|
||||
* @param {number=} [delay=Delays.Medium] - A number of milliseconds to delay resolution of the Promise.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function delayedHello(
|
||||
name: string,
|
||||
delay: number = Delays.Medium,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve: (value?: string) => void) =>
|
||||
setTimeout(() => resolve(`Hello, ${name}`), delay),
|
||||
);
|
||||
}
|
||||
// Usage example
|
||||
const pushService = new PushService(3000, 'YOUR_PRIVATE_VAPID_KEY');
|
||||
pushService.start();
|
||||
|
||||
// Please see the comment in the .eslintrc.json file about the suppressed rule!
|
||||
// Below is an example of how to use ESLint errors suppression. You can read more
|
||||
// at https://eslint.org/docs/latest/user-guide/configuring/rules#disabling-rules
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export async function greeter(name: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
// The name parameter should be of type string. Any is used only to trigger the rule.
|
||||
return await delayedHello(name, Delays.Long);
|
||||
}
|
||||
// Example of sending a notification
|
||||
pushService.sendNotification('p256dh_value_here', 'Your Push Payload Here')
|
||||
.then(() => console.log('Notification sent successfully'))
|
||||
.catch((error) => console.error('Failed to send notification:', error));
|
||||
|
||||
Reference in New Issue
Block a user