diff options
| author | gennyble <gen@nyble.dev> | 2026-09-21 18:17:02 -0500 |
|---|---|---|
| committer | gennyble <gen@nyble.dev> | 2026-09-21 18:17:02 -0500 |
| commit | 5acb56ee391f71eca30cb756b47a8cdc9273a9ce (patch) | |
| tree | 1b850991d8e21009bdde8a125dac64b9bba0f028 /sessionhash/src/lib.rs | |
| parent | 5473d1d2c0d5c6b4d2897bba8cc0c3f2199add54 (diff) | |
sessionhash
Diffstat (limited to 'sessionhash/src/lib.rs')
| -rw-r--r-- | sessionhash/src/lib.rs | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/sessionhash/src/lib.rs b/sessionhash/src/lib.rs new file mode 100644 index 0000000..67e6659 --- /dev/null +++ b/sessionhash/src/lib.rs @@ -0,0 +1,184 @@ +use std::{ + fmt, + ops::Deref, + sync::atomic::{AtomicU8, AtomicUsize}, +}; + +use axum::{ + RequestPartsExt, + extract::{FromRequestParts, OptionalFromRequestParts}, + http::{HeaderName, StatusCode, header, request::Parts}, +}; +use sha2::{Digest, Sha256}; + +static REQUEST_ID: AtomicU8 = AtomicU8::new(0); + +#[derive(Debug, Clone)] +pub struct RequestContext { + id: u8, + hash: SessionHash, + referer: Option<Referer>, +} + +impl RequestContext { + pub fn hash(&self) -> &SessionHash { + &self.hash + } + + pub fn referer(&self) -> Option<&str> { + self.referer.as_deref() + } + + pub fn identifying_string(&self) -> String { + format!("[{}|{:>3}]", self.hash, self.id) + } +} + +impl<S> FromRequestParts<S> for RequestContext +where + S: Send + Sync, +{ + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { + let id = REQUEST_ID.fetch_add(1, std::sync::atomic::Ordering::Acquire); + let hash = parts.extract::<SessionHash>().await?; + let referer = parts.extract::<Option<Referer>>().await?; + + Ok(Self { id, hash, referer }) + } +} + +impl fmt::Display for RequestContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let id = self.id; + match self.referer() { + None => write!(f, "[{}|{id:>3}]", self.hash), + Some(refer) => write!(f, "[{}|{id:>3}] (refer {refer})", self.hash), + } + } +} + +#[derive(Debug, Clone)] +pub struct RemoteIp(String); + +impl<S> FromRequestParts<S> for RemoteIp +where + S: Send + Sync, +{ + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { + if let Some(forwarded_for) = parts + .headers + .get(HeaderName::from_static("x-forwarded-for")) + { + match forwarded_for.to_str() { + Err(_e) => Err((StatusCode::INTERNAL_SERVER_ERROR, "server error")), + Ok(remoteip) => Ok(Self(remoteip.to_owned())), + } + } else { + Err((StatusCode::INTERNAL_SERVER_ERROR, "server error")) + } + } +} + +impl Deref for RemoteIp { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Copy, Clone)] +pub struct SessionHash([u8; 4]); + +impl<S> FromRequestParts<S> for SessionHash +where + S: Send + Sync, +{ + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { + let remote = parts + .extract::<RemoteIp>() + .await + .map(|rip| rip.0) + .unwrap_or(String::from("127.0.0.1")); + + let agent = parts + .headers + .get(header::USER_AGENT) + .and_then(|ua| ua.to_str().ok()) + .unwrap_or("unknown"); + + let date = timeparse::whichever_now(); + let date_str = timeparse::format::iso_date(&date.date()); + let string = format!("{remote} - {agent} - {date_str}"); + + let mut hasher = Sha256::new(); + hasher.update(string.as_bytes()); + let hash = hasher.finalize(); + let slc: [u8; 4] = hash.as_slice()[0..4].try_into().unwrap(); + + Ok(Self(slc)) + } +} + +impl fmt::Display for SessionHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in self.0.as_slice() { + write!(f, "{byte:02X}")?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct Referer(String); + +impl<S> OptionalFromRequestParts<S> for Referer +where + S: Send + Sync, +{ + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts( + parts: &mut Parts, + _state: &S, + ) -> Result<Option<Self>, Self::Rejection> { + if let Some(refer) = parts.headers.get(header::REFERER) { + match refer.to_str() { + Err(_e) => Err((StatusCode::INTERNAL_SERVER_ERROR, "server error")), + Ok(refer) => match url::Url::parse(refer) { + Err(_e) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + "failed to parse referer url", + )), + Ok(url) => match url.host_str() { + None => Err((StatusCode::INTERNAL_SERVER_ERROR, "referer url had no host")), + Some(str) => Ok(Some(Referer(str.to_owned()))), + }, + }, + } + } else { + Ok(None) + } + } +} + +impl Deref for Referer { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Display for Referer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} |
