From 8995fc3bde278ba048a9ddbc86de7df7fe952c0e Mon Sep 17 00:00:00 2001 From: mathis Date: Sat, 23 Nov 2024 12:29:12 +0100 Subject: [PATCH] * added logs * changed econ `127.0.0.1` to `localhost` * added optional config option `timeout_seconds` * added more string escapes for extra precaution --- Cargo.lock | 145 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + src/main.rs | 107 +++++++++++++++++++++++++++----------- 3 files changed, 224 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96a3cf9..f9c2914 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,64 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.86" @@ -137,6 +195,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "directories" version = "5.0.1" @@ -158,6 +222,29 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -299,6 +386,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "1.4.1" @@ -343,6 +436,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.11" @@ -550,6 +649,35 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -879,6 +1007,8 @@ dependencies = [ "anyhow", "axum", "directories", + "env_logger", + "log", "serde", "tokio", "tokio-stream", @@ -902,6 +1032,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.10.0" @@ -941,6 +1077,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index fffb7de..082b672 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ edition = "2021" anyhow = "1.0.86" axum = "0.7.5" directories = "5.0.1" +env_logger = "0.11.5" +log = "0.4.22" serde = { version = "1.0.209", features = ["derive"] } tokio = { version = "1.40.0", features = ["full"] } tokio-stream = { version = "0.1.15", features = ["sync"] } diff --git a/src/main.rs b/src/main.rs index 5455382..5493d41 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,7 +34,7 @@ struct AppState { } struct ServerProcess { - tcp_stream: tokio::net::TcpStream, + tcp_write: tokio::net::tcp::OwnedWriteHalf, server_path: PathBuf, map_path: PathBuf, port: u16, @@ -47,16 +47,53 @@ struct ServerEvent { data: String, } +fn default_timeout() -> u64 { + 60 +} + #[derive(Clone, Deserialize)] struct Config { http_port: u16, executable_path: PathBuf, port_range: (u16, u16), public_address: String, + #[serde(default = "default_timeout")] + timeout_seconds: u64, +} + +impl ServerProcess { + fn new(tcp: tokio::net::TcpStream, server_path: PathBuf, map_path: PathBuf, port: u16) -> Self { + let (tcp_read, tcp_write) = tcp.into_split(); + + tokio::task::spawn(log_errors(async move { + let mut reader = tokio::io::BufReader::new(tcp_read).lines(); + while let Some(line) = reader.next_line().await? { + log::debug!("{port} <- {line}"); + } + log::debug!("{port}: tcp stream closed"); + Ok(()) + })); + + Self { + tcp_write, + server_path, + map_path, + port, + } + } + async fn write(&mut self, buf: &str) -> tokio::io::Result<()> { + let port = self.port; + for line in buf.lines() { + log::debug!("{port} -> {line}"); + } + self.tcp_write.write_all(buf.as_bytes()).await + } } #[tokio::main] async fn main() -> Result<(), anyhow::Error> { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + let project_dirs = ProjectDirs::from("org", "ddnet", "trashmap") .context("Could not determine the user's home directory")?; let config_path = project_dirs.config_dir().join("config.toml"); @@ -106,7 +143,7 @@ async fn main() -> Result<(), anyhow::Error> { let mut processes = state.processes.lock().await; for process in processes.values_mut() { - process.tcp_stream.write_all(b"shutdown\n").await?; + process.write("shutdown\n").await?; tokio::fs::remove_dir_all(&process.server_path).await?; } @@ -115,7 +152,7 @@ async fn main() -> Result<(), anyhow::Error> { })); let listener = tokio::net::TcpListener::bind(("127.0.0.1", config.http_port)).await?; - println!("Listening on http://{}", listener.local_addr()?); + log::info!("Listening on http://{}", listener.local_addr()?); axum::serve(listener, app).await?; Ok(()) } @@ -155,7 +192,12 @@ async fn server_events( } fn escape_ddnet(str: &str) -> String { - str.replace(r"\", r"\\").replace("\"", "\\\"") + str + .replace(r"\", r"\\") + .replace("\"", "\\\"") + .replace("\n", " ") + .replace("#", "") + .replace(";", "") } #[derive(Deserialize)] @@ -169,17 +211,21 @@ async fn update_settings( State(state): State, Query(query): Query, ) -> Result { + log::debug!( + "update settings: id={}, name={}, pwd={}", + query.server_id, + query.server_name, + query.server_password + ); let mut processes = state.processes.lock().await; if let Some(process) = processes.get_mut(&query.server_id) { process - .tcp_stream - .write_all( - [ + .write( + &[ format!("sv_name \"{}\"\n", escape_ddnet(&query.server_name)), format!("password \"{}\"\n", escape_ddnet(&query.server_password)), ] - .concat() - .as_bytes(), + .concat(), ) .await?; Ok(StatusCode::ACCEPTED) @@ -201,6 +247,13 @@ async fn update_map( Query(query): Query, map_bytes: Bytes, ) -> Result { + log::debug!( + "update map: id={}, map={}, name={}, pwd={}", + query.server_id, + query.map_filename, + query.server_name, + query.server_password + ); let map_filename = Path::new(&query.map_filename) .file_name() .context("Not a valid filename")? @@ -220,11 +273,10 @@ async fn update_map( tokio::fs::write(&map_path, map_bytes).await?; if process.map_path == map_path { - process.tcp_stream.write_all(b"hot_reload\n").await?; + process.write("hot_reload\n").await?; } else { process - .tcp_stream - .write_all(format!("change_map \"{}\"\n", escape_ddnet(map_name)).as_bytes()) + .write(&format!("change_map \"{}\"\n", escape_ddnet(map_name))) .await?; tokio::fs::remove_file(&process.map_path).await?; process.map_path = map_path; @@ -250,7 +302,7 @@ async fn update_map( &format!("sv_name \"{}\"\n", escape_ddnet(&query.server_name)), &format!("password \"{}\"\n", escape_ddnet(&query.server_password)), &format!("ec_port {port}\n"), - "ec_bindaddr \"127.0.0.1\"\n", + "ec_bindaddr \"localhost\"\n", "ec_password \"open sesame\"\n", "ec_output_level -3\n", // Prevent the TCP buffer running full "sv_motd \"Use rcon password \\\"test\\\" or /practice for testing. Instead of \\\"super\\\" use \\\"invincible\\\" to toggle invincibility.\"\n", @@ -331,24 +383,19 @@ async fn update_map( let line = tokio::time::timeout_at(deadline, lines.next_line()) .await?? .context("The server process stopped unexpectedly")?; - if line.contains("econ: bound to 127.0.0.1") { + log::debug!("{line}"); + if line.contains("econ: bound to localhost") { break; } } - let mut tcp_stream = tokio::net::TcpStream::connect(("127.0.0.1", port)).await?; - + let mut tcp_stream = tokio::net::TcpStream::connect(("localhost", port)).await?; tcp_stream.write_all(b"open sesame\n").await?; - tcp_stream.write_all(b"stdout_output_level -3\n").await?; // Prevent the pipe running full + log::debug!("{port}: tcp stream opened"); processes.insert( query.server_id, - ServerProcess { - tcp_stream, - server_path, - map_path, - port, - }, + ServerProcess::new(tcp_stream, server_path, map_path, port), ); let _ = state.event_channel.send(ServerEvent { @@ -359,14 +406,14 @@ async fn update_map( let state_clone = state.clone(); tokio::task::spawn(log_errors(async move { - tokio::time::sleep(tokio::time::Duration::from_secs(60)).await; + tokio::time::sleep(tokio::time::Duration::from_secs( + state.config.timeout_seconds, + )) + .await; let mut processes = state_clone.processes.lock().await; if let Some(process) = processes.get_mut(&query.server_id) { - process - .tcp_stream - .write_all(b"sv_shutdown_when_empty 1\n") - .await?; + process.write("sv_shutdown_when_empty 1\n").await?; let _ = state_clone.event_channel.send(ServerEvent { server_id: query.server_id, @@ -384,7 +431,7 @@ async fn update_map( async fn log_errors(future: impl std::future::Future>) { if let Err(error) = future.await { - eprintln!("Error in task: {error:?}"); + log::error!("Error in task: {error:?}"); } } @@ -392,7 +439,7 @@ struct AppError(anyhow::Error); impl IntoResponse for AppError { fn into_response(self) -> Response { - eprintln!("Error in handler: {:?}", self.0); + log::error!("Error in handler: {:?}", self.0); (StatusCode::INTERNAL_SERVER_ERROR, self.0.to_string()).into_response() } }