use std::collections::HashMap;
use std::sync::{Arc, RwLock};
pub type RoleResolver = Arc<dyn Fn(&str) -> Option<String> + Send + Sync>;
struct Installed {
resolve: RoleResolver,
providers: HashMap<String, String>,
}
static HOOK: RwLock<Option<Installed>> = RwLock::new(None);
pub fn install(resolve: RoleResolver, providers: HashMap<String, String>) {
if let Ok(mut g) = HOOK.write() {
*g = Some(Installed { resolve, providers });
}
}
pub fn clear() {
if let Ok(mut g) = HOOK.write() {
*g = None;
}
}
fn is_loopback_http(url: &str) -> bool {
let Some(rest) = url.strip_prefix("http://") else { return false };
let authority = rest.split(['/', '?', '#']).next().unwrap_or("");
if authority.contains('@') {
return false;
}
let host = match authority.strip_prefix('[') {
Some(h) => h.split(']').next().unwrap_or(""),
None => authority.rsplit_once(':').map_or(authority, |(h, _)| h),
};
host == "localhost" || host.parse::<std::net::IpAddr>().is_ok_and(|ip| ip.is_loopback())
}
fn root(url: &str) -> String {
let u = url.trim_end_matches('/');
u.strip_suffix("/v1").unwrap_or(u).to_string()
}
pub fn resolve(role: &str) -> Option<String> {
let g = HOOK.read().ok()?;
let h = g.as_ref()?;
(h.resolve)(role).filter(|u| is_loopback_http(u)).map(|u| root(&u))
}
pub fn resolve_for_provider(provider: &str) -> Option<String> {
let role = {
let g = HOOK.read().ok()?;
g.as_ref()?.providers.get(provider)?.clone()
};
resolve(&role)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nothing_resolves_until_installed_and_urls_are_normalised() {
assert_eq!(resolve("hermes"), None);
let mut providers = HashMap::new();
providers.insert("local".to_string(), "hermes".to_string());
install(
Arc::new(|r| match r {
"hermes" => Some("http://127.0.0.1:18090/v1".into()),
"orpheus-tts" => Some("http://127.0.0.1:11434".into()),
_ => None,
}),
providers,
);
assert_eq!(resolve("hermes").as_deref(), Some("http://127.0.0.1:18090"));
assert_eq!(resolve("orpheus-tts").as_deref(), Some("http://127.0.0.1:11434"));
assert_eq!(resolve("other"), None);
assert_eq!(resolve_for_provider("local").as_deref(), Some("http://127.0.0.1:18090"));
assert_eq!(resolve_for_provider("openai"), None);
clear();
assert_eq!(resolve("hermes"), None);
let mut providers = HashMap::new();
providers.insert("local".to_string(), "r".to_string());
install(Arc::new(|_| Some("http://203.0.113.9:8090/v1".to_string())), providers);
assert_eq!(resolve("r"), None);
assert_eq!(resolve_for_provider("local"), None);
clear();
}
#[test]
fn only_loopback_http_answers_are_accepted() {
for ok in ["http://127.0.0.1:1/v1", "http://localhost:8/v1", "http://[::1]:9", "http://127.0.0.1"] {
assert!(is_loopback_http(ok), "{ok}");
}
for bad in [
"https://127.0.0.1:1",
"http://10.0.0.5:1/v1",
"http://evil.example/v1",
"http://127.0.0.1@evil.example/",
"http://localhost.evil.example",
"http://0.0.0.0:1",
"file:///etc/passwd",
"",
] {
assert!(!is_loopback_http(bad), "{bad}");
}
}
}