diff --git a/Cargo.lock b/Cargo.lock index d3dd49f..e3465db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,12 +11,38 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -63,12 +89,27 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + [[package]] name = "cactus" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf034765b7d19a011c6d619e880582bf95e8186b580e6fab56589872dd87dcf5" +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -89,6 +130,26 @@ dependencies = [ "vob", ] +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.48.5", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.11" @@ -187,6 +248,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + [[package]] name = "escargot" version = "0.5.8" @@ -261,6 +331,15 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.3" @@ -282,6 +361,40 @@ dependencies = [ "digest", ] +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ignore" version = "0.4.21" @@ -314,6 +427,15 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -392,6 +514,12 @@ dependencies = [ "vob", ] +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + [[package]] name = "memchr" version = "2.6.4" @@ -444,7 +572,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.3", "libc", ] @@ -709,6 +837,7 @@ dependencies = [ "hex", "hmac", "libc", + "log", "lrlex", "lrpar", "nix", @@ -721,6 +850,8 @@ dependencies = [ "serde_json", "sha2", "signal-hook", + "stderrlog", + "syslog", "tempfile", "wait-timeout", ] @@ -743,6 +874,19 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stderrlog" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69a26bbf6de627d389164afa9783739b56746c6c72c4ed16539f4ff54170327b" +dependencies = [ + "atty", + "chrono", + "log", + "termcolor", + "thread_local", +] + [[package]] name = "subtle" version = "2.4.1" @@ -760,6 +904,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syslog" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7434e95bcccce1215d30f4bf84fe8c00e8de1b9be4fb736d747ca53d36e7f96f" +dependencies = [ + "error-chain", + "hostname", + "libc", + "log", + "time", +] + [[package]] name = "tempfile" version = "3.8.1" @@ -773,6 +930,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.50" @@ -793,6 +959,16 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.30" @@ -889,6 +1065,60 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + [[package]] name = "winapi" version = "0.3.9" @@ -920,6 +1150,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index bdf1b83..9b9b92b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ getopts = "0.2" hex = "0.4" hmac = "0.12" libc = "0.2" +log = "0.4" lrlex = "0.13" lrpar = "0.13" nix = "0.26" @@ -36,6 +37,8 @@ secstr = "0.5" serde_json = "1" sha2 = "0.10" signal-hook = "0.3" +stderrlog = "0.5" +syslog = "6" tempfile = "3" [dev-dependencies] diff --git a/snare.1 b/snare.1 index 2c8364e..1e0a5cd 100644 --- a/snare.1 +++ b/snare.1 @@ -8,6 +8,7 @@ .Nm snare .Op Fl c Ar config-file .Op Fl d +.Op Fl v .Sh DESCRIPTION .Nm is a GitHub webhooks daemon. @@ -39,6 +40,11 @@ tells not to daemonise: in other words, `snare` stays in the foreground and logs errors to stderr. This can be useful for debugging. +.It Fl v +enables more verbose logging. +.Fl v +can be used up to 4 times, with each repetition increasing the quantity +of logging. .El .Sh INTEGRATION WITH GITHUB .Nm diff --git a/src/httpserver.rs b/src/httpserver.rs index fd13c27..76f963c 100644 --- a/src/httpserver.rs +++ b/src/httpserver.rs @@ -12,6 +12,7 @@ use std::{ }; use hmac::{Hmac, Mac}; +use log::{error, trace}; use percent_encoding::percent_decode; use secstr::SecStr; use sha2::Sha256; @@ -37,7 +38,7 @@ pub(crate) fn serve(snare: Arc) -> Result<(), Box> { } let active = Arc::new(AtomicUsize::new(0)); - for mut stream in listener.incoming().flatten() { + for stream in listener.incoming().flatten() { // We want to keep a limit on how many threads are started concurrently, so that an // attacker can't DOS the machine. `active` keeps track of how many threads are (or are // just about to be) active. Since the common case is that we haven't hit the limit, we @@ -56,61 +57,108 @@ pub(crate) fn serve(snare: Arc) -> Result<(), Box> { let active = Arc::clone(&active); let snare = Arc::clone(&snare); thread::spawn(move || { - match request(&snare, &mut stream) { - Ok(()) => { - http_200(&mut stream); - } - Err(e) => { - snare.error_err("Processing HTTP request", e); - http_400(&mut stream) - } - } + request(&snare, stream); active.fetch_sub(1, Ordering::Relaxed); }); } Ok(()) } -fn request(snare: &Arc, stream: &mut TcpStream) -> Result<(), Box> { - stream.set_read_timeout(Some(NET_TIMEOUT))?; - stream.set_write_timeout(Some(NET_TIMEOUT))?; +/// Try processing an HTTP request. +fn request(snare: &Arc, mut stream: TcpStream) { + match ( + stream.set_read_timeout(Some(NET_TIMEOUT)), + stream.set_write_timeout(Some(NET_TIMEOUT)), + ) { + (Ok(_), Ok(_)) => (), + _ => { + error!("Couldn't set timeout on sockets"); + http_500(stream); + return; + } + } let req_time = Instant::now(); - let (headers, body) = parse_get(stream)?; - stream.shutdown(Shutdown::Read)?; + let (headers, body) = match parse_get(&mut stream) { + Ok(x) => x, + Err(e) => { + trace!("Processing HTTP request: {e}"); + http_400(stream); + return; + } + }; + if stream.shutdown(Shutdown::Read).is_err() { + http_400(stream); + return; + } - let event_type = headers - .get("x-github-event") - .ok_or_else(|| "X-Github-Event header missing".to_owned())?; + let event_type = match headers.get("x-github-event") { + Some(x) => x, + None => { + trace!("HTTP request: X-Github-Event header missing"); + http_400(stream); + return; + } + }; if !valid_github_event(event_type) { - return Err("Invalid event type".into()); + error!("Invalid GitHub event type '{event_type}'"); + http_400(stream); + return; } let sig = match headers .get("x-hub-signature-256") .and_then(|s| s.split_once('=')) { Some(("sha256", sig)) => Some(sig), - Some(_) => return Err("Incorrectly formatted X-Hub-Signature-256 header".into()), + Some(_) => { + trace!("Incorrectly formatted X-Hub-Signature-256 header"); + http_400(stream); + return; + } None => None, }; if !body.starts_with("payload=".as_bytes()) { - return Err("Payload does not start with 'payload='".into()); + trace!("Payload does not start with 'payload='"); + http_400(stream); + return; } - let json_str = percent_decode(&body[8..]).decode_utf8()?.to_string(); - let jv = serde_json::from_str::(&json_str)?; + let json_str = match percent_decode(&body[8..]).decode_utf8() { + Ok(x) => x.to_string(), + Err(_) => { + trace!("JSON not valid UTF-8"); + http_400(stream); + return; + } + }; + let jv = match serde_json::from_str::(&json_str) { + Ok(x) => x, + Err(e) => { + trace!("Can't parse JSON: {e}"); + http_400(stream); + return; + } + }; let (owner, repo) = match ( &jv["repository"]["owner"]["login"].as_str(), &jv["repository"]["name"].as_str(), ) { (Some(o), Some(r)) => (o.to_owned(), r.to_owned()), - _ => return Err("Invalid JSON".into()), + _ => { + trace!("Invalid JSON"); + http_400(stream); + return; + } }; if !valid_github_ownername(owner) { - return Err(format!("Invalid GitHub owner '{}'.", &owner).into()); + trace!("Invalid GitHub owner syntax '{owner}'."); + http_400(stream); + return; } if !valid_github_reponame(repo) { - return Err(format!("Invalid GitHub repository '{}'.", &repo).into()); + trace!("Invalid GitHub repository syntax '{repo}'."); + http_400(stream); + return; } let conf = snare.conf.lock().unwrap(); @@ -119,25 +167,27 @@ fn request(snare: &Arc, stream: &mut TcpStream) -> Result<(), Box { if !authenticate(secret, sig, &body) { - return Err(format!("Authentication failed for {}/{}.", owner, repo).into()); + error!("Authentication failed for {owner}/{repo}."); + http_401(stream); + return; } } (Some(_), None) => { - return Err("Secret specified but request unsigned".into()); + error!("Secret specified but request unsigned"); + http_401(stream); + return; } (None, Some(_)) => { - return Err(format!( - "Request was signed but no secret was specified for {}/{}.", - owner, repo - ) - .into()); + error!("Request was signed but no secret was specified for {owner}/{repo}."); + http_401(stream); + return; } (None, None) => (), } drop(conf); if event_type == "ping" { - return Ok(()); + return; } let repo_id = format!("github/{}/{}", owner, repo); @@ -155,7 +205,8 @@ fn request(snare: &Arc, stream: &mut TcpStream) -> Result<(), Box Result<(HashMap, Vec Ok((headers_map, body)) } -fn http_200(stream: &mut TcpStream) { +fn http_200(mut stream: TcpStream) { stream.write_all(b"HTTP/1.1 200 OK\r\n\r\n").ok(); } -fn http_400(stream: &mut TcpStream) { +fn http_400(mut stream: TcpStream) { stream.write_all(b"HTTP/1.1 400\r\n\r\n").ok(); } +fn http_401(mut stream: TcpStream) { + stream.write_all(b"HTTP/1.1 401\r\n\r\n").ok(); +} + +fn http_500(mut stream: TcpStream) { + stream.write_all(b"HTTP/1.1 500\r\n\r\n").ok(); +} + /// Authenticate this request and if successful return `true` (where "success" also includes "the /// user didn't specify a secret for this repository"). fn authenticate(secret: &SecStr, sig: &str, pl: &[u8]) -> bool { diff --git a/src/jobrunner.rs b/src/jobrunner.rs index 3e72cd7..fbed5ac 100644 --- a/src/jobrunner.rs +++ b/src/jobrunner.rs @@ -22,6 +22,7 @@ use std::{ }; use libc::c_int; +use log::error; use nix::{ fcntl::{fcntl, FcntlArg, OFlag}, poll::{poll, PollFd, PollFlags}, @@ -216,10 +217,10 @@ impl JobRunner { if !exited_success { let job = &self.running[i].as_ref().unwrap(); if job.is_errorcmd { - self.snare.error(&format!( + error!( "errorcmd exited unsuccessfully: {}", job.rconf.errorcmd.as_ref().unwrap() - )); + ); } else if let Some(errorchild) = self.run_errorcmd(job) { let job = &mut self.running[i].as_mut().unwrap(); job.child = errorchild; @@ -335,19 +336,19 @@ impl JobRunner { Ok(tfile) => match tfile.into_temp_path().keep() { Ok(p) => { if let Err(e) = fs::write(&p, qj.json_str.as_bytes()) { - self.snare.error_err("Couldn't write JSON file.", e); + error!("Couldn't write JSON file: {e}"); remove_file(p).ok(); return Err(Some(qj)); } p } Err(e) => { - self.snare.error_err("Couldn't create temporary file.", e); + error!("Couldn't create temporary file: {e}"); return Err(Some(qj)); } }, Err(e) => { - self.snare.error_err("Couldn't create temporary file.", e); + error!("Couldn't create temporary file: {e}"); return Err(Some(qj)); } }; @@ -376,7 +377,7 @@ impl JobRunner { { Ok(c) => c, Err(e) => { - self.snare.error_err("Can't spawn command: {:?}", e); + error!("Can't spawn command: {e}"); return Err(None); } }; @@ -391,8 +392,7 @@ impl JobRunner { if let Err(e) = set_nonblock(stderr_fd).and_then(|_| set_nonblock(stdout_fd)) { - self.snare - .error_err("Can't set file descriptors to non-blocking: {:?}", e); + error!("Can't set file descriptors to non-blocking: {e}"); return Err(None); } @@ -507,9 +507,7 @@ impl JobRunner { .spawn() { Ok(c) => return Some(c), - Err(e) => self - .snare - .error_err(&format!("Can't spawn '{}'", errorcmd), e), + Err(e) => error!("Can't spawn '{errorcmd}': {e}"), } } None diff --git a/src/main.rs b/src/main.rs index e27c66f..23d8556 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,9 +16,6 @@ mod queue; use std::{ env::{self, current_exe, set_current_dir}, - error::Error, - ffi::CString, - fmt::Display, os::unix::io::RawFd, path::PathBuf, process, @@ -29,7 +26,7 @@ use std::{ }; use getopts::Options; -use libc::{c_char, openlog, syslog, LOG_CONS, LOG_CRIT, LOG_DAEMON, LOG_ERR}; +use log::error; use nix::{ fcntl::OFlag, unistd::{daemon, pipe2, setresgid, setresuid, Gid, Uid}, @@ -43,8 +40,6 @@ use queue::Queue; const SNARE_CONF_PATH: &str = "/etc/snare/snare.conf"; pub(crate) struct Snare { - /// Are we currently running as a daemon? - daemonised: bool, /// The location of snare.conf; this file will be reloaded if SIGHUP is received. conf_path: PathBuf, /// The current configuration: note that this can change at any point due to SIGHUP. All calls @@ -74,60 +69,11 @@ impl Snare { if self.sighup_occurred.load(Ordering::Relaxed) { match Config::from_path(&self.conf_path) { Ok(conf) => *self.conf.lock().unwrap() = conf, - Err(msg) => self.error(&msg), + Err(msg) => error!("Couldn't reload config: {msg}"), } self.sighup_occurred.store(false, Ordering::Relaxed); } } - - /// Log `msg` as an error. - /// - /// # Panics - /// - /// If `msg` contains a `NUL` byte. - fn error(&self, msg: &str) { - if self.daemonised { - // We know that `%s` and `` are both valid C strings, and - // that neither unwrap() can fail. - let fmt = CString::new("%s").unwrap(); - let msg = CString::new(msg) - .unwrap_or_else(|_| CString::new("").unwrap()); - unsafe { - syslog(LOG_ERR, fmt.as_ptr(), msg.as_ptr()); - } - } else { - eprintln!("{}", msg); - } - } - - /// Log `msg` as an error, with extra information in the Rust [`Error`](::Error) `err` and then - /// exit(1). - /// - /// # Panics - /// - /// If `msg` contains a `NUL` byte. - fn error_err> + Display>(&self, msg: &str, err: E) { - self.error(&format!("{}: {}", msg, err)); - } - - /// Log `msg` as a fatal error and then exit(1). - /// - /// # Panics - /// - /// If `msg` contains a `NUL` byte. - fn fatal(&self, msg: &str) -> ! { - fatal(self.daemonised, msg); - } - - /// Log `msg` as a fatal error, with extra information in the Rust [`Error`](::Error) `err` and - /// then exit(1). - /// - /// # Panics - /// - /// If `msg` contains a `NUL` byte. - fn fatal_err> + Display>(&self, msg: &str, err: E) -> ! { - self.fatal(&format!("{}: {}", msg, err)); - } } /// Try to find a `snare.conf` file. @@ -143,7 +89,7 @@ fn user_from_name(n: &str) -> Option { match Passwd::from_name(n) { Ok(Some(x)) => Some(x), Ok(None) => None, - Err(e) => fatal_err(false, &format!("Can't access user information for {n}"), e), + Err(e) => fatal(&format!("Can't access user information for {n}: {e}")), } } @@ -155,23 +101,20 @@ fn change_user(conf: &Config) { Some(u) => { let gid = Gid::from_raw(u.gid); if let Err(e) = setresgid(gid, gid, gid) { - fatal_err(false, &format!("Can't switch to group '{}'", user), e); + fatal(&format!("Can't switch to group '{user}': {e}")) } let uid = Uid::from_raw(u.uid); if let Err(e) = setresuid(uid, uid, uid) { - fatal_err(false, &format!("Can't switch to user '{}'", user), e); + fatal(&format!("Can't switch to user '{user}': {e}")) } env::set_var("HOME", u.dir); env::set_var("USER", user); } - None => fatal(false, &format!("Unknown user '{}'", user)), + None => fatal(&format!("Unknown user '{user}'")), }, None => { if Uid::current().is_root() { - fatal( - false, - "The 'user' option must be set if snare is run as root", - ); + fatal("The 'user' option must be set if snare is run as root"); } } } @@ -189,27 +132,11 @@ fn progname() -> String { } /// Exit with a fatal error. -fn fatal(daemonised: bool, msg: &str) -> ! { - if daemonised { - // We know that `%s` and `` are both valid C strings, and - // that neither unwrap() can fail. - let fmt = CString::new("%s").unwrap(); - let msg = CString::new(msg) - .unwrap_or_else(|_| CString::new("").unwrap()); - unsafe { - syslog(LOG_CRIT, fmt.as_ptr(), msg.as_ptr()); - } - } else { - eprintln!("{}", msg); - } +fn fatal(msg: &str) -> ! { + eprintln!("{msg:}"); process::exit(1); } -/// Exit with a fatal error, printing the contents of `err`. -fn fatal_err> + Display>(daemonised: bool, msg: &str, err: E) -> ! { - fatal(daemonised, &format!("{}: {}", msg, err)); -} - /// Print out program usage then exit. This function must not be called after daemonisation. fn usage() -> ! { eprintln!("Usage: {} [-c ] [-d]", progname()); @@ -226,6 +153,7 @@ pub fn main() { "Don't detach from the terminal and log errors to stderr.", ) .optflag("h", "help", "") + .optflagmulti("v", "verbose", "") .parse(&args[1..]) .unwrap_or_else(|_| usage()); if matches.opt_present("h") { @@ -236,33 +164,47 @@ pub fn main() { let conf_path = match matches.opt_str("c") { Some(p) => PathBuf::from(&p), - None => search_snare_conf().unwrap_or_else(|| fatal(false, "Can't find snare.conf")), + None => search_snare_conf().unwrap_or_else(|| fatal("Can't find snare.conf")), }; - let conf = Config::from_path(&conf_path).unwrap_or_else(|m| fatal(false, &m)); + let conf = Config::from_path(&conf_path).unwrap_or_else(|m| fatal(&m)); change_user(&conf); - set_current_dir("/").unwrap_or_else(|_| fatal(false, "Can't chdir to '/'")); + set_current_dir("/").unwrap_or_else(|_| fatal("Can't chdir to '/'")); if daemonise { + let formatter = syslog::Formatter3164 { + process: progname(), + ..Default::default() + }; + let logger = syslog::unix(formatter) + .unwrap_or_else(|e| fatal(&format!("Cannot connect to syslog: {e:}"))); + let levelfilter = match matches.opt_count("v") { + 0 => log::LevelFilter::Error, + 1 => log::LevelFilter::Warn, + 2 => log::LevelFilter::Info, + 3 => log::LevelFilter::Debug, + _ => log::LevelFilter::Trace, + }; + log::set_boxed_logger(Box::new(syslog::BasicLogger::new(logger))) + .map(|()| log::set_max_level(levelfilter)) + .unwrap_or_else(|e| fatal(&format!("Cannot set logger: {e:}"))); if let Err(e) = daemon(true, false) { - fatal_err(false, "Couldn't daemonise: {}", e); + fatal(&format!("Couldn't daemonise: {e}")); } - } - - // openlog's first argument `ident` is incompletely specified, but in practise we have to - // assume that syslog merely stores a pointer to the string (i.e. it doesn't copy the string). - // We thus deliberately leak memory here in order that the pointer always points to valid - // memory. The unwrap() here is ugly, but if it fails, it means we've run out of memory, so - // it's neither likely to fail nor, if it does, can we do anything to clear up from it. - let progname = - Box::into_raw(CString::new(progname()).unwrap().into_boxed_c_str()) as *const c_char; - unsafe { - openlog(progname, LOG_CONS, LOG_DAEMON); + } else { + stderrlog::new() + .module(module_path!()) + .verbosity(matches.opt_count("v")) + .init() + .unwrap(); } let (event_read_fd, event_write_fd) = match pipe2(OFlag::O_NONBLOCK) { Ok(p) => p, - Err(e) => fatal_err(daemonise, "Can't create pipe", e), + Err(e) => { + error!("Can't create pipe: {e}"); + process::exit(1); + } }; let sighup_occurred = Arc::new(AtomicBool::new(false)); { @@ -274,7 +216,8 @@ pub fn main() { nix::unistd::write(event_write_fd, &[0]).ok(); }) } { - fatal_err(daemonise, "Can't install SIGHUP handler", e); + error!("Can't install SIGHUP handler: {e}"); + process::exit(1); } if let Err(e) = unsafe { signal_hook::low_level::register(signal_hook::consts::SIGCHLD, move || { @@ -282,12 +225,12 @@ pub fn main() { nix::unistd::write(event_write_fd, &[0]).ok(); }) } { - fatal_err(daemonise, "Can't install SIGCHLD handler", e); + error!("Can't install SIGCHLD handler: {e}"); + process::exit(1); } } let snare = Arc::new(Snare { - daemonised: daemonise, conf_path, conf: Mutex::new(conf), queue: Mutex::new(Queue::new()), @@ -298,7 +241,10 @@ pub fn main() { match jobrunner::attend(Arc::clone(&snare)) { Ok(x) => x, - Err(e) => snare.fatal_err("Couldn't start runner thread", e), + Err(e) => { + error!("Couldn't start runner thread: {e}"); + process::exit(1); + } } httpserver::serve(snare).unwrap(); diff --git a/tests/auth.rs b/tests/auth.rs index cfdb749..713763c 100644 --- a/tests/auth.rs +++ b/tests/auth.rs @@ -103,7 +103,7 @@ fn bad_sha256() -> Result<(), Box> { &[( move |port| Ok(req(port, false)), move |response| { - if response.starts_with("HTTP/1.1 400") { + if response.starts_with("HTTP/1.1 401") { sleep(SNARE_PAUSE); assert!(!tp.is_file()); Ok(()) @@ -129,7 +129,7 @@ fn wrong_secret() -> Result<(), Box> { &[( move |port| Ok(req(port, true)), move |response| { - if response.starts_with("HTTP/1.1 400") { + if response.starts_with("HTTP/1.1 401") { sleep(SNARE_PAUSE); assert!(!tp.is_file()); Ok(())