|
@ -1,10 +1,9 @@ |
|
|
import SubscriptionService from './subscriptionService.js'; |
|
|
|
|
|
import VapidService from './vapidService.js'; |
|
|
import VapidService from './vapidService.js'; |
|
|
import { VapidKeys } from './VapidKeys.js'; |
|
|
import { VapidKeys } from './VapidKeys.js'; |
|
|
|
|
|
import { IncomingMessage } from 'http'; |
|
|
import * as https from 'https'; |
|
|
import * as https from 'https'; |
|
|
import * as http_ece from 'http_ece'; |
|
|
import * as http_ece from 'http_ece'; |
|
|
import crypto from 'crypto'; |
|
|
import crypto from 'crypto'; |
|
|
import { Subscription } from "./Subscription.js" |
|
|
|
|
|
|
|
|
|
|
|
export interface Message { |
|
|
export interface Message { |
|
|
title: string; |
|
|
title: string; |
|
@ -12,83 +11,105 @@ export interface Message { |
|
|
[key: string]: any; |
|
|
[key: string]: any; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export class NotificationService { |
|
|
|
|
|
|
|
|
|
|
|
private subscriptionService: SubscriptionService = SubscriptionService.getInstance(); |
|
|
export interface BrowserSubscription { |
|
|
|
|
|
endpoint: string; |
|
|
|
|
|
keys: { |
|
|
|
|
|
p256dh: string; |
|
|
|
|
|
auth: string; |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
interface MyIncomingMessage extends IncomingMessage { |
|
|
|
|
|
errno?: number; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
export class NotificationService { |
|
|
|
|
|
private static instance: NotificationService; |
|
|
private vapidService: VapidService = VapidService.getInstance(); |
|
|
private vapidService: VapidService = VapidService.getInstance(); |
|
|
|
|
|
|
|
|
constructor() { |
|
|
constructor() { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private generateSalt(length = 16): Buffer { |
|
|
public static getInstance(): NotificationService { |
|
|
return crypto.randomBytes(length); |
|
|
if (!NotificationService.instance) { |
|
|
|
|
|
NotificationService.instance = new NotificationService(); |
|
|
|
|
|
} |
|
|
|
|
|
return NotificationService.instance; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async broadcast(message: Message): Promise<void> { |
|
|
|
|
|
const subscriptions = await this.subscriptionService.fetchSubscriptions(); |
|
|
|
|
|
|
|
|
|
|
|
for (const subscription of subscriptions) { |
|
|
|
|
|
await this.pushToEndpoint(subscription, message); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async sendNotification(subscription: Subscription, message: Message) { |
|
|
async sendNotification(subscription: BrowserSubscription, message: Message) { |
|
|
await this.pushToEndpoint(subscription, message); |
|
|
await this.pushToEndpoint(subscription, message); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private async pushToEndpoint(subscription: Subscription, message: Message): Promise<void> { |
|
|
|
|
|
const payload = JSON.stringify(message); |
|
|
|
|
|
|
|
|
|
|
|
const encrypted = this.encrypt(subscription.keys_p256dh, subscription.keys_auth, payload); |
|
|
private async pushToEndpoint(subscription: BrowserSubscription, message: Message): Promise<void> { |
|
|
|
|
|
const payloadString = JSON.stringify(message); |
|
|
|
|
|
const payloadBuffer = Buffer.from(payloadString, 'utf-8'); |
|
|
|
|
|
const vapidKeys: VapidKeys[] = await this.vapidService.getVapidKeys(); |
|
|
|
|
|
const vapidkey: VapidKeys = vapidKeys[0]; |
|
|
|
|
|
|
|
|
|
|
|
const encrypted = await this.encrypt(subscription.keys.p256dh, subscription.keys.auth, payloadBuffer); |
|
|
const endpoint = subscription.endpoint; |
|
|
const endpoint = subscription.endpoint; |
|
|
|
|
|
|
|
|
const vapidHeaders = await this.vapidService.createVapidAuthHeader(endpoint, 12 * 60 * 60, 'mailto:example@example.com'); |
|
|
const vapidHeaders = await this.vapidService.createVapidAuthHeader(endpoint, 12 * 60 * 60, 'mailto:example@example.com', vapidkey); |
|
|
|
|
|
|
|
|
const parsedUrl = new URL(subscription.endpoint); |
|
|
const parsedUrl = new URL(subscription.endpoint); |
|
|
const options: https.RequestOptions = { |
|
|
const options: https.RequestOptions = { |
|
|
method: 'POST', |
|
|
method: 'POST', |
|
|
hostname: parsedUrl.hostname, |
|
|
hostname: parsedUrl.hostname, |
|
|
path: parsedUrl.pathname, |
|
|
path: parsedUrl.pathname, |
|
|
port: parsedUrl.port, |
|
|
port: 443, |
|
|
headers: { |
|
|
headers: { ...vapidHeaders, 'TTL': '60', 'Content-Encoding': 'aes128gcm', 'Content-Type': 'application/octet-stream', 'Content-Length': encrypted.length }, |
|
|
...vapidHeaders, |
|
|
|
|
|
'TTL': '60', |
|
|
|
|
|
'Content-Encoding': 'aes128gcm', |
|
|
|
|
|
'Content-Type': 'application/octet-stream', |
|
|
|
|
|
'Content-Length': encrypted.length |
|
|
|
|
|
}, |
|
|
|
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
return new Promise<void>((resolve, reject) => { |
|
|
return new Promise<void>((resolve, reject) => { |
|
|
const req = https.request(options, (res) => { |
|
|
const req = https.request(options, (res: MyIncomingMessage) => { |
|
|
if (res.statusCode! >= 200 && res.statusCode! < 300) { |
|
|
let body = ''; |
|
|
resolve(); |
|
|
|
|
|
} else { |
|
|
console.log('Headers:', res.headers); |
|
|
reject(new Error(`Failed to send push notification. Status code: ${res.statusCode}`)); |
|
|
|
|
|
} |
|
|
res.on('data', chunk => { body += chunk; }); |
|
|
}); |
|
|
|
|
|
|
|
|
res.on('end', () => { |
|
|
|
|
|
console.log('Body:', body); |
|
|
|
|
|
console.log(res.statusCode); |
|
|
|
|
|
|
|
|
req.on('error', (error) => { |
|
|
if (res.statusCode! >= 200 && res.statusCode! < 300) { |
|
|
reject(new Error(`Failed to send push notification. Error: ${error.message}`)); |
|
|
resolve(); |
|
|
|
|
|
} else { |
|
|
|
|
|
reject(new Error(`Failed to send push notification. Status code: ${res.statusCode}, Body: ${body}`)); |
|
|
|
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
req.write(encrypted); |
|
|
req.on('error', (error) => { |
|
|
req.end(); |
|
|
reject(new Error(`Failed to send push notification. Error: ${error.message}`)); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
req.write(encrypted); |
|
|
|
|
|
req.end(); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private encrypt(publicKey: string, auth: string, payload: string): Buffer { |
|
|
|
|
|
http_ece.keys = { |
|
|
|
|
|
'p256dh': publicKey, |
|
|
|
|
|
'auth': auth |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const vapidKeys: VapidKeys = this.vapidService.getVapidKeys()[0]; |
|
|
private async encrypt( p256dh: string, auth: string, payload: Buffer): Promise<Buffer> { |
|
|
|
|
|
try { |
|
|
|
|
|
const ecdh = crypto.createECDH('prime256v1'); |
|
|
|
|
|
ecdh.generateKeys(); |
|
|
|
|
|
const publicKeyBuffer: Buffer = Buffer.from(p256dh, 'base64'); |
|
|
|
|
|
|
|
|
return http_ece.encrypt(payload, { |
|
|
return http_ece.encrypt(payload, { |
|
|
'salt': this.generateSalt(), |
|
|
'version': 'aes128gcm', |
|
|
'dh': vapidKeys.publicKey, |
|
|
'privateKey': ecdh, |
|
|
'keyid': 'p256dh', |
|
|
'dh': publicKeyBuffer, |
|
|
'contentEncoding': 'aes128gcm' |
|
|
'authSecret': Buffer.from(auth) |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
console.error('Error encrypting payload:', error); |
|
|
|
|
|
throw error; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|