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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
|
/*
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.org>
*/
import { ClientboundPacket, RelayMessage, RelayMessageWrapper, ServerboundPacket } from "../../../common/packets.d.ts"
import { EventEmitter } from "../helper.ts";
import { log } from "../logger.ts"
import { encrypt, derive_seeded_key, decrypt, room_hash } from "./crypto.ts"
export class SignalingConnection {
websocket!: WebSocket
room?: string
room_hash?: string
key?: CryptoKey
my_id?: number // needed for outgoing relay messages
control_handler = new EventEmitter<ClientboundPacket>()
relay_handler = new EventEmitter<[number, RelayMessage]>()
constructor() { }
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)
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");
setInterval(() => this.send_control({ ping: null }), 30000) // stupid workaround for reverse proxies disconnecting inactive connections
}
async join(room: string) {
this.room = room;
this.key = await derive_seeded_key(room)
this.room_hash = await room_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) {
let packet: ClientboundPacket
try {
packet = JSON.parse(data)
} catch (_e) {
log({ scope: "ws", warn: true }, "server sent invalid json")
return
}
this.control_handler.dispatch(packet)
if (packet.init) this.my_id = packet.init.your_id;
if (packet.message) {
const plain_json = await decrypt(this.key!, packet.message.message)
let plain: RelayMessageWrapper
try {
plain = JSON.parse(plain_json) // TODO make sure that protocol spec is met
} catch (_e) {
return log({ scope: "ws", warn: true }, "somebody sent invalid json");
}
if (plain.sender == packet.message.sender)
this.relay_handler.dispatch([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) {
log("ws", "->", data, recipient ? recipient : " ")
recipient ??= undefined // null -> undefined
const packet: RelayMessageWrapper = { inner: data, sender: this.my_id! }
const message = await encrypt(this.key!, JSON.stringify(packet))
this.send_control({ relay: { recipient, message } })
}
}
|