// Copyright 2020-2021 Ian Jackson and contributors to Otter // SPDX-License-Identifier: AGPL-3.0-or-later // There is NO WARRANTY. use crate::prelude::*; use pwd::Passwd; pub const EXIT_SPACE : i32 = 2; pub const EXIT_NOTFOUND : i32 = 4; pub const EXIT_SITUATION : i32 = 8; pub const EXIT_USAGE : i32 = 12; pub const EXIT_DISASTER : i32 = 16; pub const DEFAULT_CONFIG_DIR : &str = "/etc/otter"; pub const DEFAULT_CONFIG_LEAFNAME : &str = "server.toml"; pub const DEFAULT_SENDMAIL_PROGRAM : &str = "/usr/sbin/sendmail"; pub const DEFAULT_SSH_PROXY_CMD : &str = "otter-ssh-proxy"; pub const SSH_PROXY_SUBCMD : &str = "mgmtchannel-proxy"; pub const DAEMON_STARTUP_REPORT: &str = "otter-daemon started"; pub const LOG_ENV_VAR: &str = "OTTER_LOG"; #[derive(Deserialize,Debug,Clone)] pub struct ServerConfigSpec { pub change_directory: Option, pub base_dir: Option, pub save_dir: Option, pub libexec_dir: Option, pub usvg_bin: Option, pub command_socket: Option, pub debug: Option, pub http_port: Option, #[serde(default)] pub listen: Vec, pub public_url: String, pub sse_wildcard_url: Option, pub template_dir: Option, pub nwtemplate_dir: Option, pub wasm_dir: Option, pub log: Option, pub bundled_sources: Option, pub shapelibs: Option>, pub specs_dir: Option, pub sendmail: Option, /// for auth keys, split on spaces pub ssh_proxy_command: Option, pub ssh_proxy_user: Option, pub ssh_restrictions: Option, pub authorized_keys: Option, pub authorized_keys_include: Option, pub debug_js_inject_file: Option, #[serde(default)] pub fake_rng: FakeRngSpec, #[serde(default)] pub fake_time: FakeTimeConfig, /// Disable this for local testing only. See LICENCE. pub check_bundled_sources: Option, } #[derive(Deserialize,Debug,Clone)] #[serde(untagged)] pub enum ShapelibConfig1 { PathGlob(String), Explicit(ShapelibExplicit1), } #[derive(Deserialize,Debug,Clone)] pub struct ShapelibExplicit1 { pub name: String, pub catalogue: String, pub dirname: String, } #[derive(Debug,Clone)] pub struct WholeServerConfig { server: Arc, log: LogSpecification, } #[derive(Debug)] pub struct ServerConfig { save_dir: String, pub command_socket: String, pub debug: bool, pub listen: Vec, pub public_url: String, pub sse_wildcard_url: Option<(String, String)>, pub template_dir: String, pub nwtemplate_dir: String, pub wasm_dir: String, pub libexec_dir: String, pub usvg_bin: String, pub bundled_sources: String, pub shapelibs: Vec, pub specs_dir: String, pub sendmail: String, pub ssh_proxy_bin: String, pub ssh_proxy_uid: Uid, pub ssh_restrictions: String, pub authorized_keys: String, pub authorized_keys_include: String, pub debug_js_inject: Arc, pub check_bundled_sources: bool, pub game_rng: RngWrap, pub global_clock: GlobalClock, pub prctx: PathResolveContext, } #[derive(Debug,Copy,Clone)] pub enum PathResolveMethod { Chdir, Prefix, } impl Default for PathResolveMethod { fn default() -> Self { Self::Prefix } } #[derive(Debug,Clone)] pub enum PathResolveContext { RelativeTo(String), Noop, } impl Default for PathResolveContext { fn default() -> Self { Self::Noop } } static PROGRAM_NAME: RwLock = parking_lot::const_rwlock(String::new()); impl PathResolveMethod { #[throws(io::Error)] fn chdir(self, cd: &str) -> PathResolveContext { use PathResolveMethod::*; use PathResolveContext::*; match self { Chdir => { env::set_current_dir(cd)?; Noop }, Prefix if cd == "." => Noop, Prefix => RelativeTo(cd.to_string()), } } } impl PathResolveContext { pub fn resolve(&self, input: &str) -> String { use PathResolveContext::*; match (self, input.as_bytes()) { (Noop , _ ) | (RelativeTo(_ ), &[b'/',..]) => input.to_owned(), (RelativeTo(cd), _ ) => format!("{}/{}", &cd, input), } } } impl ServerConfigSpec { //#[throws(AE)] pub fn resolve(self, prmeth: PathResolveMethod) -> Result { let ServerConfigSpec { change_directory, base_dir, save_dir, command_socket, debug, http_port, listen, public_url, sse_wildcard_url, template_dir, specs_dir, nwtemplate_dir, wasm_dir, libexec_dir, usvg_bin, log, bundled_sources, shapelibs, sendmail, debug_js_inject_file, check_bundled_sources, fake_rng, fake_time, ssh_proxy_command, ssh_proxy_user, ssh_restrictions, authorized_keys, authorized_keys_include, } = self; let game_rng = fake_rng.make_game_rng(); let global_clock = fake_time.make_global_clock(); let home = || env::var("HOME").context("HOME"); let prctx = if let Some(ref cd) = change_directory { prmeth.chdir(cd) .with_context(|| cd.clone()) .context("config change_directory")? } else { PathResolveContext::Noop }; let defpath = |specd: Option, leaf: &str| -> String { prctx.resolve(&specd.unwrap_or_else(|| match &base_dir { Some(base) => format!("{}/{}", &base, &leaf), None => leaf.to_owned(), })) }; let save_dir = defpath(save_dir, "save" ); let specs_dir = defpath(specs_dir, "specs" ); let command_socket = defpath(command_socket, "var/command.socket"); let template_dir = defpath(template_dir, "assets" ); let wasm_dir = defpath(wasm_dir, "assets" ); let nwtemplate_dir = defpath(nwtemplate_dir, "nwtemplates" ); let libexec_dir = defpath(libexec_dir, "libexec" ); let bundled_sources = defpath(bundled_sources, "bundled-sources" ); const DEFAULT_LIBRARY_GLOB: &str = "library/*.toml"; let in_libexec = |specd: Option, leaf: &str| -> String { specd.unwrap_or_else(|| format!("{}/{}", &libexec_dir, leaf)) }; let usvg_bin = in_libexec(usvg_bin, "usvg" ); let ssh_proxy_bin = in_libexec(ssh_proxy_command, DEFAULT_SSH_PROXY_CMD ); let ssh_restrictions = ssh_restrictions.unwrap_or_else( || concat!("restrict,no-agent-forwarding,no-port-forwarding,", "no-pty,no-user-rc,no-X11-forwarding").into()); let authorized_keys = if let Some(ak) = authorized_keys { ak } else { let home = home().context("for authorized_keys")?; // we deliberately don't create the ~/.ssh dir format!("{}/.ssh/authorized_keys", home) }; let authorized_keys_include = authorized_keys_include.unwrap_or_else( || format!("{}.static", authorized_keys) ); if authorized_keys == authorized_keys_include { throw!(anyhow!( "ssh authorized_keys and authorized_keys_include are equal {:?} \ which would imply including a file in itself", &authorized_keys )); } let ssh_proxy_uid = match ssh_proxy_user { None => Uid::current(), Some(spec) => Uid::from_raw(if let Ok(num) = spec.parse() { num } else { let pwent = (|| Ok::<_,AE>({ Passwd::from_name(&spec) .map_err(|e| anyhow!("lookup failed: {}", e))? .ok_or_else(|| anyhow!("does not exist"))? }))() .with_context(|| spec.clone()) .context("ssh_proxy_uidr")?; pwent.uid }) }; let shapelibs = shapelibs.unwrap_or_else(||{ let glob = defpath(None, DEFAULT_LIBRARY_GLOB); vec![ ShapelibConfig1::PathGlob(glob) ] }); let sendmail = prctx.resolve(&sendmail.unwrap_or_else( || DEFAULT_SENDMAIL_PROGRAM.into() )); let public_url = public_url .trim_end_matches('/') .into(); let sse_wildcard_url = sse_wildcard_url.map(|pat| { let mut it = pat.splitn(2, '*'); let lhs = it.next().unwrap(); let rhs = it.next().ok_or_else(||anyhow!( "sse_wildcard_url must containa '*'" ))?; let rhs = rhs.trim_end_matches('/'); Ok::<_,AE>((lhs.into(), rhs.into())) }).transpose()?; let debug = debug.unwrap_or(cfg!(debug_assertions)); let log = { use toml::Value::Table; let mut log = match log { Some(Table(log)) => log, None => default(), Some(x) => throw!(anyhow!( r#"wanted table for "log" config key, not {}"#, x.type_str()) ), }; // flexi_logger doesn't allow env var to override config, sigh // But we can simulate this by having it convert the env results // to toml and merging it with the stuff from the file. (||{ if let Some(v) = env::var_os(LOG_ENV_VAR) { let v = v.to_str().ok_or_else(|| anyhow!("UTF-8 conversion"))?; let v = LogSpecification::parse(v).context("parse")?; let mut buf: Vec = default(); v.to_toml(&mut buf).context("convert to toml")?; let v = toml_de::from_slice(&buf).context("reparse")?; match v { Some(Table(v)) => toml_merge(&mut log, &v), None => default(), Some(x) => throw!(anyhow!("reparse gave {:?}, no table", x)), }; } Ok::<_,AE>(()) })() .context(LOG_ENV_VAR) .context("processing env override")?; Table(log) }; let log = toml::to_string(&log)?; let log = LogSpecification::from_toml(&log) .context("log specification")?; let debug_js_inject = Arc::new(match &debug_js_inject_file { Some(f) => fs::read_to_string(f) .with_context(|| f.clone()).context("debug_js_inject_file")?, None => "".into(), }); let check_bundled_sources = check_bundled_sources.unwrap_or(true); let listen = (! listen.is_empty()).then(|| listen); let listen = match (listen, http_port) { (Some(addrs), None) => addrs, (Some(_), Some(_)) => throw!(anyhow!( "both http_port and listen specified")), (None, http_port) => { let http_port = http_port.unwrap_or(8000); let addrs: &[&dyn IpAddress] = &[ &Ipv6Addr::LOCALHOST, &Ipv4Addr::LOCALHOST, ]; addrs.iter() .map(|addr| addr.with_port(http_port)) .collect() }, }; let server = ServerConfig { save_dir, command_socket, debug, listen, public_url, sse_wildcard_url, template_dir, specs_dir, nwtemplate_dir, wasm_dir, libexec_dir, bundled_sources, shapelibs, sendmail, usvg_bin, debug_js_inject, check_bundled_sources, game_rng, global_clock, prctx, ssh_proxy_bin, ssh_proxy_uid, ssh_restrictions, authorized_keys, authorized_keys_include, }; trace_dbg!("config resolved", &server); Ok(WholeServerConfig { server: Arc::new(server), log, }) } } lazy_static! { static ref SAVE_AREA_LOCK: Mutex> = default(); static ref CONFIG: RwLock = default(); } pub fn config() -> Arc { CONFIG.read().server.clone() } pub fn log_config() -> LogSpecification { CONFIG.read().log.clone() } fn set_config(whole: WholeServerConfig) { *CONFIG.write() = whole; } impl ServerConfig { #[throws(StartupError)] pub fn read(config_filename: Option<&str>, prmeth: PathResolveMethod) { let config_filename = config_filename.map(|s| s.to_string()) .unwrap_or_else( || format!("{}/{}", DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_LEAFNAME) ); let mut buf = String::new(); File::open(&config_filename).with_context(||config_filename.to_string())? .read_to_string(&mut buf)?; let spec: ServerConfigSpec = toml_de::from_str(&buf)?; let whole = spec.resolve(prmeth)?; set_config(whole); } #[throws(AE)] pub fn lock_save_area(&self) { let mut st = SAVE_AREA_LOCK.lock(); let st = &mut *st; if st.is_none() { let lockfile = format!("{}/lock", config().save_dir); *st = Some((||{ let file = File::create(&lockfile).context("open")?; file.try_lock_exclusive().context("lock")?; Ok::<_,AE>(file) })().context(lockfile).context("lock global save area")?); } } pub fn save_dir(&self) -> &String { let st = SAVE_AREA_LOCK.lock(); let mut _f: &File = st.as_ref().unwrap(); &self.save_dir } } impl Default for WholeServerConfig { fn default() -> WholeServerConfig { let spec: ServerConfigSpec = toml_de::from_str(r#" public_url = "INTERNAL ERROR" "#) .expect("parse dummy config as ServerConfigSpec"); spec.resolve(default()).expect("empty spec into config") } } pub fn set_program_name(s: String) { *PROGRAM_NAME.write() = s; } pub fn program_name() -> String { { let set = PROGRAM_NAME.read(); if set.len() > 0 { return set.clone() } } let mut w = PROGRAM_NAME.write(); if w.len() > 0 { return w.clone() } let new = env::args().next().expect("expected at least 0 arguments"); let new = match new.rsplit_once('/') { Some((_path,leaf)) => leaf.to_owned(), None => new, }; let new = if new.len() > 0 { new } else { "otter".to_owned() }; *w = new.clone(); new }