1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
|
//! Gateway configuration from the environment.
//!
//! The upstream key is mandatory. There is no trust-on-first-use and no unsafe
//! mode: the browser's credentials ride on this connection.
use std::env;
use std::time::Duration;
pub const DEFAULT_BIND: &str = "127.0.0.1:8890";
pub const DEFAULT_UPSTREAM: &str = "127.0.0.1:11525";
pub const DEFAULT_GRACE_SECONDS: u64 = 60;
pub const DEFAULT_ORIGIN: &str = "https://zagan.2137697.xyz";
#[derive(Debug, Clone)]
pub struct Config {
/// Loopback address nginx proxies `/ws` to.
pub bind: String,
/// Exact browser origin allowed on the WebSocket handshake.
pub origin: String,
/// Game server address, `host:port`.
pub upstream: String,
/// Lowercase hex SHA-256 of the upstream certificate's SPKI.
pub fingerprint: String,
/// How long a session survives without a browser socket.
pub grace: Duration,
/// `Secure` on the session cookie. Off only for local http testing.
pub cookie_secure: bool,
}
impl Config {
pub fn from_env() -> Result<Self, String> {
let bind = value("ZAGAN_WEB_BIND", DEFAULT_BIND);
let origin = value("ZAGAN_WEB_ORIGIN", DEFAULT_ORIGIN);
if !origin.starts_with("http://") && !origin.starts_with("https://") {
return Err("ZAGAN_WEB_ORIGIN must start with http:// or https://".to_string());
}
let origin = origin.trim_end_matches('/').to_string();
let upstream = value("ZAGAN_UPSTREAM", DEFAULT_UPSTREAM);
let fingerprint = required("ZAGAN_UPSTREAM_FINGERPRINT")?;
let fingerprint = fingerprint.trim().to_ascii_lowercase();
if fingerprint.len() != 64 || !fingerprint.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err(
"ZAGAN_UPSTREAM_FINGERPRINT must be the 64-character SHA-256 of the upstream SPKI"
.to_string(),
);
}
let grace_seconds = match env::var("ZAGAN_GRACE_SECONDS") {
Ok(raw) => raw
.trim()
.parse::<u64>()
.map_err(|_| "ZAGAN_GRACE_SECONDS must be a whole number of seconds".to_string())?,
Err(_) => DEFAULT_GRACE_SECONDS,
};
let cookie_secure = match env::var("ZAGAN_WEB_COOKIE_SECURE") {
Ok(raw) => matches!(
raw.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes"
),
Err(_) => origin.starts_with("https://"),
};
Ok(Self {
bind,
origin,
upstream,
fingerprint,
grace: Duration::from_secs(grace_seconds),
cookie_secure,
})
}
}
fn value(name: &str, default: &str) -> String {
env::var(name)
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| default.to_string())
}
fn required(name: &str) -> Result<String, String> {
env::var(name)
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.ok_or_else(|| {
format!(
"{name} is required: the gateway pins the upstream key. \
Use the SPKI fingerprint the desktop client shows."
)
})
}
|