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 { 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 HOST = process.env.HOST || "0.0.0.0";
const CALL_HELPER_SHARED_SECRET = String(
process.env.CALL_HELPER_SHARED_SECRET || ""
).trim();
const PUBLIC_BASE = String(
process.env.PUBLIC_BASE || `http://127.0.0.1:${PORT}`
).replace(/\/+$/, "");
const HELPER_LOG_LEVEL = String(process.env.HELPER_LOG_LEVEL || "info").toLowerCase();
const HELPER_DEFAULT_ROUTE_MODE = String(
process.env.HELPER_DEFAULT_ROUTE_MODE || "direct"
).toLowerCase();
@ -24,9 +35,11 @@ const ENABLE_OVERLAY_METADATA = String(
const OVERLAY_YGG_ENABLED = String(
process.env.OVERLAY_YGG_ENABLED || "0"
) === "1";
const OVERLAY_YGG_BRIDGE_WSS = String(
process.env.OVERLAY_YGG_BRIDGE_WSS || ""
).trim();
const OVERLAY_YGG_BRIDGE_LABEL = String(
process.env.OVERLAY_YGG_BRIDGE_LABEL || "ygg-bridge"
).trim();
@ -34,9 +47,11 @@ const OVERLAY_YGG_BRIDGE_LABEL = String(
const OVERLAY_I2P_ENABLED = String(
process.env.OVERLAY_I2P_ENABLED || "0"
) === "1";
const OVERLAY_I2P_BRIDGE_HTTPS = String(
process.env.OVERLAY_I2P_BRIDGE_HTTPS || ""
).trim();
const OVERLAY_I2P_BRIDGE_LABEL = String(
process.env.OVERLAY_I2P_BRIDGE_LABEL || "i2p-bridge"
).trim();
@ -44,6 +59,15 @@ const OVERLAY_I2P_BRIDGE_LABEL = String(
const RELAY_ENABLED = String(process.env.RELAY_ENABLED || "1") === "1";
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) {
console.error("missing CALL_HELPER_SHARED_SECRET");
process.exit(1);
@ -100,8 +124,9 @@ function isoNow() {
function log(level, message, meta = {}) {
const allowed = ["debug", "info", "warn", "error"];
const currentIdx = allowed.indexOf(HELPER_LOG_LEVEL);
const entryIdx = allowed.indexOf(level);
const currentIdx = Math.max(0, allowed.indexOf(HELPER_LOG_LEVEL));
const entryIdx = Math.max(0, allowed.indexOf(level));
const item = {
ts: isoNow(),
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) {
const pad = value.length % 4 === 0 ? "" : "=".repeat(4 - (value.length % 4));
const normalized = value.replace(/-/g, "+").replace(/_/g, "/") + pad;
@ -248,6 +298,7 @@ function buildRouteCandidates(callId) {
function buildTransportPolicy(routeMode) {
const order = [];
if (routeMode === "overlay_ygg") {
order.push("overlay_ygg", "direct", "relay");
} 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") {
let call = calls.get(callId);
if (!call) {
call = {
callId,
@ -277,6 +347,7 @@ function getOrCreateCall(callId, room, routeMode = "direct") {
calls.set(callId, call);
log("info", "created call", { callId, room, routeMode });
}
return call;
}
@ -338,6 +409,13 @@ function statusSnapshot() {
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: {
active_calls: calls.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());
});
app.get("/status", (_req, res) => {
app.get("/status", (req, res) => {
applyCors(req, res);
res.json({
...statusSnapshot(),
calls: [...calls.values()].map(callSnapshot)
@ -387,6 +467,7 @@ app.get("/status", (_req, res) => {
});
app.get("/logs", (req, res) => {
applyCors(req, res);
const limit = Math.max(1, Math.min(200, Number(req.query.limit || 50)));
res.json({
ok: true,
@ -395,6 +476,8 @@ app.get("/logs", (req, res) => {
});
app.post("/api/call/session", (req, res) => {
applyCors(req, res);
try {
const token = req.body?.token;
const payload = verifySignedToken(token);
@ -429,7 +512,9 @@ app.post("/api/call/session", (req, res) => {
callId,
peerId,
username: payload.username,
routeMode
routeMode,
origin: req.headers.origin || null,
turnEnabled: Boolean(TURN_URLS.length && TURN_USERNAME && TURN_PASSWORD)
});
return res.json({
@ -437,17 +522,14 @@ app.post("/api/call/session", (req, res) => {
call_id: callId,
peer_id: peerId,
ws_url: buildWsUrl(ticket),
iceServers: [
{
urls: ["stun:stun.l.google.com:19302"]
}
],
iceServers: getIceServers(),
transport_policy: transportPolicy,
route_candidates: routeCandidates
});
} catch (error) {
log("warn", "call session rejected", {
error: error.message
error: error.message,
origin: req.headers.origin || null
});
return res.status(401).json({
@ -458,6 +540,14 @@ app.post("/api/call/session", (req, res) => {
});
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}`);
if (url.pathname !== "/ws") {
socket.destroy();
@ -468,6 +558,9 @@ server.on("upgrade", (req, socket, head) => {
const ticketData = ticket ? tickets.get(ticket) : null;
if (!ticketData || ticketData.exp <= nowSeconds()) {
log("warn", "websocket upgrade rejected by invalid/expired ticket", {
origin: origin || null
});
socket.destroy();
return;
}
@ -581,6 +674,7 @@ wss.on("connection", ws => {
if (msg.type === "offer" || msg.type === "answer") {
const to = String(msg.to || "").trim();
if (!to) return;
const target = call.participants.get(to);
if (!target) {
log("warn", "signal target missing", {
@ -606,6 +700,7 @@ wss.on("connection", ws => {
if (msg.type === "ice") {
const to = String(msg.to || "").trim();
if (!to) return;
const target = call.participants.get(to);
if (!target) {
log("warn", "ice target missing", {
@ -656,6 +751,11 @@ server.listen(PORT, HOST, () => {
overlays: {
ygg: OVERLAY_YGG_ENABLED,
i2p: OVERLAY_I2P_ENABLED
}
},
turn: {
enabled: Boolean(TURN_URLS.length && TURN_USERNAME && TURN_PASSWORD),
urls: TURN_URLS
},
corsAllowedOrigins: [...allowedOrigins]
});
});