1use 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 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 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 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 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 (||{
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}