Skip to main content

omaha_client_fuchsia/
http_request.rs

1// Copyright 2023 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 fidl_fuchsia_mem as fmem;
6use fidl_fuchsia_net_http as fnet_http;
7use fuchsia_async::{self as fasync, TimeoutExt as _};
8use futures::future::BoxFuture;
9use futures::prelude::*;
10use omaha_client::http_request::{Body, Error, HttpRequest, Request, Response};
11use std::time::Duration;
12
13const MAX_RESPONSE_BODY_SIZE: usize = 1 * 1024 * 1024;
14
15#[derive(Debug, thiserror::Error)]
16enum LoaderRequestError {
17    #[error("failed to connect to fuchsia.net.http.Loader protocol")]
18    ConnectToProtocol(#[source] anyhow::Error),
19    #[error("failed to create VMO for request body")]
20    CreateVmo(#[source] zx::Status),
21    #[error("failed to write request body to VMO")]
22    WriteVmo(#[source] zx::Status),
23    #[error("FIDL error calling Loader.Fetch")]
24    Fidl(#[source] fidl::Error),
25    #[error("Loader returned network error: {0:?}")]
26    Loader(fnet_http::Error),
27    #[error("Loader response missing status code")]
28    MissingStatusCode,
29    #[error("invalid HTTP status code: {0}")]
30    InvalidStatusCode(u32),
31    #[error("invalid HTTP header name")]
32    InvalidHeaderName(#[source] http::header::InvalidHeaderName),
33    #[error("invalid HTTP header value")]
34    InvalidHeaderValue(#[source] http::header::InvalidHeaderValue),
35    #[error("failed to read response body from socket")]
36    ReadBodySocket(#[source] std::io::Error),
37    #[error("response body exceeds maximum allowed size of {MAX_RESPONSE_BODY_SIZE} bytes")]
38    ResponseBodyTooLarge,
39}
40
41impl From<LoaderRequestError> for Error {
42    fn from(e: LoaderRequestError) -> Self {
43        Error::new_transport(e)
44    }
45}
46
47pub struct FuchsiaHttpRequest {
48    timeout: Duration,
49}
50
51impl HttpRequest for FuchsiaHttpRequest {
52    fn request(&mut self, req: Request<Body>) -> BoxFuture<'_, Result<Response<Vec<u8>>, Error>> {
53        let timeout = self.timeout;
54
55        make_request(req, timeout).on_timeout(timeout, || Err(Error::new_timeout())).boxed()
56    }
57}
58
59async fn make_request(req: Request<Body>, timeout: Duration) -> Result<Response<Vec<u8>>, Error> {
60    let loader = fuchsia_component::client::connect_to_protocol::<fnet_http::LoaderMarker>()
61        .map_err(LoaderRequestError::ConnectToProtocol)?;
62
63    let (parts, body) = req.into_parts();
64
65    let fidl_body = if let Some(body_bytes) = body.into_inner() {
66        let size = body_bytes.len() as u64;
67        let vmo = zx::Vmo::create(size).map_err(LoaderRequestError::CreateVmo)?;
68        vmo.write(&body_bytes, 0).map_err(LoaderRequestError::WriteVmo)?;
69        Some(fnet_http::Body::Buffer(fmem::Buffer { vmo, size }))
70    } else {
71        None
72    };
73
74    let headers: Vec<fnet_http::Header> = parts
75        .headers
76        .iter()
77        .map(|(name, value)| fnet_http::Header {
78            name: name.as_str().as_bytes().to_vec(),
79            value: value.as_bytes().to_vec(),
80        })
81        .collect();
82
83    let deadline = fasync::MonotonicInstant::after(timeout.into()).into_nanos();
84    let fidl_req = fnet_http::Request {
85        method: Some(parts.method.as_str().to_string()),
86        url: Some(parts.uri.to_string()),
87        headers: if headers.is_empty() { None } else { Some(headers) },
88        body: fidl_body,
89        deadline: Some(deadline),
90        ..Default::default()
91    };
92
93    let fidl_resp = loader.fetch(fidl_req).await.map_err(LoaderRequestError::Fidl)?;
94
95    if let Some(err) = fidl_resp.error {
96        return match err {
97            fnet_http::Error::DeadlineExceeded => Err(Error::new_timeout()),
98            other => Err(LoaderRequestError::Loader(other).into()),
99        };
100    }
101
102    let status_code = fidl_resp.status_code.ok_or(LoaderRequestError::MissingStatusCode)?;
103    let status = u16::try_from(status_code)
104        .ok()
105        .and_then(|code| http::StatusCode::from_u16(code).ok())
106        .ok_or(LoaderRequestError::InvalidStatusCode(status_code))?;
107
108    let mut resp_body = Vec::new();
109    if let Some(zx_socket) = fidl_resp.body {
110        let socket = fasync::Socket::from_socket(zx_socket);
111        socket
112            .take(MAX_RESPONSE_BODY_SIZE as u64 + 1)
113            .read_to_end(&mut resp_body)
114            .await
115            .map_err(LoaderRequestError::ReadBodySocket)?;
116        if resp_body.len() > MAX_RESPONSE_BODY_SIZE {
117            return Err(LoaderRequestError::ResponseBodyTooLarge.into());
118        }
119    }
120
121    let mut response = Response::new(resp_body);
122    *response.status_mut() = status;
123    if let Some(headers) = fidl_resp.headers {
124        for fnet_http::Header { name, value } in headers {
125            let header_name = http::header::HeaderName::from_bytes(&name)
126                .map_err(LoaderRequestError::InvalidHeaderName)?;
127            let header_value = http::header::HeaderValue::from_maybe_shared(value)
128                .map_err(LoaderRequestError::InvalidHeaderValue)?;
129            response.headers_mut().append(header_name, header_value);
130        }
131    }
132
133    Ok(response)
134}
135
136impl FuchsiaHttpRequest {
137    /// Construct a new client that uses a default timeout.
138    pub fn new() -> Self {
139        Self::using_timeout(Duration::from_secs(30))
140    }
141
142    /// Construct a new client which always uses the provided duration instead of the default.
143    pub fn using_timeout(timeout: Duration) -> Self {
144        Self { timeout }
145    }
146}
147
148impl Default for FuchsiaHttpRequest {
149    fn default() -> Self {
150        Self::new()
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use fuchsia_hyper_test_support::TestServer;
158    use fuchsia_hyper_test_support::fault_injection::{Hang, HangBody};
159    use fuchsia_hyper_test_support::handler::StaticResponse;
160
161    /// Helper that constructs a Request for a given path on the given test server.
162    fn make_request_for(server: &TestServer, path: &str) -> Request<Body> {
163        Request::builder().uri(server.local_url_for_path(path)).body(Body::default()).unwrap()
164    }
165
166    /// Test that the HttpRequest implementation works against a simple server and returns
167    /// the expected response body.
168    #[fuchsia::test]
169    async fn test_simple_request() {
170        let server =
171            TestServer::builder().handler(StaticResponse::ok_body("some data")).start().await;
172        let mut client = FuchsiaHttpRequest::using_timeout(Duration::from_secs(5));
173        let response = client.request(make_request_for(&server, "some/path")).await.unwrap();
174        let string = String::from_utf8(response.into_body()).unwrap();
175        assert_eq!(string, "some data");
176    }
177
178    /// Test that the HttpRequest implementation properly times out if the server doesn't return
179    /// a response over the socket after accepting the connection.
180    #[fuchsia::test]
181    async fn test_hang() {
182        let server = TestServer::builder().handler(Hang).start().await;
183        let mut client = FuchsiaHttpRequest::using_timeout(Duration::from_secs(1));
184        let response = client.request(make_request_for(&server, "some/path")).await;
185        assert!(response.unwrap_err().is_timeout());
186    }
187
188    /// Test that the HttpRequest implementation properly times out if the server doesn't return
189    /// a the entire body that's expected (after returning a response header).
190    #[fuchsia::test]
191    async fn test_hang_body() {
192        let server = TestServer::builder().handler(HangBody::content_length(500)).start().await;
193        let mut client = FuchsiaHttpRequest::using_timeout(Duration::from_secs(1));
194        let response = client.request(make_request_for(&server, "some/path")).await;
195        assert!(response.unwrap_err().is_timeout());
196    }
197
198    /// Test that POST requests with body and headers preserve headers, status code, and body
199    /// even for non-200 HTTP status codes.
200    #[fuchsia::test]
201    async fn test_post_and_headers_with_non_200_status() {
202        struct CustomPostHandler;
203        impl fuchsia_hyper_test_support::Handler for CustomPostHandler {
204            fn handles(
205                &self,
206                request: &hyper::Request<hyper::body::Incoming>,
207            ) -> Option<BoxFuture<'_, hyper::Response<fuchsia_hyper_test_support::Body>>>
208            {
209                if request.method() == hyper::Method::POST && request.uri().path() == "/update" {
210                    assert_eq!(request.headers().get("x-custom-req").unwrap(), "req-val");
211                    assert_eq!(
212                        request.headers().get("content-length").unwrap(),
213                        "{\"request\":\"ping\"}".len().to_string().as_str()
214                    );
215                    let resp = hyper::Response::builder()
216                        .status(hyper::StatusCode::SERVICE_UNAVAILABLE)
217                        .header("x-retry-after", "3600")
218                        .header("etag", "sig:hash")
219                        .body(b"error body".to_vec().into())
220                        .unwrap();
221                    Some(futures::future::ready(resp).boxed())
222                } else {
223                    None
224                }
225            }
226        }
227
228        let server = TestServer::builder().handler(CustomPostHandler).start().await;
229        let mut client = FuchsiaHttpRequest::using_timeout(Duration::from_secs(5));
230        let req = Request::builder()
231            .method(http::Method::POST)
232            .uri(server.local_url_for_path("update"))
233            .header("x-custom-req", "req-val")
234            .body(omaha_client::http_request::body_from("{\"request\":\"ping\"}"))
235            .unwrap();
236
237        let resp = client.request(req).await.unwrap();
238        assert_eq!(resp.status(), http::StatusCode::SERVICE_UNAVAILABLE);
239        assert_eq!(resp.headers().get("x-retry-after").unwrap(), "3600");
240        assert_eq!(resp.headers().get("etag").unwrap(), "sig:hash");
241        assert_eq!(resp.into_body(), b"error body");
242    }
243
244    #[fuchsia::test]
245    async fn test_response_body_too_large() {
246        use std::error::Error as _;
247
248        let server = TestServer::builder()
249            .handler(StaticResponse::ok_body(vec![b'a'; MAX_RESPONSE_BODY_SIZE + 1]))
250            .start()
251            .await;
252        let mut client = FuchsiaHttpRequest::using_timeout(Duration::from_secs(5));
253        let response = client.request(make_request_for(&server, "some/path")).await;
254        let err = response.unwrap_err();
255        std::assert_matches!(
256            err.source().and_then(|s| s.downcast_ref::<LoaderRequestError>()),
257            Some(LoaderRequestError::ResponseBodyTooLarge)
258        );
259    }
260}