mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 08:01:25 +00:00
628ab5692e
* fix(cli): detect upstream auth-gateway HTML responses and add poll heartbeat * fix(cli): guard tar fallback, tighten cheap-path, case-insensitive ct, add tests
67 lines
2.3 KiB
TypeScript
67 lines
2.3 KiB
TypeScript
// Detects responses that look like an upstream auth-gateway challenge
|
|
// (Cloudflare Access SSO page, Vercel auth wall, basic-auth form, etc.) and
|
|
// surfaces a clear error instead of letting the caller treat the HTML body as
|
|
// a typed Windmill response.
|
|
|
|
const ACCESS_TITLE = /<title>\s*Sign in[^<]*Cloudflare Access\s*<\/title>/i;
|
|
const HTML_DOCTYPE = /^\s*<(!doctype|html)/i;
|
|
|
|
export class AuthGatewayChallengeError extends Error {
|
|
override name = "AuthGatewayChallengeError";
|
|
constructor(
|
|
public url: string,
|
|
public cfRay: string | undefined,
|
|
public cfMitigated: string | undefined,
|
|
public status: number,
|
|
public bodySnippet: string,
|
|
) {
|
|
const cfPart = [
|
|
cfRay ? `cf-ray=${cfRay}` : null,
|
|
cfMitigated ? `cf-mitigated=${cfMitigated}` : null,
|
|
]
|
|
.filter(Boolean)
|
|
.join(", ");
|
|
super(
|
|
`Got an HTML response from ${url} (status ${status}${cfPart ? `, ${cfPart}` : ""}). ` +
|
|
`The request was intercepted by an upstream auth gateway (likely Cloudflare Access) ` +
|
|
`before reaching Windmill. Verify the runner is on the right network or pass service-token headers via the HEADERS env var ` +
|
|
`(e.g. HEADERS="CF-Access-Client-Id: <id>, CF-Access-Client-Secret: <secret>"). ` +
|
|
`Body starts with: ${JSON.stringify(bodySnippet.slice(0, 120))}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function detectAuthGatewayChallenge(
|
|
response: Response,
|
|
url?: string,
|
|
): Promise<void> {
|
|
const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
const cfMitigated = response.headers.get("cf-mitigated") ?? undefined;
|
|
const looksHtml = contentType.includes("text/html");
|
|
|
|
// Cheap check first; only peek the body when something already smells off.
|
|
if (!looksHtml && cfMitigated !== "challenge") return;
|
|
|
|
let snippet = "";
|
|
try {
|
|
snippet = (await response.clone().text()).slice(0, 256);
|
|
} catch {
|
|
/* body unreadable — fall through */
|
|
}
|
|
|
|
const isChallenge =
|
|
cfMitigated === "challenge" ||
|
|
ACCESS_TITLE.test(snippet) ||
|
|
(looksHtml && HTML_DOCTYPE.test(snippet));
|
|
|
|
if (!isChallenge) return;
|
|
|
|
throw new AuthGatewayChallengeError(
|
|
url || response.url || "(unknown)",
|
|
response.headers.get("cf-ray") ?? undefined,
|
|
cfMitigated,
|
|
response.status,
|
|
snippet,
|
|
);
|
|
}
|