systemprompt_models/
net.rs1use std::time::Duration;
10use thiserror::Error;
11
12#[derive(Debug, Error)]
14pub enum OutboundUrlError {
15 #[error("invalid url: {0}")]
16 Parse(String),
17 #[error("unsupported url scheme: {0}")]
18 Scheme(String),
19 #[error("http url only permitted for loopback hosts")]
20 NonLoopbackHttp,
21 #[error("host {0} is in a blocked private range")]
22 BlockedHost(String),
23}
24
25pub const HTTP_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
26
27pub const HTTP_DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
28
29pub const HTTP_HEALTH_CHECK_TIMEOUT: Duration = Duration::from_secs(5);
30
31pub const HTTP_AUTH_VERIFY_TIMEOUT: Duration = Duration::from_secs(10);
32
33pub const HTTP_SYNC_DEPLOY_TIMEOUT: Duration = Duration::from_secs(60);
34
35pub const HTTP_STREAM_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
36
37pub const HTTP_KEEPALIVE: Duration = Duration::from_secs(60);
38
39pub const HTTP_POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(90);
40
41pub const AGENT_MONITOR_TCP_TIMEOUT: Duration = Duration::from_secs(15);
42
43pub const AGENT_READINESS_TCP_TIMEOUT: Duration = Duration::from_secs(2);
44
45pub const IMAGE_GEN_LONG_POLL_TIMEOUT: Duration = Duration::from_secs(300);
46
47pub const IMAGE_GEN_OPENAI_TIMEOUT: Duration = Duration::from_secs(120);
48
49pub const AI_PROVIDER_REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
51
52pub const AI_STREAM_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
54
55pub const MCP_TOOL_EXECUTION_TIMEOUT: Duration = Duration::from_secs(30);
57
58pub const TRUSTED_HTTP_HOSTS_ENV: &str = "SYSTEMPROMPT_TRUSTED_HTTP_HOSTS";
69
70#[must_use]
75pub fn trusted_http_hosts_from_env() -> Vec<String> {
76 std::env::var(TRUSTED_HTTP_HOSTS_ENV)
77 .ok()
78 .map(|raw| {
79 raw.split(',')
80 .map(|s| s.trim().to_ascii_lowercase())
81 .filter(|s| !s.is_empty())
82 .collect()
83 })
84 .unwrap_or_default()
85}
86
87pub fn validate_outbound_url(url: &str) -> Result<url::Url, OutboundUrlError> {
97 let no_trust: [&str; 0] = [];
98 validate_outbound_url_with_trust(url, &no_trust)
99}
100
101pub fn validate_outbound_url_with_trust(
115 url: &str,
116 trusted_http_hosts: &[impl AsRef<str>],
117) -> Result<url::Url, OutboundUrlError> {
118 let parsed = url::Url::parse(url).map_err(|e| OutboundUrlError::Parse(e.to_string()))?;
119 let host = parsed
120 .host()
121 .ok_or_else(|| OutboundUrlError::Parse("missing host".to_owned()))?;
122
123 let is_loopback_host = match &host {
124 url::Host::Domain(d) => d.eq_ignore_ascii_case("localhost"),
125 url::Host::Ipv4(ip) => ip.is_loopback(),
126 url::Host::Ipv6(ip) => ip.is_loopback(),
127 };
128
129 let host_str = parsed.host_str().unwrap_or_default().to_ascii_lowercase();
130 let is_trusted = !host_str.is_empty()
131 && trusted_http_hosts
132 .iter()
133 .any(|h| h.as_ref().eq_ignore_ascii_case(&host_str));
134
135 match parsed.scheme() {
136 "https" => {},
137 "http" if is_loopback_host || is_trusted => {},
138 "http" => return Err(OutboundUrlError::NonLoopbackHttp),
139 scheme => return Err(OutboundUrlError::Scheme(scheme.to_owned())),
140 }
141
142 if is_loopback_host || is_trusted {
143 return Ok(parsed);
144 }
145
146 let blocked = match host {
147 url::Host::Domain(_) => false,
148 url::Host::Ipv4(ip) => is_blocked_v4(ip),
149 url::Host::Ipv6(ip) => {
150 ip.to_ipv4_mapped().map_or_else(
154 || {
155 let segments = ip.segments();
156 let is_unique_local = (segments[0] & 0xfe00) == 0xfc00;
157 let is_link_local = (segments[0] & 0xffc0) == 0xfe80;
158 ip.is_loopback() || ip.is_unspecified() || is_unique_local || is_link_local
159 },
160 is_blocked_v4,
161 )
162 },
163 };
164 if blocked {
165 return Err(OutboundUrlError::BlockedHost(
166 parsed.host_str().unwrap_or_default().to_owned(),
167 ));
168 }
169 Ok(parsed)
170}
171
172fn is_cgnat_shared_v4(ip: std::net::Ipv4Addr) -> bool {
175 let [a, b, _, _] = ip.octets();
176 a == 100 && (64..=127).contains(&b)
177}
178
179fn is_blocked_v4(ip: std::net::Ipv4Addr) -> bool {
180 ip.is_private()
181 || ip.is_loopback()
182 || ip.is_link_local()
183 || ip.is_unspecified()
184 || ip.is_broadcast()
185 || is_cgnat_shared_v4(ip)
186}