From 813ea8770ad979b46c2654dfe0e1e32e3e67c840 Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 24 Mar 2024 17:37:06 -0700 Subject: [PATCH] adding hour-based visitation key, other stuff --- Cargo.lock | 41 +++++++++++++ Cargo.toml | 6 +- env.example | 5 +- favicon.ico | 0 index.html | 26 ++++++++ migrations/20240317182405_hits.up.sql | 5 +- src/main.rs | 87 ++++++++++++++++----------- user/index.html | 25 ++++++++ 8 files changed, 154 insertions(+), 41 deletions(-) create mode 100644 favicon.ico create mode 100644 index.html create mode 100644 user/index.html diff --git a/Cargo.lock b/Cargo.lock index 302e8dc..de3cad8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -612,9 +612,13 @@ dependencies = [ "clap", "dotenvy", "env_logger", + "lazy_static", "log", + "rand", + "ring", "sqlx", "tokio", + "tower-http", "url", ] @@ -1124,6 +1128,21 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rsa" version = "0.9.6" @@ -1666,6 +1685,22 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.4.2", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.2" @@ -1764,6 +1799,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 96daf6b..c3c5168 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,11 @@ chrono = { version = "0.4", default-features = false, features = ["now"] } clap = { version = "4.5", default-features = false, features = ["std", "derive", "unicode", "help", "usage"] } dotenvy = { version = "0.15", default-features = false } env_logger = { version = "0.11", default-features = false, features = ["humantime"] } +lazy_static = "1.4" log = { version = "0.4", default-features = false, features = ["std"] } +rand = { version = "0.8", default-features = false, features = ["std", "std_rng"] } +ring = { version = "0.17", features = ["std"] } sqlx = { version = "0.7", default-features = false, features = ["macros", "sqlite", "migrate", "tls-none", "runtime-tokio"] } tokio = { version = "1", default-features = false, features = ["signal", "rt-multi-thread"] } -url = { version = "2.5.0", default-features = false } +tower-http = { version = "0.5", default-features = false, features = ["cors"] } +url = { version = "2.5", default-features = false } diff --git a/env.example b/env.example index a755e4b..96e0380 100644 --- a/env.example +++ b/env.example @@ -1,4 +1,5 @@ DATABASE_URL=sqlite:///${HOME}/.hitman.db -DATABASE_FILE=file:///${HOME}/.hitman.db -LISTENING_ADDR=127.0.0.1 +DATABASE_FILE=${HOME}/.hitman.db +LISTENING_ADDR=0.0.0.0 LISTENING_PORT=5000 +HITMAN_ORIGIN=http://localhost:3000 diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/index.html b/index.html new file mode 100644 index 0000000..721e54a --- /dev/null +++ b/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + +

+

user

+ + + + diff --git a/migrations/20240317182405_hits.up.sql b/migrations/20240317182405_hits.up.sql index 9e377ff..a5853c2 100644 --- a/migrations/20240317182405_hits.up.sql +++ b/migrations/20240317182405_hits.up.sql @@ -1,7 +1,8 @@ create table if not exists hits ( id integer primary key, - page text not null, - accessed text not null + page text not null, -- the slug from the page + hit_key blob not null unique, + accessed timestamp not null default CURRENT_TIMESTAMP ); create index if not exists hits_page_dex on hits(page); diff --git a/src/main.rs b/src/main.rs index 4126f9a..4c8ad56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,25 +6,41 @@ use std::{ }; use axum::{ + debug_handler, extract::{Path, State}, - http::{header::REFERER, HeaderMap}, + http::{header::REFERER, method::Method, HeaderMap, HeaderValue}, routing::get, Router, }; use clap::Parser; +use lazy_static::lazy_static; +use ring::digest::{Context, SHA256}; use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions}; use tokio::net::TcpListener; +use tower_http::cors::{self, CorsLayer}; use url::Url; +lazy_static! { + static ref HITMAN_ORIGIN: String = + std::env::var("HITMAN_ORIGIN").expect("could not get origin for service"); + static ref SESSION_SALT: u64 = rand::random(); +} + #[tokio::main] async fn main() { init(); let pool = db().await; + let origin = HeaderValue::from_str(&HITMAN_ORIGIN).unwrap(); + let cors_layer = CorsLayer::new() + .allow_origin(origin) + .allow_methods(cors::AllowMethods::exact(Method::GET)); + let app = Router::new() - .route("/hit", get(register_hit)) - .route("/hits", get(get_hits)) - .route("/hits/:period", get(get_hits)) + .route("/hit/:slug", get(register_hit)) + .route("/hits/:slug", get(get_hits)) + .route("/hits/:slug/:period", get(get_hits)) + .layer(cors_layer) .with_state(pool.clone()) .into_make_service(); let listener = mklistener().await; @@ -36,25 +52,29 @@ async fn main() { pool.close().await; } -async fn register_hit(State(db): State, headers: HeaderMap) -> String { +#[debug_handler] +async fn register_hit( + Path(slug): Path, + State(db): State, + req: HeaderMap, +) -> String { + let host = req + .get("host") + .cloned() + .unwrap_or(HeaderValue::from_str("").unwrap()); + let host = host.to_str().unwrap_or(""); + let now = chrono::Utc::now(); - let referer = headers.get(REFERER); - let page = if let Some(referer) = referer { - let p = referer.to_str().unwrap_or("/").to_string(); - if let Ok(path) = Url::parse(&p) { - path.path().to_string() - } else { - return "".to_string(); - } - } else { - return "".to_string(); - }; - let page = &page; - let now = now.to_rfc3339(); - let now = &now; + // What we really want is just the date + hour in 24-hour format; this limits + // duplicate views from the same host to one per hour: + let now = now.split(':').take(1).next().unwrap(); - sqlx::query!("insert into hits (page, accessed) values (?, ?)", page, now) + let salt = *SESSION_SALT; + let key = format!("{now}{host}{slug}{salt}").into_bytes(); + let key = shasum(&key); + + sqlx::query!("insert into hits (page, hit_key) values (?, ?)", slug, key,) .execute(&db) .await .unwrap_or_default(); @@ -65,35 +85,24 @@ async fn register_hit(State(db): State, headers: HeaderMap) -> Strin #[axum::debug_handler] async fn get_hits( State(db): State, + Path(slug): Path, period: Option>, - headers: HeaderMap, ) -> String { let now = chrono::Utc::now(); - let referer = headers.get(REFERER); - let page = if let Some(referer) = referer { - let p = referer.to_str().unwrap_or("/").to_string(); - if let Ok(path) = Url::parse(&p) { - path.path().to_string() - } else { - return "".to_string(); - } - } else { - return "".to_string(); - }; - let page = &page; + let slug = &slug; let count = match period.unwrap_or(Path("all".to_string())).as_str() { "day" => { let then = now - chrono::Duration::try_hours(24).unwrap(); let then = then.to_rfc3339(); - get_period_hits(&db, page, &then).await + get_period_hits(&db, slug, &then).await } "week" => { let then = now - chrono::Duration::try_days(7).unwrap(); let then = then.to_rfc3339(); - get_period_hits(&db, page, &then).await + get_period_hits(&db, slug, &then).await } - _ => sqlx::query_scalar!("select count(*) from hits where page = ?", page) + _ => sqlx::query_scalar!("select count(*) from hits where page = ?", slug) .fetch_one(&db) .await .unwrap_or(1), @@ -202,3 +211,9 @@ async fn shutdown_signal() { _ = terminate => {}, } } + +fn shasum(input: &[u8]) -> Vec { + let mut context = Context::new(&SHA256); + context.update(input); + context.finish().as_ref().to_vec() +} diff --git a/user/index.html b/user/index.html new file mode 100644 index 0000000..c8f4c46 --- /dev/null +++ b/user/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + +

+ + + + +