diff options
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 280 |
1 files changed, 242 insertions, 38 deletions
diff --git a/src/main.rs b/src/main.rs index 38a7ecb..150c993 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,7 @@ struct AuthInfo { } enum AuthStage { + Idle, Unauthorized, Authorized(String), } @@ -41,7 +42,7 @@ impl Auth { for info in infos { states.insert(info.host.clone(), AuthState { info: info, - stage: AuthStage::Unauthorized, + stage: AuthStage::Idle, }); } @@ -49,37 +50,256 @@ impl Auth { states } } +} - fn first(&mut self, host: &str) -> Option<String> { - if let Some(state) = self.states.get_mut(host) { - match state.stage { - AuthStage::Unauthorized => { return None }, - AuthStage::Authorized(ref x) => { return Some(x.clone()); }, - } +#[derive(Debug)] +enum AuthMethod { + Basic, + Bearer{realm: String}, +} + +#[derive(Debug)] +struct Challenge { + scheme: Range<usize>, + params: Vec<AuthParam>, +} + +fn parse_token(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + let mut pos = start; + while str.len() > pos { + match str[pos] { + 'a'..'z' | 'A'..'Z' => pos += 1, + _ => break, + } + } + + if pos == start { + return None; + } + + return Some((start..pos, pos)); +} + +fn parse_qdtext(str: &Vec<char>, start: usize) -> Option<usize> { + match str[start] { + '\u{0}'..'\u{25}' | '\u{127}' => return None, + _ => return Some(start + 1), + } +} + +fn parse_quotedpair(str: &Vec<char>, start: usize) -> Option<usize> { + if str[start] != '\\' { + return None; + } + + return Some(start + 2); +} + +fn parse_quotedstring(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + let mut pos = start; + if str[pos] != '"' { + return None; + } + pos += 1; + + loop { + if let Some(npos) = parse_qdtext(str, pos) { + pos = npos; + } else if let Some(npos) = parse_quotedpair(str, pos) { + pos = npos; + } else { + break; + } + } + + if str[pos] != '"' { + return None; + } + pos += 1; + + return Some((start+1..pos-1, pos)); +} + +fn parse_sp(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + let mut pos = start; + loop { + match str[pos] { + ' ' => pos += 1, + _ => break, } + } + if pos == start { return None; } - fn authenticate<T>(&mut self, host: &str, previous_response: &ureq::http::Response<T>) -> Option<String> { - 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 Some((start..pos, pos)); +} + +fn parse_crlf(str: &Vec<char>, start: usize) -> Option<usize> { + if str[start] == '\r' && str[start+1] == '\n' { + return Some(start + 2); + } + + return None; +} + +fn parse_lws(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + let mut pos = start; + + match parse_crlf(str, pos) { + Some(npos) => pos = npos, + None => {}, + } + + (_, pos) = parse_sp(str, pos).unwrap(); + loop { + match parse_sp(str, pos) { + Some((_, npos)) => pos = npos, + None => break, + } + } + + return Some((start..pos, pos)); +} + +#[derive(Debug)] +struct AuthParam { + key: Range<usize>, + value: Range<usize>, +} + +fn parse_param(str: &Vec<char>, start: usize) -> Option<(AuthParam, usize)> { + let pos = start; + let (param_range, pos) = parse_token(str, pos)?; + + let mut pos = pos; + if str[pos] == '=' { + pos += 1; + } else { + todo!(); + } + + let value_range; + if let Some((range, npos)) = parse_token(str, pos) { + value_range = range; + pos = npos; + } else if let Some((range, npos)) = parse_quotedstring(str, pos) { + value_range = range; + pos = npos; + } else { + todo!(); + } + + return Some((AuthParam{key: param_range, value: value_range}, pos)); +} + +fn parse_challenge(str: &Vec<char>, start: usize) -> Option<(Challenge, usize)> { + let pos = start; + let (scheme_range, pos) = parse_token(str, pos).unwrap(); + + let (_, pos) = parse_sp(str, pos).unwrap(); + let mut pos = pos; + loop { + match parse_sp(str, pos) { + Some((_, npos)) => pos = npos, + None => break, + } + } + + let mut params = vec!(); + + let (param, pos) = parse_param(str, pos).unwrap(); + params.push(param); + let mut pos = pos; + loop { + match parse_param(str, pos) { + Some((param, npos)) => { + pos = npos; + params.push(param); + }, + None => break, + } + } + + return Some((Challenge {scheme: scheme_range, params}, pos)); +} + +impl AuthMethod { + fn from_header(header: &ureq::http::HeaderValue) -> Option<Self> { + let header_str = header.to_str().unwrap(); + let (parse, _) = parse_challenge(&header_str.chars().collect::<Vec<char>>(), 0).unwrap(); + + if &header_str[parse.scheme.clone()] == "Basic" { + return Some(AuthMethod::Basic); + } + + if &header_str[parse.scheme.clone()] == "Bearer" { + let mut realm_param = None; + for param in parse.params { + if &header_str[param.key.clone()] == "realm" { + realm_param = Some(param); + } } + + return Some(AuthMethod::Bearer{realm: header_str[realm_param.unwrap().value].to_string()}); } return None; } } +fn perform_registry_request(registry: &str, url: String, auth: &mut Auth) -> Result<ureq::http::Response<ureq::Body>, ()> { + let mut state = auth.states.get_mut(registry); + + let url = format!("https://{}{}", registry, &url); + + + for _ in 0..2 { + let mut request = ureq::get(&url) + .config().http_status_as_error(false).build(); + + if let Some(ref state) = state { + if let AuthStage::Authorized(x) = &state.stage { + request = request.header("Authorization", x); + } + } + + let response = request.call().unwrap(); + + if response.status() == 401 { + if let Some(ref mut state) = state { + if let AuthStage::Idle = state.stage { + if let Some(auth_header) = response.headers().get("www-authenticate") { + dbg!(AuthMethod::from_header(auth_header)); + let auth_header = auth_header.to_str().unwrap(); + if auth_header.starts_with("Basic") { + let basic_auth = format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", state.info.username, state.info.password))); + state.stage = AuthStage::Authorized(basic_auth); + + continue; // try again + } else if auth_header.starts_with("Bearer") { + } else { + todo!("Implement other authentication scheme"); + } + } else { + todo!("Server didn't ask us to authenticate"); + } + } else { + todo!("Authorized request somehow failed (expired token?)"); + } + } else { + todo!("Server returned 401 but we have no credentials"); + } + } + + return Ok(response); + } + + + return Err(()); +} + enum YContext { InDocument, InObject, @@ -236,26 +456,10 @@ fn fetch_new_image(file: &str, auth: &mut Auth, img: DockerRef, edits: &mut Vec< // 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 = 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(); + let url = format!("/v2/{}/manifests/{}", &file[img.image], tag); + let mut response = perform_registry_request(registry, url, auth).unwrap(); + let body: tinyjson::JsonValue = response.body_mut().read_to_string().unwrap().parse().unwrap(); let media_type : &String = body["mediaType"].get().unwrap(); assert!(media_type == "application/vnd.docker.distribution.manifest.v2+json"); |
