/** * Development server with same-origin API proxy. * Equivalent to Angular's proxy.conf.json + ng serve. * * Port 8080 (browser-facing): * /api/* → curl → https://flights.test.aeroflot.ru (bypasses WAF via curl TLS) * /flights/* → curl → https://flights.test.aeroflot.ru * /map/* → curl → https://flights.test.aeroflot.ru (binary JPEG tiles) * /* → localhost:8081 (Modern.js SSR + HMR) */ import express from "express"; import { createProxyMiddleware } from "http-proxy-middleware"; import { execFile, spawn } from "node:child_process"; import { resolve } from "node:path"; import { existsSync } from "node:fs"; import { tmpdir } from "node:os"; const PUBLIC_PORT = 8080; const MODERNJS_PORT = 8081; const API_TARGET = "https://flights.test.aeroflot.ru"; // Shared cookie jar so the Ngenix WAF cookie challenge (`ngenix_valid` + // 307-to-self) only runs once per dev-server lifetime, not per request. const COOKIE_JAR = resolve(tmpdir(), `aeroflot-dev-cookies-${process.pid}.txt`); // --- Start Modern.js on internal port --- console.log(`Starting Modern.js on :${MODERNJS_PORT}...`); // Resolve the modern binary directly to avoid DEP0190 (shell: true with spawn) const modernBin = resolve("node_modules", ".bin", "modern"); const modernProcess = existsSync(modernBin) ? spawn(modernBin, ["dev"], { stdio: "inherit", env: { ...process.env, PORT: String(MODERNJS_PORT) }, }) : spawn(process.execPath, [resolve("node_modules", "@modern-js/app-tools", "bin", "modern.js"), "dev"], { stdio: "inherit", env: { ...process.env, PORT: String(MODERNJS_PORT) }, }); modernProcess.on("error", (err) => { console.error("Modern.js failed:", err); process.exit(1); }); await new Promise((r) => setTimeout(r, 18000)); const app = express(); // --- Mock fallback for /api/appSettings when WAF blocks requests --- app.get("/api/appSettings", (req, res) => { res.json({ showDebugVersion: "False", uiOptions: { isTestVersion: "", filter: { schedule: { searchFrom: "30d", searchTo: "30d", timeStep: "" }, onlineboard: { searchFrom: "2d", searchTo: "14d", timeStep: "" }, }, buttons: { buyTicket: { period: { min: "2h", max: "72h" } }, flightStatus: { availableFrom: "24h", visible: "" }, }, }, }); }); // --- Tile proxy via curl (binary JPEG, pipe stdout straight to response) --- const TILE_CONTENT_TYPES = { ".jpeg": "image/jpeg", ".jpg": "image/jpeg", ".png": "image/png", ".webp": "image/webp", }; app.use("/map", (req, res) => { const targetUrl = `${API_TARGET}${req.originalUrl}`; const ext = req.path.substring(req.path.lastIndexOf(".")); const contentType = TILE_CONTENT_TYPES[ext] ?? "application/octet-stream"; res.status(200); res.setHeader("Content-Type", contentType); res.setHeader("Cache-Control", "public, max-age=86400"); const args = [ "-sS", "-f", // fail on HTTP 4xx/5xx "-L", // follow Ngenix 307-to-self cookie challenge "--noproxy", "*", // bypass any inherited HTTPS_PROXY — upstream is publicly reachable "-c", COOKIE_JAR, "-b", COOKIE_JAR, // persist `ngenix_valid` cookie across requests "-H", `Accept: image/webp,image/jpeg,image/*,*/*`, "-H", `User-Agent: ${req.headers["user-agent"] || "Mozilla/5.0"}`, "-H", `Referer: ${API_TARGET}/`, targetUrl, ]; const child = spawn("/usr/bin/curl", args, { stdio: ["ignore", "pipe", "pipe"] }); child.stdout.pipe(res); child.on("exit", (code) => { if (code !== 0) { // -f means upstream failed; close cleanly with empty body. res.end(); } }); }); // --- API proxy via curl (bypasses WAF TLS fingerprinting) --- app.use(["/api", "/flights"], (req, res) => { const targetUrl = `${API_TARGET}${req.originalUrl}`; // Use curl to make the request — it passes through the system proxy // with a proper TLS fingerprint that the WAF accepts. const args = [ "-s", // silent "-L", // follow Ngenix 307-to-self cookie challenge "--noproxy", "*", // bypass any inherited HTTPS_PROXY — upstream is publicly reachable "-c", COOKIE_JAR, "-b", COOKIE_JAR, // persist `ngenix_valid` cookie across requests "-H", `Accept: ${req.headers.accept || "application/json"}`, "-H", `User-Agent: ${req.headers["user-agent"] || "Mozilla/5.0"}`, "-H", `Accept-Language: ${req.headers["accept-language"] || "ru"}`, "-w", "\n%{http_code}", // append status code at end targetUrl, ]; if (req.method === "POST") { args.unshift("-X", "POST"); // Read body and pass to curl let body = ""; req.on("data", (chunk) => { body += chunk; }); req.on("end", () => { if (body) { args.push("-d", body); args.push("-H", "Content-Type: application/json"); } execCurl(args, res); }); } else { execCurl(args, res); } }); function execCurl(args, res) { execFile("/usr/bin/curl", args, { maxBuffer: 10 * 1024 * 1024, timeout: 30000 }, (err, stdout) => { if (err) { res.status(502).json({ error: err.message }); return; } // stdout = body + "\n" + statusCode (from -w "\n%{http_code}"). When // upstream returns an empty body the stdout starts with "\n", so a // `> 0` check mis-treats the status as body and falls back to 200 — // silently hiding real 4xx/5xx responses. Use `>= 0` and split // unconditionally when the newline is present. const lastNewline = stdout.lastIndexOf("\n"); const body = lastNewline >= 0 ? stdout.substring(0, lastNewline) : stdout; const statusStr = lastNewline >= 0 ? stdout.substring(lastNewline + 1).trim() : "200"; const status = parseInt(statusStr) || 200; const isJson = body.trimStart().startsWith("{") || body.trimStart().startsWith("["); res.status(status); res.set("Content-Type", isJson ? "application/json" : "text/html"); res.set("Access-Control-Allow-Origin", "*"); res.send(body); }); } // --- Everything else → Modern.js --- const modernProxy = createProxyMiddleware({ target: `http://localhost:${MODERNJS_PORT}`, changeOrigin: false, ws: true, logLevel: "silent", }); app.use(modernProxy); const server = app.listen(PUBLIC_PORT, () => { console.log(`\n ✓ Dev server: http://localhost:${PUBLIC_PORT}`); console.log(` /api/* → curl → ${API_TARGET}`); console.log(` /* → Modern.js :${MODERNJS_PORT}\n`); }); // Forward WebSocket upgrades to Modern.js HMR server explicitly, // preventing reconnection spam from http-proxy-middleware's built-in ws handling. server.on("upgrade", modernProxy.upgrade); process.on("SIGINT", () => { modernProcess.kill(); process.exit(); }); process.on("SIGTERM", () => { modernProcess.kill(); process.exit(); });