+98
server/index.js
+98
server/index.js
···
1
+
#!/usr/bin/env node
2
+
3
+
const webpush = require('web-push');
4
+
const fs = require('node:fs');
5
+
const http = require('http');
6
+
7
+
const getOrCreateKeys = filename => {
8
+
let keys;
9
+
try {
10
+
const data = fs.readFileSync(filename);
11
+
keys = JSON.parse(data);
12
+
} catch (err) {
13
+
if (err.code != 'ENOENT') throw err;
14
+
keys = webpush.generateVAPIDKeys();
15
+
const data = JSON.stringify(keys);
16
+
fs.writeFileSync(filename, data);
17
+
}
18
+
console.log(`Keys ready with pubkey: ${keys.publicKey}`);
19
+
return keys;
20
+
}
21
+
22
+
const getRequesBody = async req => new Promise((resolve, reject) => {
23
+
let body = '';
24
+
req.on('data', chunk => body += chunk);
25
+
req.on('end', () => resolve(body));
26
+
req.on('error', err => reject(err));
27
+
});
28
+
29
+
const handleFile = (fname, ftype) => async (req, res, replace = {}) => {
30
+
let content
31
+
try {
32
+
content = await fs.promises.readFile(__dirname + '/web-content/' + fname);
33
+
content = content.toString();
34
+
} catch (err) {
35
+
console.error(err);
36
+
res.writeHead(500);
37
+
res.end('Internal server error');
38
+
return;
39
+
}
40
+
res.setHeader('Content-Type', ftype);
41
+
res.writeHead(200);
42
+
for (let k in replace) {
43
+
content = content.replace(k, JSON.stringify(replace[k]));
44
+
}
45
+
res.end(content);
46
+
}
47
+
const handleIndex = handleFile('index.html', 'text/html');
48
+
const handleServiceWorker = handleFile('service-worker.js', 'application/javascript');
49
+
50
+
const handleSubscribe = async (req, res) => {
51
+
const body = await getRequesBody(req);
52
+
console.log('got body', body);
53
+
doStuff(JSON.parse(body));
54
+
res.setHeader('Content-Type', 'application/json');
55
+
res.writeHead(201);
56
+
res.end('{"oh": "hi"}');
57
+
}
58
+
59
+
const doStuff = sub => {
60
+
let n = 0;
61
+
setInterval(() => {
62
+
webpush.sendNotification(sub, `oh hi: ${n}`);
63
+
n += 1;
64
+
}, 2000);
65
+
}
66
+
67
+
const requestListener = pubkey => (req, res) => {
68
+
if (req.method === 'GET' && req.url === '/')
69
+
return handleIndex(req, res, { PUBKEY: pubkey });
70
+
71
+
if (req.method === 'GET' && req.url === '/service-worker.js')
72
+
return handleServiceWorker(req, res, { PUBKEY: pubkey });
73
+
74
+
if (req.method === 'POST' && req.url === '/subscribe')
75
+
return handleSubscribe(req, res);
76
+
77
+
res.writeHead(200);
78
+
res.end('sup');
79
+
}
80
+
81
+
const main = env => {
82
+
if (!env.KEY_FILE) throw new Error('KEY_FILE is required to run');
83
+
const keys = getOrCreateKeys(env.KEY_FILE);
84
+
webpush.setVapidDetails(
85
+
'mailto:phil@bad-example.com',
86
+
keys.publicKey,
87
+
keys.privateKey,
88
+
);
89
+
90
+
const host = env.HOST || 'localhost';
91
+
const port = parseInt(env.PORT || 8000, 10);
92
+
93
+
http
94
+
.createServer(requestListener(keys.publicKey))
95
+
.listen(port, host, () => console.log(`listening at http://${host}:${port}`));
96
+
};
97
+
98
+
main(process.env);
+186
server/package-lock.json
+186
server/package-lock.json
···
1
+
{
2
+
"name": "server",
3
+
"lockfileVersion": 3,
4
+
"requires": true,
5
+
"packages": {
6
+
"": {
7
+
"dependencies": {
8
+
"web-push": "^3.6.7"
9
+
}
10
+
},
11
+
"node_modules/agent-base": {
12
+
"version": "7.1.3",
13
+
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
14
+
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
15
+
"license": "MIT",
16
+
"engines": {
17
+
"node": ">= 14"
18
+
}
19
+
},
20
+
"node_modules/asn1.js": {
21
+
"version": "5.4.1",
22
+
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
23
+
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
24
+
"license": "MIT",
25
+
"dependencies": {
26
+
"bn.js": "^4.0.0",
27
+
"inherits": "^2.0.1",
28
+
"minimalistic-assert": "^1.0.0",
29
+
"safer-buffer": "^2.1.0"
30
+
}
31
+
},
32
+
"node_modules/bn.js": {
33
+
"version": "4.12.2",
34
+
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
35
+
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
36
+
"license": "MIT"
37
+
},
38
+
"node_modules/buffer-equal-constant-time": {
39
+
"version": "1.0.1",
40
+
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
41
+
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
42
+
"license": "BSD-3-Clause"
43
+
},
44
+
"node_modules/debug": {
45
+
"version": "4.4.1",
46
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
47
+
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
48
+
"license": "MIT",
49
+
"dependencies": {
50
+
"ms": "^2.1.3"
51
+
},
52
+
"engines": {
53
+
"node": ">=6.0"
54
+
},
55
+
"peerDependenciesMeta": {
56
+
"supports-color": {
57
+
"optional": true
58
+
}
59
+
}
60
+
},
61
+
"node_modules/ecdsa-sig-formatter": {
62
+
"version": "1.0.11",
63
+
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
64
+
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
65
+
"license": "Apache-2.0",
66
+
"dependencies": {
67
+
"safe-buffer": "^5.0.1"
68
+
}
69
+
},
70
+
"node_modules/http_ece": {
71
+
"version": "1.2.0",
72
+
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz",
73
+
"integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==",
74
+
"license": "MIT",
75
+
"engines": {
76
+
"node": ">=16"
77
+
}
78
+
},
79
+
"node_modules/https-proxy-agent": {
80
+
"version": "7.0.6",
81
+
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
82
+
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
83
+
"license": "MIT",
84
+
"dependencies": {
85
+
"agent-base": "^7.1.2",
86
+
"debug": "4"
87
+
},
88
+
"engines": {
89
+
"node": ">= 14"
90
+
}
91
+
},
92
+
"node_modules/inherits": {
93
+
"version": "2.0.4",
94
+
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
95
+
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
96
+
"license": "ISC"
97
+
},
98
+
"node_modules/jwa": {
99
+
"version": "2.0.1",
100
+
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
101
+
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
102
+
"license": "MIT",
103
+
"dependencies": {
104
+
"buffer-equal-constant-time": "^1.0.1",
105
+
"ecdsa-sig-formatter": "1.0.11",
106
+
"safe-buffer": "^5.0.1"
107
+
}
108
+
},
109
+
"node_modules/jws": {
110
+
"version": "4.0.0",
111
+
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
112
+
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
113
+
"license": "MIT",
114
+
"dependencies": {
115
+
"jwa": "^2.0.0",
116
+
"safe-buffer": "^5.0.1"
117
+
}
118
+
},
119
+
"node_modules/minimalistic-assert": {
120
+
"version": "1.0.1",
121
+
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
122
+
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
123
+
"license": "ISC"
124
+
},
125
+
"node_modules/minimist": {
126
+
"version": "1.2.8",
127
+
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
128
+
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
129
+
"license": "MIT",
130
+
"funding": {
131
+
"url": "https://github.com/sponsors/ljharb"
132
+
}
133
+
},
134
+
"node_modules/ms": {
135
+
"version": "2.1.3",
136
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
137
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
138
+
"license": "MIT"
139
+
},
140
+
"node_modules/safe-buffer": {
141
+
"version": "5.2.1",
142
+
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
143
+
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
144
+
"funding": [
145
+
{
146
+
"type": "github",
147
+
"url": "https://github.com/sponsors/feross"
148
+
},
149
+
{
150
+
"type": "patreon",
151
+
"url": "https://www.patreon.com/feross"
152
+
},
153
+
{
154
+
"type": "consulting",
155
+
"url": "https://feross.org/support"
156
+
}
157
+
],
158
+
"license": "MIT"
159
+
},
160
+
"node_modules/safer-buffer": {
161
+
"version": "2.1.2",
162
+
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
163
+
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
164
+
"license": "MIT"
165
+
},
166
+
"node_modules/web-push": {
167
+
"version": "3.6.7",
168
+
"resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz",
169
+
"integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==",
170
+
"license": "MPL-2.0",
171
+
"dependencies": {
172
+
"asn1.js": "^5.3.0",
173
+
"http_ece": "1.2.0",
174
+
"https-proxy-agent": "^7.0.0",
175
+
"jws": "^4.0.0",
176
+
"minimist": "^1.2.5"
177
+
},
178
+
"bin": {
179
+
"web-push": "src/cli.js"
180
+
},
181
+
"engines": {
182
+
"node": ">= 16"
183
+
}
184
+
}
185
+
}
186
+
}
+103
server/web-content/index.html
+103
server/web-content/index.html
···
1
+
<!doctype html>
2
+
<style>
3
+
#error-message {
4
+
color: tomato;
5
+
}
6
+
</style>
7
+
8
+
hiiiiii
9
+
10
+
<button id="subscribe">subscribe</button>
11
+
12
+
<p id="error-message"></p>
13
+
14
+
15
+
<script>
16
+
const err = m => {
17
+
document.getElementById('error-message').textContent = m;
18
+
throw new Error(m);
19
+
};
20
+
21
+
if (!('serviceWorker' in navigator)) err('service worker not supported');
22
+
23
+
if (!('PushManager' in window)) err('push not supported');
24
+
25
+
function urlBase64ToUint8Array(base64String) {
26
+
var padding = '='.repeat((4 - base64String.length % 4) % 4);
27
+
var base64 = (base64String + padding)
28
+
.replace(/\-/g, '+')
29
+
.replace(/_/g, '/');
30
+
31
+
var rawData = window.atob(base64);
32
+
var outputArray = new Uint8Array(rawData.length);
33
+
34
+
for (var i = 0; i < rawData.length; ++i) {
35
+
outputArray[i] = rawData.charCodeAt(i);
36
+
}
37
+
return outputArray;
38
+
}
39
+
40
+
function registerServiceWorker() {
41
+
return navigator.serviceWorker
42
+
.register('/service-worker.js')
43
+
.then(function (registration) {
44
+
console.log('Service worker successfully registered.');
45
+
return registration;
46
+
})
47
+
.catch(function (err) {
48
+
console.error('Unable to register service worker.', err);
49
+
});
50
+
}
51
+
52
+
function askPermission() {
53
+
return new Promise(function (resolve, reject) {
54
+
const permissionResult = Notification.requestPermission(function (result) {
55
+
resolve(result);
56
+
});
57
+
58
+
if (permissionResult) {
59
+
permissionResult.then(resolve, reject);
60
+
}
61
+
}).then(function (permissionResult) {
62
+
if (permissionResult !== 'granted') {
63
+
throw new Error("We weren't granted permission.");
64
+
}
65
+
});
66
+
}
67
+
68
+
function subscribeUserToPush() {
69
+
return navigator.serviceWorker
70
+
.register('/service-worker.js')
71
+
.then(function (registration) {
72
+
const subscribeOptions = {
73
+
userVisibleOnly: true,
74
+
applicationServerKey: urlBase64ToUint8Array(PUBKEY),
75
+
};
76
+
77
+
return registration.pushManager.subscribe(subscribeOptions);
78
+
})
79
+
.then(function (pushSubscription) {
80
+
console.log(
81
+
'Received PushSubscription: ',
82
+
JSON.stringify(pushSubscription),
83
+
);
84
+
return pushSubscription;
85
+
});
86
+
}
87
+
88
+
document.getElementById('subscribe').addEventListener('click', async () => {
89
+
const perm = await askPermission();
90
+
console.log({ perm });
91
+
const sub = await subscribeUserToPush();
92
+
console.log({ sub });
93
+
const res = await fetch('/subscribe', {
94
+
method: 'POST',
95
+
body: JSON.stringify(sub),
96
+
headers: {
97
+
'Content-Type': 'application/json',
98
+
},
99
+
});
100
+
console.log('res', res);
101
+
});
102
+
103
+
</script>
+60
server/web-content/service-worker.js
+60
server/web-content/service-worker.js
···
1
+
console.log('hello????');
2
+
3
+
function urlBase64ToUint8Array(base64String) {
4
+
var padding = '='.repeat((4 - base64String.length % 4) % 4);
5
+
var base64 = (base64String + padding)
6
+
.replace(/\-/g, '+')
7
+
.replace(/_/g, '/');
8
+
9
+
var rawData = window.atob(base64);
10
+
var outputArray = new Uint8Array(rawData.length);
11
+
12
+
for (var i = 0; i < rawData.length; ++i) {
13
+
outputArray[i] = rawData.charCodeAt(i);
14
+
}
15
+
return outputArray;
16
+
}
17
+
18
+
self.addEventListener('push', function(event) {
19
+
console.log('Received a push message', event);
20
+
21
+
// Display notification or handle data
22
+
// Example: show a notification
23
+
const title = 'New Notification';
24
+
const body = event.data.text();
25
+
const icon = '/images/icon.png';
26
+
const tag = 'simple-push-demo-notification-tag';
27
+
28
+
event.waitUntil(
29
+
self.registration.showNotification(title, {
30
+
body: body,
31
+
icon: icon,
32
+
tag: tag
33
+
})
34
+
);
35
+
36
+
// Attempt to resubscribe after receiving a notification
37
+
event.waitUntil(resubscribeToPush());
38
+
});
39
+
40
+
function resubscribeToPush() {
41
+
return self.registration.pushManager.getSubscription()
42
+
.then(function(subscription) {
43
+
if (subscription) {
44
+
return subscription.unsubscribe();
45
+
}
46
+
})
47
+
.then(function() {
48
+
return self.registration.pushManager.subscribe({
49
+
userVisibleOnly: true,
50
+
applicationServerKey: urlBase64ToUint8Array(PUBKEY)
51
+
});
52
+
})
53
+
.then(function(subscription) {
54
+
console.log('Resubscribed to push notifications:', subscription);
55
+
// Optionally, send new subscription details to your server
56
+
})
57
+
.catch(function(error) {
58
+
console.error('Failed to resubscribe:', error);
59
+
});
60
+
}