use core::panic; use std::{net::SocketAddr, pin::Pin}; use confindent::Confindent; use http_body_util::{BodyExt, Full}; use hyper::{ 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 settings = Settings { script_filename: script.value_owned().expect("'Script' key has no value'"), env: vec![], }; 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 headers = req.headers().clone(); let body = req.into_body().collect().await.unwrap().to_bytes(); let content_length = body.len(); let mut cmd = Command::new(&settings.script_filename); cmd.env("SCRIPT_NAME", settings.script_filename) .env("PATH_INFO", path) .env("REQUEST_METHOD", method); for (header, value) in headers { if let Some(header) = header { let hname = header.as_str(); let hvalue = value.to_str().unwrap(); cmd.env(hname.to_ascii_uppercase(), hvalue); if hname.to_ascii_lowercase() == "user-agent" { println!("USER_AGENT: {hvalue}"); } } } /*if content_length > 0 { cmd.env("CONTENT_LENGTH", content_length.to_string()); }*/ let output = cmd.output().await.unwrap(); let response_raw = output.stdout; let mut response = Response::builder(); println!("{}", String::from_utf8_lossy(&response_raw)); let mut curr = response_raw.as_slice(); let mut status = None; let mut headers = vec![]; let body = loop { let nl = curr.iter().position(|b| *b == b'\n').expect("no nl in header"); let line = &curr[..nl]; let colon = line.iter().position(|b| *b == b':').expect("no colon in header"); let key = &line[..colon]; let value = &line[colon + 1..]; headers.push((key, value)); 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: usize = raw_code.parse().unwrap(); status = Some((code, raw_msg.to_owned())); } } // Body next if curr[nl + 1] == b'\n' || (curr[nl + 1] == b'\r' && curr[nl + 2] == b'\n') { break &curr[nl + 2..]; } else { curr = &curr[nl + 1..]; } }; match status { None => response = response.status(StatusCode::OK), Some((code, _status)) => { response = response.status(StatusCode::from_u16(code as u16).unwrap()); } } for (key, value) in headers { response = response.header(key.to_vec(), value.to_vec()); } response.body(Full::new(Bytes::from(body.to_vec()))).unwrap() } fn make>(b: B) -> Response> { Response::builder().body(Full::new(b.into())).unwrap() } }