about summary refs log tree commit diff
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2025-04-04 01:45:07 -0500
committergennyble <gen@nyble.dev>2025-04-04 01:45:07 -0500
commit4410950c761970d640af00f245fdba859f8795b0 (patch)
treeae2ced50c1ea96f01eed51e1ee0394473475da81
parent31d650e75acfd447cf6c58d29ca4d6ad1010a65c (diff)
downloadcorgi-4410950c761970d640af00f245fdba859f8795b0.tar.gz
corgi-4410950c761970d640af00f245fdba859f8795b0.zip
rip out module system
-rw-r--r--Cargo.lock33
-rw-r--r--Cargo.toml2
-rw-r--r--corgi/Cargo.toml2
-rw-r--r--corgi/src/caller.rs112
-rw-r--r--corgi/src/main.rs43
-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.toml6
-rw-r--r--smalldog/README.md43
-rw-r--r--smalldog/src/lib.rs138
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);
-	}
-}