mirror of
https://github.com/Twigpine/node.git
synced 2026-11-11 05:01:32 -10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
012bdb9df0 | ||
|
|
76891277e4 | ||
|
|
4652acfe26 | ||
|
|
9e8a88abe5 | ||
|
|
d454de49ac |
No files matched your search
+30
-1
@@ -148,6 +148,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
|
||||
@@ -162,16 +174,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
|
||||
@@ -195,6 +223,7 @@ impl NodeClient {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(resp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ pub struct RegisterArgs {
|
||||
)]
|
||||
pub node: String,
|
||||
|
||||
/// A pre-obtained iCaptcha proof token, forwarded on the register request
|
||||
/// instead of making the client solve a fresh challenge.
|
||||
#[arg(long, env = "GITLAWB_ICAPTCHA_PROOF")]
|
||||
pub icaptcha_proof: Option<String>,
|
||||
|
||||
/// Capabilities to advertise (comma-separated)
|
||||
#[arg(
|
||||
long,
|
||||
@@ -56,7 +61,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")?;
|
||||
|
||||
@@ -160,6 +165,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();
|
||||
@@ -173,6 +179,36 @@ mod tests {
|
||||
assert_eq!(content["node"].as_str().unwrap(), server.url());
|
||||
}
|
||||
|
||||
/// A supplied `--icaptcha-proof` must ride the register request as the
|
||||
/// `x-icaptcha-proof` header so an enforce-mode node accepts it without a
|
||||
/// fresh solve round-trip.
|
||||
#[tokio::test]
|
||||
async fn register_forwards_icaptcha_proof_header() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
write_identity(&dir);
|
||||
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let m = server
|
||||
.mock("POST", "/api/register")
|
||||
.match_header("x-icaptcha-proof", "test.supplied.proof")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(r#"{"message":"Welcome","ucan":"eyJhbGci.test.token","trust_score":0.5,"expires":"2026-12-31"}"#)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
run(RegisterArgs {
|
||||
node: server.url(),
|
||||
capabilities: vec!["git:push".to_string()],
|
||||
model: None,
|
||||
dir: Some(dir.path().to_path_buf()),
|
||||
icaptcha_proof: Some("test.supplied.proof".to_string()),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
m.assert();
|
||||
}
|
||||
|
||||
/// `gl register` READS the identity from the parent of `GITLAWB_KEY`, so it has
|
||||
/// to WRITE the bootstrap token there too. Sending it to `~/.gitlawb` instead is
|
||||
/// silent split-brain: registration prints success, and every command that later
|
||||
@@ -201,6 +237,7 @@ mod tests {
|
||||
capabilities: vec!["git:push".to_string()],
|
||||
model: None,
|
||||
dir: None,
|
||||
icaptcha_proof: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -235,6 +272,7 @@ mod tests {
|
||||
capabilities: vec!["git:push".to_string()],
|
||||
model: None,
|
||||
dir: Some(dir.path().to_path_buf()),
|
||||
icaptcha_proof: None,
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -256,6 +294,7 @@ mod tests {
|
||||
capabilities: vec![],
|
||||
model: None,
|
||||
dir: Some(dir.path().to_path_buf()),
|
||||
icaptcha_proof: None,
|
||||
})
|
||||
.await;
|
||||
|
||||
|
||||
Reference in new issue
Block a user