Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion crates/gl/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<reqwest::Response> {
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<reqwest::Response> {
self.send_signed("PUT", path, body).await
Expand All @@ -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<reqwest::Response> {
let mut proof: Option<String> = 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<reqwest::Response> {
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
Expand All @@ -124,6 +152,7 @@ impl NodeClient {
continue;
}
}

return Ok(resp);
}
}
Expand Down
8 changes: 7 additions & 1 deletion crates/gl/src/register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

/// Capabilities to advertise (comma-separated)
#[arg(
long,
Expand Down Expand Up @@ -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")?;

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;

Expand All @@ -198,6 +203,7 @@ mod tests {
capabilities: vec![],
model: None,
dir: Some(dir.path().to_path_buf()),
icaptcha_proof: None,
})
.await;

Expand Down
Loading