Skip to main content

parse_net/
host.rs

1//! `Host = uri-host [ ":" port ]` (RFC 9110 section 7.2, citing RFC 3986
2//! section 3.2.2), with real IPv4 and IPv6 validation.
3//!
4//! # Examples
5//!
6//! ```
7//! use core::net::Ipv6Addr;
8//! use parse_net::{self as pn, Host, HostPort};
9//!
10//! assert_eq!(pn::parse_host_port(b"[::1]:8080")?,
11//!            HostPort { host: Host::V6(Ipv6Addr::LOCALHOST), port: Some(8080), empty_port: false });
12//! assert_eq!(pn::parse_host_port(b"Example.COM.")?.host, Host::Name(b"Example.COM."));
13//! assert_eq!(pn::parse_host_port(b"127.1").unwrap_err().kind(), pn::ErrorKind::InvalidAddress);
14//! assert_eq!(pn::parse_host_port(b"a.com:80@b.com").unwrap_err().kind(), pn::ErrorKind::InvalidHost);
15//! # Ok::<(), pn::ParseError>(())
16//! ```
17
18use core::{
19    fmt::{self, Write as _},
20    net::{Ipv4Addr, Ipv6Addr},
21    str::FromStr,
22};
23
24use crate::{
25    class,
26    error::{ErrorKind, Result, WriteError, err},
27    scan::{control_kind, find_byte},
28    text::{skip_ows, trim_ows},
29    write::{CountSink, Sink, reserve},
30};
31
32/// A host: a registered name, an IP address, or a future IP literal.
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
34pub enum Host<'a> {
35    /// A registered name (`reg-name`), as sent: case and a trailing dot are
36    /// kept, `%XX` triples are not decoded. Empty when the field was empty.
37    Name(&'a [u8]),
38    /// A dotted-decimal IPv4 address.
39    V4(Ipv4Addr),
40    /// A bracketed IPv6 address.
41    V6(Ipv6Addr),
42    /// A bracketed `IPvFuture` literal (`vX.yyy`), without the brackets.
43    Future(&'a [u8]),
44}
45
46impl Host<'_> {
47    /// Whether two hosts are the same: names compared ASCII
48    /// case-insensitively, IP literals by value.
49    ///
50    /// # Examples
51    ///
52    /// ```
53    /// use parse_net::host::parse_host;
54    ///
55    /// assert!(parse_host(b"Example.com")?.eq_ignore_ascii_case(&parse_host(b"example.COM")?));
56    /// assert!(parse_host(b"[::1]")?.eq_ignore_ascii_case(&parse_host(b"[0:0::1]")?));
57    /// # Ok::<(), parse_net::ParseError>(())
58    /// ```
59    #[must_use]
60    pub fn eq_ignore_ascii_case(&self, other: &Host<'_>) -> bool {
61        match (self, other) {
62            (Host::Name(a), Host::Name(b)) | (Host::Future(a), Host::Future(b)) => {
63                a.eq_ignore_ascii_case(b)
64            }
65            (Host::V4(a), Host::V4(b)) => a == b,
66            (Host::V6(a), Host::V6(b)) => a == b,
67            _ => false,
68        }
69    }
70
71    /// Whether this is the empty name of an empty `Host` field.
72    #[must_use]
73    pub fn is_empty(&self) -> bool {
74        matches!(self, Host::Name(n) if n.is_empty())
75    }
76
77    /// Writes the host: names as given, IPv6 bracketed in its RFC 5952 form.
78    ///
79    /// # Errors
80    ///
81    /// [`WriteError::Full`]; nothing is written when the sink reports its
82    /// room.
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// use parse_net::{host::parse_host, SliceSink};
88    ///
89    /// let mut buf = [0u8; 64];
90    /// let mut out = SliceSink::new(&mut buf);
91    /// parse_host(b"[2001:DB8:0:0:0:0:0:1]")?.write_to(&mut out).expect("room");
92    /// assert_eq!(out.written(), b"[2001:db8::1]");
93    /// # Ok::<(), parse_net::ParseError>(())
94    /// ```
95    pub fn write_to(&self, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
96        let mut count = CountSink::new();
97        self.put(&mut count)?;
98        reserve(out, count.count())?;
99        self.put(out)
100    }
101
102    fn put(&self, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
103        match self {
104            Host::Name(n) => out.put(n)?,
105            Host::V4(a) => write!(SinkWriter(out), "{a}").map_err(|_| WriteError::Full)?,
106            Host::V6(a) => write!(SinkWriter(out), "[{a}]").map_err(|_| WriteError::Full)?,
107            Host::Future(f) => {
108                out.put(b"[")?;
109                out.put(f)?;
110                out.put(b"]")?;
111            }
112        }
113        Ok(())
114    }
115}
116
117/// Lets `core::fmt` write into a [`Sink`].
118pub(crate) struct SinkWriter<'s, S: Sink>(pub(crate) &'s mut S);
119
120impl<S: Sink> fmt::Write for SinkWriter<'_, S> {
121    fn write_str(&mut self, s: &str) -> fmt::Result {
122        self.0.put(s.as_bytes()).map_err(|_| fmt::Error)
123    }
124}
125
126/// A host and an optional port.
127#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
128pub struct HostPort<'a> {
129    /// The host.
130    pub host: Host<'a>,
131    /// The port, when digits followed the `:`.
132    pub port: Option<u16>,
133    /// Whether a `:` with no digits followed the host (`example.com:`),
134    /// which RFC 3986 allows (`port = *DIGIT`).
135    pub empty_port: bool,
136}
137
138impl HostPort<'_> {
139    /// Writes the host, then `:port` when there is a port.
140    ///
141    /// # Errors
142    ///
143    /// [`WriteError::Full`]; nothing is written when the sink reports its
144    /// room.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// use parse_net::{parse_host_port, SliceSink};
150    ///
151    /// let mut buf = [0u8; 64];
152    /// let mut out = SliceSink::new(&mut buf);
153    /// parse_host_port(b"[::1]:8080")?.write_to(&mut out).expect("room");
154    /// assert_eq!(out.written(), b"[::1]:8080");
155    /// # Ok::<(), parse_net::ParseError>(())
156    /// ```
157    pub fn write_to(&self, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
158        let mut count = CountSink::new();
159        self.put(&mut count)?;
160        reserve(out, count.count())?;
161        self.put(out)
162    }
163
164    fn put(&self, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
165        self.host.put(out)?;
166        if let Some(port) = self.port {
167            out.put(b":")?;
168            crate::write::write_decimal_u64(u64::from(port), out)?;
169        }
170        Ok(())
171    }
172}
173
174/// Parses a `Host` value: `uri-host [ ":" port ]`.
175///
176/// A registered name (`unreserved / pct-encoded / sub-delims`), a
177/// dotted-decimal IPv4 address with no leading zeros, a bracketed IPv6
178/// address in any RFC 4291 text form, or a bracketed IPvFuture literal,
179/// optionally followed by `:` and a port up to 65535. An empty port is
180/// accepted with `empty_port` set. An empty value is accepted as an empty
181/// name (RFC 9112 section 3.2: the target has no authority); the caller
182/// decides what that means.
183///
184/// A name made only of digits and dots, or whose last label is a number
185/// (`127.1`, `0x7f.1`, `example.123`), must be a canonical IPv4 address:
186/// RFC 3986 reads such hosts as names and the WHATWG URL standard as IPv4
187/// addresses, and refusing them removes the disagreement.
188///
189/// # Errors
190///
191/// [`ErrorKind::InvalidHost`] for userinfo (any `@`, checked first), a byte
192/// outside the name class, a port with no host, or unbracketed IPv6;
193/// [`ErrorKind::InvalidAddress`] for a malformed IP literal or a numeric
194/// name that is not canonical IPv4; [`ErrorKind::InvalidPort`];
195/// [`ErrorKind::InvalidPercentEncoding`]; a control kind for a control byte.
196///
197/// # Examples
198///
199/// ```
200/// use parse_net::{parse_host_port, ErrorKind};
201///
202/// let hp = parse_host_port(b"example.com:8443")?;
203/// assert_eq!(hp.port, Some(8443));
204/// assert_eq!(parse_host_port(b"a:65536").unwrap_err().kind(), ErrorKind::InvalidPort);
205/// # Ok::<(), parse_net::ParseError>(())
206/// ```
207pub fn parse_host_port(value: &[u8]) -> Result<HostPort<'_>> {
208    let lead = skip_ows(value);
209    host_port(trim_ows(value)).map_err(|e| e.shifted(lead))
210}
211
212/// Parses `host [ ":" port ]` exactly as given, for a URI authority or a
213/// request target: unlike [`parse_host_port`], no OWS is stripped, so any
214/// whitespace is refused.
215///
216/// Use this wherever the bytes are not a field value: an authority-form or
217/// absolute-form request target, a URL, a `Forwarded` node. Stripping
218/// whitespace there would accept `CONNECT  example.com:443` (two spaces),
219/// which other parsers refuse.
220///
221/// # Errors
222///
223/// As [`parse_host_port`]; whitespace is refused like any other byte outside
224/// the grammar ([`ErrorKind::InvalidHost`] in the host, [`ErrorKind::InvalidPort`]
225/// after a port).
226///
227/// # Examples
228///
229/// ```
230/// use parse_net::{host::parse_uri_host_port, parse_host_port};
231///
232/// assert!(parse_host_port(b" example.com:443").is_ok());
233/// assert!(parse_uri_host_port(b" example.com:443").is_err());
234/// assert_eq!(parse_uri_host_port(b"example.com:443")?.port, Some(443));
235/// # Ok::<(), parse_net::ParseError>(())
236/// ```
237pub fn parse_uri_host_port(value: &[u8]) -> Result<HostPort<'_>> {
238    host_port(value)
239}
240
241/// Parses a host with no port.
242///
243/// # Errors
244///
245/// As [`parse_host_port`], and [`ErrorKind::InvalidHost`] for a port.
246///
247/// # Examples
248///
249/// ```
250/// use parse_net::{host::parse_host, Host};
251///
252/// assert_eq!(parse_host(b"192.0.2.1")?, Host::V4([192, 0, 2, 1].into()));
253/// assert!(parse_host(b"a.example:80").is_err());
254/// # Ok::<(), parse_net::ParseError>(())
255/// ```
256pub fn parse_host(value: &[u8]) -> Result<Host<'_>> {
257    let hp = parse_host_port(value)?;
258    if hp.port.is_some() || hp.empty_port {
259        let at = skip_ows(value) + value.iter().rposition(|&b| b == b':').unwrap_or(0);
260        return err(ErrorKind::InvalidHost, at.min(value.len()));
261    }
262    Ok(hp.host)
263}
264
265pub(crate) fn host_port(v: &[u8]) -> Result<HostPort<'_>> {
266    host_port_inner(v).map_err(|e| userinfo_first(v, e))
267}
268
269/// Userinfo is the attack (`a.com:80@b.com`): whatever else is wrong with the
270/// value, an `@` anywhere is reported as `InvalidHost` at the `@`.
271#[cold]
272fn userinfo_first(v: &[u8], e: crate::ParseError) -> crate::ParseError {
273    match find_byte(b'@', v) {
274        Some(at) => crate::ParseError::new(ErrorKind::InvalidHost, at),
275        None => e,
276    }
277}
278
279fn host_port_inner(v: &[u8]) -> Result<HostPort<'_>> {
280    let (host, rest, rest_at) = if v.first() == Some(&b'[') {
281        let Some(close) = find_byte(b']', v) else {
282            return err(ErrorKind::InvalidAddress, v.len());
283        };
284        let inner = v.get(1..close).unwrap_or(&[]);
285        (
286            ip_literal(inner)?,
287            v.get(close + 1..).unwrap_or(&[]),
288            close + 1,
289        )
290    } else {
291        let (host, colon) = reg_name_or_ipv4(v)?;
292        let rest = v.get(colon..).unwrap_or(&[]);
293        if colon == 0 && !rest.is_empty() {
294            return err(ErrorKind::InvalidHost, 0);
295        }
296        (host, rest, colon)
297    };
298    let (port, empty_port) = match rest.split_first() {
299        None => (None, false),
300        Some((b':', digits)) => port(digits).map_err(|e| e.shifted(rest_at + 1))?,
301        Some(_) => return err(ErrorKind::InvalidHost, rest_at),
302    };
303    Ok(HostPort {
304        host,
305        port,
306        empty_port,
307    })
308}
309
310fn port(digits: &[u8]) -> Result<(Option<u16>, bool)> {
311    if digits.is_empty() {
312        return Ok((None, true));
313    }
314    let mut n: u32 = 0;
315    for (i, &d) in digits.iter().enumerate() {
316        if !d.is_ascii_digit() {
317            // A second ":" means unbracketed IPv6.
318            let kind = if d == b':' {
319                ErrorKind::InvalidHost
320            } else {
321                ErrorKind::InvalidPort
322            };
323            return err(kind, i);
324        }
325        n = n * 10 + u32::from(d - b'0');
326        if n > 65_535 {
327            return err(ErrorKind::InvalidPort, i);
328        }
329    }
330    // `n` is at most 65535.
331    Ok((u16::try_from(n).ok(), false))
332}
333
334fn ip_literal(inner: &[u8]) -> Result<Host<'_>> {
335    // Offsets inside the brackets are reported from the "[".
336    match inner.first() {
337        Some(b'v' | b'V') => {
338            // IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
339            let rest = inner.get(1..).unwrap_or(&[]);
340            let hex = rest.iter().take_while(|&&b| class::is_hexdig(b)).count();
341            let after = rest.get(hex..).unwrap_or(&[]);
342            let tail_ok = match after.split_first() {
343                Some((b'.', tail)) => {
344                    !tail.is_empty()
345                        && tail.iter().all(|&b| {
346                            class::is_unreserved(b) || class::is_sub_delim(b) || b == b':'
347                        })
348                }
349                _ => false,
350            };
351            if hex == 0 || !tail_ok {
352                return err(ErrorKind::InvalidAddress, 1);
353            }
354            Ok(Host::Future(inner))
355        }
356        _ => {
357            let text = core::str::from_utf8(inner)
358                .map_err(|_| crate::ParseError::new(ErrorKind::InvalidAddress, 1))?;
359            match Ipv6Addr::from_str(text) {
360                Ok(a) => Ok(Host::V6(a)),
361                Err(_) => err(ErrorKind::InvalidAddress, 1),
362            }
363        }
364    }
365}
366
367/// Whether `label` is a number in the WHATWG sense: decimal digits, or `0x`
368/// followed by hex digits.
369fn is_numeric_label(label: &[u8]) -> bool {
370    match label {
371        [] => false,
372        [b'0', b'x' | b'X', hex @ ..] => hex.iter().all(|&b| class::is_hexdig(b)),
373        _ => label.iter().all(u8::is_ascii_digit),
374    }
375}
376
377/// `unreserved / sub-delims`: the reg-name bytes other than `pct-encoded`.
378const REG_NAME: class::Class = class::Class::UNRESERVED.union(class::Class::SUB_DELIM);
379
380/// Reads the name at the start of `v` up to a `:` or the end, in one pass:
381/// validates it as a reg-name and notes whether it could be read as IPv4.
382/// Returns the host and the offset where the name ended.
383fn reg_name_or_ipv4(v: &[u8]) -> Result<(Host<'_>, usize)> {
384    let mut i = 0usize;
385    let mut digits_dots = true;
386    let mut label_start = 0usize;
387    let mut prev_label_start = 0usize;
388    while let Some(&b) = v.get(i) {
389        if REG_NAME.contains(b) {
390            if b == b'.' {
391                prev_label_start = label_start;
392                label_start = i + 1;
393            } else if !b.is_ascii_digit() {
394                digits_dots = false;
395            }
396            i += 1;
397        } else if b == b':' {
398            break;
399        } else if b == b'%' {
400            match (v.get(i + 1), v.get(i + 2)) {
401                (Some(&h), Some(&l)) if class::is_hexdig(h) && class::is_hexdig(l) => i += 3,
402                _ => return err(ErrorKind::InvalidPercentEncoding, i),
403            }
404            digits_dots = false;
405        } else if crate::scan::is_control(b) {
406            return err(control_kind(b), i);
407        } else {
408            return err(ErrorKind::InvalidHost, i);
409        }
410    }
411    let name = v.get(..i).unwrap_or(&[]);
412    // The last label, ignoring one trailing dot.
413    let last = if name.last() == Some(&b'.') {
414        name.get(prev_label_start..i - 1).unwrap_or(&[])
415    } else {
416        name.get(label_start..).unwrap_or(&[])
417    };
418    if (digits_dots && !name.is_empty()) || is_numeric_label(last) {
419        return match ipv4(name) {
420            Some(a) => Ok((Host::V4(a), i)),
421            None => err(ErrorKind::InvalidAddress, 0),
422        };
423    }
424    Ok((Host::Name(name), i))
425}
426
427/// Canonical dotted decimal: four parts, 0 to 255, no leading zeros.
428fn ipv4(s: &[u8]) -> Option<Ipv4Addr> {
429    let mut octets = [0u8; 4];
430    let mut parts = s.split(|&b| b == b'.');
431    for slot in &mut octets {
432        let part = parts.next()?;
433        if part.is_empty() || part.len() > 3 || (part.len() > 1 && part.first() == Some(&b'0')) {
434            return None;
435        }
436        let mut n: u16 = 0;
437        for &d in part {
438            if !d.is_ascii_digit() {
439                return None;
440            }
441            n = n * 10 + u16::from(d - b'0');
442        }
443        *slot = u8::try_from(n).ok()?;
444    }
445    if parts.next().is_some() {
446        return None;
447    }
448    Some(Ipv4Addr::from(octets))
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    fn hp(v: &[u8]) -> Result<HostPort<'_>> {
456        parse_host_port(v)
457    }
458
459    #[test]
460    fn test_host_edge_cases() {
461        use ErrorKind::*;
462        assert!(hp(b"").unwrap().host.is_empty());
463        assert_eq!(hp(b"a").unwrap().host, Host::Name(b"a"));
464        let e = hp(b"a:").unwrap();
465        assert_eq!((e.port, e.empty_port), (None, true));
466        assert_eq!(hp(b"a:0").unwrap().port, Some(0));
467        assert_eq!(hp(b"a:65535").unwrap().port, Some(65535));
468        assert_eq!(hp(b"a:65536").unwrap_err().kind(), InvalidPort);
469        assert_eq!(hp(b"a:-1").unwrap_err().kind(), InvalidPort);
470        assert_eq!(hp(b"a:8x").unwrap_err().kind(), InvalidPort);
471        assert_eq!(hp(b"[::1]").unwrap().host, Host::V6(Ipv6Addr::LOCALHOST));
472        assert_eq!(hp(b"[::1]:80").unwrap().port, Some(80));
473        assert_eq!(hp(b"[::1").unwrap_err().kind(), InvalidAddress);
474        assert_eq!(hp(b"[zz]").unwrap_err().kind(), InvalidAddress);
475        assert_eq!(hp(b"[]").unwrap_err().kind(), InvalidAddress);
476        assert_eq!(hp(b"::1").unwrap_err().kind(), InvalidHost);
477        assert_eq!(hp(b"a:1:2").unwrap_err().kind(), InvalidHost);
478        assert_eq!(hp(b"[v1.x]").unwrap().host, Host::Future(b"v1.x"));
479        assert_eq!(hp(b"[v.x]").unwrap_err().kind(), InvalidAddress);
480        assert_eq!(hp(b"[v1.]").unwrap_err().kind(), InvalidAddress);
481        assert_eq!(
482            hp(b"1.2.3.4").unwrap().host,
483            Host::V4(Ipv4Addr::new(1, 2, 3, 4))
484        );
485        assert_eq!(hp(b"[::1]x").unwrap_err().kind(), InvalidHost);
486        assert_eq!(hp(b"%41.com").unwrap().host, Host::Name(b"%41.com"));
487        assert_eq!(hp(b"%4.com").unwrap_err().kind(), InvalidPercentEncoding);
488        assert_eq!(hp(b"a b").unwrap_err().kind(), InvalidHost);
489        assert_eq!(hp(b"a/b").unwrap_err().kind(), InvalidHost);
490        assert_eq!(
491            hp(b"example.com.").unwrap().host,
492            Host::Name(b"example.com.")
493        );
494        assert_eq!(hp(b":80").unwrap_err().kind(), InvalidHost);
495        assert_eq!(hp(b"a\rb").unwrap_err().kind(), BareCr);
496    }
497
498    #[test]
499    fn test_host_partial_ipv4_refused() {
500        for v in [
501            &b"1.2.3"[..],
502            b"01.2.3.4",
503            b"256.0.0.1",
504            b"127.1",
505            b"0x7f.0.0.1",
506            b"1.2.3.4.",
507            b"example.123",
508            b"a.0x10",
509            b"1..2.3",
510        ] {
511            assert_eq!(
512                hp(v).unwrap_err().kind(),
513                ErrorKind::InvalidAddress,
514                "{}",
515                std::string::String::from_utf8_lossy(v)
516            );
517        }
518        assert_eq!(hp(b"123.example").unwrap().host, Host::Name(b"123.example"));
519        assert_eq!(
520            hp(b"0.0.0.0").unwrap().host,
521            Host::V4(Ipv4Addr::UNSPECIFIED)
522        );
523    }
524
525    #[test]
526    fn test_host_userinfo_refused() {
527        let e = hp(b"a.com:80@b.com").unwrap_err();
528        assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidHost, 8));
529        assert_eq!(
530            hp(b"user@b.com").unwrap_err().kind(),
531            ErrorKind::InvalidHost
532        );
533        assert_eq!(hp(b"[::1]@x").unwrap_err().kind(), ErrorKind::InvalidHost);
534    }
535
536    #[test]
537    fn test_ipv6_text_forms() {
538        for v in [
539            &b"[::]"[..],
540            b"[::1]",
541            b"[2001:db8::1]",
542            b"[2001:DB8:0:0:8:800:200C:417A]",
543            b"[::ffff:192.0.2.1]",
544            b"[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]",
545            b"[1::]",
546        ] {
547            assert!(
548                matches!(hp(v).unwrap().host, Host::V6(_)),
549                "{}",
550                std::string::String::from_utf8_lossy(v)
551            );
552        }
553        for v in [
554            &b"[1:2:3:4:5:6:7:8:9]"[..],
555            b"[12345::]",
556            b"[::1%25eth0]",
557            b"[:::]",
558            b"[1::2::3]",
559        ] {
560            assert_eq!(hp(v).unwrap_err().kind(), ErrorKind::InvalidAddress);
561        }
562    }
563
564    #[test]
565    fn test_host_compare_and_write() {
566        assert!(Host::V6(Ipv6Addr::LOCALHOST).eq_ignore_ascii_case(&hp(b"[0:0::1]").unwrap().host));
567        assert!(!Host::Name(b"a").eq_ignore_ascii_case(&Host::V4(Ipv4Addr::LOCALHOST)));
568        let mut buf = [0u8; 64];
569        let mut out = crate::SliceSink::new(&mut buf);
570        hp(b"1.2.3.4:5").unwrap().write_to(&mut out).unwrap();
571        assert_eq!(out.written(), b"1.2.3.4:5");
572        let mut small = [0u8; 3];
573        let mut out = crate::SliceSink::new(&mut small);
574        assert_eq!(
575            hp(b"[::1]").unwrap().write_to(&mut out),
576            Err(WriteError::Full)
577        );
578        assert!(out.is_empty());
579        assert_eq!(
580            parse_host(b"a:").unwrap_err().kind(),
581            ErrorKind::InvalidHost
582        );
583    }
584}