diff options
Diffstat (limited to 'client-web/source')
-rw-r--r-- | client-web/source/helper.ts | 7 | ||||
-rw-r--r-- | client-web/source/index.ts | 6 | ||||
-rw-r--r-- | client-web/source/menu.ts | 2 | ||||
-rw-r--r-- | client-web/source/preferences/decl.ts | 3 | ||||
-rw-r--r-- | client-web/source/protocol/mod.ts | 32 | ||||
-rw-r--r-- | client-web/source/room.ts | 4 | ||||
-rw-r--r-- | client-web/source/room_watches.ts | 58 |
7 files changed, 90 insertions, 22 deletions
diff --git a/client-web/source/helper.ts b/client-web/source/helper.ts index ecfdf95..49c36fa 100644 --- a/client-web/source/helper.ts +++ b/client-web/source/helper.ts @@ -77,3 +77,10 @@ export function display_filesize(n: number): string { if (n > 1000) return (n / 1000).toFixed(1) + "kB" return n.toString() + "B" } + +export class EventEmitter<E> { + private handlers: Set<(e: E) => unknown> = new Set() + public dispatch(e: E) { this.handlers.forEach(h => h(e)) } + public add_listener(listener: (e: E) => unknown) { this.handlers.add(listener) } + public remove_listener(listener: (e: E) => unknown) { this.handlers.delete(listener) } +} diff --git a/client-web/source/index.ts b/client-web/source/index.ts index f5f6b2d..3dbac6d 100644 --- a/client-web/source/index.ts +++ b/client-web/source/index.ts @@ -69,7 +69,7 @@ export async function main() { if (room_secret.length == 0) return window.location.href = "/" // send them back to the start page if (PREFS.warn_redirect) log({ scope: "crypto", warn: true }, "You were redirected from the old URL format. The server knows the room secret now - e2ee is insecure!") - const conn = await (new SignalingConnection().connect(room_secret)) + const conn = await (new SignalingConnection().connect()) const rtc_config: RTCConfiguration = { iceCandidatePoolSize: 10, iceServers: [{ @@ -81,9 +81,6 @@ export async function main() { r = new Room(conn, rtc_config) - conn.control_handler = (a) => r.control_handler(a) - conn.relay_handler = (a, b) => r.relay_handler(a, b) - setup_keybinds(r) r.on_ready = () => { const sud = e("div", { class: "side-ui" }) @@ -92,4 +89,5 @@ export async function main() { } if (globalThis.navigator.serviceWorker) init_serviceworker() + await conn.join(room_secret) } diff --git a/client-web/source/menu.ts b/client-web/source/menu.ts index 583f28b..3ad9c99 100644 --- a/client-web/source/menu.ts +++ b/client-web/source/menu.ts @@ -40,7 +40,7 @@ export function control_bar(room: Room, side_ui_container: HTMLElement): HTMLEle const leave = e("button", { class: "leave", onclick() { window.location.href = "/" } }, "Leave") const chat = side_ui(side_ui_container, room.chat.element, "Chat") const prefs = side_ui(side_ui_container, ui_preferences(), "Settings") - const rwatches = side_ui(side_ui_container, ui_room_watches(), "Known Rooms") + const rwatches = side_ui(side_ui_container, ui_room_watches(room.signaling), "Known Rooms") const local_controls = [ //ediv({ class: "local-controls", aria_label: "local resources" }, e("button", { onclick: () => room.local_user.await_add_resource(create_mic_res()) }, "Microphone"), e("button", { onclick: () => room.local_user.await_add_resource(create_camera_res()) }, "Camera"), diff --git a/client-web/source/preferences/decl.ts b/client-web/source/preferences/decl.ts index 40dfcd6..5f2d8a2 100644 --- a/client-web/source/preferences/decl.ts +++ b/client-web/source/preferences/decl.ts @@ -42,5 +42,6 @@ export const PREF_DECLS = { notify_join: { type: bool, default: true, description: "Send notifications when users join" }, notify_leave: { type: bool, default: true, description: "Send notifications when users leave" }, - enable_onbeforeunload: { type: bool, default: true, description: "Prompt for confirmation when leaving the site while local resources are active" } + enable_onbeforeunload: { type: bool, default: true, description: "Prompt for confirmation when leaving the site while local resources are active" }, + room_watches: { type: string, default: "Public=public", description: "Known rooms (as semicolon seperated list of name=secret pairs)" } } diff --git a/client-web/source/protocol/mod.ts b/client-web/source/protocol/mod.ts index 83ee8cb..0db1162 100644 --- a/client-web/source/protocol/mod.ts +++ b/client-web/source/protocol/mod.ts @@ -4,23 +4,22 @@ Copyright (C) 2023 metamuffin <metamuffin@disroot.org> */ import { ClientboundPacket, RelayMessage, RelayMessageWrapper, ServerboundPacket } from "../../../common/packets.d.ts" +import { EventEmitter } from "../helper.ts"; import { log } from "../logger.ts" import { crypto_encrypt, crypto_seeded_key, crypt_decrypt, crypto_hash } from "./crypto.ts" export class SignalingConnection { - room!: string websocket!: WebSocket - room_hash!: string - key!: CryptoKey + room?: string + room_hash?: string + key?: CryptoKey my_id?: number // needed for outgoing relay messages - control_handler: (_packet: ClientboundPacket) => void = () => { } - relay_handler: (_sender: number, _message: RelayMessage) => void = () => { } + control_handler = new EventEmitter<ClientboundPacket>() + relay_handler = new EventEmitter<[number, RelayMessage]>() constructor() { } - async connect(room: string): Promise<SignalingConnection> { - this.key = await crypto_seeded_key(room) - this.room_hash = await crypto_hash(room) + async connect(): Promise<SignalingConnection> { log("ws", "connecting…") const ws_url = new URL(`${window.location.protocol.endsWith("s:") ? "wss" : "ws"}://${window.location.host}/signaling`) this.websocket = new WebSocket(ws_url) @@ -44,21 +43,28 @@ export class SignalingConnection { } on_open() { log("ws", "websocket opened"); - this.send_control({ join: { hash: this.room_hash } }) setInterval(() => this.send_control({ ping: null }), 30000) // stupid workaround for nginx disconnecting inactive connections } + + async join(room: string) { + this.room = room; + this.key = await crypto_seeded_key(room) + this.room_hash = await crypto_hash(room) + this.send_control({ join: { hash: this.room_hash } }) + } + on_error() { log({ scope: "ws", error: true }, "websocket error occurred!") } async on_message(data: string) { const packet: ClientboundPacket = JSON.parse(data) // TODO dont crash if invalid - this.control_handler(packet) + this.control_handler.dispatch(packet) if (packet.init) this.my_id = packet.init.your_id; if (packet.message) { - const plain_json = await crypt_decrypt(this.key, packet.message.message) + const plain_json = await crypt_decrypt(this.key!, packet.message.message) const plain: RelayMessageWrapper = JSON.parse(plain_json) // TODO make sure that protocol spec is met if (plain.sender == packet.message.sender) - this.relay_handler(packet.message.sender, plain.inner) + this.relay_handler.dispatch([packet.message.sender, plain.inner]) else { log({ scope: "crypto", warn: true }, `message dropped: sender inconsistent (${plain.sender} != ${packet.message.sender})`) } @@ -71,7 +77,7 @@ export class SignalingConnection { async send_relay(data: RelayMessage, recipient?: number | null) { recipient ??= undefined // null -> undefined const packet: RelayMessageWrapper = { inner: data, sender: this.my_id! } - const message = await crypto_encrypt(this.key, JSON.stringify(packet)) + const message = await crypto_encrypt(this.key!, JSON.stringify(packet)) this.send_control({ relay: { recipient, message } }) } } diff --git a/client-web/source/room.ts b/client-web/source/room.ts index ba18162..8fd165d 100644 --- a/client-web/source/room.ts +++ b/client-web/source/room.ts @@ -24,6 +24,8 @@ export class Room { constructor(public signaling: SignalingConnection, public rtc_config: RTCConfiguration) { this.element = e("div", { class: "room", aria_label: "user list" }) + signaling.control_handler.add_listener(p => this.control_handler(p)) + signaling.relay_handler.add_listener(([a, b]) => this.relay_handler(a, b)) } control_handler(packet: ClientboundPacket) { @@ -58,4 +60,4 @@ export class Room { log("ws", `<- [relay from ${sender.display_name}]: `, message); sender.on_relay(message) } -}
\ No newline at end of file +} diff --git a/client-web/source/room_watches.ts b/client-web/source/room_watches.ts index 331022d..d91972d 100644 --- a/client-web/source/room_watches.ts +++ b/client-web/source/room_watches.ts @@ -1,7 +1,61 @@ import { e } from "./helper.ts"; +import { PREFS } from "./preferences/mod.ts"; +import { crypto_hash } from "./protocol/crypto.ts"; +import { SignalingConnection } from "./protocol/mod.ts"; -export function ui_room_watches(): HTMLElement { - const listing = e("div", {}) +interface Watch { + secret: string, + hash: string, + name: string, + user_count: number, +} + +export function ui_room_watches(conn: SignalingConnection): HTMLElement { + const listing = e("div", { class: "room-watches-listing" }) + + const watches: Watch[] = [] + const update_watches = () => (conn.send_control({ watch_rooms: watches.map(w => w.hash) }), update_listing()); + + (async () => { + for (const e of PREFS.room_watches.split(";")) { + const [name, secret] = e.split("="); + watches.push({ + name, + secret, + hash: await crypto_hash(secret), + user_count: 0 + }) + } + update_watches() + })() + + conn.control_handler.add_listener(packet => { + if (packet.room_info) { + const w = watches.find(w => w.hash == packet.room_info!.hash) + w!.user_count = packet.room_info.user_count + update_listing() + } + }) + + const update_listing = () => { + listing.innerHTML = "" + for (const w of watches) { + const ucont = [] + if (w.user_count > 0) ucont.push(e("div", {})) + if (w.user_count > 1) ucont.push(e("div", {})) + if (w.user_count > 2) ucont.push(e("div", {})) + if (w.user_count > 3) ucont.push(e("span", {}, `+${w.user_count - 3}`)) + listing.append(e("li", {}, + e("a", { + href: "#" + encodeURIComponent(w.secret), + class: w.secret == conn.room ? "current-room" : [] + }, + w.name, + e("div", { class: "users" }, ...ucont) + ) + )) + } + } return e("div", { class: "room-watches" }, e("h2", {}, "Known Rooms"), |