diff --git a/web/src/app/Connection.js b/web/src/app/Connection.js index d22640a..ef8a627 100644 --- a/web/src/app/Connection.js +++ b/web/src/app/Connection.js @@ -3,7 +3,8 @@ import {basicAuth, encodeBase64Url, topicShortUrl, topicUrlWs} from "./utils"; const retryBackoffSeconds = [5, 10, 15, 20, 30, 45]; class Connection { - constructor(subscriptionId, baseUrl, topic, user, since, onNotification) { + constructor(connectionId, subscriptionId, baseUrl, topic, user, since, onNotification) { + this.connectionId = connectionId; this.subscriptionId = subscriptionId; this.baseUrl = baseUrl; this.topic = topic; @@ -21,15 +22,15 @@ class Connection { // we don't want to re-trigger the main view re-render potentially hundreds of times. const wsUrl = this.wsUrl(); - console.log(`[Connection, ${this.shortUrl}] Opening connection to ${wsUrl}`); + console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Opening connection to ${wsUrl}`); this.ws = new WebSocket(wsUrl); this.ws.onopen = (event) => { - console.log(`[Connection, ${this.shortUrl}] Connection established`, event); + console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection established`, event); this.retryCount = 0; } this.ws.onmessage = (event) => { - console.log(`[Connection, ${this.shortUrl}] Message received from server: ${event.data}`); + console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Message received from server: ${event.data}`); try { const data = JSON.parse(event.data); if (data.event === 'open') { @@ -41,33 +42,33 @@ class Connection { 'time' in data && 'message' in data; if (!relevantAndValid) { - console.log(`[Connection, ${this.shortUrl}] Unexpected message. Ignoring.`); + console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Unexpected message. Ignoring.`); return; } this.since = data.id; this.onNotification(this.subscriptionId, data); } catch (e) { - console.log(`[Connection, ${this.shortUrl}] Error handling message: ${e}`); + console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Error handling message: ${e}`); } }; this.ws.onclose = (event) => { if (event.wasClean) { - console.log(`[Connection, ${this.shortUrl}] Connection closed cleanly, code=${event.code} reason=${event.reason}`); + console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection closed cleanly, code=${event.code} reason=${event.reason}`); this.ws = null; } else { const retrySeconds = retryBackoffSeconds[Math.min(this.retryCount, retryBackoffSeconds.length-1)]; this.retryCount++; - console.log(`[Connection, ${this.shortUrl}] Connection died, retrying in ${retrySeconds} seconds`); + console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection died, retrying in ${retrySeconds} seconds`); this.retryTimeout = setTimeout(() => this.start(), retrySeconds * 1000); } }; this.ws.onerror = (event) => { - console.log(`[Connection, ${this.shortUrl}] Error occurred: ${event}`, event); + console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Error occurred: ${event}`, event); }; } close() { - console.log(`[Connection, ${this.shortUrl}] Closing connection`); + console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Closing connection`); const socket = this.ws; const retryTimeout = this.retryTimeout; if (socket !== null) { diff --git a/web/src/app/ConnectionManager.js b/web/src/app/ConnectionManager.js index 93f8659..42cd016 100644 --- a/web/src/app/ConnectionManager.js +++ b/web/src/app/ConnectionManager.js @@ -1,30 +1,39 @@ import Connection from "./Connection"; +import {sha256} from "./utils"; class ConnectionManager { constructor() { - this.connections = new Map(); + this.connections = new Map(); // ConnectionId -> Connection (hash, see below) } - refresh(subscriptions, users, onNotification) { + async refresh(subscriptions, users, onNotification) { if (!subscriptions || !users) { return; } console.log(`[ConnectionManager] Refreshing connections`); - const subscriptionIds = subscriptions.map(s => s.id); - const deletedIds = Array.from(this.connections.keys()).filter(id => !subscriptionIds.includes(id)); + const subscriptionsWithUsersAndConnectionId = await Promise.all(subscriptions + .map(async s => { + const [user] = users.filter(u => u.baseUrl === s.baseUrl); + const connectionId = await makeConnectionId(s, user); + return {...s, user, connectionId}; + })); + const activeIds = subscriptionsWithUsersAndConnectionId.map(s => s.connectionId); + const deletedIds = Array.from(this.connections.keys()).filter(id => !activeIds.includes(id)); + console.log(subscriptionsWithUsersAndConnectionId); // Create and add new connections - subscriptions.forEach(subscription => { - const id = subscription.id; - const added = !this.connections.get(id) + subscriptionsWithUsersAndConnectionId.forEach(subscription => { + const subscriptionId = subscription.id; + const connectionId = subscription.connectionId; + const added = !this.connections.get(connectionId) if (added) { const baseUrl = subscription.baseUrl; const topic = subscription.topic; - const [user] = users.filter(user => user.baseUrl === baseUrl); + const user = subscription.user; const since = subscription.last; - const connection = new Connection(id, baseUrl, topic, user, since, onNotification); - this.connections.set(id, connection); - console.log(`[ConnectionManager] Starting new connection ${id}`); + const connection = new Connection(connectionId, subscriptionId, baseUrl, topic, user, since, onNotification); + this.connections.set(connectionId, connection); + console.log(`[ConnectionManager] Starting new connection ${connectionId} (subscription ${subscriptionId} with user ${user ? user.username : "anonymous"})`); connection.start(); } }); @@ -39,5 +48,12 @@ class ConnectionManager { } } +const makeConnectionId = async (subscription, user) => { + const hash = (user) + ? await sha256(`${subscription.id}|${user.username}|${user.password}`) + : await sha256(`${subscription.id}`); + return hash.substring(0, 10); +} + const connectionManager = new ConnectionManager(); export default connectionManager; diff --git a/web/src/app/utils.js b/web/src/app/utils.js index 1f458d8..19c5ae4 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -90,6 +90,12 @@ export const encodeBase64Url = (s) => { .replaceAll('=', ''); } +// https://jameshfisher.com/2017/10/30/web-cryptography-api-hello-world/ +export const sha256 = async (s) => { + const buf = await crypto.subtle.digest("SHA-256", new TextEncoder("utf-8").encode(s)); + return Array.prototype.map.call(new Uint8Array(buf), x=>(('00'+x.toString(16)).slice(-2))).join(''); +} + export const formatShortDateTime = (timestamp) => { return new Intl.DateTimeFormat('default', {dateStyle: 'short', timeStyle: 'short'}) .format(new Date(timestamp * 1000)); diff --git a/web/src/components/App.js b/web/src/components/App.js index 4d939b8..ada71cc 100644 --- a/web/src/components/App.js +++ b/web/src/components/App.js @@ -19,14 +19,11 @@ import pruner from "../app/Pruner"; import subscriptionManager from "../app/SubscriptionManager"; import userManager from "../app/UserManager"; -// TODO subscribe dialog: -// - check/use existing user -// - add baseUrl -// TODO embed into ntfy server +// TODO subscribe dialog check/use existing user // TODO make default server functional -// TODO business logic with callbacks +// TODO routing +// TODO embed into ntfy server // TODO connection indicator in subscription list -// TODO connectionmanager should react on users changes const App = () => { console.log(`[App] Rendering main view`); @@ -53,9 +50,7 @@ const App = () => { setSelectedSubscription(newSelected); }; const handleRequestPermission = () => { - notificationManager.maybeRequestPermission((granted) => { - setNotificationsGranted(granted); - }) + notificationManager.maybeRequestPermission(granted => setNotificationsGranted(granted)); }; const handlePrefsClick = () => { setPrefsOpen(true); @@ -91,7 +86,7 @@ const App = () => { console.error(`[App] Error handling notification`, e); } }; - connectionManager.refresh(subscriptions, users, handleNotification); + connectionManager.refresh(subscriptions, users, handleNotification); // Dangle }, [subscriptions, users]); useEffect(() => { const subscriptionId = (selectedSubscription) ? selectedSubscription.id : "";