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
Binary file assets/pixel.png has changed
--- 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,
             ],
         )
 }