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 fn validate_outbound_url(url: &str) -> Result<url::Url, OutboundUrlError> {
68 let parsed = url::Url::parse(url).map_err(|e| OutboundUrlError::Parse(e.to_string()))?;
69 let host = parsed
70 .host()
71 .ok_or_else(|| OutboundUrlError::Parse("missing host".to_owned()))?;
72
73 let is_loopback_host = match &host {
74 url::Host::Domain(d) => d.eq_ignore_ascii_case("localhost"),
75 url::Host::Ipv4(ip) => ip.is_loopback(),
76 url::Host::Ipv6(ip) => ip.is_loopback(),
77 };
78
79 match parsed.scheme() {
80 "https" => {},
81 "http" if is_loopback_host => {},
82 "http" => return Err(OutboundUrlError::NonLoopbackHttp),
83 scheme => return Err(OutboundUrlError::Scheme(scheme.to_owned())),
84 }
85
86 if is_loopback_host {
87 return Ok(parsed);
88 }
89
90 let blocked = match host {
91 url::Host::Domain(_) => false,
92 url::Host::Ipv4(ip) => is_blocked_v4(ip),
93 url::Host::Ipv6(ip) => {
94 ip.to_ipv4_mapped().map_or_else(
98 || {
99 let segments = ip.segments();
100 let is_unique_local = (segments[0] & 0xfe00) == 0xfc00;
101 let is_link_local = (segments[0] & 0xffc0) == 0xfe80;
102 ip.is_loopback() || ip.is_unspecified() || is_unique_local || is_link_local
103 },
104 is_blocked_v4,
105 )
106 },
107 };
108 if blocked {
109 return Err(OutboundUrlError::BlockedHost(
110 parsed.host_str().unwrap_or_default().to_owned(),
111 ));
112 }
113 Ok(parsed)
114}
115
116fn is_cgnat_shared_v4(ip: std::net::Ipv4Addr) -> bool {
119 let [a, b, _, _] = ip.octets();
120 a == 100 && (64..=127).contains(&b)
121}
122
123fn is_blocked_v4(ip: std::net::Ipv4Addr) -> bool {
124 ip.is_private()
125 || ip.is_loopback()
126 || ip.is_link_local()
127 || ip.is_unspecified()
128 || ip.is_broadcast()
129 || is_cgnat_shared_v4(ip)
130}