diff --git a/Cargo.toml b/Cargo.toml index 0f6e314911..4610ec925f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ iovec = "0.1" itoa = "0.4.1" log = "0.4" net2 = { version = "0.2.32", optional = true } +percent-encoding = "2.1.0" pin-project = { version = "=0.4.0-alpha.11", features = ["project_attr"] } time = "0.1" tokio = { version = "=0.2.0-alpha.4", optional = true, default-features = false, features = ["rt-full"] } @@ -193,3 +194,6 @@ required-features = ["runtime", "stream"] name = "server" path = "tests/server.rs" required-features = ["runtime", "stream"] + +[patch.crates-io] +http = { git = "https://github.com/ComputerDruid/http", branch = "zone" } diff --git a/src/client/connect/dns.rs b/src/client/connect/dns.rs index 7776716674..d62cc82e2b 100644 --- a/src/client/connect/dns.rs +++ b/src/client/connect/dns.rs @@ -9,7 +9,7 @@ use std::{fmt, io, vec}; use std::error::Error; use std::net::{ - IpAddr, Ipv4Addr, Ipv6Addr, + Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs, SocketAddrV4, SocketAddrV6, }; @@ -22,7 +22,7 @@ use crate::common::{Future, Never, Pin, Poll, Unpin, task}; /// Resolve a hostname to a set of IP addresses. pub trait Resolve: Unpin { /// The set of IP addresses to try to connect to. - type Addrs: Iterator; + type Addrs: Iterator; /// A Future of the resolved set of addresses. type Future: Future> + Unpin; /// Resolve a hostname. @@ -151,10 +151,10 @@ impl fmt::Debug for GaiFuture { } impl Iterator for GaiAddrs { - type Item = IpAddr; + type Item = SocketAddr; fn next(&mut self) -> Option { - self.inner.next().map(|sa| sa.ip()) + self.inner.next() } } @@ -245,6 +245,57 @@ impl Iterator for IpAddrs { } } +#[derive(Debug)] +pub(super) struct Ipv6AddrWithZone { + pub(super) partial_addr: SocketAddrV6, + pub(super) as_str: String, +} + +impl Ipv6AddrWithZone { + pub(super) fn try_parse(host: &str, port: u16) -> Option { + // IpV6 literals are always in [] + if !host.starts_with("[") || !host.ends_with(']') { + return None; + } + let host = &host[1..host.len()-1]; + + let host = host.trim_start_matches('[').trim_end_matches(']'); + + // an IpV6AddrWithZone always contains "%25" + let mut host_parts = host.splitn(2, "%25"); + let addr_part = match host_parts.next() { + Some(part) => part, + None => {return None;}, + }; + let _zone_part = match host_parts.next() { + Some(part) => part, + None => {return None;}, + }; + + // an IpV6AddrWithZone is an Ipv6Addr before the %25 + let ipv6_addr = match addr_part.parse::() { + Ok(addr) => addr, + Err(_) => {return None;}, + }; + + // rfc6874 says you should only allow zone identifiers on link-local addresses. + // TODO: use Ipv6Addr::is_unicast_link_local_strict when available in stable rust. + if ipv6_addr.segments()[..4] != [0xfe80, 0, 0, 0] { + return None; + } + + let partial_addr = SocketAddrV6::new(ipv6_addr, port, 0, 0); + let as_str = match percent_encoding::percent_decode_str(host).decode_utf8() { + Ok(s) => s, + Err(_) => {return None;}, + }.into_owned(); + Some(Self { + partial_addr, + as_str, + }) + } +} + /// A resolver using `getaddrinfo` calls via the `tokio_executor::threadpool::blocking` API. /// /// Unlike the `GaiResolver` this will not spawn dedicated threads, but only works when running on the @@ -333,4 +384,34 @@ mod tests { assert_eq!(addrs.next(), Some(expected)); } + + #[test] + fn ipv6_addr_with_zone_try_parse() { + let uri = ::http::Uri::from_static("http://[fe80::1:2:3:4%25eth0]:8080/"); + let dst = super::super::Destination { uri }; + + let addr = Ipv6AddrWithZone::try_parse( + dst.host(), + dst.port().expect("port") + ).expect("try_parse"); + + assert_eq!(addr.as_str, "fe80::1:2:3:4%eth0"); + + let expected = "[fe80::1:2:3:4]:8080".parse::().expect("expected"); + + assert_eq!(addr.partial_addr, expected); + } + + #[test] + fn ipv6_addr_with_zone_try_parse_not_link_local() { + let uri = ::http::Uri::from_static("http://[::1%25eth0]:8080/"); + let dst = super::super::Destination { uri }; + + let addr = Ipv6AddrWithZone::try_parse( + dst.host(), + dst.port().expect("port") + ); + + assert_eq!(addr.map(|a| format!("{:?}", a)), None); + } } diff --git a/src/client/connect/http.rs b/src/client/connect/http.rs index eac06f97d6..e14986ef2d 100644 --- a/src/client/connect/http.rs +++ b/src/client/connect/http.rs @@ -388,6 +388,11 @@ where if let Some(addrs) = dns::IpAddrs::try_parse(host, me.port) { state = State::Connecting(ConnectingTcp::new( local_addr, addrs, me.happy_eyeballs_timeout, me.reuse_address)); + } else if let Some(addr_with_zone) = dns::Ipv6AddrWithZone::try_parse(host, me.port) { + // As a special case, use the resolver to resolve the Ipv6AddrWithZone to + // look up the correct SocketAddrV6::scope_id. + let name = dns::Name::new(addr_with_zone.as_str); + state = State::Resolving(resolver.resolve(name), local_addr); } else { let name = dns::Name::new(mem::replace(host, String::new())); state = State::Resolving(resolver.resolve(name), local_addr); @@ -397,7 +402,10 @@ where let addrs = ready!(Pin::new(future).poll(cx))?; let port = me.port; let addrs = addrs - .map(|addr| SocketAddr::new(addr, port)) + .map(|mut addr| { + addr.set_port(port); + addr + }) .collect(); let addrs = dns::IpAddrs::new(addrs); state = State::Connecting(ConnectingTcp::new(