feat(rpc): auth handshake, server-push broker, audit logging

- auth_handshake.rs: connection-init protocol (magic 0x01, token, ack)
- push.rs: PushBroker manages per-identity push connections with gc
- server.rs: ConnectionState, auth handshake on first bi-stream, pass
  identity_key/session_token to RequestContext per stream
- client.rs: session_token in RpcClientConfig, auto auth handshake on connect
- middleware.rs: log_rpc_call with SHA-256 redaction, hex_prefix helper
- lib.rs: export auth_handshake and push modules
This commit is contained in:
2026-03-04 12:08:20 +01:00
parent ff93275dc1
commit f09dbe10ce
7 changed files with 387 additions and 8 deletions

View File

@@ -1,11 +1,15 @@
//! Tower-based middleware layers for the RPC server.
//! Middleware layers for the RPC server.
//!
//! - `AuthLayer`: validates session tokens and attaches identity to context.
//! - `RateLimitLayer`: per-IP request rate limiting.
//! - `SessionValidator`: validates session tokens and resolves identity keys.
//! - `RateLimiter`: per-key sliding-window rate limiting.
//! - `log_rpc_call`: structured audit logging for RPC calls.
use std::time::{Duration, Instant};
use dashmap::DashMap;
use sha2::Digest;
use crate::error::RpcStatus;
// ── Auth middleware ──────────────────────────────────────────────────────────
@@ -70,6 +74,45 @@ impl RateLimiter {
}
}
// ── Audit logging ───────────────────────────────────────────────────────────
/// Log an RPC call with timing and caller info.
///
/// When `redact` is true, the identity key is hashed before logging so that
/// raw keys never appear in log output.
pub fn log_rpc_call(
method_name: &str,
identity_key: Option<&[u8]>,
latency: Duration,
status: RpcStatus,
redact: bool,
) {
let ik_display = match identity_key {
Some(ik) if redact => {
let hash_input_len = 8.min(ik.len());
let digest = sha2::Sha256::digest(&ik[..hash_input_len]);
format!("h:{}", hex_prefix(&digest))
}
Some(ik) => hex_prefix(ik),
None => "anonymous".to_string(),
};
tracing::info!(
method = method_name,
identity = %ik_display,
latency_ms = latency.as_millis() as u64,
status = ?status,
"rpc"
);
}
fn hex_prefix(bytes: &[u8]) -> String {
bytes
.iter()
.take(4)
.map(|b| format!("{b:02x}"))
.collect::<String>()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -93,4 +136,19 @@ mod tests {
std::thread::sleep(Duration::from_millis(5));
assert!(rl.check(key)); // window expired
}
#[test]
fn hex_prefix_formats_first_4_bytes() {
assert_eq!(hex_prefix(&[0xab, 0xcd, 0xef, 0x01, 0x99]), "abcdef01");
assert_eq!(hex_prefix(&[0x00, 0xff]), "00ff");
assert_eq!(hex_prefix(&[]), "");
}
#[test]
fn log_rpc_call_does_not_panic() {
// Verify that audit log function does not panic with various inputs.
log_rpc_call("test.method", None, Duration::from_millis(42), RpcStatus::Ok, false);
log_rpc_call("test.method", Some(&[1, 2, 3, 4, 5, 6, 7, 8]), Duration::from_millis(1), RpcStatus::Internal, true);
log_rpc_call("test.method", Some(&[0xab]), Duration::ZERO, RpcStatus::Unauthorized, true);
}
}