Route Cloudflare challenges to FlareSolverr on cumlouder and hypnotube raw paths
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MUYdCh2nCVsQYgJAT7gwSi
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<String> {
|
||||
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<String>, Option<String>)>,
|
||||
) -> 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(
|
||||
|
||||
@@ -87,7 +87,7 @@ impl Requester {
|
||||
}
|
||||
}
|
||||
|
||||
fn remembered_flaresolverr_user_agent(url: &str) -> Option<String> {
|
||||
pub(crate) fn remembered_flaresolverr_user_agent(url: &str) -> Option<String> {
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user