summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorJesper Jensen <jesper@jnsn.dev>2025-12-16 21:34:08 +0100
committerJesper Jensen <jesper@jnsn.dev>2025-12-16 21:34:08 +0100
commit9ff76e6a201e53f1377a0f7e5bcff6b728c1b8ab (patch)
tree5ae984e004496acfe15ea88fa69960f25a28b44d /src/main.rs
parent1166c36e26829b4af3ca6a6fb2247cdd64e0ca95 (diff)
Start parsing some challenges
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs280
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");