From af6674b4ed1de7de302f0bbb09c2953e9c3f4cb9 Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 10 Oct 2026 11:59:42 +0000 Subject: [PATCH] Route Cloudflare challenges to FlareSolverr on cumlouder and hypnotube raw paths Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01MUYdCh2nCVsQYgJAT7gwSi --- docs/provider-fix-todo.md | 7 +++++ src/providers/cumlouder.rs | 23 +++++++++++++- src/providers/hypnotube.rs | 63 +++++++++++++++++++++++++++++++------- src/util/requester.rs | 4 +-- 4 files changed, 83 insertions(+), 14 deletions(-) diff --git a/docs/provider-fix-todo.md b/docs/provider-fix-todo.md index 896a979..1f347ba 100644 --- a/docs/provider-fix-todo.md +++ b/docs/provider-fix-todo.md @@ -84,3 +84,10 @@ impersonation target; if a minimum client version is needed, gate on `ClientVers `porndish`, `supjav`, `jable`, `fapello`, `missav`, `wowxxx`, `hentaihaven` return Cloudflare 403 to yt-dlp/curl from this machine while the server itself returns items. `hsex` returned 429 (rate limit). + +## Cloudflare challenge rule + +Any request path that bypasses the requester's fallback chain must still hand a Cloudflare challenge (403/503 or +"Just a moment...") to FlareSolverr. `cumlouder` (raw 404-aware listing fetch) now falls back to +`get_with_headers`; `hypnotube` (isolated search session) solves via the shared requester and replays the solved +cookies + user agent in a fresh isolated session. diff --git a/src/providers/cumlouder.rs b/src/providers/cumlouder.rs index 455ae83..975d12a 100644 --- a/src/providers/cumlouder.rs +++ b/src/providers/cumlouder.rs @@ -3,6 +3,7 @@ use crate::api::ClientVersion; use crate::providers::{Provider, report_provider_error, requester_or_default}; use crate::status::*; use crate::util::cache::VideoCache; +use crate::util::requester::Requester; use crate::util::time::parse_time_to_seconds; use crate::videos::{ServerOptions, VideoItem}; use async_trait::async_trait; @@ -370,10 +371,30 @@ impl CumlouderProvider { { Ok(response) => { let status = response.status().as_u16(); - let body = response.text().await.unwrap_or_default(); + let mut body = response.text().await.unwrap_or_default(); if status == 404 { return Ok(vec![]); } + // Cloudflare challenge: hand over to the requester's full + // chain (FlareSolverr solve + identity replay). + if status == 403 + || status == 503 + || Requester::looks_like_cloudflare_challenge(&body) + { + match requester.get_with_headers(&url, headers.clone(), None).await { + Ok(solved) => { + body = solved; + if body.contains("muestra-escena") { + html = Some(body); + break; + } + } + Err(e) => { + last_err = e.to_string(); + continue; + } + } + } if body.contains("muestra-escena") { html = Some(body); break; diff --git a/src/providers/hypnotube.rs b/src/providers/hypnotube.rs index 876be97..6c16ea6 100644 --- a/src/providers/hypnotube.rs +++ b/src/providers/hypnotube.rs @@ -258,33 +258,74 @@ impl HypnotubeProvider { /// isolated cookie session instead of the shared requester jar. Page 1 is /// the redirect target of the POST; page N>1 is fetched directly. async fn fetch_search_html(&self, query: &str, page: u8) -> Result { + let (status, body) = self.search_once(query, page, None).await?; + if !(status == 403 || status == 503 || Requester::looks_like_cloudflare_challenge(&body)) { + return Ok(body); + } + + // Cloudflare challenged the isolated session: solve it through the + // shared requester (FlareSolverr fallback), then replay the solved + // cookies and user agent in a fresh isolated session. + let mut requester = Requester::new(); + let home = format!("{}/", self.url); + let _ = requester.get(&home, Some(Version::HTTP_11)).await; + let identity = ( + requester.cookie_header_for_url(&home), + Requester::remembered_flaresolverr_user_agent(&home), + ); + let (_, body) = self.search_once(query, page, Some(identity)).await?; + Ok(body) + } + + async fn search_once( + &self, + query: &str, + page: u8, + identity: Option<(Option, Option)>, + ) -> Result<(u16, String)> { + let jar = Arc::new(Jar::default()); + let mut user_agent = None; + if let Some((cookies, ua)) = identity { + if let Some(cookies) = cookies { + for pair in cookies.split(';').map(str::trim).filter(|p| !p.is_empty()) { + jar.add(pair, self.url.as_str()); + } + } + user_agent = ua; + } let client = Client::builder() .tls_cert_verification(false) .emulation(Emulation::Firefox136) - .cookie_provider(Arc::new(Jar::default())) + .cookie_provider(jar) .redirect(if page <= 1 { Policy::default() } else { Policy::none() }) .build()?; let body = format!("q={}&type=videos", query.trim().replace(' ', "+")); - let gate = client + let mut gate = client .post(format!("{}/searchgate.php", self.url)) .header("Content-Type", "application/x-www-form-urlencoded") .header("Referer", format!("{}/", self.url)) - .body(body) - .send() - .await?; + .body(body); + if let Some(ua) = &user_agent { + gate = gate.header("User-Agent", ua.as_str()); + } + let gate = gate.send().await?; if page <= 1 { - return Ok(gate.text().await?); + let status = gate.status().as_u16(); + return Ok((status, gate.text().await?)); } let slug = query.trim().replace(' ', "%20"); let page_url = format!("{}/search/videos/{}/page{}.html", self.url, slug, page); - let response = client + let mut request = client .get(page_url) - .header("Referer", format!("{}/search/videos/{}/", self.url, slug)) - .send() - .await?; - Ok(response.text().await?) + .header("Referer", format!("{}/search/videos/{}/", self.url, slug)); + if let Some(ua) = &user_agent { + request = request.header("User-Agent", ua.as_str()); + } + let response = request.send().await?; + let status = response.status().as_u16(); + Ok((status, response.text().await?)) } async fn query( diff --git a/src/util/requester.rs b/src/util/requester.rs index 256a0a4..ebebe1c 100644 --- a/src/util/requester.rs +++ b/src/util/requester.rs @@ -87,7 +87,7 @@ impl Requester { } } - fn remembered_flaresolverr_user_agent(url: &str) -> Option { + pub(crate) fn remembered_flaresolverr_user_agent(url: &str) -> Option { let host = Self::host_key(url)?; Self::flaresolverr_user_agents() .read() @@ -218,7 +218,7 @@ impl Requester { .unwrap_or_else(|| "none".to_string()) } - fn looks_like_cloudflare_challenge(body: &str) -> bool { + pub(crate) fn looks_like_cloudflare_challenge(body: &str) -> bool { body.contains("Just a moment...") || body.contains("cf-turnstile-response") || body.contains("Enable JavaScript and cookies to continue")