systemprompt_models/
net.rs1use std::time::Duration;
13use thiserror::Error;
14
15#[derive(Debug, Error)]
17pub enum OutboundUrlError {
18 #[error("invalid url: {0}")]
19 Parse(String),
20 #[error("unsupported url scheme: {0}")]
21 Scheme(String),
22 #[error("http url only permitted for loopback hosts")]
23 NonLoopbackHttp,
24 #[error("host {0} is in a blocked private range")]
25 BlockedHost(String),
26}
27
28pub const HTTP_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
29
30pub const HTTP_DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
31
32pub const HTTP_HEALTH_CHECK_TIMEOUT: Duration = Duration::from_secs(5);
33
34pub const HTTP_AUTH_VERIFY_TIMEOUT: Duration = Duration::from_secs(10);
35
36pub const HTTP_SYNC_DEPLOY_TIMEOUT: Duration = Duration::from_secs(60);
37
38pub const HTTP_STREAM_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
39
40pub const HTTP_KEEPALIVE: Duration = Duration::from_secs(60);
41
42pub const HTTP_POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(90);
43
44pub const AGENT_MONITOR_TCP_TIMEOUT: Duration = Duration::from_secs(15);
45
46pub const AGENT_READINESS_TCP_TIMEOUT: Duration = Duration::from_secs(2);
47
48pub const IMAGE_GEN_LONG_POLL_TIMEOUT: Duration = Duration::from_secs(300);
49
50pub const IMAGE_GEN_OPENAI_TIMEOUT: Duration = Duration::from_secs(120);
51
52pub const AI_PROVIDER_REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
53
54pub const MCP_TOOL_EXECUTION_TIMEOUT: Duration = Duration::from_secs(30);
55
56pub const TRUSTED_HTTP_HOSTS_ENV: &str = "SYSTEMPROMPT_TRUSTED_HTTP_HOSTS";
57
58#[must_use]
59pub fn trusted_http_hosts_from_env() -> Vec<String> {
60 std::env::var(TRUSTED_HTTP_HOSTS_ENV)
61 .ok()
62 .map(|raw| {
63 raw.split(',')
64 .map(|s| s.trim().to_ascii_lowercase())
65 .filter(|s| !s.is_empty())
66 .collect()
67 })
68 .unwrap_or_default()
69}
70
71pub fn trusted_hosts_env_entry(
72 lookup: impl Fn(&str) -> Option<String>,
73) -> Option<(String, String)> {
74 lookup(TRUSTED_HTTP_HOSTS_ENV).map(|trusted| (TRUSTED_HTTP_HOSTS_ENV.to_owned(), trusted))
75}
76
77pub fn validate_outbound_url(url: &str) -> Result<url::Url, OutboundUrlError> {
78 let no_trust: [&str; 0] = [];
79 validate_outbound_url_with_trust(url, &no_trust)
80}
81
82pub fn validate_outbound_url_with_trust(
83 url: &str,
84 trusted_http_hosts: &[impl AsRef<str>],
85) -> Result<url::Url, OutboundUrlError> {
86 let parsed = url::Url::parse(url).map_err(|e| OutboundUrlError::Parse(e.to_string()))?;
87 let host = parsed
88 .host()
89 .ok_or_else(|| OutboundUrlError::Parse("missing host".to_owned()))?;
90
91 let is_loopback_host = match &host {
92 url::Host::Domain(d) => d.eq_ignore_ascii_case("localhost"),
93 url::Host::Ipv4(ip) => ip.is_loopback(),
94 url::Host::Ipv6(ip) => ip.is_loopback(),
95 };
96
97 let host_str = parsed.host_str().unwrap_or_default().to_ascii_lowercase();
98 let is_trusted = !host_str.is_empty()
99 && trusted_http_hosts
100 .iter()
101 .any(|h| h.as_ref().eq_ignore_ascii_case(&host_str));
102
103 match parsed.scheme() {
104 "https" => {},
105 "http" if is_loopback_host || is_trusted => {},
106 "http" => return Err(OutboundUrlError::NonLoopbackHttp),
107 scheme => return Err(OutboundUrlError::Scheme(scheme.to_owned())),
108 }
109
110 if is_loopback_host || is_trusted {
111 return Ok(parsed);
112 }
113
114 let blocked = match host {
115 url::Host::Domain(_) => false,
116 url::Host::Ipv4(ip) => is_blocked_v4(ip),
117 url::Host::Ipv6(ip) => is_blocked_v6(ip),
118 };
119 if blocked {
120 return Err(OutboundUrlError::BlockedHost(
121 parsed.host_str().unwrap_or_default().to_owned(),
122 ));
123 }
124 Ok(parsed)
125}
126
127#[must_use]
128pub fn is_blocked_ip(ip: std::net::IpAddr) -> bool {
129 match ip {
130 std::net::IpAddr::V4(v4) => is_blocked_v4(v4),
131 std::net::IpAddr::V6(v6) => is_blocked_v6(v6),
132 }
133}
134
135fn is_blocked_v6(ip: std::net::Ipv6Addr) -> bool {
138 ip.to_ipv4_mapped().map_or_else(
139 || {
140 let segments = ip.segments();
141 let is_unique_local = (segments[0] & 0xfe00) == 0xfc00;
142 let is_link_local = (segments[0] & 0xffc0) == 0xfe80;
143 ip.is_loopback() || ip.is_unspecified() || is_unique_local || is_link_local
144 },
145 is_blocked_v4,
146 )
147}
148
149fn is_cgnat_shared_v4(ip: std::net::Ipv4Addr) -> bool {
152 let [a, b, _, _] = ip.octets();
153 a == 100 && (64..=127).contains(&b)
154}
155
156fn is_blocked_v4(ip: std::net::Ipv4Addr) -> bool {
157 ip.is_private()
158 || ip.is_loopback()
159 || ip.is_link_local()
160 || ip.is_unspecified()
161 || ip.is_broadcast()
162 || is_cgnat_shared_v4(ip)
163}