1use crate::target::REDACTED_SECRET;
16use hashbrown::HashMap;
17use hyper::HeaderMap;
18use regex::Regex;
19use s3s::{S3Request, S3Response};
20use serde::{Deserialize, Serialize};
21use std::net::IpAddr;
22use std::path::Path;
23use std::sync::LazyLock;
24use thiserror::Error;
25use url::Url;
26
27const SENSITIVE_HEADERS: &[&str] = &[
33 "authorization",
34 "x-amz-security-token",
35 "x-amz-content-sha256",
36 "x-amz-server-side-encryption-customer-key",
37 "x-amz-server-side-encryption-customer-key-md5",
38 "x-amz-copy-source-server-side-encryption-customer-key",
39 "x-amz-copy-source-server-side-encryption-customer-key-md5",
40 "cookie",
41 "set-cookie",
42];
43
44fn is_sensitive_header(name: &str) -> bool {
47 SENSITIVE_HEADERS.iter().any(|h| name.eq_ignore_ascii_case(h))
48}
49
50static HOST_LABEL_REGEX: LazyLock<Regex> =
51 LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$").expect("operation should succeed"));
52
53#[derive(Error, Debug)]
55pub enum NetError {
56 #[error("invalid argument")]
57 InvalidArgument,
58 #[error("invalid hostname")]
59 InvalidHost,
60 #[error("missing '[' in host")]
61 MissingBracket,
62 #[error("parse error: {0}")]
63 ParseError(String),
64 #[error("unexpected scheme: {0}")]
65 UnexpectedScheme(String),
66 #[error("scheme appears with empty host")]
67 SchemeWithEmptyHost,
68}
69
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72pub struct Host {
73 pub name: String,
74 pub port: Option<u16>,
75}
76
77impl Host {
78 pub fn is_empty(&self) -> bool {
79 self.name.is_empty()
80 }
81
82 pub fn equal(&self, other: &Host) -> bool {
83 self.to_string() == other.to_string()
84 }
85}
86
87impl std::fmt::Display for Host {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 match self.port {
90 Some(p) => write!(f, "{}:{}", self.name, p),
91 None => write!(f, "{}", self.name),
92 }
93 }
94}
95
96pub fn extract_req_params<T>(req: &S3Request<T>) -> HashMap<String, String> {
98 extract_params_header(&req.headers)
99}
100
101#[deprecated(since = "0.1.0", note = "Use extract_params_header instead")]
104pub fn extract_req_params_header(head: &HeaderMap) -> HashMap<String, String> {
105 extract_params_header(head)
106}
107
108pub fn extract_params_header(head: &HeaderMap) -> HashMap<String, String> {
115 let mut params = HashMap::new();
116 for (key, value) in head.iter() {
117 let name = key.as_str();
118 if is_sensitive_header(name) {
119 params.insert(name.to_string(), REDACTED_SECRET.to_string());
120 } else if let Ok(val_str) = value.to_str() {
121 params.insert(name.to_string(), val_str.to_string());
122 }
123 }
124 params
125}
126
127pub fn extract_resp_elements<T>(resp: &S3Response<T>) -> HashMap<String, String> {
129 extract_params_header(&resp.headers)
130}
131
132pub fn get_request_host(headers: &HeaderMap) -> String {
134 headers
135 .get("host")
136 .and_then(|v| v.to_str().ok())
137 .unwrap_or_default()
138 .to_string()
139}
140
141pub fn get_request_port(headers: &HeaderMap) -> u16 {
148 if let Some(port) = headers
149 .get("x-forwarded-port")
150 .and_then(|v| v.to_str().ok())
151 .and_then(|s| s.parse::<u16>().ok())
152 {
153 return port;
154 }
155
156 if let Some(host) = headers.get("host").and_then(|v| v.to_str().ok()) {
157 if let Some(idx) = host.rfind(':') {
158 let valid_colon = match host.rfind(']') {
159 Some(close_bracket_idx) => idx > close_bracket_idx,
160 None => true,
161 };
162
163 if valid_colon
164 && let Ok(port) = host[idx + 1..].parse::<u16>()
165 && port > 0
166 {
167 return port;
168 }
169 }
170
171 if let Some(proto) = headers.get("x-forwarded-proto").and_then(|v| v.to_str().ok()) {
172 match proto {
173 "http" => return 80,
174 "https" => return 443,
175 _ => {}
176 }
177 }
178 }
179
180 headers
181 .get("port")
182 .and_then(|v| v.to_str().ok())
183 .and_then(|s| s.parse::<u16>().ok())
184 .unwrap_or(0)
185}
186
187pub fn get_request_content_length(headers: &HeaderMap) -> u64 {
189 headers
190 .get("content-length")
191 .and_then(|v| v.to_str().ok())
192 .and_then(|s| s.parse::<u64>().ok())
193 .unwrap_or(0)
194}
195
196pub fn get_request_referer(headers: &HeaderMap) -> String {
198 headers
199 .get("referer")
200 .and_then(|v| v.to_str().ok())
201 .unwrap_or_default()
202 .to_string()
203}
204
205pub fn get_request_user_agent(headers: &HeaderMap) -> String {
207 headers
208 .get("user-agent")
209 .and_then(|v| v.to_str().ok())
210 .unwrap_or_default()
211 .to_string()
212}
213
214pub fn parse_host(s: &str) -> Result<Host, NetError> {
216 if s.is_empty() {
217 return Err(NetError::InvalidArgument);
218 }
219
220 let is_valid_host = |host: &str| -> bool {
221 if host.is_empty() {
222 return true;
223 }
224 if host.parse::<IpAddr>().is_ok() {
225 return true;
226 }
227 if !(1..=253).contains(&host.len()) {
228 return false;
229 }
230 for (i, label) in host.split('.').enumerate() {
231 if i + 1 == host.split('.').count() && label.is_empty() {
232 continue;
233 }
234 if !(1..=63).contains(&label.len()) || !HOST_LABEL_REGEX.is_match(label) {
235 return false;
236 }
237 }
238 true
239 };
240
241 let (host, port) = if let Some(rest) = s.strip_prefix('[') {
242 let Some(end) = rest.find(']') else {
243 return Err(NetError::MissingBracket);
244 };
245 let host = rest[..end].to_string();
246 let port_str = &rest[end + 1..];
247 let port = if let Some(port_str) = port_str.strip_prefix(':') {
248 if port_str.is_empty() {
249 None
250 } else {
251 Some(port_str.parse().map_err(|_| NetError::ParseError(port_str.to_string()))?)
252 }
253 } else if port_str.is_empty() {
254 None
255 } else {
256 return Err(NetError::InvalidHost);
257 };
258
259 (host, port)
260 } else {
261 if s.contains(']') {
262 return Err(NetError::MissingBracket);
263 }
264
265 let (host_str, port_str) = if s.matches(':').count() > 1 {
266 (s, "")
267 } else {
268 s.rsplit_once(':').map_or((s, ""), |(h, p)| (h, p))
269 };
270 let port = if !port_str.is_empty() {
271 Some(port_str.parse().map_err(|_| NetError::ParseError(port_str.to_string()))?)
272 } else {
273 None
274 };
275
276 (trim_ipv6(host_str)?, port)
277 };
278
279 let trimmed_host = host.split('%').next().unwrap_or(&host);
280
281 if !is_valid_host(trimmed_host) {
282 return Err(NetError::InvalidHost);
283 }
284
285 Ok(Host { name: host, port })
286}
287
288fn trim_ipv6(host: &str) -> Result<String, NetError> {
289 if host.ends_with(']') {
290 if !host.starts_with('[') {
291 return Err(NetError::MissingBracket);
292 }
293 Ok(host[1..host.len() - 1].to_string())
294 } else {
295 Ok(host.to_string())
296 }
297}
298
299#[derive(Debug, Clone)]
301pub struct ParsedURL(pub Url);
302
303impl ParsedURL {
304 pub fn is_empty(&self) -> bool {
305 self.0.as_str() == "" || (self.0.scheme() == "about" && self.0.path() == "blank")
306 }
307
308 pub fn hostname(&self) -> String {
309 self.0.host_str().unwrap_or("").to_string()
310 }
311
312 pub fn port(&self) -> String {
313 match self.0.port() {
314 Some(p) => p.to_string(),
315 None => match self.0.scheme() {
316 "http" => "80".to_string(),
317 "https" => "443".to_string(),
318 _ => "".to_string(),
319 },
320 }
321 }
322
323 pub fn scheme(&self) -> &str {
324 self.0.scheme()
325 }
326
327 pub fn url(&self) -> &Url {
328 &self.0
329 }
330}
331
332impl std::fmt::Display for ParsedURL {
333 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334 let mut url = self.0.clone();
335 if let Some(host) = url.host_str().map(|h| h.to_string())
336 && let Some(port) = url.port()
337 && ((url.scheme() == "http" && port == 80) || (url.scheme() == "https" && port == 443))
338 {
339 let _ = url.set_host(Some(&host));
340 let _ = url.set_port(None);
341 }
342 let mut s = url.to_string();
343
344 if s.ends_with('/') && url.path() == "/" {
345 s.pop();
346 }
347
348 write!(f, "{s}")
349 }
350}
351
352impl serde::Serialize for ParsedURL {
353 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
354 where
355 S: serde::Serializer,
356 {
357 serializer.serialize_str(&self.to_string())
358 }
359}
360
361impl<'de> serde::Deserialize<'de> for ParsedURL {
362 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
363 where
364 D: serde::Deserializer<'de>,
365 {
366 let s: String = serde::Deserialize::deserialize(deserializer)?;
367 if s.is_empty() {
368 Ok(ParsedURL(Url::parse("about:blank").expect("operation should succeed")))
369 } else {
370 parse_url(&s).map_err(serde::de::Error::custom)
371 }
372 }
373}
374
375pub fn parse_url(s: &str) -> Result<ParsedURL, NetError> {
377 if let Some(scheme_end) = s.find("://")
378 && s[scheme_end + 3..].starts_with('/')
379 {
380 let scheme = &s[..scheme_end];
381 if !scheme.is_empty() {
382 return Err(NetError::SchemeWithEmptyHost);
383 }
384 }
385
386 let mut uu = Url::parse(s).map_err(|e| NetError::ParseError(e.to_string()))?;
387 if uu.host_str().is_none_or(|h| h.is_empty()) {
388 if uu.scheme() != "" {
389 return Err(NetError::SchemeWithEmptyHost);
390 }
391 } else {
392 let port_str = uu.port().map(|p| p.to_string()).unwrap_or_else(|| match uu.scheme() {
393 "http" => "80".to_string(),
394 "https" => "443".to_string(),
395 _ => "".to_string(),
396 });
397
398 if !port_str.is_empty() {
399 let host_port = format!("{}:{}", uu.host_str().expect("operation should succeed"), port_str);
400 parse_host(&host_port)?;
401 }
402 }
403
404 if !uu.path().is_empty() {
405 let mut cleaned_path = String::new();
406 for comp in Path::new(uu.path()).components() {
407 use std::path::Component;
408 match comp {
409 Component::RootDir => cleaned_path.push('/'),
410 Component::Normal(s) => {
411 if !cleaned_path.ends_with('/') {
412 cleaned_path.push('/');
413 }
414 cleaned_path.push_str(&s.to_string_lossy());
415 }
416 _ => {}
417 }
418 }
419 if s.ends_with('/') && !cleaned_path.ends_with('/') {
420 cleaned_path.push('/');
421 }
422 if cleaned_path.is_empty() {
423 cleaned_path.push('/');
424 }
425 uu.set_path(&cleaned_path);
426 }
427
428 Ok(ParsedURL(uu))
429}
430
431pub fn parse_http_url(s: &str) -> Result<ParsedURL, NetError> {
432 let u = parse_url(s)?;
433 match u.0.scheme() {
434 "http" | "https" => Ok(u),
435 _ => Err(NetError::UnexpectedScheme(u.0.scheme().to_string())),
436 }
437}
438
439pub fn is_network_or_host_down(err: &std::io::Error, expect_timeouts: bool) -> bool {
440 if err.kind() == std::io::ErrorKind::TimedOut {
441 return !expect_timeouts;
442 }
443 let err_str = err.to_string().to_lowercase();
444 err_str.contains("connection reset by peer")
445 || err_str.contains("connection timed out")
446 || err_str.contains("broken pipe")
447 || err_str.contains("use of closed network connection")
448}
449
450pub fn is_conn_reset_err(err: &std::io::Error) -> bool {
451 err.to_string().contains("connection reset by peer") || matches!(err.raw_os_error(), Some(libc::ECONNRESET))
452}
453
454pub fn is_conn_refused_err(err: &std::io::Error) -> bool {
455 err.to_string().contains("connection refused") || matches!(err.raw_os_error(), Some(libc::ECONNREFUSED))
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461 use hyper::header::HeaderValue;
462
463 #[test]
464 fn extract_params_header_redacts_credential_headers() {
465 let mut headers = HeaderMap::new();
466 headers.insert("authorization", HeaderValue::from_static("AWS4-HMAC-SHA256 Credential=AKIA.../secret"));
467 headers.insert("x-amz-security-token", HeaderValue::from_static("FQoGZXIvYXdzE.../session-token"));
468 headers.insert("x-amz-content-sha256", HeaderValue::from_static("e3b0c44298fc1c149afbf4c8996fb924"));
469 headers.insert("x-amz-server-side-encryption-customer-key", HeaderValue::from_static("destination-key"));
470 headers.insert(
471 "x-amz-server-side-encryption-customer-key-md5",
472 HeaderValue::from_static("destination-key-md5"),
473 );
474 headers.insert(
475 "x-amz-copy-source-server-side-encryption-customer-key",
476 HeaderValue::from_static("source-key"),
477 );
478 headers.insert(
479 "x-amz-copy-source-server-side-encryption-customer-key-md5",
480 HeaderValue::from_static("source-key-md5"),
481 );
482 headers.insert("cookie", HeaderValue::from_static("session=abc123"));
483 headers.insert("content-type", HeaderValue::from_static("application/octet-stream"));
484 headers.insert("user-agent", HeaderValue::from_static("aws-cli/2.0"));
485
486 let params = extract_params_header(&headers);
487
488 for name in [
490 "authorization",
491 "x-amz-security-token",
492 "x-amz-content-sha256",
493 "x-amz-server-side-encryption-customer-key",
494 "x-amz-server-side-encryption-customer-key-md5",
495 "x-amz-copy-source-server-side-encryption-customer-key",
496 "x-amz-copy-source-server-side-encryption-customer-key-md5",
497 "cookie",
498 ] {
499 assert_eq!(params.get(name).map(String::as_str), Some(REDACTED_SECRET), "{name} must be redacted");
500 }
501 assert_eq!(params.get("content-type").map(String::as_str), Some("application/octet-stream"));
503 assert_eq!(params.get("user-agent").map(String::as_str), Some("aws-cli/2.0"));
504 }
505
506 #[test]
507 fn is_sensitive_header_matches_case_insensitively() {
508 assert!(is_sensitive_header("Authorization"));
509 assert!(is_sensitive_header("X-Amz-Security-Token"));
510 assert!(is_sensitive_header("X-AMZ-CONTENT-SHA256"));
511 assert!(is_sensitive_header("X-Amz-Server-Side-Encryption-Customer-Key"));
512 assert!(is_sensitive_header("X-Amz-Server-Side-Encryption-Customer-Key-MD5"));
513 assert!(is_sensitive_header("X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key"));
514 assert!(is_sensitive_header("X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-MD5"));
515 assert!(!is_sensitive_header("content-type"));
516 assert!(!is_sensitive_header("x-amz-request-id"));
517 }
518
519 #[test]
520 fn test_get_request_port() {
521 let mut headers = HeaderMap::new();
522
523 assert_eq!(get_request_port(&headers), 0);
524
525 headers.insert("port", HeaderValue::from_static("8080"));
526 assert_eq!(get_request_port(&headers), 8080);
527
528 headers.remove("port");
529 headers.insert("host", HeaderValue::from_static("example.com:9000"));
530 assert_eq!(get_request_port(&headers), 9000);
531
532 headers.insert("host", HeaderValue::from_static("example.com"));
533 assert_eq!(get_request_port(&headers), 0);
534
535 headers.insert("host", HeaderValue::from_static("[::1]:9001"));
536 assert_eq!(get_request_port(&headers), 9001);
537
538 headers.insert("host", HeaderValue::from_static("[::1]"));
539 assert_eq!(get_request_port(&headers), 0);
540
541 headers.insert("x-forwarded-port", HeaderValue::from_static("7000"));
542 assert_eq!(get_request_port(&headers), 7000);
543
544 headers.remove("x-forwarded-port");
545 headers.insert("host", HeaderValue::from_static("example.com"));
546 headers.insert("x-forwarded-proto", HeaderValue::from_static("http"));
547 assert_eq!(get_request_port(&headers), 80);
548
549 headers.insert("x-forwarded-proto", HeaderValue::from_static("https"));
550 assert_eq!(get_request_port(&headers), 443);
551
552 headers.insert("x-forwarded-proto", HeaderValue::from_static("ftp"));
553 assert_eq!(get_request_port(&headers), 0);
554
555 headers.insert("host", HeaderValue::from_static("example.com:0"));
556 headers.insert("x-forwarded-proto", HeaderValue::from_static("https"));
557 assert_eq!(get_request_port(&headers), 443);
558
559 headers.remove("x-forwarded-proto");
560 assert_eq!(get_request_port(&headers), 0);
561 }
562
563 #[test]
564 fn parse_host_with_empty_string_returns_error() {
565 let result = parse_host("");
566 assert!(matches!(result, Err(NetError::InvalidArgument)));
567 }
568
569 #[test]
570 fn parse_host_with_valid_ipv4() {
571 let result = parse_host("192.168.1.1:8080");
572 assert!(result.is_ok());
573 let host = result.expect("operation should succeed");
574 assert_eq!(host.name, "192.168.1.1");
575 assert_eq!(host.port, Some(8080));
576 }
577
578 #[test]
579 fn parse_host_with_valid_hostname() {
580 let result = parse_host("example.com:443");
581 assert!(result.is_ok());
582 let host = result.expect("operation should succeed");
583 assert_eq!(host.name, "example.com");
584 assert_eq!(host.port, Some(443));
585 }
586
587 #[test]
588 fn parse_host_with_ipv6_brackets() {
589 let result = parse_host("[::1]:8080");
590 assert!(result.is_ok());
591 let host = result.expect("operation should succeed");
592 assert_eq!(host.name, "::1");
593 assert_eq!(host.port, Some(8080));
594 }
595
596 #[test]
597 fn parse_host_with_bare_ipv6_without_port() {
598 let result = parse_host("::1");
599 assert!(result.is_ok());
600 let host = result.expect("operation should succeed");
601 assert_eq!(host.name, "::1");
602 assert_eq!(host.port, None);
603 }
604
605 #[test]
606 fn parse_host_with_ipv6_zone_without_port() {
607 let result = parse_host("fe80::1%eth0");
608 assert!(result.is_ok());
609 let host = result.expect("operation should succeed");
610 assert_eq!(host.name, "fe80::1%eth0");
611 assert_eq!(host.port, None);
612 }
613
614 #[test]
615 fn parse_host_with_bracketed_ipv6_zone_and_port() {
616 let result = parse_host("[fe80::1%eth0]:9000");
617 assert!(result.is_ok());
618 let host = result.expect("operation should succeed");
619 assert_eq!(host.name, "fe80::1%eth0");
620 assert_eq!(host.port, Some(9000));
621 }
622
623 #[test]
624 fn parse_host_with_bracketed_ipv6_without_port() {
625 let result = parse_host("[::1]");
626 assert!(result.is_ok());
627 let host = result.expect("operation should succeed");
628 assert_eq!(host.name, "::1");
629 assert_eq!(host.port, None);
630 }
631
632 #[test]
633 fn parse_host_with_invalid_ipv6_missing_bracket() {
634 let result = parse_host("::1]:8080");
635 assert!(matches!(result, Err(NetError::MissingBracket)));
636 }
637
638 #[test]
639 fn parse_host_with_invalid_hostname() {
640 let result = parse_host("invalid..host:80");
641 assert!(matches!(result, Err(NetError::InvalidHost)));
642 }
643
644 #[test]
645 fn parse_host_without_port() {
646 let result = parse_host("example.com");
647 assert!(result.is_ok());
648 let host = result.expect("operation should succeed");
649 assert_eq!(host.name, "example.com");
650 assert_eq!(host.port, None);
651 }
652
653 #[test]
654 fn host_is_empty_when_name_is_empty() {
655 let host = Host {
656 name: "".to_string(),
657 port: None,
658 };
659 assert!(host.is_empty());
660 }
661
662 #[test]
663 fn host_is_not_empty_when_name_present() {
664 let host = Host {
665 name: "example.com".to_string(),
666 port: Some(80),
667 };
668 assert!(!host.is_empty());
669 }
670
671 #[test]
672 fn host_to_string_with_port() {
673 let host = Host {
674 name: "example.com".to_string(),
675 port: Some(80),
676 };
677 assert_eq!(host.to_string(), "example.com:80");
678 }
679
680 #[test]
681 fn host_to_string_without_port() {
682 let host = Host {
683 name: "example.com".to_string(),
684 port: None,
685 };
686 assert_eq!(host.to_string(), "example.com");
687 }
688
689 #[test]
690 fn parse_url_with_valid_http_url() {
691 let result = parse_url("http://example.com/path");
692 assert!(result.is_ok());
693 let parsed = result.expect("operation should succeed");
694 assert_eq!(parsed.hostname(), "example.com");
695 assert_eq!(parsed.port(), "80");
696 assert_eq!(parsed.scheme(), "http");
697 assert_eq!(parsed.to_string(), "http://example.com/path");
698 }
699
700 #[test]
701 fn parse_url_with_explicit_default_https_port() {
702 let result = parse_url("https://example.com:443/path");
703 assert!(result.is_ok());
704 let parsed = result.expect("operation should succeed");
705 assert_eq!(parsed.to_string(), "https://example.com/path");
706 }
707
708 #[test]
709 fn parse_url_with_empty_host_returns_error() {
710 let result = parse_url("http:///path");
711 assert!(matches!(result, Err(NetError::SchemeWithEmptyHost)));
712 }
713
714 #[test]
715 fn parse_url_with_invalid_host_returns_error() {
716 let result = parse_url("http://invalid..host/path");
717 assert!(matches!(result, Err(NetError::InvalidHost)));
718 }
719
720 #[test]
721 fn parse_url_normalizes_path() {
722 let result = parse_url("http://example.com//path/../path/");
723 assert!(result.is_ok());
724 let parsed = result.expect("operation should succeed");
725 assert_eq!(parsed.to_string(), "http://example.com/path/");
726 }
727}