Files
kkkmeet/server.js
T

168 lines
4.6 KiB
JavaScript

import http from "node:http";
import { readFile } from "node:fs/promises";
import { extname, join, normalize } from "node:path";
import { randomUUID } from "node:crypto";
const port = Number(process.env.PORT || 3000);
const host = process.env.HOST || "127.0.0.1";
const publicDir = join(process.cwd(), "public");
const rooms = new Map();
const defaultRoomId = "main";
const mimeTypes = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".svg": "image/svg+xml"
};
function sendJson(res, status, body) {
res.writeHead(status, {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store"
});
res.end(JSON.stringify(body));
}
function getRoom(roomId) {
if (!rooms.has(roomId)) {
rooms.set(roomId, new Map());
}
return rooms.get(roomId);
}
function normalizeName(name) {
const normalized = String(name || "").trim().replace(/\s+/g, " ");
return normalized.slice(0, 48) || "Guest";
}
function broadcast(roomId, senderId, event) {
const room = rooms.get(roomId);
if (!room) return;
for (const [peerId, peer] of room) {
if (peerId === senderId) continue;
peer.res.write(`data: ${JSON.stringify(event)}\n\n`);
}
}
function removePeer(roomId, peerId) {
const room = rooms.get(roomId);
if (!room) return;
room.delete(peerId);
broadcast(roomId, peerId, { type: "peer-left", peerId });
if (room.size === 0) {
rooms.delete(roomId);
}
}
async function parseJson(req) {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const raw = Buffer.concat(chunks).toString("utf8");
return raw ? JSON.parse(raw) : {};
}
async function serveStatic(req, res) {
const url = new URL(req.url, `http://${req.headers.host}`);
const requestedPath = url.pathname === "/" ? "/index.html" : url.pathname;
const safePath = normalize(decodeURIComponent(requestedPath)).replace(/^(\.\.[/\\])+/, "");
const filePath = join(publicDir, safePath);
if (!filePath.startsWith(publicDir)) {
res.writeHead(403);
res.end("Forbidden");
return;
}
try {
const data = await readFile(filePath);
res.writeHead(200, {
"content-type": mimeTypes[extname(filePath)] || "application/octet-stream",
"cache-control": "no-store"
});
res.end(req.method === "HEAD" ? undefined : data);
} catch {
res.writeHead(404);
res.end("Not found");
}
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
try {
if (req.method === "GET" && url.pathname.startsWith("/events/")) {
const roomId = url.pathname.split("/").at(-1) || defaultRoomId;
const displayName = normalizeName(url.searchParams.get("name"));
const peerId = randomUUID();
const room = getRoom(roomId);
const existingPeers = [...room].map(([id, peer]) => ({ id, name: peer.name }));
res.writeHead(200, {
"content-type": "text/event-stream; charset=utf-8",
"cache-control": "no-store, no-transform",
connection: "keep-alive",
"x-accel-buffering": "no"
});
room.set(peerId, { res, name: displayName });
res.write(
`data: ${JSON.stringify({
type: "welcome",
peerId,
name: displayName,
peers: existingPeers
})}\n\n`
);
broadcast(roomId, peerId, { type: "peer-joined", peerId, name: displayName });
const heartbeat = setInterval(() => {
res.write(": heartbeat\n\n");
}, 15000);
req.on("close", () => {
clearInterval(heartbeat);
removePeer(roomId, peerId);
});
return;
}
if (req.method === "POST" && url.pathname.startsWith("/signal/")) {
const roomId = url.pathname.split("/").at(-1);
const payload = await parseJson(req);
if (!payload.from || !payload.to || !payload.type) {
sendJson(res, 400, { error: "Signal messages require from, to, and type." });
return;
}
const room = rooms.get(roomId);
const target = room?.get(payload.to);
if (target) {
target.res.write(`data: ${JSON.stringify(payload)}\n\n`);
}
sendJson(res, 202, { ok: true });
return;
}
if (req.method === "GET" || req.method === "HEAD") {
await serveStatic(req, res);
return;
}
res.writeHead(405);
res.end("Method not allowed");
} catch (error) {
sendJson(res, 500, { error: error.message });
}
});
server.listen(port, host, () => {
console.log(`kkkmeet is running at http://${host}:${port}`);
});