Update helper/server.js

This commit is contained in:
dev 2026-10-30 04:49:40 +00:00
parent 5b3c4c3c55
commit a841e8a0ee

View File

@ -3,16 +3,27 @@ import express from "express";
import http from "http"; import http from "http";
import { WebSocketServer } from "ws"; import { WebSocketServer } from "ws";
const TURN_URLS = String(process.env.TURN_URLS || "")
.split(",")
.map(value => value.trim())
.filter(Boolean);
const TURN_USERNAME = String(process.env.TURN_USERNAME || "").trim();
const TURN_PASSWORD = String(process.env.TURN_PASSWORD || "").trim();
const PORT = Number(process.env.PORT || 8080); const PORT = Number(process.env.PORT || 8080);
const HOST = process.env.HOST || "0.0.0.0"; const HOST = process.env.HOST || "0.0.0.0";
const CALL_HELPER_SHARED_SECRET = String( const CALL_HELPER_SHARED_SECRET = String(
process.env.CALL_HELPER_SHARED_SECRET || "" process.env.CALL_HELPER_SHARED_SECRET || ""
).trim(); ).trim();
const PUBLIC_BASE = String( const PUBLIC_BASE = String(
process.env.PUBLIC_BASE || `http://127.0.0.1:${PORT}` process.env.PUBLIC_BASE || `http://127.0.0.1:${PORT}`
).replace(/\/+$/, ""); ).replace(/\/+$/, "");
const HELPER_LOG_LEVEL = String(process.env.HELPER_LOG_LEVEL || "info").toLowerCase(); const HELPER_LOG_LEVEL = String(process.env.HELPER_LOG_LEVEL || "info").toLowerCase();
const HELPER_DEFAULT_ROUTE_MODE = String( const HELPER_DEFAULT_ROUTE_MODE = String(
process.env.HELPER_DEFAULT_ROUTE_MODE || "direct" process.env.HELPER_DEFAULT_ROUTE_MODE || "direct"
).toLowerCase(); ).toLowerCase();
@ -24,9 +35,11 @@ const ENABLE_OVERLAY_METADATA = String(
const OVERLAY_YGG_ENABLED = String( const OVERLAY_YGG_ENABLED = String(
process.env.OVERLAY_YGG_ENABLED || "0" process.env.OVERLAY_YGG_ENABLED || "0"
) === "1"; ) === "1";
const OVERLAY_YGG_BRIDGE_WSS = String( const OVERLAY_YGG_BRIDGE_WSS = String(
process.env.OVERLAY_YGG_BRIDGE_WSS || "" process.env.OVERLAY_YGG_BRIDGE_WSS || ""
).trim(); ).trim();
const OVERLAY_YGG_BRIDGE_LABEL = String( const OVERLAY_YGG_BRIDGE_LABEL = String(
process.env.OVERLAY_YGG_BRIDGE_LABEL || "ygg-bridge" process.env.OVERLAY_YGG_BRIDGE_LABEL || "ygg-bridge"
).trim(); ).trim();
@ -34,9 +47,11 @@ const OVERLAY_YGG_BRIDGE_LABEL = String(
const OVERLAY_I2P_ENABLED = String( const OVERLAY_I2P_ENABLED = String(
process.env.OVERLAY_I2P_ENABLED || "0" process.env.OVERLAY_I2P_ENABLED || "0"
) === "1"; ) === "1";
const OVERLAY_I2P_BRIDGE_HTTPS = String( const OVERLAY_I2P_BRIDGE_HTTPS = String(
process.env.OVERLAY_I2P_BRIDGE_HTTPS || "" process.env.OVERLAY_I2P_BRIDGE_HTTPS || ""
).trim(); ).trim();
const OVERLAY_I2P_BRIDGE_LABEL = String( const OVERLAY_I2P_BRIDGE_LABEL = String(
process.env.OVERLAY_I2P_BRIDGE_LABEL || "i2p-bridge" process.env.OVERLAY_I2P_BRIDGE_LABEL || "i2p-bridge"
).trim(); ).trim();
@ -44,6 +59,15 @@ const OVERLAY_I2P_BRIDGE_LABEL = String(
const RELAY_ENABLED = String(process.env.RELAY_ENABLED || "1") === "1"; const RELAY_ENABLED = String(process.env.RELAY_ENABLED || "1") === "1";
const RELAY_WSS = String(process.env.RELAY_WSS || "").trim(); const RELAY_WSS = String(process.env.RELAY_WSS || "").trim();
const CORS_ALLOWED_ORIGINS = String(
process.env.CORS_ALLOWED_ORIGINS || "https://angel.33.backend-3.com"
)
.split(",")
.map(value => value.trim())
.filter(Boolean);
const allowedOrigins = new Set(CORS_ALLOWED_ORIGINS);
if (!CALL_HELPER_SHARED_SECRET) { if (!CALL_HELPER_SHARED_SECRET) {
console.error("missing CALL_HELPER_SHARED_SECRET"); console.error("missing CALL_HELPER_SHARED_SECRET");
process.exit(1); process.exit(1);
@ -100,8 +124,9 @@ function isoNow() {
function log(level, message, meta = {}) { function log(level, message, meta = {}) {
const allowed = ["debug", "info", "warn", "error"]; const allowed = ["debug", "info", "warn", "error"];
const currentIdx = allowed.indexOf(HELPER_LOG_LEVEL); const currentIdx = Math.max(0, allowed.indexOf(HELPER_LOG_LEVEL));
const entryIdx = allowed.indexOf(level); const entryIdx = Math.max(0, allowed.indexOf(level));
const item = { const item = {
ts: isoNow(), ts: isoNow(),
level, level,
@ -123,6 +148,31 @@ function log(level, message, meta = {}) {
} }
} }
function isAllowedOrigin(origin) {
if (!origin) return false;
return allowedOrigins.has(origin);
}
function applyCors(req, res) {
const origin = String(req.headers.origin || "").trim();
if (origin && isAllowedOrigin(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Vary", "Origin");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
}
}
app.use((req, res, next) => {
applyCors(req, res);
if (req.method === "OPTIONS") {
return res.status(204).end();
}
next();
});
function b64urlDecode(value) { function b64urlDecode(value) {
const pad = value.length % 4 === 0 ? "" : "=".repeat(4 - (value.length % 4)); const pad = value.length % 4 === 0 ? "" : "=".repeat(4 - (value.length % 4));
const normalized = value.replace(/-/g, "+").replace(/_/g, "/") + pad; const normalized = value.replace(/-/g, "+").replace(/_/g, "/") + pad;
@ -248,6 +298,7 @@ function buildRouteCandidates(callId) {
function buildTransportPolicy(routeMode) { function buildTransportPolicy(routeMode) {
const order = []; const order = [];
if (routeMode === "overlay_ygg") { if (routeMode === "overlay_ygg") {
order.push("overlay_ygg", "direct", "relay"); order.push("overlay_ygg", "direct", "relay");
} else if (routeMode === "overlay_i2p") { } else if (routeMode === "overlay_i2p") {
@ -263,8 +314,27 @@ function buildTransportPolicy(routeMode) {
}; };
} }
function getIceServers() {
const servers = [
{
urls: ["stun:stun.l.google.com:19302"]
}
];
if (TURN_URLS.length && TURN_USERNAME && TURN_PASSWORD) {
servers.push({
urls: TURN_URLS,
username: TURN_USERNAME,
credential: TURN_PASSWORD
});
}
return servers;
}
function getOrCreateCall(callId, room, routeMode = "direct") { function getOrCreateCall(callId, room, routeMode = "direct") {
let call = calls.get(callId); let call = calls.get(callId);
if (!call) { if (!call) {
call = { call = {
callId, callId,
@ -277,6 +347,7 @@ function getOrCreateCall(callId, room, routeMode = "direct") {
calls.set(callId, call); calls.set(callId, call);
log("info", "created call", { callId, room, routeMode }); log("info", "created call", { callId, room, routeMode });
} }
return call; return call;
} }
@ -338,6 +409,13 @@ function statusSnapshot() {
bridge_https: OVERLAY_I2P_BRIDGE_HTTPS || null bridge_https: OVERLAY_I2P_BRIDGE_HTTPS || null
} }
}, },
turn: {
enabled: Boolean(TURN_URLS.length && TURN_USERNAME && TURN_PASSWORD),
urls: TURN_URLS
},
cors: {
allowed_origins: [...allowedOrigins]
},
counts: { counts: {
active_calls: calls.size, active_calls: calls.size,
active_tickets: tickets.size active_tickets: tickets.size
@ -375,11 +453,13 @@ function removeParticipant(callId, peerId, notify = true) {
} }
} }
app.get("/healthz", (_req, res) => { app.get("/healthz", (req, res) => {
applyCors(req, res);
res.json(statusSnapshot()); res.json(statusSnapshot());
}); });
app.get("/status", (_req, res) => { app.get("/status", (req, res) => {
applyCors(req, res);
res.json({ res.json({
...statusSnapshot(), ...statusSnapshot(),
calls: [...calls.values()].map(callSnapshot) calls: [...calls.values()].map(callSnapshot)
@ -387,6 +467,7 @@ app.get("/status", (_req, res) => {
}); });
app.get("/logs", (req, res) => { app.get("/logs", (req, res) => {
applyCors(req, res);
const limit = Math.max(1, Math.min(200, Number(req.query.limit || 50))); const limit = Math.max(1, Math.min(200, Number(req.query.limit || 50)));
res.json({ res.json({
ok: true, ok: true,
@ -395,6 +476,8 @@ app.get("/logs", (req, res) => {
}); });
app.post("/api/call/session", (req, res) => { app.post("/api/call/session", (req, res) => {
applyCors(req, res);
try { try {
const token = req.body?.token; const token = req.body?.token;
const payload = verifySignedToken(token); const payload = verifySignedToken(token);
@ -429,7 +512,9 @@ app.post("/api/call/session", (req, res) => {
callId, callId,
peerId, peerId,
username: payload.username, username: payload.username,
routeMode routeMode,
origin: req.headers.origin || null,
turnEnabled: Boolean(TURN_URLS.length && TURN_USERNAME && TURN_PASSWORD)
}); });
return res.json({ return res.json({
@ -437,17 +522,14 @@ app.post("/api/call/session", (req, res) => {
call_id: callId, call_id: callId,
peer_id: peerId, peer_id: peerId,
ws_url: buildWsUrl(ticket), ws_url: buildWsUrl(ticket),
iceServers: [ iceServers: getIceServers(),
{
urls: ["stun:stun.l.google.com:19302"]
}
],
transport_policy: transportPolicy, transport_policy: transportPolicy,
route_candidates: routeCandidates route_candidates: routeCandidates
}); });
} catch (error) { } catch (error) {
log("warn", "call session rejected", { log("warn", "call session rejected", {
error: error.message error: error.message,
origin: req.headers.origin || null
}); });
return res.status(401).json({ return res.status(401).json({
@ -458,6 +540,14 @@ app.post("/api/call/session", (req, res) => {
}); });
server.on("upgrade", (req, socket, head) => { server.on("upgrade", (req, socket, head) => {
const origin = String(req.headers.origin || "").trim();
if (origin && !isAllowedOrigin(origin)) {
log("warn", "websocket upgrade rejected by origin", { origin });
socket.destroy();
return;
}
const url = new URL(req.url, `http://${req.headers.host}`); const url = new URL(req.url, `http://${req.headers.host}`);
if (url.pathname !== "/ws") { if (url.pathname !== "/ws") {
socket.destroy(); socket.destroy();
@ -468,6 +558,9 @@ server.on("upgrade", (req, socket, head) => {
const ticketData = ticket ? tickets.get(ticket) : null; const ticketData = ticket ? tickets.get(ticket) : null;
if (!ticketData || ticketData.exp <= nowSeconds()) { if (!ticketData || ticketData.exp <= nowSeconds()) {
log("warn", "websocket upgrade rejected by invalid/expired ticket", {
origin: origin || null
});
socket.destroy(); socket.destroy();
return; return;
} }
@ -581,6 +674,7 @@ wss.on("connection", ws => {
if (msg.type === "offer" || msg.type === "answer") { if (msg.type === "offer" || msg.type === "answer") {
const to = String(msg.to || "").trim(); const to = String(msg.to || "").trim();
if (!to) return; if (!to) return;
const target = call.participants.get(to); const target = call.participants.get(to);
if (!target) { if (!target) {
log("warn", "signal target missing", { log("warn", "signal target missing", {
@ -606,6 +700,7 @@ wss.on("connection", ws => {
if (msg.type === "ice") { if (msg.type === "ice") {
const to = String(msg.to || "").trim(); const to = String(msg.to || "").trim();
if (!to) return; if (!to) return;
const target = call.participants.get(to); const target = call.participants.get(to);
if (!target) { if (!target) {
log("warn", "ice target missing", { log("warn", "ice target missing", {
@ -656,6 +751,11 @@ server.listen(PORT, HOST, () => {
overlays: { overlays: {
ygg: OVERLAY_YGG_ENABLED, ygg: OVERLAY_YGG_ENABLED,
i2p: OVERLAY_I2P_ENABLED i2p: OVERLAY_I2P_ENABLED
} },
turn: {
enabled: Boolean(TURN_URLS.length && TURN_USERNAME && TURN_PASSWORD),
urls: TURN_URLS
},
corsAllowedOrigins: [...allowedOrigins]
}); });
}); });