mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 00:47:53 +00:00
new ip address filter middleware (wip)
This commit is contained in:
1 parent
edba26a2db
commit
0d99c2fc08
12 files changed
+304
-4
No files matched your search
Generated
+1
@@ -675,6 +675,7 @@ dependencies = [
|
||||
"actix-web 4.0.0-beta.10",
|
||||
"actix-web-actors",
|
||||
"errors",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user