summaryrefslogtreecommitdiff
path: root/client-web/source/protocol/mod.ts
blob: 83ee8cbe5330ea1460cbbffdea18f62ea9cede34 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/*
    This file is part of keks-meet (https://codeberg.org/metamuffin/keks-meet)
    which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
    Copyright (C) 2023 metamuffin <metamuffin@disroot.org>
*/
import { ClientboundPacket, RelayMessage, RelayMessageWrapper, ServerboundPacket } from "../../../common/packets.d.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
    my_id?: number // needed for outgoing relay messages

    control_handler: (_packet: ClientboundPacket) => void = () => { }
    relay_handler: (_sender: number, _message: RelayMessage) => void = () => { }

    constructor() { }
    async connect(room: string): Promise<SignalingConnection> {
        this.key = await crypto_seeded_key(room)
        this.room_hash = await crypto_hash(room)
        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)
        this.websocket.onerror = () => this.on_error()
        this.websocket.onclose = () => this.on_close()
        this.websocket.onmessage = e => {
            if (typeof e.data == "string") this.on_message(e.data)
        }
        await new Promise<void>(r => this.websocket!.onopen = () => {
            this.on_open()
            r()
        })
        return this
    }

    on_close() {
        log("ws", "websocket closed");
        setTimeout(() => {
            window.location.reload()
        }, 1000)
    }
    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
    }
    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)
        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: 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)
            else {
                log({ scope: "crypto", warn: true }, `message dropped: sender inconsistent (${plain.sender} != ${packet.message.sender})`)
            }
        }
    }

    send_control(data: ServerboundPacket) {
        this.websocket.send(JSON.stringify(data))
    }
    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))
        this.send_control({ relay: { recipient, message } })
    }
}