omaha_client_fuchsia/
http_request.rs1use 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 pub fn new() -> Self {
139 Self::using_timeout(Duration::from_secs(30))
140 }
141
142 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 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 #[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 #[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 #[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 #[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}