diff options
author | gennyble <gen@nyble.dev> | 2025-04-04 01:45:07 -0500 |
---|---|---|
committer | gennyble <gen@nyble.dev> | 2025-04-04 01:45:07 -0500 |
commit | 4410950c761970d640af00f245fdba859f8795b0 (patch) | |
tree | ae2ced50c1ea96f01eed51e1ee0394473475da81 | |
parent | 31d650e75acfd447cf6c58d29ca4d6ad1010a65c (diff) | |
download | corgi-4410950c761970d640af00f245fdba859f8795b0.tar.gz corgi-4410950c761970d640af00f245fdba859f8795b0.zip |
rip out module system
-rw-r--r-- | Cargo.lock | 33 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | corgi/Cargo.toml | 2 | ||||
-rw-r--r-- | corgi/src/caller.rs | 112 | ||||
-rw-r--r-- | corgi/src/main.rs | 43 | ||||
-rw-r--r-- | corgi_stats/Cargo.toml (renamed from stats_module/Cargo.toml) | 7 | ||||
-rw-r--r-- | corgi_stats/src/main.rs (renamed from stats_module/src/lib.rs) | 62 | ||||
-rw-r--r-- | smalldog/Cargo.toml | 6 | ||||
-rw-r--r-- | smalldog/README.md | 43 | ||||
-rw-r--r-- | smalldog/src/lib.rs | 138 |
10 files changed, 51 insertions, 397 deletions
diff --git a/Cargo.lock b/Cargo.lock index 85f7e8b..eefd0c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,15 +89,21 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "libloading", "regex-lite", "rusqlite", "sha2", - "smalldog", "tokio", ] [[package]] +name = "corgi-stats" +version = "0.1.0" +dependencies = [ + "rusqlite", + "time", +] + +[[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -319,16 +325,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] -name = "libloading" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" -dependencies = [ - "cfg-if", - "windows-targets", -] - -[[package]] name = "libsqlite3-sys" version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -500,10 +496,6 @@ dependencies = [ ] [[package]] -name = "smalldog" -version = "0.1.0" - -[[package]] name = "smallvec" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -520,15 +512,6 @@ dependencies = [ ] [[package]] -name = "stats_module" -version = "0.1.0" -dependencies = [ - "rusqlite", - "smalldog", - "time", -] - -[[package]] name = "syn" version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml index a6381ad..25c0cd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["corgi", "parrot", "smalldog", "stats_module"] +members = ["corgi", "parrot", "corgi_stats"] resolver = "3" # use this profile like this: diff --git a/corgi/Cargo.toml b/corgi/Cargo.toml index 0a492c6..f3f6bda 100644 --- a/corgi/Cargo.toml +++ b/corgi/Cargo.toml @@ -10,11 +10,9 @@ version = "1.0.0" edition = "2024" [dependencies] -smalldog = { path = "../smalldog" } base64 = "0.22.1" http-body-util = "0.1.3" hyper-util = { version = "0.1.10", features = ["tokio"] } -libloading = "0.8.6" regex-lite = "0.1.6" rusqlite = { version = "0.34.0", features = ["bundled"] } sha2 = "0.10.8" diff --git a/corgi/src/caller.rs b/corgi/src/caller.rs index 7014d22..29be5ca 100644 --- a/corgi/src/caller.rs +++ b/corgi/src/caller.rs @@ -1,20 +1,8 @@ -use std::{ - ffi::{self, CString}, - marker::PhantomData, - net::IpAddr, - process::Stdio, - ptr, - str::FromStr, -}; +use std::{net::IpAddr, process::Stdio}; -use smalldog::ffi::{ModuleRequest, ModuleResponse}; -use tokio::{ - io::AsyncWriteExt, - process::Command, - sync::oneshot::{self, Sender}, -}; +use tokio::{io::AsyncWriteExt, process::Command}; -use crate::{Script, ScriptKind}; +use crate::Script; pub struct HttpRequest { pub content_type: String, @@ -63,12 +51,6 @@ impl HttpRequest { } 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); // Set env specified in the conf. Be sure we do this after we @@ -169,91 +151,3 @@ pub struct CgiResponse { /// CGI response body pub body: Option<Vec<u8>>, } - -type HandleFn = unsafe extern "C" fn(*const ModuleRequest) -> *const ModuleResponse; -type CleanupFn = unsafe extern "C" fn(*const ModuleResponse); - -pub async fn call_and_parse_module(script: Script, req: HttpRequest) -> CgiResponse { - let (tx, rx) = oneshot::channel(); - std::thread::spawn(move || unsafe { module_thread(script, req, tx) }); - - rx.await.unwrap() -} - -unsafe fn module_thread(script: Script, req: HttpRequest, tx: Sender<CgiResponse>) { - let env: Vec<(String, String)> = req - .build_kv() - .into_iter() - .chain(req.http_headers.into_iter()) - .chain(script.env.into_iter()) - .collect(); - - let mut headers_owned = vec![]; - for (k, v) in env { - headers_owned.push([ - CString::from_str(k.as_str()).unwrap(), - CString::from_str(v.as_str()).unwrap(), - ]); - } - - let headers: Vec<[*const ffi::c_char; 2]> = - headers_owned.iter().map(|kvarr| [kvarr[0].as_ptr(), kvarr[1].as_ptr()]).collect(); - - let modreq = ModuleRequest { - headers_len: headers.len() as u64, - headers: headers[..].as_ptr(), - body_len: req.body.as_ref().map(|v| v.len()).unwrap_or(0) as u64, - body: req.body.as_ref().map(|v| v.as_ptr()).unwrap_or(ptr::null()), - _phantom: PhantomData::default(), - }; - - let mut cgi = CgiResponse { - status: 200, - headers: vec![], - body: None, - }; - - // Since we can only load a dynamic library once, block other requests so - // they do not return the same handle. per dlopen docs: - // > If the same shared object is opened again with dlopen(), - // > the same object handle is returned. - let lock = match script.module_lock.lock() { - Ok(lock) => lock, - Err(poison) => { - eprintln!("!!! mutex for {} was poisoned!", script.name); - - // get the guard - poison.into_inner() - } - }; - - unsafe { - let lib = libloading::Library::new(script.filename).unwrap(); - let handle: libloading::Symbol<HandleFn> = lib.get(b"cgi_handle").unwrap(); - let free: libloading::Symbol<CleanupFn> = lib.get(b"cgi_cleanup").unwrap(); - - let response = handle((&modreq) as *const ModuleRequest); - let response_ref = response.as_ref().unwrap(); - - let headers_ffi = - std::slice::from_raw_parts(response_ref.headers, response_ref.headers_len as usize); - - for pair in headers_ffi { - let k = ffi::CStr::from_ptr(pair[0]).to_string_lossy(); - let v = ffi::CStr::from_ptr(pair[1]).to_string_lossy(); - - cgi.headers.push((k.as_bytes().to_vec(), v.as_bytes().to_vec())); - } - - let maybe_body: Option<Vec<u8>> = response_ref - .body - .as_ref() - .map(|b| std::slice::from_raw_parts(b, response_ref.body_len as usize).to_vec()); - cgi.body = maybe_body; - - free(response); - }; - - drop(lock); - tx.send(cgi).unwrap() -} diff --git a/corgi/src/main.rs b/corgi/src/main.rs index fb6b75a..b8b23de 100644 --- a/corgi/src/main.rs +++ b/corgi/src/main.rs @@ -2,9 +2,7 @@ use std::{ net::{IpAddr, SocketAddr}, path::PathBuf, pin::Pin, - sync::{Arc, Mutex}, - thread::JoinHandle, - time::Instant, + sync::Arc, }; use caller::HttpRequest; @@ -32,25 +30,8 @@ pub struct Settings { } #[derive(Clone, Debug)] -pub enum ScriptKind { - Executable, - Module { thread: Arc<Option<JoinHandle<()>>> }, -} - -impl ScriptKind { - pub fn is_executable(&self) -> bool { - if let ScriptKind::Executable = self { - true - } else { - false - } - } -} - -#[derive(Clone, Debug)] pub struct Script { name: String, - kind: ScriptKind, regex: Option<Regex>, filename: String, env: Vec<(String, String)>, @@ -105,21 +86,8 @@ fn parse_script_conf(conf: &Value) -> Script { }, }; - let kind = match conf.get("Type") { - None => ScriptKind::Executable, - Some("executable") => ScriptKind::Executable, - Some("object") => ScriptKind::Module { - thread: Arc::new(None), - }, - Some(kind) => { - eprintln!("'{kind}' is not a valid script type"); - std::process::exit(1) - } - }; - Script { name, - kind, regex, filename, env: env.unwrap_or_default(), @@ -176,8 +144,6 @@ impl Svc { caddr: SocketAddr, req: Request<Incoming>, ) -> Response<Full<Bytes>> { - let start = Instant::now(); - // Collect things we need from the request before we eat it's body let method = req.method().as_str().to_ascii_uppercase(); let version = req.version(); @@ -251,12 +217,7 @@ impl Svc { body: if content_length > 0 { Some(body) } else { None }, }; - let cgi_response = match script.kind { - ScriptKind::Executable => { - caller::call_and_parse_cgi(script.clone(), http_request).await - } - ScriptKind::Object => caller::call_and_parse_module(script.clone(), http_request).await, - }; + let cgi_response = caller::call_and_parse_cgi(script.clone(), http_request).await; let status = StatusCode::from_u16(cgi_response.status).unwrap(); let mut response = Response::builder().status(status); diff --git a/stats_module/Cargo.toml b/corgi_stats/Cargo.toml index 95b6d41..a012b94 100644 --- a/stats_module/Cargo.toml +++ b/corgi_stats/Cargo.toml @@ -1,13 +1,8 @@ [package] -name = "stats_module" +name = "corgi-stats" version = "0.1.0" edition = "2024" -[lib] -name = "stats" -crate-type = ["cdylib"] - [dependencies] rusqlite = { version = "0.34.0", features = ["bundled", "time"] } time = "0.3.40" -smalldog = { path = "../smalldog" } diff --git a/stats_module/src/lib.rs b/corgi_stats/src/main.rs index ba9d199..1097685 100644 --- a/stats_module/src/lib.rs +++ b/corgi_stats/src/main.rs @@ -1,26 +1,37 @@ use rusqlite::{Connection, params}; -use smalldog::{Request, Response, ffi}; +use std::time::Instant; use time::{Duration, OffsetDateTime}; -#[unsafe(no_mangle)] -extern "C" fn cgi_handle(req: *const ffi::ModuleRequest) -> *const ffi::ModuleResponse { - let mut response = Response::new(); - let mut body = String::new(); +// Thank you, cat, for optimizing my query +const TOP_TEN_ALL_TIME: &str = "SELECT reqs.cnt, agents.agent +FROM agents +JOIN ( + SELECT count(id) as cnt, agent_id + FROM requests + GROUP BY agent_id +) reqs +ON reqs.agent_id=agents.id +ORDER BY reqs.cnt DESC LIMIT 10;"; - let request = Request::from_mod_request(req); - let db = if let Some(path) = request.header("CORGI_STATS_DB") { +fn main() { + let db_path = std::env::var("CORGI_STATS_DB").ok(); + let db = if let Some(path) = db_path { if let Ok(db) = Connection::open(path) { db } else { - return make_error(500, "failed to open database"); + error_and_die(500, "failed to open database"); } } else { - return make_error(500, "could not open stats database"); + error_and_die(500, "database key not set"); }; + let mut body = String::new(); + let now = OffsetDateTime::now_utc(); let fifteen_ago = now - Duration::minutes(15); + let start = Instant::now(); + let query = "SELECT count(requests.id) AS request_count, agents.agent FROM requests \ INNER JOIN agents ON requests.agent_id = agents.id \ WHERE requests.timestamp > ?1 \ @@ -35,11 +46,7 @@ extern "C" fn cgi_handle(req: *const ffi::ModuleRequest) -> *const ffi::ModuleRe agents.sort_by(|a, b| a.0.cmp(&b.0).reverse()); - let highest_five_query = "SELECT count(requests.id) AS request_count, agents.agent FROM requests \ - INNER JOIN agents ON requests.agent_id = agents.id \ - GROUP BY requests.agent_id \ - ORDER BY request_count DESC LIMIT 10;"; - let mut prepared = db.prepare(highest_five_query).unwrap(); + let mut prepared = db.prepare(TOP_TEN_ALL_TIME).unwrap(); let highest_five: Vec<(usize, String)> = prepared .query_map(params![], |row| Ok((row.get(0)?, row.get(1)?))) .unwrap() @@ -67,19 +74,22 @@ extern "C" fn cgi_handle(req: *const ffi::ModuleRequest) -> *const ffi::ModuleRe } body.push_str("</pre></code></p>"); - response.body(body.into_bytes()).header(c"Content-Type", c"text/html"); - response.into_mod_response(200) -} - -fn make_error<S: AsRef<str>>(code: u16, msg: S) -> *const ffi::ModuleResponse { - let mut response = Response::new(); - response.header(c"Content-Length", c"text/html"); - response.body(msg.as_ref().as_bytes().to_vec()); + body.push_str(&format!("<p>took {}ms</p>", start.elapsed().as_millis())); - response.into_mod_response(code) + println!("Status: 200\nContent-Type: text/html\n"); + println!("{body}"); } -#[unsafe(no_mangle)] -extern "C" fn cgi_cleanup(response: *const ffi::ModuleResponse) { - Response::cleanup(response); +fn error_and_die<S: Into<String>>(status: u16, msg: S) -> ! { + println!("Status: {status}"); + println!("Content-Type: text/html\n"); + println!("<html>"); + println!("\t<head><title>{status}</title></head>"); + println!("\t<body style='width: 20rem; padding: 0px; margin: 2rem;'>"); + println!("\t\t<h1>{status}</h1>"); + println!("\t\t<hr/>"); + println!("\t\t<p>{}</p>", msg.into()); + println!("\t</body>\n</html>"); + + std::process::exit(0); } diff --git a/smalldog/Cargo.toml b/smalldog/Cargo.toml deleted file mode 100644 index 6ae5cc4..0000000 --- a/smalldog/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "smalldog" -version = "0.1.0" -edition = "2024" - -[dependencies] diff --git a/smalldog/README.md b/smalldog/README.md deleted file mode 100644 index 2505f5f..0000000 --- a/smalldog/README.md +++ /dev/null @@ -1,43 +0,0 @@ -smalldog - -a crate for safely working with corgi's cgi module system. - -module's are loaded in a new thread at the time of request. - -if you want to build a module in C, these structs will be of use: -```c -// In Rust this is interpreted as a fixed size, 2-element array of char* -// but i cannot get C to like that kind of at all. sorry. -struct pair { - char *name; - char *value; -}; - -// The request from corgi. body may be null if one was not sent. -struct request { - u64_t headers_len; - struct pair *headers; - u64_t body_len; - u8_t *body; -}; - -// The request your module should return to corgi -struct response { - u16_t status; - u64_t headers_len; - struct pair *headers; - u64_t body_len; - u8_t *body; -}; -``` - -as well as that, there are two functions that corgi expects to exist and will call. - -`struct response *cgi_handle(struct request*);` -this function is called after the module is loaded. here you should process the -request and send back a response. be sure to keep track of any allocations so -you can clean them up later. - -`void cgi_cleanup(struct response*);` -this is where you clean up, later. function is called after corgi copies all -required data to an internal structure. free memory here. \ No newline at end of file diff --git a/smalldog/src/lib.rs b/smalldog/src/lib.rs deleted file mode 100644 index 1b1ddbb..0000000 --- a/smalldog/src/lib.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::{ - borrow::Cow, - ffi::{CStr, c_void}, -}; - -use ffi::{ModuleRequest, ModuleResponse}; - -pub mod ffi { - use core::ffi; - use std::marker::PhantomData; - - #[repr(C)] - pub struct ModuleRequest<'req> { - pub headers_len: u64, - pub headers: *const [*const ffi::c_char; 2], - pub body_len: u64, - pub body: *const u8, - pub _phantom: PhantomData<&'req u8>, - } - - #[repr(C)] - pub struct ModuleResponse { - pub status: u16, - pub headers_len: u64, - pub headers: *const [*const ffi::c_char; 2], - pub body_len: u64, - pub body: *const u8, - /// The etc field is not read by corgi and exists so you may - /// associate data with a request - pub etc: *const ffi::c_void, - } -} - -pub struct Request<'req> { - headers: Vec<(Cow<'req, str>, Cow<'req, str>)>, - body: Option<&'req [u8]>, -} - -impl<'req> Request<'req> { - pub fn from_mod_request(request: *const ModuleRequest<'req>) -> Self { - // SAFTEY: corgi will never give us a null pointer - let reqref = unsafe { request.as_ref() }.unwrap(); - - let headers_ffi = - unsafe { std::slice::from_raw_parts(reqref.headers, reqref.headers_len as usize) }; - - let mut headers = vec![]; - for pair in headers_ffi { - let k = unsafe { CStr::from_ptr(pair[0]) }.to_string_lossy(); - let v = unsafe { CStr::from_ptr(pair[1]) }.to_string_lossy(); - headers.push((k, v)); - } - - let body = if reqref.body.is_null() { - None - } else { - Some(unsafe { std::slice::from_raw_parts(reqref.body, reqref.body_len as usize) }) - }; - - Self { headers, body } - } - - pub fn header(&self, key: &str) -> Option<&str> { - for (hkey, hval) in &self.headers { - if hkey == key { - return Some(hval); - } - } - - None - } - - pub fn headers(&self) -> &[(Cow<str>, Cow<str>)] { - &self.headers - } - - pub fn body(&self) -> Option<&[u8]> { - self.body - } -} - -pub struct Response { - headers: Vec<(Cow<'static, CStr>, Cow<'static, CStr>)>, - ffi_headers: Vec<[*const i8; 2]>, - body: Vec<u8>, -} - -impl Response { - pub fn new() -> Self { - Self { - headers: vec![], - ffi_headers: vec![], - body: vec![], - } - } - - pub fn into_mod_response(self, status: u16) -> *const ModuleResponse { - let mut boxed_self = Box::new(self); - - for (key, value) in boxed_self.headers.iter() { - let ffi_pair = [key.as_ptr(), value.as_ptr()]; - boxed_self.ffi_headers.push(ffi_pair); - } - - let boxed = Box::new(ModuleResponse { - status, - headers_len: boxed_self.ffi_headers.len() as u64, - headers: boxed_self.ffi_headers.as_ptr(), - body_len: boxed_self.body.len() as u64, - body: boxed_self.body.as_ptr(), - etc: Box::<Response>::into_raw(boxed_self) as *const c_void, - }); - - Box::<ModuleResponse>::into_raw(boxed) - } - - pub fn header<K: Into<Cow<'static, CStr>>, V: Into<Cow<'static, CStr>>>( - &mut self, - key: K, - value: V, - ) -> &mut Self { - self.headers.push((key.into(), value.into())); - self - } - - pub fn body(&mut self, vec: Vec<u8>) -> &mut Self { - self.body = vec; - self - } - - pub fn cleanup(response: *const ModuleResponse) { - let boxed = unsafe { Box::from_raw(response as *mut ModuleResponse) }; - let etc = unsafe { Box::from_raw(boxed.etc as *mut Response) }; - - drop(boxed); - drop(etc); - } -} |