Update Dockerfile and Compose configuration

This commit is contained in:
openhands committed 2026-09-06 19:19:25 +02:00
1 parent 35f66d879f
commit d1003bb89b
9 files changed
+2046 -13

No files matched your search

+15 -3
View File
@@ -1,5 +1,5 @@
import { execFile } from "node:child_process";
import { promises as fs } from "node:fs";
import { existsSync, promises as fs } from "node:fs";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
@@ -8,8 +8,17 @@ const execFileAsync = promisify(execFile);
* Some retro CDNs (e.g. Leet.city) block Node's fetch by TLS fingerprint
* (JA3/JA4) even though the same request succeeds from a real browser or
* curl. This helper shells out to `curl` as a fallback for those hosts.
*
* The runtime image ships `curl-impersonate` (a curl compiled with Chrome's
* exact TLS fingerprint, statically-bundled BoringSSL) at /opt/curl-impersonate
* because several sources enable Cloudflare `cf-mitigated: challenge` against
* the distro curl's fingerprint too. Prefer it when present, else fall back to
* the system curl.
*/
const IMPERSONATE_CURL = "/opt/curl-impersonate/curl_chrome116";
const CURL_BIN = existsSync(IMPERSONATE_CURL) ? IMPERSONATE_CURL : "curl";
const CURL_USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
@@ -37,7 +46,7 @@ export async function curlFetchText(
url: string,
timeoutMs = 30_000,
): Promise<string> {
const { stdout } = await execFileAsync("curl", curlArgs(url, []), {
const { stdout } = await execFileAsync(CURL_BIN, curlArgs(url, []), {
timeout: timeoutMs,
maxBuffer: 512 * 1024 * 1024,
});
@@ -49,10 +58,12 @@ export async function curlDownload(
url: string,
destPath: string,
timeoutMs = 30_000,
cookieHeader?: string,
): Promise<{ ok: boolean; size: number }> {
try {
const cookieArgs = cookieHeader ? ["-H", `Cookie: ${cookieHeader}`] : [];
await execFileAsync(
"curl",
CURL_BIN,
[
"-sSL",
"--compressed",
@@ -67,6 +78,7 @@ export async function curlDownload(
"Accept: application/octet-stream,*/*;q=0.9",
"-H",
"Accept-Language: en-US,en;q=0.9",
...cookieArgs,
"-o",
destPath,
url,
+47 -1
View File
@@ -1,7 +1,8 @@
import { promises as fs } from "node:fs";
import { getFlareSolverrCookies } from "@/lib/services/flare-solver";
import { parseNitroBundle } from "../../swf/nitro-builder";
import { browserHeaders } from "./browser-headers";
import { curlDownload } from "./curl-fetch";
export function validateSwfBytes(buffer: Buffer): boolean {
if (buffer.length < 8) return false;
@@ -58,6 +59,38 @@ export async function downloadFile(
const maxRetries = options?.maxRetries ?? 3;
const baseDelay = 1000;
const curlFallback = async (): Promise<boolean> => {
let curlRes = await curlDownload(url, destPath);
if (!curlRes.ok) {
const cookieHeader = await getFlareSolverrCookies(url);
curlRes = await curlDownload(
url,
destPath,
30_000,
cookieHeader ?? undefined,
);
}
if (!curlRes.ok) return false;
const validate = options?.validate;
if (validate) {
let valid = false;
try {
const buffer = await fs.readFile(destPath);
valid =
(validate === "swf" && validateSwfBytes(buffer)) ||
(validate === "png" && validatePngBytes(buffer)) ||
(validate === "nitro" && validateNitroBytes(buffer));
} catch {
valid = false;
}
if (!valid) {
await fs.unlink(destPath).catch(() => {});
return false;
}
}
return true;
};
for (let attempt = 0; attempt <= maxRetries; attempt++) {
let stage: "download" | "write" = "download";
try {
@@ -74,6 +107,13 @@ export async function downloadFile(
const deterministic =
res.status === 404 || res.status === 410 || res.status === 403;
if (deterministic || attempt === maxRetries) {
// HTTP 403 is often a TLS-fingerprint / Cloudflare challenge
// block (e.g. Leet.city) — retry via curl, with FlareSolverr
// clearance cookies if the plain curl is also challenged.
if (res.status === 403 && (await curlFallback())) {
const stat = await fs.stat(destPath);
return { ok: true, size: stat.size };
}
console.warn(
`[import-download] Download failed ${url}: ${res.status}${deterministic ? " (deterministic, not retrying)" : ` after ${maxRetries + 1} attempts`}`,
);
@@ -108,6 +148,12 @@ export async function downloadFile(
return { ok: true, size: buffer.length };
} catch (err) {
if (attempt === maxRetries || stage === "write") {
// TLS fingerprint aborts land here too — try curl/FlareSolverr
// before reporting the network failure.
if (stage === "download" && (await curlFallback())) {
const stat = await fs.stat(destPath);
return { ok: true, size: stat.size };
}
console.warn(
`[import-download] Download error ${url}:`,
(err as Error).message,