diff options
Diffstat (limited to 'corgi/src/main.rs')
-rw-r--r-- | corgi/src/main.rs | 178 |
1 files changed, 52 insertions, 126 deletions
diff --git a/corgi/src/main.rs b/corgi/src/main.rs index aa7bf4a..0338d0e 100644 --- a/corgi/src/main.rs +++ b/corgi/src/main.rs @@ -5,6 +5,7 @@ use std::{ time::Instant, }; +use caller::HttpRequest; use confindent::{Confindent, Value, ValueParseError}; use http_body_util::{BodyExt, Full}; use hyper::{ @@ -18,15 +19,24 @@ use hyper_util::rt::TokioIo; use regex_lite::Regex; use tokio::{io::AsyncWriteExt, net::TcpListener, process::Command, runtime::Runtime}; +mod caller; + #[derive(Clone, Debug)] pub struct Settings { port: u16, scripts: Vec<Script>, } +#[derive(Clone, Debug, PartialEq)] +pub enum ScriptKind { + Executable, + Object, +} + #[derive(Clone, Debug)] pub struct Script { name: String, + kind: ScriptKind, regex: Option<Regex>, filename: String, env: Vec<(String, String)>, @@ -76,8 +86,19 @@ fn parse_script_conf(conf: &Value) -> Script { }, }; + let kind = match conf.get("Type") { + None => ScriptKind::Executable, + Some("executable") => ScriptKind::Executable, + Some("object") => ScriptKind::Object, + Some(kind) => { + eprintln!("'{kind}' is not a valid script type"); + std::process::exit(1) + } + }; + Script { name, + kind, regex, filename, env: env.unwrap_or_default(), @@ -138,22 +159,31 @@ impl Svc { let path = req.uri().path().to_owned(); let query = req.uri().query().unwrap_or_default().to_owned(); let headers = req.headers().clone(); - let body = req.into_body().collect().await.unwrap().to_bytes(); - let content_length = body.len(); - let mut script = settings.scripts[0].clone(); + let body = req.into_body().collect().await.unwrap().to_bytes().to_vec(); + let content_length = body.len(); + let mut maybe_script = None; for set_script in settings.scripts { if let Some(regex) = set_script.regex.as_ref() { if regex.is_match(&path) { - script = set_script; + maybe_script = Some(set_script); break; } } else { - script = set_script; + maybe_script = Some(set_script); + break; } } + let script = match maybe_script { + Some(script) => script, + None => { + eprintln!("path didn't match any script"); + panic!("TODO recover?"); + } + }; + let content_type = headers .get("content-type") .map(|s| s.to_str().ok()) @@ -182,42 +212,22 @@ impl Svc { .to_str() .expect("failed to decode http host as string"); - 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", script.filename) - .env("SERVER_NAME", server_name) - .env("SERVER_PORT", settings.port.to_string()) - .env("SERVER_PROTOCOL", format!("{:?}", version)) - .env("SERVER_SOFTWARE", Self::SERVER_SOFTWARE); - - if content_length > 0 { - cmd.env("CONTENT_LENGTH", content_length.to_string()); - } - - // Set env associated with the HTTP request headers - Self::set_http_env(headers, &mut cmd); - - // 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 &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 http_request = HttpRequest { + content_type, + path_info: path.clone(), + query_string: query, + remote_addr: client_addr, + request_method: method, + script_name: script.filename.to_owned(), + server_name: server_name.to_owned(), + server_port: settings.port, + server_protocol: format!("{:?}", version), + http_headers: Self::build_http_vec(headers), + body: if content_length > 0 { Some(body) } else { None }, }; let start_cgi = Instant::now(); - let cgi_response = Self::call_and_parse_cgi(cmd, cgibody).await; + let cgi_response = caller::call_and_parse_cgi(script.clone(), http_request).await; let cgi_time = start_cgi.elapsed(); let status = StatusCode::from_u16(cgi_response.status).unwrap(); @@ -242,10 +252,9 @@ impl Svc { maybe_hval.map(|h| h.to_str().ok()).flatten().map(|s| s.parse().ok()).flatten() } - const SERVER_SOFTWARE: &'static str = - concat!(env!("CARGO_PKG_NAME"), '/', env!("CARGO_PKG_VERSION")); + fn build_http_vec(headers: HeaderMap) -> Vec<(String, String)> { + let mut ret = vec![]; - fn set_http_env(headers: HeaderMap, cmd: &mut Command) { for (key, value) in headers.iter() { let key_str = key.as_str(); @@ -264,101 +273,18 @@ impl Svc { match value.to_str() { Ok(val_str) => { - cmd.env(key_upper, val_str); + ret.push((key_upper, val_str.to_owned())); } Err(err) => { eprintln!("value for header {key_str} is not a string: {err}") } } } - } - - async fn call_and_parse_cgi(mut cmd: Command, body: Option<&Bytes>) -> CgiResponse { - let mut response = CgiResponse { - // Default status code is 200 per RFC - status: 200, - headers: vec![], - body: None, - }; - - let cmd = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); - let output = if let Some(bytes) = body { - let mut child = cmd.stdin(Stdio::piped()).spawn().unwrap(); - let mut cmd_stdin = child.stdin.take().unwrap(); - cmd_stdin.write_all(bytes).await.unwrap(); - - // we might not need the explicit flush here, stdin doesn't seem - // to require it, but there used to be a BufWriter here instead - // and if you drop without a flush the buffered contents are lost, - // so it stays because i am traumatized or something. - cmd_stdin.flush().await.unwrap(); - drop(cmd_stdin); - - child.wait_with_output().await.unwrap() - } else { - cmd.spawn().unwrap().wait_with_output().await.unwrap() - }; - - let response_raw = output.stdout; - let mut curr = response_raw.as_slice(); - loop { - // Find the newline to know where this header ends - let nl = curr.iter().position(|b| *b == b'\n').expect("no nl in header"); - let line = &curr[..nl]; - - // 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 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? - let key_string = String::from_utf8_lossy(key); - if key_string == "Status" { - let value_string = String::from_utf8_lossy(value); - if let Some((raw_code, _raw_msg)) = value_string.trim().split_once(' ') { - let code: u16 = raw_code.parse().unwrap(); - response.status = code; - } - } - - // Body next? - 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()); - } - - return response; - } - - // Move past the newline - curr = &curr[nl + 1..]; - } + ret } } -struct CgiResponse { - /// The Status header of the CGI response - status: u16, - /// Headers except "Status" - headers: Vec<(Vec<u8>, Vec<u8>)>, - /// 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() { |