Mercurial > lbo > hg > analyrics
changeset 4:7e94d639963c
Implement request logging
author | Lewin Bormann <lbo@spheniscida.de> |
---|---|
date | Sat, 09 Jul 2022 13:22:42 -0700 |
parents | ca6c273aeb4f |
children | 2101c91aad43 |
files | Cargo.toml Rocket.toml assets/pixel.png schema_sqlite.sql src/main.rs |
diffstat | 5 files changed, 141 insertions(+), 9 deletions(-) [+] |
line wrap: on
line diff
--- a/Cargo.toml Sat Jul 09 09:32:13 2022 -0700 +++ b/Cargo.toml Sat Jul 09 13:22:42 2022 -0700 @@ -7,6 +7,7 @@ [dependencies] anyhow = "1.0.58" +either = "1.7.0" env_logger = "0.9.0" log = "0.4.17" rocket = { version = "0.5.0-rc.2", features = ["secrets"] }
--- a/Rocket.toml Sat Jul 09 09:32:13 2022 -0700 +++ b/Rocket.toml Sat Jul 09 13:22:42 2022 -0700 @@ -1,5 +1,7 @@ [default.databases.sqlite_main] url = "/home/lbo/dev/rust/analyrics/dev.sqlite" +[default.databases.sqlite_logs] +url = "/home/lbo/dev/rust/analyrics/devlogs.sqlite" [default] port = 8000
--- a/schema_sqlite.sql Sat Jul 09 09:32:13 2022 -0700 +++ b/schema_sqlite.sql Sat Jul 09 13:22:42 2022 -0700 @@ -2,7 +2,8 @@ DROP TABLE IF EXISTS users; CREATE TABLE users ( id INTEGER PRIMARY KEY, - username VARCHAR(64), - name VARCHAR(64), - password_hash VARCHAR(128) + username TEXT, + name TEXT, + salt TEXT, + password_hash TEXT, );
--- a/src/main.rs Sat Jul 09 09:32:13 2022 -0700 +++ b/src/main.rs Sat Jul 09 13:22:42 2022 -0700 @@ -1,20 +1,25 @@ use anyhow::{self, Error}; +use either::Either; use log::{debug, error, info, Level}; #[macro_use] use rocket::{self}; use rocket::form::Form; -use rocket::http::{Cookie, CookieJar, Header, Status}; +use rocket::fs::NamedFile; +use rocket::http::{Cookie, CookieJar, Header, HeaderMap, Status}; use rocket::request::{self, FlashMessage, FromRequest, Outcome, Request}; use rocket::response::{self, Flash, Redirect, Responder, Response}; -use rocket_db_pools::sqlx::{self, pool::PoolConnection, Executor, Sqlite, SqlitePool}; +use rocket_db_pools::sqlx::{ + self, pool::PoolConnection, Executor, Row, Sqlite, SqlitePool, Statement, +}; use rocket_db_pools::{Connection, Database, Pool}; use rocket_dyn_templates::{context, Template}; use std::io; +use std::net::IpAddr; use std::path::Path; use tokio::fs::{self, File}; @@ -25,7 +30,7 @@ struct ConfigDB(SqlitePool); async fn check_user_password<S: AsRef<str>>( - mut conn: PoolConnection<Sqlite>, + conn: &mut PoolConnection<Sqlite>, user: S, password: S, ) -> Result<bool, Error> { @@ -38,6 +43,64 @@ Ok(result.len() == 1) } +#[derive(Database)] +#[database("sqlite_logs")] +struct LogsDB(SqlitePool); + +async fn log_request< + S1: AsRef<str>, + S2: AsRef<str>, + S3: AsRef<str>, + S4: AsRef<str>, + S5: AsRef<str>, + S6: AsRef<str>, +>( + conn: &mut PoolConnection<Sqlite>, + ip: S1, + domain: S2, + path: S3, + status: u16, + page: Option<S4>, + refer: Option<S5>, + ua: S6, + ntags: u32, +) -> Result<u32, Error> { + let q = sqlx::query("INSERT INTO RequestLog (ip, atime, domain, path, status, pagename, refer, ua, ntags) VALUES (?, strftime('%s', 'now'), ?, ?, ?, ?, ?, ?, ?) RETURNING id"); + let q = q + .bind(ip.as_ref()) + .bind(domain.as_ref()) + .bind(path.as_ref()) + .bind(status) + .bind(page.map(|s| s.as_ref().to_string())) + .bind(refer.map(|s| s.as_ref().to_string())) + .bind(ua.as_ref()) + .bind(ntags); + let row: u32 = q.fetch_one(conn).await?.get(0); + Ok(row) +} + +async fn log_tags<S: AsRef<str>, I: Iterator<Item = S>>( + conn: &mut PoolConnection<Sqlite>, + requestid: u32, + tags: I, +) -> Result<usize, Error> { + let mut ntags = 0; + for tag in tags { + let (k, v) = tag.as_ref().split_once("=").unwrap_or((tag.as_ref(), "")); + sqlx::query("INSERT INTO RequestTags (requestid, key, value) VALUES (?, ?, ?)") + .bind(requestid) + .bind(k) + .bind(v) + .execute(&mut *conn) + .await + .map_err(|e| error!("Couldn't insert tag {}={}: {}", k, v, e)) + .unwrap(); + ntags += 1; + } + + Ok(ntags) +} + const USER_ID_COOKIE_KEY: &str = "user_id"; struct LoggedInGuard(String); @@ -56,6 +119,17 @@ } } +struct HeadersGuard<'h>(HeaderMap<'h>); + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for HeadersGuard<'r> { + type Error = Error; + + async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> { + Outcome::Success(HeadersGuard(req.headers().clone())) + } +} + async fn asset<P: AsRef<Path>>(p: P) -> io::Result<File> { fs::OpenOptions::new().read(true).open(p).await } @@ -118,8 +192,8 @@ login: Form<LoginForm>, ) -> Flash<Redirect> { // TO DO: look up user in database. - let db = db.into_inner(); - match check_user_password(db, &login.username, &login.password).await { + let mut db = db.into_inner(); + match check_user_password(&mut db, &login.username, &login.password).await { Ok(true) => { let c = Cookie::new(USER_ID_COOKIE_KEY, login.username.clone()); cookies.add_private(c); @@ -158,6 +232,58 @@ Template::render("index", context![loggedin: false, flash: f]) } +// TODO: ignore requests when logged in. +#[rocket::get("/log?<host>&<status>&<path>&<pagename>&<referer>&<tags>")] +async fn route_log( + conn: Connection<LogsDB>, + host: Option<String>, + status: Option<u16>, + path: Option<String>, + pagename: Option<String>, + referer: Option<String>, + tags: Vec<String>, + ip: IpAddr, + headers: HeadersGuard<'_>, +) -> (Status, Either<NamedFile, &'static str>) { + let mut conn = conn.into_inner(); + let ntags = tags.len() as u32; + let ua = headers.0.get_one("user-agent").unwrap_or(""); + let host: String = host.unwrap_or( + headers + .0 + .get("host") + .take(1) + .map(|s| s.to_string()) + .collect::<Vec<String>>() + .pop() + .unwrap_or(String::new()), + ); + match log_request( + &mut conn, + ip.to_string(), + host, + path.unwrap_or(String::new()), + status.unwrap_or(200), + pagename, + referer, + ua, + ntags, + ) + .await + { + Err(e) => error!("Couldn't log request: {}", e), + Ok(id) => { + log_tags(&mut conn, id, tags.iter()).await.ok(); + } + } + + if let Ok(f) = NamedFile::open("assets/pixel.png").await { + (Status::Ok, Either::Left(f)) + } else { + (Status::Ok, Either::Right("")) + } +} + #[rocket::launch] fn rocketmain() -> _ { env_logger::init(); @@ -165,6 +291,7 @@ info!("{:?}", rocket::Config::figment()); rocket::build() .attach(ConfigDB::init()) + .attach(LogsDB::init()) .attach(Template::fairing()) .mount( "/", @@ -173,7 +300,8 @@ route_index_loggedout, route_logout, route_login_form, - route_login_post + route_login_post, + route_log, ], ) }