From 8dfd84343ded747b0d6781640fe37508cd2578c6 Mon Sep 17 00:00:00 2001 From: Condorra Date: Sun, 1 Sep 2024 00:13:30 +1000 Subject: [PATCH] Make server configurable through environment. --- .gitignore | 1 + Cargo.lock | 8 ++++ Cargo.toml | 2 + src/main.rs | 125 ++++++++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 123 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index ea8c4bf..6e92637 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +localdata diff --git a/Cargo.lock b/Cargo.lock index 0d36072..adc4923 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,6 +306,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + [[package]] name = "autocfg" version = "1.3.0" @@ -1381,9 +1387,11 @@ version = "0.1.0" dependencies = [ "actix-web", "actix-ws", + "anyhow", "env_logger", "futures-util", "log", + "serde_json", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index e41649b..03f1406 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,9 @@ edition = "2021" [dependencies] actix-web = "4.9.0" actix-ws = "0.3.0" +anyhow = "1.0.86" env_logger = "0.11.5" futures-util = "0.3.30" log = "0.4.22" +serde_json = "1.0.127" tokio = { version = "1.39.3", features = ["net", "macros", "tokio-macros", "rt-multi-thread"] } diff --git a/src/main.rs b/src/main.rs index 38dba74..9bada03 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,66 @@ use actix_web::{ - self, get, + self, + error::InternalError, + get, + http::StatusCode, middleware::Logger, rt::{self, net::TcpStream}, - web, App, Error, HttpRequest, HttpResponse, HttpServer, Responder, + web::{self, Data}, + App, Error, HttpRequest, HttpResponse, HttpServer, Responder, }; use actix_ws::{AggregatedMessage, CloseCode, Closed}; +use anyhow::Context; use futures_util::StreamExt; +use serde_json::json; +use std::{env, fs, num::ParseIntError}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; +pub struct ServerConfig { + pub upstream_mud: String, + pub banner_to_mud: String, + pub listen_port: u16, + pub bind_address: String, + pub startup_script_file: String, +} + #[get("/ws")] -async fn ws(req: HttpRequest, body: web::Payload) -> impl Responder { +async fn ws( + config_data: Data, + req: HttpRequest, + body: web::Payload, +) -> impl Responder { let (response, mut session, stream) = actix_ws::handle(&req, body)?; let mut stream = stream.aggregate_continuations().max_continuation_size(1024); - let mut tcp_stream: TcpStream = TcpStream::connect("localhost:4000").await?; + let mut tcp_stream: TcpStream = TcpStream::connect(&config_data.upstream_mud).await?; + let subst_banner = config_data + .banner_to_mud + .replace( + "%i", + &req.peer_addr() + .map(|a| a.to_string()) + .unwrap_or_else(|| "unknown".to_owned()), + ) + .replace("%n", "\r\n"); + tcp_stream.write_all(subst_banner.as_bytes()).await?; + + let script = fetch_startup_script(&config_data.startup_script_file) + .map_err(|e| InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))?; rt::spawn(async move { + if session + .text( + json!({ + "RunLua": script + }) + .to_string(), + ) + .await + .is_err() + { + let _ = session.close(Some(CloseCode::Normal.into())).await; + return; + } let mut readbuf: [u8; 1024] = [0; 1024]; loop { tokio::select! { @@ -56,14 +101,68 @@ async fn ws(req: HttpRequest, body: web::Payload) -> impl Responder { Ok::(response) } -#[tokio::main] -async fn main() -> std::io::Result<()> { - env_logger::init(); - HttpServer::new(|| { - let logger = Logger::default(); - App::new().wrap(logger).service(ws) +fn extract_server_config_from_environment() -> anyhow::Result { + Ok(ServerConfig { + upstream_mud: env::var("UPSTREAM_MUD").map_err(|_| { + anyhow::Error::msg( + "Expected UPSTREAM_MUD environment variable specifying where to connect.", + ) + })?, + banner_to_mud: env::var("BANNER_TO_MUD").map_err(|_| { + anyhow::Error::msg( + "Expected BANNER_TO_MUD environment variable specifying message to send to MUD.", + ) + })?, + listen_port: env::var("LISTEN_PORT") + .map_err(|_| { + anyhow::Error::msg( + "Expected LISTEN_PORT environment variable specifying port to listen on.", + ) + }) + .and_then(|v| { + v.parse::().map_err(|_e: ParseIntError| { + anyhow::Error::msg("LISTEN_PORT should be a decimal port number") + }) + })?, + bind_address: env::var("BIND_ADDRESS").unwrap_or_else(|_| "::".to_owned()), + startup_script_file: env::var("STARTUP_SCRIPT_FILE") + .map_err(|_| { + anyhow::Error::msg( + "Expected STARTUP_SCRIPT_FILE environment variable containing filename of script to send to client.", + ) + })? +, }) - .bind(("127.0.0.1", 8124))? - .run() - .await +} + +// We load this on each connection so it can change. +fn fetch_startup_script(filename: &str) -> anyhow::Result { + Ok(fs::read_to_string(filename)?) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + + let config = extract_server_config_from_environment()?; + + let data = Data::new(config); + let config = data.get_ref(); + + // Do this early so we fail fast if config is wrong. + fetch_startup_script(&config.startup_script_file) + .context("While checking STARTUP_SCRIPT_FILE can be read")?; + + let server_data = data.clone(); + HttpServer::new(move || { + let logger = Logger::default(); + App::new() + .wrap(logger) + .app_data(server_data.clone()) + .service(ws) + }) + .bind((config.bind_address.clone(), config.listen_port))? + .run() + .await?; + Ok(()) }