Skip to main content

docker_registry/v2/
config.rs

1use log::trace;
2use reqwest::Certificate;
3
4use crate::{mediatypes::MediaTypes, v2::*};
5
6/// Configuration for a `Client`.
7#[derive(Clone, Debug)]
8pub struct Config {
9  index: String,
10  insecure_registry: bool,
11  user_agent: Option<String>,
12  username: Option<String>,
13  password: Option<String>,
14  accept_invalid_certs: bool,
15  root_certificates: Vec<Certificate>,
16  accepted_types: Option<Vec<(MediaTypes, Option<f64>)>>,
17  connect_timeout: Option<std::time::Duration>,
18  request_timeout: Option<std::time::Duration>,
19}
20
21impl Config {
22  /// Set registry service to use (vhost or IP).
23  #[must_use]
24  pub fn registry(mut self, reg: &str) -> Self {
25    self.index = reg.to_owned();
26    self
27  }
28
29  /// Whether to use an insecure HTTP connection to the registry.
30  #[must_use]
31  pub fn insecure_registry(mut self, insecure: bool) -> Self {
32    self.insecure_registry = insecure;
33    self
34  }
35
36  /// Set whether or not to accept invalid certificates.
37  #[must_use]
38  pub fn accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
39    self.accept_invalid_certs = accept_invalid_certs;
40    self
41  }
42
43  /// Add a root certificate the client should trust for TLS verification
44  #[must_use]
45  pub fn add_root_certificate(mut self, certificate: Certificate) -> Self {
46    self.root_certificates.push(certificate);
47    self
48  }
49
50  /// Set custom Accept headers
51  #[must_use]
52  pub fn accepted_types(mut self, accepted_types: Option<Vec<(MediaTypes, Option<f64>)>>) -> Self {
53    self.accepted_types = accepted_types;
54    self
55  }
56
57  /// Set the user-agent to be used for registry authentication.
58  #[must_use]
59  pub fn user_agent(mut self, user_agent: Option<String>) -> Self {
60    self.user_agent = user_agent;
61    self
62  }
63
64  /// Set the username to be used for registry authentication.
65  #[must_use]
66  pub fn username(mut self, user: Option<String>) -> Self {
67    self.username = user;
68    self
69  }
70
71  /// Set the password to be used for registry authentication.
72  #[must_use]
73  pub fn password(mut self, password: Option<String>) -> Self {
74    self.password = password;
75    self
76  }
77
78  /// Read credentials from a JSON config file
79  #[must_use]
80  pub fn read_credentials<T: ::std::io::Read>(mut self, reader: T) -> Self {
81    if let Ok(creds) = crate::get_credentials(reader, &self.index) {
82      self.username = creds.0;
83      self.password = creds.1;
84    };
85    self
86  }
87
88  /// Set the connect timeout for the HTTP client.
89  #[must_use]
90  pub fn connect_timeout(mut self, timeout: std::time::Duration) -> Self {
91    self.connect_timeout = Some(timeout);
92    self
93  }
94
95  /// Set the overall request timeout for the HTTP client.
96  #[must_use]
97  pub fn request_timeout(mut self, timeout: std::time::Duration) -> Self {
98    self.request_timeout = Some(timeout);
99    self
100  }
101
102  /// Return a `Client` to interact with a v2 registry.
103  pub fn build(self) -> Result<Client> {
104    let base = if self.insecure_registry {
105      "http://".to_string() + &self.index
106    } else {
107      "https://".to_string() + &self.index
108    };
109    trace!("Built client for {:?}: endpoint {:?}", self.index, base);
110    let creds = match (self.username, self.password) {
111      (None, None) => None,
112      (u, p) => Some((u.unwrap_or_else(|| "".into()), p.unwrap_or_else(|| "".into()))),
113    };
114
115    if self.insecure_registry && creds.is_some() {
116      log::warn!(
117        "Registry {:?} is configured with insecure HTTP; credentials will be transmitted in cleartext",
118        self.index
119      );
120    }
121
122    let mut builder = reqwest::ClientBuilder::new().danger_accept_invalid_certs(self.accept_invalid_certs);
123
124    if let Some(timeout) = self.connect_timeout {
125      builder = builder.connect_timeout(timeout);
126    }
127    if let Some(timeout) = self.request_timeout {
128      builder = builder.timeout(timeout);
129    }
130
131    for ca in self.root_certificates {
132      builder = builder.add_root_certificate(ca)
133    }
134
135    let client = builder.build()?;
136
137    let accepted_types = match self.accepted_types {
138      Some(a) => a,
139      None => match self.index == "gcr.io" || self.index.ends_with(".gcr.io") || self.index.ends_with(".k8s.io") {
140        false => vec![
141          // accept header types and their q value, as documented in
142          // https://tools.ietf.org/html/rfc7231#section-5.3.2
143          (MediaTypes::ManifestV2S2, Some(0.5)),
144          (MediaTypes::ManifestV2S1Signed, Some(0.4)),
145          (MediaTypes::ManifestList, Some(0.5)),
146          (MediaTypes::OciImageManifest, Some(0.5)),
147          (MediaTypes::OciImageIndexV1, Some(0.5)),
148        ],
149        // GCR incorrectly parses `q` parameters, so we use special Accept for it.
150        // Bug: https://issuetracker.google.com/issues/159827510.
151        // TODO: when bug is fixed, this workaround should be removed.
152        // *.k8s.io container registries use GCR and are similarly affected.
153        true => vec![
154          (MediaTypes::ManifestV2S2, None),
155          (MediaTypes::ManifestV2S1Signed, None),
156          (MediaTypes::ManifestList, None),
157          (MediaTypes::OciImageManifest, None),
158          (MediaTypes::OciImageIndexV1, None),
159        ],
160      },
161    };
162    let c = Client {
163      base_url: base,
164      credentials: creds,
165      user_agent: self.user_agent,
166      auth: None,
167      client,
168      accepted_types,
169    };
170    Ok(c)
171  }
172}
173
174impl Default for Config {
175  /// Initialize `Config` with default values.
176  fn default() -> Self {
177    Self {
178      index: "registry-1.docker.io".into(),
179      insecure_registry: false,
180      accept_invalid_certs: false,
181      root_certificates: Default::default(),
182      accepted_types: None,
183      user_agent: Some(crate::USER_AGENT.to_owned()),
184      username: None,
185      password: None,
186      connect_timeout: None,
187      request_timeout: None,
188    }
189  }
190}
191
192#[cfg(test)]
193mod tests {
194  use super::*;
195
196  #[test]
197  fn test_config_default() {
198    let config = Config::default();
199    assert_eq!(config.index, "registry-1.docker.io");
200    assert!(!config.insecure_registry);
201    assert!(!config.accept_invalid_certs);
202    assert!(config.username.is_none());
203    assert!(config.password.is_none());
204    assert!(config.accepted_types.is_none());
205    assert_eq!(config.user_agent.as_deref(), Some(crate::USER_AGENT));
206  }
207
208  #[test]
209  fn test_config_builder_chaining() {
210    let config = Config::default()
211      .registry("myregistry.io")
212      .insecure_registry(true)
213      .accept_invalid_certs(true)
214      .username(Some("user".to_string()))
215      .password(Some("pass".to_string()))
216      .user_agent(Some("test-agent".to_string()));
217
218    assert_eq!(config.index, "myregistry.io");
219    assert!(config.insecure_registry);
220    assert!(config.accept_invalid_certs);
221    assert_eq!(config.username.as_deref(), Some("user"));
222    assert_eq!(config.password.as_deref(), Some("pass"));
223    assert_eq!(config.user_agent.as_deref(), Some("test-agent"));
224  }
225
226  #[test]
227  fn test_config_build_insecure() {
228    let client = Config::default()
229      .registry("localhost:5000")
230      .insecure_registry(true)
231      .build()
232      .unwrap();
233    assert!(client.base_url.starts_with("http://"));
234  }
235
236  #[test]
237  fn test_config_build_secure() {
238    let client = Config::default()
239      .registry("myregistry.io")
240      .insecure_registry(false)
241      .build()
242      .unwrap();
243    assert!(client.base_url.starts_with("https://"));
244  }
245
246  #[test]
247  fn test_config_build_credentials() {
248    let client = Config::default()
249      .registry("myregistry.io")
250      .username(Some("user".to_string()))
251      .password(Some("pass".to_string()))
252      .build()
253      .unwrap();
254    assert_eq!(client.credentials, Some(("user".to_string(), "pass".to_string())));
255  }
256
257  #[test]
258  fn test_config_build_no_credentials() {
259    let client = Config::default().registry("myregistry.io").build().unwrap();
260    assert!(client.credentials.is_none());
261  }
262
263  #[test]
264  fn test_config_build_partial_credentials_username_only() {
265    let client = Config::default()
266      .registry("myregistry.io")
267      .username(Some("user".to_string()))
268      .build()
269      .unwrap();
270    assert_eq!(client.credentials, Some(("user".to_string(), "".to_string())));
271  }
272
273  #[test]
274  fn test_config_read_credentials() {
275    // base64("user:pass") = "dXNlcjpwYXNz"
276    let config_json = r#"{"auths":{"myregistry.io":{"auth":"dXNlcjpwYXNz"}}}"#;
277    let config = Config::default()
278      .registry("myregistry.io")
279      .read_credentials(config_json.as_bytes());
280    assert_eq!(config.username.as_deref(), Some("user"));
281    assert_eq!(config.password.as_deref(), Some("pass"));
282  }
283
284  #[test]
285  fn test_config_read_credentials_missing_registry() {
286    let config_json = r#"{"auths":{"other.io":{"auth":"dXNlcjpwYXNz"}}}"#;
287    let config = Config::default()
288      .registry("myregistry.io")
289      .read_credentials(config_json.as_bytes());
290    // Should silently fail and leave credentials as None
291    assert!(config.username.is_none());
292    assert!(config.password.is_none());
293  }
294
295  #[test]
296  fn test_config_timeout_fields() {
297    let config = Config::default()
298      .connect_timeout(std::time::Duration::from_secs(5))
299      .request_timeout(std::time::Duration::from_secs(30));
300
301    assert_eq!(config.connect_timeout, Some(std::time::Duration::from_secs(5)));
302    assert_eq!(config.request_timeout, Some(std::time::Duration::from_secs(30)));
303  }
304}