Update helper/server.js
This commit is contained in:
parent
5b3c4c3c55
commit
a841e8a0ee
124
helper/server.js
124
helper/server.js
@ -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]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user