blob: bca068cc4cfcb9badb599919bb80fea2e5f76c1a [file] [log] [blame]
//! Read-only Repository implementation backed by a web server.
use futures_io::AsyncRead;
use futures_util::future::{BoxFuture, FutureExt};
use futures_util::stream::TryStreamExt;
use http::{Response, StatusCode, Uri};
use hyper::body::Body;
use hyper::client::connect::Connect;
use hyper::Client;
use hyper::Request;
#[cfg(feature = "hyper_013")]
use hyper_013 as hyper;
#[cfg(feature = "hyper_014")]
use hyper_014 as hyper;
use percent_encoding::utf8_percent_encode;
use std::io;
use std::marker::PhantomData;
use url::Url;
use crate::error::Error;
use crate::interchange::DataInterchange;
use crate::metadata::{MetadataPath, MetadataVersion, TargetPath};
use crate::repository::RepositoryProvider;
use crate::util::SafeAsyncRead;
use crate::Result;
/// A builder to create a repository accessible over HTTP.
pub struct HttpRepositoryBuilder<C, D>
where
C: Connect + Sync + 'static,
D: DataInterchange,
{
uri: Uri,
client: Client<C>,
user_agent: Option<String>,
metadata_prefix: Option<Vec<String>>,
targets_prefix: Option<Vec<String>>,
min_bytes_per_second: u32,
_interchange: PhantomData<D>,
}
impl<C, D> HttpRepositoryBuilder<C, D>
where
C: Connect + Sync + 'static,
D: DataInterchange,
{
/// Create a new repository with the given `Url` and `Client`.
pub fn new(url: Url, client: Client<C>) -> Self {
HttpRepositoryBuilder {
uri: url.to_string().parse::<Uri>().unwrap(), // This is dangerous, but will only exist for a short time as we migrate APIs.
client,
user_agent: None,
metadata_prefix: None,
targets_prefix: None,
min_bytes_per_second: 4096,
_interchange: PhantomData,
}
}
/// Create a new repository with the given `Url` and `Client`.
pub fn new_with_uri(uri: Uri, client: Client<C>) -> Self {
HttpRepositoryBuilder {
uri,
client,
user_agent: None,
metadata_prefix: None,
targets_prefix: None,
min_bytes_per_second: 4096,
_interchange: PhantomData,
}
}
/// Set the User-Agent prefix.
///
/// Callers *should* include a custom User-Agent prefix to help maintainers of TUF repositories
/// keep track of which client versions exist in the field.
///
pub fn user_agent<T: Into<String>>(mut self, user_agent: T) -> Self {
self.user_agent = Some(user_agent.into());
self
}
/// The argument `metadata_prefix` is used to provide an alternate path where metadata is
/// stored on the repository. If `None`, this defaults to `/`. For example, if there is a TUF
/// repository at `https://tuf.example.com/`, but all metadata is stored at `/meta/`, then
/// passing the arg `Some("meta".into())` would cause `root.json` to be fetched from
/// `https://tuf.example.com/meta/root.json`.
pub fn metadata_prefix(mut self, metadata_prefix: Vec<String>) -> Self {
self.metadata_prefix = Some(metadata_prefix);
self
}
/// The argument `targets_prefix` is used to provide an alternate path where targets is
/// stored on the repository. If `None`, this defaults to `/`. For example, if there is a TUF
/// repository at `https://tuf.example.com/`, but all targets are stored at `/targets/`, then
/// passing the arg `Some("targets".into())` would cause `hello-world` to be fetched from
/// `https://tuf.example.com/targets/hello-world`.
pub fn targets_prefix(mut self, targets_prefix: Vec<String>) -> Self {
self.targets_prefix = Some(targets_prefix);
self
}
/// Set the minimum bytes per second for a read to be considered good.
pub fn min_bytes_per_second(mut self, min: u32) -> Self {
self.min_bytes_per_second = min;
self
}
/// Build a `HttpRepository`.
pub fn build(self) -> HttpRepository<C, D> {
let user_agent = match self.user_agent {
Some(user_agent) => user_agent,
None => "rust-tuf".into(),
};
HttpRepository {
uri: self.uri,
client: self.client,
user_agent,
metadata_prefix: self.metadata_prefix,
targets_prefix: self.targets_prefix,
min_bytes_per_second: self.min_bytes_per_second,
_interchange: PhantomData,
}
}
}
/// A repository accessible over HTTP.
pub struct HttpRepository<C, D>
where
C: Connect + Sync + 'static,
D: DataInterchange,
{
uri: Uri,
client: Client<C>,
user_agent: String,
metadata_prefix: Option<Vec<String>>,
targets_prefix: Option<Vec<String>>,
min_bytes_per_second: u32,
_interchange: PhantomData<D>,
}
// Configuration for urlencoding URI path elements.
// From https://url.spec.whatwg.org/#path-percent-encode-set
const URLENCODE_FRAGMENT: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
.add(b' ')
.add(b'"')
.add(b'<')
.add(b'>')
.add(b'`');
const URLENCODE_PATH: &percent_encoding::AsciiSet =
&URLENCODE_FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}');
fn extend_uri(uri: Uri, prefix: &Option<Vec<String>>, components: &[String]) -> Result<Uri> {
let mut uri_parts = uri.into_parts();
let (path, query) = match &uri_parts.path_and_query {
Some(path_and_query) => (path_and_query.path(), path_and_query.query()),
None => ("", None),
};
let mut modified_path = path.to_owned();
if modified_path.ends_with('/') {
modified_path.pop();
}
let mut path_split = modified_path
.split('/')
.map(String::from)
.collect::<Vec<_>>();
let mut new_path_elements: Vec<&str> = vec![];
if let Some(ref prefix) = prefix {
new_path_elements.extend(prefix.iter().map(String::as_str));
}
new_path_elements.extend(components.iter().map(String::as_str));
// Urlencode new items to match behavior of PathSegmentsMut.extend from
// https://docs.rs/url/2.1.0/url/struct.PathSegmentsMut.html
let encoded_new_path_elements = new_path_elements
.into_iter()
.map(|path_segment| utf8_percent_encode(path_segment, URLENCODE_PATH).collect());
path_split.extend(encoded_new_path_elements);
let constructed_path = path_split.join("/");
uri_parts.path_and_query =
match query {
Some(query) => Some(format!("{}?{}", constructed_path, query).parse().map_err(
|_| {
Error::IllegalArgument(format!(
"Invalid path and query: {:?}, {:?}",
constructed_path, query
))
},
)?),
None => Some(constructed_path.parse().map_err(|_| {
Error::IllegalArgument(format!("Invalid URI path: {:?}", constructed_path))
})?),
};
Uri::from_parts(uri_parts).map_err(|_| {
Error::IllegalArgument(format!(
"Invalid URI parts: {:?}, {:?}, {:?}",
constructed_path, prefix, components
))
})
}
impl<C, D> HttpRepository<C, D>
where
C: Connect + Clone + Send + Sync + 'static,
D: DataInterchange,
{
async fn get<'a>(
&'a self,
prefix: &'a Option<Vec<String>>,
components: &'a [String],
) -> Result<Response<Body>> {
let base_uri = self.uri.clone();
let uri = extend_uri(base_uri, prefix, components)?;
let req = Request::builder()
.uri(&uri)
.header("User-Agent", &*self.user_agent)
.body(Body::default())?;
let resp = self.client.request(req).await?;
let status = resp.status();
if status == StatusCode::OK {
Ok(resp)
} else if status == StatusCode::NOT_FOUND {
Err(Error::NotFound)
} else {
Err(Error::BadHttpStatus {
code: status,
uri: uri.to_string(),
})
}
}
}
impl<C, D> RepositoryProvider<D> for HttpRepository<C, D>
where
C: Connect + Clone + Send + Sync + 'static,
D: DataInterchange + Send + Sync,
{
fn fetch_metadata<'a>(
&'a self,
meta_path: &MetadataPath,
version: MetadataVersion,
) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> {
let components = meta_path.components::<D>(version);
async move {
let resp = self.get(&self.metadata_prefix, &components).await?;
// TODO(#278) check content length if known and fail early if the payload is too large.
let reader = resp
.into_body()
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))
.into_async_read()
.enforce_minimum_bitrate(self.min_bytes_per_second);
let reader: Box<dyn AsyncRead + Send + Unpin> = Box::new(reader);
Ok(reader)
}
.boxed()
}
fn fetch_target<'a>(
&'a self,
target_path: &TargetPath,
) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> {
let components = target_path.components();
async move {
// TODO(#278) check content length if known and fail early if the payload is too large.
let resp = self.get(&self.targets_prefix, &components).await?;
let reader = resp
.into_body()
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))
.into_async_read()
.enforce_minimum_bitrate(self.min_bytes_per_second);
Ok(Box::new(reader) as Box<dyn AsyncRead + Send + Unpin>)
}
.boxed()
}
}
#[cfg(test)]
mod test {
use super::*;
// Old behavior of the `HttpRepository::get` extension
// functionality
fn http_repository_extend_using_url(
base_url: Url,
prefix: &Option<Vec<String>>,
components: &[String],
) -> url::Url {
let mut url = base_url;
{
let mut segments = url.path_segments_mut().unwrap();
if let Some(ref prefix) = prefix {
segments.extend(prefix);
}
segments.extend(components);
}
url
}
#[test]
fn http_repository_uri_construction() {
let base_uri = "http://example.com/one";
let prefix = Some(vec![String::from("prefix")]);
let components = [
String::from("components_one"),
String::from("components_two"),
];
let uri = base_uri.parse::<Uri>().unwrap();
let extended_uri = extend_uri(uri, &prefix, &components).unwrap();
let url =
http_repository_extend_using_url(Url::parse(base_uri).unwrap(), &prefix, &components);
assert_eq!(url.to_string(), extended_uri.to_string());
assert_eq!(
extended_uri.to_string(),
"http://example.com/one/prefix/components_one/components_two"
);
}
#[test]
fn http_repository_uri_construction_encoded() {
let base_uri = "http://example.com/one";
let prefix = Some(vec![String::from("prefix")]);
let components = [String::from("chars to encode#?")];
let uri = base_uri.parse::<Uri>().unwrap();
let extended_uri = extend_uri(uri, &prefix, &components)
.expect("correctly generated a URI with a zone id");
let url =
http_repository_extend_using_url(Url::parse(base_uri).unwrap(), &prefix, &components);
assert_eq!(url.to_string(), extended_uri.to_string());
assert_eq!(
extended_uri.to_string(),
"http://example.com/one/prefix/chars%20to%20encode%23%3F"
);
}
#[test]
fn http_repository_uri_construction_no_components() {
let base_uri = "http://example.com/one";
let prefix = Some(vec![String::from("prefix")]);
let components = [];
let uri = base_uri.parse::<Uri>().unwrap();
let extended_uri = extend_uri(uri, &prefix, &components).unwrap();
let url =
http_repository_extend_using_url(Url::parse(base_uri).unwrap(), &prefix, &components);
assert_eq!(url.to_string(), extended_uri.to_string());
assert_eq!(extended_uri.to_string(), "http://example.com/one/prefix");
}
#[test]
fn http_repository_uri_construction_no_prefix() {
let base_uri = "http://example.com/one";
let prefix = None;
let components = [
String::from("components_one"),
String::from("components_two"),
];
let uri = base_uri.parse::<Uri>().unwrap();
let extended_uri = extend_uri(uri, &prefix, &components).unwrap();
let url =
http_repository_extend_using_url(Url::parse(base_uri).unwrap(), &prefix, &components);
assert_eq!(url.to_string(), extended_uri.to_string());
assert_eq!(
extended_uri.to_string(),
"http://example.com/one/components_one/components_two"
);
}
#[test]
fn http_repository_uri_construction_with_query() {
let base_uri = "http://example.com/one?test=1";
let prefix = None;
let components = [
String::from("components_one"),
String::from("components_two"),
];
let uri = base_uri.parse::<Uri>().unwrap();
let extended_uri = extend_uri(uri, &prefix, &components).unwrap();
let url =
http_repository_extend_using_url(Url::parse(base_uri).unwrap(), &prefix, &components);
assert_eq!(url.to_string(), extended_uri.to_string());
assert_eq!(
extended_uri.to_string(),
"http://example.com/one/components_one/components_two?test=1"
);
}
#[test]
fn http_repository_uri_construction_ipv6_zoneid() {
let base_uri = "http://[aaaa::aaaa:aaaa:aaaa:1234%252]:80";
let prefix = Some(vec![String::from("prefix")]);
let components = [
String::from("componenents_one"),
String::from("components_two"),
];
let uri = base_uri.parse::<Uri>().unwrap();
let extended_uri = extend_uri(uri, &prefix, &components)
.expect("correctly generated a URI with a zone id");
assert_eq!(
extended_uri.to_string(),
"http://[aaaa::aaaa:aaaa:aaaa:1234%252]:80/prefix/componenents_one/components_two"
);
}
}