proof of work, bug fixes, reorg, more
This commit is contained in:
8
bun.lock
8
bun.lock
@@ -5,13 +5,13 @@
|
|||||||
"name": "wormhole",
|
"name": "wormhole",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hpke/chacha20poly1305": "^1.7.1",
|
"@hpke/chacha20poly1305": "^1.7.1",
|
||||||
|
"@hpke/core": "^1.7.4",
|
||||||
"@hpke/hybridkem-x-wing": "^0.6.1",
|
"@hpke/hybridkem-x-wing": "^0.6.1",
|
||||||
"@noble/ciphers": "^1.3.0",
|
"@noble/ciphers": "^1.3.0",
|
||||||
"@noble/curves": "^1.9.0",
|
"@noble/curves": "^1.9.0",
|
||||||
"@sveltejs/adapter-node": "^5.3.1",
|
"@sveltejs/adapter-node": "^5.3.1",
|
||||||
"@types/streamsaver": "^2.0.5",
|
"@types/streamsaver": "^2.0.5",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"i": "^0.3.7",
|
|
||||||
"polka": "^0.5.2",
|
"polka": "^0.5.2",
|
||||||
"streamsaver": "^2.0.6",
|
"streamsaver": "^2.0.6",
|
||||||
"ts-mls": "^1.1.0",
|
"ts-mls": "^1.1.0",
|
||||||
@@ -99,8 +99,6 @@
|
|||||||
|
|
||||||
"@hpke/hybridkem-x-wing": ["@hpke/hybridkem-x-wing@0.6.1", "", { "dependencies": { "@hpke/common": "^1.8.1", "@hpke/dhkem-x25519": "^1.6.4", "mlkem": "^2.5.0" } }, "sha512-mNdGapyHPw9gEicUlBYlWGjOpWmQyC49dEqLm5QtGZOSjIVSjSTBX/Bq2VxXNTeNdsRYIpPOalTwYbop/+4Ykw=="],
|
"@hpke/hybridkem-x-wing": ["@hpke/hybridkem-x-wing@0.6.1", "", { "dependencies": { "@hpke/common": "^1.8.1", "@hpke/dhkem-x25519": "^1.6.4", "mlkem": "^2.5.0" } }, "sha512-mNdGapyHPw9gEicUlBYlWGjOpWmQyC49dEqLm5QtGZOSjIVSjSTBX/Bq2VxXNTeNdsRYIpPOalTwYbop/+4Ykw=="],
|
||||||
|
|
||||||
"@hpke/ml-kem": ["@hpke/ml-kem@0.2.1", "", { "dependencies": { "@hpke/common": "^1.8.1", "mlkem": "^2.5.0" } }, "sha512-9Hmf2fO8W45/h2COdD8+RAaszI2dw9/Id0lwrJD7rEwFbNki7lxb1HyEnqUuHz9ipL2D3q0L6egFKZbhBwj/5A=="],
|
|
||||||
|
|
||||||
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||||
|
|
||||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
@@ -295,8 +293,6 @@
|
|||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
"i": ["i@0.3.7", "", {}, "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q=="],
|
|
||||||
|
|
||||||
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||||
|
|
||||||
"is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
|
"is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
|
||||||
@@ -409,6 +405,8 @@
|
|||||||
|
|
||||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||||
|
|
||||||
|
"yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
|
||||||
|
|
||||||
"zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="],
|
"zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="],
|
||||||
|
|
||||||
"@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
|
"@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
|
||||||
|
|||||||
@@ -25,13 +25,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hpke/chacha20poly1305": "^1.7.1",
|
"@hpke/chacha20poly1305": "^1.7.1",
|
||||||
|
"@hpke/core": "^1.7.4",
|
||||||
"@hpke/hybridkem-x-wing": "^0.6.1",
|
"@hpke/hybridkem-x-wing": "^0.6.1",
|
||||||
"@noble/ciphers": "^1.3.0",
|
"@noble/ciphers": "^1.3.0",
|
||||||
"@noble/curves": "^1.9.0",
|
"@noble/curves": "^1.9.0",
|
||||||
"@sveltejs/adapter-node": "^5.3.1",
|
"@sveltejs/adapter-node": "^5.3.1",
|
||||||
"@types/streamsaver": "^2.0.5",
|
"@types/streamsaver": "^2.0.5",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"i": "^0.3.7",
|
|
||||||
"polka": "^0.5.2",
|
"polka": "^0.5.2",
|
||||||
"streamsaver": "^2.0.6",
|
"streamsaver": "^2.0.6",
|
||||||
"ts-mls": "^1.1.0",
|
"ts-mls": "^1.1.0",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { handler } from './build/handler.js'; // Adjust path as needed
|
|||||||
import http from 'http';
|
import http from 'http';
|
||||||
import { WebSocketServer } from 'ws';
|
import { WebSocketServer } from 'ws';
|
||||||
import polka from 'polka';
|
import polka from 'polka';
|
||||||
import { confgiureWebsocketServer } from './server/websocketHandler.ts'
|
import { confgiureWebsocketServer } from './src/lib/server/websocketHandler.ts'
|
||||||
|
|
||||||
const server = http.createServer();
|
const server = http.createServer();
|
||||||
const app = polka({ server });
|
const app = polka({ server });
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { size }: { size?: number } = $$props;
|
||||||
|
if (!size) {
|
||||||
|
size = 20;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
class="animate-spin -ml-1 mr-3 h-5 w-5"
|
class="animate-spin -ml-1 mr-3"
|
||||||
|
style="width: {size}px; height: {size}px"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 481 B After Width: | Height: | Size: 638 B |
@@ -1,23 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { derived, writable, type Writable } from "svelte/store";
|
import { derived, writable, type Writable } from "svelte/store";
|
||||||
import { WebsocketConnectionState, ws } from "../stores/websocketStore";
|
import { WebsocketConnectionState, ws } from "$stores/websocketStore";
|
||||||
import {
|
import {
|
||||||
isRTCConnected,
|
isRTCConnected,
|
||||||
dataChannelReady,
|
dataChannelReady,
|
||||||
peer,
|
peer,
|
||||||
keyExchangeDone,
|
keyExchangeDone,
|
||||||
} from "../utils/webrtcUtil";
|
} from "$lib/webrtcUtil";
|
||||||
import {
|
import {
|
||||||
advertisedOffers,
|
advertisedOffers,
|
||||||
fileRequestIds,
|
fileRequestIds,
|
||||||
messages,
|
messages,
|
||||||
receivedOffers,
|
receivedOffers,
|
||||||
} from "../stores/messageStore";
|
} from "$stores/messageStore";
|
||||||
import { WebRTCPacketType } from "../types/webrtc";
|
import { WebRTCPacketType } from "$types/webrtc";
|
||||||
import { RoomConnectionState, type Room } from "../types/websocket";
|
import { RoomConnectionState, type Room } from "$types/websocket";
|
||||||
import { MessageType } from "../types/message";
|
import { MessageType } from "$types/message";
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
import { WebBuffer } from "../utils/buffer";
|
import { WebBuffer } from "../lib/buffer";
|
||||||
|
|
||||||
let inputMessage: Writable<string> = writable("");
|
let inputMessage: Writable<string> = writable("");
|
||||||
let inputFile: Writable<FileList | null | undefined> = writable(null);
|
let inputFile: Writable<FileList | null | undefined> = writable(null);
|
||||||
|
|||||||
45
src/lib/challenge.ts
Normal file
45
src/lib/challenge.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { ws } from "$stores/websocketStore";
|
||||||
|
import { WebSocketMessageType } from "$types/websocket";
|
||||||
|
import { solveChallenge } from "./powUtil";
|
||||||
|
|
||||||
|
export async function doChallenge(additionalData: string = ""): Promise<{
|
||||||
|
challenge: string;
|
||||||
|
nonce: string;
|
||||||
|
} | null> {
|
||||||
|
let roomChallenge: string | null = null;
|
||||||
|
|
||||||
|
let challengePromise = new Promise<string | null>((resolve) => {
|
||||||
|
let unsubscribe = ws.handleEvent(
|
||||||
|
WebSocketMessageType.CHALLENGE,
|
||||||
|
async (value) => {
|
||||||
|
unsubscribe();
|
||||||
|
roomChallenge = value.challenge;
|
||||||
|
resolve(
|
||||||
|
await solveChallenge(
|
||||||
|
roomChallenge,
|
||||||
|
value.difficulty,
|
||||||
|
additionalData,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.send({
|
||||||
|
type: WebSocketMessageType.REQUEST_CHALLENGE,
|
||||||
|
});
|
||||||
|
|
||||||
|
let challengeNonce = await challengePromise;
|
||||||
|
if (!challengeNonce) {
|
||||||
|
throw new Error("Could not solve challenge within max iterations");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!roomChallenge) {
|
||||||
|
throw new Error("No room challenge");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
challenge: roomChallenge,
|
||||||
|
nonce: challengeNonce,
|
||||||
|
};
|
||||||
|
}
|
||||||
25
src/lib/powUtil.ts
Normal file
25
src/lib/powUtil.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export async function hashStringSHA256(message: string): Promise<string> {
|
||||||
|
const textEncoder = new TextEncoder();
|
||||||
|
const data = textEncoder.encode(message);
|
||||||
|
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||||
|
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function solveChallenge(challenge: string, difficulty: number, additionalData: string): Promise<string | null> {
|
||||||
|
let nonce = 0;
|
||||||
|
let targetPrefix = '0'.repeat(difficulty);
|
||||||
|
let maxIterations = 1_000_000;
|
||||||
|
|
||||||
|
while (nonce < maxIterations) {
|
||||||
|
let hash = await hashStringSHA256(`${additionalData}${challenge}${nonce}`);
|
||||||
|
if (hash.startsWith(targetPrefix)) {
|
||||||
|
return nonce.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
import { Socket, WebSocketMessageType, type WebSocketMessage } from "../src/types/websocket";
|
import { Socket, WebSocketMessageType, type WebSocketMessage } from "../../types/websocket.ts";
|
||||||
import { LiveMap } from '../src/utils/liveMap.ts';
|
import { LiveMap } from '../liveMap.ts';
|
||||||
|
import { hashStringSHA256 } from "../powUtil.ts";
|
||||||
|
|
||||||
const adjectives = ['swift', 'silent', 'hidden', 'clever', 'brave', 'sharp', 'shadow', 'crimson', 'bright', 'quiet', 'loud', 'happy', 'dark', 'evil', 'good', 'intelligent', 'lovely', 'mysterious', 'peaceful', 'powerful', 'pure', 'quiet', 'shiny', 'sleepy', 'strong', 'sweet', 'tall', 'warm', 'gentle', 'kind', 'nice', 'polite', 'rough', 'rude', 'scary', 'shy', 'silly', 'smart', 'strange', 'tough', 'ugly', 'vivid', 'wicked', 'wise', 'young', 'sleepy'];
|
const adjectives = ['swift', 'silent', 'hidden', 'clever', 'brave', 'sharp', 'shadow', 'crimson', 'bright', 'quiet', 'loud', 'happy', 'dark', 'evil', 'good', 'intelligent', 'lovely', 'mysterious', 'peaceful', 'powerful', 'pure', 'quiet', 'shiny', 'sleepy', 'strong', 'sweet', 'tall', 'warm', 'gentle', 'kind', 'nice', 'polite', 'rough', 'rude', 'scary', 'shy', 'silly', 'smart', 'strange', 'tough', 'ugly', 'vivid', 'wicked', 'wise', 'young', 'sleepy'];
|
||||||
const nouns = ['fox', 'river', 'stone', 'cipher', 'link', 'comet', 'falcon', 'signal', 'anchor', 'spark', 'stone', 'comet', 'rocket', 'snake', 'snail', 'shark', 'elephant', 'cat', 'dog', 'whale', 'orca', 'cactus', 'flower', 'frog', 'toad', 'apple', 'strawberry', 'raspberry', 'lemon', 'bot', 'gopher', 'dinosaur', 'racoon', 'penguin', 'chameleon', 'atom', 'particle', 'witch', 'wizard', 'warlock', 'deer']
|
const nouns = ['fox', 'river', 'stone', 'cipher', 'link', 'comet', 'falcon', 'signal', 'anchor', 'spark', 'stone', 'comet', 'rocket', 'snake', 'snail', 'shark', 'elephant', 'cat', 'dog', 'whale', 'orca', 'cactus', 'flower', 'frog', 'toad', 'apple', 'strawberry', 'raspberry', 'lemon', 'bot', 'gopher', 'dinosaur', 'racoon', 'penguin', 'chameleon', 'atom', 'particle', 'witch', 'wizard', 'warlock', 'deer']
|
||||||
|
|
||||||
enum ErrorCode {
|
|
||||||
ROOM_NOT_FOUND,
|
|
||||||
}
|
|
||||||
|
|
||||||
const errors = {
|
const errors = {
|
||||||
MALFORMED_MESSAGE: "Invalid message",
|
MALFORMED_MESSAGE: "Invalid message",
|
||||||
|
INVALID_CHALLENGE: "Invalid challenge",
|
||||||
|
MISSING_DATA: "One or more required fields are missing",
|
||||||
ROOM_NOT_FOUND: "Room does not exist",
|
ROOM_NOT_FOUND: "Room does not exist",
|
||||||
ROOM_FULL: "Room is full",
|
ROOM_FULL: "Room is full",
|
||||||
UNKNOWN_MESSAGE_TYPE: "Unknown message type",
|
UNKNOWN_MESSAGE_TYPE: "Unknown message type",
|
||||||
@@ -106,13 +105,13 @@ async function joinRoom(roomId: string, socket: Socket, initial?: boolean): Prom
|
|||||||
// for some reason, when you filter the array when the length is 1 it stays at 1, but we *know* that if its 1
|
// 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
|
// then when this client disconnects, the room should be deleted since the room is empty
|
||||||
if (room.length === 1) {
|
if (room.length === 1) {
|
||||||
// give a 5 second grace period before deleting the room
|
// give a 60 second grace period before deleting the room
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (rooms.get(roomId)?.length === 1) {
|
if (rooms.get(roomId)?.length === 1) {
|
||||||
console.log("Room is empty, deleting");
|
console.log("Room is empty, deleting");
|
||||||
deleteRoom(roomId);
|
deleteRoom(roomId);
|
||||||
}
|
}
|
||||||
}, 5000)
|
}, 60000)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +131,39 @@ async function joinRoom(roomId: string, socket: Socket, initial?: boolean): Prom
|
|||||||
return room;
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// How many leading zeros are required to be considered solved
|
||||||
|
// In my testing, 2 seems to be too easy, and 4 seems to be too hard, so I'm going with 3
|
||||||
|
const CHALLENGE_DIFFICULTY = 3;
|
||||||
|
// challenges that have yet to be attached to a challenged request
|
||||||
|
const outstandingChallenges = new Map<string, NodeJS.Timeout>();
|
||||||
|
|
||||||
|
function generateChallenge(): string {
|
||||||
|
let challenge = Array.from(crypto.getRandomValues(new Uint8Array(32))).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
// provide 90 seconds to solve the challenge
|
||||||
|
outstandingChallenges.set(challenge, setTimeout(() => {
|
||||||
|
console.log("Challenge timed out:", challenge);
|
||||||
|
outstandingChallenges.delete(challenge);
|
||||||
|
}, 90000));
|
||||||
|
|
||||||
|
return challenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateChallenge(challenge: string, nonce: string, additionalData: string = ""): Promise<boolean> {
|
||||||
|
if (!outstandingChallenges.has(challenge)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hash = await hashStringSHA256(`${additionalData}${challenge}${nonce}`);
|
||||||
|
let result = hash.startsWith('0'.repeat(CHALLENGE_DIFFICULTY));
|
||||||
|
if (result) {
|
||||||
|
console.log("Challenge solved:", challenge);
|
||||||
|
clearTimeout(outstandingChallenges.get(challenge)!);
|
||||||
|
outstandingChallenges.delete(challenge);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function leaveRoom(roomId: string, socket: Socket): ServerRoom | undefined {
|
function leaveRoom(roomId: string, socket: Socket): ServerRoom | undefined {
|
||||||
let room = rooms.get(roomId);
|
let room = rooms.get(roomId);
|
||||||
console.log(room?.length);
|
console.log(room?.length);
|
||||||
@@ -189,10 +221,22 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("Received message:", message);
|
||||||
|
|
||||||
let room: ServerRoom | undefined = undefined;
|
let room: ServerRoom | undefined = undefined;
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case WebSocketMessageType.CREATE_ROOM:
|
case WebSocketMessageType.CREATE_ROOM:
|
||||||
|
if (!message.nonce || !message.challenge) {
|
||||||
|
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MISSING_DATA });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await validateChallenge(message.challenge, message.nonce)) {
|
||||||
|
socket.send({ type: WebSocketMessageType.ERROR, data: errors.INVALID_CHALLENGE });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// else, create a new room
|
// else, create a new room
|
||||||
try {
|
try {
|
||||||
if (message.roomName) {
|
if (message.roomName) {
|
||||||
@@ -212,8 +256,13 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case WebSocketMessageType.JOIN_ROOM:
|
case WebSocketMessageType.JOIN_ROOM:
|
||||||
if (!message.roomId) {
|
if (!message.roomId || !message.nonce || !message.challenge) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MALFORMED_MESSAGE });
|
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MISSING_DATA });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await validateChallenge(message.challenge, message.nonce, message.roomId)) {
|
||||||
|
socket.send({ type: WebSocketMessageType.ERROR, data: errors.INVALID_CHALLENGE });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,9 +286,27 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
room = await leaveRoom(message.roomId, socket);
|
room = leaveRoom(message.roomId, socket);
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case WebSocketMessageType.CHECK_ROOM_EXISTS:
|
||||||
|
if (!message.roomId || !message.nonce || !message.challenge) {
|
||||||
|
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MISSING_DATA });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await validateChallenge(message.challenge, message.nonce, message.roomId)) {
|
||||||
|
socket.send({ type: WebSocketMessageType.ERROR, data: errors.INVALID_CHALLENGE });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.send({ type: WebSocketMessageType.ROOM_STATUS, roomId: message.roomId, status: rooms.get(message.roomId) ? 'found' : 'not-found' });
|
||||||
|
break;
|
||||||
|
case WebSocketMessageType.REQUEST_CHALLENGE:
|
||||||
|
let challenge = generateChallenge();
|
||||||
|
|
||||||
|
socket.send({ type: WebSocketMessageType.CHALLENGE, challenge, difficulty: CHALLENGE_DIFFICULTY });
|
||||||
break;
|
break;
|
||||||
case WebSocketMessageType.WEBRTC_OFFER:
|
case WebSocketMessageType.WEBRTC_OFFER:
|
||||||
case WebSocketMessageType.WERTC_ANSWER:
|
case WebSocketMessageType.WERTC_ANSWER:
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import { ws } from '../stores/websocketStore';
|
import { ws } from '$stores/websocketStore';
|
||||||
import { WebSocketMessageType } from '../types/websocket';
|
import { WebSocketMessageType } from '$types/websocket';
|
||||||
import { WebRTCPacketType, type WebRTCPeerCallbacks } from '../types/webrtc';
|
import { WebRTCPacketType, type WebRTCPeerCallbacks } from '$types/webrtc';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { createApplicationMessage, createCommit, createGroup, decodeMlsMessage, defaultCapabilities, defaultLifetime, emptyPskIndex, encodeMlsMessage, generateKeyPackage, getCiphersuiteFromName, getCiphersuiteImpl, joinGroup, processPrivateMessage, type CiphersuiteImpl, type ClientState, type Credential, type KeyPackage, type PrivateKeyPackage, type Proposal } from 'ts-mls';
|
import { createApplicationMessage, createCommit, createGroup, decodeMlsMessage, defaultCapabilities, defaultLifetime, emptyPskIndex, encodeMlsMessage, generateKeyPackage, getCiphersuiteFromName, getCiphersuiteImpl, joinGroup, processPrivateMessage, type CiphersuiteImpl, type ClientState, type Credential, type KeyPackage, type PrivateKeyPackage, type Proposal } from 'ts-mls';
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { writable, get, type Writable } from "svelte/store";
|
import { writable, get, type Writable } from "svelte/store";
|
||||||
import { WebRTCPeer } from "$lib/webrtc";
|
import { WebRTCPeer } from "$lib/webrtc";
|
||||||
import { CHUNK_SIZE, WebRTCPacketType } from "../types/webrtc";
|
import { WebRTCPacketType } from "$types/webrtc";
|
||||||
import { room } from "../stores/roomStore";
|
import { room } from "$stores/roomStore";
|
||||||
import { RoomConnectionState, type Room } from "../types/websocket";
|
import { RoomConnectionState, type Room } from "$types/websocket";
|
||||||
import { advertisedOffers, fileRequestIds, messages, receivedOffers } from "../stores/messageStore";
|
import { advertisedOffers, fileRequestIds, messages, receivedOffers } from "$stores/messageStore";
|
||||||
import { MessageType, type Message } from "../types/message";
|
import { MessageType, type Message } from "$types/message";
|
||||||
import { WebSocketMessageType, type WebSocketMessage } from "../types/websocket";
|
import { WebSocketMessageType, type WebSocketMessage } from "$types/websocket";
|
||||||
import { WebBuffer } from "./buffer";
|
import { WebBuffer } from "./buffer";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
|
|
||||||
@@ -186,8 +186,10 @@ const callbacks = {
|
|||||||
if (downloadStream === undefined) {
|
if (downloadStream === undefined) {
|
||||||
window.addEventListener("pagehide", onPageHide);
|
window.addEventListener("pagehide", onPageHide);
|
||||||
window.addEventListener("beforeunload", beforeUnload);
|
window.addEventListener("beforeunload", beforeUnload);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
downloadStream = window.streamSaver.createWriteStream(file.name, { size: Number(file.size) });
|
downloadStream = window.streamSaver.createWriteStream(file.name, { size: Number(file.size) });
|
||||||
downloadWriter = downloadStream.getWriter();
|
downloadWriter = downloadStream!.getWriter();
|
||||||
}
|
}
|
||||||
|
|
||||||
await downloadWriter!.write(new Uint8Array(messageData.read()));
|
await downloadWriter!.write(new Uint8Array(messageData.read()));
|
||||||
@@ -249,6 +251,7 @@ const callbacks = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export async function handleMessage(event: MessageEvent) {
|
export async function handleMessage(event: MessageEvent) {
|
||||||
console.log("Message received:", event.data, typeof event.data);
|
console.log("Message received:", event.data, typeof event.data);
|
||||||
const message: WebSocketMessage = JSON.parse(event.data);
|
const message: WebSocketMessage = JSON.parse(event.data);
|
||||||
@@ -299,7 +302,7 @@ export async function handleMessage(event: MessageEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!get(peer)) {
|
if (!get(peer)) {
|
||||||
console.error("Unknown message type:", message.type);
|
console.debug("Unhandled message type:", message.type);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,7 +325,7 @@ export async function handleMessage(event: MessageEvent) {
|
|||||||
await get(peer)?.addIceCandidate(message.data.candidate);
|
await get(peer)?.addIceCandidate(message.data.candidate);
|
||||||
return;
|
return;
|
||||||
default:
|
default:
|
||||||
console.warn(
|
console.debug(
|
||||||
`Unknown message type: ${message.type} from ${get(room).id}`,
|
`Unknown message type: ${message.type} from ${get(room).id}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
import "../app.css";
|
import "../app.css";
|
||||||
import favicon from "$lib/assets/favicon.svg";
|
import favicon from "$lib/assets/favicon.svg";
|
||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import { WebsocketConnectionState, ws } from "../stores/websocketStore";
|
import { WebsocketConnectionState, ws } from "$stores/websocketStore";
|
||||||
import { room } from "../stores/roomStore";
|
import { room } from "$stores/roomStore";
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
ws.connect();
|
ws.connect();
|
||||||
@@ -35,6 +35,12 @@
|
|||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
href="/fonts/InstrumentSans-VariableFont_wdth,wght.woff2"
|
href="/fonts/InstrumentSans-VariableFont_wdth,wght.woff2"
|
||||||
/>
|
/>
|
||||||
|
{#if process.env.NODE_ENV !== "production"}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
|
||||||
|
<script>
|
||||||
|
eruda.init();
|
||||||
|
</script>
|
||||||
|
{/if}
|
||||||
<script
|
<script
|
||||||
src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"
|
src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"
|
||||||
></script>
|
></script>
|
||||||
@@ -52,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a
|
<a
|
||||||
href="https://github.com"
|
href="https://github.com/juls0730/noctis"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer">GitHub</a
|
rel="noopener noreferrer">GitHub</a
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ws } from "../stores/websocketStore";
|
import { WebsocketConnectionState, ws } from "$stores/websocketStore";
|
||||||
import { WebSocketMessageType } from "../types/websocket";
|
import { WebSocketMessageType } from "$types/websocket";
|
||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
import LoadingSpinner from "../components/LoadingSpinner.svelte";
|
import LoadingSpinner from "$components/LoadingSpinner.svelte";
|
||||||
|
import { doChallenge } from "$lib/challenge";
|
||||||
|
|
||||||
let roomName: Writable<string> = writable("");
|
let roomName: Writable<string> = writable("");
|
||||||
let roomLoading: Writable<boolean> = writable(false);
|
let roomLoading: Writable<boolean> = writable(false);
|
||||||
@@ -11,12 +12,20 @@
|
|||||||
roomLoading.set(true);
|
roomLoading.set(true);
|
||||||
let roomId = $roomName.trim() === "" ? undefined : $roomName.trim();
|
let roomId = $roomName.trim() === "" ? undefined : $roomName.trim();
|
||||||
|
|
||||||
|
doChallenge().then(async (challengeResult) => {
|
||||||
|
if (!challengeResult) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ws.send({
|
ws.send({
|
||||||
type: WebSocketMessageType.CREATE_ROOM,
|
type: WebSocketMessageType.CREATE_ROOM,
|
||||||
roomName: roomId,
|
roomName: roomId,
|
||||||
|
nonce: challengeResult.nonce,
|
||||||
|
challenge: challengeResult.challenge,
|
||||||
});
|
});
|
||||||
// todo: redirect to the room
|
|
||||||
console.log("Created room:", roomId);
|
console.log("Created room:", roomId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let showRoomNameInput: Writable<boolean> = writable(false);
|
let showRoomNameInput: Writable<boolean> = writable(false);
|
||||||
@@ -36,9 +45,15 @@
|
|||||||
<form class="flex flex-col gap-5" id="roomForm">
|
<form class="flex flex-col gap-5" id="roomForm">
|
||||||
<button
|
<button
|
||||||
onclick={createRoom}
|
onclick={createRoom}
|
||||||
class="py-4 px-8 text-xl font-semibold bg-accent text-[#121826] rounded-lg cursor-pointer transition-[background-color,_translate,_box-shadow] ease-out duration-200 w-full inline-flex justify-center items-center gap-2.5 hover:bg-[#00f0c8] hover:-translate-y-1 hover:shadow-md shadow-accent/20"
|
disabled={$ws.status !==
|
||||||
|
WebsocketConnectionState.CONNECTED || $roomLoading}
|
||||||
|
class="py-4 px-8 text-xl font-semibold bg-accent text-[#121826] disabled:opacity-50 disabled:cursor-not-allowed rounded-lg cursor-pointer transition-[background-color,_translate,_box-shadow] ease-out duration-200 w-full inline-flex justify-center items-center gap-2.5 hover:bg-[#00f0c8] hover:-translate-y-1 hover:shadow-md shadow-accent/20"
|
||||||
>
|
>
|
||||||
{#if $roomLoading}
|
{#if $ws.status !== WebsocketConnectionState.CONNECTED}
|
||||||
|
<span class="flex items-center"
|
||||||
|
><LoadingSpinner /> Connecting to server...</span
|
||||||
|
>
|
||||||
|
{:else if $roomLoading}
|
||||||
<span class="flex items-center"
|
<span class="flex items-center"
|
||||||
><LoadingSpinner /> Creating Room...</span
|
><LoadingSpinner /> Creating Room...</span
|
||||||
>
|
>
|
||||||
@@ -232,7 +247,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="px-20 text-center border-t border-[#21293b]">
|
<footer class="px-20 pt-3 text-center border-t border-[#21293b]">
|
||||||
<div class="max-w-6xl px-10 mx-auto">
|
<div class="max-w-6xl px-10 mx-auto">
|
||||||
<p>
|
<p>
|
||||||
© {new Date().getFullYear()} Noctis - MIT License
|
© {new Date().getFullYear()} Noctis - MIT License
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { room } from "../../stores/roomStore";
|
import { room } from "$stores/roomStore";
|
||||||
import { ws } from "../../stores/websocketStore";
|
import { WebsocketConnectionState, ws } from "$stores/websocketStore";
|
||||||
import { WebSocketMessageType } from "../../types/websocket";
|
import { WebSocketMessageType } from "$types/websocket";
|
||||||
import { dataChannelReady, error } from "../../utils/webrtcUtil";
|
import { dataChannelReady, error } from "$lib/webrtcUtil";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import RtcMessage from "../../components/RTCMessage.svelte";
|
import RtcMessage from "$components/RTCMessage.svelte";
|
||||||
|
import { page } from "$app/state";
|
||||||
|
import LoadingSpinner from "$components/LoadingSpinner.svelte";
|
||||||
|
import { hashStringSHA256, solveChallenge } from "$lib/powUtil";
|
||||||
|
import { doChallenge } from "$lib/challenge";
|
||||||
|
const { roomId } = page.params;
|
||||||
|
|
||||||
let isHost = $room.host === true;
|
let isHost = $derived($room.host === true);
|
||||||
|
let roomExists: boolean | undefined = $state(undefined);
|
||||||
|
|
||||||
let awaitingJoinConfirmation = !isHost;
|
let awaitingJoinConfirmation = $derived(!isHost);
|
||||||
let roomLink = "";
|
let roomLink = $state("");
|
||||||
let copyButtonText = "Copy Link";
|
let copyButtonText = $state("Copy Link");
|
||||||
export let data: { roomId: string };
|
|
||||||
const { roomId } = data;
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
error.set(null);
|
error.set(null);
|
||||||
@@ -30,12 +34,23 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConfirmJoin() {
|
async function handleConfirmJoin() {
|
||||||
awaitingJoinConfirmation = false;
|
awaitingJoinConfirmation = false;
|
||||||
|
|
||||||
|
if (!roomId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let challengeResult = await doChallenge(roomId);
|
||||||
|
if (!challengeResult) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ws.send({
|
ws.send({
|
||||||
type: WebSocketMessageType.JOIN_ROOM,
|
type: WebSocketMessageType.JOIN_ROOM,
|
||||||
roomId: roomId,
|
roomId: roomId!,
|
||||||
|
nonce: challengeResult.nonce,
|
||||||
|
challenge: challengeResult.challenge,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +71,42 @@
|
|||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ws.subscribe(async (newWs) => {
|
||||||
|
if (newWs.status === WebsocketConnectionState.CONNECTED) {
|
||||||
|
if (!awaitingJoinConfirmation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!roomId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let challengeResult = await doChallenge(roomId);
|
||||||
|
|
||||||
|
if (challengeResult) {
|
||||||
|
let unsubscribe = ws.handleEvent(
|
||||||
|
WebSocketMessageType.ROOM_STATUS,
|
||||||
|
(value) => {
|
||||||
|
if (value.status === "found") {
|
||||||
|
unsubscribe();
|
||||||
|
roomExists = true;
|
||||||
|
} else if (value.status === "not-found") {
|
||||||
|
unsubscribe();
|
||||||
|
roomExists = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ws.send({
|
||||||
|
type: WebSocketMessageType.CHECK_ROOM_EXISTS,
|
||||||
|
roomId: roomId,
|
||||||
|
nonce: challengeResult.nonce,
|
||||||
|
challenge: challengeResult.challenge,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="max-w-6xl px-5 mx-auto flex flex-col items-center">
|
<div class="max-w-6xl px-5 mx-auto flex flex-col items-center">
|
||||||
@@ -97,6 +148,23 @@
|
|||||||
<RtcMessage {room} />
|
<RtcMessage {room} />
|
||||||
{/if}
|
{/if}
|
||||||
{:else if awaitingJoinConfirmation}
|
{:else if awaitingJoinConfirmation}
|
||||||
|
{#if $ws.status !== WebsocketConnectionState.CONNECTED || roomExists === undefined}
|
||||||
|
<h2 class="text-3xl font-bold text-white mb-2">
|
||||||
|
<span class="flex items-center"
|
||||||
|
><LoadingSpinner size="24" /> Connecting to server...</span
|
||||||
|
>
|
||||||
|
</h2>
|
||||||
|
<p class="!text-paragraph">
|
||||||
|
click <a href="/">here</a> to go back to the homepage
|
||||||
|
</p>
|
||||||
|
{:else if roomExists === false}
|
||||||
|
<h2 class="text-3xl font-bold text-white mb-2">
|
||||||
|
That room does not exist.
|
||||||
|
</h2>
|
||||||
|
<p class="!text-paragraph">
|
||||||
|
click <a href="/">here</a> to go back to the homepage
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
<h2 class="text-3xl font-bold text-white mb-2">
|
<h2 class="text-3xl font-bold text-white mb-2">
|
||||||
You're invited to chat.
|
You're invited to chat.
|
||||||
</h2>
|
</h2>
|
||||||
@@ -114,6 +182,7 @@
|
|||||||
Decline
|
Decline
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<RtcMessage {room} />
|
<RtcMessage {room} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
import type { Message } from "../types/message";
|
import type { Message } from "$types/message";
|
||||||
|
|
||||||
export let messages: Writable<Message[]> = writable([]);
|
export let messages: Writable<Message[]> = writable([]);
|
||||||
export let advertisedOffers = writable(new Map<bigint, File>());
|
export let advertisedOffers = writable(new Map<bigint, File>());
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { writable, type Writable } from 'svelte/store';
|
import { writable, type Writable } from 'svelte/store';
|
||||||
import { RoomConnectionState } from '../types/websocket';
|
import { RoomConnectionState } from '$types/websocket';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
export interface Room {
|
export interface Room {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { Socket, type WebSocketMessage } from '../types/websocket';
|
import { Socket, WebSocketMessageType, type WebSocketMessage } from '$types/websocket';
|
||||||
import { handleMessage } from '../utils/webrtcUtil';
|
import { handleMessage } from '../lib/webrtcUtil';
|
||||||
|
|
||||||
export enum WebsocketConnectionState {
|
export enum WebsocketConnectionState {
|
||||||
DISCONNECTED,
|
DISCONNECTED,
|
||||||
@@ -21,6 +21,7 @@ interface WebSocketStore extends Readable<WebSocketStoreValue> {
|
|||||||
connect: () => void;
|
connect: () => void;
|
||||||
disconnect: () => void;
|
disconnect: () => void;
|
||||||
send: (message: WebSocketMessage) => void;
|
send: (message: WebSocketMessage) => void;
|
||||||
|
handleEvent<T extends WebSocketMessageType>(messageType: T, func: (message: WebSocketMessage & { type: T }) => void): () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: handle reconnection logic to room elsewhere (not implemented here)
|
// TODO: handle reconnection logic to room elsewhere (not implemented here)
|
||||||
@@ -94,11 +95,23 @@ function createWebSocketStore(messageHandler: MessageHandler): WebSocketStore {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function handleEvent<T extends WebSocketMessageType>(messageType: T, func: (message: WebSocketMessage & { type: T }) => void) {
|
||||||
|
let socket = get({ subscribe }).socket;
|
||||||
|
if (!socket) {
|
||||||
|
return () => { };
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: why does the typescript compiler think this is invalid?
|
||||||
|
return socket.handleEvent<T>(messageType, func)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
connect,
|
connect,
|
||||||
disconnect,
|
disconnect,
|
||||||
send,
|
send,
|
||||||
|
handleEvent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,16 @@ export enum WebSocketMessageType {
|
|||||||
CREATE_ROOM = "create",
|
CREATE_ROOM = "create",
|
||||||
JOIN_ROOM = "join",
|
JOIN_ROOM = "join",
|
||||||
LEAVE_ROOM = "leave",
|
LEAVE_ROOM = "leave",
|
||||||
|
CHECK_ROOM_EXISTS = "check",
|
||||||
|
REQUEST_CHALLENGE = "request-challenge",
|
||||||
|
|
||||||
// response messages
|
// response messages
|
||||||
ROOM_CREATED = "created",
|
ROOM_CREATED = "created",
|
||||||
ROOM_JOINED = "joined",
|
ROOM_JOINED = "joined",
|
||||||
ROOM_LEFT = "left",
|
ROOM_LEFT = "left",
|
||||||
ROOM_READY = "ready",
|
ROOM_READY = "ready",
|
||||||
|
ROOM_STATUS = "status",
|
||||||
|
CHALLENGE = "challenge",
|
||||||
|
|
||||||
// webrtc messages
|
// webrtc messages
|
||||||
WEBRTC_OFFER = "offer",
|
WEBRTC_OFFER = "offer",
|
||||||
@@ -31,14 +35,19 @@ export enum WebSocketMessageType {
|
|||||||
ERROR = "error",
|
ERROR = "error",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: name the interfaces better
|
||||||
export type WebSocketMessage =
|
export type WebSocketMessage =
|
||||||
| CreateRoomMessage
|
| CreateRoomMessage
|
||||||
| JoinRoomMessage
|
| JoinRoomMessage
|
||||||
| LeaveRoomMessage
|
| LeaveRoomMessage
|
||||||
|
| CheckRoomExistsMessage
|
||||||
|
| RequestChallengeMessage
|
||||||
| RoomCreatedMessage
|
| RoomCreatedMessage
|
||||||
| RoomJoinedMessage
|
| RoomJoinedMessage
|
||||||
| RoomLeftMessage
|
| RoomLeftMessage
|
||||||
|
| RoomStatusMessage
|
||||||
| RoomReadyMessage
|
| RoomReadyMessage
|
||||||
|
| ChallengeMessage
|
||||||
| OfferMessage
|
| OfferMessage
|
||||||
| AnswerMessage
|
| AnswerMessage
|
||||||
| IceCandidateMessage
|
| IceCandidateMessage
|
||||||
@@ -49,14 +58,20 @@ interface ErrorMessage {
|
|||||||
data: string;
|
data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====== Query Messages ======
|
||||||
interface CreateRoomMessage {
|
interface CreateRoomMessage {
|
||||||
type: WebSocketMessageType.CREATE_ROOM;
|
type: WebSocketMessageType.CREATE_ROOM;
|
||||||
roomName?: string;
|
roomName?: string;
|
||||||
|
nonce: string;
|
||||||
|
challenge: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: this is used as a query message, but it's also used as a response message
|
||||||
interface JoinRoomMessage {
|
interface JoinRoomMessage {
|
||||||
type: WebSocketMessageType.JOIN_ROOM;
|
type: WebSocketMessageType.JOIN_ROOM;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
nonce?: string;
|
||||||
|
challenge?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LeaveRoomMessage {
|
interface LeaveRoomMessage {
|
||||||
@@ -64,6 +79,20 @@ interface LeaveRoomMessage {
|
|||||||
roomId: string;
|
roomId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CheckRoomExistsMessage {
|
||||||
|
type: WebSocketMessageType.CHECK_ROOM_EXISTS;
|
||||||
|
// if sha256(roomId + challenge + nonce) has a certain number of leading zeros, then we can give the status to the user
|
||||||
|
roomId: string;
|
||||||
|
nonce: string;
|
||||||
|
challenge: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestChallengeMessage {
|
||||||
|
type: WebSocketMessageType.REQUEST_CHALLENGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== Response Messages ======
|
||||||
|
|
||||||
interface RoomCreatedMessage {
|
interface RoomCreatedMessage {
|
||||||
type: WebSocketMessageType.ROOM_CREATED;
|
type: WebSocketMessageType.ROOM_CREATED;
|
||||||
data: string;
|
data: string;
|
||||||
@@ -80,6 +109,12 @@ interface RoomLeftMessage {
|
|||||||
roomId: string;
|
roomId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RoomStatusMessage {
|
||||||
|
type: WebSocketMessageType.ROOM_STATUS;
|
||||||
|
roomId: string;
|
||||||
|
status: 'found' | 'not-found';
|
||||||
|
}
|
||||||
|
|
||||||
interface RoomReadyMessage {
|
interface RoomReadyMessage {
|
||||||
type: WebSocketMessageType.ROOM_READY;
|
type: WebSocketMessageType.ROOM_READY;
|
||||||
data: {
|
data: {
|
||||||
@@ -87,6 +122,14 @@ interface RoomReadyMessage {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ChallengeMessage {
|
||||||
|
type: WebSocketMessageType.CHALLENGE;
|
||||||
|
challenge: string;
|
||||||
|
difficulty: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== WebRTC signaling messages ======
|
||||||
|
// as the server, we dont do anything with these messages other than relay them to the other peers in the room
|
||||||
interface OfferMessage {
|
interface OfferMessage {
|
||||||
type: WebSocketMessageType.WEBRTC_OFFER;
|
type: WebSocketMessageType.WEBRTC_OFFER;
|
||||||
data: {
|
data: {
|
||||||
@@ -122,6 +165,9 @@ export class Socket {
|
|||||||
public addEventListener: typeof WebSocket.prototype.addEventListener;
|
public addEventListener: typeof WebSocket.prototype.addEventListener;
|
||||||
public removeEventListener: typeof WebSocket.prototype.removeEventListener;
|
public removeEventListener: typeof WebSocket.prototype.removeEventListener;
|
||||||
public close: typeof WebSocket.prototype.close;
|
public close: typeof WebSocket.prototype.close;
|
||||||
|
// maps WebSocketMessageType to an array of functions that handle that message
|
||||||
|
// this allows for consumbers to subscribe to a specific message type and handle it themselves
|
||||||
|
private functionStack: Map<WebSocketMessageType, Function[]>;
|
||||||
|
|
||||||
constructor(webSocket: WebSocket) {
|
constructor(webSocket: WebSocket) {
|
||||||
this.ws = webSocket;
|
this.ws = webSocket;
|
||||||
@@ -130,6 +176,18 @@ export class Socket {
|
|||||||
console.log("WebSocket opened");
|
console.log("WebSocket opened");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.functionStack = new Map();
|
||||||
|
|
||||||
|
this.ws.addEventListener("message", async (event) => {
|
||||||
|
console.log("WebSocket received message:", event.data);
|
||||||
|
const message: WebSocketMessage = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (this.functionStack.has(message.type)) {
|
||||||
|
for (let func of this.functionStack.get(message.type)!) {
|
||||||
|
func(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.addEventListener = this.ws.addEventListener.bind(this.ws);
|
this.addEventListener = this.ws.addEventListener.bind(this.ws);
|
||||||
this.removeEventListener = this.ws.removeEventListener.bind(this.ws);
|
this.removeEventListener = this.ws.removeEventListener.bind(this.ws);
|
||||||
@@ -145,4 +203,15 @@ export class Socket {
|
|||||||
|
|
||||||
this.ws.send(JSON.stringify(message));
|
this.ws.send(JSON.stringify(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public handleEvent<T extends WebSocketMessageType>(messageType: T, func: (message: WebSocketMessage & { type: T }) => void): () => void {
|
||||||
|
if (!this.functionStack.has(messageType)) {
|
||||||
|
this.functionStack.set(messageType, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.functionStack.get(messageType)!.push(func);
|
||||||
|
return () => {
|
||||||
|
this.functionStack.get(messageType)!.splice(this.functionStack.get(messageType)!.indexOf(func), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
import { confgiureWebsocketServer } from '../server/websocketHandler.ts';
|
import { confgiureWebsocketServer } from './lib/server/websocketHandler.ts';
|
||||||
|
|
||||||
import type { ViteDevServer } from "vite";
|
import type { ViteDevServer } from "vite";
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,13 @@ const config = {
|
|||||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||||
adapter: adapter()
|
adapter: adapter(),
|
||||||
|
alias: {
|
||||||
|
$stores: './src/stores',
|
||||||
|
$components: './src/components',
|
||||||
|
$types: './src/types',
|
||||||
|
'$lib/server': './src/lib/server',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
experimental: {
|
experimental: {
|
||||||
|
|||||||
Reference in New Issue
Block a user