diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/get.rs | 255 | ||||
-rw-r--r-- | src/main.rs | 22 |
2 files changed, 273 insertions, 4 deletions
@@ -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, |