Skip to main content

ssrf_guard/
error.rs

1use std::net::IpAddr;
2use std::time::Duration;
3
4/// Why a fetch or connect didn't happen, or didn't finish.
5#[derive(Debug, thiserror::Error)]
6#[non_exhaustive]
7pub enum Error {
8    /// The URL isn't an absolute `http://` or `https://` URL with a host, or
9    /// it carries a user name or password.
10    #[error("{0}")]
11    InvalidUrl(String),
12
13    /// A string passed as an address block didn't parse.
14    #[error("{0:?} is not an address or address block")]
15    InvalidCidr(String),
16
17    /// The request sets a header this crate won't send for you, such as
18    /// `Host` or `Content-Length`. See [`Request::header`](crate::Request::header).
19    #[error("the {0} header can't be set on a request")]
20    RefusedHeader(String),
21
22    /// The host is, or resolves to, an address the policy refuses. One such
23    /// address refuses the whole name.
24    ///
25    /// The message leaves the address out when the host is a name, so it's
26    /// safe to show to whoever typed the URL. `addr` is for your logs. Shown
27    /// to the user, it tells them what your internal names resolve to.
28    #[error("{}", denied(host, addr))]
29    #[non_exhaustive]
30    Denied {
31        /// The host as the URL or the caller gave it.
32        host: String,
33        /// The first refused address it resolved to.
34        addr: IpAddr,
35    },
36
37    /// The policy has a source address, but none in any family the host
38    /// resolved to.
39    #[error("no source address to reach {host} from")]
40    #[non_exhaustive]
41    NoSource {
42        /// The host that had no reachable address.
43        host: String,
44    },
45
46    /// The name didn't resolve, or resolved to nothing.
47    #[error("{host} does not resolve")]
48    #[non_exhaustive]
49    Resolve {
50        /// The name that was looked up.
51        host: String,
52        /// What the resolver said.
53        source: std::io::Error,
54    },
55
56    /// A redirect had a `Location` that can't be followed: unparseable, to a
57    /// scheme other than http and https, or with a user name or password.
58    #[error("redirect refused: {0}")]
59    BadRedirect(String),
60
61    /// The chain was longer than the policy's redirect limit.
62    #[error("more than {0} redirects")]
63    TooManyRedirects(usize),
64
65    /// The body was larger than the policy's limit.
66    #[error("response body is over {0} bytes")]
67    BodyTooLarge(usize),
68
69    /// The policy's timeout ran out. It covers every hop of a fetch.
70    #[error("no answer within {0:?}")]
71    Timeout(Duration),
72
73    /// No TCP connection could be made to any of the host's addresses.
74    #[error("can't connect to {host}")]
75    #[non_exhaustive]
76    Connect {
77        /// The host being connected to.
78        host: String,
79        /// The error from the last address tried.
80        source: std::io::Error,
81    },
82
83    /// The TLS client couldn't be set up, usually because the system has
84    /// no trust store to verify certificates against.
85    #[error("TLS setup failed: {0}")]
86    Tls(String),
87
88    /// The request failed after it was allowed: refused, reset, a bad
89    /// certificate. This is the HTTP client's own error, with the URL taken
90    /// out of it.
91    #[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}