1use std::net::SocketAddr;
22use std::time::Duration;
23
24use bolt::messages::route::{RoutingConfig, DEFAULT_STORE_NAME};
25
26pub const BOLT_PORT_ENV: &str = "MUSHROOMDB_BOLT_PORT";
28pub const BOLT_TLS_ENV: &str = "MUSHROOMDB_BOLT_TLS";
30pub const BOLT_ADVERTISED_HOST_ENV: &str = "MUSHROOMDB_BOLT_ADVERTISED_HOST";
32pub const BOLT_ADVERTISED_PORT_ENV: &str = "MUSHROOMDB_BOLT_ADVERTISED_PORT";
34pub const BOLT_MAX_CONNECTIONS_ENV: &str = "MUSHROOMDB_BOLT_MAX_CONNECTIONS";
36
37pub const DEFAULT_BOLT_TX_TIMEOUT_SECS: u64 = 30;
39pub const DEFAULT_BOOKMARK_TIMEOUT_SECS: u64 = 5;
41
42#[derive(Debug, Clone, Default, PartialEq, Eq)]
44pub struct BoltFlags {
45 pub port: Option<u16>,
47 pub tls: Option<bool>,
49 pub advertised_host: Option<String>,
51 pub advertised_port: Option<u16>,
53 pub database: Option<String>,
55 pub max_connections: Option<usize>,
57 pub tx_timeout: Option<Duration>,
59 pub bookmark_timeout: Option<Duration>,
61}
62
63impl BoltFlags {
64 fn any_besides_port(&self) -> bool {
66 self.tls.is_some()
67 || self.advertised_host.is_some()
68 || self.advertised_port.is_some()
69 || self.database.is_some()
70 || self.max_connections.is_some()
71 || self.tx_timeout.is_some()
72 || self.bookmark_timeout.is_some()
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct BoltSettings {
79 pub bind: SocketAddr,
81 pub tls: bool,
82 pub advertised_host: Option<String>,
83 pub advertised_port: Option<u16>,
84 pub database: String,
86 pub max_connections: usize,
87 pub tx_timeout: Duration,
88 pub bookmark_timeout: Duration,
89}
90
91impl BoltSettings {
92 pub fn routing(&self, bound: SocketAddr) -> Result<RoutingConfig, String> {
95 let routing = RoutingConfig::new(bound, self.advertised_host.as_deref(), &self.database)
96 .map_err(|e| advertise_error(bound, &e.to_string()))?;
97 Ok(match self.advertised_port {
98 Some(port) => routing.with_advertised_port(port),
99 None => routing,
100 })
101 }
102
103 pub fn scheme(&self) -> &'static str {
105 if self.tls {
106 "bolt+s"
107 } else {
108 "bolt"
109 }
110 }
111}
112
113fn advertise_error(bind: SocketAddr, why: &str) -> String {
114 format!(
115 "--bolt-port on {bind}: {why}; give --bolt-advertised-host (or \
116 {BOLT_ADVERTISED_HOST_ENV}), the host drivers reach this server by, \
117 which ROUTE hands them"
118 )
119}
120
121fn env_value(env: &dyn Fn(&str) -> Option<String>, name: &str) -> Option<String> {
123 env(name)
124 .map(|v| v.trim().to_string())
125 .filter(|v| !v.is_empty())
126}
127
128fn parse_env<T: std::str::FromStr>(
129 env: &dyn Fn(&str) -> Option<String>,
130 name: &str,
131) -> Result<Option<T>, String> {
132 match env_value(env, name) {
133 None => Ok(None),
134 Some(text) => text
135 .parse()
136 .map(Some)
137 .map_err(|_| format!("{name}={text:?} is not valid")),
138 }
139}
140
141fn parse_switch(name: &str, text: &str) -> Result<bool, String> {
143 match text.to_ascii_lowercase().as_str() {
144 "on" | "true" | "1" | "yes" => Ok(true),
145 "off" | "false" | "0" | "no" => Ok(false),
146 _ => Err(format!(
147 "{name}={text:?} is not recognised; use \"on\" or \"off\""
148 )),
149 }
150}
151
152pub fn resolve_bolt_settings(
156 flags: &BoltFlags,
157 env: &dyn Fn(&str) -> Option<String>,
158 addr: SocketAddr,
159 tls_configured: bool,
160) -> Result<Option<BoltSettings>, String> {
161 let port = match flags.port {
162 Some(port) => Some(port),
163 None => parse_env::<u16>(env, BOLT_PORT_ENV)?,
164 };
165 let Some(port) = port else {
166 if flags.any_besides_port() {
167 return Err(format!(
168 "the --bolt-* flags need --bolt-port (or {BOLT_PORT_ENV}); without a port no \
169 Bolt listener is started"
170 ));
171 }
172 return Ok(None);
173 };
174 let tls = match flags.tls {
175 Some(tls) => Some(tls),
176 None => env_value(env, BOLT_TLS_ENV)
177 .map(|text| parse_switch(BOLT_TLS_ENV, &text))
178 .transpose()?,
179 };
180 let tls = match tls {
181 Some(true) if !tls_configured => {
182 return Err(
183 "--bolt-tls needs --tls-cert and --tls-key: Bolt serves the HTTP certificate"
184 .into(),
185 )
186 }
187 Some(explicit) => explicit,
188 None => tls_configured,
190 };
191 let advertised_host = match &flags.advertised_host {
192 Some(host) => Some(host.clone()),
193 None => env_value(env, BOLT_ADVERTISED_HOST_ENV),
194 };
195 let advertised_port = match flags.advertised_port {
196 Some(port) => Some(port),
197 None => parse_env::<u16>(env, BOLT_ADVERTISED_PORT_ENV)?,
198 };
199 if advertised_port == Some(0) {
200 return Err("--bolt-advertised-port must be a port from 1 to 65535".into());
201 }
202 let max_connections = match flags.max_connections {
203 Some(n) => n,
204 None => parse_env::<usize>(env, BOLT_MAX_CONNECTIONS_ENV)?
205 .unwrap_or(bolt::server::DEFAULT_MAX_CONNECTIONS),
206 };
207 if max_connections == 0 {
208 return Err("--bolt-max-connections must be at least 1".into());
209 }
210 let settings = BoltSettings {
211 bind: SocketAddr::new(addr.ip(), port),
212 tls,
213 advertised_host,
214 advertised_port,
215 database: flags
216 .database
217 .clone()
218 .unwrap_or_else(|| DEFAULT_STORE_NAME.to_string()),
219 max_connections,
220 tx_timeout: flags
221 .tx_timeout
222 .unwrap_or(Duration::from_secs(DEFAULT_BOLT_TX_TIMEOUT_SECS)),
223 bookmark_timeout: flags
224 .bookmark_timeout
225 .unwrap_or(Duration::from_secs(DEFAULT_BOOKMARK_TIMEOUT_SECS)),
226 };
227 settings.routing(settings.bind)?;
229 Ok(Some(settings))
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use std::collections::HashMap;
236
237 fn env(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
238 let map: HashMap<String, String> = pairs
239 .iter()
240 .map(|(k, v)| (k.to_string(), v.to_string()))
241 .collect();
242 move |name| map.get(name).cloned()
243 }
244
245 fn loopback() -> SocketAddr {
246 "127.0.0.1:8080".parse().unwrap()
247 }
248
249 fn with_port(port: u16) -> BoltFlags {
250 BoltFlags {
251 port: Some(port),
252 ..BoltFlags::default()
253 }
254 }
255
256 #[test]
257 fn no_port_means_no_listener() {
258 let none = resolve_bolt_settings(&BoltFlags::default(), &env(&[]), loopback(), false);
259 assert_eq!(none, Ok(None));
260 let empty = resolve_bolt_settings(
262 &BoltFlags::default(),
263 &env(&[(BOLT_PORT_ENV, "")]),
264 loopback(),
265 false,
266 );
267 assert_eq!(empty, Ok(None));
268 }
269
270 #[test]
271 fn another_bolt_flag_without_a_port_is_refused() {
272 let flags = BoltFlags {
273 tls: Some(false),
274 ..BoltFlags::default()
275 };
276 let err = resolve_bolt_settings(&flags, &env(&[]), loopback(), false).unwrap_err();
277 assert!(err.contains("--bolt-port"), "{err}");
278 }
279
280 #[test]
281 fn the_port_comes_from_the_flag_or_the_environment_and_binds_the_http_ip() {
282 let from_flag = resolve_bolt_settings(&with_port(7687), &env(&[]), loopback(), false)
283 .unwrap()
284 .unwrap();
285 assert_eq!(
286 from_flag.bind,
287 "127.0.0.1:7687".parse::<SocketAddr>().unwrap()
288 );
289 assert!(!from_flag.tls);
290 assert_eq!(from_flag.database, "mushroomdb");
291 assert_eq!(
292 from_flag.max_connections,
293 bolt::server::DEFAULT_MAX_CONNECTIONS
294 );
295 assert_eq!(from_flag.tx_timeout, Duration::from_secs(30));
296 assert_eq!(from_flag.bookmark_timeout, Duration::from_secs(5));
297 let from_env = resolve_bolt_settings(
298 &BoltFlags::default(),
299 &env(&[(BOLT_PORT_ENV, "7699")]),
300 loopback(),
301 false,
302 )
303 .unwrap()
304 .unwrap();
305 assert_eq!(from_env.bind.port(), 7699);
306 let both = resolve_bolt_settings(
308 &with_port(7687),
309 &env(&[(BOLT_PORT_ENV, "7699")]),
310 loopback(),
311 false,
312 )
313 .unwrap()
314 .unwrap();
315 assert_eq!(both.bind.port(), 7687);
316 let bad = resolve_bolt_settings(
317 &BoltFlags::default(),
318 &env(&[(BOLT_PORT_ENV, "seventy")]),
319 loopback(),
320 false,
321 );
322 assert!(bad.unwrap_err().contains(BOLT_PORT_ENV));
323 }
324
325 #[test]
326 fn tls_follows_http_unless_plaintext_is_asked_for() {
327 let resolve = |flags: &BoltFlags, vars: &[(&str, &str)], tls_configured: bool| {
328 resolve_bolt_settings(flags, &env(vars), loopback(), tls_configured)
329 };
330 assert!(resolve(&with_port(7687), &[], true).unwrap().unwrap().tls);
332 let plaintext = BoltFlags {
334 tls: Some(false),
335 ..with_port(7687)
336 };
337 assert!(!resolve(&plaintext, &[], true).unwrap().unwrap().tls);
338 assert!(
339 !resolve(&with_port(7687), &[(BOLT_TLS_ENV, "off")], true)
340 .unwrap()
341 .unwrap()
342 .tls
343 );
344 assert!(!resolve(&with_port(7687), &[], false).unwrap().unwrap().tls);
346 let tls = BoltFlags {
347 tls: Some(true),
348 ..with_port(7687)
349 };
350 assert!(resolve(&tls, &[], false)
351 .unwrap_err()
352 .contains("--tls-cert"));
353 assert!(resolve(&with_port(7687), &[(BOLT_TLS_ENV, "on")], false).is_err());
354 assert!(resolve(&with_port(7687), &[(BOLT_TLS_ENV, "maybe")], true)
355 .unwrap_err()
356 .contains(BOLT_TLS_ENV));
357 assert!(
359 resolve(&tls, &[(BOLT_TLS_ENV, "off")], true)
360 .unwrap()
361 .unwrap()
362 .tls
363 );
364 }
365
366 #[test]
367 fn a_wildcard_bind_needs_an_advertised_host() {
368 let wildcard: SocketAddr = "0.0.0.0:8080".parse().unwrap();
369 let err = resolve_bolt_settings(&with_port(7687), &env(&[]), wildcard, false).unwrap_err();
370 assert!(err.contains("--bolt-advertised-host"), "{err}");
371 let v6: SocketAddr = "[::]:8080".parse().unwrap();
372 assert!(resolve_bolt_settings(&with_port(7687), &env(&[]), v6, false).is_err());
373 let flags = BoltFlags {
375 advertised_host: Some("db.example.test".into()),
376 ..with_port(7687)
377 };
378 let settings = resolve_bolt_settings(&flags, &env(&[]), wildcard, false)
379 .unwrap()
380 .unwrap();
381 assert_eq!(
382 settings
383 .routing(settings.bind)
384 .unwrap()
385 .advertised_address(),
386 "db.example.test:7687"
387 );
388 let from_env = resolve_bolt_settings(
389 &with_port(7687),
390 &env(&[
391 (BOLT_ADVERTISED_HOST_ENV, "db.example.test"),
392 (BOLT_ADVERTISED_PORT_ENV, "17687"),
393 ]),
394 wildcard,
395 false,
396 )
397 .unwrap()
398 .unwrap();
399 assert_eq!(
401 from_env
402 .routing(from_env.bind)
403 .unwrap()
404 .advertised_address(),
405 "db.example.test:17687"
406 );
407 let wild_host = BoltFlags {
409 advertised_host: Some("0.0.0.0".into()),
410 ..with_port(7687)
411 };
412 assert!(resolve_bolt_settings(&wild_host, &env(&[]), wildcard, false).is_err());
413 }
414
415 #[test]
416 fn a_loopback_bind_advertises_itself_and_port_zero_takes_the_bound_port() {
417 let settings = resolve_bolt_settings(&with_port(0), &env(&[]), loopback(), false)
418 .unwrap()
419 .unwrap();
420 let bound: SocketAddr = "127.0.0.1:54321".parse().unwrap();
421 assert_eq!(
422 settings.routing(bound).unwrap().advertised_address(),
423 "127.0.0.1:54321"
424 );
425 }
426
427 #[test]
428 fn limits_and_names_are_checked() {
429 let zero = BoltFlags {
430 max_connections: Some(0),
431 ..with_port(7687)
432 };
433 assert!(resolve_bolt_settings(&zero, &env(&[]), loopback(), false).is_err());
434 let from_env = resolve_bolt_settings(
435 &with_port(7687),
436 &env(&[(BOLT_MAX_CONNECTIONS_ENV, "3")]),
437 loopback(),
438 false,
439 )
440 .unwrap()
441 .unwrap();
442 assert_eq!(from_env.max_connections, 3);
443 let blank_name = BoltFlags {
444 database: Some(" ".into()),
445 ..with_port(7687)
446 };
447 assert!(resolve_bolt_settings(&blank_name, &env(&[]), loopback(), false).is_err());
448 let zero_port = BoltFlags {
449 advertised_port: Some(0),
450 ..with_port(7687)
451 };
452 assert!(resolve_bolt_settings(&zero_port, &env(&[]), loopback(), false).is_err());
453 }
454
455 #[test]
456 fn the_scheme_names_tls() {
457 let mut settings = resolve_bolt_settings(&with_port(7687), &env(&[]), loopback(), false)
458 .unwrap()
459 .unwrap();
460 assert_eq!(settings.scheme(), "bolt");
461 settings.tls = true;
462 assert_eq!(settings.scheme(), "bolt+s");
463 }
464}