From 0a7972deca01c708a68ebece4d43ea975a3111e4 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sun, 29 Oct 2023 13:52:58 +0200 Subject: [PATCH] web: load cookie secret from env var if present Signed-off-by: Manos Pitsidianakis --- web/README.md | 9 +++++++++ web/src/main.rs | 39 +++++++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/web/README.md b/web/README.md index c54e80c..b7ecbd6 100644 --- a/web/README.md +++ b/web/README.md @@ -18,3 +18,12 @@ The following environment variables can be defined to configure various settings - `SITE_SUBTITLE`, default empty. - `ROOT_URL_PREFIX`, default empty. - `SSH_NAMESPACE`, default `lists.mailpot.rs`. +- `SECRET`, randomly generated by default. + Use the same value across process restarts if you want user session cookies not to expire. + The secret must be a valid UTF-8 string that is 128 bytes long. + + You can generate one with the following command: + + ```shell + head /dev/urandom | tr -dc A-Za-z0-9 | head -c128 + ``` diff --git a/web/src/main.rs b/web/src/main.rs index 45520a4..6a9aa9c 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -41,10 +41,10 @@ fn new_state(conf: Configuration) -> Arc { }) } -fn create_app(shared_state: Arc) -> Router { +fn create_app(shared_state: Arc, secret: Option<[u8; 128]>) -> Router { let store = MemoryStore::new(); - let secret = rand::thread_rng().gen::<[u8; 128]>(); - let session_layer = SessionLayer::new(store, &secret).with_secure(false); + let secret = secret.unwrap_or_else(|| rand::thread_rng().gen::<[u8; 128]>()); + let session_layer = SessionLayer::new(store, &secret).with_secure(true); let auth_layer = AuthLayer::new(shared_state.clone(), &secret); @@ -157,6 +157,22 @@ async fn main() { return; } + let secret = if let Ok(var) = std::env::var("SECRET") { + if var.as_bytes().len() != 128 { + eprintln!("Environment variable SECRET must be 128 bytes long."); + return; + } + match var.as_bytes().try_into() { + Err(err) => { + eprintln!("Environment variable SECRET must be 128 bytes long. Error: {err}"); + return; + } + Ok(v) => Some(v), + } + } else { + None + }; + std::env::remove_var("SECRET"); #[cfg(test)] let verbosity = log::LevelFilter::Trace; #[cfg(not(test))] @@ -169,7 +185,7 @@ async fn main() { .init() .unwrap(); let conf = Configuration::from_file(config_path).unwrap(); - let app = create_app(new_state(conf)); + let app = create_app(new_state(conf), secret); let hostname = std::env::var("HOSTNAME").unwrap_or_else(|_| "0.0.0.0".to_string()); let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string()); @@ -292,7 +308,10 @@ mod tests { // list() let cl = |url, state| async move { - let response = create_app(state).oneshot(req!(get & url)).await.unwrap(); + let response = create_app(state, None) + .oneshot(req!(get & url)) + .await + .unwrap(); assert_eq!(response.status(), StatusCode::OK); @@ -308,7 +327,7 @@ mod tests { { let msg_id = ""; - let res = create_app(state.clone()) + let res = create_app(state.clone(), None) .oneshot(req!( get & format!( "/list/{id}/posts/{msgid}/", @@ -324,7 +343,7 @@ mod tests { res.headers().get(http::header::CONTENT_TYPE), Some(&http::HeaderValue::from_static("text/html; charset=utf-8")) ); - let res = create_app(state.clone()) + let res = create_app(state.clone(), None) .oneshot(req!( get & format!( "/list/{id}/posts/{msgid}/raw/", @@ -340,7 +359,7 @@ mod tests { res.headers().get(http::header::CONTENT_TYPE), Some(&http::HeaderValue::from_static("text/plain; charset=utf-8")) ); - let res = create_app(state.clone()) + let res = create_app(state.clone(), None) .oneshot(req!( get & format!( "/list/{id}/posts/{msgid}/eml/", @@ -367,7 +386,7 @@ mod tests { // help(), ssh_signin(), root() for path in ["/help/", "/"] { - let response = create_app(state.clone()) + let response = create_app(state.clone(), None) .oneshot(req!(get path)) .await .unwrap(); @@ -378,7 +397,7 @@ mod tests { // ------------------------------------------------------------ // auth.rs... - let login_app = create_app(state.clone()); + let login_app = create_app(state.clone(), None); let session_cookie = { let response = login_app .clone()