2022-11-02 22:09:52 +05:30
|
|
|
use actix_web::http::Method;
|
2023-08-22 03:26:18 +05:30
|
|
|
use actix_web::{web, App, HttpRequest, HttpResponse, HttpResponseBuilder, HttpServer};
|
2023-03-25 20:55:51 +05:30
|
|
|
use once_cell::sync::Lazy;
|
2022-11-02 22:09:52 +05:30
|
|
|
use qstring::QString;
|
|
|
|
use regex::Regex;
|
2023-06-02 15:33:49 +05:30
|
|
|
use reqwest::{Body, Client, Request, Url};
|
2023-09-09 09:43:30 +05:30
|
|
|
use std::env;
|
|
|
|
use std::error::Error;
|
2022-11-02 22:09:52 +05:30
|
|
|
|
2023-09-28 02:10:47 +05:30
|
|
|
#[cfg(not(any(feature = "reqwest-native-tls", feature = "reqwest-rustls")))]
|
|
|
|
compile_error!("feature \"reqwest-native-tls\" or \"reqwest-rustls\" must be set for proxy to have TLS support");
|
|
|
|
|
|
|
|
#[cfg(any(feature = "webp", feature = "avif"))]
|
|
|
|
use tokio::{sync::oneshot, task::spawn_blocking};
|
|
|
|
|
|
|
|
#[cfg(feature = "mimalloc")]
|
2023-05-23 08:33:52 +05:30
|
|
|
#[global_allocator]
|
2023-09-28 02:10:47 +05:30
|
|
|
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
2023-05-23 08:33:52 +05:30
|
|
|
|
2022-11-02 22:09:52 +05:30
|
|
|
#[actix_web::main]
|
|
|
|
async fn main() -> std::io::Result<()> {
|
|
|
|
println!("Running server!");
|
|
|
|
|
2022-11-03 02:04:14 +05:30
|
|
|
let server = HttpServer::new(|| {
|
2022-11-02 22:09:52 +05:30
|
|
|
// match all requests
|
2023-06-02 15:33:49 +05:30
|
|
|
App::new().default_service(web::to(index))
|
2022-11-03 00:12:12 +05:30
|
|
|
});
|
2023-09-28 01:48:15 +05:30
|
|
|
|
|
|
|
// get socket/port from env
|
|
|
|
// backwards compat when only UDS is set
|
|
|
|
if env::var("UDS").is_ok() {
|
2023-09-29 17:30:59 +05:30
|
|
|
let socket_path = env::var("BIND_UNIX").unwrap_or_else(|_| "./socket/actix.sock".to_string());
|
2023-09-28 01:48:15 +05:30
|
|
|
server.bind_uds(socket_path)?
|
2022-11-03 00:12:12 +05:30
|
|
|
} else {
|
|
|
|
let bind = env::var("BIND").unwrap_or_else(|_| "0.0.0.0:8080".to_string());
|
|
|
|
server.bind(bind)?
|
2023-06-02 15:33:49 +05:30
|
|
|
}
|
2023-08-22 03:26:18 +05:30
|
|
|
.run()
|
|
|
|
.await
|
2022-11-02 22:09:52 +05:30
|
|
|
}
|
|
|
|
|
2023-06-02 15:33:49 +05:30
|
|
|
static RE_DOMAIN: Lazy<Regex> =
|
|
|
|
Lazy::new(|| Regex::new(r"^(?:[a-z\d.-]*\.)?([a-z\d-]*\.[a-z\d-]*)$").unwrap());
|
2023-03-25 20:55:51 +05:30
|
|
|
static RE_MANIFEST: Lazy<Regex> = Lazy::new(|| Regex::new("(?m)URI=\"([^\"]+)\"").unwrap());
|
2023-06-02 15:33:49 +05:30
|
|
|
static RE_DASH_MANIFEST: Lazy<Regex> =
|
|
|
|
Lazy::new(|| Regex::new("BaseURL>(https://[^<]+)</BaseURL").unwrap());
|
2022-11-03 02:13:17 +05:30
|
|
|
|
2023-03-25 20:55:51 +05:30
|
|
|
static CLIENT: Lazy<Client> = Lazy::new(|| {
|
|
|
|
let builder = Client::builder()
|
2023-06-10 00:09:06 +05:30
|
|
|
.user_agent("Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/102.0");
|
2023-03-25 20:55:51 +05:30
|
|
|
|
2023-08-22 03:26:18 +05:30
|
|
|
let proxy = if let Ok(proxy) = env::var("PROXY") {
|
|
|
|
Some(reqwest::Proxy::all(proxy).unwrap())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
|
|
|
|
2023-09-28 02:10:47 +05:30
|
|
|
let builder = if let Some(proxy) = proxy {
|
2023-08-22 03:26:18 +05:30
|
|
|
// proxy basic auth
|
2023-08-22 03:33:00 +05:30
|
|
|
if let Ok(proxy_auth_user) = env::var("PROXY_USER") {
|
2023-08-22 03:26:18 +05:30
|
|
|
let proxy_auth_pass = env::var("PROXY_PASS").unwrap_or_default();
|
2023-09-14 23:11:04 +05:30
|
|
|
builder.proxy(proxy.basic_auth(&proxy_auth_user, &proxy_auth_pass))
|
2023-08-22 03:26:18 +05:30
|
|
|
} else {
|
2023-09-28 02:10:47 +05:30
|
|
|
builder.proxy(proxy)
|
2023-08-22 03:33:00 +05:30
|
|
|
}
|
|
|
|
} else {
|
|
|
|
builder
|
|
|
|
};
|
2023-08-22 03:26:18 +05:30
|
|
|
|
2023-03-25 20:55:51 +05:30
|
|
|
if env::var("IPV4_ONLY").is_ok() {
|
|
|
|
builder
|
2022-11-03 02:13:17 +05:30
|
|
|
.local_address(Some("0.0.0.0".parse().unwrap()))
|
|
|
|
.build()
|
|
|
|
.unwrap()
|
2023-03-25 20:55:51 +05:30
|
|
|
} else {
|
|
|
|
builder.build().unwrap()
|
|
|
|
}
|
|
|
|
});
|
2022-11-02 22:09:52 +05:30
|
|
|
|
2023-08-04 15:02:29 +05:30
|
|
|
const ANDROID_USER_AGENT: &str = "com.google.android.youtube/1537338816 (Linux; U; Android 13; en_US; ; Build/TQ2A.230505.002; Cronet/113.0.5672.24)";
|
2023-07-19 05:05:51 +05:30
|
|
|
const ALLOWED_DOMAINS: [&str; 8] = [
|
2022-11-02 22:09:52 +05:30
|
|
|
"youtube.com",
|
|
|
|
"googlevideo.com",
|
|
|
|
"ytimg.com",
|
|
|
|
"ggpht.com",
|
|
|
|
"googleusercontent.com",
|
|
|
|
"lbryplayer.xyz",
|
|
|
|
"odycdn.com",
|
2023-07-19 05:05:51 +05:30
|
|
|
"ajay.app",
|
2022-11-02 22:09:52 +05:30
|
|
|
];
|
|
|
|
|
|
|
|
fn add_headers(response: &mut HttpResponseBuilder) {
|
|
|
|
response
|
|
|
|
.append_header(("Access-Control-Allow-Origin", "*"))
|
|
|
|
.append_header(("Access-Control-Allow-Headers", "*"))
|
|
|
|
.append_header(("Access-Control-Allow-Methods", "*"))
|
|
|
|
.append_header(("Access-Control-Max-Age", "1728000"));
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_header_allowed(header: &str) -> bool {
|
|
|
|
if header.starts_with("access-control") {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-06-02 15:33:49 +05:30
|
|
|
!matches!(
|
|
|
|
header,
|
|
|
|
"host"
|
|
|
|
| "content-length"
|
|
|
|
| "set-cookie"
|
|
|
|
| "alt-svc"
|
|
|
|
| "accept-ch"
|
|
|
|
| "report-to"
|
|
|
|
| "strict-transport-security"
|
|
|
|
| "user-agent"
|
|
|
|
)
|
2022-11-02 22:09:52 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
async fn index(req: HttpRequest) -> Result<HttpResponse, Box<dyn Error>> {
|
|
|
|
if req.method() == Method::OPTIONS {
|
|
|
|
let mut response = HttpResponse::Ok();
|
|
|
|
add_headers(&mut response);
|
|
|
|
return Ok(response.finish());
|
|
|
|
} else if req.method() != Method::GET && req.method() != Method::HEAD {
|
|
|
|
let mut response = HttpResponse::MethodNotAllowed();
|
|
|
|
add_headers(&mut response);
|
|
|
|
return Ok(response.finish());
|
|
|
|
}
|
|
|
|
|
|
|
|
// parse query string
|
|
|
|
let query = QString::from(req.query_string());
|
|
|
|
|
|
|
|
let res = query.get("host");
|
2023-04-25 13:44:42 +05:30
|
|
|
let res = res.map(|s| s.to_string());
|
|
|
|
|
|
|
|
if res.is_none() {
|
|
|
|
return Err("No host provided".into());
|
|
|
|
}
|
2022-11-02 22:09:52 +05:30
|
|
|
|
2023-09-28 02:10:47 +05:30
|
|
|
#[cfg(any(feature = "webp", feature = "avif"))]
|
|
|
|
let disallow_image_transcoding = env::var("DISALLOW_IMAGE_TRANSCODING").is_ok();
|
|
|
|
|
2023-08-04 15:02:29 +05:30
|
|
|
let rewrite = query.get("rewrite") != Some("false");
|
2022-12-03 01:11:28 +05:30
|
|
|
|
2023-07-13 16:16:17 +05:30
|
|
|
#[cfg(feature = "avif")]
|
2023-08-04 15:02:29 +05:30
|
|
|
let avif = query.get("avif") == Some("true");
|
2023-07-12 21:14:11 +05:30
|
|
|
|
2022-11-02 22:09:52 +05:30
|
|
|
let host = res.unwrap();
|
2023-04-25 13:44:42 +05:30
|
|
|
let domain = RE_DOMAIN.captures(host.as_str());
|
2022-11-02 22:09:52 +05:30
|
|
|
|
2022-12-03 01:50:50 +05:30
|
|
|
if domain.is_none() {
|
2022-11-02 22:09:52 +05:30
|
|
|
return Err("Invalid host provided".into());
|
|
|
|
}
|
|
|
|
|
2022-12-03 01:50:50 +05:30
|
|
|
let domain = domain.unwrap().get(1).unwrap().as_str();
|
2022-11-02 22:09:52 +05:30
|
|
|
|
2023-08-04 15:02:29 +05:30
|
|
|
if !ALLOWED_DOMAINS.contains(&domain) {
|
2022-11-02 22:09:52 +05:30
|
|
|
return Err("Domain not allowed".into());
|
|
|
|
}
|
|
|
|
|
2023-06-02 15:33:49 +05:30
|
|
|
let video_playback = req.path().eq("/videoplayback");
|
|
|
|
let is_android = video_playback && query.get("c").unwrap_or("").eq("ANDROID");
|
|
|
|
|
2022-12-03 01:11:28 +05:30
|
|
|
let qs = {
|
2023-06-02 15:33:49 +05:30
|
|
|
let collected = query
|
|
|
|
.into_pairs()
|
2022-12-03 01:11:28 +05:30
|
|
|
.into_iter()
|
|
|
|
.filter(|(key, _)| key != "host" && key != "rewrite")
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
QString::new(collected)
|
|
|
|
};
|
|
|
|
|
2022-11-19 00:59:55 +05:30
|
|
|
let mut url = Url::parse(&format!("https://{}{}", host, req.path()))?;
|
2022-12-03 01:11:28 +05:30
|
|
|
url.set_query(Some(qs.to_string().as_str()));
|
2022-11-03 01:43:27 +05:30
|
|
|
|
2023-06-02 15:33:49 +05:30
|
|
|
let method = {
|
|
|
|
if !is_android && video_playback {
|
|
|
|
Method::POST
|
|
|
|
} else {
|
|
|
|
req.method().clone()
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut request = Request::new(method, url);
|
|
|
|
|
|
|
|
if !is_android && video_playback {
|
|
|
|
request.body_mut().replace(Body::from("x\0"));
|
|
|
|
}
|
2022-11-02 22:09:52 +05:30
|
|
|
|
|
|
|
let request_headers = request.headers_mut();
|
|
|
|
|
|
|
|
for (key, value) in req.headers() {
|
|
|
|
if is_header_allowed(key.as_str()) {
|
2023-04-25 13:44:42 +05:30
|
|
|
request_headers.insert(key, value.clone());
|
2022-11-02 22:09:52 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-02 15:33:49 +05:30
|
|
|
if is_android {
|
2023-08-04 15:02:29 +05:30
|
|
|
request_headers.insert("User-Agent", ANDROID_USER_AGENT.parse().unwrap());
|
2023-06-02 15:33:49 +05:30
|
|
|
}
|
|
|
|
|
2022-11-02 22:09:52 +05:30
|
|
|
let resp = CLIENT.execute(request).await;
|
|
|
|
|
|
|
|
if resp.is_err() {
|
|
|
|
return Err(resp.err().unwrap().into());
|
|
|
|
}
|
|
|
|
|
2023-04-25 13:44:42 +05:30
|
|
|
let resp = resp?;
|
2022-11-02 22:09:52 +05:30
|
|
|
|
|
|
|
let mut response = HttpResponse::build(resp.status());
|
|
|
|
|
|
|
|
add_headers(&mut response);
|
|
|
|
|
|
|
|
for (key, value) in resp.headers() {
|
|
|
|
if is_header_allowed(key.as_str()) {
|
2023-07-19 07:42:02 +05:30
|
|
|
response.append_header((key.as_str(), value.as_bytes()));
|
2022-11-02 22:09:52 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-03 01:11:28 +05:30
|
|
|
if rewrite {
|
|
|
|
if let Some(content_type) = resp.headers().get("content-type") {
|
2023-07-13 16:16:17 +05:30
|
|
|
#[cfg(feature = "avif")]
|
2023-09-29 17:30:33 +05:30
|
|
|
if !disallow_image_transcoding
|
2023-09-28 02:10:47 +05:30
|
|
|
&& (content_type == "image/webp" || content_type == "image/jpeg" && avif)
|
|
|
|
{
|
2023-07-12 21:14:11 +05:30
|
|
|
let resp_bytes = resp.bytes().await.unwrap();
|
2023-09-09 09:43:30 +05:30
|
|
|
let (tx, rx) = oneshot::channel::<(Vec<u8>, &'static str)>();
|
|
|
|
spawn_blocking(|| {
|
|
|
|
use ravif::{Encoder, Img};
|
|
|
|
use rgb::FromSlice;
|
|
|
|
|
|
|
|
let image = image::load_from_memory(&resp_bytes).unwrap();
|
|
|
|
|
|
|
|
let width = image.width() as usize;
|
|
|
|
let height = image.height() as usize;
|
|
|
|
|
|
|
|
let buf = image.into_rgb8();
|
|
|
|
let buf = buf.as_raw().as_rgb();
|
|
|
|
|
|
|
|
let buffer = Img::new(buf, width, height);
|
|
|
|
|
|
|
|
let res = Encoder::new()
|
|
|
|
.with_quality(80f32)
|
|
|
|
.with_speed(7)
|
|
|
|
.encode_rgb(buffer);
|
|
|
|
|
2023-09-28 02:10:47 +05:30
|
|
|
if let Ok(res) = res {
|
2023-09-09 09:43:30 +05:30
|
|
|
tx.send((res.avif_file.to_vec(), "image/avif")).unwrap();
|
|
|
|
} else {
|
|
|
|
tx.send((resp_bytes.into(), "image/jpeg")).unwrap();
|
2023-09-28 02:10:47 +05:30
|
|
|
}
|
2023-09-09 09:43:30 +05:30
|
|
|
});
|
|
|
|
let (body, content_type) = rx.await.unwrap();
|
|
|
|
response.content_type(content_type);
|
|
|
|
return Ok(response.body(body));
|
2023-07-12 21:14:11 +05:30
|
|
|
}
|
|
|
|
|
2023-08-05 20:56:19 +05:30
|
|
|
#[cfg(feature = "webp")]
|
2023-09-28 02:10:47 +05:30
|
|
|
if !disallow_image_transcoding && content_type == "image/jpeg" {
|
2022-12-03 01:11:28 +05:30
|
|
|
let resp_bytes = resp.bytes().await.unwrap();
|
2023-09-09 09:43:30 +05:30
|
|
|
let (tx, rx) = oneshot::channel::<(Vec<u8>, &'static str)>();
|
|
|
|
spawn_blocking(|| {
|
|
|
|
use libwebp_sys::{WebPEncodeRGB, WebPFree};
|
|
|
|
|
|
|
|
let image = image::load_from_memory(&resp_bytes).unwrap();
|
|
|
|
let width = image.width();
|
|
|
|
let height = image.height();
|
|
|
|
|
|
|
|
let quality = 85;
|
|
|
|
|
|
|
|
let data = image.as_rgb8().unwrap().as_raw();
|
|
|
|
|
|
|
|
let bytes: Vec<u8> = unsafe {
|
|
|
|
let mut out_buf = std::ptr::null_mut();
|
|
|
|
let stride = width as i32 * 3;
|
|
|
|
let len: usize = WebPEncodeRGB(
|
|
|
|
data.as_ptr(),
|
|
|
|
width as i32,
|
|
|
|
height as i32,
|
|
|
|
stride,
|
|
|
|
quality as f32,
|
|
|
|
&mut out_buf,
|
|
|
|
);
|
|
|
|
let vec = std::slice::from_raw_parts(out_buf, len).into();
|
|
|
|
WebPFree(out_buf as *mut _);
|
|
|
|
vec
|
|
|
|
};
|
|
|
|
|
|
|
|
if bytes.len() < resp_bytes.len() {
|
|
|
|
tx.send((bytes, "image/webp")).unwrap();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
tx.send((resp_bytes.into(), "image/jpeg")).unwrap();
|
|
|
|
});
|
|
|
|
let (body, content_type) = rx.await.unwrap();
|
|
|
|
response.content_type(content_type);
|
|
|
|
return Ok(response.body(body));
|
2022-12-03 01:11:28 +05:30
|
|
|
}
|
2023-08-05 20:56:19 +05:30
|
|
|
|
2023-06-02 15:33:49 +05:30
|
|
|
if content_type == "application/x-mpegurl"
|
|
|
|
|| content_type == "application/vnd.apple.mpegurl"
|
|
|
|
{
|
2022-12-03 01:11:28 +05:30
|
|
|
let resp_str = resp.text().await.unwrap();
|
|
|
|
|
2023-06-02 15:33:49 +05:30
|
|
|
let modified = resp_str
|
|
|
|
.lines()
|
|
|
|
.map(|line| {
|
|
|
|
let captures = RE_MANIFEST.captures(line);
|
|
|
|
if let Some(captures) = captures {
|
|
|
|
let url = captures.get(1).unwrap().as_str();
|
|
|
|
if url.starts_with("https://") {
|
|
|
|
return line
|
|
|
|
.replace(url, localize_url(url, host.as_str()).as_str());
|
|
|
|
}
|
2022-12-03 01:11:28 +05:30
|
|
|
}
|
2023-06-02 15:33:49 +05:30
|
|
|
localize_url(line, host.as_str())
|
|
|
|
})
|
|
|
|
.collect::<Vec<String>>()
|
|
|
|
.join("\n");
|
2022-11-02 22:09:52 +05:30
|
|
|
|
2022-12-03 01:11:28 +05:30
|
|
|
return Ok(response.body(modified));
|
|
|
|
}
|
|
|
|
if content_type == "video/vnd.mpeg.dash.mpd" || content_type == "application/dash+xml" {
|
|
|
|
let mut resp_str = resp.text().await.unwrap();
|
|
|
|
let clone_resp = resp_str.clone();
|
|
|
|
let captures = RE_DASH_MANIFEST.captures_iter(&clone_resp);
|
|
|
|
for capture in captures {
|
|
|
|
let url = capture.get(1).unwrap().as_str();
|
2023-04-25 13:44:42 +05:30
|
|
|
let new_url = localize_url(url, host.as_str());
|
|
|
|
resp_str = resp_str.replace(url, new_url.as_str());
|
2022-12-03 01:11:28 +05:30
|
|
|
}
|
|
|
|
return Ok(response.body(resp_str));
|
2022-11-19 00:59:55 +05:30
|
|
|
}
|
|
|
|
}
|
2022-11-02 22:09:52 +05:30
|
|
|
}
|
|
|
|
|
2023-02-28 23:24:43 +05:30
|
|
|
if let Some(content_length) = resp.headers().get("content-length") {
|
|
|
|
response.append_header(("content-length", content_length));
|
|
|
|
}
|
|
|
|
|
2022-11-02 22:09:52 +05:30
|
|
|
// Stream response
|
|
|
|
Ok(response.streaming(resp.bytes_stream()))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn localize_url(url: &str, host: &str) -> String {
|
|
|
|
if url.starts_with("https://") {
|
|
|
|
let mut url = Url::parse(url).unwrap();
|
|
|
|
let host = url.host().unwrap().to_string();
|
|
|
|
|
|
|
|
// set host query param
|
2023-06-02 15:33:49 +05:30
|
|
|
url.query_pairs_mut().append_pair("host", &host);
|
2022-11-02 22:09:52 +05:30
|
|
|
|
|
|
|
return format!("{}?{}", url.path(), url.query().unwrap());
|
2022-11-03 03:17:20 +05:30
|
|
|
} else if url.ends_with(".m3u8") || url.ends_with(".ts") {
|
2022-11-03 02:04:14 +05:30
|
|
|
return if url.contains('?') {
|
2022-11-02 22:09:52 +05:30
|
|
|
format!("{}&host={}", url, host)
|
|
|
|
} else {
|
|
|
|
format!("{}?host={}", url, host)
|
2022-11-03 02:04:14 +05:30
|
|
|
};
|
2022-11-02 22:09:52 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
url.to_string()
|
|
|
|
}
|