From ac8789e257f5e4a477192bbbd8ecc2676bb485e5 Mon Sep 17 00:00:00 2001 From: Anton Ageev Date: Sun, 31 Mar 2019 03:20:21 +0300 Subject: Add a registry authentication. (#157) --- Cargo.toml | 1 + examples/imagepull_auth.rs | 28 ++++++++ src/builder.rs | 164 ++++++++++++++++++++++++++++++++++++++++++++- src/lib.rs | 27 +++++--- src/transport.rs | 22 ++++-- 5 files changed, 226 insertions(+), 16 deletions(-) create mode 100644 examples/imagepull_auth.rs diff --git a/Cargo.toml b/Cargo.toml index 1ee57a5..23e803d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ tokio-io = "0.1.11" url = "1.7.2" serde = { version = "1.0.87", features = ["derive"] } serde_json = "1.0.38" +base64 = "0.10" [dev-dependencies] env_logger = "0.6.0" diff --git a/examples/imagepull_auth.rs b/examples/imagepull_auth.rs new file mode 100644 index 0000000..1c559c7 --- /dev/null +++ b/examples/imagepull_auth.rs @@ -0,0 +1,28 @@ +// cargo run --example imagepull_auth busybox username password + +use shiplift::{Docker, PullOptions, RegistryAuth}; +use std::env; +use tokio::prelude::{Future, Stream}; + +fn main() { + env_logger::init(); + let docker = Docker::new(); + let img = env::args() + .nth(1) + .expect("You need to specify an image name"); + let username = env::args().nth(2).expect("You need to specify an username"); + let password = env::args().nth(3).expect("You need to specify a password"); + let auth = RegistryAuth::builder() + .username(username) + .password(password) + .build(); + let fut = docker + .images() + .pull(&PullOptions::builder().image(img).auth(auth).build()) + .for_each(|output| { + println!("{:?}", output); + Ok(()) + }) + .map_err(|e| eprintln!("Error: {}", e)); + tokio::run(fut); +} diff --git a/src/builder.rs b/src/builder.rs index 4923a24..f0c7d7e 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -11,8 +11,116 @@ use std::{ }; use url::form_urlencoded; +#[derive(Clone, Serialize)] +#[serde(untagged)] +pub enum RegistryAuth { + Password { + username: String, + password: String, + + #[serde(skip_serializing_if = "Option::is_none")] + email: Option, + + #[serde(rename = "serveraddress")] + #[serde(skip_serializing_if = "Option::is_none")] + server_address: Option, + }, + Token { + #[serde(rename = "identitytoken")] + identity_token: String, + }, +} + +impl RegistryAuth { + /// return a new instance with token authentication + pub fn token(token: S) -> RegistryAuth + where + S: Into, + { + RegistryAuth::Token { + identity_token: token.into(), + } + } + + /// return a new instance of a builder for authentication + pub fn builder() -> RegistryAuthBuilder { + RegistryAuthBuilder::default() + } + + /// serialize authentication as JSON in base64 + pub fn serialize(&self) -> String { + serde_json::to_string(self) + .map(|c| base64::encode(&c)) + .unwrap() + } +} + +#[derive(Default)] +pub struct RegistryAuthBuilder { + username: Option, + password: Option, + email: Option, + server_address: Option, +} + +impl RegistryAuthBuilder { + pub fn username( + &mut self, + username: I, + ) -> &mut Self + where + I: Into, + { + self.username = Some(username.into()); + self + } + + pub fn password( + &mut self, + password: I, + ) -> &mut Self + where + I: Into, + { + self.password = Some(password.into()); + self + } + + pub fn email( + &mut self, + email: I, + ) -> &mut Self + where + I: Into, + { + self.email = Some(email.into()); + self + } + + pub fn server_address( + &mut self, + server_address: I, + ) -> &mut Self + where + I: Into, + { + self.server_address = Some(server_address.into()); + self + } + + pub fn build(&self) -> RegistryAuth { + RegistryAuth::Password { + username: self.username.clone().unwrap_or_else(String::new), + password: self.password.clone().unwrap_or_else(String::new), + email: self.email.clone(), + server_address: self.server_address.clone(), + } + } +} + #[derive(Default)] pub struct PullOptions { + auth: Option, params: HashMap<&'static str, String>, } @@ -34,10 +142,15 @@ impl PullOptions { ) } } + + pub(crate) fn auth_header(&self) -> Option { + self.auth.clone().map(|a| a.serialize()) + } } #[derive(Default)] pub struct PullOptionsBuilder { + auth: Option, params: HashMap<&'static str, String>, } @@ -95,8 +208,17 @@ impl PullOptionsBuilder { self } - pub fn build(&self) -> PullOptions { + pub fn auth( + &mut self, + auth: RegistryAuth, + ) -> &mut Self { + self.auth = Some(auth); + self + } + + pub fn build(&mut self) -> PullOptions { PullOptions { + auth: self.auth.take(), params: self.params.clone(), } } @@ -1370,7 +1492,7 @@ impl VolumeCreateOptionsBuilder { #[cfg(test)] mod tests { - use super::ContainerOptionsBuilder; + use super::{ContainerOptionsBuilder, RegistryAuth}; #[test] fn container_options_simple() { @@ -1463,4 +1585,42 @@ mod tests { options.serialize().unwrap() ); } + + /// Test registry auth with token + #[test] + fn registry_auth_token() { + let options = RegistryAuth::token("abc"); + assert_eq!( + base64::encode(r#"{"identitytoken":"abc"}"#), + options.serialize() + ); + } + + /// Test registry auth with username and password + #[test] + fn registry_auth_password_simple() { + let options = RegistryAuth::builder() + .username("user_abc") + .password("password_abc") + .build(); + assert_eq!( + base64::encode(r#"{"username":"user_abc","password":"password_abc"}"#), + options.serialize() + ); + } + + /// Test registry auth with all fields + #[test] + fn registry_auth_password_all() { + let options = RegistryAuth::builder() + .username("user_abc") + .password("password_abc") + .email("email_abc") + .server_address("https://example.org") + .build(); + assert_eq!( + base64::encode(r#"{"username":"user_abc","password":"password_abc","email":"email_abc","serveraddress":"https://example.org"}"#), + options.serialize() + ); + } } diff --git a/src/lib.rs b/src/lib.rs index 5918205..61a8bde 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,8 +29,8 @@ pub use crate::{ builder::{ BuildOptions, ContainerConnectionOptions, ContainerFilter, ContainerListOptions, ContainerOptions, EventsOptions, ExecContainerOptions, ImageFilter, ImageListOptions, - LogsOptions, NetworkCreateOptions, NetworkListOptions, PullOptions, RmContainerOptions, - VolumeCreateOptions, + LogsOptions, NetworkCreateOptions, NetworkListOptions, PullOptions, RegistryAuth, + RmContainerOptions, VolumeCreateOptions, }, errors::Error, }; @@ -55,7 +55,7 @@ use mime::Mime; #[cfg(feature = "tls")] use openssl::ssl::{SslConnector, SslFiletype, SslMethod}; use serde_json::Value; -use std::{borrow::Cow, env, path::Path, time::Duration}; +use std::{borrow::Cow, env, iter, path::Path, time::Duration}; use tokio_codec::{FramedRead, LinesCodec}; use url::form_urlencoded; @@ -141,7 +141,11 @@ impl<'a> Images<'a> { match tarball::dir(&mut bytes, &opts.path[..]) { Ok(_) => Box::new( self.docker - .stream_post(&path.join("?"), Some((Body::from(bytes), tar()))) + .stream_post( + &path.join("?"), + Some((Body::from(bytes), tar())), + None::>, + ) .and_then(|bytes| { serde_json::from_slice::<'_, Value>(&bytes[..]) .map_err(Error::from) @@ -194,8 +198,11 @@ impl<'a> Images<'a> { if let Some(query) = opts.serialize() { path.push(query); } + let headers = opts + .auth_header() + .map(|a| iter::once(("X-Registry-Auth", a))); self.docker - .stream_post::(&path.join("?"), None) + .stream_post::(&path.join("?"), None, headers) // todo: give this a proper enum type .map(|r| { futures::stream::iter_result( @@ -479,6 +486,7 @@ impl<'a, 'b> Container<'a, 'b> { let chunk_stream = StreamReader::new(docker2.stream_post( &format!("/exec/{}/start", id)[..], Some((bytes, mime::APPLICATION_JSON)), + None::>, )); FramedRead::new(chunk_stream, decoder) }) @@ -1053,15 +1061,18 @@ impl Docker { }) } - fn stream_post( + fn stream_post( &self, endpoint: &str, body: Option<(B, Mime)>, + headers: Option, ) -> impl Stream where B: Into, + H: IntoIterator, { - self.transport.stream_chunks(Method::POST, endpoint, body) + self.transport + .stream_chunks(Method::POST, endpoint, body, headers) } fn stream_get( @@ -1069,7 +1080,7 @@ impl Docker { endpoint: &str, ) -> impl Stream { self.transport - .stream_chunks::(Method::GET, endpoint, None) + .stream_chunks::>(Method::GET, endpoint, None, None) } fn stream_post_upgrade_multiplexed( diff --git a/src/transport.rs b/src/transport.rs index da506b3..ce089fc 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -19,7 +19,7 @@ use log::debug; use mime::Mime; use serde::{Deserialize, Serialize}; use serde_json; -use std::fmt; +use std::{fmt, iter}; use tokio_io::{AsyncRead, AsyncWrite}; pub fn tar() -> Mime { @@ -76,7 +76,7 @@ impl Transport { B: Into, { let endpoint = endpoint.to_string(); - self.stream_chunks(method, &endpoint, body) + self.stream_chunks(method, &endpoint, body, None::>) .concat2() .and_then(|v| { String::from_utf8(v.to_vec()) @@ -87,17 +87,19 @@ impl Transport { } /// Make a request and return a `Stream` of `Chunks` as they are returned. - pub fn stream_chunks( + pub fn stream_chunks( &self, method: Method, endpoint: &str, body: Option<(B, Mime)>, + headers: Option, ) -> impl Stream where B: Into, + H: IntoIterator, { let req = self - .build_request(method, endpoint, body, |_| ()) + .build_request(method, endpoint, body, headers, |_| ()) .expect("Failed to build request!"); self.send_request(req) @@ -140,15 +142,17 @@ impl Transport { } /// Builds an HTTP request. - fn build_request( + fn build_request( &self, method: Method, endpoint: &str, body: Option<(B, Mime)>, + headers: Option, f: impl FnOnce(&mut ::http::request::Builder), ) -> Result> where B: Into, + H: IntoIterator, { let mut builder = Request::builder(); f(&mut builder); @@ -169,6 +173,12 @@ impl Transport { }; let req = req.header(header::HOST, ""); + if let Some(h) = headers { + for (k, v) in h.into_iter() { + req.header(k, v); + } + } + match body { Some((b, c)) => Ok(req .header(header::CONTENT_TYPE, &c.to_string()[..]) @@ -215,7 +225,7 @@ impl Transport { }; let req = self - .build_request(method, endpoint, body, |builder| { + .build_request(method, endpoint, body, None::>, |builder| { builder .header(header::CONNECTION, "Upgrade") .header(header::UPGRADE, "tcp"); -- cgit v1.2.3