Compare commits

...

3 commits

Author SHA1 Message Date
Joe
a0574cf066 hmm 2024-03-27 12:03:08 -07:00
Joe
813ea8770a adding hour-based visitation key, other stuff 2024-03-27 11:54:34 -07:00
Joe Ardent
3d96092799 add bare '/hits' route 2024-03-17 16:21:40 -07:00
8 changed files with 156 additions and 42 deletions

41
Cargo.lock generated
View file

@ -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"

View file

@ -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 }

View file

@ -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

0
favicon.ico Normal file
View file

26
index.html Normal file
View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<img src=http://localhost:5000/hit/index.html style="visibility:hidden">
<p id="allhits"></p>
<p><a href="http://localhost:3000/user">user</a></p>
<script>
const hits = document.getElementById('allhits');
fetch('http://localhost:5000/hits/index.html').then((resp) => {
return resp.text();
}).then((data) => {
console.log(data);
});
</script>
</body>
</html>

View file

@ -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);

View file

@ -6,24 +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/: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;
@ -35,25 +52,29 @@ async fn main() {
pool.close().await;
}
async fn register_hit(State(db): State<SqlitePool>, headers: HeaderMap) -> String {
#[debug_handler]
async fn register_hit(
Path(slug): Path<String>,
State(db): State<SqlitePool>,
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();
@ -64,35 +85,24 @@ async fn register_hit(State(db): State<SqlitePool>, headers: HeaderMap) -> Strin
#[axum::debug_handler]
async fn get_hits(
State(db): State<SqlitePool>,
period: Path<String>,
headers: HeaderMap,
Path(slug): Path<String>,
period: Option<Path<String>>,
) -> 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.as_str() {
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),
@ -201,3 +211,9 @@ async fn shutdown_signal() {
_ = terminate => {},
}
}
fn shasum(input: &[u8]) -> Vec<u8> {
let mut context = Context::new(&SHA256);
context.update(input);
context.finish().as_ref().to_vec()
}

25
user/index.html Normal file
View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<img src=http://localhost:5000/hit/user style="visibility:hidden">
<p id="allhits"></p>
<script>
const hits = document.getElementById('allhits');
fetch('http://localhost:5000/hits/user').then((resp) => {
return resp.text();
}).then((data) => {
console.log(data);
});
</script>
</body>
</html>