nautalid 0.1.0

Scratch container substrate — TLS 1.3 HTTP/2+3 kernel, LID/AetherDB, optional filter bus (GPL-3.0-or-later).
//! Axum middleware — applies [`FilterStore`] before inner handlers.

use axum::Router;
use axum::extract::Request;
use axum::http::StatusCode;
use axum::middleware::{self, Next};
use axum::response::Response;
use std::net::SocketAddr;

use super::{FilterStore, client_ip, policy_allows};

/// Applies [`FilterStore`] to every request on **`router`** (before inner handlers).
pub fn apply<S>(router: Router<S>, store: FilterStore) -> Router<S>
where
    S: Clone + Send + Sync + 'static,
{
    router.layer(middleware::from_fn(move |req: Request, next: Next| {
        let store = store.clone();
        async move { filter_gate(req, next, store).await }
    }))
}

async fn filter_gate(req: Request, next: Next, store: FilterStore) -> Result<Response, StatusCode> {
    let peer = req
        .extensions()
        .get::<axum::extract::ConnectInfo<SocketAddr>>()
        .map(|c| c.0);
    let ip = client_ip(&req, peer);
    let path = req.uri().path();

    let allowed = store.read(|policy| policy_allows(policy, ip, path));
    if allowed {
        Ok(next.run(req).await)
    } else {
        crate::metrics::record_filter_denied();
        Err(StatusCode::FORBIDDEN)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::filter::{FilterPolicy, FilterRule};
    use axum::body::Body;
    use axum::http::Request;
    use axum::routing::get;
    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
    use tower::ServiceExt;

    async fn ok_handler() -> &'static str {
        "ok"
    }

    fn policy_deny_ip(ip: IpAddr) -> FilterStore {
        FilterStore::new(FilterPolicy {
            deny: vec![FilterRule::Ip(ip)],
            allow: Vec::new(),
        })
    }

    #[tokio::test]
    async fn blocks_denied_ip() {
        let denied = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 50));
        let app = apply(
            Router::new().route("/", get(ok_handler)),
            policy_deny_ip(denied),
        );

        let mut req = Request::get("/").body(Body::empty()).unwrap();
        req.extensions_mut()
            .insert(axum::extract::ConnectInfo(SocketAddr::new(denied, 1234)));
        let res = app.oneshot(req).await.unwrap();
        assert_eq!(res.status(), StatusCode::FORBIDDEN);
    }

    #[tokio::test]
    async fn allows_other_ip() {
        let denied = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 50));
        let allowed = IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1));
        let app = apply(
            Router::new().route("/", get(ok_handler)),
            policy_deny_ip(denied),
        );

        let mut req = Request::get("/").body(Body::empty()).unwrap();
        req.extensions_mut()
            .insert(axum::extract::ConnectInfo(SocketAddr::new(allowed, 1234)));
        let res = app.oneshot(req).await.unwrap();
        assert_eq!(res.status(), StatusCode::OK);
    }
}