Skip to main content

isb_core/
net.rs

1//! Outbound connections held to an address policy (notifications, and
2//! whatever else dials an address an org member chose).
3//!
4//! A notification target is chosen by an org member, so it must not become a
5//! way into the host's own network (SSRF). Every destination is resolved
6//! here, every address it resolves to is checked, and the connection is made
7//! to one of those checked addresses (never re-resolved, so DNS rebinding
8//! cannot swap in another), and the connected peer is checked once more.
9//! Redirects are never followed. Loopback, private, link-local, CGNAT and
10//! other non-public ranges are refused unless the platform admin allows
11//! private targets server-wide.
12//!
13//! Error messages name the host, never the URL: a webhook URL's path is a
14//! credential (Slack, Discord), as is a Telegram bot token.
15
16use std::io::{Read, Write};
17use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpStream, ToSocketAddrs};
18use std::sync::{Arc, OnceLock};
19use std::time::{Duration, Instant};
20
21/// How long connecting, and each read or write, may take.
22pub const IO_TIMEOUT: Duration = Duration::from_secs(10);
23/// The longest a whole HTTP exchange may take.
24const EXCHANGE_TIMEOUT: Duration = Duration::from_secs(30);
25/// The most of a response that is read.
26const MAX_RESPONSE: usize = 64 * 1024;
27
28/// A failed send: what went wrong, and whether trying again may help.
29#[derive(Debug, Clone, PartialEq)]
30pub struct SendError {
31    pub message: String,
32    pub retryable: bool,
33}
34
35impl SendError {
36    pub fn permanent(m: impl Into<String>) -> SendError {
37        SendError {
38            message: m.into(),
39            retryable: false,
40        }
41    }
42    pub fn transient(m: impl Into<String>) -> SendError {
43        SendError {
44            message: m.into(),
45            retryable: true,
46        }
47    }
48}
49
50impl std::fmt::Display for SendError {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        f.write_str(&self.message)
53    }
54}
55
56/// Where and how outbound connections may go.
57#[derive(Clone)]
58pub struct Net {
59    /// Allow loopback, private and other non-public destinations.
60    pub allow_private: bool,
61    pub tls: Arc<rustls::ClientConfig>,
62}
63
64impl Net {
65    /// The public internet only, trusting the Mozilla root set.
66    pub fn new(allow_private: bool) -> Net {
67        Net {
68            allow_private,
69            tls: default_tls(),
70        }
71    }
72}
73
74/// A TLS client config on ring with the webpki (Mozilla) roots.
75pub fn default_tls() -> Arc<rustls::ClientConfig> {
76    static TLS: OnceLock<Arc<rustls::ClientConfig>> = OnceLock::new();
77    TLS.get_or_init(|| {
78        let roots = rustls::RootCertStore {
79            roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
80        };
81        tls_with_roots(roots)
82    })
83    .clone()
84}
85
86/// A TLS client config trusting exactly `roots`.
87pub fn tls_with_roots(roots: rustls::RootCertStore) -> Arc<rustls::ClientConfig> {
88    let provider = Arc::new(rustls::crypto::ring::default_provider());
89    Arc::new(
90        rustls::ClientConfig::builder_with_provider(provider)
91            .with_safe_default_protocol_versions()
92            .expect("ring supports the default protocol versions")
93            .with_root_certificates(roots)
94            .with_no_client_auth(),
95    )
96}
97
98/// A parsed `http(s)://host[:port]/path` URL.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct Target {
101    pub https: bool,
102    /// Lowercased; an IPv6 literal without its brackets.
103    pub host: String,
104    pub port: u16,
105    /// Path and query, starting with `/`.
106    pub path: String,
107}
108
109impl Target {
110    /// The Host header's value.
111    fn host_header(&self) -> String {
112        let h = if self.host.contains(':') {
113            format!("[{}]", self.host)
114        } else {
115            self.host.clone()
116        };
117        let default = if self.https { 443 } else { 80 };
118        if self.port == default {
119            h
120        } else {
121            format!("{h}:{}", self.port)
122        }
123    }
124}
125
126/// Parse a webhook URL. The error never repeats the URL.
127pub fn parse_url(s: &str) -> Result<Target, String> {
128    let s = s.trim();
129    let (https, rest) = if let Some(r) = strip_prefix_ci(s, "https://") {
130        (true, r)
131    } else if let Some(r) = strip_prefix_ci(s, "http://") {
132        (false, r)
133    } else {
134        return Err("the URL must start with http:// or https://".into());
135    };
136    if s.chars().any(|c| c.is_whitespace() || c.is_control()) {
137        return Err("the URL holds whitespace or control characters".into());
138    }
139    let end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
140    let (authority, tail) = rest.split_at(end);
141    if authority.contains('@') {
142        return Err("credentials in the URL are not supported".into());
143    }
144    let (host, port) = if let Some(r) = authority.strip_prefix('[') {
145        let close = r.find(']').ok_or("an unclosed [ in the URL's host")?;
146        let host = &r[..close];
147        host.parse::<Ipv6Addr>()
148            .map_err(|_| "a bad IPv6 address in the URL")?;
149        let after = &r[close + 1..];
150        let port = match after.strip_prefix(':') {
151            Some(p) => Some(p),
152            None if after.is_empty() => None,
153            None => return Err("junk after the URL's IPv6 host".into()),
154        };
155        (host.to_ascii_lowercase(), port)
156    } else {
157        match authority.rsplit_once(':') {
158            Some((h, p)) => (h.to_ascii_lowercase(), Some(p)),
159            None => (authority.to_ascii_lowercase(), None),
160        }
161    };
162    if host.is_empty() {
163        return Err("the URL has no host".into());
164    }
165    if !host.contains(':')
166        && !host
167            .chars()
168            .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
169    {
170        return Err(format!("{host:?} is not a host name"));
171    }
172    let port = match port {
173        Some(p) => p
174            .parse::<u16>()
175            .ok()
176            .filter(|p| *p != 0)
177            .ok_or_else(|| format!("a bad port in the URL for {host}"))?,
178        None if https => 443,
179        None => 80,
180    };
181    let tail = tail.split('#').next().unwrap_or_default();
182    let path = if tail.starts_with('/') {
183        tail.to_string()
184    } else {
185        format!("/{tail}")
186    };
187    Ok(Target {
188        https,
189        host,
190        port,
191        path,
192    })
193}
194
195fn strip_prefix_ci<'a>(s: &'a str, p: &str) -> Option<&'a str> {
196    (s.len() >= p.len() && s[..p.len()].eq_ignore_ascii_case(p)).then(|| &s[p.len()..])
197}
198
199/// Read a host the way `inet_aton` (and so `getaddrinfo`) does: one to four
200/// parts, each decimal, octal (leading 0) or hex (0x). `2130706433`,
201/// `0x7f000001`, `0177.1` and `127.1` are all 127.0.0.1.
202pub fn parse_inet_aton(s: &str) -> Option<Ipv4Addr> {
203    let parts: Vec<&str> = s.split('.').collect();
204    if parts.is_empty() || parts.len() > 4 {
205        return None;
206    }
207    let mut nums = Vec::with_capacity(4);
208    for p in &parts {
209        let (digits, radix) = if let Some(h) = p.strip_prefix("0x").or(p.strip_prefix("0X")) {
210            (h, 16)
211        } else if p.len() > 1 && p.starts_with('0') {
212            (&p[1..], 8)
213        } else {
214            (*p, 10)
215        };
216        if digits.is_empty() || !digits.chars().all(|c| c.is_digit(radix)) {
217            return None;
218        }
219        let n = u64::from_str_radix(digits, radix).ok()?;
220        nums.push(n);
221    }
222    let last = *nums.last()?;
223    let lead = &nums[..nums.len() - 1];
224    if lead.iter().any(|n| *n > 255) {
225        return None;
226    }
227    let rest_bits = 8 * (4 - lead.len() as u32);
228    if rest_bits < 64 && last >= (1u64 << rest_bits) {
229        return None;
230    }
231    let mut v: u64 = 0;
232    for (i, n) in lead.iter().enumerate() {
233        v |= n << (24 - 8 * i as u32);
234    }
235    Some(Ipv4Addr::from((v | last) as u32))
236}
237
238/// Why an address may not be reached: `Err(reason)`. Some ranges are never
239/// reachable (unspecified, multicast, broadcast); the rest of the non-public
240/// ranges only with `allow_private`.
241pub fn check_ip(ip: IpAddr, allow_private: bool) -> Result<(), String> {
242    match classify(ip) {
243        Class::Public => Ok(()),
244        Class::Never(why) => Err(format!("{ip} is {why}")),
245        Class::Private(_) if allow_private => Ok(()),
246        Class::Private(why) => Err(format!(
247            "{ip} is {why}; private targets are off (a platform admin can allow them)"
248        )),
249    }
250}
251
252enum Class {
253    Public,
254    Private(&'static str),
255    Never(&'static str),
256}
257
258fn classify(ip: IpAddr) -> Class {
259    match ip {
260        IpAddr::V4(v4) => classify_v4(v4),
261        IpAddr::V6(v6) => classify_v6(v6),
262    }
263}
264
265fn in_v4(ip: Ipv4Addr, net: [u8; 4], bits: u32) -> bool {
266    let mask = if bits == 0 {
267        0
268    } else {
269        u32::MAX << (32 - bits)
270    };
271    (u32::from(ip) & mask) == (u32::from(Ipv4Addr::from(net)) & mask)
272}
273
274fn classify_v4(ip: Ipv4Addr) -> Class {
275    use Class::*;
276    let table: &[([u8; 4], u32, Class)] = &[
277        ([0, 0, 0, 0], 8, Never("in 0.0.0.0/8 (this network)")),
278        ([255, 255, 255, 255], 32, Never("the broadcast address")),
279        ([224, 0, 0, 0], 4, Never("multicast")),
280        ([127, 0, 0, 0], 8, Private("loopback")),
281        ([10, 0, 0, 0], 8, Private("private (10.0.0.0/8)")),
282        ([172, 16, 0, 0], 12, Private("private (172.16.0.0/12)")),
283        ([192, 168, 0, 0], 16, Private("private (192.168.0.0/16)")),
284        (
285            [100, 64, 0, 0],
286            10,
287            Private("shared address space (100.64.0.0/10)"),
288        ),
289        ([169, 254, 0, 0], 16, Private("link-local")),
290        ([192, 0, 0, 0], 24, Private("IETF protocol space")),
291        ([192, 0, 2, 0], 24, Private("documentation space")),
292        ([198, 51, 100, 0], 24, Private("documentation space")),
293        ([203, 0, 113, 0], 24, Private("documentation space")),
294        ([198, 18, 0, 0], 15, Private("benchmarking space")),
295        ([240, 0, 0, 0], 4, Private("reserved (240.0.0.0/4)")),
296    ];
297    for (net, bits, class) in table {
298        if in_v4(ip, *net, *bits) {
299            return match class {
300                Never(w) => Never(w),
301                Private(w) => Private(w),
302                Public => Public,
303            };
304        }
305    }
306    Public
307}
308
309fn classify_v6(ip: Ipv6Addr) -> Class {
310    use Class::*;
311    let s = ip.segments();
312    let embedded = |hi: usize| {
313        Ipv4Addr::new(
314            (s[hi] >> 8) as u8,
315            s[hi] as u8,
316            (s[hi + 1] >> 8) as u8,
317            s[hi + 1] as u8,
318        )
319    };
320    if ip.is_unspecified() {
321        return Never("unspecified");
322    }
323    if ip.is_loopback() {
324        return Private("loopback");
325    }
326    if s[0] & 0xff00 == 0xff00 {
327        return Never("multicast");
328    }
329    // ::ffff:a.b.c.d (mapped) and ::a.b.c.d (compatible) reach the IPv4 host.
330    if s[..5] == [0; 5] && (s[5] == 0xffff || s[5] == 0) {
331        return classify_v4(embedded(6));
332    }
333    // NAT64: the well-known prefix reaches the embedded IPv4 address.
334    if s[..6] == [0x64, 0xff9b, 0, 0, 0, 0] {
335        return classify_v4(embedded(6));
336    }
337    if s[0] == 0x64 && s[1] == 0xff9b && s[2] == 1 {
338        return Private("local-use NAT64");
339    }
340    // 6to4 carries an IPv4 address in bits 16..48.
341    if s[0] == 0x2002 {
342        return classify_v4(embedded(1));
343    }
344    if s[0] == 0x2001 && s[1] == 0 {
345        return Private("Teredo");
346    }
347    if s[0] == 0x2001 && s[1] == 0x0db8 {
348        return Private("documentation space");
349    }
350    if s[0] & 0xfe00 == 0xfc00 {
351        return Private("unique local (fc00::/7)");
352    }
353    if s[0] & 0xffc0 == 0xfe80 {
354        return Private("link-local");
355    }
356    if s[0] & 0xffc0 == 0xfec0 {
357        return Private("site-local");
358    }
359    if s[..4] == [0x100, 0, 0, 0] {
360        return Private("discard space");
361    }
362    Public
363}
364
365/// Resolve a host and check every address: all must pass, so a name with
366/// one public and one private record is refused.
367pub fn resolve(host: &str, port: u16, allow_private: bool) -> Result<Vec<SocketAddr>, SendError> {
368    let literal = host
369        .parse::<IpAddr>()
370        .ok()
371        .or_else(|| parse_inet_aton(host).map(IpAddr::V4));
372    let addrs: Vec<SocketAddr> = match literal {
373        Some(ip) => vec![SocketAddr::new(ip, port)],
374        None => (host, port)
375            .to_socket_addrs()
376            .map_err(|e| SendError::transient(format!("cannot resolve {host}: {e}")))?
377            .collect(),
378    };
379    if addrs.is_empty() {
380        return Err(SendError::transient(format!("{host} has no addresses")));
381    }
382    for a in &addrs {
383        check_ip(a.ip(), allow_private)
384            .map_err(|why| SendError::permanent(format!("refusing {host}: {why}")))?;
385    }
386    Ok(addrs)
387}
388
389/// Connect to one of a host's checked addresses, and check the peer.
390pub fn connect(host: &str, port: u16, allow_private: bool) -> Result<TcpStream, SendError> {
391    let addrs = resolve(host, port, allow_private)?;
392    let mut last = None;
393    for a in addrs {
394        match TcpStream::connect_timeout(&a, IO_TIMEOUT) {
395            Ok(s) => {
396                let peer = s
397                    .peer_addr()
398                    .map_err(|e| SendError::transient(format!("{host}: {e}")))?;
399                check_ip(peer.ip(), allow_private)
400                    .map_err(|why| SendError::permanent(format!("refusing {host}: {why}")))?;
401                s.set_read_timeout(Some(IO_TIMEOUT)).ok();
402                s.set_write_timeout(Some(IO_TIMEOUT)).ok();
403                return Ok(s);
404            }
405            Err(e) => last = Some(e),
406        }
407    }
408    Err(SendError::transient(format!(
409        "cannot connect to {host}:{port}: {}",
410        last.map(|e| e.to_string()).unwrap_or_default()
411    )))
412}
413
414/// Wrap a connected stream in TLS to `host`.
415pub fn tls(
416    net: &Net,
417    host: &str,
418    tcp: TcpStream,
419) -> Result<rustls::StreamOwned<rustls::ClientConnection, TcpStream>, SendError> {
420    let name = rustls::pki_types::ServerName::try_from(host.to_string())
421        .map_err(|_| SendError::permanent(format!("{host} is not a TLS server name")))?;
422    let conn = rustls::ClientConnection::new(net.tls.clone(), name)
423        .map_err(|e| SendError::permanent(format!("TLS to {host}: {e}")))?;
424    Ok(rustls::StreamOwned::new(conn, tcp))
425}
426
427/// An HTTP POST.
428#[derive(Debug, Clone)]
429pub struct Request {
430    pub url: String,
431    pub headers: Vec<(String, String)>,
432    pub body: Vec<u8>,
433}
434
435/// What came back.
436#[derive(Debug, Clone)]
437pub struct Response {
438    pub status: u16,
439    pub retry_after: Option<Duration>,
440    /// The start of the body, lossily decoded.
441    pub body: String,
442}
443
444/// POST once. Never follows a redirect: a 3xx is a failure like a 4xx.
445pub fn post(net: &Net, req: &Request) -> Result<Response, SendError> {
446    let t = parse_url(&req.url).map_err(SendError::permanent)?;
447    let started = Instant::now();
448    let tcp = connect(&t.host, t.port, net.allow_private)?;
449    let mut head = format!(
450        "POST {} HTTP/1.1\r\nHost: {}\r\nUser-Agent: isb/{}\r\nContent-Length: {}\r\nConnection: close\r\n",
451        t.path,
452        t.host_header(),
453        env!("CARGO_PKG_VERSION"),
454        req.body.len()
455    );
456    for (k, v) in &req.headers {
457        if k.contains(['\r', '\n', ':']) || v.contains(['\r', '\n']) {
458            return Err(SendError::permanent(format!("a bad header {k:?}")));
459        }
460        head.push_str(&format!("{k}: {v}\r\n"));
461    }
462    head.push_str("\r\n");
463    let raw = if t.https {
464        let mut s = tls(net, &t.host, tcp)?;
465        exchange(&mut s, &t.host, head.as_bytes(), &req.body, started)?
466    } else {
467        let mut s = tcp;
468        exchange(&mut s, &t.host, head.as_bytes(), &req.body, started)?
469    };
470    parse_response(&raw).map_err(|e| SendError::transient(format!("{}: {e}", t.host)))
471}
472
473fn exchange<S: Read + Write>(
474    s: &mut S,
475    host: &str,
476    head: &[u8],
477    body: &[u8],
478    started: Instant,
479) -> Result<Vec<u8>, SendError> {
480    let io = |e: std::io::Error| SendError::transient(format!("{host}: {e}"));
481    s.write_all(head).map_err(io)?;
482    s.write_all(body).map_err(io)?;
483    s.flush().map_err(io)?;
484    let mut out = Vec::new();
485    let mut buf = [0u8; 8192];
486    loop {
487        if started.elapsed() > EXCHANGE_TIMEOUT {
488            return Err(SendError::transient(format!(
489                "{host}: no complete answer within {EXCHANGE_TIMEOUT:?}"
490            )));
491        }
492        match s.read(&mut buf) {
493            Ok(0) => break,
494            Ok(n) => {
495                out.extend_from_slice(&buf[..n]);
496                if out.len() >= MAX_RESPONSE || response_complete(&out) {
497                    break;
498                }
499            }
500            // A peer that closes without TLS close_notify, after answering.
501            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof && !out.is_empty() => break,
502            Err(e) if !out.is_empty() && header_end(&out).is_some() => {
503                let _ = e;
504                break;
505            }
506            Err(e) => return Err(io(e)),
507        }
508    }
509    Ok(out)
510}
511
512fn header_end(b: &[u8]) -> Option<usize> {
513    b.windows(4).position(|w| w == b"\r\n\r\n").map(|p| p + 4)
514}
515
516/// Headers and a Content-Length body are in (a server that keeps the
517/// connection open despite `Connection: close` does not hold us up).
518fn response_complete(b: &[u8]) -> bool {
519    let Some(end) = header_end(b) else {
520        return false;
521    };
522    let mut headers = [httparse::EMPTY_HEADER; 64];
523    let mut r = httparse::Response::new(&mut headers);
524    if r.parse(b).is_err() {
525        return false;
526    }
527    let len = r
528        .headers
529        .iter()
530        .find(|h| h.name.eq_ignore_ascii_case("content-length"))
531        .and_then(|h| {
532            std::str::from_utf8(h.value)
533                .ok()?
534                .trim()
535                .parse::<usize>()
536                .ok()
537        });
538    matches!(len, Some(n) if b.len() >= end + n)
539}
540
541fn parse_response(raw: &[u8]) -> Result<Response, String> {
542    let mut headers = [httparse::EMPTY_HEADER; 64];
543    let mut r = httparse::Response::new(&mut headers);
544    let end = match r.parse(raw) {
545        Ok(httparse::Status::Complete(n)) => n,
546        Ok(httparse::Status::Partial) => return Err("an incomplete HTTP answer".into()),
547        Err(e) => return Err(format!("a bad HTTP answer: {e}")),
548    };
549    let status = r.code.unwrap_or(0);
550    let retry_after = r
551        .headers
552        .iter()
553        .find(|h| h.name.eq_ignore_ascii_case("retry-after"))
554        .and_then(|h| {
555            std::str::from_utf8(h.value)
556                .ok()?
557                .trim()
558                .parse::<u64>()
559                .ok()
560        })
561        .map(Duration::from_secs);
562    let body = &raw[end..];
563    let body = String::from_utf8_lossy(&body[..body.len().min(512)]).into_owned();
564    Ok(Response {
565        status,
566        retry_after,
567        body,
568    })
569}
570
571/// HMAC-SHA256 of `body` under `key`, as lowercase hex.
572pub fn hmac_sha256_hex(key: &[u8], body: &[u8]) -> String {
573    let k = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, key);
574    ring::hmac::sign(&k, body)
575        .as_ref()
576        .iter()
577        .map(|b| format!("{b:02x}"))
578        .collect()
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584
585    fn refused(s: &str) -> bool {
586        let ip: IpAddr = s.parse().unwrap();
587        check_ip(ip, false).is_err()
588    }
589
590    #[test]
591    fn urls() {
592        let t = parse_url("https://hooks.slack.com/services/T/B/x?y=1#frag").unwrap();
593        assert_eq!(
594            t,
595            Target {
596                https: true,
597                host: "hooks.slack.com".into(),
598                port: 443,
599                path: "/services/T/B/x?y=1".into()
600            }
601        );
602        let t = parse_url("HTTP://[::1]:8080").unwrap();
603        assert_eq!(
604            (t.https, t.host.as_str(), t.port, t.path.as_str()),
605            (false, "::1", 8080, "/")
606        );
607        assert_eq!(t.host_header(), "[::1]:8080");
608        assert_eq!(
609            parse_url("http://Example.COM:80/a").unwrap().host_header(),
610            "example.com"
611        );
612        for bad in [
613            "ftp://x/",
614            "file:///etc/passwd",
615            "gopher://x",
616            "http://user:pw@x/",
617            "http:///x",
618            "http://x:0/",
619            "http://x:99999/",
620            "http://[::1/",
621            "http://a b/",
622            "http://x/\r\nHost: y",
623        ] {
624            assert!(parse_url(bad).is_err(), "{bad}");
625        }
626        // Errors never echo the URL (its path can be a credential).
627        let e = parse_url("ftp://x/SECRETTOKEN").unwrap_err();
628        assert!(!e.contains("SECRETTOKEN"));
629    }
630
631    #[test]
632    fn non_public_ranges_are_refused() {
633        for s in [
634            "127.0.0.1",
635            "127.255.0.9",
636            "10.1.2.3",
637            "172.16.0.1",
638            "172.31.255.255",
639            "192.168.1.1",
640            "169.254.169.254",
641            "100.64.0.1",
642            "100.86.22.100",
643            "0.0.0.0",
644            "0.1.2.3",
645            "255.255.255.255",
646            "224.0.0.1",
647            "240.0.0.1",
648            "198.18.0.1",
649            "::",
650            "::1",
651            "fe80::1",
652            "fc00::1",
653            "fd42:1:2::3",
654            "ff02::1",
655            "::ffff:127.0.0.1",
656            "::ffff:10.0.0.1",
657            "::127.0.0.1",
658            "64:ff9b::a9fe:a9fe",
659            "2002:7f00:1::",
660            "2002:c0a8:0101::1",
661            "2001:db8::1",
662            "fec0::1",
663        ] {
664            assert!(refused(s), "{s} should be refused");
665        }
666        for s in [
667            "1.1.1.1",
668            "8.8.8.8",
669            "172.32.0.1",
670            "100.128.0.1",
671            "2606:4700:4700::1111",
672            "::ffff:1.1.1.1",
673            "64:ff9b::808:808",
674            "2002:0808:0808::1",
675        ] {
676            assert!(!refused(s), "{s} should pass");
677        }
678        // The admin switch opens private ranges, never the unusable ones.
679        assert!(check_ip("127.0.0.1".parse().unwrap(), true).is_ok());
680        assert!(check_ip("::1".parse().unwrap(), true).is_ok());
681        assert!(check_ip("0.0.0.0".parse().unwrap(), true).is_err());
682        assert!(check_ip("::".parse().unwrap(), true).is_err());
683        assert!(check_ip("ff02::1".parse().unwrap(), true).is_err());
684    }
685
686    #[test]
687    fn numeric_host_forms() {
688        let lo = Ipv4Addr::new(127, 0, 0, 1);
689        for s in [
690            "2130706433",
691            "0x7f000001",
692            "0X7F000001",
693            "017700000001",
694            "0177.0.0.1",
695            "0x7f.0.0.1",
696            "127.1",
697            "127.0.1",
698            "0x7f.1",
699        ] {
700            assert_eq!(parse_inet_aton(s), Some(lo), "{s}");
701        }
702        assert_eq!(
703            parse_inet_aton("169.254.43518"),
704            Some(Ipv4Addr::new(169, 254, 169, 254))
705        );
706        for s in [
707            "example.com",
708            "256.1.1.1",
709            "1.2.3.4.5",
710            "",
711            "0x",
712            "09",
713            "1.2.3.256",
714        ] {
715            assert_eq!(parse_inet_aton(s), None, "{s}");
716        }
717        // Resolution treats them as the address they name, and refuses it.
718        for h in [
719            "2130706433",
720            "0x7f000001",
721            "0177.0.0.1",
722            "127.1",
723            "::ffff:127.0.0.1",
724        ] {
725            let e = resolve(h, 80, false).unwrap_err();
726            assert!(!e.retryable && e.message.contains("loopback"), "{h}: {e}");
727        }
728        assert!(resolve("2130706433", 80, true).is_ok());
729    }
730
731    #[test]
732    fn names_resolving_to_private_addresses_are_refused() {
733        // `localhost` resolves (without DNS) to loopback: a name is checked
734        // by what it resolves to, not by how it looks.
735        let e = resolve("localhost", 80, false).unwrap_err();
736        assert!(e.message.contains("loopback"), "{e}");
737        assert!(!e.retryable);
738        // And connect refuses before any packet leaves.
739        let e = connect("localhost", 9, false).unwrap_err();
740        assert!(e.message.contains("refusing localhost"), "{e}");
741    }
742
743    #[test]
744    fn posts_without_following_redirects() {
745        use std::net::TcpListener;
746        let l = TcpListener::bind("127.0.0.1:0").unwrap();
747        let port = l.local_addr().unwrap().port();
748        let h = std::thread::spawn(move || {
749            let (mut s, _) = l.accept().unwrap();
750            let mut buf = vec![0u8; 4096];
751            let mut got = Vec::new();
752            while !String::from_utf8_lossy(&got).contains("hello") {
753                let n = s.read(&mut buf).unwrap();
754                got.extend_from_slice(&buf[..n]);
755            }
756            s.write_all(
757                b"HTTP/1.1 302 Found\r\nLocation: http://169.254.169.254/\r\nRetry-After: 7\r\nContent-Length: 2\r\n\r\nno",
758            )
759            .unwrap();
760            String::from_utf8(got).unwrap()
761        });
762        let net = Net::new(true);
763        let r = post(
764            &net,
765            &Request {
766                url: format!("http://127.0.0.1:{port}/hook?a=1"),
767                headers: vec![("X-Test".into(), "1".into())],
768                body: b"hello".to_vec(),
769            },
770        )
771        .unwrap();
772        assert_eq!(r.status, 302);
773        assert_eq!(r.retry_after, Some(Duration::from_secs(7)));
774        assert_eq!(r.body, "no");
775        let req = h.join().unwrap();
776        assert!(req.starts_with("POST /hook?a=1 HTTP/1.1\r\n"), "{req}");
777        assert!(req.contains(&format!("Host: 127.0.0.1:{port}\r\n")));
778        assert!(req.contains("X-Test: 1\r\n"));
779        // Without the admin switch the same target is refused up front.
780        let e = post(
781            &Net::new(false),
782            &Request {
783                url: format!("http://127.0.0.1:{port}/hook"),
784                headers: vec![],
785                body: vec![],
786            },
787        )
788        .unwrap_err();
789        assert!(e.message.contains("private targets are off"), "{e}");
790    }
791
792    #[test]
793    fn hmac_matches_rfc_4231() {
794        // RFC 4231 test case 2.
795        assert_eq!(
796            hmac_sha256_hex(b"Jefe", b"what do ya want for nothing?"),
797            "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
798        );
799    }
800}