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
|
`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).
|
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::providers::{Provider, report_provider_error, requester_or_default};
|
||||||
use crate::status::*;
|
use crate::status::*;
|
||||||
use crate::util::cache::VideoCache;
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::requester::Requester;
|
||||||
use crate::util::time::parse_time_to_seconds;
|
use crate::util::time::parse_time_to_seconds;
|
||||||
use crate::videos::{ServerOptions, VideoItem};
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -370,10 +371,30 @@ impl CumlouderProvider {
|
|||||||
{
|
{
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
let status = response.status().as_u16();
|
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 {
|
if status == 404 {
|
||||||
return Ok(vec![]);
|
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") {
|
if body.contains("muestra-escena") {
|
||||||
html = Some(body);
|
html = Some(body);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -258,33 +258,74 @@ impl HypnotubeProvider {
|
|||||||
/// isolated cookie session instead of the shared requester jar. Page 1 is
|
/// isolated cookie session instead of the shared requester jar. Page 1 is
|
||||||
/// the redirect target of the POST; page N>1 is fetched directly.
|
/// the redirect target of the POST; page N>1 is fetched directly.
|
||||||
async fn fetch_search_html(&self, query: &str, page: u8) -> Result<String> {
|
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()
|
let client = Client::builder()
|
||||||
.tls_cert_verification(false)
|
.tls_cert_verification(false)
|
||||||
.emulation(Emulation::Firefox136)
|
.emulation(Emulation::Firefox136)
|
||||||
.cookie_provider(Arc::new(Jar::default()))
|
.cookie_provider(jar)
|
||||||
.redirect(if page <= 1 { Policy::default() } else { Policy::none() })
|
.redirect(if page <= 1 { Policy::default() } else { Policy::none() })
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let body = format!("q={}&type=videos", query.trim().replace(' ', "+"));
|
let body = format!("q={}&type=videos", query.trim().replace(' ', "+"));
|
||||||
let gate = client
|
let mut gate = client
|
||||||
.post(format!("{}/searchgate.php", self.url))
|
.post(format!("{}/searchgate.php", self.url))
|
||||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
.header("Referer", format!("{}/", self.url))
|
.header("Referer", format!("{}/", self.url))
|
||||||
.body(body)
|
.body(body);
|
||||||
.send()
|
if let Some(ua) = &user_agent {
|
||||||
.await?;
|
gate = gate.header("User-Agent", ua.as_str());
|
||||||
|
}
|
||||||
|
let gate = gate.send().await?;
|
||||||
if page <= 1 {
|
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 slug = query.trim().replace(' ', "%20");
|
||||||
let page_url = format!("{}/search/videos/{}/page{}.html", self.url, slug, page);
|
let page_url = format!("{}/search/videos/{}/page{}.html", self.url, slug, page);
|
||||||
let response = client
|
let mut request = client
|
||||||
.get(page_url)
|
.get(page_url)
|
||||||
.header("Referer", format!("{}/search/videos/{}/", self.url, slug))
|
.header("Referer", format!("{}/search/videos/{}/", self.url, slug));
|
||||||
.send()
|
if let Some(ua) = &user_agent {
|
||||||
.await?;
|
request = request.header("User-Agent", ua.as_str());
|
||||||
Ok(response.text().await?)
|
}
|
||||||
|
let response = request.send().await?;
|
||||||
|
let status = response.status().as_u16();
|
||||||
|
Ok((status, response.text().await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn query(
|
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)?;
|
let host = Self::host_key(url)?;
|
||||||
Self::flaresolverr_user_agents()
|
Self::flaresolverr_user_agents()
|
||||||
.read()
|
.read()
|
||||||
@@ -218,7 +218,7 @@ impl Requester {
|
|||||||
.unwrap_or_else(|| "none".to_string())
|
.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("Just a moment...")
|
||||||
|| body.contains("cf-turnstile-response")
|
|| body.contains("cf-turnstile-response")
|
||||||
|| body.contains("Enable JavaScript and cookies to continue")
|
|| body.contains("Enable JavaScript and cookies to continue")
|
||||||
|
|||||||
Reference in New Issue
Block a user