|
import http from 'http'; |
|
import fs from 'fs'; |
|
import path from 'path'; |
|
import { Router } from 'express'; |
|
import type { Request, Response, NextFunction } from 'express'; |
|
|
|
const routesJsonPath = path.join(__dirname, 'routes.json'); |
|
const routesJsonFile = path.basename(routesJsonPath); |
|
const routesJsonDir = path.dirname(routesJsonPath); |
|
|
|
let activeRoutesRouter = Router(); |
|
|
|
const errorHtmlContent = `<!DOCTYPE html> |
|
<html><head><title>Server Error</title></head> |
|
<body style="font-family: sans-serif; text-align: center; margin-top: 10%;"> |
|
<h1>502 Bad Gateway</h1> |
|
<p>The backend service appears to be offline or misconfigured.</p> |
|
</body></html>`; |
|
|
|
function sendErrorHtmlPage(res: Response, statusCode: number = 502) { |
|
if (res.headersSent) return; |
|
res.status(statusCode).type('text/html').send(errorHtmlContent); |
|
} |
|
|
|
interface RouteConfig { |
|
method: string; |
|
path: string; |
|
} |
|
|
|
interface RoutesFile { |
|
port: number; |
|
routes: RouteConfig[]; |
|
} |
|
|
|
let allRoutes: RouteConfig[] = []; |
|
let currentProxyPort: number | null = null; |
|
let portOnlineStatus: Record<number, boolean> = {}; |
|
let isCheckingPort = false; |
|
|
|
async function isPortOnline(port: number): Promise<boolean> { |
|
return new Promise(resolve => { |
|
const req = http.request({ hostname: 'localhost', port, method: 'HEAD', timeout: 500 }, () => { |
|
req.destroy(); |
|
resolve(true); |
|
}); |
|
req.on('error', () => resolve(false)); |
|
req.on('timeout', () => { |
|
req.destroy(); |
|
resolve(false); |
|
}); |
|
req.end(); |
|
}); |
|
} |
|
|
|
function rebuildActiveRouter() { |
|
const newRouter = Router(); |
|
|
|
if (!currentProxyPort || !portOnlineStatus[currentProxyPort]) { |
|
activeRoutesRouter = newRouter; |
|
console.log('[ProxyToServerTS] ✅ Router is active but empty (backend port is offline or not configured).'); |
|
return; |
|
} |
|
|
|
allRoutes.forEach(route => { |
|
const method = route.method.trim().toLowerCase(); |
|
|
|
if (typeof (newRouter as any)[method] === 'function') { |
|
(newRouter as any)[method](route.path, (req: Request, res: Response) => { |
|
const targetPort = currentProxyPort as number; |
|
const options: http.RequestOptions = { |
|
hostname: 'localhost', |
|
port: targetPort, |
|
path: req.originalUrl, |
|
method: req.method, |
|
headers: { ...req.headers, host: `localhost:${targetPort}` } |
|
}; |
|
|
|
if (options.headers) { |
|
const headers = options.headers as http.OutgoingHttpHeaders; |
|
if (headers.connection) { |
|
delete headers.connection; |
|
} |
|
} |
|
|
|
const backendRequest = http.request(options, backendResponse => { |
|
if (backendResponse.statusCode && backendResponse.statusCode >= 400) { |
|
sendErrorHtmlPage(res, backendResponse.statusCode); |
|
backendResponse.resume(); |
|
return; |
|
} |
|
|
|
res.writeHead(backendResponse.statusCode || 200, backendResponse.headers); |
|
backendResponse.pipe(res); |
|
}); |
|
|
|
backendRequest.on('error', (err) => { |
|
console.error(`[ProxyToServerTS] Backend request error for port ${targetPort}:`, err.message); |
|
sendErrorHtmlPage(res, 503) |
|
}); |
|
req.pipe(backendRequest); |
|
}); |
|
} |
|
}); |
|
|
|
activeRoutesRouter = newRouter; |
|
console.log(`[ProxyToServerTS] ✅ Router rebuilt successfully for port ${currentProxyPort} with ${allRoutes.length} routes.`); |
|
} |
|
|
|
function loadRoutesFromFile() { |
|
try { |
|
console.log(`[ProxyToServerTS] Attempting to load routes from ${routesJsonPath}`); |
|
if (!fs.existsSync(routesJsonPath)) { |
|
console.warn(`[ProxyToServerTS] 🟡 routes.json not found. Waiting for the file to be created...`); |
|
if (currentProxyPort !== null) { |
|
allRoutes = []; |
|
currentProxyPort = null; |
|
portOnlineStatus = {}; |
|
rebuildActiveRouter(); |
|
} |
|
return; |
|
} |
|
|
|
const content = fs.readFileSync(routesJsonPath, 'utf8'); |
|
const parsed = JSON.parse(content) as RoutesFile; |
|
|
|
if (typeof parsed.port !== 'number') { |
|
console.error(`[ProxyToServerTS] ❌ 'port' is missing or not a number in routes.json.`); |
|
currentProxyPort = null; |
|
allRoutes = []; |
|
rebuildActiveRouter(); |
|
return; |
|
} |
|
|
|
if (currentProxyPort !== parsed.port) { |
|
console.log(`[ProxyToServerTS] Port configuration changed from ${currentProxyPort || 'none'} to ${parsed.port}.`); |
|
currentProxyPort = parsed.port; |
|
portOnlineStatus = {}; |
|
} |
|
|
|
if (!Array.isArray(parsed.routes)) { |
|
console.warn(`[ProxyToServerTS] Invalid format: 'routes' key is not an array.`); |
|
return; |
|
} |
|
|
|
allRoutes = parsed.routes.filter(route => { |
|
if (!route.path || typeof route.path !== 'string' || !route.method) { |
|
console.warn(`[ProxyToServerTS] Invalid route found (missing path or method). Skipping.`, route); |
|
return false; |
|
} |
|
return true; |
|
}); |
|
|
|
console.log(`[ProxyToServerTS] ✔️ Loaded ${allRoutes.length} routes for port ${currentProxyPort}.`); |
|
checkPortStatus(); |
|
|
|
} catch (err) { |
|
console.error(`[ProxyToServerTS] ❌ Error loading or parsing routes.json: ${(err as Error).message}`); |
|
currentProxyPort = null; |
|
allRoutes = []; |
|
rebuildActiveRouter(); |
|
} |
|
} |
|
|
|
async function checkPortStatus() { |
|
if (isCheckingPort || currentProxyPort === null) return; |
|
isCheckingPort = true; |
|
|
|
const portToCheck = currentProxyPort; |
|
const wasOnline = portOnlineStatus[portToCheck]; |
|
const isOnline = await isPortOnline(portToCheck); |
|
|
|
if (wasOnline !== isOnline) { |
|
console.log(`[ProxyToServerTS] Port ${portToCheck} is now ${isOnline ? '🟢 ONLINE' : '🔴 OFFLINE'}`); |
|
portOnlineStatus[portToCheck] = isOnline; |
|
rebuildActiveRouter(); |
|
} |
|
|
|
isCheckingPort = false; |
|
} |
|
|
|
function setupWatcherAndInterval() { |
|
fs.watch(routesJsonDir, { persistent: true }, (eventType, filename) => { |
|
if (filename === routesJsonFile) { |
|
console.log(`[ProxyToServerTS] 🔄 Change detected in ${routesJsonFile}. Reloading...`); |
|
loadRoutesFromFile(); |
|
} |
|
}); |
|
|
|
setInterval(checkPortStatus, 30000); |
|
} |
|
|
|
loadRoutesFromFile(); |
|
setupWatcherAndInterval(); |
|
|
|
const mainProxyRouter = Router(); |
|
mainProxyRouter.use((req: Request, res: Response, next: NextFunction) => { |
|
activeRoutesRouter(req, res, next); |
|
}); |
|
|
|
export { mainProxyRouter as serverProxy }; |
|
|