1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
//! Client library for Docker Registry API v2.
//!
//! This module provides a `Client` which can be used to list
//! images and tags, to check for the presence of blobs (manifests,
//! layers and other objects) by digest, and to retrieve them.
//!
//! ## Example
//!
//! ```rust,no_run
//! # use tokio;
//!
//! # #[tokio::main]
//! # async fn main() {
//! # async fn run() -> docker_registry::errors::Result<()> {
//! #
//! use docker_registry::v2::Client;
//!
//! // Retrieve an image manifest.
//! let client = Client::configure().registry("quay.io").build()?;
//! let manifest = client.get_manifest("coreos/etcd", "v3.1.0").await?;
//! #
//! # Ok(())
//! # };
//! # run().await.unwrap();
//! # }
//! ```

use futures::prelude::*;
use log::trace;
use reqwest::{Method, StatusCode, Url};
use serde::{Deserialize, Serialize};

use crate::{errors::*, mediatypes::MediaTypes};

mod config;
pub use self::config::Config;

mod catalog;

mod auth;
pub use auth::WwwHeaderParseError;

pub mod manifest;

mod tags;

mod blobs;

mod content_digest;
pub(crate) use self::content_digest::ContentDigest;
pub use self::content_digest::ContentDigestError;

/// A Client to make outgoing API requests to a registry.
#[derive(Clone, Debug)]
pub struct Client {
  base_url: String,
  credentials: Option<(String, String)>,
  user_agent: Option<String>,
  auth: Option<auth::Auth>,
  client: reqwest::Client,
  accepted_types: Vec<(MediaTypes, Option<f64>)>,
}

impl Client {
  pub fn configure() -> Config {
    Config::default()
  }

  /// Ensure remote registry supports v2 API.
  pub async fn ensure_v2_registry(self) -> Result<Self> {
    if !self.is_v2_supported().await? {
      Err(Error::V2NotSupported)
    } else {
      Ok(self)
    }
  }

  /// Check whether remote registry supports v2 API.
  pub async fn is_v2_supported(&self) -> Result<bool> {
    match self.is_v2_supported_and_authorized().await {
      Ok((v2_supported, _)) => Ok(v2_supported),
      Err(crate::Error::UnexpectedHttpStatus(_)) => Ok(false),
      Err(e) => Err(e),
    }
  }

  /// Check whether remote registry supports v2 API and `self` is authorized.
  /// Authorized means to successfully GET the `/v2` endpoint on the remote registry.
  pub async fn is_v2_supported_and_authorized(&self) -> Result<(bool, bool)> {
    let api_header = "Docker-Distribution-API-Version";
    let api_version = "registry/2.0";

    // GET request to bare v2 endpoint.
    let v2_endpoint = format!("{}/v2/", self.base_url);
    let request = reqwest::Url::parse(&v2_endpoint).map(|url| {
      trace!("GET {:?}", url);
      self.build_reqwest(Method::GET, url)
    })?;

    let response = request.send().await?;

    let b = match (response.status(), response.headers().get(api_header)) {
      (StatusCode::OK, Some(x)) => Ok((x == api_version, true)),
      (StatusCode::UNAUTHORIZED, Some(x)) => Ok((x == api_version, false)),
      (s, v) => {
        trace!("Got unexpected status {}, header version {:?}", s, v);
        return Err(crate::Error::UnexpectedHttpStatus(s));
      }
    };

    b
  }

  /// Takes reqwest's async RequestBuilder and injects an authentication header if a token is present
  fn build_reqwest(&self, method: Method, url: Url) -> reqwest::RequestBuilder {
    let mut builder = self.client.request(method, url);

    if let Some(auth) = &self.auth {
      builder = auth.add_auth_headers(builder);
    };

    if let Some(ua) = &self.user_agent {
      builder = builder.header(reqwest::header::USER_AGENT, ua.as_str());
    };

    builder
  }
}

#[derive(Debug, Default, Deserialize, Serialize)]
struct ApiError {
  code: String,
  message: String,
  detail: String,
}

#[derive(Debug, Default, Deserialize, Serialize)]
struct Errors {
  errors: Vec<ApiError>,
}