From 2fa67bbcbc31e2790eb718b8bc224b41a844ff37 Mon Sep 17 00:00:00 2001 From: Yezzi Hsueh Date: Mon, 13 Nov 2023 21:19:20 +0800 Subject: [PATCH] refactor: extract authentication function --- src/bin/dockertags.rs | 21 ++++--- src/{docker.rs => docker_hub.rs} | 10 ++- src/lib.rs | 4 +- src/registry.rs | 101 ++++++------------------------- src/registry/docker_auth.rs | 27 +++++++++ src/registry/docker_config.rs | 49 +++++++++++++++ 6 files changed, 118 insertions(+), 94 deletions(-) rename src/{docker.rs => docker_hub.rs} (92%) create mode 100644 src/registry/docker_auth.rs create mode 100644 src/registry/docker_config.rs diff --git a/src/bin/dockertags.rs b/src/bin/dockertags.rs index bbb752f..e743341 100644 --- a/src/bin/dockertags.rs +++ b/src/bin/dockertags.rs @@ -1,8 +1,8 @@ use std::io::Write; use clap::{arg, Parser}; -//use docker_tags::docker::DockerHubTagsFetcher; -use docker_tags::{DockerTags, QueryArgs}; -use docker_tags::registry::RegistryTagsFetcher; +use docker_tags::docker_hub::DockerHubTagsFetcher; +use docker_tags::{DockerTagsFetcher, QueryArgs}; +use docker_tags::registry::{docker_config, RegistryTagsFetcher}; #[derive(Parser)] #[command(name = "dockertags", author, version, about = "List all tags for a Docker image on a remote registry", long_about = "List all tags for a Docker image on a remote registry")] @@ -22,10 +22,17 @@ struct Args { fn main() { let args = Args::parse(); let query_args = QueryArgs::new(args.repository.as_str(), args.filter, args.arch); - // let docker_tags_fetcher = DockerHubTagsFetcher {}; - let docker_tags_fetcher = RegistryTagsFetcher {}; - let tags = docker_tags_fetcher.get_tags(&query_args); - + let config = docker_config::read_config(); + let tags = match config { + None => { + let fetcher = DockerHubTagsFetcher::new(); + fetcher.get_tags(&query_args) + } + Some(x) => { + let fetcher = RegistryTagsFetcher::new(&x); + fetcher.get_tags(&query_args) + } + }; for tag in tags { let _ = writeln!(std::io::stdout(), "{}", &tag); } diff --git a/src/docker.rs b/src/docker_hub.rs similarity index 92% rename from src/docker.rs rename to src/docker_hub.rs index d4359cf..61ebcbe 100644 --- a/src/docker.rs +++ b/src/docker_hub.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use crate::{DockerTags, QueryArgs}; +use crate::{DockerTagsFetcher, QueryArgs}; #[derive(Serialize, Deserialize)] struct DockerResult { @@ -45,7 +45,13 @@ struct DockerResponse { pub struct DockerHubTagsFetcher {} -impl DockerTags for DockerHubTagsFetcher { +impl DockerHubTagsFetcher { + pub fn new() -> DockerHubTagsFetcher { + DockerHubTagsFetcher {} + } +} + +impl DockerTagsFetcher for DockerHubTagsFetcher { fn get_tags(&self, args: &QueryArgs) -> Vec { let page = 0; let page_size = 1000; diff --git a/src/lib.rs b/src/lib.rs index 64d63b0..7d6fff8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ pub mod registry; -pub mod docker; +pub mod docker_hub; /// query args pub struct QueryArgs { @@ -9,7 +9,7 @@ pub struct QueryArgs { pub arch: Option, } -pub trait DockerTags { +pub trait DockerTagsFetcher { /// query tags fn get_tags(&self, args: &QueryArgs) -> Vec; } diff --git a/src/registry.rs b/src/registry.rs index fc401cc..68c0d30 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -1,8 +1,9 @@ -use std::collections::HashMap; -use std::fs; -use directories::UserDirs; +pub mod docker_auth; +pub mod docker_config; + use serde::{Deserialize, Serialize}; -use crate::{DockerTags, QueryArgs}; +use crate::{DockerTagsFetcher, QueryArgs}; +use crate::registry::docker_config::DockerPassword; /// registry api response struct #[derive(Serialize, Deserialize)] @@ -11,23 +12,25 @@ struct RegistryTagsResponse { tags: Option>, } -#[derive(Serialize, Deserialize)] -struct RegistryTokenResponse { - token: Option, - access_token: Option, - expires_in: u32, - issued_at: Option, +pub struct RegistryTagsFetcher { + username: String, + password: String, } +impl RegistryTagsFetcher { + pub fn new(docker_password: &DockerPassword) -> RegistryTagsFetcher { + RegistryTagsFetcher { + username: docker_password.username.to_string(), + password: docker_password.password.to_string(), + } + } +} -pub struct RegistryTagsFetcher {} - -impl DockerTags for RegistryTagsFetcher { +impl DockerTagsFetcher for RegistryTagsFetcher { fn get_tags(&self, args: &QueryArgs) -> Vec { let namespace = args.namespace.as_str(); let repository = args.repository.as_str(); - - let token = get_token(namespace, repository).unwrap(); + let token = docker_auth::get_token(namespace, repository, self.username.as_str(), self.password.as_str()).unwrap(); let url = format!("https://registry-1.docker.io/v2/{namespace}/{repository}/tags/list"); let client = reqwest::blocking::Client::new(); let response = client.get(url) @@ -41,71 +44,3 @@ impl DockerTags for RegistryTagsFetcher { results } } - -/// get the access token for tags request -fn get_token(namespace: &str, repository: &str) -> Option { - let config = match read_config() { - None => { return None; } - Some(x) => { x } - }; - let service = "registry.docker.io"; - let scope = format!("repository:{namespace}/{repository}:pull"); - let auth = format!("https://auth.docker.io/token?service={service}&scope={scope}"); - let client = reqwest::blocking::Client::new(); - let response = client.get(auth) - .basic_auth(config.username, Some(config.password)) - .send(); - match response { - Ok(r) => { - let json: RegistryTokenResponse = serde_json::from_str(r.text().unwrap().as_str()).unwrap(); - Some(json.token.unwrap()) - } - Err(_) => { None } - } -} - -#[derive(Serialize, Deserialize)] -struct DockerConfig { - auths: HashMap, -} - -#[derive(Serialize, Deserialize)] -struct DockerAuth { - auth: Option, -} - -use base64::{Engine as _, engine::general_purpose}; - -/// read authentication info from local docker config file -fn read_config() -> Option { - let user_dirs = UserDirs::new().unwrap(); - let home_dir = user_dirs.home_dir(); - let config_path = home_dir.join(".docker").join("config.json"); - let config_context = fs::read_to_string(config_path); - let docker_config: DockerConfig = serde_json::from_str(config_context.unwrap().as_str()).unwrap(); - let auths = docker_config.auths; - let auth = match auths.get("https://index.docker.io/v1/") { - None => { return None; } - Some(docker_auth) => { docker_auth.auth.clone().unwrap() } - }; - let mut buffer = Vec::::new(); - let _result = general_purpose::URL_SAFE_NO_PAD.decode_vec(auth.as_bytes(), &mut buffer); - let decoded = match std::str::from_utf8(&buffer) { - Ok(v) => v, - Err(e) => panic!("Invalid UTF-8 sequence: {}", e), - }; - let idx = match decoded.find(':') { - None => { panic!("Invalid authentication: {}", decoded) } - Some(i) => { i } - }; - return Some(DockerPassword { - username: String::from(&decoded[0..idx]), - password: String::from(&decoded[(idx + 1)..]), - }); -} - -#[derive(Serialize, Deserialize)] -struct DockerPassword { - username: String, - password: String, -} \ No newline at end of file diff --git a/src/registry/docker_auth.rs b/src/registry/docker_auth.rs new file mode 100644 index 0000000..382ede2 --- /dev/null +++ b/src/registry/docker_auth.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +struct RegistryTokenResponse { + token: Option, + access_token: Option, + expires_in: u32, + issued_at: Option, +} + +/// get the access token for tags request +pub fn get_token(namespace: &str, repository: &str, username: &str, password: &str) -> Option { + let service = "registry.docker.io"; + let scope = format!("repository:{namespace}/{repository}:pull"); + let auth = format!("https://auth.docker.io/token?service={service}&scope={scope}"); + let client = reqwest::blocking::Client::new(); + let response = client.get(auth) + .basic_auth(username, Some(password)) + .send(); + match response { + Ok(r) => { + let json: RegistryTokenResponse = serde_json::from_str(r.text().unwrap().as_str()).unwrap(); + Some(json.token.unwrap()) + } + Err(_) => { None } + } +} diff --git a/src/registry/docker_config.rs b/src/registry/docker_config.rs new file mode 100644 index 0000000..512b853 --- /dev/null +++ b/src/registry/docker_config.rs @@ -0,0 +1,49 @@ +use std::collections::HashMap; +use std::fs; +use base64::{Engine as _, engine::general_purpose}; +use directories::UserDirs; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +struct DockerConfig { + auths: HashMap, +} + +#[derive(Serialize, Deserialize)] +struct DockerAuth { + auth: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct DockerPassword { + pub username: String, + pub password: String, +} + +/// read authentication info from local docker config file +pub fn read_config() -> Option { + let user_dirs = UserDirs::new().unwrap(); + let home_dir = user_dirs.home_dir(); + let config_path = home_dir.join(".docker").join("config.json"); + let config_context = fs::read_to_string(config_path); + let docker_config: DockerConfig = serde_json::from_str(config_context.unwrap().as_str()).unwrap(); + let auths = docker_config.auths; + let auth = match auths.get("https://index.docker.io/v1/") { + None => { return None; } + Some(docker_auth) => { docker_auth.auth.clone().unwrap() } + }; + let mut buffer = Vec::::new(); + let _result = general_purpose::URL_SAFE_NO_PAD.decode_vec(auth.as_bytes(), &mut buffer); + let decoded = match std::str::from_utf8(&buffer) { + Ok(v) => v, + Err(e) => panic!("Invalid UTF-8 sequence: {}", e), + }; + let idx = match decoded.find(':') { + None => { panic!("Invalid authentication: {}", decoded) } + Some(i) => { i } + }; + return Some(DockerPassword { + username: String::from(&decoded[0..idx]), + password: String::from(&decoded[(idx + 1)..]), + }); +}