0.5 缩减重构
This commit is contained in:
@@ -1,367 +1,367 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 自定义 Next dev server:
|
||||
* - 解决 ONLYOFFICE 在 `/onlyoffice-server/*` 下的 WebSocket Upgrade 需求(socket.io / coauthoring)。
|
||||
* - Next App Router 的 Route Handler 无法处理 Upgrade,因此必须在 Node http server 层做透传。
|
||||
*
|
||||
* 用法(保持与 next dev 类似):
|
||||
* - pnpm dev -p 3000
|
||||
* - node scripts/dev-server.js -p 3000
|
||||
*
|
||||
* 依赖环境变量:
|
||||
* - ONLYOFFICE_INTERNAL_URL:默认 http://127.0.0.1:8081
|
||||
*/
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
// 说明:ONLYOFFICE 在反向代理下会依赖 X-Forwarded-* 推导“对外地址”,用于拼出 ws/wss 与缓存资源 URL。
|
||||
// 这里尽量沿用上游(frp/nginx)注入的 x-forwarded-proto/host;若缺失,则回退到本机 http。
|
||||
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)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 说明:补齐/覆盖 forward 信息,避免 ONLYOFFICE 返回指向内部端口的绝对 URL。
|
||||
lines.push(`x-forwarded-host: ${forwardedHost}`);
|
||||
lines.push(`x-forwarded-proto: ${forwardedProto}`);
|
||||
lines.push(`x-forwarded-port: ${forwardedPort}`);
|
||||
lines.push(`x-forwarded-prefix: ${prefix}`);
|
||||
|
||||
// 说明:Host 必须指向 ONLYOFFICE_INTERNAL_URL,否则上游可能拒绝 Upgrade。
|
||||
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 (e) {
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch {}
|
||||
try {
|
||||
upstream.destroy();
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
const onError = (err) => {
|
||||
try {
|
||||
const msg = err && err.message ? String(err.message) : String(err || "");
|
||||
|
||||
console.log("[dev-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() {
|
||||
// 说明:Convex 本地 dev server 默认 3210;对外访问(如 frp https)时,浏览器需要 wss,
|
||||
// 因此这里通过同源反代把 Upgrade 转发到本机 3210。
|
||||
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("[dev-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;
|
||||
// 说明:让上游能感知对外协议/域名(主要用于调试;Convex 本身通常不依赖这些头)。
|
||||
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("[dev-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 = true;
|
||||
|
||||
const app = next({ dev, dir: path.join(__dirname, "..") });
|
||||
const handle = app.getRequestHandler();
|
||||
|
||||
await app.prepare();
|
||||
// 说明:Next dev 的 HMR 依赖 WebSocket(/_next/webpack-hmr),需要交给 Next 自己处理 upgrade。
|
||||
const handleUpgrade = typeof app.getUpgradeHandler === "function" ? app.getUpgradeHandler() : null;
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
try {
|
||||
res.setHeader("x-mnote-dev-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 || "/")) {
|
||||
try {
|
||||
|
||||
console.log(
|
||||
"[dev-server][convex-ws] upgrade",
|
||||
req.url,
|
||||
"host=",
|
||||
req.headers.host,
|
||||
"xfp=",
|
||||
req.headers["x-forwarded-proto"],
|
||||
"xfh=",
|
||||
req.headers["x-forwarded-host"],
|
||||
);
|
||||
} catch {}
|
||||
proxyConvexUpgrade(req, socket, head);
|
||||
return;
|
||||
}
|
||||
if (isOnlyOfficePath(req.url || "/")) {
|
||||
try {
|
||||
|
||||
console.log(
|
||||
"[dev-server][onlyoffice-ws] upgrade",
|
||||
req.url,
|
||||
"host=",
|
||||
req.headers.host,
|
||||
"xfp=",
|
||||
req.headers["x-forwarded-proto"],
|
||||
"xfh=",
|
||||
req.headers["x-forwarded-host"],
|
||||
);
|
||||
} catch {}
|
||||
proxyOnlyOfficeUpgrade(req, socket, head);
|
||||
return;
|
||||
}
|
||||
if (handleUpgrade) {
|
||||
handleUpgrade(req, socket, head);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(port, hostname, () => {
|
||||
|
||||
console.log(
|
||||
`[dev-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);
|
||||
});
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 自定义 Next dev server:
|
||||
* - 解决 ONLYOFFICE 在 `/onlyoffice-server/*` 下的 WebSocket Upgrade 需求(socket.io / coauthoring)。
|
||||
* - Next App Router 的 Route Handler 无法处理 Upgrade,因此必须在 Node http server 层做透传。
|
||||
*
|
||||
* 用法(保持与 next dev 类似):
|
||||
* - pnpm dev -p 3000
|
||||
* - node scripts/dev-server.js -p 3000
|
||||
*
|
||||
* 依赖环境变量:
|
||||
* - ONLYOFFICE_INTERNAL_URL:默认 http://127.0.0.1:8081
|
||||
*/
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
// 说明:ONLYOFFICE 在反向代理下会依赖 X-Forwarded-* 推导“对外地址”,用于拼出 ws/wss 与缓存资源 URL。
|
||||
// 这里尽量沿用上游(frp/nginx)注入的 x-forwarded-proto/host;若缺失,则回退到本机 http。
|
||||
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)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 说明:补齐/覆盖 forward 信息,避免 ONLYOFFICE 返回指向内部端口的绝对 URL。
|
||||
lines.push(`x-forwarded-host: ${forwardedHost}`);
|
||||
lines.push(`x-forwarded-proto: ${forwardedProto}`);
|
||||
lines.push(`x-forwarded-port: ${forwardedPort}`);
|
||||
lines.push(`x-forwarded-prefix: ${prefix}`);
|
||||
|
||||
// 说明:Host 必须指向 ONLYOFFICE_INTERNAL_URL,否则上游可能拒绝 Upgrade。
|
||||
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 (e) {
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch {}
|
||||
try {
|
||||
upstream.destroy();
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
const onError = (err) => {
|
||||
try {
|
||||
const msg = err && err.message ? String(err.message) : String(err || "");
|
||||
|
||||
console.log("[dev-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() {
|
||||
// 说明:Convex 本地 dev server 默认 3210;对外访问(如 frp https)时,浏览器需要 wss,
|
||||
// 因此这里通过同源反代把 Upgrade 转发到本机 3210。
|
||||
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("[dev-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;
|
||||
// 说明:让上游能感知对外协议/域名(主要用于调试;Convex 本身通常不依赖这些头)。
|
||||
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("[dev-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 = true;
|
||||
|
||||
const app = next({ dev, dir: path.join(__dirname, "..") });
|
||||
const handle = app.getRequestHandler();
|
||||
|
||||
await app.prepare();
|
||||
// 说明:Next dev 的 HMR 依赖 WebSocket(/_next/webpack-hmr),需要交给 Next 自己处理 upgrade。
|
||||
const handleUpgrade = typeof app.getUpgradeHandler === "function" ? app.getUpgradeHandler() : null;
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
try {
|
||||
res.setHeader("x-mnote-dev-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 || "/")) {
|
||||
try {
|
||||
|
||||
console.log(
|
||||
"[dev-server][convex-ws] upgrade",
|
||||
req.url,
|
||||
"host=",
|
||||
req.headers.host,
|
||||
"xfp=",
|
||||
req.headers["x-forwarded-proto"],
|
||||
"xfh=",
|
||||
req.headers["x-forwarded-host"],
|
||||
);
|
||||
} catch {}
|
||||
proxyConvexUpgrade(req, socket, head);
|
||||
return;
|
||||
}
|
||||
if (isOnlyOfficePath(req.url || "/")) {
|
||||
try {
|
||||
|
||||
console.log(
|
||||
"[dev-server][onlyoffice-ws] upgrade",
|
||||
req.url,
|
||||
"host=",
|
||||
req.headers.host,
|
||||
"xfp=",
|
||||
req.headers["x-forwarded-proto"],
|
||||
"xfh=",
|
||||
req.headers["x-forwarded-host"],
|
||||
);
|
||||
} catch {}
|
||||
proxyOnlyOfficeUpgrade(req, socket, head);
|
||||
return;
|
||||
}
|
||||
if (handleUpgrade) {
|
||||
handleUpgrade(req, socket, head);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(port, hostname, () => {
|
||||
|
||||
console.log(
|
||||
`[dev-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);
|
||||
});
|
||||
|
||||
@@ -14,43 +14,43 @@ function setConvexEnv(name, value) {
|
||||
const privateKey = `-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCPcrWDwjh74nu1
|
||||
gfYo9ywVIRMedfPwbNiDjHnovzHAIeToc1+Lw3ju/io9OME3KLWzIibUHX39LilK
|
||||
sSQKigJ3+i7J13IquJD++ozY9C8Dp/P34txVY+ECSQRdIBbETLJxYBWXOx6Ysw/d
|
||||
/CUuDOdEw2IhNvN97sTh5jUZe6f/H6mfCpJ3X5SZrqrIUTTL6r4Lj9ZS3PUX5ivU
|
||||
6sd2rXzpDsUQgrNgmvGN3w9nAHudxAUv/zOGa4802Z+z14LCBIX6v6PIK79YQ4s9
|
||||
/5hH73MesA6vgzWShJcyVy3guyde+duhpECo1q3d6dIZeqrazQwqAW3vcpmEFJdX
|
||||
jkpociDfAgMBAAECggEAAYtRE+mH1SGThlkvTrKWeWXBQG8xoJFzZTsiZtSEExbq
|
||||
UWxIh4cjqqL2znDpd5ALILIJ6/ejTxHrpN+yTSC+NQ9u6IJWusoA2ZXV5VH/nZD1
|
||||
yeHZ0FuCZRVnJB9/zz4qH5lSsi2TPz6SOagIuG2wIafeyw+94EmtOedSBAO2Q8NN
|
||||
3jHoINBCyRu6hU3ml0h7daoIhUw9ONI7MZUlYvuV7Ti3yf+czzpqwYx3gZA39sa8
|
||||
qUbJPFk6ts+CVjqSAdYVSfU3TC1Us1usOM3+04mACp3vc6ZUKxnB4Dsk/sSHj71n
|
||||
EcZ3fOR7EgjmXf6wBiq24T2+0UzoHW2yDaiAowr6iQKBgQDDs2eJTopFejt47EMm
|
||||
zY//e8fyUDKx07CWwUZKP78IMTZcp8BwHmKTSY+VbQLvVYnNxADkckYapokjU3a7
|
||||
GMOXxvKVLqfgU/oUBIkRFDRo+nasFCd5cPpnYjQ6lGalBkDghVlyvq7kKF/MPtIK
|
||||
dLqsV3tZDXoHX2mjTga94ygTZwKBgQC7pa+eZdWiz275mpYql9Gs6N3G3HhcpRxb
|
||||
oWYWekCtZQ9gtecpvA9e0tW3ShoF19ksWYGpM4vxAOXul5Ei9IPVVNHc7RvGF5CO
|
||||
0Zryx3qaVk7E6XQc3BQVVAenT2fNiv3+fkCMeAvcZBZOGiPPziCCT3wU3bd3tsn8
|
||||
EsMiAPvTyQKBgEs0iWhBr29NrsckfBXQTzMN/WOIIEMoJ6d3dKyZ3K6oQszOhmxP
|
||||
sPALB8uTjdotk/xoAzPHGlupfe/+ZhU2Sgvsn1JnEIprmyHQMGBI1G83OR2dzSGl
|
||||
IgVSvuF4IA3w3kOp2xr2Xj09qrrRtWPhQc9y+urY+/kTWIQyOvMD9WWnAoGADdUN
|
||||
2BBLqj++P3oMvcEJPMTBrGoOGU42g+6m1ttWLzH26zsdei8ZtvS1ulglCO87XBCR
|
||||
BUb+dtqJGIhls3zwxuYEvlNgK78K8ewzjtfzirL4BX3sCECU3mmeUtAAp98qD/uA
|
||||
iJpEzY83MbStlSDtto1jaSpa3uFDjGhZqAUIizkCgYABf/HSMwWsnvRgo0JMmDru
|
||||
3L/xScJiFVdFV9vNiSckjUYhx7lgFqEjsDEN1mZiRIa2UhLOqpo5S/cIXfMkeEPh
|
||||
9Rusf9STwNwOOyhFRrHCxqhTH6Ivnj6oZSM/UrS8GvCVMPWs1z8k45FWKI/IRXQq
|
||||
AT5PRKihrWbg63/rXaSCxw==
|
||||
-----END PRIVATE KEY-----`;
|
||||
|
||||
const jwks = '{"keys":[{"use":"sig","kty":"RSA","n":"j3K1g8I4e-J7tYH2KPcsFSETHnXz8GzYg4x56L8xwCHk6HNfi8N47v4qPTjBNyi1syIm1B19_S4pSrEkCooCd_ouyddyKriQ_vqM2PQvA6fz9-LcVWPhAkkEXSAWxEyycWAVlzsemLMP3fwlLgznRMNiITbzfe7E4eY1GXun_x-pnwqSd1-Uma6qyFE0y-q-C4_WUtz1F-Yr1OrHdq186Q7FEIKzYJrxjd8PZwB7ncQFL_8zhmuPNNmfs9eCwgSF-r-jyCu_WEOLPf-YR-9zHrAOr4M1koSXMlct4LsnXvnboaRAqNat3enSGXqq2s0MKgFt73KZhBSXV45KaHIg3w","e":"AQAB"}]}';
|
||||
|
||||
// 使用临时目录(跨平台兼容)
|
||||
const tmpDir = process.env.TMPDIR || process.env.TEMP || '/tmp';
|
||||
const keyFile = path.join(tmpDir, 'convex_jwt_key.txt');
|
||||
const jwksFile = path.join(tmpDir, 'convex_jwks.txt');
|
||||
|
||||
// 写入临时文件
|
||||
fs.writeFileSync(keyFile, privateKey);
|
||||
fs.writeFileSync(jwksFile, jwks);
|
||||
|
||||
sSQKigJ3+i7J13IquJD++ozY9C8Dp/P34txVY+ECSQRdIBbETLJxYBWXOx6Ysw/d
|
||||
/CUuDOdEw2IhNvN97sTh5jUZe6f/H6mfCpJ3X5SZrqrIUTTL6r4Lj9ZS3PUX5ivU
|
||||
6sd2rXzpDsUQgrNgmvGN3w9nAHudxAUv/zOGa4802Z+z14LCBIX6v6PIK79YQ4s9
|
||||
/5hH73MesA6vgzWShJcyVy3guyde+duhpECo1q3d6dIZeqrazQwqAW3vcpmEFJdX
|
||||
jkpociDfAgMBAAECggEAAYtRE+mH1SGThlkvTrKWeWXBQG8xoJFzZTsiZtSEExbq
|
||||
UWxIh4cjqqL2znDpd5ALILIJ6/ejTxHrpN+yTSC+NQ9u6IJWusoA2ZXV5VH/nZD1
|
||||
yeHZ0FuCZRVnJB9/zz4qH5lSsi2TPz6SOagIuG2wIafeyw+94EmtOedSBAO2Q8NN
|
||||
3jHoINBCyRu6hU3ml0h7daoIhUw9ONI7MZUlYvuV7Ti3yf+czzpqwYx3gZA39sa8
|
||||
qUbJPFk6ts+CVjqSAdYVSfU3TC1Us1usOM3+04mACp3vc6ZUKxnB4Dsk/sSHj71n
|
||||
EcZ3fOR7EgjmXf6wBiq24T2+0UzoHW2yDaiAowr6iQKBgQDDs2eJTopFejt47EMm
|
||||
zY//e8fyUDKx07CWwUZKP78IMTZcp8BwHmKTSY+VbQLvVYnNxADkckYapokjU3a7
|
||||
GMOXxvKVLqfgU/oUBIkRFDRo+nasFCd5cPpnYjQ6lGalBkDghVlyvq7kKF/MPtIK
|
||||
dLqsV3tZDXoHX2mjTga94ygTZwKBgQC7pa+eZdWiz275mpYql9Gs6N3G3HhcpRxb
|
||||
oWYWekCtZQ9gtecpvA9e0tW3ShoF19ksWYGpM4vxAOXul5Ei9IPVVNHc7RvGF5CO
|
||||
0Zryx3qaVk7E6XQc3BQVVAenT2fNiv3+fkCMeAvcZBZOGiPPziCCT3wU3bd3tsn8
|
||||
EsMiAPvTyQKBgEs0iWhBr29NrsckfBXQTzMN/WOIIEMoJ6d3dKyZ3K6oQszOhmxP
|
||||
sPALB8uTjdotk/xoAzPHGlupfe/+ZhU2Sgvsn1JnEIprmyHQMGBI1G83OR2dzSGl
|
||||
IgVSvuF4IA3w3kOp2xr2Xj09qrrRtWPhQc9y+urY+/kTWIQyOvMD9WWnAoGADdUN
|
||||
2BBLqj++P3oMvcEJPMTBrGoOGU42g+6m1ttWLzH26zsdei8ZtvS1ulglCO87XBCR
|
||||
BUb+dtqJGIhls3zwxuYEvlNgK78K8ewzjtfzirL4BX3sCECU3mmeUtAAp98qD/uA
|
||||
iJpEzY83MbStlSDtto1jaSpa3uFDjGhZqAUIizkCgYABf/HSMwWsnvRgo0JMmDru
|
||||
3L/xScJiFVdFV9vNiSckjUYhx7lgFqEjsDEN1mZiRIa2UhLOqpo5S/cIXfMkeEPh
|
||||
9Rusf9STwNwOOyhFRrHCxqhTH6Ivnj6oZSM/UrS8GvCVMPWs1z8k45FWKI/IRXQq
|
||||
AT5PRKihrWbg63/rXaSCxw==
|
||||
-----END PRIVATE KEY-----`;
|
||||
|
||||
const jwks = '{"keys":[{"use":"sig","kty":"RSA","n":"j3K1g8I4e-J7tYH2KPcsFSETHnXz8GzYg4x56L8xwCHk6HNfi8N47v4qPTjBNyi1syIm1B19_S4pSrEkCooCd_ouyddyKriQ_vqM2PQvA6fz9-LcVWPhAkkEXSAWxEyycWAVlzsemLMP3fwlLgznRMNiITbzfe7E4eY1GXun_x-pnwqSd1-Uma6qyFE0y-q-C4_WUtz1F-Yr1OrHdq186Q7FEIKzYJrxjd8PZwB7ncQFL_8zhmuPNNmfs9eCwgSF-r-jyCu_WEOLPf-YR-9zHrAOr4M1koSXMlct4LsnXvnboaRAqNat3enSGXqq2s0MKgFt73KZhBSXV45KaHIg3w","e":"AQAB"}]}';
|
||||
|
||||
// 使用临时目录(跨平台兼容)
|
||||
const tmpDir = process.env.TMPDIR || process.env.TEMP || '/tmp';
|
||||
const keyFile = path.join(tmpDir, 'convex_jwt_key.txt');
|
||||
const jwksFile = path.join(tmpDir, 'convex_jwks.txt');
|
||||
|
||||
// 写入临时文件
|
||||
fs.writeFileSync(keyFile, privateKey);
|
||||
fs.writeFileSync(jwksFile, jwks);
|
||||
|
||||
console.log('Setting JWT_PRIVATE_KEY from file...');
|
||||
const keyContent = fs.readFileSync(keyFile, 'utf8');
|
||||
try {
|
||||
@@ -67,13 +67,13 @@ try {
|
||||
} catch (e) {
|
||||
console.error('✗ Failed to set JWKS:', e.message);
|
||||
}
|
||||
|
||||
// 清理临时文件
|
||||
try {
|
||||
fs.unlinkSync(keyFile);
|
||||
fs.unlinkSync(jwksFile);
|
||||
} catch (e) {
|
||||
// 忽略清理错误
|
||||
}
|
||||
|
||||
console.log('Done! You can now test registration.');
|
||||
|
||||
// 清理临时文件
|
||||
try {
|
||||
fs.unlinkSync(keyFile);
|
||||
fs.unlinkSync(jwksFile);
|
||||
} catch (e) {
|
||||
// 忽略清理错误
|
||||
}
|
||||
|
||||
console.log('Done! You can now test registration.');
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { exportJWK, exportPKCS8, generateKeyPair } from "jose";
|
||||
|
||||
const keys = await generateKeyPair("RS256", { extractable: true });
|
||||
const privateKey = await exportPKCS8(keys.privateKey);
|
||||
const publicKey = await exportJWK(keys.publicKey);
|
||||
const jwks = JSON.stringify({ keys: [{ use: "sig", ...publicKey }] });
|
||||
|
||||
process.stdout.write(
|
||||
`JWT_PRIVATE_KEY="${privateKey.trimEnd().replace(/\n/g, " ")}"`,
|
||||
);
|
||||
process.stdout.write("\n");
|
||||
process.stdout.write(`JWKS=${jwks}`);
|
||||
process.stdout.write("\n");
|
||||
import { exportJWK, exportPKCS8, generateKeyPair } from "jose";
|
||||
|
||||
const keys = await generateKeyPair("RS256", { extractable: true });
|
||||
const privateKey = await exportPKCS8(keys.privateKey);
|
||||
const publicKey = await exportJWK(keys.publicKey);
|
||||
const jwks = JSON.stringify({ keys: [{ use: "sig", ...publicKey }] });
|
||||
|
||||
process.stdout.write(
|
||||
`JWT_PRIVATE_KEY="${privateKey.trimEnd().replace(/\n/g, " ")}"`,
|
||||
);
|
||||
process.stdout.write("\n");
|
||||
process.stdout.write(`JWKS=${jwks}`);
|
||||
process.stdout.write("\n");
|
||||
|
||||
@@ -1,330 +1,330 @@
|
||||
#!/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);
|
||||
});
|
||||
|
||||
#!/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);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
/**
|
||||
* 注册测试账号脚本
|
||||
*
|
||||
* 用途:快速注册测试账号,方便开发和测试
|
||||
* 运行:node scripts/register-test-user.js
|
||||
*/
|
||||
|
||||
const TEST_CREDENTIALS = {
|
||||
email: "test@example.com",
|
||||
password: "Test123456",
|
||||
name: "测试用户",
|
||||
};
|
||||
|
||||
async function registerTestUser() {
|
||||
const baseUrl = "http://localhost:3000";
|
||||
|
||||
console.log("正在注册测试账号...");
|
||||
console.log(`邮箱: ${TEST_CREDENTIALS.email}`);
|
||||
console.log(`密码: ${TEST_CREDENTIALS.password}`);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/auth/signin`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: TEST_CREDENTIALS.email,
|
||||
password: TEST_CREDENTIALS.password,
|
||||
name: TEST_CREDENTIALS.name,
|
||||
flow: "signUp",
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
console.log("✓ 测试账号注册成功!");
|
||||
console.log(`\n您现在可以使用以下凭据登录:`);
|
||||
console.log(` 邮箱: ${TEST_CREDENTIALS.email}`);
|
||||
console.log(` 密码: ${TEST_CREDENTIALS.password}`);
|
||||
console.log(`\n或在登录页面点击"测试账号快速登录"按钮。`);
|
||||
} else if (response.status === 501) {
|
||||
console.log("ℹ API 路由暂未实现,请通过浏览器手动注册:");
|
||||
console.log(` 1. 访问 http://localhost:3000/auth`);
|
||||
console.log(` 2. 点击"还没有账户?立即注册"`);
|
||||
console.log(` 3. 填写:`);
|
||||
console.log(` 邮箱: ${TEST_CREDENTIALS.email}`);
|
||||
console.log(` 密码: ${TEST_CREDENTIALS.password}`);
|
||||
console.log(` 姓名: ${TEST_CREDENTIALS.name}`);
|
||||
} else {
|
||||
console.error("✗ 注册失败:", result.error || result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("✗ 请求失败:", error.message);
|
||||
console.log("\n请确保开发服务器正在运行 (pnpm dev)");
|
||||
}
|
||||
}
|
||||
|
||||
registerTestUser();
|
||||
/**
|
||||
* 注册测试账号脚本
|
||||
*
|
||||
* 用途:快速注册测试账号,方便开发和测试
|
||||
* 运行:node scripts/register-test-user.js
|
||||
*/
|
||||
|
||||
const TEST_CREDENTIALS = {
|
||||
email: "test@example.com",
|
||||
password: "Test123456",
|
||||
name: "测试用户",
|
||||
};
|
||||
|
||||
async function registerTestUser() {
|
||||
const baseUrl = "http://localhost:3000";
|
||||
|
||||
console.log("正在注册测试账号...");
|
||||
console.log(`邮箱: ${TEST_CREDENTIALS.email}`);
|
||||
console.log(`密码: ${TEST_CREDENTIALS.password}`);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/auth/signin`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: TEST_CREDENTIALS.email,
|
||||
password: TEST_CREDENTIALS.password,
|
||||
name: TEST_CREDENTIALS.name,
|
||||
flow: "signUp",
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
console.log("✓ 测试账号注册成功!");
|
||||
console.log(`\n您现在可以使用以下凭据登录:`);
|
||||
console.log(` 邮箱: ${TEST_CREDENTIALS.email}`);
|
||||
console.log(` 密码: ${TEST_CREDENTIALS.password}`);
|
||||
console.log(`\n或在登录页面点击"测试账号快速登录"按钮。`);
|
||||
} else if (response.status === 501) {
|
||||
console.log("ℹ API 路由暂未实现,请通过浏览器手动注册:");
|
||||
console.log(` 1. 访问 http://localhost:3000/auth`);
|
||||
console.log(` 2. 点击"还没有账户?立即注册"`);
|
||||
console.log(` 3. 填写:`);
|
||||
console.log(` 邮箱: ${TEST_CREDENTIALS.email}`);
|
||||
console.log(` 密码: ${TEST_CREDENTIALS.password}`);
|
||||
console.log(` 姓名: ${TEST_CREDENTIALS.name}`);
|
||||
} else {
|
||||
console.error("✗ 注册失败:", result.error || result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("✗ 请求失败:", error.message);
|
||||
console.log("\n请确保开发服务器正在运行 (pnpm dev)");
|
||||
}
|
||||
}
|
||||
|
||||
registerTestUser();
|
||||
|
||||
@@ -12,32 +12,32 @@ function setConvexEnv(name, value) {
|
||||
const privateKey = `-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCPcrWDwjh74nu1
|
||||
gfYo9ywVIRMedfPwbNiDjHnovzHAIeToc1+Lw3ju/io9OME3KLWzIibUHX39LilK
|
||||
sSQKigJ3+i7J13IquJD++ozY9C8Dp/P34txVY+ECSQRdIBbETLJxYBWXOx6Ysw/d
|
||||
/CUuDOdEw2IhNvN97sTh5jUZe6f/H6mfCpJ3X5SZrqrIUTTL6r4Lj9ZS3PUX5ivU
|
||||
6sd2rXzpDsUQgrNgmvGN3w9nAHudxAUv/zOGa4802Z+z14LCBIX6v6PIK79YQ4s9
|
||||
/5hH73MesA6vgzWShJcyVy3guyde+duhpECo1q3d6dIZeqrazQwqAW3vcpmEFJdX
|
||||
jkpociDfAgMBAAECggEAAYtRE+mH1SGThlkvTrKWeWXBQG8xoJFzZTsiZtSEExbq
|
||||
UWxIh4cjqqL2znDpd5ALILIJ6/ejTxHrpN+yTSC+NQ9u6IJWusoA2ZXV5VH/nZD1
|
||||
yeHZ0FuCZRVnJB9/zz4qH5lSsi2TPz6SOagIuG2wIafeyw+94EmtOedSBAO2Q8NN
|
||||
3jHoINBCyRu6hU3ml0h7daoIhUw9ONI7MZUlYvuV7Ti3yf+czzpqwYx3gZA39sa8
|
||||
qUbJPFk6ts+CVjqSAdYVSfU3TC1Us1usOM3+04mACp3vc6ZUKxnB4Dsk/sSHj71n
|
||||
EcZ3fOR7EgjmXf6wBiq24T2+0UzoHW2yDaiAowr6iQKBgQDDs2eJTopFejt47EMm
|
||||
zY//e8fyUDKx07CWwUZKP78IMTZcp8BwHmKTSY+VbQLvVYnNxADkckYapokjU3a7
|
||||
GMOXxvKVLqfgU/oUBIkRFDRo+nasFCd5cPpnYjQ6lGalBkDghVlyvq7kKF/MPtIK
|
||||
dLqsV3tZDXoHX2mjTga94ygTZwKBgQC7pa+eZdWiz275mpYql9Gs6N3G3HhcpRxb
|
||||
oWYWekCtZQ9gtecpvA9e0tW3ShoF19ksWYGpM4vxAOXul5Ei9IPVVNHc7RvGF5CO
|
||||
0Zryx3qaVk7E6XQc3BQVVAenT2fNiv3+fkCMeAvcZBZOGiPPziCCT3wU3bd3tsn8
|
||||
EsMiAPvTyQKBgEs0iWhBr29NrsckfBXQTzMN/WOIIEMoJ6d3dKyZ3K6oQszOhmxP
|
||||
sPALB8uTjdotk/xoAzPHGlupfe/+ZhU2Sgvsn1JnEIprmyHQMGBI1G83OR2dzSGl
|
||||
IgVSvuF4IA3w3kOp2xr2Xj09qrrRtWPhQc9y+urY+/kTWIQyOvMD9WWnAoGADdUN
|
||||
2BBLqj++P3oMvcEJPMTBrGoOGU42g+6m1ttWLzH26zsdei8ZtvS1ulglCO87XBCR
|
||||
BUb+dtqJGIhls3zwxuYEvlNgK78K8ewzjtfzirL4BX3sCECU3mmeUtAAp98qD/uA
|
||||
iJpEzY83MbStlSDtto1jaSpa3uFDjGhZqAUIizkCgYABf/HSMwWsnvRgo0JMmDru
|
||||
3L/xScJiFVdFV9vNiSckjUYhx7lgFqEjsDEN1mZiRIa2UhLOqpo5S/cIXfMkeEPh
|
||||
9Rusf9STwNwOOyhFRrHCxqhTH6Ivnj6oZSM/UrS8GvCVMPWs1z8k45FWKI/IRXQq
|
||||
AT5PRKihrWbg63/rXaSCxw==
|
||||
-----END PRIVATE KEY-----`;
|
||||
|
||||
sSQKigJ3+i7J13IquJD++ozY9C8Dp/P34txVY+ECSQRdIBbETLJxYBWXOx6Ysw/d
|
||||
/CUuDOdEw2IhNvN97sTh5jUZe6f/H6mfCpJ3X5SZrqrIUTTL6r4Lj9ZS3PUX5ivU
|
||||
6sd2rXzpDsUQgrNgmvGN3w9nAHudxAUv/zOGa4802Z+z14LCBIX6v6PIK79YQ4s9
|
||||
/5hH73MesA6vgzWShJcyVy3guyde+duhpECo1q3d6dIZeqrazQwqAW3vcpmEFJdX
|
||||
jkpociDfAgMBAAECggEAAYtRE+mH1SGThlkvTrKWeWXBQG8xoJFzZTsiZtSEExbq
|
||||
UWxIh4cjqqL2znDpd5ALILIJ6/ejTxHrpN+yTSC+NQ9u6IJWusoA2ZXV5VH/nZD1
|
||||
yeHZ0FuCZRVnJB9/zz4qH5lSsi2TPz6SOagIuG2wIafeyw+94EmtOedSBAO2Q8NN
|
||||
3jHoINBCyRu6hU3ml0h7daoIhUw9ONI7MZUlYvuV7Ti3yf+czzpqwYx3gZA39sa8
|
||||
qUbJPFk6ts+CVjqSAdYVSfU3TC1Us1usOM3+04mACp3vc6ZUKxnB4Dsk/sSHj71n
|
||||
EcZ3fOR7EgjmXf6wBiq24T2+0UzoHW2yDaiAowr6iQKBgQDDs2eJTopFejt47EMm
|
||||
zY//e8fyUDKx07CWwUZKP78IMTZcp8BwHmKTSY+VbQLvVYnNxADkckYapokjU3a7
|
||||
GMOXxvKVLqfgU/oUBIkRFDRo+nasFCd5cPpnYjQ6lGalBkDghVlyvq7kKF/MPtIK
|
||||
dLqsV3tZDXoHX2mjTga94ygTZwKBgQC7pa+eZdWiz275mpYql9Gs6N3G3HhcpRxb
|
||||
oWYWekCtZQ9gtecpvA9e0tW3ShoF19ksWYGpM4vxAOXul5Ei9IPVVNHc7RvGF5CO
|
||||
0Zryx3qaVk7E6XQc3BQVVAenT2fNiv3+fkCMeAvcZBZOGiPPziCCT3wU3bd3tsn8
|
||||
EsMiAPvTyQKBgEs0iWhBr29NrsckfBXQTzMN/WOIIEMoJ6d3dKyZ3K6oQszOhmxP
|
||||
sPALB8uTjdotk/xoAzPHGlupfe/+ZhU2Sgvsn1JnEIprmyHQMGBI1G83OR2dzSGl
|
||||
IgVSvuF4IA3w3kOp2xr2Xj09qrrRtWPhQc9y+urY+/kTWIQyOvMD9WWnAoGADdUN
|
||||
2BBLqj++P3oMvcEJPMTBrGoOGU42g+6m1ttWLzH26zsdei8ZtvS1ulglCO87XBCR
|
||||
BUb+dtqJGIhls3zwxuYEvlNgK78K8ewzjtfzirL4BX3sCECU3mmeUtAAp98qD/uA
|
||||
iJpEzY83MbStlSDtto1jaSpa3uFDjGhZqAUIizkCgYABf/HSMwWsnvRgo0JMmDru
|
||||
3L/xScJiFVdFV9vNiSckjUYhx7lgFqEjsDEN1mZiRIa2UhLOqpo5S/cIXfMkeEPh
|
||||
9Rusf9STwNwOOyhFRrHCxqhTH6Ivnj6oZSM/UrS8GvCVMPWs1z8k45FWKI/IRXQq
|
||||
AT5PRKihrWbg63/rXaSCxw==
|
||||
-----END PRIVATE KEY-----`;
|
||||
|
||||
const jwks = '{"keys":[{"use":"sig","kty":"RSA","n":"j3K1g8I4e-J7tYH2KPcsFSETHnXz8GzYg4x56L8xwCHk6HNfi8N47v4qPTjBNyi1syIm1B19_S4pSrEkCooCd_ouyddyKriQ_vqM2PQvA6fz9-LcVWPhAkkEXSAWxEyycWAVlzsemLMP3fwlLgznRMNiITbzfe7E4eY1GXun_x-pnwqSd1-Uma6qyFE0y-q-C4_WUtz1F-Yr1OrHdq186Q7FEIKzYJrxjd8PZwB7ncQFL_8zhmuPNNmfs9eCwgSF-r-jyCu_WEOLPf-YR-9zHrAOr4M1koSXMlct4LsnXvnboaRAqNat3enSGXqq2s0MKgFt73KZhBSXV45KaHIg3w","e":"AQAB"}]}';
|
||||
|
||||
console.log('Setting JWT_PRIVATE_KEY...');
|
||||
|
||||
Reference in New Issue
Block a user