Files
mnote/wolai-frontend/scripts/prod-server.js
T
2026-04-13 19:21:42 +08:00

331 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 自定义 Next 生产 server
* - 解决 ONLYOFFICE 在 `/onlyoffice-server/*` 下的 WebSocket Upgrade 需求(socket.io / coauthoring)。
* - 解决 Convex 在 HTTPSfrp/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);
});