use std::sync::OnceLock;
pub const NAMES: [&str; 6] = [
"https_proxy",
"HTTPS_PROXY",
"all_proxy",
"ALL_PROXY",
"http_proxy",
"HTTP_PROXY",
];
#[derive(Clone, Debug, PartialEq)]
pub struct Proxy {
pub from: &'static str,
pub url: String,
}
static CHOSEN: OnceLock<Option<Proxy>> = OnceLock::new();
pub fn init() -> anyhow::Result<()> {
let chosen = choose(|name| std::env::var(name).ok())?;
let _ = CHOSEN.set(chosen);
Ok(())
}
pub fn get() -> Option<&'static Proxy> {
CHOSEN.get().and_then(Option::as_ref)
}
fn choose(var: impl Fn(&str) -> Option<String>) -> anyhow::Result<Option<Proxy>> {
let Some((from, value)) = NAMES
.iter()
.find_map(|&name| Some((name, var(name).filter(|v| !v.trim().is_empty())?)))
else {
return Ok(None);
};
let value = value.trim();
let url = if value.contains("://") {
value.to_string()
} else {
format!("http://{value}")
};
let parsed = reqwest::Url::parse(&url)
.map_err(|_| anyhow::anyhow!("{from} doesn't hold a proxy address tuitube can read"))?;
if parsed.scheme() != "http" || parsed.host_str().is_none() {
anyhow::bail!(
"{from} names a {} proxy; tuitube needs an http:// one, because mpv can't send \
videos through any other kind and would go around it",
parsed.scheme()
);
}
Ok(Some(Proxy { from, url }))
}
pub fn shown() -> String {
match get() {
None => "none (direct)".into(),
Some(proxy) => {
let host = reqwest::Url::parse(&proxy.url)
.ok()
.map(|u| {
let port = u.port().map(|p| format!(":{p}")).unwrap_or_default();
format!("{}{port}", u.host_str().unwrap_or(""))
})
.unwrap_or_default();
crate::text::clean(&format!("http://{host} (from {})", proxy.from))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn env<'a>(vars: &'a [(&str, &str)]) -> impl Fn(&str) -> Option<String> + 'a {
move |name| {
vars.iter()
.find(|(k, _)| *k == name)
.map(|(_, v)| v.to_string())
}
}
#[test]
fn the_first_set_variable_is_the_proxy_for_everything() {
assert_eq!(choose(env(&[])).unwrap(), None);
assert_eq!(choose(env(&[("NO_PROXY", "*")])).unwrap(), None);
let both = [("HTTPS_PROXY", "http://a:1"), ("https_proxy", "http://b:2")];
assert_eq!(choose(env(&both)).unwrap().unwrap().url, "http://b:2");
let http = [("HTTP_PROXY", "proxy.example:3128")];
assert_eq!(
choose(env(&http)).unwrap(),
Some(Proxy {
from: "HTTP_PROXY",
url: "http://proxy.example:3128".into()
})
);
}
#[test]
fn a_proxy_mpv_cant_use_is_refused_without_showing_it() {
for value in ["socks5h://127.0.0.1:9050", "https://u:secret@p.example"] {
let error = choose(env(&[("ALL_PROXY", value)]))
.unwrap_err()
.to_string();
assert!(
error.contains("ALL_PROXY") && error.contains("http://"),
"{error}"
);
assert!(
!error.contains("secret") && !error.contains("9050"),
"{error}"
);
}
let bad = choose(env(&[("https_proxy", "http://")])).unwrap_err();
assert!(bad.to_string().contains("https_proxy"), "{bad}");
}
}