Compare commits
3 commits
369915854c
...
a0574cf066
Author | SHA1 | Date | |
---|---|---|---|
|
a0574cf066 | ||
|
813ea8770a | ||
|
3d96092799 |
8 changed files with 156 additions and 42 deletions
41
Cargo.lock
generated
41
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
0
favicon.ico
Normal file
26
index.html
Normal file
26
index.html
Normal 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>
|
|
@ -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);
|
||||
|
|
90
src/main.rs
90
src/main.rs
|
@ -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
25
user/index.html
Normal 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>
|
Loading…
Reference in a new issue