Commit
Author: Manos Pitsidianakis [manos@pitsidianak.is]
Hash: 0a7972deca01c708a68ebece4d43ea975a3111e4
Timestamp: Sun, 29 Oct 2023 11:52:58 +0000 (1 year ago)

+38 -10 +/-2 browse
web: load cookie secret from env var if present
web: load cookie secret from env var if present

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
1diff --git a/web/README.md b/web/README.md
2index c54e80c..b7ecbd6 100644
3--- a/web/README.md
4+++ b/web/README.md
5 @@ -18,3 +18,12 @@ The following environment variables can be defined to configure various settings
6 - `SITE_SUBTITLE`, default empty.
7 - `ROOT_URL_PREFIX`, default empty.
8 - `SSH_NAMESPACE`, default `lists.mailpot.rs`.
9+ - `SECRET`, randomly generated by default.
10+ Use the same value across process restarts if you want user session cookies not to expire.
11+ The secret must be a valid UTF-8 string that is 128 bytes long.
12+
13+ You can generate one with the following command:
14+
15+ ```shell
16+ head /dev/urandom | tr -dc A-Za-z0-9 | head -c128
17+ ```
18 diff --git a/web/src/main.rs b/web/src/main.rs
19index 45520a4..6a9aa9c 100644
20--- a/web/src/main.rs
21+++ b/web/src/main.rs
22 @@ -41,10 +41,10 @@ fn new_state(conf: Configuration) -> Arc<AppState> {
23 })
24 }
25
26- fn create_app(shared_state: Arc<AppState>) -> Router {
27+ fn create_app(shared_state: Arc<AppState>, secret: Option<[u8; 128]>) -> Router {
28 let store = MemoryStore::new();
29- let secret = rand::thread_rng().gen::<[u8; 128]>();
30- let session_layer = SessionLayer::new(store, &secret).with_secure(false);
31+ let secret = secret.unwrap_or_else(|| rand::thread_rng().gen::<[u8; 128]>());
32+ let session_layer = SessionLayer::new(store, &secret).with_secure(true);
33
34 let auth_layer = AuthLayer::new(shared_state.clone(), &secret);
35
36 @@ -157,6 +157,22 @@ async fn main() {
37
38 return;
39 }
40+ let secret = if let Ok(var) = std::env::var("SECRET") {
41+ if var.as_bytes().len() != 128 {
42+ eprintln!("Environment variable SECRET must be 128 bytes long.");
43+ return;
44+ }
45+ match var.as_bytes().try_into() {
46+ Err(err) => {
47+ eprintln!("Environment variable SECRET must be 128 bytes long. Error: {err}");
48+ return;
49+ }
50+ Ok(v) => Some(v),
51+ }
52+ } else {
53+ None
54+ };
55+ std::env::remove_var("SECRET");
56 #[cfg(test)]
57 let verbosity = log::LevelFilter::Trace;
58 #[cfg(not(test))]
59 @@ -169,7 +185,7 @@ async fn main() {
60 .init()
61 .unwrap();
62 let conf = Configuration::from_file(config_path).unwrap();
63- let app = create_app(new_state(conf));
64+ let app = create_app(new_state(conf), secret);
65
66 let hostname = std::env::var("HOSTNAME").unwrap_or_else(|_| "0.0.0.0".to_string());
67 let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string());
68 @@ -292,7 +308,10 @@ mod tests {
69 // list()
70
71 let cl = |url, state| async move {
72- let response = create_app(state).oneshot(req!(get & url)).await.unwrap();
73+ let response = create_app(state, None)
74+ .oneshot(req!(get & url))
75+ .await
76+ .unwrap();
77
78 assert_eq!(response.status(), StatusCode::OK);
79
80 @@ -308,7 +327,7 @@ mod tests {
81
82 {
83 let msg_id = "<abcdefgh@sator.example.com>";
84- let res = create_app(state.clone())
85+ let res = create_app(state.clone(), None)
86 .oneshot(req!(
87 get & format!(
88 "/list/{id}/posts/{msgid}/",
89 @@ -324,7 +343,7 @@ mod tests {
90 res.headers().get(http::header::CONTENT_TYPE),
91 Some(&http::HeaderValue::from_static("text/html; charset=utf-8"))
92 );
93- let res = create_app(state.clone())
94+ let res = create_app(state.clone(), None)
95 .oneshot(req!(
96 get & format!(
97 "/list/{id}/posts/{msgid}/raw/",
98 @@ -340,7 +359,7 @@ mod tests {
99 res.headers().get(http::header::CONTENT_TYPE),
100 Some(&http::HeaderValue::from_static("text/plain; charset=utf-8"))
101 );
102- let res = create_app(state.clone())
103+ let res = create_app(state.clone(), None)
104 .oneshot(req!(
105 get & format!(
106 "/list/{id}/posts/{msgid}/eml/",
107 @@ -367,7 +386,7 @@ mod tests {
108 // help(), ssh_signin(), root()
109
110 for path in ["/help/", "/"] {
111- let response = create_app(state.clone())
112+ let response = create_app(state.clone(), None)
113 .oneshot(req!(get path))
114 .await
115 .unwrap();
116 @@ -378,7 +397,7 @@ mod tests {
117 // ------------------------------------------------------------
118 // auth.rs...
119
120- let login_app = create_app(state.clone());
121+ let login_app = create_app(state.clone(), None);
122 let session_cookie = {
123 let response = login_app
124 .clone()