noodlemagazine thumb proxy
This commit is contained in:
@@ -1,11 +1,21 @@
|
||||
use ntex::http::header::CONTENT_TYPE;
|
||||
use ntex::web::{self, HttpRequest, error};
|
||||
use ntex::http::header::{CONTENT_LENGTH, CONTENT_TYPE};
|
||||
use ntex::{
|
||||
http::Response,
|
||||
web::{self, HttpRequest, error},
|
||||
};
|
||||
use serde_json::Value;
|
||||
use url::Url;
|
||||
use wreq::Version;
|
||||
|
||||
use crate::util::requester::Requester;
|
||||
|
||||
const FIREFOX_USER_AGENT: &str =
|
||||
"Mozilla/5.0 (X11; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0";
|
||||
const HTML_ACCEPT: &str =
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8";
|
||||
const IMAGE_ACCEPT: &str =
|
||||
"image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NoodlemagazineProxy {}
|
||||
|
||||
@@ -57,6 +67,75 @@ impl NoodlemagazineProxy {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_image_url(url: &str) -> String {
|
||||
if url.starts_with("http://") || url.starts_with("https://") {
|
||||
url.to_string()
|
||||
} else {
|
||||
format!("https://{}", url.trim_start_matches('/'))
|
||||
}
|
||||
}
|
||||
|
||||
fn root_referer() -> &'static str {
|
||||
"https://noodlemagazine.com/"
|
||||
}
|
||||
|
||||
fn root_html_headers() -> Vec<(String, String)> {
|
||||
vec![
|
||||
("Referer".to_string(), Self::root_referer().to_string()),
|
||||
("User-Agent".to_string(), FIREFOX_USER_AGENT.to_string()),
|
||||
("Accept".to_string(), HTML_ACCEPT.to_string()),
|
||||
("Accept-Language".to_string(), "en-US,en;q=0.9".to_string()),
|
||||
]
|
||||
}
|
||||
|
||||
fn image_headers(requester: &Requester, image_url: &str) -> Vec<(String, String)> {
|
||||
let mut headers = vec![
|
||||
("Referer".to_string(), Self::root_referer().to_string()),
|
||||
("User-Agent".to_string(), FIREFOX_USER_AGENT.to_string()),
|
||||
("Accept".to_string(), IMAGE_ACCEPT.to_string()),
|
||||
("Accept-Language".to_string(), "en-US,en;q=0.9".to_string()),
|
||||
];
|
||||
|
||||
if let Some(cookie) = requester.cookie_header_for_url(image_url) {
|
||||
headers.push(("Cookie".to_string(), cookie));
|
||||
}
|
||||
|
||||
headers
|
||||
}
|
||||
|
||||
fn has_allowed_image_extension(path: &str) -> bool {
|
||||
let path = path.to_ascii_lowercase();
|
||||
[".jpg", ".jpeg", ".png", ".webp", ".avif", ".gif"]
|
||||
.iter()
|
||||
.any(|ext| path.ends_with(ext))
|
||||
}
|
||||
|
||||
fn is_allowed_thumb_url(url: &str) -> bool {
|
||||
let Some(url) = Url::parse(url).ok() else {
|
||||
return false;
|
||||
};
|
||||
if url.scheme() != "https" {
|
||||
return false;
|
||||
}
|
||||
let Some(host) = url.host_str() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
(host == "noodlemagazine.com" || host.ends_with(".noodlemagazine.com"))
|
||||
&& Self::has_allowed_image_extension(url.path())
|
||||
}
|
||||
|
||||
fn is_binary_image_content_type(content_type: &str) -> bool {
|
||||
let media_type = content_type
|
||||
.split(';')
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
|
||||
media_type.starts_with("image/")
|
||||
}
|
||||
|
||||
fn is_hls_url(url: &str) -> bool {
|
||||
Url::parse(url)
|
||||
.ok()
|
||||
@@ -184,6 +263,82 @@ pub async fn serve_media(
|
||||
.body(rewritten_manifest))
|
||||
}
|
||||
|
||||
pub async fn get_image(
|
||||
req: HttpRequest,
|
||||
requester: web::types::State<Requester>,
|
||||
) -> Result<impl web::Responder, web::Error> {
|
||||
let endpoint = req.match_info().query("endpoint").to_string();
|
||||
let image_url = NoodlemagazineProxy::normalize_image_url(&endpoint);
|
||||
|
||||
if !NoodlemagazineProxy::is_allowed_thumb_url(&image_url) {
|
||||
return Ok(web::HttpResponse::BadRequest().finish());
|
||||
}
|
||||
|
||||
let mut requester = requester.get_ref().clone();
|
||||
let _ = requester
|
||||
.get_with_headers(
|
||||
NoodlemagazineProxy::root_referer(),
|
||||
NoodlemagazineProxy::root_html_headers(),
|
||||
Some(Version::HTTP_11),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut headers = NoodlemagazineProxy::image_headers(&requester, image_url.as_str());
|
||||
let mut upstream = requester
|
||||
.get_raw_with_headers(image_url.as_str(), headers.clone())
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let needs_warmup = upstream
|
||||
.as_ref()
|
||||
.map(|response| !response.status().is_success())
|
||||
.unwrap_or(true);
|
||||
|
||||
if needs_warmup {
|
||||
let _ = requester
|
||||
.get_with_headers(
|
||||
image_url.as_str(),
|
||||
headers.clone(),
|
||||
Some(Version::HTTP_11),
|
||||
)
|
||||
.await;
|
||||
headers = NoodlemagazineProxy::image_headers(&requester, image_url.as_str());
|
||||
upstream = requester
|
||||
.get_raw_with_headers(image_url.as_str(), headers)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
let Some(upstream) = upstream.filter(|response| response.status().is_success()) else {
|
||||
return Ok(web::HttpResponse::NotFound().finish());
|
||||
};
|
||||
|
||||
let status = upstream.status();
|
||||
let headers = upstream.headers().clone();
|
||||
let content_type = headers
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(str::to_string)
|
||||
.unwrap_or_default();
|
||||
|
||||
if !NoodlemagazineProxy::is_binary_image_content_type(&content_type) {
|
||||
return Ok(web::HttpResponse::BadGateway().finish());
|
||||
}
|
||||
|
||||
let bytes = upstream.bytes().await.map_err(error::ErrorBadGateway)?;
|
||||
let mut resp = Response::build(status);
|
||||
if !content_type.is_empty() {
|
||||
resp.set_header(CONTENT_TYPE, content_type);
|
||||
}
|
||||
if let Some(cl) = headers.get(CONTENT_LENGTH) {
|
||||
if let Ok(cl_str) = cl.to_str() {
|
||||
resp.set_header(CONTENT_LENGTH, cl_str);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resp.body(bytes.to_vec()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::NoodlemagazineProxy;
|
||||
@@ -231,4 +386,36 @@ mod tests {
|
||||
"#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1\nhttps://cdn.example/hls/low/index.m3u8\n#EXT-X-KEY:METHOD=AES-128,URI=\"https://cdn.example/hls/keys/key.bin\"\nhttps://cdn.example/hls/segment0.ts"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_only_noodlemagazine_image_thumbs() {
|
||||
assert!(NoodlemagazineProxy::is_allowed_thumb_url(
|
||||
"https://noodlemagazine.com/thumbs/example.webp"
|
||||
));
|
||||
assert!(NoodlemagazineProxy::is_allowed_thumb_url(
|
||||
"https://img.noodlemagazine.com/previews/example.jpg"
|
||||
));
|
||||
assert!(!NoodlemagazineProxy::is_allowed_thumb_url(
|
||||
"https://noodlemagazine.com/watch/-123_456"
|
||||
));
|
||||
assert!(!NoodlemagazineProxy::is_allowed_thumb_url(
|
||||
"https://cdn.example/thumb.jpg"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognizes_binary_image_content_types() {
|
||||
assert!(NoodlemagazineProxy::is_binary_image_content_type(
|
||||
"image/webp"
|
||||
));
|
||||
assert!(NoodlemagazineProxy::is_binary_image_content_type(
|
||||
"image/jpeg; charset=binary"
|
||||
));
|
||||
assert!(!NoodlemagazineProxy::is_binary_image_content_type(
|
||||
"text/html; charset=utf-8"
|
||||
));
|
||||
assert!(!NoodlemagazineProxy::is_binary_image_content_type(
|
||||
"application/json"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user