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:
135
crates/quicproquo-rpc/src/auth_handshake.rs
Normal file
135
crates/quicproquo-rpc/src/auth_handshake.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
//! Auth handshake protocol for QUIC connections.
|
||||
//!
|
||||
//! The session token is sent as a "connection init" message on the first
|
||||
//! bi-stream before any RPC calls.
|
||||
//!
|
||||
//! ## Protocol
|
||||
//! ```text
|
||||
//! Client → Server: [0x01 magic][token_len: u16 BE][token bytes]
|
||||
//! Server → Client: [0x01 magic][0x00 status OK]
|
||||
//! ```
|
||||
|
||||
use crate::error::RpcError;
|
||||
|
||||
/// Magic byte identifying an auth init frame.
|
||||
pub const AUTH_INIT_MAGIC: u8 = 0x01;
|
||||
|
||||
/// Status byte: auth accepted.
|
||||
const AUTH_STATUS_OK: u8 = 0x00;
|
||||
|
||||
/// Maximum token length (64 KiB).
|
||||
const MAX_TOKEN_LEN: usize = 65535;
|
||||
|
||||
/// Write an auth init frame to a QUIC send stream.
|
||||
pub async fn send_auth_init(
|
||||
send: &mut quinn::SendStream,
|
||||
token: &[u8],
|
||||
) -> Result<(), RpcError> {
|
||||
if token.len() > MAX_TOKEN_LEN {
|
||||
return Err(RpcError::Encode(format!(
|
||||
"auth token too large: {} bytes (max {MAX_TOKEN_LEN})",
|
||||
token.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let token_len = token.len() as u16;
|
||||
let mut buf = Vec::with_capacity(1 + 2 + token.len());
|
||||
buf.push(AUTH_INIT_MAGIC);
|
||||
buf.extend_from_slice(&token_len.to_be_bytes());
|
||||
buf.extend_from_slice(token);
|
||||
|
||||
send.write_all(&buf)
|
||||
.await
|
||||
.map_err(|e| RpcError::Connection(format!("send auth init: {e}")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read an auth init frame from a QUIC recv stream.
|
||||
pub async fn recv_auth_init(
|
||||
recv: &mut quinn::RecvStream,
|
||||
) -> Result<Vec<u8>, RpcError> {
|
||||
// Read magic byte.
|
||||
let mut header = [0u8; 3];
|
||||
recv.read_exact(&mut header)
|
||||
.await
|
||||
.map_err(|e| RpcError::Connection(format!("read auth init header: {e}")))?;
|
||||
|
||||
if header[0] != AUTH_INIT_MAGIC {
|
||||
return Err(RpcError::Decode(format!(
|
||||
"bad auth init magic: expected 0x{AUTH_INIT_MAGIC:02x}, got 0x{:02x}",
|
||||
header[0]
|
||||
)));
|
||||
}
|
||||
|
||||
let token_len = u16::from_be_bytes([header[1], header[2]]) as usize;
|
||||
if token_len > MAX_TOKEN_LEN {
|
||||
return Err(RpcError::Decode(format!(
|
||||
"auth token length {token_len} exceeds max {MAX_TOKEN_LEN}"
|
||||
)));
|
||||
}
|
||||
|
||||
let mut token = vec![0u8; token_len];
|
||||
if token_len > 0 {
|
||||
recv.read_exact(&mut token)
|
||||
.await
|
||||
.map_err(|e| RpcError::Connection(format!("read auth token: {e}")))?;
|
||||
}
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
/// Send auth ack (success response).
|
||||
pub async fn send_auth_ack(
|
||||
send: &mut quinn::SendStream,
|
||||
) -> Result<(), RpcError> {
|
||||
let buf = [AUTH_INIT_MAGIC, AUTH_STATUS_OK];
|
||||
send.write_all(&buf)
|
||||
.await
|
||||
.map_err(|e| RpcError::Connection(format!("send auth ack: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read auth ack from the server.
|
||||
pub async fn recv_auth_ack(
|
||||
recv: &mut quinn::RecvStream,
|
||||
) -> Result<(), RpcError> {
|
||||
let mut buf = [0u8; 2];
|
||||
recv.read_exact(&mut buf)
|
||||
.await
|
||||
.map_err(|e| RpcError::Connection(format!("read auth ack: {e}")))?;
|
||||
|
||||
if buf[0] != AUTH_INIT_MAGIC {
|
||||
return Err(RpcError::Decode(format!(
|
||||
"bad auth ack magic: expected 0x{AUTH_INIT_MAGIC:02x}, got 0x{:02x}",
|
||||
buf[0]
|
||||
)));
|
||||
}
|
||||
|
||||
if buf[1] != AUTH_STATUS_OK {
|
||||
return Err(RpcError::Decode(format!(
|
||||
"auth rejected with status 0x{:02x}",
|
||||
buf[1]
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn auth_init_magic_is_0x01() {
|
||||
assert_eq!(AUTH_INIT_MAGIC, 0x01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_too_large_returns_encode_error() {
|
||||
// We cannot call send_auth_init without a real stream, but we can
|
||||
// verify the length check logic by constructing the guard condition.
|
||||
let big_token = vec![0u8; MAX_TOKEN_LEN + 1];
|
||||
assert!(big_token.len() > MAX_TOKEN_LEN);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user