1use std::net::IpAddr;
2use std::time::Duration;
3
4#[derive(Debug, thiserror::Error)]
6#[non_exhaustive]
7pub enum Error {
8 #[error("{0}")]
11 InvalidUrl(String),
12
13 #[error("{0:?} is not an address or address block")]
15 InvalidCidr(String),
16
17 #[error("the {0} header can't be set on a request")]
20 RefusedHeader(String),
21
22 #[error("{}", denied(host, addr))]
29 #[non_exhaustive]
30 Denied {
31 host: String,
33 addr: IpAddr,
35 },
36
37 #[error("no source address to reach {host} from")]
40 #[non_exhaustive]
41 NoSource {
42 host: String,
44 },
45
46 #[error("{host} does not resolve")]
48 #[non_exhaustive]
49 Resolve {
50 host: String,
52 source: std::io::Error,
54 },
55
56 #[error("redirect refused: {0}")]
59 BadRedirect(String),
60
61 #[error("more than {0} redirects")]
63 TooManyRedirects(usize),
64
65 #[error("response body is over {0} bytes")]
67 BodyTooLarge(usize),
68
69 #[error("no answer within {0:?}")]
71 Timeout(Duration),
72
73 #[error("can't connect to {host}")]
75 #[non_exhaustive]
76 Connect {
77 host: String,
79 source: std::io::Error,
81 },
82
83 #[error("TLS setup failed: {0}")]
86 Tls(String),
87
88 #[error(transparent)]
92 Http(Box<dyn std::error::Error + Send + Sync>),
93}
94
95fn denied(host: &str, addr: &IpAddr) -> String {
96 let bare = host.trim_start_matches('[').trim_end_matches(']');
97 if bare.parse::<IpAddr>().is_ok() {
98 format!("{addr} is an address this policy refuses")
99 } else {
100 format!("{host} resolves to an address this policy refuses")
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 fn shown(host: &str) -> String {
109 Error::Denied {
110 host: host.to_string(),
111 addr: "10.1.2.3".parse().unwrap(),
112 }
113 .to_string()
114 }
115
116 #[test]
117 fn a_denied_name_keeps_its_address_quiet() {
118 assert_eq!(
119 shown("intranet.example"),
120 "intranet.example resolves to an address this policy refuses"
121 );
122 assert_eq!(
123 shown("10.1.2.3"),
124 "10.1.2.3 is an address this policy refuses"
125 );
126 assert_eq!(
127 shown("[::ffff:10.1.2.3]"),
128 "10.1.2.3 is an address this policy refuses"
129 );
130 }
131
132 #[test]
133 fn the_cause_is_in_the_source_not_the_message() {
134 let e = Error::Resolve {
135 host: "nope.test".to_string(),
136 source: std::io::Error::other("not in the table"),
137 };
138 assert_eq!(e.to_string(), "nope.test does not resolve");
139 let source = std::error::Error::source(&e).unwrap();
140 assert_eq!(source.to_string(), "not in the table");
141 }
142}