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",
|
"clap",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
"rand",
|
||||||
|
"ring",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tower-http",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1124,6 +1128,21 @@ 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"
|
||||||
|
@ -1666,6 +1685,22 @@ 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"
|
||||||
|
@ -1764,6 +1799,12 @@ 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"
|
||||||
|
|
|
@ -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"] }
|
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"] }
|
||||||
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_URL=sqlite:///${HOME}/.hitman.db
|
||||||
DATABASE_FILE=file:///${HOME}/.hitman.db
|
DATABASE_FILE=${HOME}/.hitman.db
|
||||||
LISTENING_ADDR=127.0.0.1
|
LISTENING_ADDR=0.0.0.0
|
||||||
LISTENING_PORT=5000
|
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 (
|
create table if not exists hits (
|
||||||
id integer primary key,
|
id integer primary key,
|
||||||
page text not null,
|
page text not null, -- the slug from the page
|
||||||
accessed text not null
|
hit_key blob not null unique,
|
||||||
|
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);
|
||||||
|
|
90
src/main.rs
90
src/main.rs
|
@ -6,24 +6,41 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
|
debug_handler,
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
http::{header::REFERER, HeaderMap},
|
http::{header::REFERER, method::Method, HeaderMap, HeaderValue},
|
||||||
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", get(register_hit))
|
.route("/hit/:slug", get(register_hit))
|
||||||
.route("/hits/:period", get(get_hits))
|
.route("/hits/:slug", 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;
|
||||||
|
@ -35,25 +52,29 @@ async fn main() {
|
||||||
pool.close().await;
|
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 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();
|
||||||
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)
|
.execute(&db)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
@ -64,35 +85,24 @@ async fn register_hit(State(db): State<SqlitePool>, headers: HeaderMap) -> Strin
|
||||||
#[axum::debug_handler]
|
#[axum::debug_handler]
|
||||||
async fn get_hits(
|
async fn get_hits(
|
||||||
State(db): State<SqlitePool>,
|
State(db): State<SqlitePool>,
|
||||||
period: Path<String>,
|
Path(slug): Path<String>,
|
||||||
headers: HeaderMap,
|
period: Option<Path<String>>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let now = chrono::Utc::now();
|
let now = chrono::Utc::now();
|
||||||
let referer = headers.get(REFERER);
|
let slug = &slug;
|
||||||
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.as_str() {
|
let count = match period.unwrap_or(Path("all".to_string())).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, page, &then).await
|
get_period_hits(&db, slug, &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, 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)
|
.fetch_one(&db)
|
||||||
.await
|
.await
|
||||||
.unwrap_or(1),
|
.unwrap_or(1),
|
||||||
|
@ -201,3 +211,9 @@ 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()
|
||||||
|
}
|
||||||
|
|
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