2026-04-13 19:21:42 +08:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 自定义 Next 生产 server:
|
|
|
|
|
|
* - 解决 ONLYOFFICE 在 `/onlyoffice-server/*` 下的 WebSocket Upgrade 需求(socket.io / coauthoring)。
|
|
|
|
|
|
* - 解决 Convex 在 HTTPS(frp/nginx)场景下浏览器不能连接 ws:// 的问题:通过同源 `/convex/*` 反代到本机 Convex。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 用法:
|
|
|
|
|
|
* - pnpm build
|
|
|
|
|
|
* - pnpm start (默认会执行本脚本)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 依赖环境变量:
|
|
|
|
|
|
* - ONLYOFFICE_INTERNAL_URL:默认 http://127.0.0.1:8081
|
|
|
|
|
|
* - CONVEX_INTERNAL_URL:默认 http://127.0.0.1:3210
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const http = require("http");
|
|
|
|
|
|
const net = require("net");
|
|
|
|
|
|
const path = require("path");
|
|
|
|
|
|
const next = require("next");
|
|
|
|
|
|
const { parse: parseUrl } = require("url");
|
|
|
|
|
|
|
|
|
|
|
|
const ONLYOFFICE_PREFIX = "/onlyoffice-server";
|
|
|
|
|
|
const CONVEX_PREFIX = "/convex";
|
|
|
|
|
|
|
|
|
|
|
|
function readArgValue(flag) {
|
|
|
|
|
|
const idx = process.argv.findIndex((x) => x === flag);
|
|
|
|
|
|
if (idx === -1) return null;
|
|
|
|
|
|
const v = process.argv[idx + 1];
|
|
|
|
|
|
if (!v || v.startsWith("-")) return null;
|
|
|
|
|
|
return v;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolvePort() {
|
|
|
|
|
|
const fromArg = readArgValue("-p") || readArgValue("--port");
|
|
|
|
|
|
const raw = fromArg || process.env.PORT || "3000";
|
|
|
|
|
|
const n = Number(raw);
|
|
|
|
|
|
return Number.isFinite(n) ? Math.max(1, Math.min(65535, Math.floor(n))) : 3000;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveHostname() {
|
|
|
|
|
|
return readArgValue("-H") || readArgValue("--hostname") || process.env.HOSTNAME || "0.0.0.0";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isOnlyOfficePath(urlString) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const u = new URL(urlString, "http://localhost");
|
|
|
|
|
|
return u.pathname === ONLYOFFICE_PREFIX || u.pathname.startsWith(`${ONLYOFFICE_PREFIX}/`);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isConvexPath(urlString) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const u = new URL(urlString, "http://localhost");
|
|
|
|
|
|
return u.pathname === CONVEX_PREFIX || u.pathname.startsWith(`${CONVEX_PREFIX}/`);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildUpstreamRequestHead(req, targetUrl, prefix) {
|
|
|
|
|
|
const incoming = new URL(req.url || "/", "http://localhost");
|
|
|
|
|
|
const rawPath = incoming.pathname || "/";
|
|
|
|
|
|
const stripped = rawPath === prefix ? "/" : rawPath.slice(prefix.length) || "/";
|
|
|
|
|
|
|
|
|
|
|
|
const basePath = String(targetUrl.pathname || "/").replace(/\/+$/, "") || "";
|
|
|
|
|
|
const upstreamPath = `${basePath}${stripped}`.replace(/\/{2,}/g, "/") + (incoming.search || "");
|
|
|
|
|
|
|
|
|
|
|
|
const lines = [];
|
|
|
|
|
|
lines.push(`${req.method || "GET"} ${upstreamPath} HTTP/1.1`);
|
|
|
|
|
|
|
|
|
|
|
|
const headers = req.headers || {};
|
|
|
|
|
|
const headerValue = (name) => {
|
|
|
|
|
|
const v = headers[name];
|
|
|
|
|
|
if (!v) return "";
|
|
|
|
|
|
return Array.isArray(v) ? v[0] : String(v);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const originLike = headerValue("origin") || headerValue("referer") || "";
|
|
|
|
|
|
const originUrl = (() => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!originLike) return null;
|
|
|
|
|
|
return new URL(originLike);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
const forwardedHostRaw =
|
|
|
|
|
|
headerValue("x-forwarded-host") || (originUrl ? originUrl.host : "") || headerValue("host") || "";
|
|
|
|
|
|
const forwardedProto =
|
|
|
|
|
|
(headerValue("x-forwarded-proto") || "").split(",")[0].trim() ||
|
|
|
|
|
|
(originUrl ? originUrl.protocol.replace(":", "") : "") ||
|
|
|
|
|
|
"http";
|
|
|
|
|
|
const forwardedPort = (() => {
|
|
|
|
|
|
const fromHeader = (headerValue("x-forwarded-port") || "").split(",")[0].trim();
|
|
|
|
|
|
if (fromHeader) return fromHeader;
|
|
|
|
|
|
const hostHasPort = forwardedHostRaw.includes(":") ? forwardedHostRaw.split(":").pop() : "";
|
|
|
|
|
|
if (hostHasPort && /^\d+$/.test(hostHasPort)) return hostHasPort;
|
|
|
|
|
|
return forwardedProto === "https" ? "443" : "80";
|
|
|
|
|
|
})();
|
|
|
|
|
|
const forwardedHost = forwardedHostRaw.split(",")[0].trim() || "localhost";
|
|
|
|
|
|
|
|
|
|
|
|
for (const [k, v] of Object.entries(headers)) {
|
|
|
|
|
|
if (!v) continue;
|
|
|
|
|
|
const key = String(k);
|
|
|
|
|
|
if (key.toLowerCase() === "host") continue;
|
|
|
|
|
|
if (Array.isArray(v)) {
|
|
|
|
|
|
lines.push(`${key}: ${v.join(", ")}`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
lines.push(`${key}: ${String(v)}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lines.push(`x-forwarded-host: ${forwardedHost}`);
|
|
|
|
|
|
lines.push(`x-forwarded-proto: ${forwardedProto}`);
|
|
|
|
|
|
lines.push(`x-forwarded-port: ${forwardedPort}`);
|
|
|
|
|
|
lines.push(`x-forwarded-prefix: ${prefix}`);
|
|
|
|
|
|
lines.push(`Host: ${targetUrl.host}`);
|
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
return lines.join("\r\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function proxyOnlyOfficeUpgrade(req, socket, head) {
|
|
|
|
|
|
const target = new URL((process.env.ONLYOFFICE_INTERNAL_URL || "http://127.0.0.1:8081").replace(/\/+$/, "") + "/");
|
|
|
|
|
|
const port = Number(target.port) || (target.protocol === "https:" ? 443 : 80);
|
|
|
|
|
|
|
|
|
|
|
|
const upstream = net.connect({ host: target.hostname, port }, () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const reqHead = buildUpstreamRequestHead(req, target, ONLYOFFICE_PREFIX);
|
|
|
|
|
|
upstream.write(reqHead);
|
|
|
|
|
|
if (head && head.length > 0) upstream.write(head);
|
|
|
|
|
|
socket.pipe(upstream);
|
|
|
|
|
|
upstream.pipe(socket);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
try {
|
|
|
|
|
|
socket.destroy();
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
try {
|
|
|
|
|
|
upstream.destroy();
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const onError = (err) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const msg = err && err.message ? String(err.message) : String(err || "");
|
|
|
|
|
|
console.log("[prod-server][onlyoffice-ws] proxy error", msg);
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
try {
|
|
|
|
|
|
socket.destroy();
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
try {
|
|
|
|
|
|
upstream.destroy();
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
upstream.on("error", onError);
|
|
|
|
|
|
socket.on("error", onError);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveConvexInternalUrl() {
|
|
|
|
|
|
const raw = (process.env.CONVEX_INTERNAL_URL || "http://127.0.0.1:3210").trim();
|
|
|
|
|
|
try {
|
|
|
|
|
|
return new URL(raw.replace(/\/+$/, "") + "/");
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return new URL("http://127.0.0.1:3210/");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function proxyConvexUpgrade(req, socket, head) {
|
|
|
|
|
|
const target = resolveConvexInternalUrl();
|
|
|
|
|
|
const port = Number(target.port) || (target.protocol === "https:" ? 443 : 80);
|
|
|
|
|
|
|
|
|
|
|
|
const upstream = net.connect({ host: target.hostname, port }, () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const reqHead = buildUpstreamRequestHead(req, target, CONVEX_PREFIX);
|
|
|
|
|
|
upstream.write(reqHead);
|
|
|
|
|
|
if (head && head.length > 0) upstream.write(head);
|
|
|
|
|
|
socket.pipe(upstream);
|
|
|
|
|
|
upstream.pipe(socket);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
try {
|
|
|
|
|
|
socket.destroy();
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
try {
|
|
|
|
|
|
upstream.destroy();
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const onError = (err) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const msg = err && err.message ? String(err.message) : String(err || "");
|
|
|
|
|
|
console.log("[prod-server][convex-ws] proxy error", msg);
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
try {
|
|
|
|
|
|
socket.destroy();
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
try {
|
|
|
|
|
|
upstream.destroy();
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
upstream.on("error", onError);
|
|
|
|
|
|
socket.on("error", onError);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function proxyConvexHttp(req, res) {
|
|
|
|
|
|
const target = resolveConvexInternalUrl();
|
|
|
|
|
|
const incoming = new URL(req.url || "/", "http://localhost");
|
|
|
|
|
|
const rawPath = incoming.pathname || "/";
|
|
|
|
|
|
const stripped = rawPath === CONVEX_PREFIX ? "/" : rawPath.slice(CONVEX_PREFIX.length) || "/";
|
|
|
|
|
|
const upstreamPath = stripped + (incoming.search || "");
|
|
|
|
|
|
|
|
|
|
|
|
const isHttps = target.protocol === "https:";
|
|
|
|
|
|
const mod = isHttps ? require("https") : require("http");
|
|
|
|
|
|
|
|
|
|
|
|
const headers = { ...(req.headers || {}) };
|
|
|
|
|
|
headers.host = target.host;
|
|
|
|
|
|
|
|
|
|
|
|
const forwardedHostRaw = String(headers["x-forwarded-host"] || req.headers.host || "");
|
|
|
|
|
|
const forwardedHost = forwardedHostRaw.split(",")[0].trim() || "localhost";
|
|
|
|
|
|
const forwardedProto =
|
|
|
|
|
|
String(headers["x-forwarded-proto"] || "").split(",")[0].trim() ||
|
|
|
|
|
|
(String(req.headers.origin || "").startsWith("https") ? "https" : "http");
|
|
|
|
|
|
const forwardedPortRaw = String(headers["x-forwarded-port"] || "").split(",")[0].trim();
|
|
|
|
|
|
const forwardedPort = (() => {
|
|
|
|
|
|
if (forwardedPortRaw) return forwardedPortRaw;
|
|
|
|
|
|
const hostHasPort = forwardedHost.includes(":") ? forwardedHost.split(":").pop() : "";
|
|
|
|
|
|
if (hostHasPort && /^\d+$/.test(hostHasPort)) return hostHasPort;
|
|
|
|
|
|
return forwardedProto === "https" ? "443" : "80";
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
headers["x-forwarded-host"] = forwardedHost;
|
|
|
|
|
|
headers["x-forwarded-proto"] = forwardedProto;
|
|
|
|
|
|
headers["x-forwarded-port"] = forwardedPort;
|
|
|
|
|
|
headers["x-forwarded-prefix"] = CONVEX_PREFIX;
|
|
|
|
|
|
|
|
|
|
|
|
const upstreamReq = mod.request(
|
|
|
|
|
|
{
|
|
|
|
|
|
protocol: target.protocol,
|
|
|
|
|
|
hostname: target.hostname,
|
|
|
|
|
|
port: target.port || (isHttps ? 443 : 80),
|
|
|
|
|
|
method: req.method,
|
|
|
|
|
|
path: upstreamPath,
|
|
|
|
|
|
headers,
|
|
|
|
|
|
},
|
|
|
|
|
|
(upstreamRes) => {
|
|
|
|
|
|
res.writeHead(upstreamRes.statusCode || 502, upstreamRes.headers || {});
|
|
|
|
|
|
upstreamRes.pipe(res);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
upstreamReq.on("error", (err) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log("[prod-server][convex-http] proxy error", err && err.message ? err.message : String(err || ""));
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
try {
|
|
|
|
|
|
res.statusCode = 502;
|
|
|
|
|
|
res.end("Bad Gateway");
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
req.pipe(upstreamReq);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function main() {
|
|
|
|
|
|
const port = resolvePort();
|
|
|
|
|
|
const hostname = resolveHostname();
|
|
|
|
|
|
const dev = false;
|
|
|
|
|
|
|
|
|
|
|
|
const app = next({ dev, dir: path.join(__dirname, "..") });
|
|
|
|
|
|
const handle = app.getRequestHandler();
|
|
|
|
|
|
|
|
|
|
|
|
await app.prepare();
|
|
|
|
|
|
const handleUpgrade = typeof app.getUpgradeHandler === "function" ? app.getUpgradeHandler() : null;
|
|
|
|
|
|
|
|
|
|
|
|
const server = http.createServer((req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
res.setHeader("x-mnote-prod-server", "1");
|
|
|
|
|
|
res.setHeader("x-mnote-onlyoffice-ws-proxy", "1");
|
|
|
|
|
|
res.setHeader("x-mnote-convex-ws-proxy", "1");
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isConvexPath(req.url || "/")) {
|
|
|
|
|
|
proxyConvexHttp(req, res);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const parsed = parseUrl(req.url || "/", true);
|
|
|
|
|
|
handle(req, res, parsed);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
server.on("upgrade", (req, socket, head) => {
|
|
|
|
|
|
if (isConvexPath(req.url || "/")) {
|
|
|
|
|
|
proxyConvexUpgrade(req, socket, head);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isOnlyOfficePath(req.url || "/")) {
|
|
|
|
|
|
proxyOnlyOfficeUpgrade(req, socket, head);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (handleUpgrade) {
|
|
|
|
|
|
handleUpgrade(req, socket, head);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
socket.destroy();
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
server.listen(port, hostname, () => {
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
`[prod-server] ready http://${hostname}:${port} (ONLYOFFICE ws via ${ONLYOFFICE_PREFIX} -> ${process.env.ONLYOFFICE_INTERNAL_URL || "http://127.0.0.1:8081"}; Convex ws via ${CONVEX_PREFIX} -> ${process.env.CONVEX_INTERNAL_URL || "http://127.0.0.1:3210"})`,
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main().catch((err) => {
|
|
|
|
|
|
console.error(err instanceof Error ? err.stack : String(err));
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
});
|
|
|
|
|
|
|