Skip to main content

rust_web_server/blocklist/
mod.rs

1//! Runtime IP blocklist middleware.
2//!
3//! Unlike [`crate::ip_filter::IpFilter`] (configured at startup), `Blocklist`
4//! is mutable at runtime — add and remove IPs while the server is running.
5//! The global singleton is accessible from MCP tools, admin handlers, and
6//! middleware without passing explicit references.
7//!
8//! # Example
9//!
10//! ```rust,no_run
11//! use rust_web_server::app::App;
12//! use rust_web_server::core::New;
13//! use rust_web_server::blocklist::{self, BlocklistLayer};
14//!
15//! let app = App::new().wrap(BlocklistLayer);
16//!
17//! // Block an IP at runtime.
18//! blocklist::global().block("1.2.3.4");
19//!
20//! // Unblock later.
21//! blocklist::global().unblock("1.2.3.4");
22//! ```
23
24#[cfg(test)]
25mod tests;
26
27use std::sync::{Mutex, OnceLock};
28
29use crate::application::Application;
30use crate::error::{AppError, IntoResponse};
31use crate::middleware::Middleware;
32use crate::request::Request;
33use crate::response::Response;
34use crate::server::ConnectionInfo;
35
36/// A thread-safe list of blocked IPv4 addresses.
37pub struct Blocklist {
38    denied: Mutex<Vec<String>>,
39}
40
41impl Blocklist {
42    fn new() -> Self {
43        Blocklist { denied: Mutex::new(Vec::new()) }
44    }
45
46    /// Add `ip` to the blocklist. No-op if already present.
47    pub fn block(&self, ip: &str) {
48        let mut guard = self.denied.lock().unwrap();
49        if !guard.iter().any(|e| e == ip) {
50            guard.push(ip.to_string());
51        }
52    }
53
54    /// Remove `ip` from the blocklist. No-op if not present.
55    pub fn unblock(&self, ip: &str) {
56        self.denied.lock().unwrap().retain(|e| e != ip);
57    }
58
59    /// `true` if `ip` is currently blocked.
60    pub fn is_blocked(&self, ip: &str) -> bool {
61        self.denied.lock().unwrap().iter().any(|e| e == ip)
62    }
63
64    /// Snapshot of all blocked IPs in insertion order.
65    pub fn list(&self) -> Vec<String> {
66        self.denied.lock().unwrap().clone()
67    }
68
69    /// Remove all entries.
70    pub fn clear(&self) {
71        self.denied.lock().unwrap().clear();
72    }
73}
74
75static INSTANCE: OnceLock<Blocklist> = OnceLock::new();
76
77/// Return the process-wide `Blocklist` singleton.
78pub fn global() -> &'static Blocklist {
79    INSTANCE.get_or_init(Blocklist::new)
80}
81
82/// Middleware that checks each request's client IP against [`global()`].
83///
84/// Blocked IPs receive `403 Forbidden`. All other requests pass through
85/// to the next layer.
86pub struct BlocklistLayer;
87
88impl Middleware for BlocklistLayer {
89    fn handle(
90        &self,
91        request: &Request,
92        connection: &ConnectionInfo,
93        next: &dyn Application,
94    ) -> Result<Response, String> {
95        if global().is_blocked(&connection.client.ip) {
96            return Ok(AppError::Forbidden.into_response());
97        }
98        next.execute(request, connection)
99    }
100}