diff --git a/Makefile b/Makefile index 5e08b223..e60db889 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,8 @@ PNPM := pnpm PID_FILE := .dev.pid LOG_FILE := .dev.log API_TARGET ?= https://flights.test.aeroflot.ru -SIGNALR_HUB_URL ?= https://platform.test.aeroflot.ru/tracker/hub +TRACKER_TARGET ?= https://platform.test.aeroflot.ru +SIGNALR_HUB_URL ?= http://localhost:8080/tracker/hub # Development dev: @@ -48,10 +49,11 @@ dev: dev-full: @echo "Starting dev server with API proxy in background..." - @API_TARGET="$(API_TARGET)" SIGNALR_HUB_URL="$(SIGNALR_HUB_URL)" nohup $(PNPM) dev:full > $(LOG_FILE) 2>&1 & echo $$! > $(PID_FILE) + @API_TARGET="$(API_TARGET)" TRACKER_TARGET="$(TRACKER_TARGET)" SIGNALR_HUB_URL="$(SIGNALR_HUB_URL)" nohup $(PNPM) dev:full > $(LOG_FILE) 2>&1 & echo $$! > $(PID_FILE) @echo "Dev server started (PID: $$(cat $(PID_FILE)))" @echo " App & API: http://localhost:8080" @echo " API target: $(API_TARGET)" + @echo " Tracker target: $(TRACKER_TARGET)" @echo " SignalR hub: $(SIGNALR_HUB_URL)" @echo "View logs: make logs" diff --git a/modern.config.ts b/modern.config.ts index 7b146fc8..309e2d7a 100644 --- a/modern.config.ts +++ b/modern.config.ts @@ -18,7 +18,7 @@ const isRemote = buildTarget === "remote"; // '{z}/{x}/{y}', so we serialize the whole payload to base64 and decode // it in the browser at runtime. Base64 output is A-Z/a-z/0-9/+/=/ — no // braces for the template engine to grab. -const PUBLIC_ENV_KEYS = ["MAP_TILE_URL", "API_BASE_URL"] as const; +const PUBLIC_ENV_KEYS = ["MAP_TILE_URL", "API_BASE_URL", "SIGNALR_HUB_URL"] as const; const PUBLIC_ENV: Record = {}; for (const k of PUBLIC_ENV_KEYS) { const v = process.env[k]; diff --git a/scripts/dev-server.mjs b/scripts/dev-server.mjs index 1dd20752..1a049456 100644 --- a/scripts/dev-server.mjs +++ b/scripts/dev-server.mjs @@ -6,10 +6,12 @@ * /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) + * /tracker/* → https://platform.test.aeroflot.ru (SignalR, same-origin) * /* → localhost:8081 (Modern.js SSR + HMR) */ import express from "express"; import { createProxyMiddleware } from "http-proxy-middleware"; +import { HttpsProxyAgent } from "https-proxy-agent"; import { execFile, spawn } from "node:child_process"; import { resolve } from "node:path"; import { existsSync } from "node:fs"; @@ -18,6 +20,8 @@ import { tmpdir } from "node:os"; const PUBLIC_PORT = 8080; const MODERNJS_PORT = 8081; const API_TARGET = process.env.API_TARGET || "https://flights.test.aeroflot.ru"; +const TRACKER_TARGET = process.env.TRACKER_TARGET || "https://platform.test.aeroflot.ru"; +const SYSTEM_PROXY = process.env.https_proxy || process.env.HTTPS_PROXY || ""; const DEBUG_PROXY_BODY = process.env.DEBUG_PROXY_BODY === "1"; // Shared cookie jar so the Ngenix WAF cookie challenge (`ngenix_valid` + @@ -138,6 +142,21 @@ app.use(["/api", "/flights"], (req, res) => { } }); +// --- SignalR TrackerHub proxy --- +// Browser-direct localhost → platform.test.aeroflot.ru fails CORS. Keep the +// hub same-origin in development and let proxy-helper / gost route the +// upstream request through the TIM tunnel when HTTPS_PROXY is set. +const trackerProxy = createProxyMiddleware({ + target: TRACKER_TARGET, + changeOrigin: true, + secure: false, + ws: true, + logLevel: "warn", + pathRewrite: (path) => `/tracker${path}`, + ...(SYSTEM_PROXY ? { agent: new HttpsProxyAgent(SYSTEM_PROXY) } : {}), +}); +app.use("/tracker", trackerProxy); + function execCurlWithFallback(buildArgs, extraArgs, res) { runCurl([...extraArgs, ...buildArgs(true)], (direct) => { if (isSuccessfulUpstream(direct)) { @@ -252,12 +271,19 @@ 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(` /tracker/* → proxy → ${TRACKER_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); +server.on("upgrade", (req, socket, head) => { + if (req.url?.startsWith("/tracker")) { + trackerProxy.upgrade(req, socket, head); + return; + } + modernProxy.upgrade(req, socket, head); +}); process.on("SIGINT", () => { modernProcess.kill(); process.exit(); }); process.on("SIGTERM", () => { modernProcess.kill(); process.exit(); }); diff --git a/src/shared/signalr/connection.ts b/src/shared/signalr/connection.ts index 5e4bf3e3..3e05a098 100644 --- a/src/shared/signalr/connection.ts +++ b/src/shared/signalr/connection.ts @@ -228,9 +228,9 @@ async function defaultBuildConnection( url: string, delays: number[], ): Promise { - const { HubConnectionBuilder, HttpTransportType, LogLevel } = await import("@microsoft/signalr"); + const { HubConnectionBuilder, LogLevel } = await import("@microsoft/signalr"); return new HubConnectionBuilder() - .withUrl(url, { transport: HttpTransportType.WebSockets }) + .withUrl(url) .withAutomaticReconnect(delays) // Suppress SignalR's internal console logging. The hub is optional // (failures degrade to polling) and we already handle status via