diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.rs | 654 | ||||
| -rw-r--r-- | src/rfc8288.rs | 72 |
2 files changed, 668 insertions, 58 deletions
diff --git a/src/main.rs b/src/main.rs index 074b580..dd7eb46 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,11 @@ +mod rfc8288; + use base64::prelude::*; use yaml_rust2::parser::Parser; use yaml_rust2::Event; use std::collections::HashMap; use std::ops::Range; +use std::sync; fn help(cmd: &str) { println!( @@ -52,6 +55,62 @@ impl Auth { } } +impl AuthState{ + fn add_to_request<T>(&self, req: ureq::RequestBuilder<T>) -> ureq::RequestBuilder<T> { + if let AuthStage::Authorized(x) = &self.stage { + return req.header("Authorization", x); + } + + return req + } + + fn attempt_authorization(&mut self, response: &ureq::http::Response<ureq::Body>) -> Result<(), ()> { + if let AuthStage::Idle = self.stage { + if let Some(auth_header) = response.headers().get("www-authenticate") { + let auth_header = AuthMethod::from_header(auth_header); + match auth_header { + Some(AuthMethod::Basic) => { + let basic_auth = format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", self.info.username, self.info.password))); + self.stage = AuthStage::Authorized(basic_auth); + + return Ok(()); + }, + Some(AuthMethod::Bearer{realm, scope, service}) => { + let basic_auth = format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", self.info.username, self.info.password))); + + let url = format!("{}?service={}&scope={}", realm, service, scope); + + let body = ureq::get(url) + .config().http_status_as_error(false).build() + .header("Authorization", basic_auth) + .call(); + + dbg!(&body); + + let body = body.unwrap() + .body_mut().read_to_string().unwrap(); + + dbg!(&body); + + let body: tinyjson::JsonValue = body + .parse().unwrap(); + + let token: &String = body["token"].get().unwrap(); + + self.stage = AuthStage::Authorized(format!("Bearer {}", token)); + return Ok(()); + }, + None => todo!("Failed parsing the challenge header"), + } + } else { + todo!("Server didn't ask us to authenticate"); + } + } else { + todo!("Authorized request somehow failed (expired token?)"); + } + } +} + #[derive(Debug)] enum AuthMethod { Basic, @@ -82,7 +141,7 @@ fn parse_token(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { fn parse_qdtext(str: &Vec<char>, start: usize) -> Option<usize> { match str[start] { - '\u{0}'..'\u{25}' | '\u{127}' => return None, + '\x00'..'\x1F' | '"' | '\x7F' => return None, _ => return Some(start + 1), } } @@ -197,7 +256,7 @@ fn parse_param(str: &Vec<char>, start: usize) -> Option<(AuthParam, usize)> { value_range = range; pos = npos; } else { - todo!(); + todo!("{:?}", &str[pos..]); } return Some((AuthParam{key: param_range, value: value_range}, pos)); @@ -277,7 +336,6 @@ impl AuthMethod { let mut scope_param = None; let mut service_param = None; for param in parse.params { - dbg!(&header_str[param.key.clone()]); match &header_str[param.key.clone()] { "realm" => realm_param = Some(param), "scope" => scope_param = Some(param), @@ -297,70 +355,462 @@ impl AuthMethod { } } -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); +#[derive(Debug)] +enum LinkParam { + Rel{relation: Range<usize>}, +} - let url = format!("https://{}{}", registry, &url); +impl LinkParam { + fn value(&self) -> &Range<usize> { + match self { + LinkParam::Rel{relation} => &relation, + } + } +} + +#[derive(Debug)] +struct LinkValue { + uri: Range<usize>, + params: Vec<LinkParam>, +} +fn parse_alpha(str: &Vec<char>, pos: usize) -> Option<(Range<usize>, usize)> { + return match str[pos] { + 'a'..'z' | 'A'..'Z' => Some((pos..pos+1, pos+1)), + _ => None, + } +} - for _ in 0..2 { - let mut request = ureq::get(&url) - .config().http_status_as_error(false).build(); +fn parse_digit(str: &Vec<char>, pos: usize) -> Option<(Range<usize>, usize)> { + return match str[pos] { + '0'..'9' => Some((pos..pos+1, pos+1)), + _ => None, + } +} - if let Some(ref state) = state { - if let AuthStage::Authorized(x) = &state.stage { - request = request.header("Authorization", x); +fn parse_scheme(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + let mut pos = start; + + let (_, npos) = parse_alpha(str, pos)?; + pos = npos; + + loop { + if let Some((_, npos)) = parse_alpha(str, pos) { + pos = npos; + } else { + break; + } + }; + + return None; +} + +fn parse_authority(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + return None; +} + +fn parse_relative_part_1(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + let mut pos = start; + + if str[pos] != '/' && str[pos+1] != '/' { + return None; + } + pos += 2; + + let (_, npos) = parse_authority(str, pos)?; + pos = npos; + + return Some((start..pos, pos)); +} + +fn parse_unreserved(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + if let Some(x) = parse_alpha(str, start) { + return Some(x); + } + + if let Some(x) = parse_digit(str, start) { + return Some(x); + } + + match str[start] { + '-' | '.' | '_' | '~' => Some((start..start+1, start+1)), + _ => None + } +} + +fn parse_sub_delims(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + match str[start] { + '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '=' => Some((start..start+1, start+1)), + _ => None + } + +} + +fn parse_pchar(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + if let Some(x) = parse_unreserved(str, start) { + return Some(x); + } + + if let Some(x) = parse_sub_delims(str, start) { + return Some(x); + } + + return None; +} + +fn parse_segment_nz(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + let mut pos = start; + + let (_, npos) = parse_pchar(str, pos)?; + pos = npos; + + loop { + if let Some((_, npos)) = parse_pchar(str, pos) { + pos = npos; + } else { + break; + } + } + + return Some((start..pos, pos)); +} + +fn parse_segment(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + let mut pos = start; + + loop { + if let Some((_, npos)) = parse_pchar(str, pos) { + pos = npos; + } else { + break; + } + } + + return Some((start..pos, pos)); +} + +fn parse_path_absolute(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + let mut pos = start; + + if str[pos] != '/' { + return None; + } + pos += 1; + + if let Some((_, npos)) = parse_segment_nz(str, pos) { + pos = npos; + + loop { + if str[pos] != '/' { + break; + } + + if let Some((_, npos)) = parse_segment(str, pos+1) { + pos = npos; + } else { + break; } } + } - let response = request.call().unwrap(); + return Some((start..pos, pos)); +} - 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") { - let auth_header = AuthMethod::from_header(auth_header); - match auth_header { - Some(AuthMethod::Basic) => { - let basic_auth = format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", state.info.username, state.info.password))); - state.stage = AuthStage::Authorized(basic_auth); +fn parse_relative_part(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + let mut pos = start; + + if let Some((_, npos)) = parse_relative_part_1(str, pos) { + pos = npos; + return Some((start..pos, pos)); + } + + if let Some((_, npos)) = parse_path_absolute(str, pos) { + pos = npos; + return Some((start..pos, pos)); + } + + return None; +} + +fn parse_query(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + let mut pos = start; + + loop { + if let Some((_, npos)) = parse_pchar(str, pos) { + pos = npos; + } else if str[pos] == '/' { + pos += 1; + } else if str[pos] == '?' { + pos += 1; + } else { + break; + } + } + + return Some((start..pos, pos)); +} - continue; // try again - }, - Some(AuthMethod::Bearer{realm, scope, service}) => { - let basic_auth = format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", state.info.username, state.info.password))); +fn parse_fragment(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + return None; +} + +fn parse_relative_ref(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + let mut pos = start; + + let (_, npos) = parse_relative_part(str, pos)?; + pos = npos; + + if str[pos] == '?' && let Some((_, npos)) = parse_query(str, pos+1) { + pos = npos; + } + + if str[pos] == '#' && let Some((_, npos)) = parse_fragment(str, pos+1) { + pos = npos; + } - let url = format!("{}?service={}&scope={}", realm, service, scope); - dbg!(&url); + return Some((start..pos, pos)); +} - let body = ureq::get(url) - .config().http_status_as_error(false).build() - .header("Authorization", basic_auth) - .call(); +fn parse_uri(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + let mut pos = start; - dbg!(&body); + let(_, npos) = parse_scheme(str, pos)?; + pos = npos; - let body = body.unwrap() - .body_mut().read_to_string().unwrap(); + return Some((start..pos, pos)); +} - dbg!(&body); +fn parse_exact_string(str: &Vec<char>, start: usize, to_match: &str) -> Option<(Range<usize>, usize)> { + let mut pos = start; + let end = pos + to_match.len(); - let body: tinyjson::JsonValue = body - .parse().unwrap(); + if end > str.len() { + return None; + } - let token: &String = body["token"].get().unwrap(); + if !to_match.chars().eq(str[start..start+to_match.len()].iter().map(|x| *x)) { + return None; + } + pos += to_match.len(); - state.stage = AuthStage::Authorized(format!("Bearer {}", token)); - continue; - }, - None => todo!(), + return Some((start..pos, pos)); +} + +fn parse_loalpha(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + match str[start] { + 'a'..'z' => Some((start..start+1, start+1)), + _ => None, + } +} + +fn parse_relation_type(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + let mut pos = start; + + let (_, npos) = parse_loalpha(str, pos)?; + pos = npos; + + loop { + dbg!(&str[pos..]); + if let Some((_, npos)) = parse_loalpha(str, pos) { + pos = npos; + continue; + } else if let Some((_, npos)) = parse_digit(str, pos) { + pos = npos; + continue; + } else if let Some((_, npos)) = parse_exact_string(str, pos, ".") { + pos = npos; + continue; + } else if let Some((_, npos)) = parse_exact_string(str, pos, "-") { + pos = npos; + continue; + } else { + break; + } + } + + return Some((start..pos, pos)); +} + +fn parse_relation_types(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { + let mut pos = start; + + if let Some((_, npos)) = parse_relation_type(str, pos) { + pos = npos; + + return Some((start..pos, pos)); + } else if let Some((_, npos)) = parse_exact_string(str, pos, "\"") { + pos = npos; + + if let Some((_, npos)) = parse_relation_type(str, pos) { + pos = npos; + + loop { + if let Some((_, npos)) = parse_sp(str, pos) { + + loop { + if let Some((_, npos)) = parse_sp(str, pos) { + pos = npos; + } else { + break; } - } else { - todo!("Server didn't ask us to authenticate"); } - } else { - todo!("Authorized request somehow failed (expired token?)"); + + pos = npos; + if let Some((_, npos)) = parse_relation_type(str, pos) { + pos = npos; + continue; + } } + break; + } + + if let Some((_, npos)) = parse_exact_string(str, pos, "\"") { + pos = npos; + return Some((start+1..pos-1, pos)); + } + } + } + + return None; +} + +fn parse_link_param(str: &Vec<char>, start: usize) -> Option<(LinkParam, usize)> { + let mut pos = start; + + // Non-standard but seen in the wild + if let Some((_, npos)) = parse_sp(str, pos) { + pos = npos; + } + + if let Some((_, npos)) = parse_exact_string(str, pos, "rel=") { + if let Some((relation, npos)) = parse_relation_types(str, npos) { + pos = npos; + return Some((LinkParam::Rel{relation}, pos)); + } + } + + return None; +} + +fn parse_link_value(str: &Vec<char>, start: usize) -> Option<(LinkValue, usize)> { + let mut pos = start; + + if str[pos] == '<' { + pos += 1; + } else { + return None; + } + + let uri_range; + let mut params = vec!(); + + if let Some((range, npos)) = parse_uri(str, pos) { + uri_range = range; + pos = npos; + } else if let Some((range, npos)) = parse_relative_ref(str, pos) { + uri_range = range; + pos = npos; + } else { + return None; + } + + if str[pos] == '>' { + pos += 1; + } else { + return None; + } + + loop { + if let Some((_, npos)) = parse_exact_string(str, pos, ";") { + pos = npos; + } else { + break; + } + + if let Some((param, npos)) = parse_link_param(str, pos) { + pos = npos; + params.push(param); + } else { + break; + } + } + + return Some((LinkValue{ uri: uri_range, params: params }, pos)); +} + +fn parse_link(str: &Vec<char>, start: usize) -> Option<(Vec<LinkValue>, usize)> { + let mut pos = start; + + let mut values = vec!(); + + loop { + match parse_lws(str, pos) { + Some((_, npos)) => pos = npos, + None => break, + } + } + + let (param, npos) = parse_link_value(str, pos)?; + values.push(param); + pos = npos; + + loop { + let mut ipos = pos; + loop { + match parse_lws(str, ipos) { + Some((_, npos)) => ipos = npos, + None => break, + } + } + + if ipos >= str.len() || str[ipos] != ',' { + break; + } + ipos += 1; + + loop { + match parse_lws(str, ipos) { + Some((_, npos)) => ipos = npos, + None => break, + } + } + + match parse_link_value(str, ipos) { + Some((param, npos)) => { + ipos = npos; + values.push(param); + }, + None => { + break; + }, + } + + pos = ipos; + } + + + return Some((values, pos)); +} + +fn perform_registry_request(registry: &str, url: &str, 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 { + request = state.add_to_request(request); + } + + let response = request.call().unwrap(); + + if response.status() == 401 { + if let Some(ref mut state) = state { + state.attempt_authorization(&response)?; + continue; } else { todo!("Server returned 401 but we have no credentials"); } @@ -369,7 +819,6 @@ fn perform_registry_request(registry: &str, url: String, auth: &mut Auth) -> Res return Ok(response); } - return Err(()); } @@ -523,28 +972,117 @@ struct Update { content: String, } +#[derive(Debug)] +struct Version { + // It always starts with a string part, so the first string can be empty + string_parts: Vec<String>, + number_parts: Vec<u64>, +} + +#[derive(Debug)] +enum CompareOutcome { + Higher, + Lower, + + Incompatible, + Identical, +} + +impl Version { + fn parse(tag: &str) -> Self { + static RE: sync::LazyLock<regex::Regex> = sync::LazyLock::new(|| regex::Regex::new(r"(?<str>[^0-9]*)(?<num>[0-9]+)").unwrap()); + let mut string_parts = vec!(); + let mut number_parts = vec!(); + for it in RE.captures_iter(tag) { + string_parts.push(it.name("str").unwrap().as_str().to_string()); + number_parts.push(it.name("num").unwrap().as_str().to_string().parse().unwrap()); + } + + return Version { + string_parts, + number_parts, + } + } + + fn compare(&self, other: &Self) -> CompareOutcome { + assert!(self.number_parts.len() == self.string_parts.len()); + assert!(other.number_parts.len() == other.string_parts.len()); + + if self.number_parts.len() != other.number_parts.len() { + return CompareOutcome::Incompatible; + } + + for (self_string, other_string) in self.string_parts.iter().zip(other.string_parts.iter()) { + if self_string != other_string { + return CompareOutcome::Incompatible; + } + } + + for (self_number, other_number) in self.number_parts.iter().zip(other.number_parts.iter()) { + if self_number > other_number { + return CompareOutcome::Lower; + } else if self_number < other_number { + return CompareOutcome::Higher; + } + } + + return CompareOutcome::Identical; + } +} + fn fetch_new_image(file: &str, auth: &mut Auth, img: DockerRef, edits: &mut Vec<Update>) { let registry = img.registry.map(|x| &file[x]).unwrap_or("registry.jnsn.dev/"); - let tag = img.tag.map(|x| &file[x]).unwrap_or("latest"); + let mut tag = img.tag.map(|x| file[x].to_string()); - let digest = None; + let mut digest = None; - // Find the digest for the tag - if let Some(digest) = img.digest { - let url = format!("/v2/{}/manifests/{}", &file[img.image], tag); - let mut response = perform_registry_request(registry, url, auth).unwrap(); + + if let Some(ref tag_str) = tag { + let mut current = Version::parse(&tag_str); + + let mut url = format!("/v2/{}/tags/list", &file[img.image.clone()]); + loop { + let mut response = perform_registry_request(registry, &url, auth).unwrap(); + + let link_str = response.headers()["link"].to_str().unwrap(); + let (link, _) = parse_link(&link_str.chars().collect(), 0).unwrap(); + dbg!(&link_str[link.get(0).unwrap().params.get(0).unwrap().value().clone()]); + + let body: tinyjson::JsonValue = response.body_mut().read_to_string().unwrap().parse().unwrap(); + for it in body["tags"].get::<Vec<tinyjson::JsonValue>>().unwrap().iter() { + let candidate_str = it.get::<String>().unwrap(); + let candidate = Version::parse(candidate_str); + match current.compare(&candidate) { + CompareOutcome::Higher => { + tag = Some(candidate_str.clone()); + current = candidate; + }, + CompareOutcome::Lower => {}, + CompareOutcome::Incompatible => {}, + CompareOutcome::Identical => {}, + } + } + dbg!(&tag); + } + } + + if img.digest.is_some() { + // Find the digest for the selected tag + let url = format!("/v2/{}/manifests/{}", &file[img.image], tag.unwrap_or("latest".to_string())); + let mut response = perform_registry_request(registry, &url, auth).unwrap(); let body: tinyjson::JsonValue = response.body_mut().read_to_string().unwrap().parse().unwrap(); dbg!(&body); - let media_type : &String = body["mediaType"].get().unwrap(); + // let media_type : &String = body["mediaType"].get().unwrap(); // assert!(media_type == "application/vnd.docker.distribution.manifest.v2+json"); + + digest = Some(response.headers()["docker-content-digest"].to_str().unwrap().to_string()); } 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) + let prefix = &file[img.full_range.start..img.digest.unwrap().start]; + format!("{}{}", prefix, digest.unwrap_or("".to_string())) }; edits.push(Update{ diff --git a/src/rfc8288.rs b/src/rfc8288.rs new file mode 100644 index 0000000..b068573 --- /dev/null +++ b/src/rfc8288.rs @@ -0,0 +1,72 @@ +enum LinkParserEvent { + BeginLinkValue(usize), + EndLinkValue(usize), + + BeginLinkParam(usize), + EndLinkParam(usize), + + EndOfBuffer, +} + + +enum State { + Start, + UriRef, +} + +struct LinkParser { + state: State, + offset: usize, +} + +impl LinkParser { + fn new() -> Self { + return LinkParser { + state: State::Start, + offset: 0, + }; + } + + fn try_match_char(&self, buf: &[u8], b: u8) -> Option<usize> { + if buf[self.offset] != b { + return None; + } + + return Some(1); + } + + fn push(&mut self, new_data: &[u8]) -> LinkParserEvent { + match self.state { + State::Start => { + if let Some(len) = self.try_match_char(new_data, b'<') { + self.offset += len; + self.state = State::UriRef; + + return LinkParserEvent::BeginLinkValue(self.offset - len); + } + + todo!(); + }, + State::UriRef => { + loop { + if let Some(_) = self.try_match_char(new_data, b'>') { + break; + } else { + self.offset += 1; + } + } + todo!(); + }, + } + } + + fn finish(&mut self) { + } +} + +#[cfg(test)] +mod test { + #[test] + fn test() { + } +} |
