Watch
0
0
Fork
You've already forked hyperhive
0

swarm-nats-auth: answer callout requests concurrently, skip expired ones

Each request now runs in its own task, at most CONCURRENT_REQUESTS (32)
at once, instead of being awaited inline by the subscription loop. A
request whose exp (nats-server sets it to send time plus its
authorization.timeout) has passed is skipped without a lookup or reply.

Fixes #5434.
This commit is contained in:
atlas 2026-10-17 15:56:06 +02:00 • committed by mara
commit 6971b4e4ed
6 changed files with 334 additions and 48 deletions

View file

@ -48,19 +48,24 @@ pub struct ServerId {
pub id: String,
}
/// Split a NATS JWT and deserialize its claim set's `nats` object.
/// The claims of a request that this responder acts on. `iss`/`sub` are the
/// server's business, and claiming to understand fields we do not act on
/// would invite someone to trust them.
#[derive(Debug, Deserialize)]
pub struct Claims {
/// When the server stops waiting for the answer, in whole Unix seconds:
/// nats-server sets it to the time it sent the request plus its
/// `authorization.timeout`, rounded down.
#[serde(default)]
pub exp: Option<i64>,
pub nats: AuthRequest,
}
/// Split a NATS JWT and deserialize its claim set.
///
/// The signature is **not** verified here — see the module docs on
/// `main.rs` for why that is a decision and not an oversight.
pub fn decode(payload: &[u8]) -> Result<AuthRequest> {
/// Only the `nats` object is modelled; `iss`/`sub`/`exp` are the server's
/// business, and claiming to understand fields we do not act on would
/// invite someone to trust them.
#[derive(Deserialize)]
struct Claims {
nats: AuthRequest,
}
pub fn decode(payload: &[u8]) -> Result<Claims> {
let text = std::str::from_utf8(payload).context("auth request is not utf-8")?;
let mut parts = text.split('.');
// Exactly three segments: a fourth means this is not the shape we think
@ -73,9 +78,7 @@ pub fn decode(payload: &[u8]) -> Result<AuthRequest> {
};
let raw = base64_url_decode(claims).context("claim segment is not base64url")?;
let claims: Claims =
serde_json::from_slice(&raw).context("claim segment is not the expected JSON")?;
Ok(claims.nats)
serde_json::from_slice(&raw).context("claim segment is not the expected JSON")
}
/// base64url without padding, as JWT uses. Written out rather than pulled in:
@ -101,15 +104,16 @@ mod tests {
fn decodes_a_token_request() {
let req = decode(
jwt_with(
r#"{"nats":{"connect_opts":{"auth_token":"t0k"},
r#"{"exp":1760000002,"nats":{"connect_opts":{"auth_token":"t0k"},
"user_nkey":"UABC","server_id":{"id":"NDEADBEEF"}}}"#,
)
.as_bytes(),
)
.expect("decode");
assert_eq!(req.connect_opts.auth_token.as_deref(), Some("t0k"));
assert_eq!(req.user_nkey, "UABC");
assert_eq!(req.server_id.id, "NDEADBEEF");
assert_eq!(req.nats.connect_opts.auth_token.as_deref(), Some("t0k"));
assert_eq!(req.nats.user_nkey, "UABC");
assert_eq!(req.nats.server_id.id, "NDEADBEEF");
assert_eq!(req.exp, Some(1_760_000_002));
}
#[test]
@ -122,7 +126,8 @@ mod tests {
.as_bytes(),
)
.expect("decode");
assert!(req.connect_opts.auth_token.is_none());
assert!(req.nats.connect_opts.auth_token.is_none());
assert_eq!(req.exp, None);
}
#[test]