| 1 | //! `snowbound-relay`: see `crates/relay/README.md`. |
| 2 | |
| 3 | use relay::server::{self, Config}; |
| 4 | use std::{net::TcpListener, process::ExitCode, time::Duration}; |
| 5 | |
| 6 | const USAGE: &str = "\ |
| 7 | usage: snowbound-relay [OPTION VALUE]... |
| 8 | |
| 9 | Each option may also come from the environment as SNOWBOUND_RELAY_<OPTION>, upper case with |
| 10 | underscores: SNOWBOUND_RELAY_LISTEN=127.0.0.1:23592. Flags win. |
| 11 | |
| 12 | --listen ADDRESS where to listen (127.0.0.1:23592) |
| 13 | --trust-forwarded true|false count peers by X-Forwarded-For, behind a proxy (false) |
| 14 | --max-connections N (256) |
| 15 | --max-connections-per-address N per IPv4 address or IPv6 /64 (128) |
| 16 | --max-rooms N (128) |
| 17 | --max-room-peers N (64) |
| 18 | --max-message BYTES (262144) |
| 19 | --queue BYTES waiting to go to one peer before it is dropped (1048576) |
| 20 | --idle SECONDS silence before a connection is closed (600) |
| 21 | --room-bytes-per-second BYTES (4194304) |
| 22 | --joins-per-minute N per address (240) |
| 23 | --room-joins-per-minute N (120) |
| 24 | --failures-per-minute N wrong codes per address before a lockout (10) |
| 25 | --burn-after N wrong codes before a code admits no one new (5) |
| 26 | --pending SECONDS for a peer joining a code to meet its owner (20) |
| 27 | "; |
| 28 | |
| 29 | fn main() -> ExitCode { |
| 30 | let mut listen = String::from("127.0.0.1:23592"); |
| 31 | let mut config = Config::default(); |
| 32 | let from_environment = std::env::vars().filter_map(|(key, value)| { |
| 33 | let option = key.strip_prefix("SNOWBOUND_RELAY_")?; |
| 34 | Some((option.to_lowercase().replace('_', "-"), value)) |
| 35 | }); |
| 36 | let mut arguments = std::env::args().skip(1); |
| 37 | let mut from_flags = Vec::new(); |
| 38 | while let Some(flag) = arguments.next() { |
| 39 | if flag == "--help" { |
| 40 | print!("{USAGE}"); |
| 41 | return ExitCode::SUCCESS; |
| 42 | } |
| 43 | let Some(option) = flag.strip_prefix("--") else { |
| 44 | eprint!("{USAGE}"); |
| 45 | return ExitCode::from(2); |
| 46 | }; |
| 47 | let (option, value) = match option.split_once('=') { |
| 48 | Some((option, value)) => (option.to_owned(), value.to_owned()), |
| 49 | None => match arguments.next() { |
| 50 | Some(value) => (option.to_owned(), value), |
| 51 | None => { |
| 52 | eprintln!("--{option} needs a value\n\n{USAGE}"); |
| 53 | return ExitCode::from(2); |
| 54 | } |
| 55 | }, |
| 56 | }; |
| 57 | from_flags.push((option, value)); |
| 58 | } |
| 59 | for (option, value) in from_environment.chain(from_flags) { |
| 60 | let set = match option.as_str() { |
| 61 | "listen" => { |
| 62 | listen = value.clone(); |
| 63 | Ok(()) |
| 64 | } |
| 65 | _ => set(&mut config, &option, &value), |
| 66 | }; |
| 67 | if let Err(error) = set { |
| 68 | eprintln!("--{option} {value}: {error}\n\n{USAGE}"); |
| 69 | return ExitCode::from(2); |
| 70 | } |
| 71 | } |
| 72 | let listener = match TcpListener::bind(&listen) { |
| 73 | Ok(listener) => listener, |
| 74 | Err(error) => { |
| 75 | eprintln!("Cannot listen on {listen}: {error}"); |
| 76 | return ExitCode::FAILURE; |
| 77 | } |
| 78 | }; |
| 79 | match listener.local_addr() { |
| 80 | Ok(address) => println!( |
| 81 | "snowbound-relay {} listening on {address}", |
| 82 | env!("CARGO_PKG_VERSION") |
| 83 | ), |
| 84 | Err(error) => eprintln!("{error}"), |
| 85 | } |
| 86 | match server::serve(listener, config) { |
| 87 | Ok(()) => ExitCode::SUCCESS, |
| 88 | Err(error) => { |
| 89 | eprintln!("{error}"); |
| 90 | ExitCode::FAILURE |
| 91 | } |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | fn set(config: &mut Config, option: &str, value: &str) -> Result<(), String> { |
| 96 | let number = || value.parse::<u64>().map_err(|error| error.to_string()); |
| 97 | let count = || number().map(|number| number as usize); |
| 98 | let small = |
| 99 | || number().and_then(|number| u32::try_from(number).map_err(|error| error.to_string())); |
| 100 | match option { |
| 101 | "trust-forwarded" => { |
| 102 | config.trust_forwarded = value.parse().map_err(|_| "true or false".to_owned())? |
| 103 | } |
| 104 | "max-connections" => config.max_connections = count()?, |
| 105 | "max-connections-per-address" => config.max_connections_per_address = count()?, |
| 106 | "max-rooms" => config.max_rooms = count()?, |
| 107 | "max-room-peers" => config.max_room_peers = count()?, |
| 108 | "max-message" => config.max_message = count()?, |
| 109 | "queue" => config.queue = count()?, |
| 110 | "idle" => config.idle = Duration::from_secs(number()?), |
| 111 | "room-bytes-per-second" => config.room_bytes_per_second = number()?.max(1), |
| 112 | "joins-per-minute" => config.joins_per_minute = small()?.max(1), |
| 113 | "room-joins-per-minute" => config.room_joins_per_minute = small()?.max(1), |
| 114 | "failures-per-minute" => config.failures_per_minute = small()?.max(1), |
| 115 | "burn-after" => config.burn_after = small()?.max(1), |
| 116 | "pending" => config.pending = Duration::from_secs(number()?), |
| 117 | _ => return Err("no such option".into()), |
| 118 | } |
| 119 | Ok(()) |
| 120 | } |