diff --git a/crates/gl/src/http.rs b/crates/gl/src/http.rs index dc9f45d..712af13 100644 --- a/crates/gl/src/http.rs +++ b/crates/gl/src/http.rs @@ -77,6 +77,18 @@ impl NodeClient { self.send_signed("POST", path, body).await } + /// POST with JSON body + RFC 9421 signing and an optional user-provided + /// iCaptcha proof, sent on the initial request. + pub async fn post_with_proof( + &self, + path: &str, + body: &[u8], + initial_proof: Option<&str>, + ) -> Result { + self.send_signed_with_proof("POST", path, body, initial_proof) + .await + } + /// PUT with RFC 9421 signing + transparent iCaptcha solve/retry. pub async fn put(&self, path: &str, body: &[u8]) -> Result { self.send_signed("PUT", path, body).await @@ -91,16 +103,32 @@ impl NodeClient { /// `x-icaptcha-*` headers) solve it and retry the same signed request with /// the proof header, up to [`MAX_ICAPTCHA_RETRIES`]. Emits an actionable /// hint on a 401 "not an agent" (the old-CLI / unregistered failure mode). + /// Existing call path: no user-supplied proof on the first request. async fn send_signed( &self, method: &str, path: &str, body: &[u8], ) -> Result { - let mut proof: Option = None; + self.send_signed_with_proof(method, path, body, None).await + } + + /// Sign + send a write. `initial_proof`, if provided, is attached to the + /// first request. On a 403 iCaptcha challenge advertised via + /// `x-icaptcha-*` headers, solve and retry up to MAX_ICAPTCHA_RETRIES. + async fn send_signed_with_proof( + &self, + method: &str, + path: &str, + body: &[u8], + initial_proof: Option<&str>, + ) -> Result { + let mut proof = initial_proof.map(str::to_owned); let mut attempts = 0; + loop { let resp = self.send_once(method, path, body, proof.as_deref()).await?; + let status = resp.status(); if status == reqwest::StatusCode::UNAUTHORIZED @@ -124,6 +152,7 @@ impl NodeClient { continue; } } + return Ok(resp); } } diff --git a/crates/gl/src/register.rs b/crates/gl/src/register.rs index 8a17a77..2b16635 100644 --- a/crates/gl/src/register.rs +++ b/crates/gl/src/register.rs @@ -17,6 +17,9 @@ pub struct RegisterArgs { #[arg(long, default_value = "https://node.gitlawb.com", env = "GITLAWB_NODE")] pub node: String, + #[arg(long, env = "GITLAWB_ICAPTCHA_PROOF")] + pub icaptcha_proof: Option, + /// Capabilities to advertise (comma-separated) #[arg( long, @@ -51,7 +54,7 @@ pub async fn run(args: RegisterArgs) -> Result<()> { }))?; let resp = client - .post("/api/register", &body) + .post_with_proof("/api/register", &body, args.icaptcha_proof.as_deref()) .await .context("failed to connect to node")?; @@ -147,6 +150,7 @@ mod tests { capabilities: vec!["git:push".to_string(), "git:fetch".to_string()], model: None, dir: Some(dir.path().to_path_buf()), + icaptcha_proof: None, }) .await .unwrap(); @@ -179,6 +183,7 @@ mod tests { capabilities: vec!["git:push".to_string()], model: None, dir: Some(dir.path().to_path_buf()), + icaptcha_proof: None, }) .await; @@ -198,6 +203,7 @@ mod tests { capabilities: vec![], model: None, dir: Some(dir.path().to_path_buf()), + icaptcha_proof: None, }) .await;