1use std::error::Error;
25use std::fmt;
26use std::io;
27use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs};
28use std::str::FromStr;
29use std::sync::mpsc;
30use std::sync::Arc;
31use std::time::Duration;
32
33use crate::error::{HttpError, Phase};
34
35#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
37pub enum AddressClass {
38 Public,
39 Loopback,
40 LinkLocal,
42 Private,
44 Reserved,
47 Unspecified,
48 Multicast,
49}
50
51pub fn classify_ip(ip: IpAddr) -> AddressClass {
53 match ip {
54 IpAddr::V4(v4) => classify_v4(v4),
55 IpAddr::V6(v6) => classify_v6(v6),
56 }
57}
58
59fn classify_v4(ip: Ipv4Addr) -> AddressClass {
60 let [a, b, c, _] = ip.octets();
61 if a == 127 {
62 AddressClass::Loopback
63 } else if a == 0 {
64 AddressClass::Unspecified
65 } else if a == 169 && b == 254 {
66 AddressClass::LinkLocal
67 } else if a == 10
68 || (a == 172 && (16..=31).contains(&b))
69 || (a == 192 && b == 168)
70 || (a == 100 && (64..=127).contains(&b))
71 {
72 AddressClass::Private
73 } else if (224..=239).contains(&a) {
74 AddressClass::Multicast
75 } else if a >= 240
76 || (a == 192 && b == 0 && c == 0)
77 || (a == 192 && b == 0 && c == 2)
78 || (a == 198 && (b == 18 || b == 19))
79 || (a == 198 && b == 51 && c == 100)
80 || (a == 203 && b == 0 && c == 113)
81 {
82 AddressClass::Reserved
83 } else {
84 AddressClass::Public
85 }
86}
87
88fn v4_from_segments(hi: u16, lo: u16) -> Ipv4Addr {
89 Ipv4Addr::new((hi >> 8) as u8, hi as u8, (lo >> 8) as u8, lo as u8)
90}
91
92fn v4_from_octets(o: &[u8; 16], idx: [usize; 4]) -> Ipv4Addr {
93 Ipv4Addr::new(o[idx[0]], o[idx[1]], o[idx[2]], o[idx[3]])
94}
95
96pub fn embedded_v4s(ip: IpAddr) -> Vec<Ipv4Addr> {
102 let IpAddr::V6(v6) = ip else {
103 return Vec::new();
104 };
105 if v6.is_loopback() || v6.is_unspecified() {
106 return Vec::new();
107 }
108 let s = v6.segments();
109 if let Some(v4) = v6.to_ipv4_mapped() {
110 return vec![v4];
111 }
112 if s[..6] == [0, 0, 0, 0, 0, 0] || s[..6] == [0x64, 0xff9b, 0, 0, 0, 0] {
113 return vec![v4_from_segments(s[6], s[7])];
114 }
115 if s[0] == 0x2002 {
116 return vec![v4_from_segments(s[1], s[2])];
117 }
118 if s[0] == 0x64 && s[1] == 0xff9b && s[2] == 1 {
119 let o = v6.octets();
120 return vec![
121 v4_from_octets(&o, [12, 13, 14, 15]),
122 v4_from_octets(&o, [9, 10, 11, 12]),
123 v4_from_octets(&o, [7, 9, 10, 11]),
124 v4_from_octets(&o, [6, 7, 9, 10]),
125 ];
126 }
127 Vec::new()
128}
129
130pub fn embedded_v4(ip: IpAddr) -> Option<Ipv4Addr> {
133 let all = embedded_v4s(ip);
134 all.iter()
135 .copied()
136 .find(|v4| classify_v4(*v4) != AddressClass::Public)
137 .or_else(|| all.first().copied())
138}
139
140fn classify_v6(v6: Ipv6Addr) -> AddressClass {
141 let s = v6.segments();
142 if v6.is_loopback() {
143 return AddressClass::Loopback;
144 }
145 if v6.is_unspecified() {
146 return AddressClass::Unspecified;
147 }
148 let embedded = embedded_v4s(IpAddr::V6(v6));
149 if !embedded.is_empty() {
150 return embedded
151 .into_iter()
152 .map(classify_v4)
153 .find(|c| *c != AddressClass::Public)
154 .unwrap_or(AddressClass::Public);
155 }
156 if v6.is_multicast() {
157 AddressClass::Multicast
158 } else if s[0] & 0xffc0 == 0xfe80 {
159 AddressClass::LinkLocal
160 } else if s[0] & 0xfe00 == 0xfc00 || s[0] & 0xffc0 == 0xfec0 {
161 AddressClass::Private
162 } else if (s[0] == 0x2001 && s[1] == 0x0db8) || (s[0] == 0x2001 && s[1] == 0) {
163 AddressClass::Reserved
164 } else {
165 AddressClass::Public
166 }
167}
168
169fn hard_block(ip: IpAddr) -> Option<&'static str> {
175 fn one(ip: IpAddr) -> Option<&'static str> {
176 if classify_ip(ip) == AddressClass::LinkLocal {
177 return Some("is link-local (cloud metadata range)");
178 }
179 match ip {
180 IpAddr::V4(v4)
181 if [[100, 100, 100, 200], [168, 63, 129, 16], [192, 0, 0, 192]]
182 .contains(&v4.octets()) =>
183 {
184 Some("is a cloud metadata address")
185 }
186 IpAddr::V6(v6) if v6.segments()[0] == 0xfd00 && v6.segments()[1] == 0x0ec2 => {
187 Some("is in the AWS IPv6 metadata block fd00:ec2::/32")
188 }
189 _ => None,
190 }
191 }
192 one(ip).or_else(|| {
193 embedded_v4s(ip)
194 .into_iter()
195 .find_map(|v4| one(IpAddr::V4(v4)))
196 })
197}
198
199#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
201pub enum HostIpKind {
202 Ipv4DottedQuad,
204 Ipv4Legacy,
207 Ipv6,
209}
210
211#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
213pub struct ParsedHostIp {
214 pub addr: IpAddr,
215 pub kind: HostIpKind,
216 pub is_canonical: bool,
219}
220
221pub fn parse_host_ip(host: &str) -> Option<ParsedHostIp> {
245 let trimmed = host.trim_matches(['[', ']']);
246 if let Ok(ip) = trimmed.parse::<IpAddr>() {
247 let (kind, is_canonical) = match ip {
248 IpAddr::V4(_) => (HostIpKind::Ipv4DottedQuad, true),
249 IpAddr::V6(_) => (HostIpKind::Ipv6, true),
250 };
251 return Some(ParsedHostIp {
252 addr: ip,
253 kind,
254 is_canonical,
255 });
256 }
257 let (body, trailing_dot) = match trimmed.strip_suffix('.') {
258 Some(b) => (b, true),
259 None => (trimmed, false),
260 };
261 let v4 = parse_legacy_ipv4(body)?;
262 let canonical = !trailing_dot && body.eq_ignore_ascii_case(&v4.to_string());
263 Some(ParsedHostIp {
264 addr: IpAddr::V4(v4),
265 kind: if canonical {
266 HostIpKind::Ipv4DottedQuad
267 } else {
268 HostIpKind::Ipv4Legacy
269 },
270 is_canonical: canonical,
271 })
272}
273
274pub fn parse_legacy_ipv4(host: &str) -> Option<Ipv4Addr> {
278 let parts: Vec<&str> = host.split('.').collect();
279 if parts.is_empty() || parts.len() > 4 {
280 return None;
281 }
282 let mut values = Vec::with_capacity(parts.len());
283 for part in &parts {
284 let (digits, radix) =
285 if let Some(hex) = part.strip_prefix("0x").or_else(|| part.strip_prefix("0X")) {
286 (hex, 16)
287 } else if part.len() > 1 && part.starts_with('0') {
288 (&part[1..], 8)
289 } else {
290 (*part, 10)
291 };
292 if digits.is_empty() || !digits.chars().all(|c| c.is_digit(radix)) {
293 return None;
294 }
295 values.push(u64::from_str_radix(digits, radix).ok()?);
296 }
297 let (last, init) = values.split_last()?;
298 if init.iter().any(|v| *v > 255) {
299 return None;
300 }
301 let tail_bytes = 4 - init.len();
302 if *last >= 1u64 << (8 * tail_bytes) {
303 return None;
304 }
305 let mut addr: u32 = 0;
306 for v in init {
307 addr = (addr << 8) | *v as u32;
308 }
309 addr = addr.checked_shl(8 * tail_bytes as u32).unwrap_or(0) | *last as u32;
310 Some(Ipv4Addr::from(addr))
311}
312
313fn is_metadata_host(host: &str) -> bool {
315 matches!(
316 host,
317 "metadata" | "metadata.google.internal" | "metadata.goog" | "instance-data"
318 ) || host.ends_with(".metadata.google.internal")
319}
320
321pub(crate) fn normalize_host(host: &str) -> String {
323 let h = host.trim_matches(['[', ']']).to_ascii_lowercase();
324 h.strip_suffix('.').map(str::to_string).unwrap_or(h)
325}
326
327#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
329pub struct IpNet {
330 addr: IpAddr,
331 prefix: u8,
332}
333
334#[derive(Clone, Debug, Eq, PartialEq)]
336pub struct ParseIpNetError(pub String);
337
338impl fmt::Display for ParseIpNetError {
339 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340 write!(f, "invalid CIDR range: {}", self.0)
341 }
342}
343impl Error for ParseIpNetError {}
344
345impl IpNet {
346 pub fn new(addr: IpAddr, prefix: u8) -> Result<Self, ParseIpNetError> {
348 let max = if addr.is_ipv4() { 32 } else { 128 };
349 if prefix > max {
350 return Err(ParseIpNetError(format!("prefix {prefix} exceeds {max}")));
351 }
352 Ok(Self { addr, prefix })
353 }
354
355 pub fn addr(&self) -> IpAddr {
356 self.addr
357 }
358
359 pub fn prefix(&self) -> u8 {
360 self.prefix
361 }
362
363 pub fn contains(&self, ip: &IpAddr) -> bool {
366 match (self.addr, ip) {
367 (IpAddr::V4(net), IpAddr::V4(ip)) => {
368 let mask = if self.prefix == 0 {
369 0
370 } else {
371 u32::MAX << (32 - u32::from(self.prefix))
372 };
373 u32::from(net) & mask == u32::from(*ip) & mask
374 }
375 (IpAddr::V6(net), IpAddr::V6(ip)) => {
376 let mask = if self.prefix == 0 {
377 0
378 } else {
379 u128::MAX << (128 - u32::from(self.prefix))
380 };
381 u128::from(net) & mask == u128::from(*ip) & mask
382 }
383 _ => false,
384 }
385 }
386}
387
388impl FromStr for IpNet {
389 type Err = ParseIpNetError;
390
391 fn from_str(s: &str) -> Result<Self, Self::Err> {
393 let (addr, prefix) = match s.split_once('/') {
394 Some((a, p)) => (a, Some(p)),
395 None => (s, None),
396 };
397 let addr: IpAddr = addr
398 .trim()
399 .parse()
400 .map_err(|_| ParseIpNetError(s.to_string()))?;
401 let prefix = match prefix {
402 Some(p) => p
403 .trim()
404 .parse::<u8>()
405 .map_err(|_| ParseIpNetError(s.to_string()))?,
406 None => {
407 if addr.is_ipv4() {
408 32
409 } else {
410 128
411 }
412 }
413 };
414 Self::new(addr, prefix)
415 }
416}
417
418impl fmt::Display for IpNet {
419 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420 write!(f, "{}/{}", self.addr, self.prefix)
421 }
422}
423
424#[derive(Clone, Debug, Eq, PartialEq)]
426pub struct AddressDenial {
427 pub host: String,
428 pub addr: Option<IpAddr>,
430 pub class: Option<AddressClass>,
431 pub reason: String,
432}
433
434impl fmt::Display for AddressDenial {
435 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
436 write!(
437 f,
438 "address denied for host '{}': {}",
439 self.host, self.reason
440 )
441 }
442}
443impl Error for AddressDenial {}
444
445type DenyHook = Arc<dyn Fn(&IpAddr) -> Option<String> + Send + Sync>;
446type AllowHook = Arc<dyn Fn(&IpAddr) -> bool + Send + Sync>;
447type ResolverHook = Arc<dyn Fn(&str) -> Vec<IpAddr> + Send + Sync>;
448
449#[derive(Clone)]
450enum Mode {
451 PublicWeb,
452 FirstParty(Vec<String>),
453}
454
455#[derive(Clone)]
456struct AllowRule {
457 hosts: Vec<String>,
458 any_host: bool,
460 pred: AllowHook,
461}
462
463#[derive(Clone)]
470pub struct AddressPolicy {
471 mode: Mode,
472 deny_ranges: Vec<IpNet>,
473 deny_if: Vec<DenyHook>,
474 allow: Vec<AllowRule>,
475 private_ok: Vec<String>,
477 allow_downgrade: bool,
479 allow_legacy_ipv4: bool,
481 refuse_mixed: bool,
483 resolver: Option<ResolverHook>,
484}
485
486#[derive(Debug)]
488pub enum ResolveError {
489 Private { addr: IpAddr },
492 Blocked {
494 addr: Option<IpAddr>,
495 class: Option<AddressClass>,
496 reason: String,
497 },
498 Dns(HttpError),
500}
501
502impl fmt::Display for ResolveError {
503 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504 match self {
505 ResolveError::Private { addr } => {
506 write!(f, "address {addr} is private and needs approval")
507 }
508 ResolveError::Blocked { reason, .. } => write!(f, "address refused: {reason}"),
509 ResolveError::Dns(e) => write!(f, "name lookup failed: {e}"),
510 }
511 }
512}
513
514impl Error for ResolveError {}
515
516impl Default for AddressPolicy {
517 fn default() -> Self {
518 Self::public_web()
519 }
520}
521
522impl fmt::Debug for AddressPolicy {
523 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
524 let mut d = f.debug_struct("AddressPolicy");
525 match &self.mode {
526 Mode::PublicWeb => d.field("mode", &"public-web"),
527 Mode::FirstParty(hosts) => d.field("mode", &format_args!("first-party {hosts:?}")),
528 };
529 d.field("deny_ranges", &self.deny_ranges)
530 .field("deny_if", &self.deny_if.len())
531 .field(
532 "allow_if_hosts",
533 &self
534 .allow
535 .iter()
536 .flat_map(|r| r.hosts.iter())
537 .collect::<Vec<_>>(),
538 )
539 .field("private_resolution_hosts", &self.private_ok)
540 .field("allow_insecure_redirects", &self.allow_downgrade)
541 .field("allow_legacy_ipv4", &self.allow_legacy_ipv4)
542 .field("refuse_mixed_loopback", &self.refuse_mixed)
543 .field("custom_resolver", &self.resolver.is_some())
544 .finish()
545 }
546}
547
548impl AddressPolicy {
549 pub fn public_web() -> Self {
551 Self {
552 mode: Mode::PublicWeb,
553 deny_ranges: Vec::new(),
554 deny_if: Vec::new(),
555 allow: Vec::new(),
556 private_ok: Vec::new(),
557 allow_downgrade: false,
558 allow_legacy_ipv4: false,
559 refuse_mixed: false,
560 resolver: None,
561 }
562 }
563
564 pub fn first_party(hosts: Vec<String>) -> Self {
571 Self {
572 mode: Mode::FirstParty(hosts.iter().map(|h| normalize_host(h)).collect()),
573 ..Self::public_web()
574 }
575 }
576
577 pub fn is_first_party(&self) -> bool {
579 matches!(self.mode, Mode::FirstParty(_))
580 }
581
582 pub fn allow_legacy_ipv4(mut self) -> Self {
589 self.allow_legacy_ipv4 = true;
590 self
591 }
592
593 pub fn allow_insecure_redirects(mut self) -> Self {
596 self.allow_downgrade = true;
597 self
598 }
599
600 pub fn allows_redirect(&self, from_scheme: &str, to_scheme: &str) -> bool {
604 self.allow_downgrade
605 || !from_scheme.eq_ignore_ascii_case("https")
606 || !to_scheme.eq_ignore_ascii_case("http")
607 }
608
609 pub(crate) fn check_redirect_scheme(
611 &self,
612 from_scheme: &str,
613 to_scheme: &str,
614 host: &str,
615 ) -> Result<(), AddressDenial> {
616 if self.allows_redirect(from_scheme, to_scheme) {
617 return Ok(());
618 }
619 Err(denial(
620 &normalize_host(host),
621 None,
622 None,
623 "redirect from HTTPS to HTTP refused",
624 ))
625 }
626
627 pub fn allow_private_resolution(mut self, host: impl Into<String>) -> Self {
632 self.private_ok.push(normalize_host(&host.into()));
633 self
634 }
635
636 pub fn deny_ranges(mut self, ranges: Vec<IpNet>) -> Self {
639 self.deny_ranges.extend(ranges);
640 self
641 }
642
643 pub fn deny_if<F>(mut self, f: F) -> Self
647 where
648 F: Fn(&IpAddr) -> Option<String> + Send + Sync + 'static,
649 {
650 self.deny_if.push(Arc::new(f));
651 self
652 }
653
654 pub fn allow_if<F>(mut self, hosts: Vec<String>, pred: F) -> Self
659 where
660 F: Fn(&IpAddr) -> bool + Send + Sync + 'static,
661 {
662 self.allow.push(AllowRule {
663 hosts: hosts.iter().map(|h| normalize_host(h)).collect(),
664 any_host: false,
665 pred: Arc::new(pred),
666 });
667 self
668 }
669
670 pub fn allow_if_any_host<F>(mut self, pred: F) -> Self
675 where
676 F: Fn(&IpAddr) -> bool + Send + Sync + 'static,
677 {
678 self.allow.push(AllowRule {
679 hosts: Vec::new(),
680 any_host: true,
681 pred: Arc::new(pred),
682 });
683 self
684 }
685
686 pub fn allow_loopback(self) -> Self {
689 self.allow_if_any_host(|ip| classify_ip(*ip) == AddressClass::Loopback)
690 }
691
692 pub fn local_first(self) -> Self {
696 self.allow_if_any_host(|ip| {
697 if classify_ip(*ip) == AddressClass::Loopback {
698 return true;
699 }
700 let v4 = match ip {
701 IpAddr::V4(v) => Some(*v),
702 IpAddr::V6(_) => embedded_v4(*ip),
703 };
704 v4.is_some_and(|v| v.is_private())
705 })
706 }
707
708 pub fn refuse_mixed_loopback(mut self) -> Self {
712 self.refuse_mixed = true;
713 self
714 }
715
716 pub fn with_resolver<F>(mut self, f: F) -> Self
719 where
720 F: Fn(&str) -> Vec<IpAddr> + Send + Sync + 'static,
721 {
722 self.resolver = Some(Arc::new(f));
723 self
724 }
725
726 pub fn resolve_vetted(&self, host: &str, port: u16) -> Result<Vec<SocketAddr>, HttpError> {
742 self.admit("https", host)?;
743 let host = normalize_host(host);
744 let addrs = match self.fixed_addrs(&host) {
745 Some(addrs) => addrs,
746 None => match self.lookup_blocking(&host, port, Some(Duration::from_secs(10))) {
747 Ok(addrs) => addrs,
748 Err(e) if e.kind() == io::ErrorKind::TimedOut => {
749 return Err(HttpError::timeout("name lookup", Phase::Connect))
750 }
751 Err(_) => return Err(HttpError::transport("host not found", Phase::Connect)),
752 },
753 };
754 let vetted = self.vet(&host, addrs)?;
755 Ok(vetted
756 .into_iter()
757 .map(|ip| SocketAddr::new(ip, port))
758 .collect())
759 }
760
761 fn host_listed(&self, host: &str) -> bool {
763 self.allow
764 .iter()
765 .any(|rule| rule.hosts.iter().any(|h| h == host))
766 }
767
768 pub(crate) fn vet_via_proxy(&self, host: &str) -> Result<(), AddressDenial> {
774 let host = normalize_host(host);
775 if let Some(addrs) = self.fixed_addrs(&host) {
776 return self.vet(&host, addrs).map(|_| ());
777 }
778 if matches!(self.mode, Mode::FirstParty(_)) || self.host_listed(&host) {
779 return Ok(());
780 }
781 let reason = if host.ends_with(".local") {
782 Some("mDNS name is refused through a proxy")
783 } else if host.ends_with(".internal") {
784 Some("internal name is refused through a proxy")
785 } else if !host.contains('.') {
786 Some("single-label name is refused through a proxy")
787 } else {
788 None
789 };
790 match reason {
791 Some(r) => Err(denial(
792 &host,
793 None,
794 Some(AddressClass::Private),
795 format!("{r} (through a proxy only names and literals can be checked)"),
796 )),
797 None => Ok(()),
798 }
799 }
800
801 pub(crate) fn admit(&self, scheme: &str, host: &str) -> Result<(), AddressDenial> {
807 if !self.allow_legacy_ipv4 {
808 if let Some(parsed) = parse_host_ip(host) {
809 if !parsed.is_canonical {
810 return Err(denial(
811 &normalize_host(host),
812 Some(parsed.addr),
813 Some(classify_ip(parsed.addr)),
814 format!(
815 "non-canonical IPv4 spelling is refused (it reads as {})",
816 parsed.addr
817 ),
818 ));
819 }
820 }
821 }
822 let host = normalize_host(host);
823 if is_metadata_host(&host) {
824 return Err(denial(
825 &host,
826 None,
827 Some(AddressClass::LinkLocal),
828 "host is a cloud metadata endpoint",
829 ));
830 }
831 if let Mode::FirstParty(hosts) = &self.mode {
832 if !scheme.eq_ignore_ascii_case("https") {
833 return Err(denial(
834 &host,
835 None,
836 None,
837 "first-party client allows HTTPS only",
838 ));
839 }
840 if !hosts.contains(&host) {
841 return Err(denial(
842 &host,
843 None,
844 None,
845 "host is not in the first-party host list",
846 ));
847 }
848 }
849 Ok(())
850 }
851
852 pub(crate) fn fixed_addrs(&self, host: &str) -> Option<Vec<IpAddr>> {
854 let host = normalize_host(host);
855 if let Some(parsed) = parse_host_ip(&host) {
856 return Some(vec![parsed.addr]);
857 }
858 if host == "localhost" || host.ends_with(".localhost") {
859 return Some(vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]);
860 }
861 None
862 }
863
864 pub(crate) fn lookup_blocking(
867 &self,
868 host: &str,
869 port: u16,
870 limit: Option<Duration>,
871 ) -> io::Result<Vec<IpAddr>> {
872 let host = normalize_host(host);
873 let hook = self.resolver.clone();
874 let run = move || -> io::Result<Vec<IpAddr>> {
875 match hook {
876 Some(hook) => Ok(hook(&host)),
877 None => Ok((host, port).to_socket_addrs()?.map(|a| a.ip()).collect()),
878 }
879 };
880 match limit {
881 None => run(),
882 Some(limit) => {
883 let (tx, rx) = mpsc::sync_channel(1);
884 std::thread::spawn(move || tx.send(run()).ok());
885 match rx.recv_timeout(limit) {
886 Ok(r) => r,
887 Err(_) => Err(io::Error::new(
888 io::ErrorKind::TimedOut,
889 "name lookup timed out",
890 )),
891 }
892 }
893 }
894 }
895
896 #[cfg(feature = "async")]
898 pub(crate) fn resolver_hook(&self) -> Option<ResolverHook> {
899 self.resolver.clone()
900 }
901
902 pub(crate) fn vet(&self, host: &str, addrs: Vec<IpAddr>) -> Result<Vec<IpAddr>, AddressDenial> {
905 self.vet_with(host, addrs)
906 }
907
908 fn vet_with(&self, host: &str, addrs: Vec<IpAddr>) -> Result<Vec<IpAddr>, AddressDenial> {
909 let host = normalize_host(host);
910 if addrs.is_empty() {
911 return Err(denial(
912 &host,
913 None,
914 None,
915 "host did not resolve to any address",
916 ));
917 }
918 if self.refuse_mixed {
919 let loopback = addrs
920 .iter()
921 .any(|ip| classify_ip(*ip) == AddressClass::Loopback);
922 let other = addrs
923 .iter()
924 .any(|ip| classify_ip(*ip) != AddressClass::Loopback);
925 if loopback && other {
926 return Err(denial(
927 &host,
928 None,
929 None,
930 "host resolves to both loopback and non-loopback addresses",
931 ));
932 }
933 }
934 let literal = parse_host_ip(&host).is_some();
935 for &ip in &addrs {
936 let class = classify_ip(ip);
937 if let Some(why) = hard_block(ip) {
938 return Err(denial(
939 &host,
940 Some(ip),
941 Some(class),
942 format!("{} {why}", subject(literal, ip)),
943 ));
944 }
945 let forms: Vec<IpAddr> = std::iter::once(ip)
946 .chain(embedded_v4s(ip).into_iter().map(IpAddr::V4))
947 .collect();
948 for form in forms.iter() {
949 if let Some(net) = self.deny_ranges.iter().find(|n| n.contains(form)) {
950 return Err(denial(
951 &host,
952 Some(ip),
953 Some(class),
954 format!("{} is in denied range {net}", subject(literal, ip)),
955 ));
956 }
957 for hook in &self.deny_if {
958 if let Some(text) = hook(form) {
959 return Err(denial(&host, Some(ip), Some(class), text));
960 }
961 }
962 }
963 let private_ok =
964 matches!(self.mode, Mode::FirstParty(_)) && self.private_ok.contains(&host);
965 if class != AddressClass::Public && !private_ok && !self.allowed(&host, &ip) {
966 let reason = if literal {
967 format!("address {ip} is {class:?}")
968 } else {
969 format!("host resolves to {class:?} address {ip}")
970 };
971 return Err(denial(&host, Some(ip), Some(class), reason));
972 }
973 }
974 Ok(addrs)
975 }
976
977 fn allowed(&self, host: &str, ip: &IpAddr) -> bool {
978 self.allow
979 .iter()
980 .any(|rule| (rule.any_host || rule.hosts.iter().any(|h| h == host)) && (rule.pred)(ip))
981 }
982
983 #[cfg(feature = "async")]
984 fn approvable(&self, ip: IpAddr) -> bool {
987 classify_ip(ip) == AddressClass::Private && !self.hard_denied(ip)
988 }
989
990 #[cfg(feature = "async")]
991 fn hard_denied(&self, ip: IpAddr) -> bool {
995 hard_block(ip).is_some()
996 || std::iter::once(ip)
997 .chain(embedded_v4s(ip).into_iter().map(IpAddr::V4))
998 .any(|form| {
999 self.deny_ranges.iter().any(|n| n.contains(&form))
1000 || self.deny_if.iter().any(|hook| hook(&form).is_some())
1001 })
1002 }
1003
1004 #[cfg(feature = "async")]
1005 fn to_resolve_error(
1009 &self,
1010 host: &str,
1011 denial: AddressDenial,
1012 addrs: &[IpAddr],
1013 ) -> ResolveError {
1014 let ok = |ip: &IpAddr| {
1015 !self.hard_denied(*ip)
1016 && (classify_ip(*ip) == AddressClass::Public
1017 || self.allowed(host, ip)
1018 || self.approvable(*ip))
1019 };
1020 match denial.addr {
1021 Some(addr)
1022 if classify_ip(addr) == AddressClass::Private
1023 && self.approvable(addr)
1024 && addrs.iter().all(ok) =>
1025 {
1026 ResolveError::Private { addr }
1027 }
1028 _ => ResolveError::Blocked {
1029 addr: denial.addr,
1030 class: denial.class,
1031 reason: denial.reason,
1032 },
1033 }
1034 }
1035
1036 #[cfg(feature = "async")]
1043 pub async fn resolve_vetted_async(
1044 &self,
1045 host: &str,
1046 port: u16,
1047 ) -> Result<Vec<SocketAddr>, ResolveError> {
1048 let host = normalize_host(host);
1049 self.admit("https", &host)
1050 .map_err(|d| self.to_resolve_error(&host, d, &[]))?;
1051 let addrs: Vec<IpAddr> = match self.fixed_addrs(&host) {
1052 Some(addrs) => addrs,
1053 None => match self.resolver_hook() {
1054 Some(hook) => hook(&host),
1055 None => match tokio::time::timeout(
1056 Duration::from_secs(10),
1057 tokio::net::lookup_host((host.as_str(), port)),
1058 )
1059 .await
1060 {
1061 Ok(Ok(found)) => found.map(|a| a.ip()).collect(),
1062 Ok(Err(_)) => {
1063 return Err(ResolveError::Dns(HttpError::transport(
1064 "host not found",
1065 Phase::Connect,
1066 )))
1067 }
1068 Err(_) => {
1069 return Err(ResolveError::Dns(HttpError::timeout(
1070 "name lookup",
1071 Phase::Connect,
1072 )))
1073 }
1074 },
1075 },
1076 };
1077 if addrs.is_empty() {
1078 return Err(ResolveError::Dns(HttpError::transport(
1079 "host not found",
1080 Phase::Connect,
1081 )));
1082 }
1083 match self.vet(&host, addrs.clone()) {
1084 Ok(vetted) => Ok(vetted
1085 .into_iter()
1086 .map(|ip| SocketAddr::new(ip, port))
1087 .collect()),
1088 Err(d) => Err(self.to_resolve_error(&host, d, &addrs)),
1089 }
1090 }
1091}
1092
1093fn subject(literal: bool, ip: IpAddr) -> String {
1094 if literal {
1095 format!("address {ip}")
1096 } else {
1097 format!("resolved address {ip}")
1098 }
1099}
1100
1101fn denial(
1102 host: &str,
1103 addr: Option<IpAddr>,
1104 class: Option<AddressClass>,
1105 reason: impl Into<String>,
1106) -> AddressDenial {
1107 AddressDenial {
1108 host: host.to_string(),
1109 addr,
1110 class,
1111 reason: reason.into(),
1112 }
1113}
1114
1115pub(crate) fn denial_in_chain(err: &(dyn Error + 'static)) -> Option<AddressDenial> {
1117 let mut current: Option<&(dyn Error + 'static)> = Some(err);
1118 while let Some(e) = current {
1119 if let Some(d) = e.downcast_ref::<AddressDenial>() {
1120 return Some(d.clone());
1121 }
1122 current = e.source();
1123 }
1124 None
1125}
1126
1127thread_local! {
1128 static VIA_PROXY: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1130}
1131
1132pub(crate) struct ViaProxy(bool);
1137
1138impl ViaProxy {
1139 pub(crate) fn enter(on: bool) -> Self {
1140 Self(VIA_PROXY.with(|c| c.replace(on)))
1141 }
1142}
1143
1144impl Drop for ViaProxy {
1145 fn drop(&mut self) {
1146 VIA_PROXY.with(|c| c.set(self.0));
1147 }
1148}
1149
1150const MAX_RESOLVED_ADDRS: usize = 16;
1152
1153#[derive(Debug)]
1158pub(crate) struct VettingResolver {
1159 pub(crate) policy: Arc<AddressPolicy>,
1160 pub(crate) proxies: Vec<(String, u16)>,
1165 pub(crate) lookup_limit: Duration,
1167}
1168
1169impl ureq::unversioned::resolver::Resolver for VettingResolver {
1170 fn resolve(
1171 &self,
1172 uri: &ureq::http::Uri,
1173 _config: &ureq::config::Config,
1174 timeout: ureq::unversioned::transport::NextTimeout,
1175 ) -> Result<ureq::unversioned::resolver::ResolvedSocketAddrs, ureq::Error> {
1176 let scheme = uri.scheme_str().unwrap_or("").to_ascii_lowercase();
1177 let default_port = match scheme.as_str() {
1178 "http" => 80,
1179 "https" => 443,
1180 _ => return Err(ureq::Error::BadUri("unsupported scheme".into())),
1181 };
1182 let raw_host = uri
1183 .host()
1184 .ok_or_else(|| ureq::Error::BadUri("missing host".into()))?;
1185 let host = normalize_host(raw_host);
1186 let port = uri.port_u16().unwrap_or(default_port);
1187 let is_proxy = VIA_PROXY.with(|c| c.get())
1188 && self.proxies.iter().any(|(h, p)| *h == host && *p == port);
1189 if !is_proxy && !matches!(scheme.as_str(), "http" | "https") {
1190 return Err(ureq::Error::BadUri("unsupported scheme".into()));
1191 }
1192 if !is_proxy {
1193 self.policy.admit(&scheme, raw_host).map_err(to_ureq)?;
1194 }
1195 let addrs = match self.policy.fixed_addrs(&host) {
1196 Some(addrs) => addrs,
1197 None => {
1198 let limit = if timeout.after.is_not_happening() {
1199 self.lookup_limit
1200 } else {
1201 (*timeout.after).min(self.lookup_limit)
1202 };
1203 match self.policy.lookup_blocking(&host, port, Some(limit)) {
1204 Ok(addrs) => addrs,
1205 Err(e) if e.kind() == io::ErrorKind::TimedOut => {
1206 return Err(ureq::Error::Timeout(timeout.reason))
1207 }
1208 Err(_) => return Err(ureq::Error::HostNotFound),
1209 }
1210 }
1211 };
1212 let vetted = if is_proxy {
1213 if addrs.is_empty() {
1214 return Err(ureq::Error::HostNotFound);
1215 }
1216 addrs
1217 } else {
1218 self.policy.vet(&host, addrs).map_err(to_ureq)?
1219 };
1220 let mut out = ureq::unversioned::resolver::Resolver::empty(self);
1221 for ip in vetted.into_iter().take(MAX_RESOLVED_ADDRS) {
1222 out.push(std::net::SocketAddr::new(ip, port));
1223 }
1224 Ok(out)
1225 }
1226}
1227
1228fn to_ureq(d: AddressDenial) -> ureq::Error {
1229 ureq::Error::Io(io::Error::new(io::ErrorKind::PermissionDenied, d))
1230}
1231
1232#[cfg(test)]
1233mod tests {
1234 use super::*;
1235
1236 fn ip(s: &str) -> IpAddr {
1237 s.parse().unwrap()
1238 }
1239
1240 #[test]
1241 fn ipnet_parses_and_matches() {
1242 let net: IpNet = "10.0.0.0/8".parse().unwrap();
1243 assert!(net.contains(&ip("10.255.0.1")));
1244 assert!(!net.contains(&ip("11.0.0.1")));
1245 assert!(!net.contains(&ip("::1")));
1246 let v6: IpNet = "fd00::/8".parse().unwrap();
1247 assert!(v6.contains(&ip("fd12::1")));
1248 assert!(!v6.contains(&ip("fe80::1")));
1249 let host: IpNet = "203.0.113.9".parse().unwrap();
1250 assert_eq!(host.prefix(), 32);
1251 assert!(host.contains(&ip("203.0.113.9")) && !host.contains(&ip("203.0.113.8")));
1252 assert!("10.0.0.0/33".parse::<IpNet>().is_err());
1253 assert!("nonsense".parse::<IpNet>().is_err());
1254 let all: IpNet = "0.0.0.0/0".parse().unwrap();
1255 assert!(all.contains(&ip("8.8.8.8")));
1256 }
1257
1258 #[test]
1259 fn embedded_forms_extract_the_v4() {
1260 assert_eq!(
1261 embedded_v4(ip("::ffff:10.1.2.3")),
1262 Some("10.1.2.3".parse().unwrap())
1263 );
1264 assert_eq!(
1265 embedded_v4(ip("::10.1.2.3")),
1266 Some("10.1.2.3".parse().unwrap())
1267 );
1268 assert_eq!(
1269 embedded_v4(ip("64:ff9b::a00:1")),
1270 Some("10.0.0.1".parse().unwrap())
1271 );
1272 assert_eq!(
1273 embedded_v4(ip("2002:0a00:0001::1")),
1274 Some("10.0.0.1".parse().unwrap())
1275 );
1276 assert_eq!(embedded_v4(ip("::1")), None);
1277 assert_eq!(embedded_v4(ip("::")), None);
1278 assert_eq!(embedded_v4(ip("2606:4700::1")), None);
1279 assert_eq!(embedded_v4(ip("1.2.3.4")), None);
1280 }
1281
1282 #[test]
1283 fn host_ip_parsing_reports_the_spelling() {
1284 let k = |h: &str| parse_host_ip(h).map(|p| (p.addr, p.kind, p.is_canonical));
1285 let v4 = |s: &str| IpAddr::V4(s.parse().unwrap());
1286 assert_eq!(
1287 k("1.1.1.1"),
1288 Some((v4("1.1.1.1"), HostIpKind::Ipv4DottedQuad, true))
1289 );
1290 for (spelling, want) in [
1291 ("127.1", "127.0.0.1"),
1292 ("10.1.2", "10.1.0.2"),
1293 ("0177.0.0.1", "127.0.0.1"),
1294 ("0x7f.0.0.1", "127.0.0.1"),
1295 ("0X7F000001", "127.0.0.1"),
1296 ("2130706433", "127.0.0.1"),
1297 ("017700000001", "127.0.0.1"),
1298 ("0251.0376.0251.0376", "169.254.169.254"),
1299 ("1.1.1.1.", "1.1.1.1"),
1300 ("0", "0.0.0.0"),
1301 ] {
1302 assert_eq!(
1303 k(spelling),
1304 Some((v4(want), HostIpKind::Ipv4Legacy, false)),
1305 "{spelling}"
1306 );
1307 }
1308 for v6 in [
1309 "::1",
1310 "[::1]",
1311 "::ffff:127.0.0.1",
1312 "[::ffff:7f00:1]",
1313 "64:ff9b::a00:1",
1314 ] {
1315 let p = parse_host_ip(v6).unwrap();
1316 assert!(p.is_canonical && p.kind == HostIpKind::Ipv6, "{v6}");
1317 }
1318 for not_ip in [
1319 "example.com",
1320 "1.2.3.4.5",
1321 "256.1.1.1",
1322 "1.2.3.256",
1323 "4294967296",
1324 "08.1",
1325 "0x",
1326 "1..2",
1327 "",
1328 ".",
1329 "1.2.3.",
1330 "a.b.c.d",
1331 "127.0.0.1x",
1332 ] {
1333 let got = parse_host_ip(not_ip);
1334 if not_ip == "1.2.3." {
1336 assert!(got.is_some());
1337 } else {
1338 assert!(got.is_none(), "{not_ip}: {got:?}");
1339 }
1340 }
1341 assert_eq!(
1342 parse_legacy_ipv4("0x7f.1"),
1343 Some("127.0.0.1".parse().unwrap())
1344 );
1345 assert_eq!(parse_legacy_ipv4("1.1.1.1."), None);
1346 assert_eq!(parse_legacy_ipv4("::1"), None);
1347 }
1348
1349 #[test]
1350 fn non_canonical_spellings_are_refused_by_default_and_allowed_on_request() {
1351 let strict = AddressPolicy::default();
1352 for h in ["0177.0.0.1", "0x7f.1", "2130706433", "127.1", "1.1.1.1."] {
1353 let d = strict.admit("http", h).unwrap_err();
1354 assert!(d.reason.contains("non-canonical"), "{h}: {d}");
1355 assert!(d.addr.is_some(), "{h}");
1356 }
1357 for h in [
1358 "1.1.1.1",
1359 "::ffff:1.1.1.1",
1360 "[::1]",
1361 "example.com",
1362 "example.com.",
1363 ] {
1364 assert!(strict.admit("http", h).is_ok(), "{h}");
1365 }
1366 let lax = AddressPolicy::default().allow_legacy_ipv4();
1367 assert!(lax.admit("http", "127.1").is_ok());
1368 assert!(lax.vet("127.1", lax.fixed_addrs("127.1").unwrap()).is_err());
1370 }
1371
1372 #[test]
1373 fn host_normalization() {
1374 assert_eq!(normalize_host("[::1]"), "::1");
1375 assert_eq!(normalize_host("Example.COM."), "example.com");
1376 }
1377
1378 #[test]
1379 fn vet_denies_empty_resolution() {
1380 let d = AddressPolicy::default()
1381 .vet("nowhere.test", vec![])
1382 .unwrap_err();
1383 assert_eq!(d.addr, None);
1384 assert!(d.reason.contains("did not resolve"), "{d}");
1385 }
1386}
1387
1388#[cfg(test)]
1389mod loopback_tests {
1390 use super::*;
1391
1392 fn answers(addrs: Vec<IpAddr>) -> AddressPolicy {
1393 AddressPolicy::public_web().with_resolver(move |_| addrs.clone())
1394 }
1395
1396 #[test]
1397 fn loopback_is_admitted_only_when_allowed() {
1398 let lo: IpAddr = "127.0.0.1".parse().unwrap();
1399 assert!(answers(vec![lo]).resolve_vetted("svc.test", 80).is_err());
1400 assert!(answers(vec![lo])
1401 .allow_loopback()
1402 .resolve_vetted("svc.test", 80)
1403 .is_ok());
1404 }
1405
1406 #[test]
1407 fn local_first_admits_rfc1918_but_not_cgnat_link_local_or_metadata() {
1408 let rfc1918: IpAddr = "192.168.1.5".parse().unwrap();
1409 assert!(answers(vec![rfc1918])
1410 .local_first()
1411 .resolve_vetted("svc.test", 80)
1412 .is_ok());
1413 let meta: IpAddr = "169.254.169.254".parse().unwrap();
1414 assert!(answers(vec![meta])
1415 .local_first()
1416 .resolve_vetted("svc.test", 80)
1417 .is_err());
1418 let cgnat: IpAddr = "100.64.0.1".parse().unwrap();
1419 assert!(answers(vec![cgnat])
1420 .local_first()
1421 .resolve_vetted("svc.test", 80)
1422 .is_err());
1423 }
1424
1425 #[test]
1426 fn mixed_loopback_answers_are_refused_when_asked() {
1427 let mixed: Vec<IpAddr> = vec![
1428 "127.0.0.1".parse().unwrap(),
1429 "93.184.216.34".parse().unwrap(),
1430 ];
1431 assert!(answers(mixed.clone())
1432 .allow_loopback()
1433 .resolve_vetted("svc.test", 80)
1434 .is_ok());
1435 assert!(answers(mixed)
1436 .allow_loopback()
1437 .refuse_mixed_loopback()
1438 .resolve_vetted("svc.test", 80)
1439 .is_err());
1440 }
1441}
1442
1443#[derive(Clone, Debug, PartialEq, Eq)]
1457pub struct CredentialScope {
1458 scheme: String,
1459 host: String,
1460 port: u16,
1461 segments: Vec<String>,
1462}
1463
1464impl CredentialScope {
1465 pub fn parse(scope: &str) -> Option<Self> {
1469 if scope.contains(['?', '#']) {
1470 return None;
1471 }
1472 let (scheme, host, port, segments, userinfo) = credential_url_parts(scope)?;
1473 if userinfo {
1474 return None;
1475 }
1476 Some(Self {
1477 scheme,
1478 host,
1479 port,
1480 segments,
1481 })
1482 }
1483
1484 pub fn allows(&self, url: &str) -> bool {
1487 let Some((scheme, host, port, segments, _)) = credential_url_parts(url) else {
1488 return false;
1489 };
1490 scheme == self.scheme
1491 && host == self.host
1492 && port == self.port
1493 && segments.len() >= self.segments.len()
1494 && segments[..self.segments.len()] == self.segments[..]
1495 }
1496}
1497
1498fn credential_url_parts(url: &str) -> Option<(String, String, u16, Vec<String>, bool)> {
1503 if url
1504 .chars()
1505 .any(|c| c.is_control() || c.is_whitespace() || c == '\\')
1506 {
1507 return None;
1508 }
1509 let (scheme, rest) = url.split_once("://")?;
1510 if !scheme.eq_ignore_ascii_case("http") && !scheme.eq_ignore_ascii_case("https") {
1511 return None;
1512 }
1513 let scheme = scheme.to_ascii_lowercase();
1514 let end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
1515 let (authority, tail) = rest.split_at(end);
1516 let (host_port, userinfo) = match authority.rsplit_once('@') {
1517 Some((_, host_port)) => (host_port, true),
1518 None => (authority, false),
1519 };
1520 let (raw_host, port) = if let Some(inner) = host_port.strip_prefix('[') {
1521 let (literal, after) = inner.split_once(']')?;
1522 let port = match after {
1523 "" => None,
1524 _ => Some(after.strip_prefix(':')?.parse().ok()?),
1525 };
1526 (literal, port)
1527 } else if let Some((host, port)) = host_port.rsplit_once(':') {
1528 if port.is_empty() {
1529 (host, None)
1530 } else {
1531 (host, Some(port.parse().ok()?))
1532 }
1533 } else {
1534 (host_port, None)
1535 };
1536 if raw_host.is_empty() {
1537 return None;
1538 }
1539 let port = port.unwrap_or(if scheme == "https" { 443 } else { 80 });
1540 let path = tail.split(['?', '#']).next().unwrap_or("");
1541 let segments = credential_path_segments(path)?;
1542 Some((scheme, normalize_host(raw_host), port, segments, userinfo))
1543}
1544
1545fn credential_path_segments(path: &str) -> Option<Vec<String>> {
1548 let mut out = Vec::new();
1549 for raw in path.split('/') {
1550 if raw.is_empty() {
1551 continue;
1552 }
1553 let bytes = raw.as_bytes();
1554 let mut decoded = Vec::with_capacity(bytes.len());
1555 let mut i = 0;
1556 while i < bytes.len() {
1557 if bytes[i] == b'%' {
1558 let digits = bytes.get(i + 1..i + 3)?;
1559 if !digits.iter().all(u8::is_ascii_hexdigit) {
1560 return None;
1561 }
1562 let text = std::str::from_utf8(digits).ok()?;
1563 decoded.push(u8::from_str_radix(text, 16).ok()?);
1564 i += 3;
1565 } else {
1566 decoded.push(bytes[i]);
1567 i += 1;
1568 }
1569 }
1570 let segment = String::from_utf8(decoded).ok()?;
1571 if segment == "." || segment == ".." || segment.contains(['/', '\\']) {
1572 return None;
1573 }
1574 if second_layer_is_unsafe(&segment) {
1577 return None;
1578 }
1579 out.push(segment);
1580 }
1581 Some(out)
1582}
1583
1584fn second_layer_is_unsafe(segment: &str) -> bool {
1587 let bytes = segment.as_bytes();
1588 let mut decoded = Vec::with_capacity(bytes.len());
1589 let mut i = 0;
1590 let mut any_escape = false;
1591 while i < bytes.len() {
1592 let escape = if bytes[i] == b'%' {
1593 bytes
1594 .get(i + 1..i + 3)
1595 .filter(|d| d.len() == 2 && d.iter().all(u8::is_ascii_hexdigit))
1596 .and_then(|d| std::str::from_utf8(d).ok())
1597 .and_then(|t| u8::from_str_radix(t, 16).ok())
1598 } else {
1599 None
1600 };
1601 match escape {
1602 Some(byte) => {
1603 any_escape = true;
1604 decoded.push(byte);
1605 i += 3;
1606 }
1607 None => {
1608 decoded.push(bytes[i]);
1609 i += 1;
1610 }
1611 }
1612 }
1613 if !any_escape {
1614 return false;
1615 }
1616 match String::from_utf8(decoded) {
1617 Ok(text) => text == "." || text == ".." || text.contains(['/', '\\']),
1618 Err(_) => true,
1619 }
1620}
1621
1622#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
1624#[non_exhaustive]
1625pub enum UrlPolicyError {
1626 NotHttps,
1628 Credentials,
1630 NoHost,
1632 NonPublicHost,
1634 Malformed,
1637}
1638
1639impl fmt::Display for UrlPolicyError {
1640 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1641 f.write_str(match self {
1642 Self::NotHttps => "URL must use https",
1643 Self::Credentials => "URL must not carry credentials",
1644 Self::NoHost => "URL has no host",
1645 Self::NonPublicHost => "URL host is not a public address",
1646 Self::Malformed => "URL is malformed",
1647 })
1648 }
1649}
1650
1651impl Error for UrlPolicyError {}
1652
1653pub fn validate_public_https_url(url: &str) -> Result<(), UrlPolicyError> {
1659 let (scheme, rest) = url.split_once("://").ok_or(UrlPolicyError::NotHttps)?;
1660 if !scheme.eq_ignore_ascii_case("https") {
1661 return Err(UrlPolicyError::NotHttps);
1662 }
1663 let end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
1664 let authority = &rest[..end];
1665 if authority.contains('@') {
1666 return Err(UrlPolicyError::Credentials);
1667 }
1668 let (bracketed, raw_host, port) = if let Some(inner) = authority.strip_prefix('[') {
1671 let (literal, after) = inner.split_once(']').ok_or(UrlPolicyError::Malformed)?;
1672 let port = match after {
1673 "" => None,
1674 _ => Some(after.strip_prefix(':').ok_or(UrlPolicyError::Malformed)?),
1675 };
1676 (true, literal, port)
1677 } else {
1678 match authority.rsplit_once(':') {
1679 Some((host, port)) => (false, host, Some(port)),
1680 None => (false, authority, None),
1681 }
1682 };
1683 if let Some(port) = port {
1684 validate_url_port(port)?;
1685 }
1686 if bracketed {
1687 let addr = raw_host
1688 .parse::<Ipv6Addr>()
1689 .map_err(|_| UrlPolicyError::Malformed)?;
1690 return if classify_ip(IpAddr::V6(addr)) == AddressClass::Public {
1691 Ok(())
1692 } else {
1693 Err(UrlPolicyError::NonPublicHost)
1694 };
1695 }
1696 let host = normalize_url_host(raw_host)?;
1700 if let Some(parsed) = parse_host_ip(&host) {
1701 if parsed.kind == HostIpKind::Ipv4Legacy {
1702 return Err(UrlPolicyError::Malformed);
1703 }
1704 return if classify_ip(parsed.addr) == AddressClass::Public {
1705 Ok(())
1706 } else {
1707 Err(UrlPolicyError::NonPublicHost)
1708 };
1709 }
1710 let local_name = host == "localhost"
1711 || host.ends_with(".localhost")
1712 || host.ends_with(".local")
1713 || host.ends_with(".internal");
1714 if local_name || !host.contains('.') {
1715 return Err(UrlPolicyError::NonPublicHost);
1716 }
1717 Ok(())
1718}
1719
1720fn validate_url_port(port: &str) -> Result<(), UrlPolicyError> {
1722 if port.is_empty() {
1723 return Ok(());
1724 }
1725 if !port.bytes().all(|b| b.is_ascii_digit()) {
1726 return Err(UrlPolicyError::Malformed);
1727 }
1728 match port.parse::<u16>() {
1729 Ok(0) | Err(_) => Err(UrlPolicyError::Malformed),
1730 Ok(_) => Ok(()),
1731 }
1732}
1733
1734fn normalize_url_host(raw: &str) -> Result<String, UrlPolicyError> {
1737 let bytes = raw.as_bytes();
1738 let mut decoded = Vec::with_capacity(bytes.len());
1739 let mut i = 0;
1740 while i < bytes.len() {
1741 if bytes[i] == b'%' {
1742 let digits = bytes.get(i + 1..i + 3).ok_or(UrlPolicyError::Malformed)?;
1743 if !digits.iter().all(u8::is_ascii_hexdigit) {
1744 return Err(UrlPolicyError::Malformed);
1745 }
1746 let text = std::str::from_utf8(digits).map_err(|_| UrlPolicyError::Malformed)?;
1747 decoded.push(u8::from_str_radix(text, 16).map_err(|_| UrlPolicyError::Malformed)?);
1748 i += 3;
1749 } else {
1750 decoded.push(bytes[i]);
1751 i += 1;
1752 }
1753 }
1754 let text = String::from_utf8(decoded).map_err(|_| UrlPolicyError::Malformed)?;
1755 if text.chars().any(|c| {
1756 c.is_control()
1757 || c.is_whitespace()
1758 || matches!(c, '/' | '\\' | '?' | '#' | '@' | '%' | '[' | ']' | ':')
1759 }) {
1760 return Err(UrlPolicyError::Malformed);
1761 }
1762 let host = text.to_ascii_lowercase();
1763 let host = host.trim_end_matches('.');
1764 if host.is_empty() {
1765 return Err(UrlPolicyError::NoHost);
1766 }
1767 Ok(host.to_string())
1768}
1769
1770#[cfg(test)]
1771mod public_url_tests {
1772 use super::*;
1773
1774 #[test]
1775 fn plain_public_https_urls_pass() {
1776 assert_eq!(
1777 validate_public_https_url("https://example.com/path?q=1#frag"),
1778 Ok(())
1779 );
1780 assert_eq!(
1781 validate_public_https_url("HTTPS://Example.COM:8443"),
1782 Ok(())
1783 );
1784 assert_eq!(validate_public_https_url("https://93.184.216.34/x"), Ok(()));
1785 assert_eq!(
1786 validate_public_https_url("https://[2606:4700::1111]/"),
1787 Ok(())
1788 );
1789 }
1790
1791 #[test]
1792 fn non_https_and_credentials_are_refused() {
1793 assert_eq!(
1794 validate_public_https_url("http://example.com"),
1795 Err(UrlPolicyError::NotHttps)
1796 );
1797 assert_eq!(
1798 validate_public_https_url("example.com"),
1799 Err(UrlPolicyError::NotHttps)
1800 );
1801 assert_eq!(
1802 validate_public_https_url("https://user:pw@example.com"),
1803 Err(UrlPolicyError::Credentials)
1804 );
1805 }
1806
1807 #[test]
1808 fn local_and_private_hosts_are_refused() {
1809 for url in [
1810 "https://localhost/",
1811 "https://api.localhost/",
1812 "https://printer.local/",
1813 "https://db.internal/",
1814 "https://intranet/",
1815 "https://127.0.0.1/",
1816 "https://10.0.0.5/",
1817 "https://169.254.169.254/latest",
1818 "https://[::1]/",
1819 "https://[fd00::1]/",
1820 ] {
1821 assert_eq!(
1822 validate_public_https_url(url),
1823 Err(UrlPolicyError::NonPublicHost),
1824 "{url}"
1825 );
1826 }
1827 }
1828
1829 #[test]
1830 fn legacy_ipv4_spellings_and_empty_hosts_are_refused() {
1831 assert_eq!(
1832 validate_public_https_url("https://0x7f.1/"),
1833 Err(UrlPolicyError::Malformed)
1834 );
1835 assert_eq!(
1836 validate_public_https_url("https:///path"),
1837 Err(UrlPolicyError::NoHost)
1838 );
1839 assert_eq!(
1840 validate_public_https_url("https://[::1/"),
1841 Err(UrlPolicyError::Malformed)
1842 );
1843 }
1844
1845 #[test]
1846 fn trailing_dot_local_names_are_refused() {
1847 for url in [
1848 "https://localhost./",
1849 "https://LOCALHOST../x",
1850 "https://printer.local./",
1851 "https://db.internal./",
1852 "https://api.localhost./",
1853 "https://intranet./",
1854 "https://127.0.0.1./",
1855 ] {
1856 assert_eq!(
1857 validate_public_https_url(url),
1858 Err(UrlPolicyError::NonPublicHost),
1859 "{url}"
1860 );
1861 }
1862 }
1863
1864 #[test]
1865 fn percent_encoded_hosts_are_decoded_before_classification() {
1866 assert_eq!(
1867 validate_public_https_url("https://%31%32%37.0.0.1/"),
1868 Err(UrlPolicyError::NonPublicHost)
1869 );
1870 assert_eq!(
1871 validate_public_https_url("https://%31%32%37.0.0.1./"),
1872 Err(UrlPolicyError::NonPublicHost)
1873 );
1874 assert_eq!(
1875 validate_public_https_url("https://%6c%6f%63%61%6c%68%6f%73%74/"),
1876 Err(UrlPolicyError::NonPublicHost)
1877 );
1878 assert_eq!(
1879 validate_public_https_url("https://%2fexample.com/"),
1880 Err(UrlPolicyError::Malformed)
1881 );
1882 assert_eq!(
1883 validate_public_https_url("https://exa%3Fmple.com/"),
1884 Err(UrlPolicyError::Malformed)
1885 );
1886 assert_eq!(
1887 validate_public_https_url("https://example%2.com/"),
1888 Err(UrlPolicyError::Malformed)
1889 );
1890 assert_eq!(
1891 validate_public_https_url("https://%+1.example.com/"),
1892 Err(UrlPolicyError::Malformed)
1893 );
1894 assert_eq!(
1895 validate_public_https_url("https://%25.example.com/"),
1896 Err(UrlPolicyError::Malformed)
1897 );
1898 }
1899
1900 #[test]
1901 fn trailing_dot_public_names_and_valid_ports_pass() {
1902 assert_eq!(validate_public_https_url("https://example.com./x"), Ok(()));
1903 assert_eq!(
1904 validate_public_https_url("https://example.com:443/"),
1905 Ok(())
1906 );
1907 assert_eq!(validate_public_https_url("https://example.com:/"), Ok(()));
1908 assert_eq!(
1909 validate_public_https_url("https://[2606:4700::1111]:8443/"),
1910 Ok(())
1911 );
1912 }
1913
1914 #[test]
1915 fn invalid_ports_are_refused() {
1916 for url in [
1917 "https://example.com:0/",
1918 "https://example.com:65536/",
1919 "https://example.com:abc/",
1920 "https://example.com:+443/",
1921 "https://example.com:80:90/",
1922 "https://[2606:4700::1111]:0/",
1923 "https://[2606:4700::1111]x/",
1924 ] {
1925 assert_eq!(
1926 validate_public_https_url(url),
1927 Err(UrlPolicyError::Malformed),
1928 "{url}"
1929 );
1930 }
1931 }
1932}