use ntex::http::Method; use ntex::http::header::LOCATION; use ntex::web::{self, HttpRequest}; use crate::util::hanime_resolve::{fetch_sources, pick_source, source_url}; use crate::util::requester::Requester; /// `{slug}[/{height}][.m3u8]`. fn parse_endpoint(endpoint: &str) -> Option<(String, Option)> { let endpoint = endpoint.trim_matches('/').trim_end_matches(".m3u8"); let mut parts = endpoint.split('/'); let slug = parts .next() .filter(|slug| { !slug.is_empty() && slug .chars() .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') })?; let height = parts.next().and_then(|h| h.parse::().ok()); Some((slug.to_string(), height)) } /// hanime's stream links expire, so the channel hands out this stable URL and a /// fresh handshake happens on every play: redirects to the current playlist. pub async fn redirect_to_media( req: HttpRequest, requester: web::types::State, ) -> Result { let endpoint = req.match_info().query("endpoint").to_string(); let Some((slug, height)) = parse_endpoint(&endpoint) else { return Ok(web::HttpResponse::BadRequest().finish()); }; if req.method() == Method::HEAD { return Ok(web::HttpResponse::Ok().finish()); } let mut requester = requester.get_ref().clone(); let Ok(sources) = fetch_sources(&mut requester, &slug).await else { return Ok(web::HttpResponse::BadGateway().finish()); }; let Some(source) = pick_source(&sources, height) else { return Ok(web::HttpResponse::NotFound().finish()); }; Ok(web::HttpResponse::Found() .header(LOCATION, source_url(source)) .finish()) } #[cfg(test)] mod tests { use super::parse_endpoint; #[test] fn parses_slug_and_height() { assert_eq!( parse_endpoint("natsu-zuma-2/720.m3u8"), Some(("natsu-zuma-2".to_string(), Some(720))) ); assert_eq!( parse_endpoint("/natsu-zuma-2"), Some(("natsu-zuma-2".to_string(), None)) ); } #[test] fn rejects_unsafe_slugs() { assert_eq!(parse_endpoint("../etc/720.m3u8"), None); assert_eq!(parse_endpoint(""), None); } }