b0f5d6a51c
Split platform admin and ops APIs into standalone admin-server.mjs with network guards; simplify billing to RMB token pricing, refactor user auth, and add rsync deploy plus local-test scripts and docs. Co-authored-by: Cursor <cursoragent@cursor.com>
169 lines
4.7 KiB
JavaScript
169 lines
4.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Unified HTTPS reverse proxy for local test domains.
|
|
*
|
|
* test.tkmind.cn -> Vite :5173 (H5 + HMR)
|
|
* testadm.tkmind.cn -> admin :8082
|
|
* testpla.tkmind.cn -> Plaza :3001
|
|
* testops.tkmind.cn -> Ops :3002
|
|
*
|
|
* Requires root on macOS for :443:
|
|
* sudo pnpm dev:local-proxy
|
|
*/
|
|
import http from 'node:http';
|
|
import https from 'node:https';
|
|
import fs from 'node:fs';
|
|
import {
|
|
ADMIN_HOST,
|
|
H5_HOST,
|
|
OPS_HOST,
|
|
PLAZA_HOST,
|
|
PUBLIC_PORT,
|
|
PUBLIC_SCHEME,
|
|
publicUrl,
|
|
} from './local-test-config.mjs';
|
|
import { ensureLocalTestTls } from './local-test-tls.mjs';
|
|
|
|
const listenHost = process.env.LOCAL_TEST_PROXY_BIND ?? '0.0.0.0';
|
|
const targetHost = process.env.LOCAL_TEST_PROXY_TARGET ?? '127.0.0.1';
|
|
const vitePort = Number(process.env.VITE_PORT ?? 5173);
|
|
const adminPort = Number(process.env.ADMIN_PORT ?? 8082);
|
|
const plazaPort = Number(process.env.PLAZA_PORT ?? 3001);
|
|
const opsPort = Number(process.env.OPS_PORT ?? 3002);
|
|
const useHttps = PUBLIC_SCHEME === 'https';
|
|
|
|
const ROUTES = {
|
|
[H5_HOST.toLowerCase()]: { port: vitePort },
|
|
[ADMIN_HOST.toLowerCase()]: { port: adminPort },
|
|
[PLAZA_HOST.toLowerCase()]: { port: plazaPort, rewriteHost: true },
|
|
[OPS_HOST.toLowerCase()]: { port: opsPort, rewriteHost: true },
|
|
};
|
|
|
|
function routeFor(req) {
|
|
const hostname = String(req.headers.host ?? '').split(':')[0].toLowerCase();
|
|
return ROUTES[hostname] ? { hostname, ...ROUTES[hostname] } : null;
|
|
}
|
|
|
|
function proxyRequest(clientReq, clientRes) {
|
|
const route = routeFor(clientReq);
|
|
if (!route) {
|
|
clientRes.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
clientRes.end(`Unknown host: ${clientReq.headers.host ?? '(missing)'}`);
|
|
return;
|
|
}
|
|
|
|
const headers = { ...clientReq.headers };
|
|
if (route.rewriteHost) {
|
|
headers.host = `${route.hostname}:${route.port}`;
|
|
}
|
|
|
|
const proxyReq = http.request(
|
|
{
|
|
hostname: targetHost,
|
|
port: route.port,
|
|
method: clientReq.method,
|
|
path: clientReq.url,
|
|
headers,
|
|
},
|
|
(proxyRes) => {
|
|
clientRes.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
|
|
proxyRes.pipe(clientRes);
|
|
},
|
|
);
|
|
|
|
proxyReq.on('error', (err) => {
|
|
if (!clientRes.headersSent) {
|
|
clientRes.writeHead(502, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
}
|
|
clientRes.end(
|
|
`Upstream unavailable (${route.hostname} -> ${targetHost}:${route.port}): ${err.message}`,
|
|
);
|
|
});
|
|
|
|
clientReq.pipe(proxyReq);
|
|
}
|
|
|
|
function proxyUpgrade(clientReq, clientSocket, head) {
|
|
const route = routeFor(clientReq);
|
|
if (!route) {
|
|
clientSocket.destroy();
|
|
return;
|
|
}
|
|
|
|
const headers = { ...clientReq.headers };
|
|
if (route.rewriteHost) {
|
|
headers.host = `${route.hostname}:${route.port}`;
|
|
}
|
|
|
|
const proxyReq = http.request({
|
|
hostname: targetHost,
|
|
port: route.port,
|
|
method: clientReq.method,
|
|
path: clientReq.url,
|
|
headers,
|
|
});
|
|
|
|
proxyReq.on('upgrade', (proxyRes, proxySocket, proxyHead) => {
|
|
clientSocket.write(
|
|
`HTTP/1.1 ${proxyRes.statusCode ?? 101} ${proxyRes.statusMessage ?? 'Switching Protocols'}\r\n` +
|
|
Object.entries(proxyRes.headers)
|
|
.filter(([, value]) => value != null)
|
|
.map(([key, value]) => `${key}: ${value}`)
|
|
.join('\r\n') +
|
|
'\r\n\r\n',
|
|
);
|
|
if (proxyHead.length) proxySocket.write(proxyHead);
|
|
proxySocket.pipe(clientSocket);
|
|
clientSocket.pipe(proxySocket);
|
|
});
|
|
|
|
proxyReq.on('error', () => clientSocket.destroy());
|
|
proxyReq.end(head);
|
|
}
|
|
|
|
function bindErrorHandler(server) {
|
|
server.on('error', (err) => {
|
|
if (err.code === 'EACCES') {
|
|
console.error(`无法监听 ${PUBLIC_PORT} 端口。若使用 443 需 sudo;默认 8443 无需 root。`);
|
|
process.exit(1);
|
|
}
|
|
if (err.code === 'EADDRINUSE') {
|
|
console.error(`端口 ${PUBLIC_PORT} 已被占用。`);
|
|
process.exit(1);
|
|
}
|
|
throw err;
|
|
});
|
|
}
|
|
|
|
let server;
|
|
if (useHttps) {
|
|
const { keyPath, certPath } = ensureLocalTestTls();
|
|
server = https.createServer(
|
|
{
|
|
key: fs.readFileSync(keyPath),
|
|
cert: fs.readFileSync(certPath),
|
|
},
|
|
proxyRequest,
|
|
);
|
|
} else {
|
|
server = http.createServer(proxyRequest);
|
|
}
|
|
|
|
server.on('upgrade', proxyUpgrade);
|
|
bindErrorHandler(server);
|
|
|
|
server.listen(PUBLIC_PORT, listenHost, () => {
|
|
const scheme = useHttps ? 'https' : 'http';
|
|
console.log(`Local test proxy (${scheme}:${PUBLIC_PORT})`);
|
|
console.log(` ${publicUrl(H5_HOST)} -> :${vitePort} (H5)`);
|
|
console.log(` ${publicUrl(ADMIN_HOST)} -> :${adminPort} (memind_adm)`);
|
|
console.log(` ${publicUrl(PLAZA_HOST)}/plaza -> :${plazaPort} (Plaza)`);
|
|
console.log(` ${publicUrl(OPS_HOST)}/ops/ -> :${opsPort} (Ops)`);
|
|
});
|
|
|
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
process.on(signal, () => {
|
|
server.close(() => process.exit(0));
|
|
});
|
|
}
|