1//! `snowbound-site`: see `crates/relay/README.md`.
2
3use relay::site::{self, Site};
4use std::{net::TcpListener, path::PathBuf, process::ExitCode};
5
6const USAGE: &str = "\
7usage: snowbound-site [OPTION VALUE]...
8
9Each option may also come from the environment as SNOWBOUND_SITE_<OPTION>, upper case with
10underscores: SNOWBOUND_SITE_ROOT=/srv/snowbound/web. Flags win.
11
12 --listen ADDRESS where to listen (127.0.0.1:23593)
13 --root FOLDER the web build's folder (web)
14 --web URL where Open in Web goes, the code appended, as
15 https://snowbound.paperclover.net/?join= (none: no Open in Web)
16 --crashes FOLDER where crash reports are kept (crashes, beside the root)
17 --trust-forwarded true|false
18 count senders by X-Forwarded-For, behind a proxy (false)
19";
20
21fn main() -> ExitCode {
22 let mut listen = String::from("127.0.0.1:23593");
23 let mut site = Site {
24 root: PathBuf::from("web"),
25 web: None,
26 crashes: PathBuf::new(),
27 trust_forwarded: false,
28 };
29 let mut crashes = None;
30 let from_environment = std::env::vars().filter_map(|(key, value)| {
31 let option = key.strip_prefix("SNOWBOUND_SITE_")?;
32 Some((option.to_lowercase().replace('_', "-"), value))
33 });
34 let mut arguments = std::env::args().skip(1);
35 let mut from_flags = Vec::new();
36 while let Some(flag) = arguments.next() {
37 if flag == "--help" {
38 print!("{USAGE}");
39 return ExitCode::SUCCESS;
40 }
41 let (Some(option), Some(value)) = (flag.strip_prefix("--"), arguments.next()) else {
42 eprint!("{USAGE}");
43 return ExitCode::from(2);
44 };
45 from_flags.push((option.to_owned(), value));
46 }
47 for (option, value) in from_environment.chain(from_flags) {
48 match option.as_str() {
49 "listen" => listen = value,
50 "root" => site.root = PathBuf::from(value),
51 "web" => site.web = Some(value).filter(|web| !web.is_empty()),
52 "crashes" => crashes = Some(PathBuf::from(value)),
53 "trust-forwarded" => match value.parse() {
54 Ok(trust) => site.trust_forwarded = trust,
55 Err(_) => {
56 eprintln!("--trust-forwarded {value}: true or false\n\n{USAGE}");
57 return ExitCode::from(2);
58 }
59 },
60 _ => {
61 eprintln!("--{option}: no such option\n\n{USAGE}");
62 return ExitCode::from(2);
63 }
64 }
65 }
66 site.crashes = crashes.unwrap_or_else(|| site.root.with_file_name("crashes"));
67 let listener = match TcpListener::bind(&listen) {
68 Ok(listener) => listener,
69 Err(error) => {
70 eprintln!("Cannot listen on {listen}: {error}");
71 return ExitCode::FAILURE;
72 }
73 };
74 if let Ok(address) = listener.local_addr() {
75 println!(
76 "snowbound-site {} listening on {address}, serving {}",
77 env!("CARGO_PKG_VERSION"),
78 site.root.display()
79 );
80 }
81 match site::serve(listener, site) {
82 Ok(()) => ExitCode::SUCCESS,
83 Err(error) => {
84 eprintln!("{error}");
85 ExitCode::FAILURE
86 }
87 }
88}