From aad1583d8b5ae737bb424c461925bc69119c36e9 Mon Sep 17 00:00:00 2001 From: gennyble Date: Sun, 16 Mar 2025 13:49:12 -0500 Subject: Refactor and prepare for object loading --- corgi/Cargo.toml | 9 ++- corgi/src/caller.rs | 145 ++++++++++++++++++++++++++++++++++++++++++ corgi/src/main.rs | 178 +++++++++++++++------------------------------------- 3 files changed, 204 insertions(+), 128 deletions(-) create mode 100644 corgi/src/caller.rs diff --git a/corgi/Cargo.toml b/corgi/Cargo.toml index 5847211..0abd9df 100644 --- a/corgi/Cargo.toml +++ b/corgi/Cargo.toml @@ -1,17 +1,22 @@ [package] name = "corgi" +description = "a simple CGI server" +authors = ["gennyble "] +license = "ISC" +repository = "https://git.nyble.dev/corgi/about" +readme = "../README.md" + version = "1.0.0" edition = "2024" [dependencies] http-body-util = "0.1.3" hyper-util = { version = "0.1.10", features = ["tokio"] } +libloading = "0.8.6" regex-lite = "0.1.6" [dependencies.confindent] version = "2.2.1" -git = "https://github.com/gennyble/confindent" -branch = "v2" [dependencies.tokio] version = "1.44.0" diff --git a/corgi/src/caller.rs b/corgi/src/caller.rs new file mode 100644 index 0000000..1803ccf --- /dev/null +++ b/corgi/src/caller.rs @@ -0,0 +1,145 @@ +use std::{net::IpAddr, process::Stdio}; + +use tokio::{io::AsyncWriteExt, process::Command}; + +use crate::{Script, ScriptKind}; + +pub struct HttpRequest { + pub content_type: String, + // gateway_interface = "CGI/1.1" + pub path_info: String, + pub query_string: String, + pub remote_addr: IpAddr, + pub request_method: String, + pub script_name: String, + pub server_name: String, + pub server_port: u16, + pub server_protocol: String, + // server_version = Self::SERVER_VERSION + pub http_headers: Vec<(String, String)>, + pub body: Option>, +} + +impl HttpRequest { + pub const GATEWAY_INTERFACE: &str = "CGI/1.1"; + const SERVER_SOFTWARE: &'static str = + concat!(env!("CARGO_PKG_NAME"), '/', env!("CARGO_PKG_VERSION")); +} + +pub async fn call_and_parse_cgi(script: Script, http: HttpRequest) -> CgiResponse { + if script.kind != ScriptKind::Executable { + eprintln!("Somehow made it to executable path with module script"); + eprintln!("Script: {}", script.name); + panic!("TODO: recover") + } + + let mut cmd = Command::new(&script.filename); + cmd.env("CONTENT_TYPE", http.content_type) + .env("GATEWAY_INTERFACE", HttpRequest::GATEWAY_INTERFACE) + .env("PATH_INFO", http.path_info) + .env("QUERY_STRING", http.query_string) + .env("REMOTE_ADDR", http.remote_addr.to_string()) + .env("REQUEST_METHOD", http.request_method) + .env("SCRIPT_NAME", http.script_name) + .env("SERVER_NAME", http.server_name) + .env("SERVER_PORT", http.server_port.to_string()) + .env("SERVER_PROTOCOL", http.server_protocol) + .env("SERVER_SOFTWARE", HttpRequest::SERVER_SOFTWARE); + + http.http_headers.into_iter().for_each(|(key, val)| { + cmd.env(key, val); + }); + + // Set env specified in the conf. Be sure we do this after we + // set the HTTP headers as to overwrite any we might want + script.env.iter().for_each(|(key, val)| { + cmd.env(key.to_ascii_uppercase(), val); + }); + + let cmd = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + let output = if let Some(bytes) = http.body { + cmd.env("CONTENT_LENGTH", bytes.len().to_string()); + let mut child = cmd.stdin(Stdio::piped()).spawn().unwrap(); + + let mut cmd_stdin = child.stdin.take().unwrap(); + cmd_stdin.write_all(&bytes).await.unwrap(); + + // we might not need the explicit flush here, stdin doesn't seem + // to require it, but there used to be a BufWriter here instead + // and if you drop without a flush the buffered contents are lost, + // so it stays because i am traumatized or something. + cmd_stdin.flush().await.unwrap(); + drop(cmd_stdin); + + child.wait_with_output().await.unwrap() + } else { + cmd.spawn().unwrap().wait_with_output().await.unwrap() + }; + + parse_cgi_response(&output.stdout) +} + +fn parse_cgi_response(stdout: &[u8]) -> CgiResponse { + let mut response = CgiResponse { + // Default status code is 200 per RFC + status: 200, + headers: vec![], + body: None, + }; + + let mut curr = stdout; + loop { + // Find the newline to know where this header ends + let nl = curr.iter().position(|b| *b == b'\n').expect("no nl in header"); + let line = &curr[..nl]; + + // Find the colon to separate the key from the value + let colon = line.iter().position(|b| *b == b':').expect("no colon in header"); + let key = &line[..colon]; + let mut value = &line[colon + 1..]; + + if value[0] == b' ' { + value = &value[1..]; + } + if value[value.len().saturating_sub(1)] == b'\r' { + value = &value[..value.len().saturating_sub(1)]; + } + + response.headers.push((key.to_vec(), value.to_vec())); + + // Is this header a status line? + let key_string = String::from_utf8_lossy(key); + if key_string == "Status" { + let value_string = String::from_utf8_lossy(value); + if let Some((raw_code, _raw_msg)) = value_string.trim().split_once(' ') { + let code: u16 = raw_code.parse().unwrap(); + response.status = code; + } + } + + // Body next? + let next_nl = curr[nl + 1] == b'\n'; + let next_crlf = curr[nl + 1] == b'\r' && curr[nl + 2] == b'\n'; + if next_nl || next_crlf { + let offset = if next_nl { 2 } else { 3 }; + let body = &curr[nl + offset..]; + if body.len() > 0 { + response.body = Some(body.to_vec()); + } + + return response; + } + + // Move past the newline + curr = &curr[nl + 1..]; + } +} + +pub struct CgiResponse { + /// The Status header of the CGI response + pub status: u16, + /// Headers except "Status" + pub headers: Vec<(Vec, Vec)>, + /// CGI response body + pub body: Option>, +} diff --git a/corgi/src/main.rs b/corgi/src/main.rs index aa7bf4a..0338d0e 100644 --- a/corgi/src/main.rs +++ b/corgi/src/main.rs @@ -5,6 +5,7 @@ use std::{ time::Instant, }; +use caller::HttpRequest; use confindent::{Confindent, Value, ValueParseError}; use http_body_util::{BodyExt, Full}; use hyper::{ @@ -18,15 +19,24 @@ use hyper_util::rt::TokioIo; use regex_lite::Regex; use tokio::{io::AsyncWriteExt, net::TcpListener, process::Command, runtime::Runtime}; +mod caller; + #[derive(Clone, Debug)] pub struct Settings { port: u16, scripts: Vec