diff --git a/.gitignore b/.gitignore index 921f09c..272494a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .history /frontend/node_modules/* db.sqlite +config.toml diff --git a/Cargo.lock b/Cargo.lock index 09d48e7..2743aee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,41 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -263,6 +298,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -284,7 +329,11 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ + "aes-gcm", + "base64", "percent-encoding", + "rand", + "subtle", "time", "version_check", ] @@ -360,9 +409,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "der" version = "0.7.10" @@ -586,6 +645,16 @@ dependencies = [ "wasi 0.14.2+wasi-0.2.4", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.31.1" @@ -925,6 +994,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "io-uring" version = "0.7.9" @@ -1193,6 +1271,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "overload" version = "0.1.1" @@ -1282,6 +1366,18 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "potential_utf" version = "0.1.2" @@ -1509,7 +1605,7 @@ dependencies = [ "serde_json", "serde_yaml", "siphasher", - "toml", + "toml 0.8.23", "triomphe", ] @@ -1631,6 +1727,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2143,11 +2248,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_edit", ] +[[package]] +name = "toml" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 1.0.0", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -2157,6 +2277,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -2165,18 +2294,33 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_write", "winnow", ] +[[package]] +name = "toml_parser" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" + [[package]] name = "tower" version = "0.5.2" @@ -2343,6 +2487,16 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -2520,7 +2674,9 @@ dependencies = [ "rust-i18n", "serde", "sqlx", + "time", "tokio", + "toml 0.9.5", "tower-http", "tracing", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index addd7ed..bf2464d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,13 +5,15 @@ edition = "2024" [dependencies] axum = "0.8" -axum-extra = { version = "0.10", features = ["cookie"] } +axum-extra = { version = "0.10", features = ["cookie-private", "cookie"] } chrono = { version = "0.4", features = ["serde"] } maud = { version = "0.27", features = ["axum"] } rust-i18n = "3.1" serde = { version = "1", features = ["derive"] } sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros", "chrono"] } +time = "0.3.41" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +toml = "0.9.5" tower-http = { version = "0.6", features = ["fs"] } tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } diff --git a/src/game.rs b/src/game.rs index 6403878..e04eaab 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,7 +1,7 @@ use crate::{ language::language, page::{MyMessage, Page}, - Backend, NameUpdateError, + AppState, Backend, NameUpdateError, }; use axum::{ extract::{Path, State}, @@ -10,7 +10,7 @@ use axum::{ routing::{get, post}, Form, Router, }; -use axum_extra::extract::CookieJar; +use axum_extra::extract::PrivateCookieJar; use maud::{html, Markup, PreEscaped}; use serde::Deserialize; use std::sync::Arc; @@ -18,7 +18,7 @@ use uuid::Uuid; async fn index( State(backend): State>, - cookies: CookieJar, + cookies: PrivateCookieJar, headers: HeaderMap, ) -> Response { retu(backend, cookies, headers, None).await @@ -26,7 +26,7 @@ async fn index( async fn retu( backend: Arc, - cookies: CookieJar, + cookies: PrivateCookieJar, headers: HeaderMap, message: Option, ) -> Response { @@ -106,7 +106,7 @@ async fn retu( async fn game( State(backend): State>, - cookies: CookieJar, + cookies: PrivateCookieJar, headers: HeaderMap, Path(uuid): Path, ) -> Response { @@ -135,7 +135,7 @@ async fn game( retu(backend, cookies, headers, Some(message)).await } -async fn not_found(cookies: CookieJar, headers: HeaderMap) -> Markup { +async fn not_found(cookies: PrivateCookieJar, headers: HeaderMap) -> Markup { let lang = language(&cookies, &headers); Page::new(lang).content(html! { h1 { (t!("not_found_title")) } @@ -149,7 +149,7 @@ struct NameForm { async fn set_name( State(backend): State>, - cookies: CookieJar, + cookies: PrivateCookieJar, headers: HeaderMap, Form(form): Form, ) -> Response { @@ -179,7 +179,7 @@ async fn set_name( retu(backend, cookies, headers, Some(message)).await } -pub(super) fn routes() -> Router> { +pub(super) fn routes() -> Router { Router::new() .route("/game", get(index)) .route("/game", post(set_name)) diff --git a/src/index.rs b/src/index.rs index a8d7a60..1ab1fcb 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1,9 +1,9 @@ use crate::{language::language, page::Page}; use axum::http::HeaderMap; -use axum_extra::extract::CookieJar; +use axum_extra::extract::PrivateCookieJar; use maud::{html, Markup, PreEscaped}; -pub(super) async fn index(cookies: CookieJar, headers: HeaderMap) -> Markup { +pub(super) async fn index(cookies: PrivateCookieJar, headers: HeaderMap) -> Markup { let lang = language(&cookies, &headers); rust_i18n::set_locale(lang.to_locale()); diff --git a/src/language.rs b/src/language.rs index f30f1da..71f44d8 100644 --- a/src/language.rs +++ b/src/language.rs @@ -1,8 +1,8 @@ use crate::Language; use axum::http::HeaderMap; -use axum_extra::extract::CookieJar; +use axum_extra::extract::PrivateCookieJar; -pub(crate) fn language(cookies: &CookieJar, headers: &HeaderMap) -> Language { +pub(crate) fn language(cookies: &PrivateCookieJar, headers: &HeaderMap) -> Language { if let Some(lang_cookie) = cookies.clone().get("language") { // Return existing language cookie lang_cookie.value().to_string().into() diff --git a/src/main.rs b/src/main.rs index 50694b1..b4b10ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,20 @@ use crate::model::client::Client; use axum::{http::HeaderMap, routing::get, Router}; -use axum_extra::extract::{cookie::Cookie, CookieJar}; +use axum_extra::extract::{ + cookie::{Cookie, Expiration, Key}, + PrivateCookieJar, +}; +use serde::{Deserialize, Serialize}; use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, SqlitePool}; use std::{ collections::HashSet, fmt::Display, + fs, + path::Path, str::FromStr, sync::{Arc, LazyLock}, }; +use time::{Duration, OffsetDateTime}; use tower_http::services::ServeDir; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; use uuid::Uuid; @@ -140,7 +147,7 @@ mod tests { } impl Backend { - async fn client(&self, cookies: CookieJar) -> (CookieJar, Client) { + async fn client(&self, cookies: PrivateCookieJar) -> (PrivateCookieJar, Client) { let existing_uuid = cookies .get("client_id") .and_then(|cookie| Uuid::parse_str(cookie.value()).ok()); @@ -149,14 +156,24 @@ impl Backend { Some(uuid) => (cookies, self.get_client(&uuid).await), None => { let new_id = Uuid::new_v4(); - let updated_cookies = cookies.add(Cookie::new("client_id", new_id.to_string())); + let expiration_date = OffsetDateTime::now_utc() + Duration::days(30); + let mut cookie = Cookie::new("client_id", new_id.to_string()); + cookie.set_expires(Expiration::DateTime(expiration_date.into())); + cookie.set_http_only(true); + cookie.set_secure(true); + + let updated_cookies = cookies.add(cookie); (updated_cookies, self.get_client(&new_id).await) } } } // Combined method for getting both client and language - async fn client_full(&self, cookies: CookieJar, headers: &HeaderMap) -> (CookieJar, Req) { + async fn client_full( + &self, + cookies: PrivateCookieJar, + headers: &HeaderMap, + ) -> (PrivateCookieJar, Req) { let (cookies, client) = self.client(cookies).await; let lang = language::language(&cookies, headers); (cookies, Req { client, lang }) @@ -190,6 +207,55 @@ impl Backend { } } +#[derive(Clone)] +pub struct AppState { + pub(crate) backend: Arc, + pub key: Key, +} + +impl axum::extract::FromRef for Key { + fn from_ref(state: &AppState) -> Self { + state.key.clone() + } +} + +impl axum::extract::FromRef for Arc { + fn from_ref(state: &AppState) -> Self { + state.backend.clone() + } +} + +#[derive(Serialize, Deserialize)] +struct Config { + key: Vec, +} + +impl Config { + fn generate() -> Self { + Self { + key: Key::generate().master().to_vec(), + } + } +} + +fn load_or_create_key() -> Result> { + let config_path = "config.toml"; + + // Try to read existing config + if Path::new(config_path).exists() { + let content = fs::read_to_string(config_path)?; + let config: Config = toml::from_str(&content)?; + return Ok(Key::from(&config.key)); + } + + // Create new config if file doesn't exist + let config = Config::generate(); + let toml_string = toml::to_string(&config)?; + fs::write(config_path, toml_string)?; + + Ok(Key::from(&config.key)) +} + #[tokio::main] async fn main() { tracing_subscriber::registry() @@ -203,11 +269,17 @@ async fn main() { .await .unwrap(); + let key = load_or_create_key().unwrap(); + let state = AppState { + backend: Arc::new(Backend::Sqlite(db)), + key, + }; + let app = Router::new() .route("/", get(index::index)) .nest_service("/static", ServeDir::new("./static/serve")) .merge(game::routes()) - .with_state(Arc::new(Backend::Sqlite(db))); + .with_state(state); // run our app with hyper, listening globally on port 3000 let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();