blob: 94320d73cd4772fd0b07443e837e1d6132c52346 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! The http module contains utilities for making HTTP requests.
use crate::error::{ResultExt, TokenProviderError};
use async_trait::async_trait;
use fidl_fuchsia_identity_external::Error as ApiError;
use fuchsia_async as fasync;
use fuchsia_zircon as zx;
use futures::io::AsyncReadExt;
use hyper::StatusCode;
type TokenProviderResult<T> = Result<T, TokenProviderError>;
/// Representation of an HTTP request.
pub struct HttpRequest(fidl_fuchsia_net_http::Request);
/// A builder for `HttpRequest`.
pub struct HttpRequestBuilder<'a> {
url: String,
method: String,
headers: Option<Vec<fidl_fuchsia_net_http::Header>>,
body: Option<&'a str>,
}
#[must_use = "HttpRequestBuilder must be consumed with finish()."]
impl<'a> HttpRequestBuilder<'a> {
/// Create a new `HttpRequestBuilder`.
pub fn new<T, U>(url: T, method: U) -> Self
where
String: From<T> + From<U>,
{
HttpRequestBuilder {
url: String::from(url),
method: String::from(method),
headers: None,
body: None,
}
}
/// Add a header to the HTTP request.
pub fn with_header<T, U>(mut self, name: T, value: U) -> Self
where
Vec<u8>: From<T> + From<U>,
{
let headers = self.headers.get_or_insert(vec![]);
headers.push(fidl_fuchsia_net_http::Header { name: name.into(), value: value.into() });
self
}
/// Adds a body to the HTTP request. Replaces any existing body.
pub fn set_body<T>(mut self, body: &'a T) -> Self
where
T: AsRef<str>,
{
self.body.replace(body.as_ref());
self
}
/// Build an HttpRequest.
pub fn finish(self) -> TokenProviderResult<HttpRequest> {
let url_body = match self.body {
Some(body_str) => {
let vmo = zx::Vmo::create(body_str.as_bytes().len() as u64)
.token_provider_error(ApiError::Unknown)?;
vmo.write(&body_str.as_bytes(), 0).token_provider_error(ApiError::Unknown)?;
Some(fidl_fuchsia_net_http::Body::Buffer(fidl_fuchsia_mem::Buffer {
vmo,
size: body_str.as_bytes().len() as u64,
}))
}
None => None,
};
Ok(HttpRequest(fidl_fuchsia_net_http::Request {
url: Some(self.url),
method: Some(self.method),
headers: self.headers,
body: url_body,
deadline: None,
}))
}
}
/// Number of bytes initially allocated for retrieving an HTTP response.
const RESPONSE_BUFFER_SIZE: usize = 2048;
/// A trait expressing functionality for making requests over HTTP.
#[async_trait]
pub trait HttpClient {
/// Asynchronously make an HTTP request. Returns the response body if any
/// and HTTP status code.
async fn request(
&self,
http_request: HttpRequest,
) -> TokenProviderResult<(Option<String>, StatusCode)>;
}
/// A client capable of making HTTP requests using the Fuchsia oldhttp URL
/// Loader service.
#[derive(Clone)]
pub struct UrlLoaderHttpClient {
url_loader: fidl_fuchsia_net_http::LoaderProxy,
}
impl UrlLoaderHttpClient {
/// Create a new `UrlLoaderHttpClient`.
pub fn new(url_loader: fidl_fuchsia_net_http::LoaderProxy) -> Self {
UrlLoaderHttpClient { url_loader }
}
}
#[async_trait]
impl HttpClient for UrlLoaderHttpClient {
/// Asynchronously send an HTTP request using oldhttp URLLoader service.
async fn request(
&self,
HttpRequest(http_request): HttpRequest,
) -> TokenProviderResult<(Option<String>, StatusCode)> {
let fidl_fuchsia_net_http::Response { error, body, status_code, .. } =
self.url_loader.fetch(http_request).await.token_provider_error(ApiError::Unknown)?;
if error.is_some() {
return Err(TokenProviderError::new(ApiError::Network));
}
let status_code =
status_code.ok_or(TokenProviderError { api_error: ApiError::Unknown, cause: None })?;
let status =
StatusCode::from_u16(status_code as u16).token_provider_error(ApiError::Server)?;
match body {
Some(sock) => {
let mut socket =
fasync::Socket::from_socket(sock).token_provider_error(ApiError::Unknown)?;
let mut response_body = Vec::<u8>::with_capacity(RESPONSE_BUFFER_SIZE);
socket
.read_to_end(&mut response_body)
.await
.token_provider_error(ApiError::Unknown)?;
let response_str =
String::from_utf8(response_body).token_provider_error(ApiError::Unknown)?;
Ok((Some(response_str), status))
}
None => Ok((None, status)),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use anyhow::Error;
use fidl::endpoints::create_proxy_and_stream;
use futures::prelude::*;
use lazy_static::lazy_static;
lazy_static! {
static ref TEST_URL: String = String::from("https://test/test");
}
#[test]
fn test_build_http_request_empty() {
assert_build_http_request_empty_with_method("GET");
assert_build_http_request_empty_with_method("POST");
}
fn assert_build_http_request_empty_with_method(method: &str) {
let request = HttpRequestBuilder::new(TEST_URL.as_str(), method)
.finish()
.expect("Failed to build HTTP request.");
assert_eq!(
request.0,
fidl_fuchsia_net_http::Request {
url: Some(TEST_URL.clone()),
method: Some(method.to_string()),
headers: None,
body: None,
deadline: None,
}
);
}
#[test]
fn test_build_http_request_with_headers() {
let request = HttpRequestBuilder::new(TEST_URL.as_str(), "GET")
.with_header("name-1", "value-1")
.with_header(String::from("name-2"), String::from("value-2"))
.finish()
.expect("Failed to build HTTP request.");
assert_eq!(
request.0.headers.unwrap(),
vec![
fidl_fuchsia_net_http::Header { name: "name-1".into(), value: "value-1".into() },
fidl_fuchsia_net_http::Header { name: "name-2".into(), value: "value-2".into() },
]
);
assert!(request.0.body.is_none());
}
#[test]
fn test_build_http_request_with_body() {
let test_body: &str = "test-body";
let request = HttpRequestBuilder::new(TEST_URL.as_str(), "GET")
.set_body(&test_body)
.finish()
.expect("Failed to build HTTP request.");
assert!(request.0.headers.is_none());
match request.0.body.unwrap() {
fidl_fuchsia_net_http::Body::Stream(_) => panic!("Expected body to be a buffer."),
fidl_fuchsia_net_http::Body::Buffer(fidl_fuchsia_mem::Buffer { vmo, size }) => {
assert_eq!(size as usize, test_body.len());
let mut result_body = vec![0u8; test_body.len()];
vmo.read(&mut result_body, 0).expect("Failed to read vmo.");
assert_eq!(String::from_utf8(result_body).unwrap(), test_body);
}
}
}
fn url_loader_with_response(
body: &str,
status_code: u16,
error: Option<fidl_fuchsia_net_http::Error>,
) -> fidl_fuchsia_net_http::LoaderProxy {
let response = fidl_fuchsia_net_http::Response {
error,
body: Some(socket_with_body(body)),
final_url: None,
status_code: Some(status_code as u32),
status_line: None,
headers: None,
redirect: None,
};
let (url_loader_proxy, mut url_loader_stream) =
create_proxy_and_stream::<fidl_fuchsia_net_http::LoaderMarker>()
.expect("Failed to create URL loader proxy.");
fasync::Task::spawn(async move {
let req =
url_loader_stream.try_next().await.expect("Failed to get request from stream");
if let Some(fidl_fuchsia_net_http::LoaderRequest::Fetch { responder, .. }) = req {
responder.send(response).expect("Failed to send response");
} else {
panic!("Got unexpected URL Loader request.")
}
})
.detach();
url_loader_proxy
}
fn socket_with_body(body: &str) -> zx::Socket {
let (sock_read, sock_write) =
zx::Socket::create(zx::SocketOpts::empty()).expect("Failed to create sockets");
sock_write.write(body.as_bytes()).expect("Failed to write to socket");
sock_read
}
#[fasync::run_until_stalled(test)]
async fn test_request() -> Result<(), Error> {
let http_client = UrlLoaderHttpClient::new(url_loader_with_response(
"response-body",
StatusCode::FORBIDDEN.as_u16(),
None,
));
let request = HttpRequestBuilder::new(TEST_URL.as_str(), "GET").finish()?;
let (response_body, status) = http_client.request(request).await?;
assert_eq!(response_body, Some("response-body".to_string()));
assert_eq!(status, StatusCode::FORBIDDEN);
Ok(())
}
#[fasync::run_until_stalled(test)]
async fn test_request_large_response() -> Result<(), Error> {
// Verify that responses won't get truncated if larger than initial buffer
// as Oauth tokens are of indeterminate length
let long_body = "a".repeat(RESPONSE_BUFFER_SIZE * 2);
let http_client = UrlLoaderHttpClient::new(url_loader_with_response(
long_body.as_str(),
StatusCode::OK.as_u16(),
None,
));
let request = HttpRequestBuilder::new(TEST_URL.as_str(), "GET").finish()?;
let (response_body, status) = http_client.request(request).await?;
assert_eq!(Some(long_body), response_body);
assert_eq!(StatusCode::OK, status);
Ok(())
}
#[fasync::run_until_stalled(test)]
async fn test_network_error() -> Result<(), Error> {
let http_client = UrlLoaderHttpClient::new(url_loader_with_response(
"",
0,
Some(fidl_fuchsia_net_http::Error::Connect),
));
let request = HttpRequestBuilder::new(TEST_URL.as_str(), "GET").finish()?;
let result = http_client.request(request).await;
assert_eq!(result.unwrap_err().api_error, ApiError::Network);
Ok(())
}
}
#[cfg(test)]
pub mod mock {
use super::*;
use std::collections::VecDeque;
use std::sync::Mutex;
/// A mock implementation of `HttpClient` that returns responses supplied at creation
/// time.
pub struct TestHttpClient {
/// Response returned on `request`.
responses: Mutex<VecDeque<TokenProviderResult<(Option<String>, StatusCode)>>>,
}
impl TestHttpClient {
/// Create a new test client that returns the given responses during calls
/// to `request`.
pub fn with_responses(
responses: Vec<TokenProviderResult<(Option<String>, StatusCode)>>,
) -> Self {
TestHttpClient { responses: Mutex::new(VecDeque::from(responses)) }
}
/// Create a new test client that returns the given response on `request`.
pub fn with_response(body: Option<&str>, status: StatusCode) -> Self {
Self::with_responses(vec![Ok((body.map(str::to_string), status))])
}
/// Create a new test client that returns the given response on `request`.
pub fn with_error(error: ApiError) -> Self {
Self::with_responses(vec![Err(TokenProviderError::new(error))])
}
}
#[async_trait]
impl HttpClient for TestHttpClient {
async fn request(
&self,
_http_request: HttpRequest,
) -> TokenProviderResult<(Option<String>, StatusCode)> {
self.responses
.lock()
.unwrap()
.pop_front()
.expect("Mock received more requests than the supplied requests!")
}
}
mod test {
use super::*;
fn get_http_request() -> HttpRequest {
HttpRequestBuilder::new("http://url", "GET").finish().unwrap()
}
#[fasync::run_until_stalled(test)]
async fn test_mock_with_responses() {
let responses =
vec![Ok((None, StatusCode::OK)), Err(TokenProviderError::new(ApiError::Unknown))];
let test_client = TestHttpClient::with_responses(responses);
assert_eq!(
(None, StatusCode::OK),
test_client.request(get_http_request()).await.unwrap()
);
assert_eq!(
ApiError::Unknown,
test_client.request(get_http_request()).await.unwrap_err().api_error
);
}
#[fasync::run_until_stalled(test)]
async fn test_mock_with_response() {
let test_client =
TestHttpClient::with_response(Some("response"), StatusCode::UNAUTHORIZED);
assert_eq!(
(Some("response".to_string()), StatusCode::UNAUTHORIZED),
test_client.request(get_http_request()).await.unwrap()
);
}
#[fasync::run_until_stalled(test)]
async fn test_mock_with_error() {
let test_client = TestHttpClient::with_error(ApiError::Internal);
assert_eq!(
ApiError::Internal,
test_client.request(get_http_request()).await.unwrap_err().api_error
);
}
}
}