elph_ai/api/
http_proxy.rs1use 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
119pub 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
130pub 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}