use core::panic; use std::{net::SocketAddr, pin::Pin}; use confindent::Confindent; use http_body_util::{BodyExt, Full}; use hyper::{ HeaderMap, Request, Response, StatusCode, body::{Body, Bytes, Incoming}, server::conn::http1, service::Service, }; use hyper_util::rt::TokioIo; use tokio::{net::TcpListener, process::Command, runtime::Runtime}; #[derive(Clone, Debug)] pub struct Settings { script_filename: String, env: Vec<(String, String)>, } const CONF_DEFAULT: &str = "/etc/corgi.conf"; 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 settings = Settings { script_filename: script.value_owned().expect("'Script' key has no value'"), env: env.unwrap_or_default(), }; let rt = Runtime::new().unwrap(); rt.block_on(async { run(settings).await }); } async fn run(settings: Settings) { let addr = SocketAddr::from(([0, 0, 0, 0], 2562)); let listen = TcpListener::bind(addr).await.unwrap(); let svc = Svc { settings }; loop { let (stream, _caddr) = listen.accept().await.unwrap(); let io = TokioIo::new(stream); let svc_clone = svc.clone(); tokio::task::spawn( async move { http1::Builder::new().serve_connection(io, svc_clone).await }, ); } } #[derive(Clone, Debug)] struct Svc { settings: Settings, } impl Service> for Svc { type Response = Response>; type Error = hyper::Error; type Future = Pin> + Send>>; fn call(&self, req: Request) -> Self::Future { fn make>(b: B) -> Result>, hyper::Error> { Ok(Response::builder().body(Full::new(b.into())).unwrap()) } let settings = self.settings.clone(); Box::pin(async { Ok(Self::handle(settings, req).await) }) } } impl Svc { async fn handle(settings: Settings, req: Request) -> Response> { let method = req.method().as_str().to_ascii_uppercase(); 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(); // Not setting PATH_TRANSLATED let mut cmd = Command::new(&settings.script_filename); cmd.env("GATEWAY_INTERFACE", "CGI/1.1") .env("SCRIPT_NAME", settings.script_filename) .env("PATH_INFO", path) .env("QUERY_STRING", query) .env("REQUEST_METHOD", method); // 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 &settings.env { cmd.env(key.to_ascii_uppercase(), value); } /*if content_length > 0 { cmd.env("CONTENT_LENGTH", content_length.to_string()); }*/ let cgi_response = Self::call_and_parse_cgi(cmd).await; let mut response = Response::builder().status(StatusCode::from_u16(cgi_response.status).unwrap()); for (key, value) in cgi_response.headers { response = response.header(key, value); } response.body(Full::new(Bytes::from(body.to_vec()))).unwrap() } fn make>(b: B) -> Response> { Response::builder().body(Full::new(b.into())).unwrap() } fn set_http_env(headers: HeaderMap, cmd: &mut Command) { for (key, value) in headers.iter() { let key_str = key.as_str(); let mut key_upper = String::with_capacity(key_str.len() + 5); key_upper.push_str("HTTP_"); for ch in key_str.chars() { match ch { _ if ch as u8 > 0x60 && ch as u8 <= 0x7A => { key_upper.push((ch as u8 - 0x20) as char); } '-' => key_upper.push('_'), ch => key_upper.push(ch), } } match value.to_str() { Ok(val_str) => { cmd.env(key_upper, val_str); } Err(err) => { eprintln!("value for header {key_str} is not a string: {err}") } } } } async fn call_and_parse_cgi(mut cmd: Command) -> CgiResponse { let mut response = CgiResponse { // Default status code is 200 per RFC status: 200, headers: vec![], body: None, }; let output = cmd.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 value = &line[colon + 1..]; response.headers.push((key.to_vec(), value.to_vec())); 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 if curr[nl + 1] == b'\n' || (curr[nl + 1] == b'\r' && curr[nl + 2] == b'\n') { let body = &curr[nl + 2..]; if body.len() > 0 { response.body = Some(body.to_vec()); } } else { curr = &curr[nl + 1..]; } } response } } struct CgiResponse { /// The Status header of the CGI response status: u16, /// Headers except "Status" headers: Vec<(Vec, Vec)>, /// CGI response body body: Option>, }