From 5752e94ad28ad0d4d5d0599f32e8f2ad795cdf77 Mon Sep 17 00:00:00 2001 From: ErrorNoInternet Date: Sat, 15 Feb 2025 22:47:22 -0500 Subject: [PATCH] feat: run http server to receive commands --- Cargo.lock | 10 ++++++ Cargo.toml | 3 ++ src/arguments.rs | 6 +++- src/events.rs | 27 +++++++++++++-- src/http.rs | 90 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 9 +++-- 6 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 src/http.rs diff --git a/Cargo.lock b/Cargo.lock index 9e29f7d..31d36a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1180,6 +1180,9 @@ dependencies = [ "azalea", "clap", "futures", + "http-body-util", + "hyper", + "hyper-util", "log", "mlua", "parking_lot", @@ -1493,6 +1496,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.6.0" @@ -1505,6 +1514,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", diff --git a/Cargo.toml b/Cargo.toml index 7c1c862..e6f67b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,9 @@ anyhow = "1" azalea = { git = "https://github.com/azalea-rs/azalea.git" } clap = { version = "4", features = ["derive"] } futures = "0" +http-body-util = "0" +hyper = { version = "1", features = ["server"] } +hyper-util = "0" log = { version = "0" } mlua = { version = "0", features = ["async", "luau", "send"] } parking_lot = { version = "0" } diff --git a/src/arguments.rs b/src/arguments.rs index eb075ab..85de8e5 100644 --- a/src/arguments.rs +++ b/src/arguments.rs @@ -1,5 +1,5 @@ use clap::Parser; -use std::path::PathBuf; +use std::{net::SocketAddr, path::PathBuf}; /// A Minecraft utility bot #[derive(Parser)] @@ -7,4 +7,8 @@ pub struct Arguments { /// Path to main Lua file #[arg(short, long)] pub script: Option, + + /// Socket address to bind HTTP server to + #[arg(short, long)] + pub address: Option, } diff --git a/src/events.rs b/src/events.rs index e720349..69a6d43 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,7 +1,10 @@ -use crate::{State, commands::CommandSource, scripting}; +use crate::{State, commands::CommandSource, http::handle, scripting}; use azalea::prelude::*; -use log::info; +use hyper::{server::conn::http1, service::service_fn}; +use hyper_util::rt::TokioIo; +use log::{error, info}; use mlua::Function; +use tokio::net::TcpListener; pub async fn handle_event(client: Client, event: Event, state: State) -> anyhow::Result<()> { let globals = state.lua.lock().globals(); @@ -40,7 +43,25 @@ pub async fn handle_event(client: Client, event: Event, state: State) -> anyhow: inner: Some(client), }, )?; - globals.get::("Init")?.call::<()>(())? + globals.get::("Init")?.call::<()>(())?; + + if let Some(address) = state.address { + let listener = TcpListener::bind(address).await?; + loop { + let (stream, _) = listener.accept().await?; + let io = TokioIo::new(stream); + + let state = state.clone(); + let service = service_fn(move |request| { + let state = state.clone(); + async move { handle(request, state).await } + }); + + if let Err(error) = http1::Builder::new().serve_connection(io, service).await { + error!("failed to serve connection: {error:?}"); + } + } + } } _ => (), }; diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 0000000..f795846 --- /dev/null +++ b/src/http.rs @@ -0,0 +1,90 @@ +use crate::State; +use http_body_util::{BodyExt, Empty, Full, combinators::BoxBody}; +use hyper::{Method, Request, Response, StatusCode, body::Bytes}; + +pub async fn handle( + request: Request, + state: State, +) -> Result>, hyper::Error> { + let path = request.uri().path().to_owned(); + + match (request.method(), path.as_str()) { + (&Method::POST, "/reload") => { + let lua = state.lua.lock(); + let config_path = match lua.globals().get::("config_path") { + Ok(path) => path, + Err(error) => { + return Ok(status_code_response( + StatusCode::INTERNAL_SERVER_ERROR, + Some(full(format!( + "failed to get config_path from lua globals: {error:?}" + ))), + )); + } + }; + if let Err(error) = match &std::fs::read_to_string(&config_path) { + Ok(string) => lua.load(string).exec(), + Err(error) => { + return Ok(status_code_response( + StatusCode::INTERNAL_SERVER_ERROR, + Some(full(format!("failed to read {config_path:?}: {error:?}"))), + )); + } + } { + return Ok(status_code_response( + StatusCode::INTERNAL_SERVER_ERROR, + Some(full(format!( + "failed to execute configuration as lua code: {error:?}" + ))), + )); + } + + Ok(Response::new(empty())) + } + + (&Method::POST, "/eval" | "/exec") => { + let bytes = request.into_body().collect().await?.to_bytes(); + let code = match std::str::from_utf8(&bytes) { + Ok(code) => code, + Err(error) => { + return Ok(status_code_response( + StatusCode::BAD_REQUEST, + Some(full(format!("invalid utf-8 data received: {error:?}"))), + )); + } + }; + let chunk = state.lua.lock().load(code); + + Ok(Response::new(full(match path.as_str() { + "/eval" => format!("{:?}", chunk.eval::()), + "/exec" => format!("{:?}", chunk.exec()), + _ => unreachable!(), + }))) + } + + (&Method::GET, "/ping") => Ok(Response::new(full("pong!"))), + + _ => Ok(status_code_response(StatusCode::NOT_FOUND, None)), + } +} + +fn status_code_response( + status_code: StatusCode, + bytes: Option>, +) -> Response> { + let mut response = Response::new(bytes.unwrap_or(empty())); + *response.status_mut() = status_code; + response +} + +fn full>(chunk: T) -> BoxBody { + Full::new(chunk.into()) + .map_err(|never| match never {}) + .boxed() +} + +fn empty() -> BoxBody { + Empty::::new() + .map_err(|never| match never {}) + .boxed() +} diff --git a/src/main.rs b/src/main.rs index 774efa7..fb1439c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod arguments; mod commands; mod events; +mod http; mod scripting; use azalea::{brigadier::prelude::CommandDispatcher, prelude::*}; @@ -11,12 +12,13 @@ use commands::{CommandSource, register}; use events::handle_event; use mlua::Lua; use parking_lot::Mutex; -use std::{path::PathBuf, process::ExitCode, sync::Arc}; +use std::{net::SocketAddr, path::PathBuf, process::ExitCode, sync::Arc}; #[derive(Default, Clone, Component)] pub struct State { - lua: Arc>, commands: Arc>>, + lua: Arc>, + address: Option, } #[tokio::main] @@ -73,8 +75,9 @@ async fn main() -> ExitCode { let Err(error) = ClientBuilder::new() .set_handler(handle_event) .set_state(State { - lua: Arc::new(Mutex::new(lua)), commands: Arc::new(commands), + lua: Arc::new(Mutex::new(lua)), + address: args.address, }) .start(account, server.as_ref()) .await;