Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81631ef08f | |||
| ba679223b2 |
@@ -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
|
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
|
the series page) runs in the background so a slow CDN can't consume a
|
||||||
Lane's gap.
|
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
|
Fetches use `bogdanfinn/tls-client` with Chrome profile as defence in depth
|
||||||
against fingerprint-based blocking; any failure log and skip. kagane, comix
|
against fingerprint-based blocking; any failure log and skip. kagane, comix
|
||||||
and novelfull sit behind Cloudflare JavaScript challenges the TLS client
|
and novelfull sit behind Cloudflare JavaScript challenges the TLS client
|
||||||
|
|||||||
@@ -250,11 +250,18 @@ func browserConnectionLost(ctx context.Context) bool {
|
|||||||
const challengePollInterval = 2 * time.Second
|
const challengePollInterval = 2 * time.Second
|
||||||
|
|
||||||
// isInterstitial reports whether html is Cloudflare's challenge page rather
|
// 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
|
// than the site's own. Matched on the challenge orchestration path
|
||||||
// stable across the interstitial's wording and locale — the visible "Just a
|
// (/cdn-cgi/challenge-platform/h/<b|g|x>/orchestrate/...), which is stable
|
||||||
|
// across the interstitial's wording and locale — the visible "Just a
|
||||||
// moment..." title is neither.
|
// 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 {
|
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
|
// run navigates to target and re-reads until done reports an answer, bounded by
|
||||||
|
|||||||
@@ -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 = `<html><head><title>The Possessed Grappler</title>
|
||||||
|
<script src="/cdn-cgi/challenge-platform/scripts/jsd/main.js"></script></head>
|
||||||
|
<body><a href="/chaptered.php?manga=13721&chapter=22">Chapter 22</a></body></html>`
|
||||||
|
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) {
|
func TestClassifyBrowserInterruption(t *testing.T) {
|
||||||
if err := classifyBrowserError(context.Background(), true, context.Canceled); !errors.Is(err, errBrowserInterrupted) {
|
if err := classifyBrowserError(context.Background(), true, context.Canceled); !errors.Is(err, errBrowserInterrupted) {
|
||||||
t.Fatalf("classifyBrowserError(context.Canceled) = %v, want browser interruption", err)
|
t.Fatalf("classifyBrowserError(context.Canceled) = %v, want browser interruption", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user