manabrew_server/
config.rs1const DEFAULT_CAPTURE_MAX_GB: u64 = 20;
2const DEFAULT_ART_PORT: u16 = 9528;
5
6const BYTES_PER_GB: u64 = 1024 * 1024 * 1024;
7
8pub use crate::protocol::IceServer as TransportIceServer;
9
10pub fn parse_ice_servers(raw: &str) -> Vec<TransportIceServer> {
12 let raw = raw.trim();
13 if raw.is_empty() {
14 return Vec::new();
15 }
16 if raw.starts_with('[') {
17 return match serde_json::from_str::<Vec<TransportIceServer>>(raw) {
18 Ok(servers) => servers.into_iter().filter(|s| !s.urls.is_empty()).collect(),
19 Err(error) => {
20 tracing::error!(%error, "MANABREW_ICE_SERVERS is not valid JSON; ignoring it");
21 Vec::new()
22 }
23 };
24 }
25 let urls: Vec<String> = raw
26 .split([',', ' ', '\t', '\n'])
27 .map(str::trim)
28 .filter(|url| !url.is_empty())
29 .map(str::to_string)
30 .collect();
31 if urls.is_empty() {
32 Vec::new()
33 } else {
34 vec![TransportIceServer {
35 urls,
36 username: None,
37 credential: None,
38 }]
39 }
40}
41
42pub struct ServerConfig {
43 pub host: String,
44 pub port: u16,
45 pub health_port: u16,
46 pub max_rooms: usize,
47 pub server_key: String,
48 pub official_key: Option<String>,
49 pub events_dir: Option<String>,
50 pub capture_dir: Option<String>,
51 pub capture_max_gb: u64,
52 pub deck_hub_enabled: bool,
53 pub hub_url: Option<String>,
56 pub hub_token: Option<String>,
57 pub hub_jwks_url: Option<String>,
58 pub direct_transport: bool,
60 pub ice_servers: Vec<TransportIceServer>,
62 pub art_dir: Option<String>,
66 pub art_port: u16,
67 pub art_base_url: Option<String>,
72 pub lan_advertise: bool,
75}
76
77impl ServerConfig {
78 pub fn from_env() -> Self {
79 let lan_advertise = std::env::var("MANABREW_LAN_ADVERTISE")
80 .map(|value| {
81 matches!(
82 value.to_ascii_lowercase().as_str(),
83 "1" | "true" | "yes" | "on"
84 )
85 })
86 .unwrap_or(false);
87 ServerConfig {
88 host: std::env::var("FORGE_HOST").unwrap_or_else(|_| "0.0.0.0".into()),
89 port: std::env::var("FORGE_PORT")
90 .ok()
91 .and_then(|p| p.parse().ok())
92 .unwrap_or(9443),
93 health_port: std::env::var("FORGE_HEALTH_PORT")
94 .ok()
95 .and_then(|p| p.parse().ok())
96 .unwrap_or(9444),
97 max_rooms: std::env::var("FORGE_MAX_ROOMS")
98 .ok()
99 .and_then(|r| r.parse().ok())
100 .unwrap_or(100),
101 server_key: std::env::var("MANABREW_SERVER_KEY")
102 .unwrap_or_else(|_| default_server_key(lan_advertise).into()),
103 official_key: std::env::var("SECRET_MANABREW_KEY")
104 .ok()
105 .filter(|key| !key.is_empty()),
106 events_dir: std::env::var("MANABREW_EVENTS_DIR")
107 .ok()
108 .filter(|dir| !dir.is_empty()),
109 capture_dir: std::env::var("MANABREW_GAME_CAPTURE_DIR")
110 .ok()
111 .filter(|dir| !dir.is_empty()),
112 capture_max_gb: std::env::var("MANABREW_GAME_CAPTURE_MAX_GB")
113 .ok()
114 .and_then(|gb| gb.parse().ok())
115 .unwrap_or(DEFAULT_CAPTURE_MAX_GB),
116 deck_hub_enabled: std::env::var("DECK_HUB").ok().is_some_and(|value| {
117 matches!(
118 value.to_ascii_lowercase().as_str(),
119 "1" | "true" | "yes" | "on"
120 )
121 }),
122 hub_url: std::env::var("MANABREW_HUB_DECK_PLAYS_URL")
123 .ok()
124 .filter(|url| !url.is_empty()),
125 hub_token: std::env::var("MANABREW_HUB_DECK_PLAYS_TOKEN")
126 .ok()
127 .filter(|token| !token.is_empty()),
128 hub_jwks_url: std::env::var("MANABREW_HUB_JWKS_URL")
129 .ok()
130 .filter(|url| !url.is_empty()),
131 direct_transport: std::env::var("MANABREW_DIRECT_TRANSPORT")
132 .is_ok_and(|v| v == "1" || v.eq_ignore_ascii_case("true")),
133 ice_servers: std::env::var("MANABREW_ICE_SERVERS")
134 .ok()
135 .map(|raw| parse_ice_servers(&raw))
136 .unwrap_or_default(),
137 art_dir: std::env::var("MANABREW_ART_DIR")
138 .ok()
139 .filter(|dir| !dir.is_empty()),
140 art_port: std::env::var("MANABREW_ART_PORT")
141 .ok()
142 .and_then(|port| port.parse().ok())
143 .unwrap_or(DEFAULT_ART_PORT),
144 art_base_url: std::env::var("MANABREW_ART_BASE_URL")
145 .ok()
146 .filter(|url| !url.is_empty()),
147 lan_advertise,
148 }
149 }
150
151 pub fn capture_max_bytes(&self) -> u64 {
152 self.capture_max_gb.saturating_mul(BYTES_PER_GB)
153 }
154}
155
156fn default_server_key(lan_advertise: bool) -> &'static str {
160 if lan_advertise {
161 manabrew_lan_discovery::LAN_RELAY_KEY
162 } else {
163 "forge"
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn a_plain_url_list_becomes_one_server() {
173 let parsed = parse_ice_servers("stun:a.example.org:19302, stun:b.example.org");
174 assert_eq!(parsed.len(), 1);
175 assert_eq!(
176 parsed[0].urls,
177 vec!["stun:a.example.org:19302", "stun:b.example.org"]
178 );
179 assert!(parsed[0].username.is_none());
180 }
181
182 #[test]
183 fn json_carries_turn_credentials() {
184 let parsed = parse_ice_servers(
185 r#"[{"urls":["turn:t.example.org"],"username":"u","credential":"p"}]"#,
186 );
187 assert_eq!(parsed.len(), 1);
188 assert_eq!(parsed[0].username.as_deref(), Some("u"));
189 assert_eq!(parsed[0].credential.as_deref(), Some("p"));
190 }
191
192 #[test]
193 fn anything_unparseable_yields_no_servers_rather_than_a_panic() {
194 assert!(parse_ice_servers("").is_empty());
195 assert!(parse_ice_servers(" ").is_empty());
196 assert!(parse_ice_servers("[not json").is_empty());
197 assert!(parse_ice_servers(r#"[{"username":"u"}]"#).is_empty());
198 assert!(parse_ice_servers(r#"[{"urls":[]}]"#).is_empty());
199 }
200
201 #[test]
202 fn a_relay_that_advertises_answers_to_the_key_desktops_use() {
203 assert_eq!(
204 default_server_key(true),
205 manabrew_lan_discovery::LAN_RELAY_KEY
206 );
207 assert_eq!(default_server_key(false), "forge");
208 }
209}