use std::{ net::{IpAddr, SocketAddr}, pin::Pin, }; use confindent::Confindent; use http_body_util::{BodyExt, Full}; use hyper::{ HeaderMap, Request, Response, StatusCode, body::{Bytes, Incoming}, header::HeaderValue, server::conn::http1, service::Service, }; use hyper_util::rt::TokioIo; use tokio::{net::TcpListener, process::Command, runtime::Runtime}; #[derive(Clone, Debug)] pub struct Settings { port: u16, 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 { port: 26744, 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 }); } // We have tokio::main at home :) async fn run(settings: Settings) { let addr = SocketAddr::from(([0, 0, 0, 0], settings.port)); let listen = TcpListener::bind(addr).await.unwrap(); let svc = Svc { settings, client_addr: addr, }; loop { let (stream, caddr) = listen.accept().await.unwrap(); let io = TokioIo::new(stream); let mut svc_clone = svc.clone(); svc_clone.client_addr = caddr; tokio::task::spawn( async move { http1::Builder::new().serve_connection(io, svc_clone).await }, ); } } #[derive(Clone, Debug)] struct Svc { settings: Settings, client_addr: SocketAddr, } impl Service> for Svc { type Response = Response>; type Error = hyper::Error; type Future = Pin> + Send>>; fn call(&self, req: Request) -> Self::Future { let settings = self.settings.clone(); let caddr = self.client_addr; Box::pin(async move { Ok(Self::handle(settings, caddr, req).await) }) } } impl Svc { async fn handle( settings: Settings, caddr: SocketAddr, req: Request, ) -> Response> { // Collect things we need from the request before we eat it's body let method = req.method().as_str().to_ascii_uppercase(); let version = req.version(); 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(); // Find the client address let client_addr = { let x_forward = Self::parse_addr_from_header(headers.get("x-forwarded-for")); let forward = Self::parse_addr_from_header(headers.get("forwarded-for")); forward.unwrap_or(x_forward.unwrap_or(caddr.ip())) }; let server_name = headers .get("Host") .expect("no http host header set") .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) .env("QUERY_STRING", query) .env("REMOTE_ADDR", client_addr.to_string()) .env("REQUEST_METHOD", method) .env("SCRIPT_NAME", settings.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 &settings.env { cmd.env(key.to_ascii_uppercase(), value); } let cgi_response = Self::call_and_parse_cgi(cmd).await; let status = StatusCode::from_u16(cgi_response.status).unwrap(); let mut response = Response::builder().status(status); for (key, value) in cgi_response.headers { response = response.header(key, value); } response.body(Full::new(Bytes::from(body.to_vec()))).unwrap() } fn parse_addr_from_header(maybe_hval: Option<&HeaderValue>) -> Option { 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 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())); // 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? 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()); } return response; } // Move past the newline curr = &curr[nl + 1..]; } } } struct CgiResponse { /// The Status header of the CGI response status: u16, /// Headers except "Status" headers: Vec<(Vec, Vec)>, /// CGI response body body: Option>, }