new ip address filter middleware (wip)

This commit is contained in:
Brandon Thomas committed 2023-03-10 01:12:37 -05:00
1 parent edba26a2db
commit 0d99c2fc08
12 files changed
+304 -4

No files matched your search

Generated
+1
View File
@@ -675,6 +675,7 @@ dependencies = [
"actix-web 4.0.0-beta.10",
"actix-web-actors",
"errors",
"futures-util",
]
[[package]]
+3
View File
@@ -28,5 +28,8 @@ actix-utils = "3.0.0"
actix-web = "4.0.0-beta.10"
actix-web-actors = "4.0.0-beta.7"
# Futures
futures-util = "0.3.9"
[dev-dependencies]
# None yet
@@ -0,0 +1,37 @@
use actix_http::http::HeaderMap;
use actix_web::http::HeaderName;
use std::str::FromStr;
pub (crate) fn get_ip_from_header(headers: &HeaderMap, header_name: &str) -> Option<String> {
if let Ok(header_name) = HeaderName::from_str(header_name) {
headers.get(&header_name)
.and_then(|value| value.to_str().ok())
.and_then(|value| first_ip_from_list(value))
} else {
None
}
}
// NB: GKE packs in multiple IP addresses into the header:
// Example of header: x-forwarded-for: Some("136.55.189.34, 34.117.9.171")
fn first_ip_from_list(ip_list: &str) -> Option<String> {
ip_list.split(",")
.into_iter()
.map(|ip| ip.trim())
.find(|ip| !ip.is_empty())
.map(|ip| ip.to_string())
}
#[cfg(test)]
mod tests {
use crate::extractors::get_ip_from_header::first_ip_from_list;
#[test]
fn test_first_ip() {
assert_eq!(first_ip_from_list("1.2.3.4"), Some("1.2.3.4".to_string()));
assert_eq!(first_ip_from_list("1.2.3.4, 34.117.9.171"), Some("1.2.3.4".to_string()));
assert_eq!(first_ip_from_list(", 1.2.3.4"), Some("1.2.3.4".to_string()));
assert_eq!(first_ip_from_list("1.2.3.4, "), Some("1.2.3.4".to_string()));
assert_eq!(first_ip_from_list(""), None);
}
}
@@ -0,0 +1,29 @@
use actix_web::dev::ServiceRequest;
use crate::extractors::get_ip_from_header::get_ip_from_header;
pub fn get_service_request_ip_address(request: &ServiceRequest) -> String {
let headers = request.headers();
let maybe_x_forwarded = get_ip_from_header(headers, "x-forwarded-for");
let maybe_forwarded = get_ip_from_header(headers, "forwarded");
let maybe_ip = maybe_x_forwarded.or(maybe_forwarded);
match maybe_ip {
Some(ip_address) => ip_address,
None => {
// If we're running without the upstream Rust proxy, we can grab 'x-forwarded-for', which is
// populated by the DigitalOcean load balancer.
let ip_address_and_port = request.connection_info()
.remote_addr()
.unwrap_or("")
.to_string();
let ip_address = ip_address_and_port.split(":")
.collect::<Vec<&str>>()
.get(0)
.copied()
.unwrap_or("")
.to_string();
ip_address
},
}
}
@@ -0,0 +1,4 @@
pub mod get_service_request_ip_address;
// Utility
pub (crate) mod get_ip_from_header;
+6 -4
View File
@@ -1,7 +1,7 @@
//! errors
//! actix_helpers
//!
//! The purpose of this library is to pin to a single 'anyhow' and also develop
//! common error utilities.
//! The purpose of this library is to collect utility functionality that may be useful
//! to reuse across Actix installs.
//!
// Never allow these
@@ -18,4 +18,6 @@
#![allow(dead_code)]
#![allow(non_snake_case)]
pub mod route_builder;
pub mod extractors;
pub mod middleware;
pub mod route_builder;
@@ -0,0 +1,31 @@
use actix_http::body::Body;
use actix_http::header::CONTENT_TYPE;
use actix_web::http::StatusCode;
use actix_web::http::header::ContentType;
use actix_web::{HttpResponse, HttpResponseBuilder, ResponseError};
#[derive(Debug)]
pub struct BannedError {}
impl std::fmt::Display for BannedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "BannedError")
}
}
impl std::error::Error for BannedError {}
impl ResponseError for BannedError {
fn status_code(&self) -> StatusCode {
StatusCode::UNAUTHORIZED
}
fn error_response(&self) -> HttpResponse<Body> {
// NB: I'm setting a string error because I mistakenly got caught by this in local dev
// and couldn't figure out the issue for a bit. At least I can grep for this string.
// However, I need to balance this requirement with not cluing in those that are banned.
HttpResponseBuilder::new(self.status_code())
.append_header((CONTENT_TYPE, ContentType::json()))
.body(r#"{\"success\": false, \"error_message\": \"ERR64: database error\"}"#)
}
}
@@ -0,0 +1,75 @@
use crate::middleware::ip_filter::ip_set::IpSet;
use errors::{anyhow, AnyhowResult};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
#[derive(Clone)]
pub struct IpBanList {
ip_sets: Arc<RwLock<HashMap<String, IpSet>>>
}
impl IpBanList {
pub fn new() -> Self {
Self {
ip_sets: Arc::new(RwLock::new(HashMap::new()))
}
}
pub fn contains_ip_address<S: AsRef<str>>(&self, ip_address: S) -> AnyhowResult<bool> {
match self.ip_sets.read() {
Err(_) => Err(anyhow!("Can't read lock")),
Ok(sets) => {
for set in sets.values() {
if set.contains_ip_address(ip_address.as_ref()) {
return Ok(true)
}
}
Ok(false)
},
}
}
pub fn add_set(&self, set_name: String, ip_set: IpSet) -> AnyhowResult<Option<IpSet>> {
match self.ip_sets.write() {
Err(_) => Err(anyhow!("Can't read lock")),
Ok(mut sets) => {
Ok(sets.insert(set_name, ip_set))
},
}
}
pub fn remove_set(&self, set_name: &str) -> AnyhowResult<Option<IpSet>> {
match self.ip_sets.write() {
Err(_) => Err(anyhow!("Can't read lock")),
Ok(mut sets) => {
Ok(sets.remove(set_name))
},
}
}
}
#[cfg(test)]
mod tests {
use crate::middleware::ip_filter::ip_ban_list::IpBanList;
use crate::middleware::ip_filter::ip_set::IpSet;
#[test]
fn test_contains_ip() {
let mut local_ip_set = IpSet::new();
local_ip_set.add_ip_address("127.0.0.1".to_string());
local_ip_set.add_ip_address("192.168.0.1".to_string());
let mut remote_ip_set = IpSet::new();
remote_ip_set.add_ip_address("4.4.4.4".to_string());
remote_ip_set.add_ip_address("8.8.8.8".to_string());
let ip_ban_list = IpBanList::new();
ip_ban_list.add_set("local".to_string(), local_ip_set).unwrap();
ip_ban_list.add_set("remote".to_string(), remote_ip_set).unwrap();
assert_eq!(ip_ban_list.contains_ip_address("192.168.0.1").unwrap(), true);
assert_eq!(ip_ban_list.contains_ip_address("8.8.8.8").unwrap(), true);
assert_eq!(ip_ban_list.contains_ip_address("1.2.3.4").unwrap(), false);
}
}
@@ -0,0 +1,78 @@
use actix_web::dev::{Service, Transform};
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::Error;
use crate::middleware::ip_filter::banned_error::BannedError;
use crate::middleware::ip_filter::ip_ban_list::IpBanList;
use futures_util::future::{err, ok, Either, Ready};
use std::task::{Context, Poll};
use crate::extractors::get_service_request_ip_address::get_service_request_ip_address;
// There are two steps in middleware processing.
// 1. Middleware initialization, middleware factory gets called with
// next service in chain as parameter.
// 2. Middleware's call method gets called with normal request.
#[derive(Clone)]
pub struct IpFilter {
ip_ban_list: IpBanList,
}
impl IpFilter {
pub fn new(ip_ban_list: IpBanList) -> Self {
Self {
ip_ban_list,
}
}
}
impl<S> Transform<S, ServiceRequest> for IpFilter
where
S: Service<ServiceRequest, Response = ServiceResponse, Error = Error>,
S::Future: 'static,
{
type Response = ServiceResponse;
type Error = Error;
type InitError = ();
type Transform = IpFilterMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(IpFilterMiddleware { service, ip_ban_list: self.ip_ban_list.clone() })
}
}
pub struct IpFilterMiddleware<S> {
service: S,
ip_ban_list: IpBanList,
}
impl<S> Service<ServiceRequest> for IpFilterMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse, Error = Error>,
S::Future: 'static,
{
type Response = ServiceResponse;
type Error = Error;
type Future = Either<S::Future, Ready<Result<Self::Response, Self::Error>>>;
// alternatively(?), actix_service::forward_ready!(service);
fn poll_ready(&self, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&self, request: ServiceRequest) -> Self::Future {
let ip_address = get_service_request_ip_address(&request);
// NB: Fail open.
// We don't want our service to explode because we can't read bans.
let is_banned = self.ip_ban_list
.contains_ip_address(&ip_address)
.unwrap_or(false);
if is_banned {
Either::Right(err(Error::from(BannedError {})))
} else {
Either::Left(self.service.call(request))
}
}
}
@@ -0,0 +1,35 @@
use std::collections::HashSet;
#[derive(Clone)]
pub struct IpSet {
pub ip_set: HashSet<String>,
}
impl IpSet {
pub fn new() -> Self {
Self {
ip_set: HashSet::new(),
}
}
pub fn replace_set(&mut self, ip_set: HashSet<String>) {
self.ip_set = ip_set;
}
pub fn add_ip_address(&mut self, ip_address: String) -> bool {
self.ip_set.insert(ip_address)
}
pub fn remove_ip_address(&mut self, ip_address: &str) -> bool {
self.ip_set.remove(ip_address)
}
pub fn contains_ip_address<S: AsRef<str>>(&self, ip_address: S) -> bool {
self.ip_set.contains(ip_address.as_ref())
}
pub fn len(&self) -> usize {
self.ip_set.len()
}
}
@@ -0,0 +1,4 @@
pub mod banned_error;
pub mod ip_ban_list;
pub mod ip_filter_middleware;
pub mod ip_set;
@@ -0,0 +1 @@
pub mod ip_filter;