proof of work, bug fixes, reorg, more
This commit is contained in:
8
bun.lock
8
bun.lock
@@ -5,13 +5,13 @@
|
||||
"name": "wormhole",
|
||||
"dependencies": {
|
||||
"@hpke/chacha20poly1305": "^1.7.1",
|
||||
"@hpke/core": "^1.7.4",
|
||||
"@hpke/hybridkem-x-wing": "^0.6.1",
|
||||
"@noble/ciphers": "^1.3.0",
|
||||
"@noble/curves": "^1.9.0",
|
||||
"@sveltejs/adapter-node": "^5.3.1",
|
||||
"@types/streamsaver": "^2.0.5",
|
||||
"@types/ws": "^8.18.1",
|
||||
"i": "^0.3.7",
|
||||
"polka": "^0.5.2",
|
||||
"streamsaver": "^2.0.6",
|
||||
"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/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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"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-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=="],
|
||||
|
||||
"yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
|
||||
|
||||
"zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="],
|
||||
|
||||
"@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hpke/chacha20poly1305": "^1.7.1",
|
||||
"@hpke/core": "^1.7.4",
|
||||
"@hpke/hybridkem-x-wing": "^0.6.1",
|
||||
"@noble/ciphers": "^1.3.0",
|
||||
"@noble/curves": "^1.9.0",
|
||||
"@sveltejs/adapter-node": "^5.3.1",
|
||||
"@types/streamsaver": "^2.0.5",
|
||||
"@types/ws": "^8.18.1",
|
||||
"i": "^0.3.7",
|
||||
"polka": "^0.5.2",
|
||||
"streamsaver": "^2.0.6",
|
||||
"ts-mls": "^1.1.0",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { handler } from './build/handler.js'; // Adjust path as needed
|
||||
import http from 'http';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import polka from 'polka';
|
||||
import { confgiureWebsocketServer } from './server/websocketHandler.ts'
|
||||
import { confgiureWebsocketServer } from './src/lib/server/websocketHandler.ts'
|
||||
|
||||
const server = http.createServer();
|
||||
const app = polka({ server });
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<script lang="ts">
|
||||
let { size }: { size?: number } = $$props;
|
||||
if (!size) {
|
||||
size = 20;
|
||||
}
|
||||
</script>
|
||||
|
||||
<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"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
|
Before Width: | Height: | Size: 481 B After Width: | Height: | Size: 638 B |
@@ -1,23 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { derived, writable, type Writable } from "svelte/store";
|
||||
import { WebsocketConnectionState, ws } from "../stores/websocketStore";
|
||||
import { WebsocketConnectionState, ws } from "$stores/websocketStore";
|
||||
import {
|
||||
isRTCConnected,
|
||||
dataChannelReady,
|
||||
peer,
|
||||
keyExchangeDone,
|
||||
} from "../utils/webrtcUtil";
|
||||
} from "$lib/webrtcUtil";
|
||||
import {
|
||||
advertisedOffers,
|
||||
fileRequestIds,
|
||||
messages,
|
||||
receivedOffers,
|
||||
} from "../stores/messageStore";
|
||||
import { WebRTCPacketType } from "../types/webrtc";
|
||||
import { RoomConnectionState, type Room } from "../types/websocket";
|
||||
import { MessageType } from "../types/message";
|
||||
} from "$stores/messageStore";
|
||||
import { WebRTCPacketType } from "$types/webrtc";
|
||||
import { RoomConnectionState, type Room } from "$types/websocket";
|
||||
import { MessageType } from "$types/message";
|
||||
import { fade } from "svelte/transition";
|
||||
import { WebBuffer } from "../utils/buffer";
|
||||
import { WebBuffer } from "../lib/buffer";
|
||||
|
||||
let inputMessage: Writable<string> = writable("");
|
||||
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 { Socket, WebSocketMessageType, type WebSocketMessage } from "../src/types/websocket";
|
||||
import { LiveMap } from '../src/utils/liveMap.ts';
|
||||
import { Socket, WebSocketMessageType, type WebSocketMessage } from "../../types/websocket.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 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 = {
|
||||
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_FULL: "Room is full",
|
||||
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
|
||||
// then when this client disconnects, the room should be deleted since the room is empty
|
||||
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(() => {
|
||||
if (rooms.get(roomId)?.length === 1) {
|
||||
console.log("Room is empty, deleting");
|
||||
deleteRoom(roomId);
|
||||
}
|
||||
}, 5000)
|
||||
}, 60000)
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -132,6 +131,39 @@ async function joinRoom(roomId: string, socket: Socket, initial?: boolean): Prom
|
||||
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 {
|
||||
let room = rooms.get(roomId);
|
||||
console.log(room?.length);
|
||||
@@ -189,10 +221,22 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Received message:", message);
|
||||
|
||||
let room: ServerRoom | undefined = undefined;
|
||||
|
||||
switch (message.type) {
|
||||
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
|
||||
try {
|
||||
if (message.roomName) {
|
||||
@@ -212,8 +256,13 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
|
||||
}
|
||||
break;
|
||||
case WebSocketMessageType.JOIN_ROOM:
|
||||
if (!message.roomId) {
|
||||
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MALFORMED_MESSAGE });
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -237,9 +286,27 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
room = await leaveRoom(message.roomId, socket);
|
||||
room = leaveRoom(message.roomId, socket);
|
||||
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;
|
||||
case WebSocketMessageType.WEBRTC_OFFER:
|
||||
case WebSocketMessageType.WERTC_ANSWER:
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import { ws } from '../stores/websocketStore';
|
||||
import { WebSocketMessageType } from '../types/websocket';
|
||||
import { WebRTCPacketType, type WebRTCPeerCallbacks } from '../types/webrtc';
|
||||
import { ws } from '$stores/websocketStore';
|
||||
import { WebSocketMessageType } from '$types/websocket';
|
||||
import { WebRTCPacketType, type WebRTCPeerCallbacks } from '$types/webrtc';
|
||||
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';
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { writable, get, type Writable } from "svelte/store";
|
||||
import { WebRTCPeer } from "$lib/webrtc";
|
||||
import { CHUNK_SIZE, WebRTCPacketType } from "../types/webrtc";
|
||||
import { room } from "../stores/roomStore";
|
||||
import { RoomConnectionState, type Room } from "../types/websocket";
|
||||
import { advertisedOffers, fileRequestIds, messages, receivedOffers } from "../stores/messageStore";
|
||||
import { MessageType, type Message } from "../types/message";
|
||||
import { WebSocketMessageType, type WebSocketMessage } from "../types/websocket";
|
||||
import { WebRTCPacketType } from "$types/webrtc";
|
||||
import { room } from "$stores/roomStore";
|
||||
import { RoomConnectionState, type Room } from "$types/websocket";
|
||||
import { advertisedOffers, fileRequestIds, messages, receivedOffers } from "$stores/messageStore";
|
||||
import { MessageType, type Message } from "$types/message";
|
||||
import { WebSocketMessageType, type WebSocketMessage } from "$types/websocket";
|
||||
import { WebBuffer } from "./buffer";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
@@ -186,8 +186,10 @@ const callbacks = {
|
||||
if (downloadStream === undefined) {
|
||||
window.addEventListener("pagehide", onPageHide);
|
||||
window.addEventListener("beforeunload", beforeUnload);
|
||||
|
||||
// @ts-ignore
|
||||
downloadStream = window.streamSaver.createWriteStream(file.name, { size: Number(file.size) });
|
||||
downloadWriter = downloadStream.getWriter();
|
||||
downloadWriter = downloadStream!.getWriter();
|
||||
}
|
||||
|
||||
await downloadWriter!.write(new Uint8Array(messageData.read()));
|
||||
@@ -249,6 +251,7 @@ const callbacks = {
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export async function handleMessage(event: MessageEvent) {
|
||||
console.log("Message received:", event.data, typeof event.data);
|
||||
const message: WebSocketMessage = JSON.parse(event.data);
|
||||
@@ -299,7 +302,7 @@ export async function handleMessage(event: MessageEvent) {
|
||||
}
|
||||
|
||||
if (!get(peer)) {
|
||||
console.error("Unknown message type:", message.type);
|
||||
console.debug("Unhandled message type:", message.type);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -322,7 +325,7 @@ export async function handleMessage(event: MessageEvent) {
|
||||
await get(peer)?.addIceCandidate(message.data.candidate);
|
||||
return;
|
||||
default:
|
||||
console.warn(
|
||||
console.debug(
|
||||
`Unknown message type: ${message.type} from ${get(room).id}`,
|
||||
);
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
import "../app.css";
|
||||
import favicon from "$lib/assets/favicon.svg";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { WebsocketConnectionState, ws } from "../stores/websocketStore";
|
||||
import { room } from "../stores/roomStore";
|
||||
import { WebsocketConnectionState, ws } from "$stores/websocketStore";
|
||||
import { room } from "$stores/roomStore";
|
||||
|
||||
onMount(() => {
|
||||
ws.connect();
|
||||
@@ -35,6 +35,12 @@
|
||||
crossorigin="anonymous"
|
||||
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
|
||||
src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"
|
||||
></script>
|
||||
@@ -52,7 +58,7 @@
|
||||
</div>
|
||||
<nav>
|
||||
<a
|
||||
href="https://github.com"
|
||||
href="https://github.com/juls0730/noctis"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">GitHub</a
|
||||
>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { ws } from "../stores/websocketStore";
|
||||
import { WebSocketMessageType } from "../types/websocket";
|
||||
import { WebsocketConnectionState, ws } from "$stores/websocketStore";
|
||||
import { WebSocketMessageType } from "$types/websocket";
|
||||
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 roomLoading: Writable<boolean> = writable(false);
|
||||
@@ -11,12 +12,20 @@
|
||||
roomLoading.set(true);
|
||||
let roomId = $roomName.trim() === "" ? undefined : $roomName.trim();
|
||||
|
||||
doChallenge().then(async (challengeResult) => {
|
||||
if (!challengeResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
ws.send({
|
||||
type: WebSocketMessageType.CREATE_ROOM,
|
||||
roomName: roomId,
|
||||
nonce: challengeResult.nonce,
|
||||
challenge: challengeResult.challenge,
|
||||
});
|
||||
// todo: redirect to the room
|
||||
|
||||
console.log("Created room:", roomId);
|
||||
});
|
||||
}
|
||||
|
||||
let showRoomNameInput: Writable<boolean> = writable(false);
|
||||
@@ -36,9 +45,15 @@
|
||||
<form class="flex flex-col gap-5" id="roomForm">
|
||||
<button
|
||||
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"
|
||||
><LoadingSpinner /> Creating Room...</span
|
||||
>
|
||||
@@ -232,7 +247,7 @@
|
||||
</div>
|
||||
</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">
|
||||
<p>
|
||||
© {new Date().getFullYear()} Noctis - MIT License
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { room } from "../../stores/roomStore";
|
||||
import { ws } from "../../stores/websocketStore";
|
||||
import { WebSocketMessageType } from "../../types/websocket";
|
||||
import { dataChannelReady, error } from "../../utils/webrtcUtil";
|
||||
import { room } from "$stores/roomStore";
|
||||
import { WebsocketConnectionState, ws } from "$stores/websocketStore";
|
||||
import { WebSocketMessageType } from "$types/websocket";
|
||||
import { dataChannelReady, error } from "$lib/webrtcUtil";
|
||||
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 roomLink = "";
|
||||
let copyButtonText = "Copy Link";
|
||||
export let data: { roomId: string };
|
||||
const { roomId } = data;
|
||||
let awaitingJoinConfirmation = $derived(!isHost);
|
||||
let roomLink = $state("");
|
||||
let copyButtonText = $state("Copy Link");
|
||||
|
||||
onMount(() => {
|
||||
error.set(null);
|
||||
@@ -30,12 +34,23 @@
|
||||
});
|
||||
}
|
||||
|
||||
function handleConfirmJoin() {
|
||||
async function handleConfirmJoin() {
|
||||
awaitingJoinConfirmation = false;
|
||||
|
||||
if (!roomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let challengeResult = await doChallenge(roomId);
|
||||
if (!challengeResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
ws.send({
|
||||
type: WebSocketMessageType.JOIN_ROOM,
|
||||
roomId: roomId,
|
||||
roomId: roomId!,
|
||||
nonce: challengeResult.nonce,
|
||||
challenge: challengeResult.challenge,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,6 +71,42 @@
|
||||
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>
|
||||
|
||||
<div class="max-w-6xl px-5 mx-auto flex flex-col items-center">
|
||||
@@ -97,6 +148,23 @@
|
||||
<RtcMessage {room} />
|
||||
{/if}
|
||||
{: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">
|
||||
You're invited to chat.
|
||||
</h2>
|
||||
@@ -114,6 +182,7 @@
|
||||
Decline
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<RtcMessage {room} />
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 advertisedOffers = writable(new Map<bigint, File>());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import { RoomConnectionState } from '../types/websocket';
|
||||
import { RoomConnectionState } from '$types/websocket';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export interface Room {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
import { Socket, type WebSocketMessage } from '../types/websocket';
|
||||
import { handleMessage } from '../utils/webrtcUtil';
|
||||
import { Socket, WebSocketMessageType, type WebSocketMessage } from '$types/websocket';
|
||||
import { handleMessage } from '../lib/webrtcUtil';
|
||||
|
||||
export enum WebsocketConnectionState {
|
||||
DISCONNECTED,
|
||||
@@ -21,6 +21,7 @@ interface WebSocketStore extends Readable<WebSocketStoreValue> {
|
||||
connect: () => void;
|
||||
disconnect: () => 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)
|
||||
@@ -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 {
|
||||
subscribe,
|
||||
connect,
|
||||
disconnect,
|
||||
send,
|
||||
handleEvent,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,16 @@ export enum WebSocketMessageType {
|
||||
CREATE_ROOM = "create",
|
||||
JOIN_ROOM = "join",
|
||||
LEAVE_ROOM = "leave",
|
||||
CHECK_ROOM_EXISTS = "check",
|
||||
REQUEST_CHALLENGE = "request-challenge",
|
||||
|
||||
// response messages
|
||||
ROOM_CREATED = "created",
|
||||
ROOM_JOINED = "joined",
|
||||
ROOM_LEFT = "left",
|
||||
ROOM_READY = "ready",
|
||||
ROOM_STATUS = "status",
|
||||
CHALLENGE = "challenge",
|
||||
|
||||
// webrtc messages
|
||||
WEBRTC_OFFER = "offer",
|
||||
@@ -31,14 +35,19 @@ export enum WebSocketMessageType {
|
||||
ERROR = "error",
|
||||
}
|
||||
|
||||
// TODO: name the interfaces better
|
||||
export type WebSocketMessage =
|
||||
| CreateRoomMessage
|
||||
| JoinRoomMessage
|
||||
| LeaveRoomMessage
|
||||
| CheckRoomExistsMessage
|
||||
| RequestChallengeMessage
|
||||
| RoomCreatedMessage
|
||||
| RoomJoinedMessage
|
||||
| RoomLeftMessage
|
||||
| RoomStatusMessage
|
||||
| RoomReadyMessage
|
||||
| ChallengeMessage
|
||||
| OfferMessage
|
||||
| AnswerMessage
|
||||
| IceCandidateMessage
|
||||
@@ -49,14 +58,20 @@ interface ErrorMessage {
|
||||
data: string;
|
||||
}
|
||||
|
||||
// ====== Query Messages ======
|
||||
interface CreateRoomMessage {
|
||||
type: WebSocketMessageType.CREATE_ROOM;
|
||||
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 {
|
||||
type: WebSocketMessageType.JOIN_ROOM;
|
||||
roomId: string;
|
||||
nonce?: string;
|
||||
challenge?: string;
|
||||
}
|
||||
|
||||
interface LeaveRoomMessage {
|
||||
@@ -64,6 +79,20 @@ interface LeaveRoomMessage {
|
||||
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 {
|
||||
type: WebSocketMessageType.ROOM_CREATED;
|
||||
data: string;
|
||||
@@ -80,6 +109,12 @@ interface RoomLeftMessage {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
interface RoomStatusMessage {
|
||||
type: WebSocketMessageType.ROOM_STATUS;
|
||||
roomId: string;
|
||||
status: 'found' | 'not-found';
|
||||
}
|
||||
|
||||
interface RoomReadyMessage {
|
||||
type: WebSocketMessageType.ROOM_READY;
|
||||
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 {
|
||||
type: WebSocketMessageType.WEBRTC_OFFER;
|
||||
data: {
|
||||
@@ -122,6 +165,9 @@ export class Socket {
|
||||
public addEventListener: typeof WebSocket.prototype.addEventListener;
|
||||
public removeEventListener: typeof WebSocket.prototype.removeEventListener;
|
||||
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) {
|
||||
this.ws = webSocket;
|
||||
@@ -130,6 +176,18 @@ export class Socket {
|
||||
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.removeEventListener = this.ws.removeEventListener.bind(this.ws);
|
||||
@@ -145,4 +203,15 @@ export class Socket {
|
||||
|
||||
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 { confgiureWebsocketServer } from '../server/websocketHandler.ts';
|
||||
import { confgiureWebsocketServer } from './lib/server/websocketHandler.ts';
|
||||
|
||||
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.
|
||||
// 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.
|
||||
adapter: adapter()
|
||||
adapter: adapter(),
|
||||
alias: {
|
||||
$stores: './src/stores',
|
||||
$components: './src/components',
|
||||
$types: './src/types',
|
||||
'$lib/server': './src/lib/server',
|
||||
}
|
||||
},
|
||||
compilerOptions: {
|
||||
experimental: {
|
||||
|
||||
Reference in New Issue
Block a user