about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock7
-rw-r--r--Cargo.toml11
-rw-r--r--README.md10
-rw-r--r--corgi.conf16
-rw-r--r--src/main.rs169
5 files changed, 184 insertions, 29 deletions
diff --git a/Cargo.lock b/Cargo.lock
index f7f322a..a379ab8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -57,6 +57,7 @@ dependencies = [
  "http-body-util",
  "hyper",
  "hyper-util",
+ "regex-lite",
  "tokio",
 ]
 
@@ -245,6 +246,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
 [[package]]
+name = "regex-lite"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
+
+[[package]]
 name = "rustc-demangle"
 version = "0.1.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index f53fcbc..fa58787 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,10 +4,10 @@ version = "0.1.0"
 edition = "2024"
 
 [profile.release]
-strip = true
-opt-level = "z"
-lto = true
-codegen-units = 1
+#strip = true
+#opt-level = "z"
+#lto = true
+#codegen-units = 1
 
 # 1538792 none of the above flags
 #  829896 the above flags reduced binary size by around 700K :)
@@ -15,6 +15,7 @@ codegen-units = 1
 [dependencies]
 http-body-util = "0.1.3"
 hyper-util = { version = "0.1.10", features = ["tokio"] }
+regex-lite = "0.1.6"
 
 [dependencies.confindent]
 version = "2.2.1"
@@ -23,7 +24,7 @@ branch = "v2"
 
 [dependencies.tokio]
 version = "1.44.0"
-features = ["rt", "rt-multi-thread", "io-std", "net", "process"]
+features = ["rt", "rt-multi-thread", "io-std", "io-util", "net", "process"]
 
 [dependencies.hyper]
 version = "1.6.0"
diff --git a/README.md b/README.md
index ef2dd2b..47b3449 100644
--- a/README.md
+++ b/README.md
@@ -8,12 +8,22 @@ corgi listens on port 26744 by default.
 
 `/etc/corgi.conf`
 ```
+Server
+	Port 26744
+
+Script <path-to-cgi-script>
+	Match
+		Regex <regular-expression>
+
 Script <path-to-cgi-script>
 	Environment
 		HTTP_HOST <hostname>
 		ENV_KEY <some-env-value>
 ```
 
+Scripts are tried in order, looking for one that matches. If none match,
+the first script is ran.
+
 Sets the following environmental variables for the CGI script, many following [RFC 3875][rfc]:  
 - **`GATEWAY_INTERFACE`** to the fixed value `CGI/1.1`  
 - **`PATH_INFO`** to the HTTP path the client requested  
diff --git a/corgi.conf b/corgi.conf
index fb3e7ad..207acc7 100644
--- a/corgi.conf
+++ b/corgi.conf
@@ -1,3 +1,17 @@
-Script /usr/lib/cgit/cgit.cgi
+Server
+	Port 26744
+
+Script git-backend
+	Path /usr/lib/git-core/git-http-backend
+	Match
+		Regex /.+/(info/refs|git-upload-pack)
+	Environment
+		GIT_HTTP_EXPORT_ALL 1
+		GIT_PROJECT_ROOT /srv/git
+		HOME /srv/git
+		HTTP_HOST git.nyble.dev
+
+Script cgit
+	Path /usr/lib/cgit/cgit.cgi
 	Environment
 		HTTP_HOST git.nyble.dev
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index 0493fdb..9831331 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,25 +1,39 @@
 use std::{
 	net::{IpAddr, SocketAddr},
 	pin::Pin,
+	process::Stdio,
 	time::Instant,
 };
 
-use confindent::{Confindent, ValueParseError};
+use confindent::{Confindent, Value, ValueParseError};
 use http_body_util::{BodyExt, Full};
 use hyper::{
 	HeaderMap, Request, Response, StatusCode,
 	body::{Bytes, Incoming},
-	header::HeaderValue,
+	header::{self, HeaderValue},
 	server::conn::http1,
 	service::Service,
 };
 use hyper_util::rt::TokioIo;
-use tokio::{net::TcpListener, process::Command, runtime::Runtime};
+use regex_lite::Regex;
+use tokio::{
+	io::{AsyncWriteExt, BufWriter},
+	net::TcpListener,
+	process::Command,
+	runtime::Runtime,
+};
 
 #[derive(Clone, Debug)]
 pub struct Settings {
 	port: u16,
-	script_filename: String,
+	scripts: Vec<Script>,
+}
+
+#[derive(Clone, Debug)]
+pub struct Script {
+	name: String,
+	regex: Option<Regex>,
+	filename: String,
 	env: Vec<(String, String)>,
 }
 
@@ -29,15 +43,9 @@ fn main() {
 	let conf_path = std::env::args().nth(1).unwrap_or(String::from(CONF_DEFAULT));
 	let conf = Confindent::from_file(conf_path).expect("failed to open conf");
 
-	let script = conf.child("Script").expect("no 'Script' key in conf");
-	let environment = script.child("Environment");
-	let env = environment
-		.map(|e| e.values().map(|v| (v.key_owned(), v.value_owned().unwrap())).collect());
-
 	let mut settings = Settings {
 		port: 26744,
-		script_filename: script.value_owned().expect("'Script' key has no value'"),
-		env: env.unwrap_or_default(),
+		scripts: conf.children("Script").into_iter().map(parse_script_conf).collect(),
 	};
 
 	if let Some(server) = conf.child("Server") {
@@ -55,6 +63,32 @@ fn main() {
 	rt.block_on(async { run(settings).await });
 }
 
+fn parse_script_conf(conf: &Value) -> Script {
+	let name = conf.value_owned().expect("Missing value for 'Script' key");
+	let filename = conf.child_owned("Path").expect("Missing 'Path' key");
+	let environment = conf.child("Environment");
+	let env = environment
+		.map(|e| e.values().map(|v| (v.key_owned(), v.value_owned().unwrap())).collect());
+
+	let regex = match conf.get("Match/Regex") {
+		None => None,
+		Some(restr) => match Regex::new(restr) {
+			Err(err) => {
+				eprintln!("Failed to compile regex: {restr}\nerror: {err}");
+				std::process::exit(1);
+			}
+			Ok(re) => Some(re),
+		},
+	};
+
+	Script {
+		name,
+		regex,
+		filename,
+		env: env.unwrap_or_default(),
+	}
+}
+
 // We have tokio::main at home :)
 async fn run(settings: Settings) {
 	let addr = SocketAddr::from(([0, 0, 0, 0], settings.port));
@@ -112,6 +146,29 @@ impl Svc {
 		let body = req.into_body().collect().await.unwrap().to_bytes();
 		let content_length = body.len();
 
+		let mut script = settings.scripts[0].clone();
+
+		for set_script in settings.scripts {
+			if let Some(regex) = set_script.regex.as_ref() {
+				if regex.is_match(&path) {
+					script = set_script;
+					break;
+				}
+			} else {
+				script = set_script;
+			}
+		}
+
+		let content_type = headers
+			.get("content-type")
+			.map(|s| s.to_str().ok())
+			.flatten()
+			.unwrap_or_default()
+			.to_owned();
+
+		println!("!!! new request. type {content_type} // {method}");
+		println!("!!! {path}?{query}");
+
 		let uagent = headers
 			.get("user-agent")
 			.map(|s| s.to_str().ok())
@@ -133,13 +190,14 @@ impl Svc {
 			.to_str()
 			.expect("failed to decode http host as string");
 
-		let mut cmd = Command::new(&settings.script_filename);
-		cmd.env("GATEWAY_INTERFACE", "CGI/1.1")
-			.env("PATH_INFO", path)
+		let mut cmd = Command::new(&script.filename);
+		cmd.env("CONTENT_TYPE", content_type)
+			.env("GATEWAY_INTERFACE", "CGI/1.1")
+			.env("PATH_INFO", &path)
 			.env("QUERY_STRING", query)
 			.env("REMOTE_ADDR", client_addr.to_string())
 			.env("REQUEST_METHOD", method)
-			.env("SCRIPT_NAME", settings.script_filename)
+			.env("SCRIPT_NAME", script.filename)
 			.env("SERVER_NAME", server_name)
 			.env("SERVER_PORT", settings.port.to_string())
 			.env("SERVER_PROTOCOL", format!("{:?}", version))
@@ -154,12 +212,21 @@ impl Svc {
 
 		// Set env specified in the conf. Be sure we do this after we
 		// set the HTTP headers as to overwrite any we might want
-		for (key, value) in &settings.env {
+		for (key, value) in &script.env {
 			cmd.env(key.to_ascii_uppercase(), value);
 		}
 
+		let debugcgi = script.name == "git-backend";
+
+		let cgibody = if content_length > 0 {
+			Some(&body)
+		} else {
+			None
+		};
+
 		let start_cgi = Instant::now();
-		let cgi_response = Self::call_and_parse_cgi(cmd).await;
+		let cgi_response =
+			Self::call_and_parse_cgi(cmd, cgibody, caddr.ip(), debugcgi, &path).await;
 		let cgi_time = start_cgi.elapsed();
 
 		let status = StatusCode::from_u16(cgi_response.status).unwrap();
@@ -170,7 +237,8 @@ impl Svc {
 		}
 
 		println!(
-			"served to [{client_addr}]\n\tcgi took {}ms. total time {}ms\n\tUA: {uagent}",
+			"served to [{client_addr}]\n\tscript: {}\n\tpath: {path}\n\tcgi took {}ms. total time {}ms\n\tUA: {uagent}",
+			&script.name,
 			cgi_time.as_millis(),
 			start.elapsed().as_millis()
 		);
@@ -214,7 +282,13 @@ impl Svc {
 		}
 	}
 
-	async fn call_and_parse_cgi(mut cmd: Command) -> CgiResponse {
+	async fn call_and_parse_cgi(
+		mut cmd: Command,
+		body: Option<&Bytes>,
+		caddr: IpAddr,
+		debug: bool,
+		path: &str,
+	) -> CgiResponse {
 		let mut response = CgiResponse {
 			// Default status code is 200 per RFC
 			status: 200,
@@ -222,9 +296,35 @@ impl Svc {
 			body: None,
 		};
 
-		let output = cmd.output().await.unwrap();
+		println!("!!! before spawn: {path}");
+		let cmd = cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
+		let output = if let Some(bytes) = body {
+			println!("!!! has body len={}", bytes.len());
+			let mut child = cmd.stdin(Stdio::piped()).spawn().unwrap();
+
+			let cmd_stdin = child.stdin.take().unwrap();
+			let mut bufwrite = BufWriter::new(cmd_stdin);
+			bufwrite.write_all(bytes).await.unwrap();
+
+			drop(bufwrite);
+			println!("!!! after drop ({path})");
+
+			child.wait_with_output().await.unwrap()
+		} else {
+			cmd.spawn().unwrap().wait_with_output().await.unwrap()
+		};
+		println!("!!! after spawn ({path})");
+
 		let response_raw = output.stdout;
 
+		if debug {
+			std::fs::write(
+				format!("/tmp/{caddr}-gitbackend-{}", path_to_name(path)),
+				&response_raw,
+			)
+			.unwrap();
+		}
+
 		let mut curr = response_raw.as_slice();
 		loop {
 			// Find the newline to know where this header ends
@@ -234,7 +334,15 @@ impl Svc {
 			// 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 value = &line[colon + 1..];
+			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?
@@ -248,12 +356,16 @@ impl Svc {
 			}
 
 			// Body next?
-			if curr[nl + 1] == b'\n' || (curr[nl + 1] == b'\r' && curr[nl + 2] == b'\n') {
-				let body = &curr[nl + 2..];
+			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());
 				}
 
+				println!("!!! before call_and_parse return ({path})");
 				return response;
 			}
 
@@ -271,3 +383,14 @@ struct CgiResponse {
 	/// CGI response body
 	body: Option<Vec<u8>>,
 }
+
+fn path_to_name(path: &str) -> String {
+	let mut ret = String::with_capacity(path.len());
+	for ch in path.chars() {
+		match ch {
+			'/' => ret.push('-'),
+			ch => ret.push(ch),
+		}
+	}
+	ret
+}