Skip to main content

otter_support/
config.rs

1// Copyright 2020-2021 Ian Jackson and contributors to Otter
2// SPDX-License-Identifier: AGPL-3.0-or-later
3// There is NO WARRANTY.
4
5use crate::prelude::*;
6use pwd::Passwd;
7
8pub const EXIT_SPACE     : i32 =  2;
9pub const EXIT_NOTFOUND  : i32 =  4;
10pub const EXIT_SITUATION : i32 =  8;
11pub const EXIT_USAGE     : i32 = 12;
12pub const EXIT_DISASTER  : i32 = 16;
13
14pub const DEFAULT_CONFIG_DIR       : &str = "/etc/otter";
15pub const DEFAULT_CONFIG_LEAFNAME  : &str = "server.toml";
16pub const DEFAULT_SENDMAIL_PROGRAM : &str = "/usr/sbin/sendmail";
17pub const DEFAULT_SSH_PROXY_CMD    : &str = "otter-ssh-proxy";
18pub const SSH_PROXY_SUBCMD         : &str = "mgmtchannel-proxy";
19
20pub const DAEMON_STARTUP_REPORT: &str = "otter-daemon started";
21pub const LOG_ENV_VAR: &str = "OTTER_LOG";
22
23#[derive(Deserialize,Debug,Clone)]
24pub struct ServerConfigSpec {
25  pub change_directory: Option<String>,
26  pub base_dir: Option<String>,
27  pub save_dir: Option<String>,
28  pub libexec_dir: Option<String>,
29  pub usvg_bin: Option<String>,
30  pub command_socket: Option<String>,
31  pub debug: Option<bool>,
32  pub http_port: Option<u16>,
33  #[serde(default)] pub listen: Vec<SocketAddr>,
34  pub public_url: String,
35  pub sse_wildcard_url: Option<String>,
36  pub template_dir: Option<String>,
37  pub nwtemplate_dir: Option<String>,
38  pub wasm_dir: Option<String>,
39  pub log: Option<toml::Value>,
40  pub bundled_sources: Option<String>,
41  pub shapelibs: Option<Vec<ShapelibConfig1>>,
42  pub specs_dir: Option<String>,
43  pub sendmail: Option<String>,
44  /// for auth keys, split on spaces
45  pub ssh_proxy_command: Option<String>,
46  pub ssh_proxy_user: Option<String>,
47  pub ssh_restrictions: Option<String>,
48  pub authorized_keys: Option<String>,
49  pub authorized_keys_include: Option<String>,
50  pub debug_js_inject_file: Option<String>,
51  #[serde(default)] pub fake_rng: FakeRngSpec,
52  #[serde(default)] pub fake_time: FakeTimeConfig,
53  /// Disable this for local testing only.  See LICENCE.
54  pub check_bundled_sources: Option<bool>,
55}
56
57#[derive(Deserialize,Debug,Clone)]
58#[serde(untagged)]
59pub enum ShapelibConfig1 {
60  PathGlob(String),
61  Explicit(ShapelibExplicit1),
62}
63
64#[derive(Deserialize,Debug,Clone)]
65pub struct ShapelibExplicit1 {
66  pub name: String,
67  pub catalogue: String,
68  pub dirname: String,
69}
70
71#[derive(Debug,Clone)]
72pub struct WholeServerConfig {
73  server: Arc<ServerConfig>,
74  log: LogSpecification,
75}
76
77#[derive(Debug)]
78pub struct ServerConfig {
79  save_dir: String,
80  pub command_socket: String,
81  pub debug: bool,
82  pub listen: Vec<SocketAddr>,
83  pub public_url: String,
84  pub sse_wildcard_url: Option<(String, String)>,
85  pub template_dir: String,
86  pub nwtemplate_dir: String,
87  pub wasm_dir: String,
88  pub libexec_dir: String,
89  pub usvg_bin: String,
90  pub bundled_sources: String,
91  pub shapelibs: Vec<ShapelibConfig1>,
92  pub specs_dir: String,
93  pub sendmail: String,
94  pub ssh_proxy_bin: String,
95  pub ssh_proxy_uid: Uid,
96  pub ssh_restrictions: String,
97  pub authorized_keys: String,
98  pub authorized_keys_include: String,
99  pub debug_js_inject: Arc<String>,
100  pub check_bundled_sources: bool,
101  pub game_rng: RngWrap,
102  pub global_clock: GlobalClock,
103  pub prctx: PathResolveContext,
104}
105
106#[derive(Debug,Copy,Clone)]
107pub enum PathResolveMethod {
108  Chdir,
109  Prefix,
110}
111impl Default for PathResolveMethod { fn default() -> Self { Self::Prefix } }
112#[derive(Debug,Clone)]
113pub enum PathResolveContext {
114  RelativeTo(String),
115  Noop,
116}
117impl Default for PathResolveContext { fn default() -> Self { Self::Noop } }
118
119static PROGRAM_NAME: RwLock<String> = parking_lot::const_rwlock(String::new());
120
121impl PathResolveMethod {
122  #[throws(io::Error)]
123  fn chdir(self, cd: &str) -> PathResolveContext {
124    use PathResolveMethod::*;
125    use PathResolveContext::*;
126    match self {
127      Chdir => { env::set_current_dir(cd)?; Noop },
128      Prefix if cd == "." => Noop,
129      Prefix => RelativeTo(cd.to_string()),
130    }
131  }
132}
133impl PathResolveContext {
134  pub fn resolve(&self, input: &str) -> String {
135    use PathResolveContext::*;
136    match (self, input.as_bytes()) {
137      (Noop           , _         ) |
138      (RelativeTo(_  ), &[b'/',..]) => input.to_owned(),
139      (RelativeTo(cd), _          ) => format!("{}/{}", &cd, input),
140    }
141  }
142}
143
144impl ServerConfigSpec {
145  //#[throws(AE)]
146  pub fn resolve(self, prmeth: PathResolveMethod)
147                 -> Result<WholeServerConfig,AE> {
148    let ServerConfigSpec {
149      change_directory, base_dir, save_dir, command_socket, debug,
150      http_port, listen, public_url, sse_wildcard_url,
151      template_dir, specs_dir, nwtemplate_dir, wasm_dir, libexec_dir, usvg_bin,
152      log, bundled_sources, shapelibs, sendmail,
153      debug_js_inject_file, check_bundled_sources, fake_rng, fake_time,
154      ssh_proxy_command, ssh_proxy_user, ssh_restrictions, authorized_keys,
155      authorized_keys_include,
156    } = self;
157
158    let game_rng = fake_rng.make_game_rng();
159    let global_clock = fake_time.make_global_clock();
160    let home = || env::var("HOME").context("HOME");
161
162    let prctx = if let Some(ref cd) = change_directory {
163      prmeth.chdir(cd)
164        .with_context(|| cd.clone())
165        .context("config change_directory")?
166    } else {
167      PathResolveContext::Noop
168    };
169
170    let defpath = |specd: Option<String>, leaf: &str| -> String {
171      prctx.resolve(&specd.unwrap_or_else(|| match &base_dir {
172        Some(base) => format!("{}/{}", &base, &leaf),
173        None       => leaf.to_owned(),
174      }))
175    };
176    let save_dir        = defpath(save_dir,        "save"              );
177    let specs_dir       = defpath(specs_dir,       "specs"             );
178    let command_socket  = defpath(command_socket,  "var/command.socket");
179    let template_dir    = defpath(template_dir,    "assets"            );
180    let wasm_dir        = defpath(wasm_dir,        "assets"            );
181    let nwtemplate_dir  = defpath(nwtemplate_dir,  "nwtemplates"       );
182    let libexec_dir     = defpath(libexec_dir,     "libexec"           );
183    let bundled_sources = defpath(bundled_sources, "bundled-sources"   );
184    const DEFAULT_LIBRARY_GLOB: &str = "library/*.toml";
185
186    let in_libexec = |specd: Option<String>, leaf: &str| -> String {
187      specd.unwrap_or_else(|| format!("{}/{}", &libexec_dir, leaf))
188    };
189    let usvg_bin        = in_libexec(usvg_bin,     "usvg"              );
190    let ssh_proxy_bin = in_libexec(ssh_proxy_command, DEFAULT_SSH_PROXY_CMD );
191
192    let ssh_restrictions = ssh_restrictions.unwrap_or_else(
193      || concat!("restrict,no-agent-forwarding,no-port-forwarding,",
194                 "no-pty,no-user-rc,no-X11-forwarding").into());
195
196    let authorized_keys = if let Some(ak) = authorized_keys { ak } else {
197      let home = home().context("for authorized_keys")?;
198      // we deliberately don't create the ~/.ssh dir
199      format!("{}/.ssh/authorized_keys", home)
200    };
201    let authorized_keys_include = authorized_keys_include.unwrap_or_else(
202      || format!("{}.static", authorized_keys)
203    );
204    if authorized_keys == authorized_keys_include {
205      throw!(anyhow!(
206        "ssh authorized_keys and authorized_keys_include are equal {:?} \
207         which would imply including a file in itself",
208        &authorized_keys
209      ));
210    }
211
212    let ssh_proxy_uid = match ssh_proxy_user {
213      None => Uid::current(),
214      Some(spec) => Uid::from_raw(if let Ok(num) = spec.parse() {
215        num
216      } else {
217        let pwent = (|| Ok::<_,AE>({
218          Passwd::from_name(&spec)
219            .map_err(|e| anyhow!("lookup failed: {}", e))?
220            .ok_or_else(|| anyhow!("does not exist"))?
221        }))()
222          .with_context(|| spec.clone())
223          .context("ssh_proxy_uidr")?;
224        pwent.uid
225      })
226    };
227
228    let shapelibs = shapelibs.unwrap_or_else(||{
229      let glob = defpath(None, DEFAULT_LIBRARY_GLOB);
230      vec![ ShapelibConfig1::PathGlob(glob) ]
231    });
232
233    let sendmail = prctx.resolve(&sendmail.unwrap_or_else(
234      || DEFAULT_SENDMAIL_PROGRAM.into()
235    ));
236
237    let public_url = public_url
238      .trim_end_matches('/')
239      .into();
240
241    let sse_wildcard_url = sse_wildcard_url.map(|pat| {
242      let mut it = pat.splitn(2, '*');
243      let lhs = it.next().unwrap();
244      let rhs = it.next().ok_or_else(||anyhow!(
245        "sse_wildcard_url must containa '*'"
246      ))?;
247      let rhs = rhs.trim_end_matches('/');
248      Ok::<_,AE>((lhs.into(), rhs.into()))
249    }).transpose()?;
250
251    let debug = debug.unwrap_or(cfg!(debug_assertions));
252
253    let log = {
254      use toml::Value::Table;
255
256      let mut log = match log {
257        Some(Table(log)) => log,
258        None => default(),
259        Some(x) => throw!(anyhow!(
260          r#"wanted table for "log" config key, not {}"#,
261          x.type_str())
262        ),
263      };
264      
265      // flexi_logger doesn't allow env var to override config, sigh
266      // But we can simulate this by having it convert the env results
267      // to toml and merging it with the stuff from the file.
268      (||{
269        if let Some(v) = env::var_os(LOG_ENV_VAR) {
270          let v = v.to_str().ok_or_else(|| anyhow!("UTF-8 conversion"))?;
271          let v = LogSpecification::parse(v).context("parse")?;
272          let mut buf: Vec<u8> = default();
273          v.to_toml(&mut buf).context("convert to toml")?;
274          let v = toml_de::from_slice(&buf).context("reparse")?;
275          match v {
276            Some(Table(v)) => toml_merge(&mut log, &v),
277            None => default(),
278            Some(x) => throw!(anyhow!("reparse gave {:?}, no table", x)),
279          };
280        }
281        Ok::<_,AE>(())
282      })()
283        .context(LOG_ENV_VAR)
284        .context("processing env override")?;
285
286      Table(log)
287    };
288
289    let log = toml::to_string(&log)?;
290    let log = LogSpecification::from_toml(&log)
291      .context("log specification")?;
292
293    let debug_js_inject = Arc::new(match &debug_js_inject_file {
294      Some(f) => fs::read_to_string(f)
295        .with_context(|| f.clone()).context("debug_js_inject_file")?,
296      None => "".into(),
297    });
298
299    let check_bundled_sources = check_bundled_sources.unwrap_or(true); 
300
301    let listen = (! listen.is_empty()).then(|| listen);
302    let listen = match (listen, http_port) {
303      (Some(addrs), None) => addrs,
304      (Some(_), Some(_)) => throw!(anyhow!(
305        "both http_port and listen specified")),
306      (None, http_port) => {
307        let http_port = http_port.unwrap_or(8000);
308        let addrs: &[&dyn IpAddress] = &[
309          &Ipv6Addr::LOCALHOST,
310          &Ipv4Addr::LOCALHOST,
311        ];
312        addrs.iter()
313          .map(|addr| addr.with_port(http_port))
314          .collect()
315      },
316    };
317
318    let server = ServerConfig {
319      save_dir, command_socket, debug,
320      listen, public_url, sse_wildcard_url,
321      template_dir, specs_dir, nwtemplate_dir, wasm_dir, libexec_dir,
322      bundled_sources, shapelibs, sendmail, usvg_bin,
323      debug_js_inject, check_bundled_sources, game_rng, global_clock, prctx,
324      ssh_proxy_bin, ssh_proxy_uid, ssh_restrictions,
325      authorized_keys, authorized_keys_include,
326    };
327    trace_dbg!("config resolved", &server);
328    Ok(WholeServerConfig {
329      server: Arc::new(server),
330      log,
331    })
332  }
333}
334
335lazy_static! {
336  static ref SAVE_AREA_LOCK: Mutex<Option<File>> = default();
337
338  static ref CONFIG: RwLock<WholeServerConfig> = default();
339}
340
341pub fn config() -> Arc<ServerConfig> {
342  CONFIG.read().server.clone()
343}
344pub fn log_config() -> LogSpecification {
345  CONFIG.read().log.clone()
346}
347
348fn set_config(whole: WholeServerConfig) {
349  *CONFIG.write() = whole;
350}
351
352impl ServerConfig {
353  #[throws(StartupError)]
354  pub fn read(config_filename: Option<&str>, prmeth: PathResolveMethod) {
355    let config_filename = config_filename.map(|s| s.to_string())
356      .unwrap_or_else(
357        || format!("{}/{}", DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_LEAFNAME)
358      );
359    let mut buf = String::new();
360    File::open(&config_filename).with_context(||config_filename.to_string())?
361      .read_to_string(&mut buf)?;
362    let spec: ServerConfigSpec = toml_de::from_str(&buf)?;
363    let whole = spec.resolve(prmeth)?;
364    set_config(whole);
365  }
366
367  #[throws(AE)]
368  pub fn lock_save_area(&self) {
369    let mut st = SAVE_AREA_LOCK.lock();
370    let st = &mut *st;
371    if st.is_none() {
372      let lockfile = format!("{}/lock", config().save_dir);
373      *st = Some((||{
374        let file = File::create(&lockfile).context("open")?;
375        file.try_lock_exclusive().context("lock")?;
376        Ok::<_,AE>(file)
377      })().context(lockfile).context("lock global save area")?);
378    }
379  }
380
381  pub fn save_dir(&self) -> &String {
382    let st = SAVE_AREA_LOCK.lock();
383    let mut _f: &File = st.as_ref().unwrap();
384    &self.save_dir
385  }
386}
387
388impl Default for WholeServerConfig {
389  fn default() -> WholeServerConfig {
390    let spec: ServerConfigSpec = toml_de::from_str(r#"
391      public_url = "INTERNAL ERROR"
392      "#)
393      .expect("parse dummy config as ServerConfigSpec");
394    spec.resolve(default()).expect("empty spec into config")
395  }
396}
397
398pub fn set_program_name(s: String) {
399  *PROGRAM_NAME.write() = s;
400}
401
402pub fn program_name() -> String {
403  {
404    let set = PROGRAM_NAME.read();
405    if set.len() > 0 { return set.clone() }
406  }
407
408  let mut w = PROGRAM_NAME.write();
409  if w.len() > 0 { return w.clone() }
410
411  let new = env::args().next().expect("expected at least 0 arguments");
412  let new = match new.rsplit_once('/') {
413    Some((_path,leaf)) => leaf.to_owned(),
414    None => new,
415  };
416  let new = if new.len() > 0 { new } else { "otter".to_owned() };
417  *w = new.clone();
418  new
419}