1use crate::http_input::error::HttpInputError;
4use reqwest::header::{HeaderName, HeaderValue};
5use std::fmt;
6use std::time::Duration;
7
8pub const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
10pub const DEFAULT_HEADER_TIMEOUT: Duration = Duration::from_secs(15);
12pub const DEFAULT_READ_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
14pub const STOP_POLL_TICK: Duration = Duration::from_millis(25);
16
17const MAX_HEADER_NAME_BYTES: usize = 8 * 1024;
18const MAX_HEADER_VALUE_BYTES: usize = 8 * 1024;
19const MAX_HEADER_TOTAL_BYTES: usize = 32 * 1024;
20
21#[derive(Debug, Clone)]
23#[non_exhaustive]
24pub struct HttpTimeouts {
25 pub connect: Duration,
29 pub response_headers: Duration,
31 pub read_idle: Option<Duration>,
33}
34
35impl Default for HttpTimeouts {
36 fn default() -> Self {
37 Self {
38 connect: DEFAULT_CONNECT_TIMEOUT,
39 response_headers: DEFAULT_HEADER_TIMEOUT,
40 read_idle: Some(DEFAULT_READ_IDLE_TIMEOUT),
41 }
42 }
43}
44
45impl HttpTimeouts {
46 pub(crate) fn validate(&self) -> Result<(), HttpInputError> {
47 if self.connect.is_zero() || self.response_headers.is_zero() {
48 return Err(HttpInputError::InvalidTimeout);
49 }
50 if let Some(idle) = self.read_idle {
51 if idle.is_zero() {
52 return Err(HttpInputError::InvalidTimeout);
53 }
54 }
55 Ok(())
56 }
57}
58
59#[derive(Debug, Clone)]
61#[non_exhaustive]
62pub struct ReconnectPolicy {
63 pub enabled: bool,
65 pub reconnect_streamed: bool,
71 pub reconnect_at_eof: bool,
75 pub max_retries: u32,
77 pub max_delay: Duration,
79 pub max_total_delay: Duration,
81 pub respect_retry_after: bool,
83 pub retry_http_statuses: Vec<u16>,
85 pub require_validator: bool,
88}
89
90impl Default for ReconnectPolicy {
91 fn default() -> Self {
92 Self {
93 enabled: false,
94 reconnect_streamed: false,
95 reconnect_at_eof: false,
96 max_retries: 0,
97 max_delay: Duration::from_secs(30),
98 max_total_delay: Duration::ZERO,
99 respect_retry_after: true,
100 retry_http_statuses: vec![408, 429, 500, 502, 503, 504],
101 require_validator: true,
102 }
103 }
104}
105
106impl ReconnectPolicy {
107 pub fn seekable_default() -> Self {
109 Self {
110 enabled: true,
111 reconnect_streamed: false,
112 reconnect_at_eof: false,
113 max_retries: 5,
114 max_delay: Duration::from_secs(30),
115 max_total_delay: Duration::from_secs(60),
116 respect_retry_after: true,
117 retry_http_statuses: vec![408, 429, 500, 502, 503, 504],
118 require_validator: true,
119 }
120 }
121
122 pub fn streamed_default() -> Self {
132 Self {
133 enabled: true,
134 reconnect_streamed: true,
135 reconnect_at_eof: true,
136 max_retries: 5,
137 max_delay: Duration::from_secs(30),
138 max_total_delay: Duration::from_secs(60),
139 respect_retry_after: true,
140 retry_http_statuses: vec![408, 429, 500, 502, 503, 504],
141 require_validator: true,
142 }
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148#[non_exhaustive]
149pub enum RootPolicy {
150 System,
152 CustomOnly,
154}
155
156#[derive(Clone, Default)]
158#[non_exhaustive]
159pub enum ProxyPolicy {
160 #[default]
162 Environment,
163 Disabled,
165 Explicit(ProxyConfig),
167}
168
169impl fmt::Debug for ProxyPolicy {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 match self {
172 Self::Environment => f.write_str("Environment"),
173 Self::Disabled => f.write_str("Disabled"),
174 Self::Explicit(cfg) => f.debug_tuple("Explicit").field(cfg).finish(),
175 }
176 }
177}
178
179#[derive(Clone)]
181pub struct ProxyConfig {
182 url: String,
183 username: Option<String>,
184 password: Option<String>,
185}
186
187impl fmt::Debug for ProxyConfig {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 f.debug_struct("ProxyConfig")
190 .field("url", &redact_proxy_url(&self.url))
191 .field("username", &self.username.as_ref().map(|_| "***"))
192 .field("password", &self.password.as_ref().map(|_| "***"))
193 .finish()
194 }
195}
196
197fn redact_proxy_url(raw: &str) -> String {
198 match reqwest::Url::parse(raw) {
199 Ok(mut url) => {
200 let _ = url.set_username("");
201 let _ = url.set_password(None);
202 url.set_query(None);
203 url.set_fragment(None);
204 url.to_string()
205 }
206 Err(_) => "[invalid-proxy-url]".into(),
207 }
208}
209
210impl ProxyConfig {
211 pub fn new(url: impl Into<String>) -> Self {
213 Self {
214 url: url.into(),
215 username: None,
216 password: None,
217 }
218 }
219
220 pub fn username(mut self, username: impl Into<String>) -> Self {
222 self.username = Some(username.into());
223 self
224 }
225
226 pub fn password(mut self, password: impl Into<String>) -> Self {
228 self.password = Some(password.into());
229 self
230 }
231
232 pub(crate) fn url(&self) -> &str {
233 &self.url
234 }
235
236 pub(crate) fn username_ref(&self) -> Option<&str> {
237 self.username.as_deref()
238 }
239
240 pub(crate) fn password_ref(&self) -> Option<&str> {
241 self.password.as_deref()
242 }
243}
244
245const RESERVED_HEADERS: &[&str] = &[
246 "host",
247 "content-length",
248 "content-range",
249 "transfer-encoding",
250 "connection",
251 "range",
252 "if-range",
253 "accept-encoding",
254 "proxy-authorization",
255];
256
257pub(crate) fn is_reserved_header(name: &str) -> bool {
258 RESERVED_HEADERS
259 .iter()
260 .any(|reserved| name.eq_ignore_ascii_case(reserved))
261}
262
263pub(crate) fn validate_header(name: &str, value: &str) -> Result<(), HttpInputError> {
264 if name.len() > MAX_HEADER_NAME_BYTES || value.len() > MAX_HEADER_VALUE_BYTES {
265 return Err(HttpInputError::HeaderInvalid {
266 name: name.to_string(),
267 });
268 }
269 if is_reserved_header(name) {
270 return Err(HttpInputError::HeaderReserved {
271 name: name.to_string(),
272 });
273 }
274 if HeaderName::from_bytes(name.as_bytes()).is_err()
275 || HeaderValue::from_bytes(value.as_bytes()).is_err()
276 {
277 return Err(HttpInputError::HeaderInvalid {
278 name: name.to_string(),
279 });
280 }
281 Ok(())
282}
283
284pub(crate) fn validate_header_set(headers: &[(String, String)]) -> Result<(), HttpInputError> {
285 let mut total = 0usize;
286 for (name, value) in headers {
287 validate_header(name, value)?;
288 total = total.saturating_add(name.len()).saturating_add(value.len());
289 if total > MAX_HEADER_TOTAL_BYTES {
290 return Err(HttpInputError::HeaderInvalid { name: name.clone() });
291 }
292 }
293 Ok(())
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn reserved_headers_are_rejected() {
302 for name in ["Accept-Encoding", "RANGE", "host", "Proxy-Authorization"] {
303 let err = validate_header(name, "x").unwrap_err();
304 assert!(
305 matches!(err, HttpInputError::HeaderReserved { .. }),
306 "{err}"
307 );
308 }
309 }
310
311 #[test]
312 fn authorization_is_allowed() {
313 validate_header("Authorization", "Bearer abc").unwrap();
314 validate_header("Cookie", "a=b").unwrap();
315 }
316
317 #[test]
318 fn zero_timeout_is_rejected() {
319 let t = HttpTimeouts {
320 connect: Duration::ZERO,
321 ..Default::default()
322 };
323 assert!(matches!(t.validate(), Err(HttpInputError::InvalidTimeout)));
324 }
325
326 #[test]
327 fn proxy_debug_redacts_password() {
328 let cfg = ProxyConfig::new("http://127.0.0.1:8080")
329 .username("u")
330 .password("super-secret");
331 let rendered = format!("{cfg:?}");
332 assert!(!rendered.contains("super-secret"), "{rendered}");
333 assert!(rendered.contains("***"), "{rendered}");
334 }
335
336 #[test]
337 fn proxy_debug_redacts_userinfo() {
338 let cfg = ProxyConfig::new("http://user:hunter2@127.0.0.1:8080").username("u");
339 let rendered = format!("{cfg:?}");
340 assert!(!rendered.contains("hunter2"), "{rendered}");
341 assert!(!rendered.contains("user:"), "{rendered}");
342 assert!(!rendered.contains("username: Some(\"u\")"), "{rendered}");
343 assert!(rendered.contains("127.0.0.1:8080"), "{rendered}");
344 }
345
346 #[test]
347 fn require_validator_defaults_true() {
348 assert!(ReconnectPolicy::default().require_validator);
349 assert!(ReconnectPolicy::seekable_default().require_validator);
350 assert!(!ReconnectPolicy::default().enabled);
351 assert!(ReconnectPolicy::seekable_default().enabled);
352 }
353
354 #[test]
355 fn streamed_default_has_a_usable_retry_budget() {
356 let policy = ReconnectPolicy::streamed_default();
357 assert!(policy.enabled);
358 assert!(policy.reconnect_streamed);
359 assert!(policy.reconnect_at_eof);
360 assert!(
361 policy.max_retries > 0,
362 "reconnect_streamed without max_retries is a silent no-op"
363 );
364 assert!(policy.require_validator);
365 }
366}