Skip to main content

fuchsia_hyper/
lib.rs

1// Copyright 2018 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use futures::future::{Future, FutureExt};
6use futures::io::{self, AsyncRead, AsyncWrite};
7use futures::task::{Context, Poll};
8use hyper::body::Body;
9use hyper_util::client::legacy::Client;
10use hyper_util::client::legacy::connect::{Connected, Connection};
11use hyper_util::rt::TokioIo;
12#[cfg(not(target_os = "fuchsia"))]
13use netext::MultithreadedTokioAsyncWrapper;
14use std::marker::PhantomData;
15use std::net::{AddrParseError, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
16use std::num::ParseIntError;
17use std::pin::Pin;
18use tokio::io::ReadBuf;
19
20#[cfg(not(target_os = "fuchsia"))]
21use tokio::net;
22
23#[cfg(target_os = "fuchsia")]
24use fidl_fuchsia_posix_socket as fposix_socket;
25
26#[cfg(target_os = "fuchsia")]
27use fuchsia_async::net;
28
29#[cfg(not(target_os = "fuchsia"))]
30mod not_fuchsia;
31#[cfg(not(target_os = "fuchsia"))]
32pub use not_fuchsia::*;
33
34#[cfg(target_os = "fuchsia")]
35mod fuchsia;
36#[cfg(target_os = "fuchsia")]
37pub use crate::fuchsia::*;
38
39#[cfg(target_os = "fuchsia")]
40mod happy_eyeballs;
41
42/// A Fuchsia-compatible hyper client configured for making HTTP requests.
43pub type HttpClient<B = http_body_util::Full<hyper::body::Bytes>> = Client<HyperConnector, B>;
44
45/// A Fuchsia-compatible hyper client configured for making HTTP and HTTPS requests.
46pub type HttpsClient<B = http_body_util::Full<hyper::body::Bytes>> =
47    Client<hyper_rustls::HttpsConnector<HyperConnector>, B>;
48
49/// A trait to implement a builder for a Fuchsia compatible hyper client
50/// configured for either only HTTP or both HTTP and HTTPS requests.
51pub trait MakeClientBuilder: Sized {
52    fn builder() -> HttpClientBuilder<Self> {
53        HttpClientBuilder::default()
54    }
55}
56
57impl<B: Body + Send> MakeClientBuilder for HttpClient<B> {}
58impl<B: Body + Send> MakeClientBuilder for HttpsClient<B> {}
59
60/// A future that yields a hyper-compatible TCP stream.
61#[must_use = "futures do nothing unless polled"]
62pub struct HyperConnectorFuture {
63    // FIXME(https://github.com/rust-lang/rust/issues/63063): We should be able to remove this
64    // `Box` once rust allows impl Traits in type aliases.
65    fut: Pin<Box<dyn Future<Output = Result<TokioIo<TcpStream>, io::Error>> + Send>>,
66}
67
68impl Future for HyperConnectorFuture {
69    type Output = Result<TokioIo<TcpStream>, io::Error>;
70
71    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
72        self.fut.as_mut().poll(cx)
73    }
74}
75
76pub struct TcpStream {
77    #[cfg(target_os = "fuchsia")]
78    pub stream: net::TcpStream,
79    #[cfg(not(target_os = "fuchsia"))]
80    pub stream: MultithreadedTokioAsyncWrapper<net::TcpStream>,
81}
82
83impl tokio::io::AsyncRead for TcpStream {
84    fn poll_read(
85        mut self: Pin<&mut Self>,
86        cx: &mut Context<'_>,
87        buf: &mut ReadBuf<'_>,
88    ) -> Poll<io::Result<()>> {
89        Pin::new(&mut self.stream).poll_read(cx, buf.initialize_unfilled()).map_ok(|sz| {
90            buf.advance(sz);
91            ()
92        })
93    }
94
95    // TODO: override poll_read_buf and call readv on the underlying stream
96}
97
98impl tokio::io::AsyncWrite for TcpStream {
99    fn poll_write(
100        mut self: Pin<&mut Self>,
101        cx: &mut Context<'_>,
102        buf: &[u8],
103    ) -> Poll<io::Result<usize>> {
104        Pin::new(&mut self.stream).poll_write(cx, buf)
105    }
106
107    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
108        Pin::new(&mut self.get_mut().stream).poll_flush(cx)
109    }
110
111    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
112        Pin::new(&mut self.get_mut().stream).poll_close(cx)
113    }
114
115    // TODO: override poll_write_buf and call writev on the underlying stream
116}
117
118impl Connection for TcpStream {
119    fn connected(&self) -> Connected {
120        Connected::new()
121    }
122}
123
124#[non_exhaustive]
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
126/// A container of TCP settings to be applied to the sockets created by the hyper client.
127pub struct TcpOptions {
128    /// This sets TCP_KEEPIDLE and SO_KEEPALIVE.
129    pub keepalive_idle: Option<std::time::Duration>,
130    /// This sets TCP_KEEPINTVL and SO_KEEPALIVE.
131    pub keepalive_interval: Option<std::time::Duration>,
132    /// This sets TCP_KEEPCNT and SO_KEEPALIVE.
133    pub keepalive_count: Option<u32>,
134    /// This sets SO_RCVBUF on the TCP socket.
135    pub tcp_receive_buffer_size: Option<usize>,
136}
137
138impl TcpOptions {
139    /// keepalive_timeout returns a TCP keepalive policy that times out after the specified
140    /// duration. The keepalive policy returned waits for half of the supplied duration before
141    /// sending keepalive packets, and attempts to keep the connection alive three times for
142    /// the remaining period.
143    ///
144    /// If the supplied duration does not contain at least one whole second, no TCP keepalive
145    /// policy is returned.
146    pub fn keepalive_timeout(dur: std::time::Duration) -> Self {
147        if dur.as_secs() == 0 {
148            return TcpOptions::default();
149        }
150
151        TcpOptions {
152            keepalive_idle: dur.checked_div(2),
153            keepalive_interval: dur.checked_div(6),
154            keepalive_count: Some(3),
155            tcp_receive_buffer_size: None,
156        }
157    }
158
159    pub(crate) fn apply<T: std::os::fd::AsFd>(&self, stream: &T) -> io::Result<()> {
160        let stream = socket2::SockRef::from(stream);
161        let mut any = false;
162        let mut keepalive = socket2::TcpKeepalive::new();
163        if let Some(idle) = self.keepalive_idle {
164            any = true;
165            keepalive = keepalive.with_time(idle);
166        };
167        if let Some(interval) = self.keepalive_interval {
168            any = true;
169            keepalive = keepalive.with_interval(interval);
170        }
171        if let Some(count) = self.keepalive_count {
172            any = true;
173            keepalive = keepalive.with_retries(count);
174        }
175        if any {
176            stream.set_tcp_keepalive(&keepalive)?;
177        }
178        if let Some(size) = self.tcp_receive_buffer_size {
179            stream.set_recv_buffer_size(size)?;
180        }
181        Ok(())
182    }
183}
184
185/// Extra socket options to ensure that requests are made through a particular
186/// device or over a particular IP domain.
187#[derive(Clone, Debug, Default)]
188pub struct SocketOptions {
189    /// Specifies, as a string, the device that the created socket should bind to.
190    pub bind_device: Option<String>,
191}
192
193#[derive(Clone)]
194pub struct Executor;
195
196impl<F: Future + Send + 'static> hyper::rt::Executor<F> for Executor {
197    fn execute(&self, fut: F) {
198        fuchsia_async::Task::spawn(fut.map(|_| ())).detach()
199    }
200}
201
202#[derive(Clone)]
203pub struct LocalExecutor;
204
205/// Implements the Builder pattern for constructing HTTP or HTTPS clients.
206#[derive(Clone)]
207pub struct HttpClientBuilder<T> {
208    tcp_options: Option<TcpOptions>,
209    socket_options: Option<SocketOptions>,
210    tls: Option<rustls::ClientConfig>,
211    phantom: PhantomData<T>,
212}
213
214impl<T> Default for HttpClientBuilder<T> {
215    fn default() -> Self {
216        Self {
217            tcp_options: Default::default(),
218            socket_options: Default::default(),
219            tls: Default::default(),
220            phantom: Default::default(),
221        }
222    }
223}
224
225impl<B: Body + Send> HttpClientBuilder<HttpClient<B>>
226where
227    B::Data: Send,
228{
229    /// Constructs an HttpClient
230    pub fn build(mut self) -> HttpClient<B> {
231        Client::builder(Executor).build(self.connector())
232    }
233}
234
235impl<B: Body + Send> HttpClientBuilder<HttpsClient<B>>
236where
237    B::Data: Send,
238{
239    /// Constructs an HttpsClient
240    pub fn build(mut self) -> HttpsClient<B> {
241        let https = hyper_rustls::HttpsConnector::from((
242            self.connector(),
243            self.tls.unwrap_or_else(|| {
244                let root_store = new_root_cert_store();
245                rustls::ClientConfig::builder()
246                    .with_root_certificates(root_store)
247                    .with_no_client_auth()
248            }),
249        ));
250        Client::builder(Executor).build(https)
251    }
252
253    /// Overrides the default tls `ClientConfig`
254    pub fn tls(self, tls: rustls::ClientConfig) -> Self {
255        Self { tls: Some(tls), ..self }
256    }
257}
258
259impl<T> HttpClientBuilder<T> {
260    fn connector(&mut self) -> HyperConnector {
261        HyperConnector::from((
262            self.tcp_options.take().unwrap_or_default(),
263            self.socket_options.take().unwrap_or_default(),
264        ))
265    }
266
267    /// Sets the TCP options for the underlying HyperConnector
268    pub fn tcp_options(self, tcp_options: TcpOptions) -> Self {
269        Self { tcp_options: Some(tcp_options), ..self }
270    }
271
272    /// Sets the SocketOptions for the underlying HyperConnector
273    pub fn socket_options(self, socket_options: SocketOptions) -> Self {
274        Self { socket_options: Some(socket_options), ..self }
275    }
276}
277
278impl<F: Future + 'static> hyper::rt::Executor<F> for LocalExecutor {
279    fn execute(&self, fut: F) {
280        fuchsia_async::Task::local(fut.map(drop)).detach()
281    }
282}
283
284/// Returns a new Fuchsia-compatible hyper client for making HTTP requests.
285pub fn new_client() -> HttpClient {
286    HttpClient::builder().build()
287}
288
289pub fn new_https_client_dangerous(
290    tls: rustls::ClientConfig,
291    tcp_options: TcpOptions,
292) -> HttpsClient {
293    HttpsClient::builder().tls(tls).tcp_options(tcp_options).build()
294}
295
296/// Returns a new Fuchsia-compatible hyper client for making HTTP and HTTPS requests.
297pub fn new_https_client_from_tcp_options(tcp_options: TcpOptions) -> HttpsClient {
298    HttpsClient::builder().tcp_options(tcp_options).build()
299}
300
301/// Returns a new Fuchsia-compatible hyper client for making HTTP and HTTPS requests.
302pub fn new_https_client() -> HttpsClient {
303    HttpsClient::builder().build()
304}
305
306pub(crate) async fn parse_ip_addr<'a, F, Fut>(
307    host: &'a str,
308    port: u16,
309    interface_name_to_index: F,
310) -> Result<Option<SocketAddr>, io::Error>
311where
312    F: Fn(&'a str) -> Fut + 'a,
313    Fut: Future<Output = Result<u32, io::Error>> + 'a,
314{
315    match host.parse::<Ipv4Addr>() {
316        Ok(addr) => {
317            return Ok(Some(SocketAddr::V4(SocketAddrV4::new(addr, port))));
318        }
319        Err(AddrParseError { .. }) => {}
320    }
321
322    // IPv6 literals are always enclosed in [].
323    if !host.starts_with("[") || !host.ends_with(']') {
324        return Ok(None);
325    }
326
327    let host = &host[1..host.len() - 1];
328
329    // IPv6 addresses with zones always contain "%25", which is "%" URL encoded.
330    let (host, zone_id) = if let Some((host, zone_id)) = host.split_once("%25") {
331        (host, Some(zone_id))
332    } else {
333        (host, None)
334    };
335
336    let addr = match host.parse::<Ipv6Addr>() {
337        Ok(addr) => addr,
338        Err(AddrParseError { .. }) => {
339            return Ok(None);
340        }
341    };
342
343    let scope_id = if let Some(zone_id) = zone_id {
344        // rfc6874 section 4 states:
345        //
346        //     The security considerations from the URI syntax specification
347        //     [RFC3986] and the IPv6 Scoped Address Architecture specification
348        //     [RFC4007] apply.  In particular, this URI format creates a specific
349        //     pathway by which a deceitful zone index might be communicated, as
350        //     mentioned in the final security consideration of the Scoped Address
351        //     Architecture specification.  It is emphasised that the format is
352        //     intended only for debugging purposes, but of course this intention
353        //     does not prevent misuse.
354        //
355        //     To limit this risk, implementations MUST NOT allow use of this format
356        //     except for well-defined usages, such as sending to link-local
357        //     addresses under prefix fe80::/10.  At the time of writing, this is
358        //     the only well-defined usage known.
359        //
360        // Since the only known use-case of IPv6 Zone Identifiers on Fuchsia is to communicate
361        // with link-local devices, restrict addresses to link-local zone identifiers.
362        //
363        // TODO: use Ipv6Addr::is_unicast_link_local_strict when available in stable rust.
364        if addr.segments()[..4] != [0xfe80, 0, 0, 0] {
365            return Err(io::Error::other("zone_id is only usable with link local addresses"));
366        }
367
368        // TODO: validate that the value matches rfc6874 grammar `ZoneID = 1*( unreserved / pct-encoded )`.
369        match zone_id.parse::<u32>() {
370            Ok(scope_id) => scope_id,
371            Err(ParseIntError { .. }) => interface_name_to_index(zone_id).await?,
372        }
373    } else {
374        0
375    };
376
377    Ok(Some(SocketAddr::V6(SocketAddrV6::new(addr, port, 0, scope_id))))
378}
379
380#[cfg(target_os = "fuchsia")]
381pub(crate) fn connect_and_bind_device<D: AsRef<[u8]>, T: ProviderConnector>(
382    provider: &T,
383    addr: SocketAddr,
384    bind_device: Option<D>,
385) -> io::Result<net::TcpConnector> {
386    // TODO(https://fxbug.dev/477371935): Specify a network to use to
387    // route the socket traffic using marks rather than bind_to_device.
388    let socket = stream_socket(
389        provider,
390        match addr {
391            SocketAddr::V4(_) => fposix_socket::Domain::Ipv4,
392            SocketAddr::V6(_) => fposix_socket::Domain::Ipv6,
393        },
394        fposix_socket::StreamSocketProtocol::Tcp,
395    )?;
396    if let Some(bind_device) = bind_device {
397        socket.bind_device(Some(bind_device.as_ref()))?;
398    }
399    net::TcpStream::connect_from_raw(socket, addr)
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405    use assert_matches::assert_matches;
406    use fuchsia_async::{self as fasync};
407
408    async fn unsupported(_name: &str) -> Result<u32, io::Error> {
409        panic!("should not have happened")
410    }
411
412    #[fasync::run_singlethreaded(test)]
413    async fn test_parse_ipv4_addr() {
414        let expected = "1.2.3.4:8080".parse::<SocketAddr>().unwrap();
415        assert_matches!(
416            parse_ip_addr("1.2.3.4", 8080, unsupported).await,
417            Ok(Some(addr)) if addr == expected);
418    }
419
420    #[fasync::run_singlethreaded(test)]
421    async fn test_parse_invalid_addresses() {
422        assert_matches!(parse_ip_addr("1.2.3", 8080, unsupported).await, Ok(None));
423        assert_matches!(parse_ip_addr("1.2.3.4.5", 8080, unsupported).await, Ok(None));
424        assert_matches!(parse_ip_addr("localhost", 8080, unsupported).await, Ok(None));
425        assert_matches!(parse_ip_addr("[fe80::1:2:3:4", 8080, unsupported).await, Ok(None));
426        assert_matches!(parse_ip_addr("[[fe80::1:2:3:4]", 8080, unsupported).await, Ok(None));
427        assert_matches!(parse_ip_addr("[]", 8080, unsupported).await, Ok(None));
428    }
429
430    #[fasync::run_singlethreaded(test)]
431    async fn test_parse_ipv6_addr() {
432        let expected = "[fe80::1:2:3:4]:8080".parse::<SocketAddr>().unwrap();
433        assert_matches!(
434            parse_ip_addr("[fe80::1:2:3:4]", 8080, unsupported).await,
435            Ok(Some(addr)) if addr == expected
436        );
437    }
438
439    #[fasync::run_singlethreaded(test)]
440    async fn test_parse_ipv6_addr_with_zone_must_be_local() {
441        assert_matches!(
442            parse_ip_addr("[fe81::1:2:3:4%252]", 8080, unsupported).await,
443            Err(err) if err.kind() == io::ErrorKind::Other
444        );
445    }
446}