about summary refs log tree commit diff
path: root/sessionhash/src/lib.rs
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2026-09-21 18:17:02 -0500
committergennyble <gen@nyble.dev>2026-09-21 18:17:02 -0500
commit5acb56ee391f71eca30cb756b47a8cdc9273a9ce (patch)
tree1b850991d8e21009bdde8a125dac64b9bba0f028 /sessionhash/src/lib.rs
parent5473d1d2c0d5c6b4d2897bba8cc0c3f2199add54 (diff)
sessionhash
Diffstat (limited to 'sessionhash/src/lib.rs')
-rw-r--r--sessionhash/src/lib.rs184
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)
+	}
+}