From f78a156f346e4dc3b9bd7f38c3f4119a4f5d335e Mon Sep 17 00:00:00 2001 From: Zoe <62722391+juls0730@users.noreply.github.com> Date: Sat, 6 Sep 2025 00:23:37 -0500 Subject: [PATCH] fix state pollution and other bugs, and random other stuff --- ORGANIZATION.md | 30 ++++++ server/websocketHandler.ts | 125 ++++++++++++++++-------- src/components/RTCMessage.svelte | 71 +++++++++++--- src/lib/index.ts | 1 - src/lib/webrtc.ts | 23 ++--- src/routes/+layout.svelte | 10 +- src/routes/+page.svelte | 22 ++--- src/routes/[roomId]/+page.svelte | 45 ++++----- src/routes/[roomId]/+page.ts | 17 ++++ src/stores/roomStore.ts | 3 + src/stores/websocketStore.ts | 142 +++++---------------------- src/types/websocket.ts | 163 ++++++++++++++++++++----------- src/utils/liveMap.ts | 65 ++++++++++++ src/utils/webrtcUtil.ts | 19 ++-- 14 files changed, 442 insertions(+), 294 deletions(-) create mode 100644 ORGANIZATION.md delete mode 100644 src/lib/index.ts create mode 100644 src/routes/[roomId]/+page.ts create mode 100644 src/utils/liveMap.ts diff --git a/ORGANIZATION.md b/ORGANIZATION.md new file mode 100644 index 0000000..9efd8e8 --- /dev/null +++ b/ORGANIZATION.md @@ -0,0 +1,30 @@ +This document lists the current state of files in this repository. This will serve as a tool for me to reorganize my code and make it easier to find things. + +## Directories + +### /src + +This is the SvelteKit project. + +- **lib/**: + - **webrtc.ts**: Holds the WebRTCPeer class, which is used to handle WebRTC connections. It is the place where encryption and decryption is handled. +- **shared/**: + - **keyConfig.ts**: Holds the configuration for the RSA key pair used for wrapping the unique AES-GCM key for each + message, literally nothing else. +- **stores/**: + - **messageStore.ts**: Holds the messages that are sent between the client and the peer. + - **roomStore.ts**: Holds the room information, such as the room ID, the number of participants, and the connection state. + - **websocketStore.ts**: Holds the WebSocket connection. +- **types/**: + - **message.ts**: Defines the types of application messages that are sent between the client and the peer via WebRTC + post initialization. + - **webrtc.ts**: Defines the WebRTCPeerCallbacks, the WebRTCPacketType, the structure of the WebRTCPacket (even + though all WebRTC packets are binary data), and the structure of the KeyStore. + - **websocket.ts**: Defines the WebSocketMessageType, and the types for each message along with the union. +- **utils/**: + - **webrtcUtil.ts**: This file feels like a hodgepodge of random shit. Its responsible for handling application messages that come from the + data channel, as well as handling the websocket signaling and room notifications. It need to be usable by both peers. + +### /server + +This is the server that handles the webrtc signaling. \ No newline at end of file diff --git a/server/websocketHandler.ts b/server/websocketHandler.ts index 8f4dae9..e72dbec 100644 --- a/server/websocketHandler.ts +++ b/server/websocketHandler.ts @@ -1,22 +1,61 @@ import { WebSocketServer } from "ws"; -import type { WebSocket } from "ws"; -import { SocketMessageType, type SocketMessage } from "../src/types/websocket"; +import { Socket, WebSocketMessageType, type WebSocketMessage } from "../src/types/websocket"; +import { LiveMap } from '../src/utils/liveMap.ts'; -// TODO: remove stale rooms somehow -const rooms = new Map(); +export class ServerRoom { + private clients: Socket[] = []; -async function createRoom(socket: WebSocket): Promise { + constructor(clients?: Socket[]) { + if (clients) { + this.clients = clients; + } + } + + notifyAll(message: WebSocketMessage) { + this.clients.forEach(client => { + client.send(message); + }); + } + + get length(): number { + return this.clients.length; + } + + push(client: Socket) { + this.clients.push(client); + } + + set(clients: Socket[]) { + this.clients = clients; + } + + filter(callback: (client: Socket) => boolean): Socket[] { + return this.clients.filter(callback); + } + + forEachClient(callback: (client: Socket) => void) { + this.clients.forEach(callback); + } +} + +const rooms = new LiveMap(); + +async function createRoom(socket: Socket): Promise { let roomId = Math.random().toString(36).substring(2, 10); - rooms.set(roomId, []); + let room = rooms.set(roomId, new ServerRoom()); - socket.send(JSON.stringify({ type: SocketMessageType.ROOM_CREATED, data: roomId })); + socket.send({ type: WebSocketMessageType.ROOM_CREATED, data: room.key }); - await joinRoom(roomId, socket); + try { + await joinRoom(room.key, socket); + } catch (e: any) { + throw e; + } return roomId; } -async function joinRoom(roomId: string, socket: WebSocket) { +async function joinRoom(roomId: string, socket: Socket): Promise { let room = rooms.get(roomId); console.log(room?.length); @@ -26,22 +65,21 @@ async function joinRoom(roomId: string, socket: WebSocket) { } if (room.length == 2) { - socket.send(JSON.stringify({ type: SocketMessageType.ERROR, data: 'Room is full' })); - return; + throw new Error("Room is full"); } // notify all clients in the room of the new client, except the client itself - room.forEach(client => { - client.send(JSON.stringify({ type: SocketMessageType.JOIN_ROOM, data: roomId })); - }); + room.notifyAll({ type: WebSocketMessageType.JOIN_ROOM, roomId }); room.push(socket); socket.addEventListener('close', (ev) => { room = rooms.get(roomId) if (!room) { - return; + throw new Error("Room not found"); } + room.notifyAll({ type: WebSocketMessageType.ROOM_LEFT, roomId }); + // for some reason, when you filter the array when the length is 1 it stays at 1, but we *know* that if its 1 // then when this client disconnects, the room should be deleted since the room is empty if (room.length === 1) { @@ -52,33 +90,33 @@ async function joinRoom(roomId: string, socket: WebSocket) { deleteRoom(roomId); } }, 5000) - deleteRoom(roomId); return; } - rooms.set(roomId, room.filter(client => client !== ev.target)); + room.set(room.filter(client => client.ws !== ev.target)); }); // TODO: consider letting rooms get larger than 2 clients if (room.length == 2) { - room.forEach(async client => { - // announce the room is ready, and tell each peer if they are the initiator - client.send(JSON.stringify({ type: SocketMessageType.ROOM_READY, data: { isInitiator: client !== socket } })); - }); + room.forEachClient(client => client.send({ type: WebSocketMessageType.ROOM_READY, data: { isInitiator: client !== socket } })); } console.log("Room created:", roomId, room.length); + + return room; } function deleteRoom(roomId: string) { rooms.delete(roomId); } -export function confgiureWebsocketServer(ws: WebSocketServer) { - ws.on('connection', socket => { +export function confgiureWebsocketServer(wss: WebSocketServer) { + wss.on('connection', ws => { + let socket = new Socket(ws); + // Handle messages from the client - socket.on('message', async event => { - let message: SocketMessage | undefined = undefined; + ws.on('message', async event => { + let message: WebSocketMessage | undefined = undefined; if (event instanceof Buffer) { // Assuming JSON is sent as a string try { @@ -91,50 +129,57 @@ export function confgiureWebsocketServer(ws: WebSocketServer) { if (message === undefined) { console.log("Received non-JSON message:", event); // If the message is not JSON, send an error message - socket.send(JSON.stringify({ type: SocketMessageType.ERROR, data: 'Invalid message' })); + socket.send({ type: WebSocketMessageType.ERROR, data: 'Invalid message' }); return; } + let room: ServerRoom | undefined = undefined; + switch (message.type) { - case SocketMessageType.CREATE_ROOM: + case WebSocketMessageType.CREATE_ROOM: // else, create a new room - await createRoom(socket); + try { + await createRoom(socket); + } catch (e: any) { + socket.send({ type: WebSocketMessageType.ERROR, data: e.message }); + throw e; + } break; - case SocketMessageType.JOIN_ROOM: + case WebSocketMessageType.JOIN_ROOM: // if join message has a roomId, join the room if (!message.roomId) { - socket.send(JSON.stringify({ type: SocketMessageType.ERROR, data: 'Invalid message' })); + socket.send({ type: WebSocketMessageType.ERROR, data: 'Invalid message' }); return; } // if the user tries to join a room that doesnt exist, send an error message if (rooms.get(message.roomId) == undefined) { - socket.send(JSON.stringify({ type: SocketMessageType.ERROR, data: 'Invalid roomId' })); + socket.send({ type: WebSocketMessageType.ERROR, data: 'Invalid roomId' }); return; } - await joinRoom(message.roomId, socket); + room = await joinRoom(message.roomId, socket); // the client is now in the room and the peer knows about it - socket.send(JSON.stringify({ type: SocketMessageType.ROOM_JOINED, roomId: message.roomId })); + socket.send({ type: WebSocketMessageType.ROOM_JOINED, roomId: message.roomId, participants: room.length }); break; - case SocketMessageType.OFFER: - case SocketMessageType.ANSWER: - case SocketMessageType.ICE_CANDIDATE: + case WebSocketMessageType.WEBRTC_OFFER: + case WebSocketMessageType.WERTC_ANSWER: + case WebSocketMessageType.WEBRTC_ICE_CANDIDATE: // relay these messages to the other peers in the room - const room = rooms.get(message.data.roomId); + room = rooms.get(message.data.roomId); if (room) { - room.forEach(client => { + room.forEachClient(client => { if (client !== socket) { - client.send(JSON.stringify(message)); + client.send(message); } }); } break; default: console.warn(`Unknown message type: ${message.type}`); - socket.send(JSON.stringify({ type: SocketMessageType.ERROR, data: 'Unknown message type' })); + socket.send({ type: WebSocketMessageType.ERROR, data: 'Unknown message type' }); break; } }); diff --git a/src/components/RTCMessage.svelte b/src/components/RTCMessage.svelte index e526417..5140dda 100644 --- a/src/components/RTCMessage.svelte +++ b/src/components/RTCMessage.svelte @@ -1,7 +1,7 @@ -

{$room?.id} - {$room?.connectionState} - {$webSocketConnected}

+

+ {$room?.id} + ({$room?.participants}) - {$room?.connectionState} - {$webSocketConnected} + - Initial connection {$initialConnectionComplete + ? "complete" + : "incomplete"} +

- -{#if $room !== null && $webSocketConnected === true && $room.connectionState === ConnectionState.CONNECTED} + +{#if ($room !== null && $webSocketConnected === true && $room.connectionState === ConnectionState.CONNECTED) || $room.connectionState === ConnectionState.RECONNECTING}
- {#if !$isRTCConnected || !$dataChannelReady || !$keyExchangeDone || !$canCloseLoadingOverlay} + {#if !$initialConnectionComplete || $room.connectionState === ConnectionState.RECONNECTING || $room.participants !== 2 || !$canCloseLoadingOverlay}
Establishing data channel...

{:else if !$keyExchangeDone}

Establishing a secure connection with the peer...

+ {:else if $room.connectionState === ConnectionState.RECONNECTING} +

+ Disconnect from peer, attempting to reconnecting... +

+ {:else if $room.participants !== 2} +

+ Peer has disconnected, waiting for other peer to + reconnect... +

{:else}

Successfully established a secure connection to @@ -100,7 +130,7 @@

{/if}
- {#if !$keyExchangeDone} + {#if !$keyExchangeDone || $room.participants !== 2 || $room.connectionState === ConnectionState.RECONNECTING} e.key === "Enter" && sendMessage()} + onkeyup={(e) => e.key === "Enter" && sendMessage()} disabled={!$isRTCConnected || !$dataChannelReady || - !$keyExchangeDone} + !$keyExchangeDone || + $room.connectionState === ConnectionState.RECONNECTING} placeholder="Type your message..." class="flex-grow p-2 rounded bg-gray-700 border border-gray-600 text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" />
{/if} + + diff --git a/src/lib/index.ts b/src/lib/index.ts deleted file mode 100644 index 856f2b6..0000000 --- a/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/src/lib/webrtc.ts b/src/lib/webrtc.ts index b439eb9..dfcbd34 100644 --- a/src/lib/webrtc.ts +++ b/src/lib/webrtc.ts @@ -1,7 +1,9 @@ import { get } from 'svelte/store'; -import { WebSocketMessageType, ws } from '../stores/websocketStore'; +import { ws } from '../stores/websocketStore'; +import { WebSocketMessageType } from '../types/websocket'; import { WebRTCPacketType, type KeyStore, type WebRTCPeerCallbacks } from '../types/webrtc'; import { clientKeyConfig } from '../shared/keyConfig'; +import { browser } from '$app/environment'; export class WebRTCPeer { private peer: RTCPeerConnection | null = null; @@ -38,6 +40,8 @@ export class WebRTCPeer { } public async initialize() { + if (!browser) throw new Error("Cannot initialize WebRTCPeer in non-browser environment"); + // dont initialize twice if (this.peer) return; @@ -115,14 +119,9 @@ export class WebRTCPeer { console.log("Received key exchange", data.buffer); - const textDecoder = new TextDecoder(); - const jsonKey = JSON.parse(textDecoder.decode(data)); - - console.log("Received key exchange", jsonKey); - this.keys.peersPublicKey = await window.crypto.subtle.importKey( - "jwk", - jsonKey, + "spki", + data.buffer, clientKeyConfig, true, ["wrapKey"], @@ -276,14 +275,12 @@ export class WebRTCPeer { console.log("exporting key", this.keys.localKeys.publicKey); - const exported = await window.crypto.subtle.exportKey("jwk", this.keys.localKeys.publicKey); + const exported = await window.crypto.subtle.exportKey("spki", this.keys.localKeys.publicKey); // convert exported key to a string then pack that sting into an array buffer - const exportedKeyBuffer = new TextEncoder().encode(JSON.stringify(exported)); + console.log("exported key buffer", exported); - console.log("exported key buffer", exportedKeyBuffer); - - this.send(exportedKeyBuffer.buffer, WebRTCPacketType.KEY_EXCHANGE); + this.send(exported, WebRTCPacketType.KEY_EXCHANGE); } private async encrypt(data: Uint8Array, key: CryptoKey, iv: Uint8Array): Promise { diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 9cba825..07409f8 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,12 +1,12 @@ - + {@render children?.()} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 77488d6..8b02617 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,9 +1,6 @@