use fandhe_backend_http::request::RequestHead;
use fandhe_backend_http::response::Response;
use std::net::SocketAddr;
use std::time::Duration;
pub trait Middleware: Send + Sync {
fn name(&self) -> &'static str;
fn on_request(&self, head: &RequestHead);
fn on_response(&self, head: &RequestHead, elapsed: Duration) {
let _ = (head, elapsed);
}
fn on_response_with_status(&self, head: &RequestHead, status: u16, elapsed: Duration) {
let _ = status;
self.on_response(head, elapsed);
}
}
pub trait UpgradeHandler: Send + Sync {
fn name(&self) -> &'static str;
fn matches(&self, head: &RequestHead) -> bool;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GateOutcome {
Allow,
Reject {
response: Response,
},
}
impl GateOutcome {
#[must_use]
pub fn reject(status: u16, body: Vec<u8>) -> Self {
Self::Reject {
response: Response::new(status, body),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GateContext {
peer_addr: Option<SocketAddr>,
}
impl GateContext {
#[must_use]
pub fn new(peer_addr: Option<SocketAddr>) -> Self {
Self { peer_addr }
}
#[must_use]
pub fn peer_addr(&self) -> Option<SocketAddr> {
self.peer_addr
}
}
pub trait RequestGate: Send + Sync {
fn name(&self) -> &'static str;
fn check(&self, head: &RequestHead, ctx: &GateContext) -> GateOutcome;
}
#[cfg(test)]
mod tests {
use super::*;
use fandhe_backend_http::request::{ParseOutcome, parse_request_head};
fn _assert_object_safe(
_mw: &dyn Middleware,
_uh: &dyn UpgradeHandler,
_gate: &dyn RequestGate,
) {
}
fn _assert_send_sync<T: Send + Sync + ?Sized>() {}
#[allow(dead_code)]
fn _assert_bounds() {
_assert_send_sync::<dyn Middleware>();
_assert_send_sync::<dyn UpgradeHandler>();
_assert_send_sync::<dyn RequestGate>();
}
fn head_from(buf: &[u8]) -> RequestHead {
match parse_request_head(buf).expect("parse should succeed") {
ParseOutcome::Complete { head, .. } => head,
ParseOutcome::Incomplete => panic!("expected Complete"),
}
}
struct RecordingMiddleware {
calls: std::sync::Mutex<Vec<&'static str>>,
}
impl Middleware for RecordingMiddleware {
fn name(&self) -> &'static str {
"recording-middleware"
}
fn on_request(&self, _head: &RequestHead) {
self.calls.lock().unwrap().push("on_request");
}
fn on_response(&self, _head: &RequestHead, _elapsed: Duration) {
self.calls.lock().unwrap().push("on_response");
}
}
#[test]
fn middleware_hooks_are_called_in_order() {
let mw = RecordingMiddleware {
calls: std::sync::Mutex::new(Vec::new()),
};
let head = head_from(b"GET / HTTP/1.1\r\n\r\n");
mw.on_request(&head);
mw.on_response(&head, Duration::from_millis(1));
assert_eq!(*mw.calls.lock().unwrap(), vec!["on_request", "on_response"]);
assert_eq!(mw.name(), "recording-middleware");
}
#[test]
fn on_response_with_status_default_delegates_to_on_response() {
let mw = RecordingMiddleware {
calls: std::sync::Mutex::new(Vec::new()),
};
let head = head_from(b"GET / HTTP/1.1\r\n\r\n");
mw.on_response_with_status(&head, 404, Duration::from_millis(1));
assert_eq!(*mw.calls.lock().unwrap(), vec!["on_response"]);
}
struct MinimalMiddleware;
impl Middleware for MinimalMiddleware {
fn name(&self) -> &'static str {
"minimal-middleware"
}
fn on_request(&self, _head: &RequestHead) {}
}
#[test]
fn on_response_default_is_noop() {
let mw = MinimalMiddleware;
let head = head_from(b"GET / HTTP/1.1\r\n\r\n");
mw.on_response(&head, Duration::from_millis(1));
mw.on_response_with_status(&head, 200, Duration::from_millis(1));
}
struct AlwaysMatchUpgrade;
impl UpgradeHandler for AlwaysMatchUpgrade {
fn name(&self) -> &'static str {
"always-match"
}
fn matches(&self, _head: &RequestHead) -> bool {
true
}
}
struct NeverMatchUpgrade;
impl UpgradeHandler for NeverMatchUpgrade {
fn name(&self) -> &'static str {
"never-match"
}
fn matches(&self, _head: &RequestHead) -> bool {
false
}
}
#[test]
fn upgrade_handler_matches_decision() {
let head = head_from(b"GET /ws HTTP/1.1\r\nUpgrade: websocket\r\n\r\n");
assert!(AlwaysMatchUpgrade.matches(&head));
assert!(!NeverMatchUpgrade.matches(&head));
}
struct AllowAllGate;
impl RequestGate for AllowAllGate {
fn name(&self) -> &'static str {
"allow-all"
}
fn check(&self, _head: &RequestHead, _ctx: &GateContext) -> GateOutcome {
GateOutcome::Allow
}
}
struct FailClosedGate;
impl RequestGate for FailClosedGate {
fn name(&self) -> &'static str {
"fail-closed"
}
fn check(&self, head: &RequestHead, _ctx: &GateContext) -> GateOutcome {
match head.header("authorization") {
Some(_) => GateOutcome::Allow,
None => GateOutcome::reject(401, Vec::new()),
}
}
}
struct PeerRequiredGate;
impl RequestGate for PeerRequiredGate {
fn name(&self) -> &'static str {
"peer-required"
}
fn check(&self, _head: &RequestHead, ctx: &GateContext) -> GateOutcome {
match ctx.peer_addr() {
Some(_) => GateOutcome::Allow,
None => GateOutcome::reject(403, Vec::new()),
}
}
}
#[test]
fn request_gate_allow_outcome() {
let head = head_from(b"GET / HTTP/1.1\r\n\r\n");
let ctx = GateContext::new(None);
assert_eq!(AllowAllGate.check(&head, &ctx), GateOutcome::Allow);
}
#[test]
fn request_gate_fail_closed_rejects_missing_authorization() {
let head = head_from(b"GET / HTTP/1.1\r\n\r\n");
let ctx = GateContext::new(None);
assert_eq!(
FailClosedGate.check(&head, &ctx),
GateOutcome::reject(401, Vec::new())
);
}
#[test]
fn request_gate_fail_closed_allows_with_authorization() {
let head = head_from(b"GET / HTTP/1.1\r\nAuthorization: Bearer x\r\n\r\n");
let ctx = GateContext::new(None);
assert_eq!(FailClosedGate.check(&head, &ctx), GateOutcome::Allow);
}
#[test]
fn gate_context_new_and_peer_addr_roundtrip() {
let addr: std::net::SocketAddr = "192.0.2.1:8080".parse().unwrap();
let ctx = GateContext::new(Some(addr));
assert_eq!(ctx.peer_addr(), Some(addr));
let ctx_none = GateContext::new(None);
assert_eq!(ctx_none.peer_addr(), None);
}
#[test]
fn request_gate_peer_required_rejects_when_peer_addr_missing() {
let head = head_from(b"GET / HTTP/1.1\r\n\r\n");
let ctx = GateContext::new(None);
assert_eq!(
PeerRequiredGate.check(&head, &ctx),
GateOutcome::reject(403, Vec::new())
);
}
#[test]
fn request_gate_peer_required_allows_when_peer_addr_present() {
let head = head_from(b"GET / HTTP/1.1\r\n\r\n");
let addr: std::net::SocketAddr = "203.0.113.5:54321".parse().unwrap();
let ctx = GateContext::new(Some(addr));
assert_eq!(PeerRequiredGate.check(&head, &ctx), GateOutcome::Allow);
}
#[test]
fn gate_outcome_reject_carries_status_and_body() {
let outcome = GateOutcome::Reject {
response: Response::new(403, b"forbidden".to_vec()),
};
match outcome {
GateOutcome::Reject { response } => {
assert_eq!(response.status, 403);
assert_eq!(response.body, b"forbidden");
}
GateOutcome::Allow => panic!("expected Reject"),
}
}
#[test]
fn gate_outcome_reject_carries_headers() {
let response = Response::new(429, Vec::new())
.with_header("Retry-After", "30")
.expect("リテラル値は構築時検証を通る");
let outcome = GateOutcome::Reject { response };
let GateOutcome::Reject { response } = outcome else {
panic!("expected Reject");
};
let wire = response.serialize(false);
let text = String::from_utf8(wire).unwrap();
assert!(text.starts_with("HTTP/1.1 429"));
assert!(text.contains("Retry-After: 30\r\n"));
}
#[test]
fn gate_outcome_reject_helper_matches_manual_construction() {
assert_eq!(
GateOutcome::reject(401, b"denied".to_vec()),
GateOutcome::Reject {
response: Response::new(401, b"denied".to_vec())
}
);
}
#[test]
fn gate_outcome_allow_and_reject_are_not_equal() {
let allow = GateOutcome::Allow;
let reject = GateOutcome::reject(401, Vec::new());
assert_ne!(allow, reject);
}
#[test]
fn gate_outcome_reject_with_different_status_are_not_equal() {
let a = GateOutcome::reject(401, Vec::new());
let b = GateOutcome::reject(403, Vec::new());
assert_ne!(a, b);
}
}