diff options
Diffstat (limited to 'src/web/extract.rs')
-rw-r--r-- | src/web/extract.rs | 139 |
1 files changed, 139 insertions, 0 deletions
diff --git a/src/web/extract.rs b/src/web/extract.rs new file mode 100644 index 0000000..4b6cd7c --- /dev/null +++ b/src/web/extract.rs @@ -0,0 +1,139 @@ +use std::{ + io::Read, + net::{IpAddr, SocketAddr}, +}; + +use axum::{ + async_trait, + body::HttpBody, + extract::{ConnectInfo, FromRequest, FromRequestParts}, + http::{request::Parts, Request}, + response::Response, + BoxError, +}; +use bytes::Bytes; +use moka::future::Cache; +use tower_cookies::Cookies; + +use super::http::text; + +pub struct Authorized; + +#[async_trait] +impl<S> FromRequestParts<S> for Authorized +where + S: Send + Sync, +{ + type Rejection = Response; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { + let Ok(Some(cookies)) = Option::<Cookies>::from_request_parts(parts, state).await else { + return Err(text(403, "No cookies provided")) + }; + + let Some(token) = cookies.get("auth") else { + return Err(text(403, "No auth token provided")) + }; + + let auth_ip: IpAddr; + { + let Some(cache) = parts.extensions.get::<Cache<String, IpAddr>>() else { + return Err(text(500, "Failed to load auth store")) + }; + + let Some(ip) = cache.get(token.value()) else { + return Err(text(401, "Unauthorized")) + }; + + auth_ip = ip + } + + let Ok(Some(RequestIp(ip))) = Option::<RequestIp>::from_request_parts(parts, state).await else { + return Err(text(403, "You have no ip")) + }; + + if auth_ip != ip { + return Err(text(403, "Auth token does not match current ip")); + } + + Ok(Self) + } +} + +pub struct RequestIp(pub IpAddr); + +#[async_trait] +impl<S> FromRequestParts<S> for RequestIp +where + S: Send + Sync, +{ + type Rejection = Response; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { + let headers = &parts.headers; + + let forwardedfor = headers + .get("x-forwarded-for") + .and_then(|h| h.to_str().ok()) + .and_then(|h| { + h.split(',') + .rev() + .find_map(|s| s.trim().parse::<IpAddr>().ok()) + }); + + if let Some(forwardedfor) = forwardedfor { + return Ok(Self(forwardedfor)); + } + + let realip = headers + .get("x-real-ip") + .and_then(|hv| hv.to_str().ok()) + .and_then(|s| s.parse::<IpAddr>().ok()); + + if let Some(realip) = realip { + return Ok(Self(realip)); + } + + let realip = headers + .get("x-real-ip") + .and_then(|hv| hv.to_str().ok()) + .and_then(|s| s.parse::<IpAddr>().ok()); + + if let Some(realip) = realip { + return Ok(Self(realip)); + } + + let info = parts.extensions.get::<ConnectInfo<SocketAddr>>(); + + if let Some(info) = info { + return Ok(Self(info.0.ip())); + } + + Err(text(403, "You have no ip")) + } +} + +pub struct Body(pub String); + +#[async_trait] +impl<S, B> FromRequest<S, B> for Body +where + B: HttpBody + Sync + Send + 'static, + B::Data: Send, + B::Error: Into<BoxError>, + S: Send + Sync, +{ + type Rejection = Response; + + async fn from_request(req: Request<B>, state: &S) -> Result<Self, Self::Rejection> { + let Ok(bytes) = Bytes::from_request(req, state).await else { + return Err(text(413, "Payload too large")); + }; + + let Ok(body) = String::from_utf8(bytes.bytes().flatten().collect()) else { + return Err(text(400, "Invalid utf8 body")) + }; + + Ok(Self(body)) + } +} |