From 1166c36e26829b4af3ca6a6fb2247cdd64e0ca95 Mon Sep 17 00:00:00 2001 From: Jesper Jensen Date: Tue, 16 Dec 2025 07:50:24 +0100 Subject: Add multiregistry login --- src/main.rs | 131 +++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 94 insertions(+), 37 deletions(-) (limited to 'src') diff --git a/src/main.rs b/src/main.rs index fefa4cf..38a7ecb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use base64::prelude::*; use yaml_rust2::parser::Parser; use yaml_rust2::Event; +use std::collections::HashMap; use std::ops::Range; fn help(cmd: &str) { @@ -9,32 +10,73 @@ fn help(cmd: &str) { Search FILE for docker images and suggest updates Options: ---auth Authenticate against registry", +--auth Authenticate against REGISTRY (repeatable)", cmd ); } +struct AuthInfo { + host: String, + username: String, + password: String, +} + +enum AuthStage { + Unauthorized, + Authorized(String), +} + +struct AuthState { + info: AuthInfo, + stage: AuthStage, +} + struct Auth { - header: Option, + states: HashMap } impl Auth { - fn new() -> Self { - return Auth{ - header: None, - }; + fn new(infos: Vec) -> Self { + let mut states = HashMap::new(); + for info in infos { + states.insert(info.host.clone(), AuthState { + info: info, + stage: AuthStage::Unauthorized, + }); + } + + return Auth { + states + } } - fn parse(&mut self, username: &str, password: &str) { - self.header = Some(format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", username, password)))); + fn first(&mut self, host: &str) -> Option { + if let Some(state) = self.states.get_mut(host) { + match state.stage { + AuthStage::Unauthorized => { return None }, + AuthStage::Authorized(ref x) => { return Some(x.clone()); }, + } + } + + return None; } - fn apply(&self, req: ureq::RequestBuilder) -> ureq::RequestBuilder { - if let Some(header) = &self.header { - return req.header("Authorization", header.clone()); - } else { - return req; + fn authenticate(&mut self, host: &str, previous_response: &ureq::http::Response) -> Option { + if let Some(state) = self.states.get_mut(host) { + match previous_response.headers().get("WWW-Authenticate").map(|x| x.to_str()) { + Some(Ok("basic")) => { + let header = format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", state.info.username, state.info.password))); + state.stage = AuthStage::Authorized(header.clone()); + + return Some(header); + }, + Some(Ok("bearer")) => todo!(), + Some(_) => return None, + None => return None, + } } + + return None; } } @@ -153,7 +195,7 @@ impl DockerRef{ let mut tag = None; if let Some(idx) = file[string_range.clone()].rfind(":") { tag = Some(string_range.start+idx+1..string_range.end); - string_range.end = idx; + string_range.end = string_range.start+idx; } let mut registry = None; @@ -188,37 +230,45 @@ struct Update { content: String, } -fn fetch_new_image(file: &str, auth: &Auth, img: DockerRef, edits: &mut Vec) { - let mut resulting_ref = img.clone(); - - dbg!(&img); - let registry = img.registry.map(|x| &file[x]).unwrap_or("https://registry.jnsn.dev/"); +fn fetch_new_image(file: &str, auth: &mut Auth, img: DockerRef, edits: &mut Vec) { + let registry = img.registry.map(|x| &file[x]).unwrap_or("registry.jnsn.dev/"); let tag = img.tag.map(|x| &file[x]).unwrap_or("latest"); - if tag == "latest" || img.digest.is_some() { + // Find the digest for the newest image + if let Some(digest) = img.digest { let url = format!("https://{}/v2/{}/manifests/{}", registry, &file[img.image], tag); dbg!(&url); - let mut response = auth.apply(ureq::get(url)) + let mut response = ureq::get(&url) + .header("Authorization", auth.first(registry)) .call().unwrap(); + if response.status() == 401 { + response = ureq::get(&url) + .header("Authorize", auth.authenticate(registry, &response).unwrap()) + .call().unwrap(); + } + let body: tinyjson::JsonValue = response .body_mut() - .read_to_string().unwrap().parse().unwrap(); + .read_to_string() + .unwrap() + .parse() + .unwrap(); let media_type : &String = body["mediaType"].get().unwrap(); assert!(media_type == "application/vnd.docker.distribution.manifest.v2+json"); - let digest = &response.headers()["docker-content-digest"]; - // resulting_ref.digest = Some(digest.to_str().unwrap().to_string()); - - // dbg!(&resulting_ref.raw[resulting_ref.digest.as_ref().unwrap().clone()]); - dbg!(&resulting_ref); + let image_ref = { + let prefix = &file[img.full_range.start..digest.start]; + let digest = &response.headers()["docker-content-digest"].to_str().unwrap(); + format!("{}{}", prefix, digest) + }; edits.push(Update{ position: img.full_range, - content: "AHH".to_string(), + content: image_ref, }); } } @@ -228,17 +278,21 @@ fn main() { let mut it = argv.iter(); let cmd = &it.next().unwrap(); - let mut auth = Auth::new(); + let mut auths = vec![]; let mut positional: Vec<&str> = vec!(); loop { match it.next().map(|x| x.as_str()) { None => break, Some("--auth") => { - if let Some(username) = it.next() && let Some(password) = it.next() { - auth.parse(username, password); + if let Some(registry) = it.next() && let Some(username) = it.next() && let Some(password) = it.next() { + auths.push(AuthInfo { + host: registry.clone(), + username: username.clone(), + password: password.clone(), + }); } else { - println!("Error: --auth requires two parameters"); + println!("Error: --auth requires three parameters"); help(cmd); std::process::exit(1); } @@ -251,11 +305,14 @@ fn main() { }; } + if positional.len() != 1 { panic!("Bad arguments"); } let file = positional[0]; + let mut auth = Auth::new(auths); + let file_content = &std::fs::read_to_string(file).unwrap(); let yaml = Parser::new_from_str(&file_content); let images : Vec<_> = scan_yaml_for_images(yaml); @@ -264,16 +321,16 @@ fn main() { let images : Vec<_> = images.iter() .map(|x| DockerRef::parse(&file_content, x)) - .map(|x| fetch_new_image(&file_content, &auth, x, &mut edits)) + .map(|x| fetch_new_image(&file_content, &mut auth, x, &mut edits)) .collect(); dbg!(&images); dbg!(&edits); // dbg!(&file_content[images[0].digest.as_ref().unwrap().clone()]); - let body: String = auth.apply(ureq::get("https://registry.jnsn.dev/v2/autobrr/tags/list")) - .call().unwrap() - .body_mut() - .read_to_string().unwrap(); + // let body: String = auth.apply(ureq::get("https://registry.jnsn.dev/v2/autobrr/tags/list")) + // .call().unwrap() + // .body_mut() + // .read_to_string().unwrap(); // dbg!(body); } -- cgit v1.2.3