about summary refs log tree commit diff
path: root/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs169
1 files changed, 146 insertions, 23 deletions
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
+}