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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
mod auth;
use auth::{Authenticate, Credential};

use crate::image::Image;

use reqwest::{Client, StatusCode};
use ttl_cache::TtlCache;

#[derive(Debug, Fail)]
#[allow(clippy::large_enum_variant)]
pub enum RegistryError {
    #[fail(display = "Request Error: {:?}", _0)]
    ReqwestError(#[cause] reqwest::Error),

    #[fail(display = "Invalid authentication challenge: {}", _0)]
    InvalidAuthenticationChallenge(String),

    #[fail(display = "Could not get token: {}", _0)]
    CouldNotGetToken(StatusCode),

    #[fail(display = "Could not authenticate")]
    CouldNotAuthenticate,

    #[fail(display = "Manifest Error: {:?}", _0)]
    ManifestError(#[cause] crate::image::manifest::ManifestError),

    #[fail(display = "Unsupported Manifest Schema: {:?}", _0)]
    UnsupportedManifestSchema(crate::image::manifest::ManifestV2Schema),

    #[fail(display = "Image Spec Error: {:?}", _0)]
    ImageSpecError(#[cause] crate::image::spec::ImageSpecError),
}

/// Represents a Registry implementing the [OpenContainer Distribution
/// Spec](https://github.com/opencontainers/distribution-spec/blob/master/spec.md)
pub struct Registry {
    pub url: String,
    client: Client,
    credential_cache: TtlCache<String, Credential>,
}

impl std::fmt::Debug for Registry {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(
            f,
            "Registry {{ url: {}, client: {:?} }}",
            self.url, self.client
        )
    }
}

impl Registry {
    /// Create a new registry interface given the URL to a registry.
    ///
    /// Note: The URL should **not** contain a trailing slash.
    ///
    /// # Example
    /// ```
    ///# extern crate opencontainers;
    ///# use opencontainers::Registry;
    /// let registry = Registry::new("https://registry-1.docker.io");
    ///# assert_eq!(registry.url, "https://registry-1.docker.io");
    /// ```
    ///
    /// # Panics
    /// This function can panic if the backing
    /// [ClientBuilder](https://docs.rs/reqwest/0/reqwest/struct.ClientBuilder.html)
    /// cannot be initialized. This can happen if the native TLS backend
    /// cannot be initialized.
    pub fn new(url: &str) -> Self {
        let client = Client::builder()
            .gzip(true)
            .build()
            .expect("Could not build request client");

        let credential_cache: TtlCache<String, Credential> = TtlCache::new(32);

        Registry {
            url: url.into(),
            client,
            credential_cache,
        }
    }

    fn try_auth(
        &self,
        authenticate: &reqwest::header::HeaderValue,
    ) -> Result<Vec<Credential>, RegistryError> {
        auth::do_challenge(&self.client, authenticate)
    }

    fn attempt_request(
        &self,
        url: &str,
        headers: Option<&reqwest::header::HeaderMap>,
        cred: Option<&Credential>,
    ) -> Result<Result<reqwest::Response, reqwest::Response>, RegistryError> {
        let mut request = self.client.get(url);

        if let Some(headers) = headers {
            request = request.headers(headers.clone());
        }

        if let Some(credential) = cred {
            request = request.authenticate(&credential);
        } else {
            info!("Attempting unauthenticated request");
        }

        let response = request.send().map_err(RegistryError::ReqwestError)?;

        let status = response.status();

        info!("got response: {:?}", response);

        if status.is_success() {
            return Ok(Ok(response));
        }

        Ok(Err(response))
    }

    /// Perform a GET request on the Registry, handling authentication.
    ///
    /// # Authentication
    /// Authentication is handled transiently according to the [Docker
    /// Registry Token Authentication
    /// Specification](https://docs.docker.com/registry/spec/auth/token/)
    ///
    /// # Example
    /// ```
    ///# extern crate opencontainers;
    ///# use opencontainers::Registry;
    ///# let registry = Registry::new("https://registry-1.docker.io");
    /// let endpoint = format!("{}/v2/", registry.url);
    /// let response = registry.get(endpoint.as_str(), None)
    ///     .expect("Could not perform API Version Check");
    /// assert!(response.status().is_success());
    /// ```
    pub fn get(
        &self,
        url: &str,
        headers: Option<&reqwest::header::HeaderMap>,
    ) -> Result<reqwest::Response, RegistryError> {
        // Try to use the credential if it is cached
        let credential = self.credential_cache.get(url);

        // Attempt request
        let response = match self.attempt_request(url, headers, credential)? {
            Ok(response) => return Ok(response),
            Err(response) => response,
        };

        // Unauthorized
        let unauthorized = response.status() == StatusCode::UNAUTHORIZED;
        let has_authenticate = response
            .headers()
            .contains_key(reqwest::header::WWW_AUTHENTICATE);

        if unauthorized && !has_authenticate {
            return Err(RegistryError::InvalidAuthenticationChallenge(
                "No authentication challenge presented".into(),
            ));
        } else if !unauthorized {
            return Err(RegistryError::CouldNotGetToken(response.status()));
        }

        info!("Authentication required");
        #[allow(clippy::or_fun_call)]
        let authenticate = response
            .headers()
            .get(reqwest::header::WWW_AUTHENTICATE)
            .ok_or(RegistryError::InvalidAuthenticationChallenge(
                "Missing WWW-Authenticate Header".into(),
            ))?;

        let credentials = self.try_auth(authenticate)?;

        // Attempt with each credential we got
        for credential in credentials {
            if let Ok(response) = self.attempt_request(url, headers, Some(&credential))? {
                info!("Got response: {:?}", response);

                // TODO: Cache credential.
                return Ok(response);
            }
        }

        Err(RegistryError::CouldNotAuthenticate)
    }

    /// Create an image handle for a given image
    ///
    /// # Example
    /// ```
    ///# extern crate opencontainers;
    ///# use opencontainers::Registry;
    ///# let registry = Registry::new("https://registry-1.docker.io");
    /// let manifest = registry.image("library/hello-world", "latest")
    ///     .expect("Could not get image");
    /// ```
    pub fn image(&self, name: &str, reference: &str) -> Result<Image, RegistryError> {
        Image::new(self, name, reference)
    }
}