// TODO: server // Peer mssages ✅ // Routing ✅ // Video files being fully sent ❓ // Use Deno static serving for static ✅ // Use Workers, at least for serving static files. Why not nginx? Single binary server. import { serveDir } from "jsr:@std/http/file-server" const memoryCache = false; const memoryResponseMap: Map = new Map(); // deno-lint-ignore-file prefer-const no-explicit-any async function serveFile(filename: string) { // console.log(filename) if (!memoryCache) { const file = await Deno.readFile("../" + filename); const newResponse = new Response(file); if (filename.endsWith('.js')) { newResponse.headers.set('content-type', 'application/javascript') } return newResponse; } const response = memoryResponseMap.get(filename); if (response) { return response.clone(); } const file = await Deno.readFile("../" + filename); const newResponse = new Response(file); if (filename.endsWith('.js')) { newResponse.headers.set('content-type', 'application/javascript') } console.log(`Caching: ${filename}`); memoryResponseMap.set(filename, newResponse); return newResponse.clone(); } function hashIdToNumber(id: string, range: number) { let number = 0; let hash = 0x811c9dc5 for (let char of id) { if (char !== '0' && char !== '-') { hash ^= char.charCodeAt(0); hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24); } } return (hash >>> 0) % range; } const colors = [ 160, 196, 202, 208, 214, 220, 226, 190, 154, 118, 82, 46, 47, 48, 49, 51, 45, 44, 43, 42, 41, 40, 39, 33, 27, 21, 57, 93, 129, 165, 201, ]; const resetCode = "\x1b[0m"; function colorID(id: string) { if (typeof id !== 'string') { console.error(`colorID: expected string but got `, id); return ""; } const colorCode = `\x1b[38;5;${colors[hashIdToNumber(id, colors.length)]}m` return `${colorCode}${id.substring(0, 5)}${resetCode}` } function pingHandler(m: any) { let time = Temporal.Now.zonedDateTimeISO(); // console.log("ping", m); console.log(time, `ping handler ${colorID(m.peer_id)}:${m.peer_name} ${colorID(m.user_id)}:${m.user_name}`); return '{"type":"pong"}' } interface HelloMessage { type: string user_id: string user_name: string peer_id: string peer_name: string known_users: string[] } const userPeers: Map> = new Map(); const peerSockets: Map = new Map(); const socketPeers: Map = new Map(); function helloHandler(m: HelloMessage, socket: WebSocket) { console.log(`Received hello from peer ${colorID(m.peer_id)}:${m.peer_name}, user ${colorID(m.user_id)}:${m.user_name}`); if (!userPeers.has(m.user_id)) { userPeers.set(m.user_id, new Set()); } userPeers.get(m.user_id)?.add(m.peer_id); peerSockets.set(m.peer_id, socket); socketPeers.set(socket, m.peer_id); if (Symbol.iterator in Object(m.known_users)) { for (const knownUserID of m.known_users) { console.log(`Adding user ${knownUserID} from peer ${colorID(m.peer_id)}`); if (!userPeers.get(knownUserID)) { userPeers.set(knownUserID, new Set()); } userPeers.get(knownUserID)?.add(m.peer_id); } } let returnValue: any = {}; for (let key of userPeers.keys()) { let peers = userPeers.get(key); if (!peers || peers.size === 0) { continue; } returnValue[key] = [...peers.keys()]; } // console.log(returnValue); return JSON.stringify({ type: 'hello', userPeers: returnValue }); } interface InnerMessage { type: string user_id: string } interface PeerMessage { type: string from: string from_username: string from_peername: string to: string message: InnerMessage } function peerMessageHandler(m: PeerMessage, _socket: WebSocket) { console.log(`pm:${m.message.type} f:${colorID(m.from)}:${m.from_peername}:${m.from_username} t:${colorID(m.to)}`) const toPeer = peerSockets.get(m.to); if (!toPeer) { console.log(`Couln't find peer ${m.to}`) return null; } if (toPeer.readyState !== WebSocket.OPEN) { console.log("Peer socket is not open:", toPeer); deletePeerFromUserPeers(m.to); return null; } const messageToSend = JSON.stringify(m); // console.log("ws->", toPeer, messageToSend); toPeer.send(messageToSend) return null; } const messageDispatch: Map string | null> = new Map(); function deletePeerFromUserPeers(peerIDToDelete: string) { for (let [userID, peers] of userPeers.entries()) { for (let peerID of peers) { if (peerID === peerIDToDelete) { peers.delete(peerIDToDelete); } } } } function connectWebsocket(request: Request) { if (request.headers.get("upgrade") != "websocket") { return new Response(null, { status: 501 }); } const { socket, response } = Deno.upgradeWebSocket(request); socket.addEventListener("open", (event) => { console.log("New peer websocket connection"); }); socket.addEventListener("message", (event) => { // console.log(event); let message: any; try { message = JSON.parse(event.data); } catch (e) { console.error("socket.message: ", e); return null; } const dispatchHandler = messageDispatch.get(message?.type) if (!dispatchHandler) { console.log("Got message I don't understand: ", event.data); return; } const response = dispatchHandler(message, socket); // console.log(response); if (response) { socket.send(response); } }); socket.addEventListener("close", (event: CloseEvent) => { let peerID = socketPeers.get(socket); if (!peerID) { console.log("Websocket close: couldn't find peer 🤔"); return; } console.log("Websocket close:", colorID(peerID), `code:${event.code} reason:${event.reason} wasClean: ${event.wasClean}`); peerSockets.delete(peerID); deletePeerFromUserPeers(peerID); }); return response; } function handler(request: Request, info: any) { if (request.url === "https://ddln.app/") { return serveFile("/static/index.html") } console.log(info.remoteAddr.hostname, request.url, request.headers.get('user-agent')); const url = new URL(request.url); if (url.pathname.endsWith('mp4') || url.pathname.endsWith('webm')) { console.log("Not serving video..."); return new Response("Not serving video", { status: 404 }); } if (url.pathname === "/") { return serveFile("/static/index.html") } if (url.pathname === "/ws") { return connectWebsocket(request); } if (url.pathname === "/sw.js") { return serveFile("static/sw.js") } if (url.pathname === "/robots.txt") { return serveFile("static/robots.txt") } if (url.pathname === "/favicon.ico") { return serveFile("static/favicon.ico") } if (url.pathname.includes("/static/")) { return serveFile(url.pathname); // return serveDir(request, { fsRoot: "../" }); } return serveFile("/static/index.html") } function main() { messageDispatch.set('ping', pingHandler); messageDispatch.set('hello', helloHandler); messageDispatch.set('peer_message', peerMessageHandler); Deno.serve({ port: 6789, cert: Deno.readTextFileSync("/etc/letsencrypt/live/ddlion.net/fullchain.pem"), key: Deno.readTextFileSync("/etc/letsencrypt/live/ddlion.net/privkey.pem"), }, handler); } main();