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, Copy, Debug, Eq, PartialEq, Hash)]
1445#[non_exhaustive]
1446pub enum UrlPolicyError {
1447 NotHttps,
1449 Credentials,
1451 NoHost,
1453 NonPublicHost,
1455 Malformed,
1458}
1459
1460impl fmt::Display for UrlPolicyError {
1461 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1462 f.write_str(match self {
1463 Self::NotHttps => "URL must use https",
1464 Self::Credentials => "URL must not carry credentials",
1465 Self::NoHost => "URL has no host",
1466 Self::NonPublicHost => "URL host is not a public address",
1467 Self::Malformed => "URL is malformed",
1468 })
1469 }
1470}
1471
1472impl Error for UrlPolicyError {}
1473
1474pub fn validate_public_https_url(url: &str) -> Result<(), UrlPolicyError> {
1480 let (scheme, rest) = url.split_once("://").ok_or(UrlPolicyError::NotHttps)?;
1481 if !scheme.eq_ignore_ascii_case("https") {
1482 return Err(UrlPolicyError::NotHttps);
1483 }
1484 let end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
1485 let authority = &rest[..end];
1486 if authority.contains('@') {
1487 return Err(UrlPolicyError::Credentials);
1488 }
1489 let (bracketed, raw_host, port) = if let Some(inner) = authority.strip_prefix('[') {
1492 let (literal, after) = inner.split_once(']').ok_or(UrlPolicyError::Malformed)?;
1493 let port = match after {
1494 "" => None,
1495 _ => Some(after.strip_prefix(':').ok_or(UrlPolicyError::Malformed)?),
1496 };
1497 (true, literal, port)
1498 } else {
1499 match authority.rsplit_once(':') {
1500 Some((host, port)) => (false, host, Some(port)),
1501 None => (false, authority, None),
1502 }
1503 };
1504 if let Some(port) = port {
1505 validate_url_port(port)?;
1506 }
1507 if bracketed {
1508 let addr = raw_host
1509 .parse::<Ipv6Addr>()
1510 .map_err(|_| UrlPolicyError::Malformed)?;
1511 return if classify_ip(IpAddr::V6(addr)) == AddressClass::Public {
1512 Ok(())
1513 } else {
1514 Err(UrlPolicyError::NonPublicHost)
1515 };
1516 }
1517 let host = normalize_url_host(raw_host)?;
1521 if let Some(parsed) = parse_host_ip(&host) {
1522 if parsed.kind == HostIpKind::Ipv4Legacy {
1523 return Err(UrlPolicyError::Malformed);
1524 }
1525 return if classify_ip(parsed.addr) == AddressClass::Public {
1526 Ok(())
1527 } else {
1528 Err(UrlPolicyError::NonPublicHost)
1529 };
1530 }
1531 let local_name = host == "localhost"
1532 || host.ends_with(".localhost")
1533 || host.ends_with(".local")
1534 || host.ends_with(".internal");
1535 if local_name || !host.contains('.') {
1536 return Err(UrlPolicyError::NonPublicHost);
1537 }
1538 Ok(())
1539}
1540
1541fn validate_url_port(port: &str) -> Result<(), UrlPolicyError> {
1543 if port.is_empty() {
1544 return Ok(());
1545 }
1546 if !port.bytes().all(|b| b.is_ascii_digit()) {
1547 return Err(UrlPolicyError::Malformed);
1548 }
1549 match port.parse::<u16>() {
1550 Ok(0) | Err(_) => Err(UrlPolicyError::Malformed),
1551 Ok(_) => Ok(()),
1552 }
1553}
1554
1555fn normalize_url_host(raw: &str) -> Result<String, UrlPolicyError> {
1558 let bytes = raw.as_bytes();
1559 let mut decoded = Vec::with_capacity(bytes.len());
1560 let mut i = 0;
1561 while i < bytes.len() {
1562 if bytes[i] == b'%' {
1563 let digits = bytes.get(i + 1..i + 3).ok_or(UrlPolicyError::Malformed)?;
1564 if !digits.iter().all(u8::is_ascii_hexdigit) {
1565 return Err(UrlPolicyError::Malformed);
1566 }
1567 let text = std::str::from_utf8(digits).map_err(|_| UrlPolicyError::Malformed)?;
1568 decoded.push(u8::from_str_radix(text, 16).map_err(|_| UrlPolicyError::Malformed)?);
1569 i += 3;
1570 } else {
1571 decoded.push(bytes[i]);
1572 i += 1;
1573 }
1574 }
1575 let text = String::from_utf8(decoded).map_err(|_| UrlPolicyError::Malformed)?;
1576 if text.chars().any(|c| {
1577 c.is_control()
1578 || c.is_whitespace()
1579 || matches!(c, '/' | '\\' | '?' | '#' | '@' | '%' | '[' | ']' | ':')
1580 }) {
1581 return Err(UrlPolicyError::Malformed);
1582 }
1583 let host = text.to_ascii_lowercase();
1584 let host = host.trim_end_matches('.');
1585 if host.is_empty() {
1586 return Err(UrlPolicyError::NoHost);
1587 }
1588 Ok(host.to_string())
1589}
1590
1591#[cfg(test)]
1592mod public_url_tests {
1593 use super::*;
1594
1595 #[test]
1596 fn plain_public_https_urls_pass() {
1597 assert_eq!(
1598 validate_public_https_url("https://example.com/path?q=1#frag"),
1599 Ok(())
1600 );
1601 assert_eq!(
1602 validate_public_https_url("HTTPS://Example.COM:8443"),
1603 Ok(())
1604 );
1605 assert_eq!(validate_public_https_url("https://93.184.216.34/x"), Ok(()));
1606 assert_eq!(
1607 validate_public_https_url("https://[2606:4700::1111]/"),
1608 Ok(())
1609 );
1610 }
1611
1612 #[test]
1613 fn non_https_and_credentials_are_refused() {
1614 assert_eq!(
1615 validate_public_https_url("http://example.com"),
1616 Err(UrlPolicyError::NotHttps)
1617 );
1618 assert_eq!(
1619 validate_public_https_url("example.com"),
1620 Err(UrlPolicyError::NotHttps)
1621 );
1622 assert_eq!(
1623 validate_public_https_url("https://user:pw@example.com"),
1624 Err(UrlPolicyError::Credentials)
1625 );
1626 }
1627
1628 #[test]
1629 fn local_and_private_hosts_are_refused() {
1630 for url in [
1631 "https://localhost/",
1632 "https://api.localhost/",
1633 "https://printer.local/",
1634 "https://db.internal/",
1635 "https://intranet/",
1636 "https://127.0.0.1/",
1637 "https://10.0.0.5/",
1638 "https://169.254.169.254/latest",
1639 "https://[::1]/",
1640 "https://[fd00::1]/",
1641 ] {
1642 assert_eq!(
1643 validate_public_https_url(url),
1644 Err(UrlPolicyError::NonPublicHost),
1645 "{url}"
1646 );
1647 }
1648 }
1649
1650 #[test]
1651 fn legacy_ipv4_spellings_and_empty_hosts_are_refused() {
1652 assert_eq!(
1653 validate_public_https_url("https://0x7f.1/"),
1654 Err(UrlPolicyError::Malformed)
1655 );
1656 assert_eq!(
1657 validate_public_https_url("https:///path"),
1658 Err(UrlPolicyError::NoHost)
1659 );
1660 assert_eq!(
1661 validate_public_https_url("https://[::1/"),
1662 Err(UrlPolicyError::Malformed)
1663 );
1664 }
1665
1666 #[test]
1667 fn trailing_dot_local_names_are_refused() {
1668 for url in [
1669 "https://localhost./",
1670 "https://LOCALHOST../x",
1671 "https://printer.local./",
1672 "https://db.internal./",
1673 "https://api.localhost./",
1674 "https://intranet./",
1675 "https://127.0.0.1./",
1676 ] {
1677 assert_eq!(
1678 validate_public_https_url(url),
1679 Err(UrlPolicyError::NonPublicHost),
1680 "{url}"
1681 );
1682 }
1683 }
1684
1685 #[test]
1686 fn percent_encoded_hosts_are_decoded_before_classification() {
1687 assert_eq!(
1688 validate_public_https_url("https://%31%32%37.0.0.1/"),
1689 Err(UrlPolicyError::NonPublicHost)
1690 );
1691 assert_eq!(
1692 validate_public_https_url("https://%31%32%37.0.0.1./"),
1693 Err(UrlPolicyError::NonPublicHost)
1694 );
1695 assert_eq!(
1696 validate_public_https_url("https://%6c%6f%63%61%6c%68%6f%73%74/"),
1697 Err(UrlPolicyError::NonPublicHost)
1698 );
1699 assert_eq!(
1700 validate_public_https_url("https://%2fexample.com/"),
1701 Err(UrlPolicyError::Malformed)
1702 );
1703 assert_eq!(
1704 validate_public_https_url("https://exa%3Fmple.com/"),
1705 Err(UrlPolicyError::Malformed)
1706 );
1707 assert_eq!(
1708 validate_public_https_url("https://example%2.com/"),
1709 Err(UrlPolicyError::Malformed)
1710 );
1711 assert_eq!(
1712 validate_public_https_url("https://%+1.example.com/"),
1713 Err(UrlPolicyError::Malformed)
1714 );
1715 assert_eq!(
1716 validate_public_https_url("https://%25.example.com/"),
1717 Err(UrlPolicyError::Malformed)
1718 );
1719 }
1720
1721 #[test]
1722 fn trailing_dot_public_names_and_valid_ports_pass() {
1723 assert_eq!(validate_public_https_url("https://example.com./x"), Ok(()));
1724 assert_eq!(
1725 validate_public_https_url("https://example.com:443/"),
1726 Ok(())
1727 );
1728 assert_eq!(validate_public_https_url("https://example.com:/"), Ok(()));
1729 assert_eq!(
1730 validate_public_https_url("https://[2606:4700::1111]:8443/"),
1731 Ok(())
1732 );
1733 }
1734
1735 #[test]
1736 fn invalid_ports_are_refused() {
1737 for url in [
1738 "https://example.com:0/",
1739 "https://example.com:65536/",
1740 "https://example.com:abc/",
1741 "https://example.com:+443/",
1742 "https://example.com:80:90/",
1743 "https://[2606:4700::1111]:0/",
1744 "https://[2606:4700::1111]x/",
1745 ] {
1746 assert_eq!(
1747 validate_public_https_url(url),
1748 Err(UrlPolicyError::Malformed),
1749 "{url}"
1750 );
1751 }
1752 }
1753}