about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock135
-rw-r--r--Cargo.toml9
-rw-r--r--readme.md14
-rw-r--r--src/main.rs31
-rw-r--r--src/util.rs134
5 files changed, 314 insertions, 9 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 1659482..b58a234 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -52,12 +52,14 @@ dependencies = [
  "camino",
  "confindent",
  "cutie",
+ "sha2",
  "snafu",
  "time",
  "tokio",
  "tokio-util",
  "tracing",
  "tracing-subscriber",
+ "url",
 ]
 
 [[package]]
@@ -142,6 +144,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
 name = "bytes"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -171,6 +182,25 @@ version = "3.0.0"
 source = "git+https://github.com/gennyble/confindent#e44cf398923fe90ec07f1ee0cdeae97c85a97d1a"
 
 [[package]]
+name = "cpufeatures"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
 name = "cutie"
 version = "0.1.0"
 dependencies = [
@@ -187,6 +217,16 @@ dependencies = [
 ]
 
 [[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
 name = "equivalent"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -247,6 +287,16 @@ dependencies = [
 ]
 
 [[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
 name = "gimli"
 version = "0.28.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -372,6 +422,16 @@ dependencies = [
 ]
 
 [[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
 name = "indexmap"
 version = "2.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -489,6 +549,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "num_threads"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
+dependencies = [
+ "libc",
+]
+
+[[package]]
 name = "object"
 version = "0.32.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -725,6 +794,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
 name = "sharded-slab"
 version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -843,7 +923,9 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
 dependencies = [
  "deranged",
  "itoa",
+ "libc",
  "num-conv",
+ "num_threads",
  "powerfmt",
  "serde",
  "time-core",
@@ -867,6 +949,21 @@ dependencies = [
 ]
 
 [[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
 name = "tokio"
 version = "1.36.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1001,18 +1098,56 @@ dependencies = [
 ]
 
 [[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+
+[[package]]
 name = "unicode-ident"
 version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 
 [[package]]
+name = "unicode-normalization"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
 name = "valuable"
 version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
 
 [[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
 name = "wasi"
 version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index c13ed5b..685ff75 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,4 +16,11 @@ confindent = { git = "https://github.com/gennyble/confindent" }
 cutie = { path = "../cutie" }
 tracing = "0.1.40"
 tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
-time = { version = "0.3.36", features = ["macros", "parsing", "formatting"] }
+time = { version = "0.3.36", features = [
+	"macros",
+	"parsing",
+	"formatting",
+	"local-offset",
+] }
+sha2 = "0.10.8"
+url = "2.5.0"
diff --git a/readme.md b/readme.md
index 11fb06d..5bc226f 100644
--- a/readme.md
+++ b/readme.md
@@ -10,6 +10,20 @@ where I am in the tile.
 Times are stored by whenwasit in a CSV files. I want to add the times to the
 bottom of the page again. I miss them.
 
+**Anlytics: For Fun**
+Seems fun, anyway, to try and design some weird anyltics system. Can we make it
+private enough that it can just be public?
+
+A friend says that the Analytics should be on a different system so that when it
+gets overhwlemed the main site can keep functioning. So, here's an idea:  
+Just send it a stream of data through UDP? lmao. It can be on a separate VPS
+in the same datacenter *(so we can use a private IPv4)* and maybe we have a sort
+of heartbeat system to have a reasonable chance of not sending data to a dead
+service.
+
+TODO:
+- [] don't log referer if it's ourself
+
 ## Dirfiles
 These are files that match the name of the directory.
 
diff --git a/src/main.rs b/src/main.rs
index b43e463..57c373e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,6 +6,7 @@ mod markup;
 mod settings;
 mod templated;
 mod timeparse;
+mod util;
 
 use std::{os::unix::fs::MetadataExt, str::FromStr};
 
@@ -25,6 +26,7 @@ use fs::Filesystem;
 use settings::Settings;
 use tokio_util::io::ReaderStream;
 use tracing_subscriber::{prelude::*, EnvFilter};
+use util::{Referer, RemoteIp, SessionId};
 
 use crate::{
 	fs::{PathResolution, Webpath},
@@ -35,11 +37,8 @@ use crate::{
 async fn main() {
 	match std::env::args().nth(1).as_deref() {
 		Some("atomizer") => atomizer::main(),
-		Some("serve") =>
 		/* fallthrough*/
-		{
-			()
-		}
+		Some("serve") => (),
 		_ => (),
 	}
 
@@ -74,16 +73,23 @@ async fn main() {
 	axum::serve(listener, app).await.unwrap()
 }
 
-async fn index_handler(fse: Extension<Filesystem>, se: Extension<Settings>) -> Response {
-	handler(fse, se, Path(String::from("/"))).await
+async fn index_handler(
+	fse: Extension<Filesystem>,
+	se: Extension<Settings>,
+	sid: SessionId,
+	rfr: Option<Referer>,
+) -> Response {
+	handler(fse, se, sid, rfr, Path(String::from("/"))).await
 }
 
 async fn handler(
 	Extension(fs): Extension<Filesystem>,
 	Extension(settings): Extension<Settings>,
+	sid: SessionId,
+	rfr: Option<Referer>,
 	Path(path): Path<String>,
 ) -> Response {
-	match falible_handler(fs, settings, path).await {
+	match falible_handler(fs, settings, sid, rfr, path).await {
 		Ok(resp) => resp,
 		Err(re) => Response::builder()
 			.body(Body::from(re.to_string()))
@@ -94,6 +100,8 @@ async fn handler(
 async fn falible_handler(
 	fs: Filesystem,
 	settings: Settings,
+	sid: SessionId,
+	rfr: Option<Referer>,
 	path: String,
 ) -> Result<Response, RuntimeError> {
 	tracing::debug!("webpath = {path}");
@@ -105,7 +113,14 @@ async fn falible_handler(
 		return Ok(redirect(webpath.as_dir()));
 	}
 
-	tracing::info!("serving {webpath}");
+	match rfr {
+		None => {
+			tracing::info!("[{sid}] serving {webpath}");
+		}
+		Some(referer) => {
+			tracing::info!("[{sid}] (refer {referer}) serving {webpath}");
+		}
+	}
 
 	let ext = resolve.filepath.extension().unwrap_or_default();
 
diff --git a/src/util.rs b/src/util.rs
new file mode 100644
index 0000000..11431e5
--- /dev/null
+++ b/src/util.rs
@@ -0,0 +1,134 @@
+use core::fmt;
+use std::ops::Deref;
+
+use axum::{
+	async_trait,
+	extract::FromRequestParts,
+	http::{header, request::Parts, HeaderName, StatusCode},
+	RequestPartsExt,
+};
+use sha2::{Digest, Sha256};
+use time::{format_description::FormatItem, macros::format_description, OffsetDateTime};
+
+pub struct RemoteIp(String);
+
+#[async_trait]
+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
+	}
+}
+
+const FMT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]");
+
+pub struct SessionId([u8; 4]);
+
+#[async_trait]
+impl<S> FromRequestParts<S> for SessionId
+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 = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc());
+
+		let string = format!("{remote} - {agent} - {}", date.format(FMT).unwrap());
+
+		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 SessionId {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		for byte in self.0.as_slice() {
+			write!(f, "{byte:02X}")?;
+		}
+
+		Ok(())
+	}
+}
+
+pub struct Referer(String);
+
+#[async_trait]
+impl<S> FromRequestParts<S> for Referer
+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(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(Referer(str.to_owned())),
+					},
+				},
+			}
+		} else {
+			Err((StatusCode::INTERNAL_SERVER_ERROR, "server error"))
+		}
+	}
+}
+
+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)
+	}
+}