diff --git a/crates/app/src/routes/api/v1/auth/ipbans.rs b/crates/app/src/routes/api/v1/auth/ipbans.rs index eefe767..8a71d25 100644 --- a/crates/app/src/routes/api/v1/auth/ipbans.rs +++ b/crates/app/src/routes/api/v1/auth/ipbans.rs @@ -5,7 +5,7 @@ use crate::{ }; use axum::{Extension, Json, extract::Path, response::IntoResponse}; use axum_extra::extract::CookieJar; -use tetratto_core::model::{auth::IpBan, permissions::FinePermission}; +use tetratto_core::model::{addr::RemoteAddr, auth::IpBan, permissions::FinePermission}; /// Create a new IP ban. pub async fn create_request( @@ -24,7 +24,14 @@ pub async fn create_request( return Json(Error::NotAllowed.into()); } - match data.create_ipban(IpBan::new(ip, user.id, req.reason)).await { + match data + .create_ipban(IpBan::new( + RemoteAddr::from(ip.as_str()).prefix(None), + user.id, + req.reason, + )) + .await + { Ok(_) => Json(ApiReturn { ok: true, message: "IP ban created".to_string(), diff --git a/crates/app/src/routes/api/v1/auth/mod.rs b/crates/app/src/routes/api/v1/auth/mod.rs index 0d9fe50..cfc6c29 100644 --- a/crates/app/src/routes/api/v1/auth/mod.rs +++ b/crates/app/src/routes/api/v1/auth/mod.rs @@ -18,6 +18,7 @@ use axum::{ }; use axum_extra::extract::CookieJar; use serde::Deserialize; +use tetratto_core::model::addr::RemoteAddr; use tetratto_shared::hash::hash; use cf_turnstile::{SiteVerifyRequest, TurnstileClient}; @@ -52,7 +53,11 @@ pub async fn register_request( .to_string(); // check for ip ban - if data.get_ipban_by_ip(&real_ip).await.is_ok() { + if data + .get_ipban_by_addr(RemoteAddr::from(real_ip.as_str())) + .await + .is_ok() + { return (None, Json(Error::NotAllowed.into())); } diff --git a/crates/app/src/routes/api/v1/communities/posts.rs b/crates/app/src/routes/api/v1/communities/posts.rs index ea510ef..dd9d19d 100644 --- a/crates/app/src/routes/api/v1/communities/posts.rs +++ b/crates/app/src/routes/api/v1/communities/posts.rs @@ -1,6 +1,12 @@ -use axum::{extract::Path, response::IntoResponse, Extension, Json}; +use axum::{ + extract::Path, + http::{HeaderMap, HeaderValue}, + response::IntoResponse, + Extension, Json, +}; use axum_extra::extract::CookieJar; use tetratto_core::model::{ + addr::RemoteAddr, communities::Post, permissions::FinePermission, uploads::{MediaType, MediaUpload}, @@ -18,6 +24,7 @@ pub const MAXIMUM_FILE_SIZE: usize = 4194304; pub async fn create_request( jar: CookieJar, + headers: HeaderMap, Extension(data): Extension, JsonMultipart(images, req): JsonMultipart, ) -> impl IntoResponse { @@ -42,6 +49,24 @@ pub async fn create_request( ); } + // get real ip + let real_ip = headers + .get(data.0.security.real_ip_header.to_owned()) + .unwrap_or(&HeaderValue::from_static("")) + .to_str() + .unwrap_or("") + .to_string(); + + // check for ip ban + if data + .get_ipban_by_addr(RemoteAddr::from(real_ip.as_str())) + .await + .is_ok() + { + return Json(Error::NotAllowed.into()); + } + + // ... let mut props = Post::new( req.content, match req.community.parse::() { diff --git a/crates/core/src/database/ipbans.rs b/crates/core/src/database/ipbans.rs index 64a64f6..71478cc 100644 --- a/crates/core/src/database/ipbans.rs +++ b/crates/core/src/database/ipbans.rs @@ -1,5 +1,6 @@ use super::*; use crate::cache::Cache; +use crate::model::addr::RemoteAddr; use crate::model::moderation::AuditLogEntry; use crate::model::{Error, Result, auth::IpBan, auth::User, permissions::FinePermission}; use crate::{auto_method, execute, get, query_row, query_rows, params}; @@ -26,6 +27,30 @@ impl DataManager { auto_method!(get_ipban_by_ip(&str)@get_ipban_from_row -> "SELECT * FROM ipbans WHERE ip = $1" --name="ip ban" --returns=IpBan --cache-key-tmpl="atto.ipban:{}"); + /// Get an IP ban as a [`RemoteAddr`]. + /// + /// # Arguments + /// * `prefix` + pub async fn get_ipban_by_addr(&self, addr: RemoteAddr) -> Result { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = query_row!( + &conn, + "SELECT * FROM ipbans WHERE ip LIKE $1", + &[&format!("{}%", addr.prefix(None))], + |x| { Ok(Self::get_ipban_from_row(x)) } + ); + + if res.is_err() { + return Err(Error::GeneralNotFound("ip ban".to_string())); + } + + Ok(res.unwrap()) + } + /// Get all IP bans (paginated). /// /// # Arguments diff --git a/crates/core/src/model/addr.rs b/crates/core/src/model/addr.rs new file mode 100644 index 0000000..2f92e63 --- /dev/null +++ b/crates/core/src/model/addr.rs @@ -0,0 +1,61 @@ +use std::net::SocketAddr; + +/// How many bytes should be chopped off the end of an IPV6 address to get its prefix. +pub(crate) const IPV6_PREFIX_BYTES: usize = 8; + +/// The protocol of a [`RemoteAddr`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AddrProto { + IPV4, + IPV6, +} + +/// A representation of a remote IP address. +#[derive(Clone, Debug)] +pub struct RemoteAddr(pub SocketAddr, pub AddrProto); + +impl From<&str> for RemoteAddr { + fn from(value: &str) -> Self { + if value.len() >= 16 { + // ipv6 (16 bytes; 128 bits) + if !(value.starts_with("[") | value.contains("]:1000")) { + Self(format!("[{value}]:1000").parse().unwrap(), AddrProto::IPV6) + } else { + Self(value.parse().unwrap(), AddrProto::IPV6) + } + } else { + // ipv4 (4 bytes; 32 bits) + Self(value.parse().unwrap(), AddrProto::IPV4) + } + } +} + +impl RemoteAddr { + /// Get the address' prefix (returns the entire address for IPV4 addresses). + /// + /// Operates on the IP address **without** colon characters. + pub fn prefix(&self, chop: Option) -> String { + if self.1 == AddrProto::IPV4 { + return self.0.to_string(); + } + + // we're adding 2 bytes to the chop bytes because we also + // need to remove 2 colons + let as_string = self.ipv6_inner(); + (&as_string[0..(match chop { + Some(c) => c, + None => IPV6_PREFIX_BYTES, + } + (IPV6_PREFIX_BYTES / 4))]) + .to_string() + } + + /// [`Self::prefix`], but it returns another [`RemoteAddr`]. + pub fn to_prefix(self, chop: Option) -> Self { + Self::from(self.prefix(chop).as_str()) + } + + /// Get the innter content from the address (as ipv6). + pub fn ipv6_inner(&self) -> String { + self.0.to_string().replace("[", "").replace("]:1000", "") + } +} diff --git a/crates/core/src/model/mod.rs b/crates/core/src/model/mod.rs index 12b52b4..abd044a 100644 --- a/crates/core/src/model/mod.rs +++ b/crates/core/src/model/mod.rs @@ -1,3 +1,4 @@ +pub mod addr; pub mod auth; pub mod communities; pub mod communities_permissions;