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