Ctrl K
ComunidadGuías…

browser/server.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)));
}