diff --git a/src/snek/static/push.js b/src/snek/static/push.js
index 2e40f2e..89aacb6 100644
--- a/src/snek/static/push.js
+++ b/src/snek/static/push.js
@@ -1,62 +1,49 @@
-// this.onpush = (event) => {
-// console.log(event.data);
-// // From here we can write the data to IndexedDB, send it to any open
-// // windows, display a notification, etc.
-// };
+export const registerServiceWorker = async (silent = false) => {
+ try {
+ const serviceWorkerRegistration = await navigator.serviceWorker
+ .register("/service-worker.js")
-function arrayBufferToBase64(buffer) {
- return btoa(String.fromCharCode(...new Uint8Array(buffer)))
+ await serviceWorkerRegistration.update()
+
+ const keyResponse = await fetch('/push.json')
+ const keyData = await keyResponse.json()
+
+ const publicKey = Uint8Array.from(atob(keyData.publicKey), c => c.charCodeAt(0))
+
+ const pushSubscription = await serviceWorkerRegistration.pushManager.subscribe({
+ userVisibleOnly: true, applicationServerKey: publicKey,
+ })
+
+ const subscriptionObject = {
+ ...pushSubscription.toJSON(), encoding: PushManager.supportedContentEncodings,
+ };
+ console.log(pushSubscription.endpoint, pushSubscription, pushSubscription.toJSON(), subscriptionObject);
+
+ const response = await fetch('/push.json', {
+ method: 'POST', headers: {
+ 'Content-Type': 'application/json',
+ }, body: JSON.stringify(subscriptionObject),
+ })
+
+ if (!response.ok) {
+ throw new Error('Bad status code from server.');
+ }
+
+ const responseData = await response.json();
+ console.log('Registration response', responseData);
+ } catch (error) {
+ console.error("Error registering service worker:", error);
+ if (!silent) {
+ alert("Registering push notifications failed. Please check your browser settings and try again.\n\n" + error);
+ }
+ }
}
-const keyResponse = await fetch('/push.json')
-const keyData = await keyResponse.json()
-console.log("Key data", keyData);
-
-const publicKey = Uint8Array.from(atob(keyData.publicKey), c => c.charCodeAt(0))
-
-export const registerServiceWorker = async () => {
- navigator.serviceWorker
- .register("/service-worker.js")
- .then((serviceWorkerRegistration) => {
- console.log(serviceWorkerRegistration);
- serviceWorkerRegistration.pushManager.subscribe({
- userVisibleOnly: true,
- applicationServerKey: publicKey,
- }).then(
- (pushSubscription) => {
- const subscriptionObject = {
- ...pushSubscription.toJSON(),
- encoding: PushManager.supportedContentEncodings,
- /* other app-specific data, such as user identity */
- };
- console.log(pushSubscription.endpoint, pushSubscription, pushSubscription.toJSON(), subscriptionObject);
-
- fetch('/push.json', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(subscriptionObject),
- }).then((response) => {
- if (!response.ok) {
- throw new Error('Bad status code from server.');
- }
- return response.json();
- }).then((responseData) => {
- console.log(responseData);
- });
- },
- (error) => {
- console.error(error);
- },
- );
- });
-}
-
-window.registerServiceWorker = () => {
+window.registerNotificationsServiceWorker = () => {
return Notification.requestPermission().then((permission) => {
if (permission === "granted") {
+ console.log("Permission was granted");
return registerServiceWorker();
} else if (permission === "denied") {
console.log("Permission was denied");
@@ -65,4 +52,4 @@ window.registerServiceWorker = () => {
}
});
};
-registerServiceWorker().catch(console.error);
+registerServiceWorker(true).catch(console.error);
diff --git a/src/snek/static/service-worker.js b/src/snek/static/service-worker.js
index 038bb5f..310fa96 100644
--- a/src/snek/static/service-worker.js
+++ b/src/snek/static/service-worker.js
@@ -1,83 +1,66 @@
-async function requestNotificationPermission() {
- const permission = await Notification.requestPermission();
- return permission === "granted";
-}
+self.addEventListener("install", (event) => {
+ console.log("Service worker installing...");
+ event.waitUntil(
+ caches.open("snek-cache").then((cache) => {
+ return cache.addAll([]);
+ })
+ );
+})
-// Subscribe to Push Notifications
-async function subscribeUser() {
- const registration =
- await navigator.serviceWorker.register("/service-worker.js");
- const subscription = await registration.pushManager.subscribe({
- userVisibleOnly: true,
- applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY),
- });
+self.addEventListener("activate", (event) => {
+ event.waitUntil(self.registration?.navigationPreload.enable());
+});
- // Send subscription to your backend
- await fetch("/subscribe", {
- method: "POST",
- body: JSON.stringify(subscription),
- headers: {
- "Content-Type": "application/json",
- },
- });
-}
+self.addEventListener("push", (event) => {
+ if (!self.Notification || self.Notification.permission !== "granted") {
+ console.log("Notification permission not granted");
+ return;
+ }
-// Service Worker (service-worker.js)
-// self.addEventListener("push", (event) => {
-// const data = event.data.json();
-// self.registration.showNotification(data.title, {
-// body: data.message,
-// icon: data.icon,
-// });
-// });
+ const data = event.data?.json() ?? {};
+ console.log("Received a push message", event, data);
+ const title = data.title || "Something Has Happened";
+ const message =
+ data.message || "Here's something you might want to check out.";
+ const icon = data.icon || "/image/snek512.png";
-self.addEventListener("push", (event) => {
- if (!self.Notification || self.Notification.permission !== "granted") {
- console.log("Notification permission not granted");
- return;
- }
+ const notificationSettings = data.notificationSettings || {};
- const data = event.data?.json() ?? {};
- console.log("Received a push message", event, data);
+ console.log("Showing message", title, message, icon);
- const title = data.title || "Something Has Happened";
- const message =
- data.message || "Here's something you might want to check out.";
- const icon = data.icon || "images/new-notification.png";
- console.log("showing message", title, message, icon);
-
- const reg = self.registration.showNotification(title, {
- body: message,
- tag: "simple-push-demo-notification",
- icon,
- }).then(e => console.log("success", e)).catch(console.error);
-
- event.waitUntil(reg);
+ const reg = self.registration.showNotification(title, {
+ body: message,
+ tag: "message-received",
+ icon,
+ badge: icon,
+ ...notificationSettings,
+ data,
+ }).then(e => console.log("Showing notification", e)).catch(console.error);
+ event.waitUntil(reg);
});
self.addEventListener("notificationclick", (event) => {
console.log("Notification click Received.", event);
event.notification.close();
- event.waitUntil(clients.openWindow(
- "https://snek.molodetz.nl",));
+ event.waitUntil(clients.openWindow(`${event.notification.data.url || event.notification.data.link || `/web.html`}`));
});
self.addEventListener("notificationclose", (event) => {
-
+ console.log("Notification closed", event);
})
self.addEventListener("fetch", (event) => {
// console.log("Fetch event for ", event.request.url);
event.respondWith(
caches.match(event.request).then((response) => {
- if (response) {
- // console.log("Found response in cache: ", response);
- return response;
- }
- // console.log("No response found in cache. About to fetch from network...");
- return fetch(event.request);
+ if (response) {
+ // console.log("Found response in cache: ", response);
+ return response;
+ }
+ // console.log("No response found in cache. About to fetch from network...");
+ return fetch(event.request);
})
);
})
\ No newline at end of file
diff --git a/src/snek/system/notification.py b/src/snek/system/notification.py
index 3a60b7c..7f4274d 100644
--- a/src/snek/system/notification.py
+++ b/src/snek/system/notification.py
@@ -2,6 +2,7 @@ import time
import base64
import uuid
from functools import cache
+from pathlib import Path
import jwt
from cryptography.hazmat.primitives.asymmetric import ec
@@ -14,66 +15,55 @@ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
-PRIVATE_KEY_FILE = "./notification-private.pem"
-PRIVATE_KEY_PKCS8_FILE = "./notification-private.pkcs8.pem"
-PUBLIC_KEY_FILE = "./notification-public.pem"
+# The only reason to persist the keys is to be able to use them in the web push
+
+PRIVATE_KEY_FILE = Path("./notification-private.pem")
+PRIVATE_KEY_PKCS8_FILE = Path("./notification-private.pkcs8.pem")
+PUBLIC_KEY_FILE = Path("./notification-public.pem")
def generate_private_key():
- if not os.path.isfile(PRIVATE_KEY_FILE):
+ if not PRIVATE_KEY_FILE.exists():
private_key = ec.generate_private_key(ec.SECP256R1(), default_backend())
- # Serialize the private key to PEM format
pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
- # Write the private key to a file
- with open(PRIVATE_KEY_FILE, "wb") as pem_out:
- pem_out.write(pem)
+ PRIVATE_KEY_FILE.write_bytes(pem)
def generate_pcks8_private_key():
- # openssl pkcs8 -topk8 -nocrypt -in private.pem -out private.pkcs8.pem
- if not os.path.isfile(PRIVATE_KEY_PKCS8_FILE):
- with open(PRIVATE_KEY_FILE, "rb") as pem_in:
- private_key = serialization.load_pem_private_key(
- pem_in.read(), password=None, backend=default_backend()
- )
+ if not PRIVATE_KEY_PKCS8_FILE.exists():
+ private_key = serialization.load_pem_private_key(
+ PRIVATE_KEY_FILE.read_bytes(), password=None, backend=default_backend()
+ )
- # Serialize the private key to PKCS8 format
pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)
- # Write the private key to a file
- with open(PRIVATE_KEY_PKCS8_FILE, "wb") as pem_out:
- pem_out.write(pem)
+ PRIVATE_KEY_PKCS8_FILE.write_bytes(pem)
def generate_public_key():
- if not os.path.isfile(PUBLIC_KEY_FILE):
- with open(PRIVATE_KEY_FILE, "rb") as pem_in:
- private_key = serialization.load_pem_private_key(
- pem_in.read(), password=None, backend=default_backend()
- )
+ if not PUBLIC_KEY_FILE.exists():
+ private_key = serialization.load_pem_private_key(
+ PRIVATE_KEY_FILE.read_bytes(), password=None, backend=default_backend()
+ )
- # Get the public key from the private key
public_key = private_key.public_key()
- # Serialize the public key to PEM format
pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
- # Write the public key to a file
- with open(PUBLIC_KEY_FILE, "wb") as pem_out:
- pem_out.write(pem)
+ PUBLIC_KEY_FILE.write_bytes(pem)
def ensure_certificates():
@@ -92,41 +82,37 @@ def hkdf(input_key, salt, info, length):
).derive(input_key)
+def _browser_base64(data):
+ return base64.urlsafe_b64encode(data).decode("utf-8").rstrip("=")
+
+
class Notifications:
private_key_pem = None
public_key = None
- public_key_jwk = None
+ public_key_base64 = None
def __init__(self):
ensure_certificates()
- with open(PRIVATE_KEY_FILE, "rb") as pem_in:
- private_key = serialization.load_pem_private_key(
- pem_in.read(), password=None, backend=default_backend()
- )
- self.private_key_pem = private_key.private_bytes(
- encoding=serialization.Encoding.PEM,
- format=serialization.PrivateFormat.TraditionalOpenSSL,
- encryption_algorithm=serialization.NoEncryption(),
- )
- with open(PUBLIC_KEY_FILE, "rb") as pem_in:
- self.public_key = serialization.load_pem_public_key(
- pem_in.read(), backend=default_backend()
+ private_key = serialization.load_pem_private_key(
+ PRIVATE_KEY_FILE.read_bytes(), password=None, backend=default_backend()
+ )
+ self.private_key_pem = private_key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
+ encryption_algorithm=serialization.NoEncryption(),
+ )
+
+ self.public_key = serialization.load_pem_public_key(
+ PUBLIC_KEY_FILE.read_bytes(), backend=default_backend()
+ )
+
+ self.public_key_base64 = _browser_base64(
+ self.public_key.public_bytes(
+ encoding=serialization.Encoding.X962,
+ format=serialization.PublicFormat.UncompressedPoint,
)
- public_numbers = self.public_key.public_numbers()
-
- self.public_key_jwk = {
- "kty": "EC",
- "crv": "P-256",
- "x": base64.urlsafe_b64encode(
- public_numbers.x.to_bytes(32, byteorder="big")
- ).decode("utf-8"),
- "y": base64.urlsafe_b64encode(
- public_numbers.y.to_bytes(32, byteorder="big")
- ).decode("utf-8"),
- }
-
- print(f"Public key JWK: {self.public_key_jwk}")
+ )
def create_notification_authorization(self, push_url):
target = urlparse(push_url)
@@ -152,7 +138,7 @@ class Notifications:
algorithm="ES256",
)
- def create_encrypted_payload(self, auth: str, p256dh:str, payload:str):
+ def create_encrypted_payload(self, auth: str, p256dh: str, payload: str):
# 1. Generate private key
private_key = ec.generate_private_key(ec.SECP256R1(), default_backend())
@@ -161,13 +147,13 @@ class Notifications:
salt = os.urandom(16)
- subscription_pub_key_bytes = base64.urlsafe_b64decode(p256dh+ '==')
+ subscription_pub_key_bytes = base64.urlsafe_b64decode(p256dh + "====")
subscription_public_key = ec.EllipticCurvePublicKey.from_encoded_point(
ec.SECP256R1(), subscription_pub_key_bytes
)
shared_secret = private_key.exchange(ec.ECDH(), subscription_public_key)
- auth_dec = base64.b64decode(auth+ '==')
+ auth_dec = base64.urlsafe_b64decode(auth + "==")
auth_enc = b"Content-Encoding: auth\x00"
prk = hkdf(shared_secret, auth_dec, auth_enc, 32)
@@ -175,7 +161,8 @@ class Notifications:
key_label = b"P-256\x00"
subscription_len = len(subscription_pub_key_bytes).to_bytes(2, "big")
local_pub_bytes = public_key.public_bytes(
- encoding=serialization.Encoding.X962, format=serialization.PublicFormat.UncompressedPoint
+ encoding=serialization.Encoding.X962,
+ format=serialization.PublicFormat.UncompressedPoint,
)
local_len = len(local_pub_bytes).to_bytes(2, "big")
context = (
@@ -208,18 +195,6 @@ class Notifications:
'public_key': local_pub_bytes
}
- @property
- def public_key_base64(self):
- return (
- base64.urlsafe_b64encode(
- self.public_key.public_bytes(
- encoding=serialization.Encoding.X962,
- format=serialization.PublicFormat.UncompressedPoint,
- )
- )
- .decode("utf-8")
- .rstrip("=")
- )
@cache
def get_notifications():
diff --git a/src/snek/templates/app.html b/src/snek/templates/app.html
index c27b803..0119606 100644
--- a/src/snek/templates/app.html
+++ b/src/snek/templates/app.html
@@ -41,7 +41,7 @@
📥
👥
⚙️
- ✉️
+ ✉️
🔒
diff --git a/src/snek/view/push.py b/src/snek/view/push.py
index 00581ba..bf1bca3 100644
--- a/src/snek/view/push.py
+++ b/src/snek/view/push.py
@@ -82,8 +82,8 @@ class PushView(BaseFormView):
test_payload = {
"title": "Hey retoor",
"message": "Guess what? ;P",
- "icon": "https://molodetz.online/image/snek192.png",
- "url": "https://localhost:8081",
+ "icon": "/image/snek192.png",
+ "url": "/web.html",
}
encryped = notifications.create_encrypted_payload(