Fix Aeroflot shell navigation links

This commit is contained in:
2026-05-22 11:13:16 +03:00
parent 1158673fdf
commit 6e947f2aa9
5 changed files with 208 additions and 17 deletions
+85 -11
View File
@@ -16,6 +16,7 @@ import { execFile, spawn } from "node:child_process";
import { resolve } from "node:path";
import { existsSync } from "node:fs";
import { tmpdir } from "node:os";
import { rewriteAeroflotShellUrls } from "./aeroflot-url-rewrite.mjs";
const PUBLIC_PORT = 8080;
const MODERNJS_PORT = 8081;
@@ -25,6 +26,7 @@ const AEROFLOT_STATIC_TARGET = process.env.AEROFLOT_STATIC_TARGET || "https://ww
const SYSTEM_PROXY = process.env.https_proxy || process.env.HTTPS_PROXY || "";
const DEBUG_PROXY_BODY = process.env.DEBUG_PROXY_BODY === "1";
const LOCAL_PUBLIC_ORIGIN = `http://localhost:${PUBLIC_PORT}`;
const shellStaticAssetCache = new Map();
// Shared cookie jar so the Ngenix WAF cookie challenge (`ngenix_valid` +
// 307-to-self) only runs once per dev-server lifetime, not per request.
@@ -233,23 +235,86 @@ app.use(
pathRewrite: (path) => `/frontend/static${path}`,
selfHandleResponse: true,
on: {
proxyRes: responseInterceptor(async (buffer, proxyRes) => {
const contentType = proxyRes.headers["content-type"] ?? "";
proxyRes: responseInterceptor(async (buffer, proxyRes, req, res) => {
const cacheKey = req.originalUrl ?? req.url ?? "";
const contentType = inferShellStaticContentType(
cacheKey,
proxyRes.headers["content-type"],
);
if (!/(css|html|javascript|json|text)/i.test(String(contentType))) {
if (proxyRes.statusCode && proxyRes.statusCode < 500) {
shellStaticAssetCache.set(cacheKey, { body: buffer, contentType });
}
return buffer;
}
return buffer
.toString("utf8")
.replaceAll("https://gw.aeroflot.ru", "/gw")
.replaceAll("https://www.aeroflot.ru", "")
.replaceAll("https://aeroflot.ru", "");
if (proxyRes.statusCode && proxyRes.statusCode >= 500) {
const cached = shellStaticAssetCache.get(cacheKey);
if (cached) {
res.statusCode = 200;
res.statusMessage = "OK";
res.setHeader("content-type", cached.contentType);
return cached.body;
}
const retried = await retryAeroflotStaticAsset(cacheKey);
if (retried) {
const body = rewriteAeroflotShellUrls(retried.body);
shellStaticAssetCache.set(cacheKey, { body, contentType });
res.statusCode = 200;
res.statusMessage = "OK";
res.setHeader("content-type", contentType);
return body;
}
}
const body = rewriteAeroflotShellUrls(buffer.toString("utf8"));
if (!proxyRes.statusCode || proxyRes.statusCode < 500) {
shellStaticAssetCache.set(cacheKey, { body, contentType });
}
return body;
}),
},
...(SYSTEM_PROXY ? { agent: new HttpsProxyAgent(SYSTEM_PROXY) } : {}),
}),
);
function inferShellStaticContentType(requestPath, upstreamContentType) {
const contentType = String(upstreamContentType ?? "");
if (/(css|html|javascript|json|text)/i.test(contentType)) return contentType;
const pathname = requestPath.split("?")[0] ?? "";
if (pathname.endsWith(".js") || pathname.endsWith(".mjs")) return "application/javascript";
if (pathname.endsWith(".css")) return "text/css";
if (pathname.endsWith(".json")) return "application/json";
if (pathname.endsWith(".html")) return "text/html";
return contentType;
}
function retryAeroflotStaticAsset(originalUrl) {
const targetUrl = `${AEROFLOT_STATIC_TARGET}${originalUrl}`;
const buildArgs = (noproxy) => [
"-sS",
"-f",
"-L",
...(noproxy ? ["--noproxy", "*"] : []),
"-H", "Accept: */*",
"-H", "User-Agent: Mozilla/5.0",
targetUrl,
];
return new Promise((resolveRetry) => {
runCurlWithoutStatus(buildArgs(true), (direct) => {
if (!direct.err && direct.stdout) {
resolveRetry({ body: direct.stdout });
return;
}
runCurlWithoutStatus(buildArgs(false), (proxy) => {
resolveRetry(!proxy.err && proxy.stdout ? { body: proxy.stdout } : null);
});
});
});
}
app.use("/personal/services/internal/v.0.0.1/json/get_member_info", (_req, res) => {
res.status(200).json({ data: null });
});
@@ -337,6 +402,18 @@ function runCurl(args, cb) {
});
}
function runCurlWithoutStatus(args, cb) {
execFile("/usr/bin/curl", args, { maxBuffer: 10 * 1024 * 1024, timeout: 30000 }, (err, stdout, stderr) => {
if (err) {
console.warn(`static curl retry failed: ${err.message}`);
}
if (stderr) {
console.warn(`static curl retry stderr: ${stderr.substring(0, 300)}`);
}
cb({ err, stdout: stdout ?? "" });
});
}
function isSuccessfulUpstream({ err, stdout }) {
if (err) return false;
const lastNewline = stdout.lastIndexOf("\n");
@@ -395,10 +472,7 @@ function respondWithAeroflotFrontendResult(result, res) {
const lastNewline = result.stdout.lastIndexOf("\n");
const rawBody = lastNewline >= 0 ? result.stdout.substring(0, lastNewline) : result.stdout;
const body = rawBody
.replaceAll("https://gw.aeroflot.ru", "/gw")
.replaceAll("https://www.aeroflot.ru", "")
.replaceAll("https://aeroflot.ru", "");
const body = rewriteAeroflotShellUrls(rawBody);
const statusStr = lastNewline >= 0 ? result.stdout.substring(lastNewline + 1).trim() : "200";
const status = parseInt(statusStr) || 200;
const isJson = body.trimStart().startsWith("{") || body.trimStart().startsWith("[");