Compare commits

..

No commits in common. "a0574cf066cb093308448aba44354be63ea1cff8" and "369915854cd6cc35cff524c68d40ddd99a7a23ae" have entirely different histories.

8 changed files with 42 additions and 156 deletions

41
Cargo.lock generated
View file

@ -612,13 +612,9 @@ dependencies = [
"clap", "clap",
"dotenvy", "dotenvy",
"env_logger", "env_logger",
"lazy_static",
"log", "log",
"rand",
"ring",
"sqlx", "sqlx",
"tokio", "tokio",
"tower-http",
"url", "url",
] ]
@ -1128,21 +1124,6 @@ dependencies = [
"bitflags 1.3.2", "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]] [[package]]
name = "rsa" name = "rsa"
version = "0.9.6" version = "0.9.6"
@ -1685,22 +1666,6 @@ dependencies = [
"tower-service", "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]] [[package]]
name = "tower-layer" name = "tower-layer"
version = "0.3.2" version = "0.3.2"
@ -1799,12 +1764,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.0" version = "2.5.0"

View file

@ -15,11 +15,7 @@ chrono = { version = "0.4", default-features = false, features = ["now"] }
clap = { version = "4.5", default-features = false, features = ["std", "derive", "unicode", "help", "usage"] } clap = { version = "4.5", default-features = false, features = ["std", "derive", "unicode", "help", "usage"] }
dotenvy = { version = "0.15", default-features = false } dotenvy = { version = "0.15", default-features = false }
env_logger = { version = "0.11", default-features = false, features = ["humantime"] } env_logger = { version = "0.11", default-features = false, features = ["humantime"] }
lazy_static = "1.4"
log = { version = "0.4", default-features = false, features = ["std"] } 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"] } 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"] } tokio = { version = "1", default-features = false, features = ["signal", "rt-multi-thread"] }
tower-http = { version = "0.5", default-features = false, features = ["cors"] } url = { version = "2.5.0", default-features = false }
url = { version = "2.5", default-features = false }

View file

@ -1,5 +1,4 @@
DATABASE_URL=sqlite:///${HOME}/.hitman.db DATABASE_URL=sqlite:///${HOME}/.hitman.db
DATABASE_FILE=${HOME}/.hitman.db DATABASE_FILE=file:///${HOME}/.hitman.db
LISTENING_ADDR=0.0.0.0 LISTENING_ADDR=127.0.0.1
LISTENING_PORT=5000 LISTENING_PORT=5000
HITMAN_ORIGIN=http://localhost:3000

View file

View file

@ -1,26 +0,0 @@
<!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,8 +1,7 @@
create table if not exists hits ( create table if not exists hits (
id integer primary key, id integer primary key,
page text not null, -- the slug from the page page text not null,
hit_key blob not null unique, accessed text not null
accessed timestamp not null default CURRENT_TIMESTAMP
); );
create index if not exists hits_page_dex on hits(page); create index if not exists hits_page_dex on hits(page);

View file

@ -6,41 +6,24 @@ use std::{
}; };
use axum::{ use axum::{
debug_handler,
extract::{Path, State}, extract::{Path, State},
http::{header::REFERER, method::Method, HeaderMap, HeaderValue}, http::{header::REFERER, HeaderMap},
routing::get, routing::get,
Router, Router,
}; };
use clap::Parser; use clap::Parser;
use lazy_static::lazy_static;
use ring::digest::{Context, SHA256};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions}; use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tower_http::cors::{self, CorsLayer};
use url::Url; 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] #[tokio::main]
async fn main() { async fn main() {
init(); init();
let pool = db().await; 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() let app = Router::new()
.route("/hit/:slug", get(register_hit)) .route("/hit", get(register_hit))
.route("/hits/:slug", get(get_hits)) .route("/hits/:period", get(get_hits))
.route("/hits/:slug/:period", get(get_hits))
.layer(cors_layer)
.with_state(pool.clone()) .with_state(pool.clone())
.into_make_service(); .into_make_service();
let listener = mklistener().await; let listener = mklistener().await;
@ -52,29 +35,25 @@ async fn main() {
pool.close().await; pool.close().await;
} }
#[debug_handler] async fn register_hit(State(db): State<SqlitePool>, headers: HeaderMap) -> String {
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 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.to_rfc3339();
// What we really want is just the date + hour in 24-hour format; this limits let now = &now;
// duplicate views from the same host to one per hour:
let now = now.split(':').take(1).next().unwrap();
let salt = *SESSION_SALT; sqlx::query!("insert into hits (page, accessed) values (?, ?)", page, now)
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) .execute(&db)
.await .await
.unwrap_or_default(); .unwrap_or_default();
@ -85,24 +64,35 @@ async fn register_hit(
#[axum::debug_handler] #[axum::debug_handler]
async fn get_hits( async fn get_hits(
State(db): State<SqlitePool>, State(db): State<SqlitePool>,
Path(slug): Path<String>, period: Path<String>,
period: Option<Path<String>>, headers: HeaderMap,
) -> String { ) -> String {
let now = chrono::Utc::now(); let now = chrono::Utc::now();
let slug = &slug; 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 count = match period.unwrap_or(Path("all".to_string())).as_str() { let count = match period.as_str() {
"day" => { "day" => {
let then = now - chrono::Duration::try_hours(24).unwrap(); let then = now - chrono::Duration::try_hours(24).unwrap();
let then = then.to_rfc3339(); let then = then.to_rfc3339();
get_period_hits(&db, slug, &then).await get_period_hits(&db, page, &then).await
} }
"week" => { "week" => {
let then = now - chrono::Duration::try_days(7).unwrap(); let then = now - chrono::Duration::try_days(7).unwrap();
let then = then.to_rfc3339(); let then = then.to_rfc3339();
get_period_hits(&db, slug, &then).await get_period_hits(&db, page, &then).await
} }
_ => sqlx::query_scalar!("select count(*) from hits where page = ?", slug) _ => sqlx::query_scalar!("select count(*) from hits where page = ?", page)
.fetch_one(&db) .fetch_one(&db)
.await .await
.unwrap_or(1), .unwrap_or(1),
@ -211,9 +201,3 @@ async fn shutdown_signal() {
_ = terminate => {}, _ = terminate => {},
} }
} }
fn shasum(input: &[u8]) -> Vec<u8> {
let mut context = Context::new(&SHA256);
context.update(input);
context.finish().as_ref().to_vec()
}

View file

@ -1,25 +0,0 @@
<!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>