feat: v2 Phase 1 — foundation, proto schemas, RPC framework, SDK skeleton
New workspace structure with 9 crates. Adds: - proto/qpq/v1/*.proto: 11 protobuf schemas covering all 33 RPC methods - quicproquo-proto: dual codegen (capnp legacy + prost v2) - quicproquo-rpc: QUIC RPC framework (framing, server, client, middleware) - quicproquo-sdk: client SDK (QpqClient, events, conversation store) - quicproquo-server/domain/: protocol-agnostic domain types and services - justfile: build commands Wire format: [method_id:u16][req_id:u32][len:u32][protobuf] per QUIC stream. All 151 existing tests pass. Backward compatible with v1 capnp code.
This commit is contained in:
96
crates/quicproquo-rpc/src/middleware.rs
Normal file
96
crates/quicproquo-rpc/src/middleware.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
//! Tower-based middleware layers for the RPC server.
|
||||
//!
|
||||
//! - `AuthLayer`: validates session tokens and attaches identity to context.
|
||||
//! - `RateLimitLayer`: per-IP request rate limiting.
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use dashmap::DashMap;
|
||||
|
||||
// ── Auth middleware ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Validates bearer tokens and resolves identity keys.
|
||||
pub trait SessionValidator: Send + Sync + 'static {
|
||||
/// Validate a session token, returning the identity key if valid.
|
||||
fn validate(&self, token: &[u8]) -> Option<Vec<u8>>;
|
||||
}
|
||||
|
||||
/// Auth context extracted from a validated session.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AuthContext {
|
||||
/// The Ed25519 identity key of the authenticated caller.
|
||||
pub identity_key: Vec<u8>,
|
||||
}
|
||||
|
||||
// ── Rate limiter ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Simple per-key sliding-window rate limiter.
|
||||
pub struct RateLimiter {
|
||||
/// Max requests per window.
|
||||
max_requests: u32,
|
||||
/// Window duration.
|
||||
window: Duration,
|
||||
/// Map from key → (count, window_start).
|
||||
state: DashMap<Vec<u8>, (u32, Instant)>,
|
||||
}
|
||||
|
||||
impl RateLimiter {
|
||||
/// Create a new rate limiter.
|
||||
pub fn new(max_requests: u32, window: Duration) -> Self {
|
||||
Self {
|
||||
max_requests,
|
||||
window,
|
||||
state: DashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a request from `key` is allowed. Returns `true` if allowed.
|
||||
pub fn check(&self, key: &[u8]) -> bool {
|
||||
let now = Instant::now();
|
||||
let mut entry = self.state.entry(key.to_vec()).or_insert((0, now));
|
||||
let (count, window_start) = entry.value_mut();
|
||||
|
||||
if now.duration_since(*window_start) >= self.window {
|
||||
// Reset window.
|
||||
*count = 1;
|
||||
*window_start = now;
|
||||
true
|
||||
} else if *count < self.max_requests {
|
||||
*count += 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove expired entries (call periodically for memory hygiene).
|
||||
pub fn gc(&self) {
|
||||
let now = Instant::now();
|
||||
self.state.retain(|_, (_, start)| now.duration_since(*start) < self.window * 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rate_limiter_allows_within_limit() {
|
||||
let rl = RateLimiter::new(3, Duration::from_secs(60));
|
||||
let key = b"test-key";
|
||||
assert!(rl.check(key));
|
||||
assert!(rl.check(key));
|
||||
assert!(rl.check(key));
|
||||
assert!(!rl.check(key)); // 4th request denied
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rate_limiter_resets_after_window() {
|
||||
let rl = RateLimiter::new(1, Duration::from_millis(1));
|
||||
let key = b"test-key";
|
||||
assert!(rl.check(key));
|
||||
assert!(!rl.check(key));
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
assert!(rl.check(key)); // window expired
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user