#!/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)); }); }