Compare commits
No commits in common. "a0574cf066cb093308448aba44354be63ea1cff8" and "369915854cd6cc35cff524c68d40ddd99a7a23ae" have entirely different histories.
a0574cf066
...
369915854c
8 changed files with 42 additions and 156 deletions
41
Cargo.lock
generated
41
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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 }
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
26
index.html
26
index.html
|
@ -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>
|
|
|
@ -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);
|
||||||
|
|
90
src/main.rs
90
src/main.rs
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
|
@ -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>
|
|
Loading…
Reference in a new issue