366 lines
10 KiB
TypeScript
366 lines
10 KiB
TypeScript
// 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"
|
|
|
|
import { brotli } from "jsr:@deno-library/compress";
|
|
|
|
const superLog=true;
|
|
|
|
const memoryCache = true;
|
|
const filepathResponseCache: Map<string, Response> = new Map();
|
|
// deno-lint-ignore-file prefer-const no-explicit-any
|
|
async function serveFile(filename: string) {
|
|
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 = filepathResponseCache.get(filename);
|
|
|
|
if (response) {
|
|
// console.log('serveFile: cache hit:', filename);
|
|
return response.clone();
|
|
}
|
|
|
|
|
|
const file = await Deno.readFile("../" + filename);
|
|
|
|
const compressed = await brotli.compress(file);
|
|
|
|
const newResponse = await new Response(compressed);
|
|
// const newResponse = await new Response(file);
|
|
|
|
newResponse.headers.set('Cache-Control', 'no-transform');
|
|
newResponse.headers.set('Content-Encoding', 'br');
|
|
|
|
|
|
if (filename.endsWith('.js')) {
|
|
newResponse.headers.set('content-type', 'application/javascript')
|
|
}
|
|
|
|
console.log(`Caching: ${filename}`);
|
|
filepathResponseCache.set(filename, newResponse);
|
|
|
|
return newResponse.clone();
|
|
}
|
|
|
|
function hashIdToNumber(id: string, range: number) {
|
|
let hash = 0x811c9dc5
|
|
for (const 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) {
|
|
const 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[]
|
|
}
|
|
|
|
interface Hello2Message {
|
|
type: string
|
|
user_id: string
|
|
user_name: string
|
|
peer_id: string
|
|
peer_name: string
|
|
is_bootstrap_peer: boolean
|
|
// peer_description:RTCSessionDescription
|
|
}
|
|
|
|
// interface PeerState {
|
|
// socket:WebSocket;
|
|
// lastSeen: number;
|
|
// }
|
|
|
|
// const peerStates:Map<string, PeerState> = new Map();
|
|
const bootstrapPeers: Set<string> = new Set();
|
|
const userPeers: Map<string, Set<string>> = new Map();
|
|
const peerSockets: Map<string, WebSocket> = new Map();
|
|
const socketPeers: Map<WebSocket, string> = new Map();
|
|
|
|
// function updatePeerState(peerID:string, socket:WebSocket) {
|
|
|
|
// }
|
|
|
|
function hello2Handler(m:Hello2Message, socket:WebSocket) {
|
|
// bootstrapPeers.add(m.)
|
|
console.log(`[>hello2]: peer ${colorID(m.peer_id)}:${m.peer_name}, user ${colorID(m.user_id)}:${m.user_name}`);
|
|
|
|
if (m.is_bootstrap_peer) {
|
|
bootstrapPeers.add(m.peer_id);
|
|
console.log(`This is a bootstrap peer. bootstrap peers:${[...bootstrapPeers.values()]}`);
|
|
}
|
|
|
|
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); // TODO:MAYBEBUG - what happens with multiple windows each with their own websocket?
|
|
socketPeers.set(socket, m.peer_id);
|
|
|
|
if (!m.is_bootstrap_peer) {
|
|
|
|
return JSON.stringify({ type: 'hello2', bootstrapPeers: [...bootstrapPeers.values()] });
|
|
}
|
|
|
|
return JSON.stringify({type:'hello2', message:'hello2 bootstrap peer!'});
|
|
|
|
}
|
|
|
|
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); // TODO:MAYBEBUG - what happens with multiple windows each with their own websocket?
|
|
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);
|
|
}
|
|
}
|
|
|
|
const returnValue: any = {};
|
|
for (const key of userPeers.keys()) {
|
|
const 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);
|
|
|
|
console.log("peerMessageHandler: before toPeer.send");
|
|
toPeer.send(messageToSend)
|
|
console.log("peerMessageHandler: after toPeer.send");
|
|
return null;
|
|
}
|
|
|
|
const messageDispatch: Map<string, (m: any, socket: WebSocket) => string | null> = new Map();
|
|
|
|
function deletePeerFromUserPeers(peerIDToDelete: string) {
|
|
for (const peers of userPeers.values()) {
|
|
for (const 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", () => {
|
|
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;
|
|
}
|
|
|
|
superLog && console.log(message);
|
|
|
|
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) => {
|
|
const 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}`);
|
|
|
|
bootstrapPeers.delete(peerID);
|
|
peerSockets.delete(peerID);
|
|
deletePeerFromUserPeers(peerID);
|
|
});
|
|
|
|
return response;
|
|
|
|
}
|
|
|
|
async function devServerWatchFiles() {
|
|
const watcher = Deno.watchFs("../static/");
|
|
for await (const event of watcher) {
|
|
if (event.kind === "modify") {
|
|
for (const path of event.paths) {
|
|
const cachedPath = path.replace(Deno.cwd() + '/..', '')
|
|
filepathResponseCache.delete(cachedPath);
|
|
console.log('Purging updated file:', cachedPath)
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
function handler(request: Request, info: any): Promise<Response> | Response {
|
|
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 serveFile("/static/index.html")
|
|
}
|
|
|
|
async function main() {
|
|
|
|
messageDispatch.set('ping', pingHandler);
|
|
messageDispatch.set('hello', helloHandler);
|
|
messageDispatch.set('hello2', hello2Handler);
|
|
messageDispatch.set('peer_message', peerMessageHandler);
|
|
|
|
Deno.serve({
|
|
port: 6789,
|
|
cert: Deno.readTextFileSync("/etc/letsencrypt/live/ddln.app/fullchain.pem"),
|
|
key: Deno.readTextFileSync("/etc/letsencrypt/live/ddln.app/privkey.pem"),
|
|
}, handler);
|
|
|
|
await devServerWatchFiles();
|
|
}
|
|
|
|
await main();
|