Skip to main content

tuf/repository/
http.rs

1//! Read-only Repository implementation backed by a web server.
2
3use futures_io::AsyncRead;
4use futures_util::future::{BoxFuture, FutureExt as _, TryFutureExt as _};
5use futures_util::stream::TryStreamExt;
6use http::{Response, StatusCode, Uri};
7use http_body_util::BodyExt;
8use hyper_util::client::legacy::connect::Connect;
9use hyper_util::client::legacy::Client;
10type Body = http_body_util::Full<hyper::body::Bytes>;
11use hyper::Request;
12use percent_encoding::utf8_percent_encode;
13use std::future::Future;
14use std::io;
15use std::marker::PhantomData;
16use url::Url;
17
18use crate::error::Error;
19use crate::metadata::{MetadataPath, MetadataVersion, TargetPath};
20use crate::pouf::Pouf;
21use crate::repository::RepositoryProvider;
22use crate::util::SafeAsyncRead;
23use crate::Result;
24
25/// A builder to create a repository accessible over HTTP.
26pub struct HttpRepositoryBuilder<C, D>
27where
28    C: Connect + Sync + 'static,
29    D: Pouf,
30{
31    uri: Uri,
32    client: Client<C, Body>,
33    user_agent: Option<String>,
34    metadata_prefix: Option<Vec<String>>,
35    targets_prefix: Option<Vec<String>>,
36    min_bytes_per_second: u32,
37    _pouf: PhantomData<D>,
38}
39
40impl<C, D> HttpRepositoryBuilder<C, D>
41where
42    C: Connect + Sync + 'static,
43    D: Pouf,
44{
45    /// Create a new repository with the given `Url` and `Client`.
46    pub fn new(url: Url, client: Client<C, Body>) -> Self {
47        HttpRepositoryBuilder {
48            uri: url.to_string().parse::<Uri>().unwrap(), // This is dangerous, but will only exist for a short time as we migrate APIs.
49            client,
50            user_agent: None,
51            metadata_prefix: None,
52            targets_prefix: None,
53            min_bytes_per_second: 4096,
54            _pouf: PhantomData,
55        }
56    }
57
58    /// Create a new repository with the given `Url` and `Client`.
59    pub fn new_with_uri(uri: Uri, client: Client<C, Body>) -> Self {
60        HttpRepositoryBuilder {
61            uri,
62            client,
63            user_agent: None,
64            metadata_prefix: None,
65            targets_prefix: None,
66            min_bytes_per_second: 4096,
67            _pouf: PhantomData,
68        }
69    }
70
71    /// Set the User-Agent prefix.
72    ///
73    /// Callers *should* include a custom User-Agent prefix to help maintainers of TUF repositories
74    /// keep track of which client versions exist in the field.
75    ///
76    pub fn user_agent<T: Into<String>>(mut self, user_agent: T) -> Self {
77        self.user_agent = Some(user_agent.into());
78        self
79    }
80
81    /// The argument `metadata_prefix` is used to provide an alternate path where metadata is
82    /// stored on the repository. If `None`, this defaults to `/`. For example, if there is a TUF
83    /// repository at `https://tuf.example.com/`, but all metadata is stored at `/meta/`, then
84    /// passing the arg `Some("meta".into())` would cause `root.json` to be fetched from
85    /// `https://tuf.example.com/meta/root.json`.
86    pub fn metadata_prefix(mut self, metadata_prefix: Vec<String>) -> Self {
87        self.metadata_prefix = Some(metadata_prefix);
88        self
89    }
90
91    /// The argument `targets_prefix` is used to provide an alternate path where targets is
92    /// stored on the repository. If `None`, this defaults to `/`. For example, if there is a TUF
93    /// repository at `https://tuf.example.com/`, but all targets are stored at `/targets/`, then
94    /// passing the arg `Some("targets".into())` would cause `hello-world` to be fetched from
95    /// `https://tuf.example.com/targets/hello-world`.
96    pub fn targets_prefix(mut self, targets_prefix: Vec<String>) -> Self {
97        self.targets_prefix = Some(targets_prefix);
98        self
99    }
100
101    /// Set the minimum bytes per second for a read to be considered good.
102    pub fn min_bytes_per_second(mut self, min: u32) -> Self {
103        self.min_bytes_per_second = min;
104        self
105    }
106
107    /// Build a `HttpRepository`.
108    pub fn build(self) -> HttpRepository<C, D> {
109        let user_agent = match self.user_agent {
110            Some(user_agent) => user_agent,
111            None => "rust-tuf".into(),
112        };
113
114        HttpRepository {
115            uri: self.uri,
116            client: self.client,
117            user_agent,
118            metadata_prefix: self.metadata_prefix,
119            targets_prefix: self.targets_prefix,
120            min_bytes_per_second: self.min_bytes_per_second,
121            _pouf: PhantomData,
122        }
123    }
124}
125
126/// A repository accessible over HTTP.
127#[derive(Debug)]
128pub struct HttpRepository<C, D>
129where
130    C: Connect + Sync + 'static,
131    D: Pouf,
132{
133    uri: Uri,
134    client: Client<C, Body>,
135    user_agent: String,
136    metadata_prefix: Option<Vec<String>>,
137    targets_prefix: Option<Vec<String>>,
138    min_bytes_per_second: u32,
139    _pouf: PhantomData<D>,
140}
141
142// Configuration for urlencoding URI path elements.
143// From https://url.spec.whatwg.org/#path-percent-encode-set
144const URLENCODE_FRAGMENT: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
145    .add(b' ')
146    .add(b'"')
147    .add(b'<')
148    .add(b'>')
149    .add(b'`');
150const URLENCODE_PATH: &percent_encoding::AsciiSet =
151    &URLENCODE_FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}');
152
153fn extend_uri(uri: &Uri, prefix: &Option<Vec<String>>, components: &[String]) -> Result<Uri> {
154    let uri = uri.clone();
155    let mut uri_parts = uri.into_parts();
156
157    let (path, query) = match &uri_parts.path_and_query {
158        Some(path_and_query) => (path_and_query.path(), path_and_query.query()),
159        None => ("", None),
160    };
161
162    let mut modified_path = path.to_owned();
163    if modified_path.ends_with('/') {
164        modified_path.pop();
165    }
166
167    let mut path_split = modified_path
168        .split('/')
169        .map(String::from)
170        .collect::<Vec<_>>();
171    let mut new_path_elements: Vec<&str> = vec![];
172
173    if let Some(ref prefix) = prefix {
174        new_path_elements.extend(prefix.iter().map(String::as_str));
175    }
176    new_path_elements.extend(components.iter().map(String::as_str));
177
178    // Urlencode new items to match behavior of PathSegmentsMut.extend from
179    // https://docs.rs/url/2.1.0/url/struct.PathSegmentsMut.html
180    let encoded_new_path_elements = new_path_elements
181        .into_iter()
182        .map(|path_segment| utf8_percent_encode(path_segment, URLENCODE_PATH).collect());
183    path_split.extend(encoded_new_path_elements);
184    let constructed_path = path_split.join("/");
185
186    uri_parts.path_and_query =
187        match query {
188            Some(query) => Some(format!("{}?{}", constructed_path, query).parse().map_err(
189                |_| {
190                    Error::IllegalArgument(format!(
191                        "Invalid path and query: {:?}, {:?}",
192                        constructed_path, query
193                    ))
194                },
195            )?),
196            None => Some(constructed_path.parse().map_err(|_| {
197                Error::IllegalArgument(format!("Invalid URI path: {:?}", constructed_path))
198            })?),
199        };
200
201    Uri::from_parts(uri_parts).map_err(|_| {
202        Error::IllegalArgument(format!(
203            "Invalid URI parts: {:?}, {:?}, {:?}",
204            constructed_path, prefix, components
205        ))
206    })
207}
208
209impl<C, D> HttpRepository<C, D>
210where
211    C: Connect + Clone + Send + Sync + 'static,
212    D: Pouf,
213{
214    fn get<'a>(
215        &self,
216        uri: &'a Uri,
217    ) -> Result<impl Future<Output = Result<Response<hyper::body::Incoming>>> + 'a> {
218        let req = Request::builder()
219            .uri(uri)
220            .header("User-Agent", &*self.user_agent)
221            .body(http_body_util::Full::default())
222            .map_err(|err| Error::Http {
223                uri: uri.to_string(),
224                err,
225            })?;
226
227        Ok(self.client.request(req).map_err(|err| Error::Hyper {
228            uri: uri.to_string(),
229            err,
230        }))
231    }
232}
233
234impl<C, D> RepositoryProvider<D> for HttpRepository<C, D>
235where
236    C: Connect + Clone + Send + Sync + 'static,
237    D: Pouf,
238{
239    fn fetch_metadata<'a>(
240        &'a self,
241        meta_path: &MetadataPath,
242        version: MetadataVersion,
243    ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> {
244        let meta_path = meta_path.clone();
245        let components = meta_path.components::<D>(version);
246        let uri = extend_uri(&self.uri, &self.metadata_prefix, &components);
247
248        async move {
249            // TODO(#278) check content length if known and fail early if the payload is too large.
250
251            let uri = uri?;
252            let resp = self.get(&uri)?.await?;
253
254            let status = resp.status();
255            if status == StatusCode::OK {
256                let reader = http_body_util::BodyStream::new(resp.into_body())
257                    .map_ok(|frame| frame.into_data().unwrap_or_default())
258                    .map_err(|err| io::Error::new(io::ErrorKind::Other, err))
259                    .into_async_read()
260                    .enforce_minimum_bitrate(self.min_bytes_per_second);
261
262                let reader: Box<dyn AsyncRead + Send + Unpin> = Box::new(reader);
263                Ok(reader)
264            } else if status == StatusCode::NOT_FOUND {
265                Err(Error::MetadataNotFound {
266                    path: meta_path,
267                    version,
268                })
269            } else {
270                Err(Error::BadHttpStatus {
271                    uri: uri.to_string(),
272                    code: status,
273                })
274            }
275        }
276        .boxed()
277    }
278
279    fn fetch_target<'a>(
280        &'a self,
281        target_path: &TargetPath,
282    ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> {
283        let target_path = target_path.clone();
284        let components = target_path.components();
285        let uri = extend_uri(&self.uri, &self.targets_prefix, &components);
286
287        async move {
288            // TODO(#278) check content length if known and fail early if the payload is too large.
289
290            let uri = uri?;
291            let resp = self.get(&uri)?.await?;
292
293            let status = resp.status();
294            if status == StatusCode::OK {
295                let reader = http_body_util::BodyStream::new(resp.into_body())
296                    .map_ok(|frame| frame.into_data().unwrap_or_default())
297                    .map_err(|err| io::Error::new(io::ErrorKind::Other, err))
298                    .into_async_read()
299                    .enforce_minimum_bitrate(self.min_bytes_per_second);
300
301                let reader: Box<dyn AsyncRead + Send + Unpin> = Box::new(reader);
302                Ok(reader)
303            } else if status == StatusCode::NOT_FOUND {
304                Err(Error::TargetNotFound(target_path))
305            } else {
306                Err(Error::BadHttpStatus {
307                    uri: uri.to_string(),
308                    code: status,
309                })
310            }
311        }
312        .boxed()
313    }
314}
315
316#[cfg(test)]
317mod test {
318    use super::*;
319
320    // Old behavior of the `HttpRepository::get` extension
321    // functionality
322    fn http_repository_extend_using_url(
323        base_url: Url,
324        prefix: &Option<Vec<String>>,
325        components: &[String],
326    ) -> url::Url {
327        let mut url = base_url;
328        {
329            let mut segments = url.path_segments_mut().unwrap();
330            if let Some(ref prefix) = prefix {
331                segments.extend(prefix);
332            }
333            segments.extend(components);
334        }
335        url
336    }
337
338    #[test]
339    fn http_repository_uri_construction() {
340        let base_uri = "http://example.com/one";
341
342        let prefix = Some(vec![String::from("prefix")]);
343        let components = [
344            String::from("components_one"),
345            String::from("components_two"),
346        ];
347
348        let uri = base_uri.parse::<Uri>().unwrap();
349        let extended_uri = extend_uri(&uri, &prefix, &components).unwrap();
350
351        let url =
352            http_repository_extend_using_url(Url::parse(base_uri).unwrap(), &prefix, &components);
353
354        assert_eq!(url.to_string(), extended_uri.to_string());
355        assert_eq!(
356            extended_uri.to_string(),
357            "http://example.com/one/prefix/components_one/components_two"
358        );
359    }
360
361    #[test]
362    fn http_repository_uri_construction_encoded() {
363        let base_uri = "http://example.com/one";
364
365        let prefix = Some(vec![String::from("prefix")]);
366        let components = [String::from("chars to encode#?")];
367        let uri = base_uri.parse::<Uri>().unwrap();
368        let extended_uri = extend_uri(&uri, &prefix, &components)
369            .expect("correctly generated a URI with a zone id");
370
371        let url =
372            http_repository_extend_using_url(Url::parse(base_uri).unwrap(), &prefix, &components);
373
374        assert_eq!(url.to_string(), extended_uri.to_string());
375        assert_eq!(
376            extended_uri.to_string(),
377            "http://example.com/one/prefix/chars%20to%20encode%23%3F"
378        );
379    }
380
381    #[test]
382    fn http_repository_uri_construction_no_components() {
383        let base_uri = "http://example.com/one";
384
385        let prefix = Some(vec![String::from("prefix")]);
386        let components = [];
387
388        let uri = base_uri.parse::<Uri>().unwrap();
389        let extended_uri = extend_uri(&uri, &prefix, &components).unwrap();
390
391        let url =
392            http_repository_extend_using_url(Url::parse(base_uri).unwrap(), &prefix, &components);
393
394        assert_eq!(url.to_string(), extended_uri.to_string());
395        assert_eq!(extended_uri.to_string(), "http://example.com/one/prefix");
396    }
397
398    #[test]
399    fn http_repository_uri_construction_no_prefix() {
400        let base_uri = "http://example.com/one";
401
402        let prefix = None;
403        let components = [
404            String::from("components_one"),
405            String::from("components_two"),
406        ];
407
408        let uri = base_uri.parse::<Uri>().unwrap();
409        let extended_uri = extend_uri(&uri, &prefix, &components).unwrap();
410
411        let url =
412            http_repository_extend_using_url(Url::parse(base_uri).unwrap(), &prefix, &components);
413
414        assert_eq!(url.to_string(), extended_uri.to_string());
415        assert_eq!(
416            extended_uri.to_string(),
417            "http://example.com/one/components_one/components_two"
418        );
419    }
420
421    #[test]
422    fn http_repository_uri_construction_with_query() {
423        let base_uri = "http://example.com/one?test=1";
424
425        let prefix = None;
426        let components = [
427            String::from("components_one"),
428            String::from("components_two"),
429        ];
430
431        let uri = base_uri.parse::<Uri>().unwrap();
432        let extended_uri = extend_uri(&uri, &prefix, &components).unwrap();
433
434        let url =
435            http_repository_extend_using_url(Url::parse(base_uri).unwrap(), &prefix, &components);
436
437        assert_eq!(url.to_string(), extended_uri.to_string());
438        assert_eq!(
439            extended_uri.to_string(),
440            "http://example.com/one/components_one/components_two?test=1"
441        );
442    }
443
444    #[test]
445    fn http_repository_uri_construction_ipv6_zoneid() {
446        let base_uri = "http://[aaaa::aaaa:aaaa:aaaa:1234%252]:80";
447
448        let prefix = Some(vec![String::from("prefix")]);
449        let components = [
450            String::from("componenents_one"),
451            String::from("components_two"),
452        ];
453        let uri = base_uri.parse::<Uri>().unwrap();
454        let extended_uri = extend_uri(&uri, &prefix, &components)
455            .expect("correctly generated a URI with a zone id");
456        assert_eq!(
457            extended_uri.to_string(),
458            "http://[aaaa::aaaa:aaaa:aaaa:1234%252]:80/prefix/componenents_one/components_two"
459        );
460    }
461}