diff options
Diffstat (limited to 'src/main.rs')
-rw-r--r-- | src/main.rs | 169 |
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 +} |