aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/get.rs255
-rw-r--r--src/main.rs22
2 files changed, 273 insertions, 4 deletions
diff --git a/src/get.rs b/src/get.rs
index a00ac48..1e364be 100644
--- a/src/get.rs
+++ b/src/get.rs
@@ -1,3 +1,4 @@
+use std::collections::hash_map::HashMap;
use std::net::SocketAddr;
use axum::extract::{ConnectInfo, Path};
@@ -9,9 +10,16 @@ use axum::Extension;
use info_utils::prelude::*;
use crate::ServerState;
+use crate::TrackingRow;
use crate::UdsConnectInfo;
use crate::UrlRow;
+enum TrackingParameter {
+ Ip,
+ Referrer,
+ UserAgent,
+}
+
pub async fn index(Extension(state): Extension<ServerState>) -> impl IntoResponse {
if let Some(redirect) = state.main_page_redirect {
return Redirect::temporary(redirect.as_str()).into_response();
@@ -212,3 +220,250 @@ pub async fn create_id(Extension(state): Extension<ServerState>) -> Html<String>
state.host
))
}
+
+pub async fn tracking(Extension(state): Extension<ServerState>) -> impl IntoResponse {
+ let url_rows: Vec<UrlRow> = sqlx::query_as("SELECT * FROM chela.urls")
+ .fetch_all(&state.db_pool)
+ .await
+ .unwrap();
+ let html = format!(
+ r#"
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>{} Tracking</title>
+ </head>
+ <style>{}</style>
+ <body>
+ {}
+ </body>
+ </html>
+ "#,
+ state.host,
+ table_css(),
+ make_table_from_urls(&url_rows)
+ );
+
+ return Html(html).into_response();
+}
+
+pub async fn tracking_id(
+ Extension(state): Extension<ServerState>,
+ Path(id): Path<String>,
+) -> impl IntoResponse {
+ let tracking_rows: Vec<TrackingRow> =
+ sqlx::query_as("SELECT * FROM chela.tracking WHERE id = $1")
+ .bind(id.clone())
+ .fetch_all(&state.db_pool)
+ .await
+ .unwrap();
+ let url: UrlRow = sqlx::query_as("SELECT * FROM chela.urls WHERE id = $1")
+ .bind(id.clone())
+ .fetch_one(&state.db_pool)
+ .await
+ .unwrap();
+
+ let html = format!(
+ r#"
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>{} Tracking {}</title>
+ </head>
+ <style>{}</style>
+ <body>
+ <h1>Tracking for <a href="{}">{}</a> from ID '{}'</h1>
+ <h2>Visited {} times</h2>
+ {}
+
+ <h2>By IP</h2>
+ {}
+ <h2>By Referrer</h2>
+ {}
+ <h2>By User Agent</h2>
+ {}
+ </body>
+ </html>
+ "#,
+ state.host,
+ id,
+ table_css(),
+ url.url,
+ url.url,
+ url.id,
+ tracking_rows.len(),
+ make_table_from_tracking(&tracking_rows),
+ make_grouped_table_from_tracking(&tracking_rows, TrackingParameter::Ip),
+ make_grouped_table_from_tracking(&tracking_rows, TrackingParameter::Referrer),
+ make_grouped_table_from_tracking(&tracking_rows, TrackingParameter::UserAgent)
+ );
+
+ return Html(html).into_response();
+}
+
+fn make_table_from_tracking(rows: &Vec<TrackingRow>) -> String {
+ let mut html = r#"<table>
+ <colgroup>
+ <col>
+ <col>
+ <col>
+ <col>
+ <col>
+ </colgroup>
+ <tr>
+ <th>Timestamp</th>
+ <th>ID</th>
+ <th>IP</th>
+ <th>Referrer</th>
+ <th>User Agent</th>
+ </tr>
+ "#
+ .to_string();
+
+ for row in rows {
+ html += &format!(
+ r#"
+ <tr>
+ <td>{}</td>
+ <td>{}</td>
+ <td>{}</td>
+ <td>{}</td>
+ <td>{}</td>
+ </tr>
+ "#,
+ row.timestamp,
+ row.id,
+ row.ip.as_ref().unwrap_or(&String::default()),
+ row.referrer.as_ref().unwrap_or(&String::default()),
+ row.user_agent.as_ref().unwrap_or(&String::default())
+ );
+ }
+
+ html += r#"
+ </table>
+ "#;
+
+ html
+}
+
+fn make_grouped_table_from_tracking(rows: &Vec<TrackingRow>, group: TrackingParameter) -> String {
+ let column_name = match group {
+ TrackingParameter::Ip => "IP",
+ TrackingParameter::Referrer => "Referrer",
+ TrackingParameter::UserAgent => "User Agent",
+ }
+ .to_string();
+
+ let mut html = format!(
+ r#"
+ <table>
+ <colgroup>
+ <col>
+ <col>
+ </colgroup>
+ <tr>
+ <th>Occurrences</th>
+ <th>{}</th>
+ </tr>
+ "#,
+ column_name
+ );
+
+ let mut aggregate: HashMap<String, u32> = HashMap::new();
+
+ for row in rows {
+ let tracker = match group {
+ TrackingParameter::Ip => {
+ let v = match &row.ip {
+ Some(val) => val,
+ None => continue,
+ };
+ v
+ }
+ TrackingParameter::Referrer => {
+ let v = match &row.referrer {
+ Some(val) => val,
+ None => continue,
+ };
+ v
+ }
+ TrackingParameter::UserAgent => {
+ let v = match &row.user_agent {
+ Some(val) => val,
+ None => continue,
+ };
+ v
+ }
+ };
+ let count = aggregate.get(tracker).unwrap_or(&0);
+ aggregate.insert(tracker.to_string(), count + 1);
+ }
+
+ for (key, val) in aggregate {
+ html += &format!(
+ r#"
+ <tr>
+ <td>{}</td>
+ <td>{}</td>
+ </tr>
+ "#,
+ val, key
+ );
+ }
+
+ html += r#"
+ </table>
+ "#;
+
+ html
+}
+
+fn make_table_from_urls(urls: &Vec<UrlRow>) -> String {
+ let mut html = r#"<table>
+ <colgroup>
+ <col>
+ <col>
+ <col>
+ <col>
+ </colgroup>
+ <tr>
+ <th>Index</th>
+ <th>ID</th>
+ <th>URL</th>
+ <th>Custom ID</th>
+ </tr>
+ "#
+ .to_string();
+
+ for url in urls {
+ html += &format!(
+ r#"
+ <tr>
+ <td>{}</td>
+ <td><a href="/tracking/{}">{}</a></td>
+ <td><a href="{}">{}</a></td>
+ <td>{}</td>
+ </tr>
+ "#,
+ url.index, url.id, url.id, url.url, url.url, url.custom_id
+ );
+ }
+ html += r#"
+ </table>
+ "#;
+
+ html
+}
+
+fn table_css() -> String {
+ r#"
+ tr:nth-child(even) {
+ background: #f2f2f2;
+ }
+
+ table, th, td {
+ border: 1px solid black;
+ }
+ "#
+ .to_string()
+}
diff --git a/src/main.rs b/src/main.rs
index 21d29e0..83731fa 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -36,6 +36,16 @@ pub struct UrlRow {
pub index: i64,
pub id: String,
pub url: String,
+ pub custom_id: bool,
+}
+
+#[derive(Debug, Clone, sqlx::FromRow, PartialEq, Eq)]
+pub struct TrackingRow {
+ pub timestamp: chrono::DateTime<chrono::Utc>,
+ pub id: String,
+ pub ip: Option<String>,
+ pub referrer: Option<String>,
+ pub user_agent: Option<String>,
}
#[derive(Deserialize, Debug, Clone)]
@@ -73,7 +83,7 @@ async fn main() -> eyre::Result<()> {
.unwrap_or("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".to_string());
let sqids = Sqids::builder()
.alphabet(alphabet.chars().collect())
- .blocklist(["create".to_string()].into())
+ .blocklist(["create".to_string(), "tracking".to_string()].into())
.build()?;
let main_page_redirect = env::var("CHELA_MAIN_PAGE_REDIRECT").unwrap_or_default();
let behind_proxy = env::var("CHELA_BEHIND_PROXY").is_ok();
@@ -94,8 +104,10 @@ async fn serve(state: ServerState) -> eyre::Result<()> {
if unix_socket.is_empty() {
let router = Router::new()
.route("/", get(get::index))
- .route("/:id", get(get::id))
.route("/create", get(get::create_id))
+ .route("/tracking", get(get::tracking))
+ .route("/tracking/:id", get(get::tracking_id))
+ .route("/:id", get(get::id))
.route("/", post(post::create_link))
.layer(axum::Extension(state));
let address = env::var("CHELA_LISTEN_ADDRESS").unwrap_or("0.0.0.0".to_string());
@@ -110,8 +122,10 @@ async fn serve(state: ServerState) -> eyre::Result<()> {
} else {
let router = Router::new()
.route("/", get(get::index))
- .route("/:id", get(get::id_unix))
.route("/create", get(get::create_id))
+ .route("/tracking", get(get::tracking))
+ .route("/tracking/:id", get(get::tracking_id))
+ .route("/:id", get(get::id_unix))
.route("/", post(post::create_link))
.layer(axum::Extension(state));
let unix_socket_path = std::path::Path::new(&unix_socket);
@@ -184,7 +198,7 @@ CREATE TABLE IF NOT EXISTS chela.urls (
sqlx::query(
"
CREATE TABLE IF NOT EXISTS chela.tracking (
- timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
id TEXT NOT NULL,
ip TEXT,
referrer TEXT,