Skip to main content

elph_ai/api/
http_proxy.rs

1use anyhow::{Result, anyhow};
2use url::Url;
3
4use crate::types::ProviderEnv;
5use crate::utils::provider_env::get_provider_env_value;
6
7const DEFAULT_PROXY_PORTS: &[(&str, u16)] = &[
8    ("ftp", 21),
9    ("gopher", 70),
10    ("http", 80),
11    ("https", 443),
12    ("ws", 80),
13    ("wss", 443),
14];
15
16pub const UNSUPPORTED_PROXY_PROTOCOL_MESSAGE: &str =
17    "Unsupported proxy protocol. SOCKS and PAC proxy URLs are not supported; use an HTTP or HTTPS proxy URL.";
18
19fn get_proxy_env(key: &str, env: Option<&ProviderEnv>) -> String {
20    let lowercase = key.to_lowercase();
21    let uppercase = key.to_uppercase();
22
23    if let Some(env) = env {
24        if let Some(value) = env.get(&lowercase) {
25            return value.clone();
26        }
27        if let Some(value) = env.get(&uppercase) {
28            return value.clone();
29        }
30    }
31
32    get_provider_env_value(&lowercase, None)
33        .or_else(|| get_provider_env_value(&uppercase, None))
34        .unwrap_or_default()
35}
36
37fn parse_proxy_target_url(target_url: &str) -> Option<Url> {
38    Url::parse(target_url).ok()
39}
40
41fn default_port_for_protocol(protocol: &str) -> u16 {
42    DEFAULT_PROXY_PORTS
43        .iter()
44        .find_map(|(name, port)| (*name == protocol).then_some(*port))
45        .unwrap_or(0)
46}
47
48fn parse_no_proxy_entry(entry: &str) -> (String, u16) {
49    if let Some((host, port_str)) = entry.rsplit_once(':')
50        && let Ok(port) = port_str.parse::<u16>()
51    {
52        return (host.to_string(), port);
53    }
54    (entry.to_string(), 0)
55}
56
57fn should_proxy_hostname(hostname: &str, port: u16, env: Option<&ProviderEnv>) -> bool {
58    let no_proxy = get_proxy_env("no_proxy", env).to_lowercase();
59    if no_proxy.is_empty() {
60        return true;
61    }
62    if no_proxy == "*" {
63        return false;
64    }
65
66    no_proxy.split(|c: char| c == ',' || c.is_whitespace()).all(|proxy| {
67        if proxy.is_empty() {
68            return true;
69        }
70
71        let (mut proxy_hostname, proxy_port) = parse_no_proxy_entry(proxy);
72        if proxy_port != 0 && proxy_port != port {
73            return true;
74        }
75
76        let starts_with_wildcard = proxy_hostname.starts_with('.') || proxy_hostname.starts_with('*');
77        if !starts_with_wildcard {
78            return hostname != proxy_hostname;
79        }
80
81        if proxy_hostname.starts_with('*') {
82            proxy_hostname = proxy_hostname[1..].to_string();
83        }
84        !hostname.ends_with(&proxy_hostname)
85    })
86}
87
88fn get_proxy_for_url(target_url: &str, env: Option<&ProviderEnv>) -> String {
89    let Some(parsed_url) = parse_proxy_target_url(target_url) else {
90        return String::new();
91    };
92
93    let Some(protocol) = parsed_url.scheme().split(':').next() else {
94        return String::new();
95    };
96    let Some(hostname) = parsed_url.host_str() else {
97        return String::new();
98    };
99
100    let port = parsed_url.port().unwrap_or_else(|| default_port_for_protocol(protocol));
101    if !should_proxy_hostname(hostname, port, env) {
102        return String::new();
103    }
104
105    let protocol_proxy_key = format!("{protocol}_proxy");
106    let mut proxy = get_proxy_env(&protocol_proxy_key, env);
107    if proxy.is_empty() {
108        proxy = get_proxy_env("all_proxy", env);
109    }
110    if proxy.is_empty() {
111        return String::new();
112    }
113    if !proxy.contains("://") {
114        proxy = format!("{protocol}://{proxy}");
115    }
116    proxy
117}
118
119/// Map a WebSocket URL to an HTTP(S) URL for proxy rule lookup (mirroring pi-ai Codex).
120pub fn websocket_proxy_lookup_url(ws_url: &str) -> String {
121    if let Some(rest) = ws_url.strip_prefix("wss://") {
122        format!("https://{rest}")
123    } else if let Some(rest) = ws_url.strip_prefix("ws://") {
124        format!("http://{rest}")
125    } else {
126        ws_url.to_string()
127    }
128}
129
130/// Resolve an HTTP or HTTPS proxy URL for `target_url` from scoped env and process env.
131pub fn resolve_http_proxy_url_for_target(target_url: &str, env: Option<&ProviderEnv>) -> Result<Option<Url>> {
132    let proxy = get_proxy_for_url(target_url, env);
133    if proxy.is_empty() {
134        return Ok(None);
135    }
136
137    let proxy_url = Url::parse(&proxy).map_err(|error| anyhow!("Invalid proxy URL {:?}: {error}", proxy))?;
138
139    if proxy_url.scheme() != "http" && proxy_url.scheme() != "https" {
140        return Err(anyhow!(
141            "{UNSUPPORTED_PROXY_PROTOCOL_MESSAGE} Got {}:",
142            proxy_url.scheme()
143        ));
144    }
145
146    Ok(Some(proxy_url))
147}