Create models/proxy-to-server.ts
Browse files
exocore-web/models/proxy-to-server.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import http from 'http';
|
| 2 |
+
import fs from 'fs';
|
| 3 |
+
import path from 'path';
|
| 4 |
+
import { Router } from 'express';
|
| 5 |
+
import type { Request, Response, NextFunction } from 'express';
|
| 6 |
+
|
| 7 |
+
const routesJsonPath = path.join(__dirname, 'routes.json');
|
| 8 |
+
const routesJsonFile = path.basename(routesJsonPath);
|
| 9 |
+
const routesJsonDir = path.dirname(routesJsonPath);
|
| 10 |
+
|
| 11 |
+
let activeRoutesRouter = Router();
|
| 12 |
+
|
| 13 |
+
const errorHtmlContent = `<!DOCTYPE html>
|
| 14 |
+
<html><head><title>Server Error</title></head>
|
| 15 |
+
<body style="font-family: sans-serif; text-align: center; margin-top: 10%;">
|
| 16 |
+
<h1>502 Bad Gateway</h1>
|
| 17 |
+
<p>The backend service appears to be offline or misconfigured.</p>
|
| 18 |
+
</body></html>`;
|
| 19 |
+
|
| 20 |
+
function sendErrorHtmlPage(res: Response, statusCode: number = 502) {
|
| 21 |
+
if (res.headersSent) return;
|
| 22 |
+
res.status(statusCode).type('text/html').send(errorHtmlContent);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
interface RouteConfig {
|
| 26 |
+
method: string;
|
| 27 |
+
path: string;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
interface RoutesFile {
|
| 31 |
+
port: number;
|
| 32 |
+
routes: RouteConfig[];
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
let allRoutes: RouteConfig[] = [];
|
| 36 |
+
let currentProxyPort: number | null = null;
|
| 37 |
+
let portOnlineStatus: Record<number, boolean> = {};
|
| 38 |
+
let isCheckingPort = false;
|
| 39 |
+
|
| 40 |
+
async function isPortOnline(port: number): Promise<boolean> {
|
| 41 |
+
return new Promise(resolve => {
|
| 42 |
+
const req = http.request({ hostname: 'localhost', port, method: 'HEAD', timeout: 500 }, () => {
|
| 43 |
+
req.destroy();
|
| 44 |
+
resolve(true);
|
| 45 |
+
});
|
| 46 |
+
req.on('error', () => resolve(false));
|
| 47 |
+
req.on('timeout', () => {
|
| 48 |
+
req.destroy();
|
| 49 |
+
resolve(false);
|
| 50 |
+
});
|
| 51 |
+
req.end();
|
| 52 |
+
});
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
function rebuildActiveRouter() {
|
| 56 |
+
const newRouter = Router();
|
| 57 |
+
|
| 58 |
+
if (!currentProxyPort || !portOnlineStatus[currentProxyPort]) {
|
| 59 |
+
activeRoutesRouter = newRouter;
|
| 60 |
+
console.log('[ProxyToServerTS] ✅ Router is active but empty (backend port is offline or not configured).');
|
| 61 |
+
return;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
allRoutes.forEach(route => {
|
| 65 |
+
const method = route.method.trim().toLowerCase();
|
| 66 |
+
|
| 67 |
+
if (typeof (newRouter as any)[method] === 'function') {
|
| 68 |
+
(newRouter as any)[method](route.path, (req: Request, res: Response) => {
|
| 69 |
+
const targetPort = currentProxyPort as number;
|
| 70 |
+
const options: http.RequestOptions = {
|
| 71 |
+
hostname: 'localhost',
|
| 72 |
+
port: targetPort,
|
| 73 |
+
path: req.originalUrl,
|
| 74 |
+
method: req.method,
|
| 75 |
+
headers: { ...req.headers, host: `localhost:${targetPort}` }
|
| 76 |
+
};
|
| 77 |
+
|
| 78 |
+
if (options.headers) {
|
| 79 |
+
const headers = options.headers as http.OutgoingHttpHeaders;
|
| 80 |
+
if (headers.connection) {
|
| 81 |
+
delete headers.connection;
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
const backendRequest = http.request(options, backendResponse => {
|
| 86 |
+
if (backendResponse.statusCode && backendResponse.statusCode >= 400) {
|
| 87 |
+
sendErrorHtmlPage(res, backendResponse.statusCode);
|
| 88 |
+
backendResponse.resume();
|
| 89 |
+
return;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
res.writeHead(backendResponse.statusCode || 200, backendResponse.headers);
|
| 93 |
+
backendResponse.pipe(res);
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
backendRequest.on('error', (err) => {
|
| 97 |
+
console.error(`[ProxyToServerTS] Backend request error for port ${targetPort}:`, err.message);
|
| 98 |
+
sendErrorHtmlPage(res, 503)
|
| 99 |
+
});
|
| 100 |
+
req.pipe(backendRequest);
|
| 101 |
+
});
|
| 102 |
+
}
|
| 103 |
+
});
|
| 104 |
+
|
| 105 |
+
activeRoutesRouter = newRouter;
|
| 106 |
+
console.log(`[ProxyToServerTS] ✅ Router rebuilt successfully for port ${currentProxyPort} with ${allRoutes.length} routes.`);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
function loadRoutesFromFile() {
|
| 110 |
+
try {
|
| 111 |
+
console.log(`[ProxyToServerTS] Attempting to load routes from ${routesJsonPath}`);
|
| 112 |
+
if (!fs.existsSync(routesJsonPath)) {
|
| 113 |
+
console.warn(`[ProxyToServerTS] 🟡 routes.json not found. Waiting for the file to be created...`);
|
| 114 |
+
if (currentProxyPort !== null) {
|
| 115 |
+
allRoutes = [];
|
| 116 |
+
currentProxyPort = null;
|
| 117 |
+
portOnlineStatus = {};
|
| 118 |
+
rebuildActiveRouter();
|
| 119 |
+
}
|
| 120 |
+
return;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
const content = fs.readFileSync(routesJsonPath, 'utf8');
|
| 124 |
+
const parsed = JSON.parse(content) as RoutesFile;
|
| 125 |
+
|
| 126 |
+
if (typeof parsed.port !== 'number') {
|
| 127 |
+
console.error(`[ProxyToServerTS] ❌ 'port' is missing or not a number in routes.json.`);
|
| 128 |
+
currentProxyPort = null;
|
| 129 |
+
allRoutes = [];
|
| 130 |
+
rebuildActiveRouter();
|
| 131 |
+
return;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
if (currentProxyPort !== parsed.port) {
|
| 135 |
+
console.log(`[ProxyToServerTS] Port configuration changed from ${currentProxyPort || 'none'} to ${parsed.port}.`);
|
| 136 |
+
currentProxyPort = parsed.port;
|
| 137 |
+
portOnlineStatus = {};
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
if (!Array.isArray(parsed.routes)) {
|
| 141 |
+
console.warn(`[ProxyToServerTS] Invalid format: 'routes' key is not an array.`);
|
| 142 |
+
return;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
allRoutes = parsed.routes.filter(route => {
|
| 146 |
+
if (!route.path || typeof route.path !== 'string' || !route.method) {
|
| 147 |
+
console.warn(`[ProxyToServerTS] Invalid route found (missing path or method). Skipping.`, route);
|
| 148 |
+
return false;
|
| 149 |
+
}
|
| 150 |
+
return true;
|
| 151 |
+
});
|
| 152 |
+
|
| 153 |
+
console.log(`[ProxyToServerTS] ✔️ Loaded ${allRoutes.length} routes for port ${currentProxyPort}.`);
|
| 154 |
+
checkPortStatus();
|
| 155 |
+
|
| 156 |
+
} catch (err) {
|
| 157 |
+
console.error(`[ProxyToServerTS] ❌ Error loading or parsing routes.json: ${(err as Error).message}`);
|
| 158 |
+
currentProxyPort = null;
|
| 159 |
+
allRoutes = [];
|
| 160 |
+
rebuildActiveRouter();
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
async function checkPortStatus() {
|
| 165 |
+
if (isCheckingPort || currentProxyPort === null) return;
|
| 166 |
+
isCheckingPort = true;
|
| 167 |
+
|
| 168 |
+
const portToCheck = currentProxyPort;
|
| 169 |
+
const wasOnline = portOnlineStatus[portToCheck];
|
| 170 |
+
const isOnline = await isPortOnline(portToCheck);
|
| 171 |
+
|
| 172 |
+
if (wasOnline !== isOnline) {
|
| 173 |
+
console.log(`[ProxyToServerTS] Port ${portToCheck} is now ${isOnline ? '🟢 ONLINE' : '🔴 OFFLINE'}`);
|
| 174 |
+
portOnlineStatus[portToCheck] = isOnline;
|
| 175 |
+
rebuildActiveRouter();
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
isCheckingPort = false;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
function setupWatcherAndInterval() {
|
| 182 |
+
fs.watch(routesJsonDir, { persistent: true }, (eventType, filename) => {
|
| 183 |
+
if (filename === routesJsonFile) {
|
| 184 |
+
console.log(`[ProxyToServerTS] 🔄 Change detected in ${routesJsonFile}. Reloading...`);
|
| 185 |
+
loadRoutesFromFile();
|
| 186 |
+
}
|
| 187 |
+
});
|
| 188 |
+
|
| 189 |
+
setInterval(checkPortStatus, 30000);
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
loadRoutesFromFile();
|
| 193 |
+
setupWatcherAndInterval();
|
| 194 |
+
|
| 195 |
+
const mainProxyRouter = Router();
|
| 196 |
+
mainProxyRouter.use((req: Request, res: Response, next: NextFunction) => {
|
| 197 |
+
activeRoutesRouter(req, res, next);
|
| 198 |
+
});
|
| 199 |
+
|
| 200 |
+
export { mainProxyRouter as serverProxy };
|