browser/server.js
Rama main · réplica de lectura del repositorio original.
import { createServer } from "node:http";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import express from "express";
import { server as wisp } from "@mercuryworkshop/wisp-js/server";
import { uvPath } from "@titaniumnetwork-dev/ultraviolet";
import { publicPath } from "ultraviolet-static";
import { baremuxPath } from "@mercuryworkshop/bare-mux/node";
const __dirname = dirname(fileURLToPath(import.meta.url));
const overlayPath = join(__dirname, "public");
const epoxyPath = join(__dirname, "node_modules", "@mercuryworkshop", "epoxy-transport", "dist");
const HOST = process.env.HOST || "127.0.0.1";
const PORT = parseInt(process.env.PORT || "8090", 10);
Object.assign(wisp.options, {
allow_private_ips: false,
allow_loopback_ips: false,
allow_udp_streams: true,
allow_tcp_streams: true,
hostname_blacklist: [
/^localhost$/i,
/\.internal$/i,
/(^|\.)panel\.northwind\.example$/i,
],
stream_limit_per_host: -1,
stream_limit_total: 512,
wisp_motd: "panel-browser",
});
const app = express();
app.disable("x-powered-by");
app.get("/healthz", (_req, res) => res.json({ ok: true, service: "panel-browser" }));
const bwCounters = { totalSent: 0, totalRecv: 0, perOrigin: {} };
app.get("/api/bandwidth", (_req, res) => res.json(bwCounters));
const BYPARR_URL = process.env.BYPARR_URL || "http://127.0.0.1:8191/v1";
const SOLVER_TIMEOUT_MS = 90000;
app.post("/api/solve", express.json({ limit: "32kb" }), async (req, res) => {
const url = (req.body && req.body.url || "").toString().trim();
if (!/^https?:\/\//i.test(url)) {
return res.status(400).json({ error: "invalid url" });
}
const ctl = new AbortController();
const to = setTimeout(() => ctl.abort(), SOLVER_TIMEOUT_MS + 5000);
try {
const r = await fetch(BYPARR_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cmd: "request.get", url, maxTimeout: SOLVER_TIMEOUT_MS }),
signal: ctl.signal,
});
clearTimeout(to);
if (!r.ok) {
return res.status(502).json({ error: "solver error " + r.status });
}
const data = await r.json();
if (data.status !== "ok") {
return res.status(502).json({ error: data.message || "solver failed", raw: data });
}
const sol = data.solution || {};
res.json({
url: sol.url || url,
status: sol.status,
userAgent: sol.userAgent || "",
cookies: (sol.cookies || []).map(c => ({
name: c.name, value: c.value, domain: c.domain,
path: c.path || "/", secure: !!c.secure, httpOnly: !!c.httpOnly,
sameSite: c.sameSite || null, expires: c.expires || null,
})),
html: req.query.withHtml === "1" ? (sol.response || "") : undefined,
htmlSize: (sol.response || "").length,
tookMs: data.endTimestamp && data.startTimestamp ? (data.endTimestamp - data.startTimestamp) : null,
});
} catch (e) {
clearTimeout(to);
res.status(502).json({ error: "solver unreachable: " + (e.message || e) });
}
});
app.use((req, res, next) => {
if (req.path === "/" || req.path === "/index.html" || req.path === "/register-sw.js") {
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
}
next();
});
app.use(express.static(overlayPath, { fallthrough: true }));
app.use(express.static(publicPath, { fallthrough: true }));
app.use("/uv/", express.static(uvPath, { fallthrough: true }));
app.use("/epoxy/", express.static(epoxyPath, { fallthrough: true }));
app.use("/baremux/", express.static(baremuxPath, { fallthrough: true }));
const server = createServer();
server.on("request", app);
server.on("upgrade", (req, socket, head) => {
if (req.url.endsWith("/wisp/")) {
wisp.routeRequest(req, socket, head);
} else {
socket.end();
}
});
server.listen(PORT, HOST, () => {
console.log(`[panel-browser] Ultraviolet+Wisp on http://${HOST}:${PORT} (wisp on /wisp/)`);
});
for (const sig of ["SIGINT", "SIGTERM"]) {
process.on(sig, () => server.close(() => process.exit(0)));
}