From 81631ef08f129497b620f6bec239897f53df47df Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Sun, 16 Aug 2026 21:17:10 +0700 Subject: [PATCH] fix: don't read Cloudflare's injected jsd script as a refusal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit demonicscans.org turned JS detections on, so Cloudflare began injecting /cdn-cgi/challenge-platform/scripts/jsd/main.js into ordinary 200 pages. isInterstitial matched the bare /cdn-cgi/challenge-platform/ prefix, so every real demonic series page became errChallengeHeld: two per pass parked the Lane in the 15m refusal backoff while plain TLS was in fact returning the full page (200, 133KB, 22 chapter anchors, verified live 2026-08-16). Match the challenge orchestration path /cdn-cgi/challenge-platform/h/ instead — served only by the interstitial itself, stable across its wording and locale. --- backend/AGENTS.md | 7 +++++++ backend/internal/latest/browser.go | 13 ++++++++++--- backend/internal/latest/browser_test.go | 19 +++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/backend/AGENTS.md b/backend/AGENTS.md index 23ecc92..9d35324 100644 --- a/backend/AGENTS.md +++ b/backend/AGENTS.md @@ -127,6 +127,13 @@ Guidance for OpenCode (and Claude Code) working under `backend/`. See root `AGEN and cover work (both healing a stored source URL and filling a blank from the series page) runs in the background so a slow CDN can't consume a Lane's gap. + A refusal is only ever the challenge *page*: `isInterstitial` matches the + orchestration path `/cdn-cgi/challenge-platform/h/`, never the bare prefix. + Cloudflare injects `/cdn-cgi/challenge-platform/scripts/jsd/main.js` into + ordinary 200 pages once a zone turns JS detections on, which demonic did on + 2026-08-16 — the prefix match then read every real demonic page as a refusal + and parked that Lane in 15m backoff while plain TLS was returning the full + series page. Fetches use `bogdanfinn/tls-client` with Chrome profile as defence in depth against fingerprint-based blocking; any failure log and skip. kagane, comix and novelfull sit behind Cloudflare JavaScript challenges the TLS client diff --git a/backend/internal/latest/browser.go b/backend/internal/latest/browser.go index d8c7a20..88a7254 100644 --- a/backend/internal/latest/browser.go +++ b/backend/internal/latest/browser.go @@ -250,11 +250,18 @@ func browserConnectionLost(ctx context.Context) bool { const challengePollInterval = 2 * time.Second // isInterstitial reports whether html is Cloudflare's challenge page rather -// than the site's own. Matched on the challenge runtime's script path, which is -// stable across the interstitial's wording and locale — the visible "Just a +// than the site's own. Matched on the challenge orchestration path +// (/cdn-cgi/challenge-platform/h//orchestrate/...), which is stable +// across the interstitial's wording and locale — the visible "Just a // moment..." title is neither. +// +// The bare "/cdn-cgi/challenge-platform/" prefix is NOT enough: Cloudflare +// injects /cdn-cgi/challenge-platform/scripts/jsd/main.js into ordinary 200 +// pages when JS detections are on, so matching the prefix declared every real +// demonic page a refusal and parked that Lane in 15m backoff (observed +// 2026-08-16, demonic turned detections on). func isInterstitial(html string) bool { - return strings.Contains(html, "/cdn-cgi/challenge-platform/") + return strings.Contains(html, "/cdn-cgi/challenge-platform/h/") } // run navigates to target and re-reads until done reports an answer, bounded by diff --git a/backend/internal/latest/browser_test.go b/backend/internal/latest/browser_test.go index 34c20dc..4cabb75 100644 --- a/backend/internal/latest/browser_test.go +++ b/backend/internal/latest/browser_test.go @@ -117,6 +117,25 @@ func TestBrowserOnlyCoverURL(t *testing.T) { }) } } + +// The jsd script is injected into ordinary 200 pages when a zone turns JS +// detections on; only the orchestration path means the page itself is the +// challenge. Conflating the two parked the demonic Lane in refusal backoff +// while every fetch was in fact the real series page (observed 2026-08-16). +func TestIsInterstitial(t *testing.T) { + if !isInterstitial(challengeFixture) { + t.Fatal("challenge page not detected as interstitial") + } + const jsdInjected = `The Possessed Grappler + +Chapter 22` + if isInterstitial(jsdInjected) { + t.Fatal("real page carrying the injected jsd script misread as interstitial") + } + if got, ok := demonicLatestChapter("", jsdInjected); !ok || got.Label != "Chapter 22" { + t.Fatalf("demonicLatestChapter = %+v, ok = %v, want Chapter 22", got, ok) + } +} func TestClassifyBrowserInterruption(t *testing.T) { if err := classifyBrowserError(context.Background(), true, context.Canceled); !errors.Is(err, errBrowserInterrupted) { t.Fatalf("classifyBrowserError(context.Canceled) = %v, want browser interruption", err)