From 427827ef3777c13ffe2a33ecbc54c316e58693d5 Mon Sep 17 00:00:00 2001 From: Marcel Koch Date: Tue, 25 Feb 2025 22:28:05 +0100 Subject: [PATCH 01/11] support variable `PATH_INFO` in routes --- cgi-rs/src/request.rs | 13 ++++++++++++- sample-cgi-script/src/main.rs | 3 +++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/cgi-rs/src/request.rs b/cgi-rs/src/request.rs index 63c8128..2667140 100644 --- a/cgi-rs/src/request.rs +++ b/cgi-rs/src/request.rs @@ -44,11 +44,22 @@ impl CGIRequest { self.var(MetaVariableKind::RequestUri) .map(|uri| Ok(uri.as_str()?.to_string())) .unwrap_or_else(|| { + + let path_info_str = match MetaVariableKind::PathInfo.try_from_env() { + Ok(meta_variable) => { + String::from(meta_variable.as_str().unwrap_or("")) + } + Err(_) => { + String::from("") + } + }; + let script_name = MetaVariableKind::ScriptName.try_from_env()?; let query_string = MetaVariableKind::QueryString.try_from_env()?; Ok(format!( - "{}?{}", + "{}{}?{}", script_name.as_str()?, + path_info_str, query_string.as_str()? )) }) diff --git a/sample-cgi-script/src/main.rs b/sample-cgi-script/src/main.rs index 4495f83..3d2641c 100644 --- a/sample-cgi-script/src/main.rs +++ b/sample-cgi-script/src/main.rs @@ -6,6 +6,9 @@ async fn main() { let app = Router::new().route( "/cgi-bin/sample-cgi-server", get(|| async { "Hello, World!" }), + ).route( + "/cgi-bin/sample-cgi-server/with/path-info", + get(|| async { "Hello, PATH_INFO" }), ); if let Err(e) = serve_cgi(app).await { From 03d1f1ccd5ecc69494f666972bba9716ee68a983 Mon Sep 17 00:00:00 2001 From: Marcel Koch Date: Wed, 16 Apr 2025 22:12:50 +0200 Subject: [PATCH 02/11] support axum 0.8.1 --- cgi-rs/Cargo.toml | 4 +++- cgi-rs/src/request.rs | 30 ++++++++++++++++++++--------- cgi-rs/src/response.rs | 37 +++++++++++------------------------- sample-cgi-script/Cargo.toml | 2 +- tower-cgi/Cargo.toml | 5 +++-- tower-cgi/src/lib.rs | 31 +++++++++++++++++++++++------- 6 files changed, 63 insertions(+), 46 deletions(-) diff --git a/cgi-rs/Cargo.toml b/cgi-rs/Cargo.toml index 102c53f..ee4b499 100644 --- a/cgi-rs/Cargo.toml +++ b/cgi-rs/Cargo.toml @@ -4,6 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -hyper = "0.14" +http-body-util = "0.1.2" +hyper = "1.6.0" snafu = "0.8" tokio = "1" +bytes = "1.10.0" diff --git a/cgi-rs/src/request.rs b/cgi-rs/src/request.rs index 2667140..2f5a5e0 100644 --- a/cgi-rs/src/request.rs +++ b/cgi-rs/src/request.rs @@ -1,14 +1,18 @@ +use std::convert::Infallible; use crate::{error, CGIError, MetaVariable, MetaVariableKind, Result}; -use hyper::{Body as HttpBody, Request}; +use hyper::Request; +use hyper::body::{Body, Bytes}; use snafu::ResultExt; use std::io::{stdin, Read}; +use http_body_util::combinators::BoxBody; +use http_body_util::Full; -pub struct CGIRequest { - pub request_body: HttpBody, +pub struct CGIRequest { + pub request_body: B } -impl CGIRequest { - pub fn from_env() -> Result { +impl CGIRequest where B: Body { + pub fn from_env() -> Result>> { let content_length = MetaVariableKind::ContentLength .from_env() .map(|content_length| { @@ -19,8 +23,15 @@ impl CGIRequest { .transpose()? .unwrap_or_default(); - let request_body = HttpBody::from(Self::request_body_from_env(content_length)?); - Ok(Self { request_body }) + let read_content = Self::request_body_from_env(content_length)?; + + let request_body = Bytes::from(read_content); + + let full = Full::from(request_body); + + let result = CGIRequest { request_body: full }; + + Ok(result) } pub fn var(&self, kind: MetaVariableKind) -> Option { @@ -76,10 +87,11 @@ macro_rules! try_set_headers { }; } -impl TryFrom for Request { +impl TryFrom> for Request where B: Body { type Error = CGIError; - fn try_from(cgi_request: CGIRequest) -> Result { + fn try_from(cgi_request: CGIRequest) -> Result { + let mut request_builder = Request::builder() .method( cgi_request diff --git a/cgi-rs/src/response.rs b/cgi-rs/src/response.rs index 616fff0..ebdaf09 100644 --- a/cgi-rs/src/response.rs +++ b/cgi-rs/src/response.rs @@ -2,16 +2,19 @@ use crate::{error, CGIError, Result}; use hyper::{http::HeaderValue, HeaderMap, Response}; use snafu::ResultExt; use std::io::Write; +use bytes::Bytes; +use http_body_util::{Full}; +use hyper::body::{Body}; #[derive(Debug)] -pub struct CGIResponse { - headers: HeaderMap, - status: String, - reason: Option, - body: B, +pub struct CGIResponse { + pub headers: HeaderMap, + pub status: String, + pub reason: Option, + pub body: Bytes, } -impl CGIResponse { +impl CGIResponse { pub async fn write_response_to_output(self, mut output: impl Write) -> Result<()> { self.write_status(&mut output).await?; self.write_headers(&mut output).await?; @@ -50,29 +53,11 @@ impl CGIResponse { } async fn write_body(self, output: &mut impl Write) -> Result<()> { - let body = hyper::body::to_bytes(self.body) - .await - .or_else(|_| error::BuildResponseSnafu.fail())?; + let body = self.body; - output.write(&body).context(error::WriteResponseSnafu)?; + output.write(body.as_ref()).context(error::WriteResponseSnafu)?; Ok(()) } } -impl TryFrom> for CGIResponse { - type Error = CGIError; - - fn try_from(response: Response) -> Result { - let headers = response.headers().clone(); - let status = response.status().to_string(); - let reason = response.status().canonical_reason().map(|s| s.to_string()); - let body = response.into_body(); - Ok(CGIResponse { - headers, - status, - reason, - body, - }) - } -} diff --git a/sample-cgi-script/Cargo.toml b/sample-cgi-script/Cargo.toml index 12561c6..f2fa7d1 100644 --- a/sample-cgi-script/Cargo.toml +++ b/sample-cgi-script/Cargo.toml @@ -6,6 +6,6 @@ license = "Apache-2.0" publish = false [dependencies] -axum = "0.6" +axum = "0.8.1" tower-cgi = { path = "../tower-cgi" } tokio = { version = "1", features = ["full"] } diff --git a/tower-cgi/Cargo.toml b/tower-cgi/Cargo.toml index 3f7d6c8..e4dd881 100644 --- a/tower-cgi/Cargo.toml +++ b/tower-cgi/Cargo.toml @@ -6,10 +6,11 @@ license = "Apache-2.0" [dependencies] cgi-rs = { path = "../cgi-rs" } -hyper = { version = "0.14", default-features = false } +hyper = { version = "1.6.0", default-features = false } snafu = "0.8" tower = { version = "0.5", default-features = false, features = ["util"] } +http-body-util = "0.1.2" +axum = "0.8.1" [dev-dependencies] -axum = "0.6" tokio = { version = "1", features = ["full"] } diff --git a/tower-cgi/src/lib.rs b/tower-cgi/src/lib.rs index 38287b6..45dc744 100644 --- a/tower-cgi/src/lib.rs +++ b/tower-cgi/src/lib.rs @@ -17,10 +17,13 @@ //! ``` use cgi_rs::{CGIError, CGIRequest, CGIResponse}; -use hyper::{Body as HttpBody, Request, Response}; use snafu::ResultExt; use std::convert::Infallible; +use std::fmt::Debug; use std::io::Write; +use http_body_util::{Full, BodyExt}; +use hyper::body::{Body, Bytes}; +use hyper::{Request, Response}; use tower::{Service, ServiceExt}; /// Serve a CGI application. @@ -28,11 +31,11 @@ use tower::{Service, ServiceExt}; /// Responses are emitted to stdout per the CGI RFC3875 pub async fn serve_cgi(app: S) -> Result<()> where - S: Service, Response = Response, Error = Infallible> + S: Service>, Response = Response, Error = Infallible> + Clone + Send + 'static, - B: hyper::body::HttpBody, + B: Body, ::Error: Debug { serve_cgi_with_output(std::io::stdout(), app).await } @@ -42,13 +45,13 @@ where /// Responses are emitted to the provided output stream. pub async fn serve_cgi_with_output(output: impl Write, app: S) -> Result<()> where - S: Service, Response = Response, Error = Infallible> + S: Service>, Response = Response, Error = Infallible> + Clone + Send + 'static, - B: hyper::body::HttpBody, + B: Body, ::Error: Debug { - let request = CGIRequest::from_env() + let request = CGIRequest::>::from_env() .and_then(Request::try_from) .context(error::CGIRequestParseSnafu)?; @@ -57,7 +60,21 @@ where .await .expect("The Error type is Infallible, this should never fail."); - let cgi_response: CGIResponse = response.try_into().context(error::CGIResponseParseSnafu)?; + let headers = response.headers().clone(); + let status = response.status().to_string(); + let reason = response.status().canonical_reason().map(|s| s.to_string()); + + let collected = response.into_body().collect().await; + + let body_bytes = collected.unwrap().to_bytes(); + + let cgi_response = CGIResponse { + headers, + status, + reason, + body: body_bytes, + }; + cgi_response .write_response_to_output(output) .await From ec8a99912dd6b9909bd9414a8b1c86bfee88a19d Mon Sep 17 00:00:00 2001 From: Marcel Koch Date: Sun, 13 Jul 2025 17:28:04 +0200 Subject: [PATCH 03/11] fix doc tests --- cgi-rs/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cgi-rs/src/lib.rs b/cgi-rs/src/lib.rs index e4d9fa6..bc29071 100644 --- a/cgi-rs/src/lib.rs +++ b/cgi-rs/src/lib.rs @@ -11,7 +11,9 @@ //! ## Examples //! ### Parsing an HTTP Request //! ```rust -//! use hyper::{Request, Body}; +//! use hyper::Request; +//! use hyper::body::Bytes; +//! use http_body_util::Full; //! use cgi_rs::CGIRequest; //! //! // In a CGI environment, the CGI server would set these variables, as well as others. @@ -19,7 +21,7 @@ //! std::env::set_var("CONTENT_LENGTH", "0"); //! std::env::set_var("REQUEST_URI", "/"); //! -//! let cgi_request: Request = CGIRequest::from_env() +//! let cgi_request: Request> = CGIRequest::>::from_env() //! .and_then(Request::try_from).unwrap(); //! //! assert_eq!(cgi_request.method(), "GET"); From b7a59986775c032594a3106c17761e8f3010dbba Mon Sep 17 00:00:00 2001 From: Marcel Koch Date: Mon, 14 Jul 2025 01:38:13 +0200 Subject: [PATCH 04/11] support cookies --- cgi-rs/src/lib.rs | 2 ++ cgi-rs/src/request.rs | 1 + sample-cgi-script/Cargo.toml | 3 +++ sample-cgi-script/src/main.rs | 29 ++++++++++++++++++++++++++--- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/cgi-rs/src/lib.rs b/cgi-rs/src/lib.rs index bc29071..6ad77e6 100644 --- a/cgi-rs/src/lib.rs +++ b/cgi-rs/src/lib.rs @@ -108,6 +108,7 @@ pub enum MetaVariableKind { HttpHost, HttpUserAgent, HttpAccept, + HttpCookie, ServerSignature, DocumentRoot, RequestScheme, @@ -150,6 +151,7 @@ impl MetaVariableKind { MetaVariableKind::ScriptFilename => "SCRIPT_FILENAME", MetaVariableKind::RemotePort => "REMOTE_PORT", MetaVariableKind::RequestUri => "REQUEST_URI", + MetaVariableKind::HttpCookie => "HTTP_COOKIE" } } diff --git a/cgi-rs/src/request.rs b/cgi-rs/src/request.rs index 2f5a5e0..0f33293 100644 --- a/cgi-rs/src/request.rs +++ b/cgi-rs/src/request.rs @@ -107,6 +107,7 @@ impl TryFrom> for Request where B: Body { ["Accept", MetaVariableKind::HttpAccept], ["Host", MetaVariableKind::HttpHost], ["User-Agent", MetaVariableKind::HttpUserAgent], + ["Cookie", MetaVariableKind::HttpCookie], ); request_builder diff --git a/sample-cgi-script/Cargo.toml b/sample-cgi-script/Cargo.toml index f2fa7d1..f89f8a3 100644 --- a/sample-cgi-script/Cargo.toml +++ b/sample-cgi-script/Cargo.toml @@ -9,3 +9,6 @@ publish = false axum = "0.8.1" tower-cgi = { path = "../tower-cgi" } tokio = { version = "1", features = ["full"] } +tower-sessions = "0.14.0" +tower-cookies = "0.11.0" +tower-sessions-file-based-store = "*" diff --git a/sample-cgi-script/src/main.rs b/sample-cgi-script/src/main.rs index 3d2641c..af7b57b 100644 --- a/sample-cgi-script/src/main.rs +++ b/sample-cgi-script/src/main.rs @@ -1,15 +1,38 @@ use axum::{routing::get, Router}; +use axum::http::StatusCode; +use axum::response::Response; +use tower_cookies::{Cookie, Cookies}; +use tower_sessions::cookie::time::Duration; +use tower_sessions::{MemoryStore, Session, SessionStore}; +use tower_sessions::session::Record; use tower_cgi::serve_cgi; +use tower_sessions_file_based_store::FileStore; + #[tokio::main] async fn main() { + let session_store = FileStore::new("./", "prefix-", ".json"); + // let session_store = MemoryStore::default(); + let session_layer = tower_sessions::SessionManagerLayer::new(session_store) + .with_secure(false) + //.with_always_save(true) + .with_expiry(tower_sessions::Expiry::OnInactivity(Duration::seconds(15))); + let app = Router::new().route( - "/cgi-bin/sample-cgi-server", - get(|| async { "Hello, World!" }), + "/cgi-bin/sample-cgi-server/", + get(|cookies: Cookies, session: Session| async move { + cookies.add(Cookie::new("hello_world", "hello_world")); + session.clear().await; + session.insert("foo", "bar").await.unwrap(); + let value: String = session.get("foo").await.unwrap().unwrap_or("no value".to_string()); + + value + + }), ).route( "/cgi-bin/sample-cgi-server/with/path-info", get(|| async { "Hello, PATH_INFO" }), - ); + ).layer(session_layer); if let Err(e) = serve_cgi(app).await { eprintln!("Error while serving CGI request: {}", e); From d11a2140f674bb9d63e80e32559f1f1f8567f397 Mon Sep 17 00:00:00 2001 From: Marcel Koch Date: Mon, 14 Jul 2025 01:45:37 +0200 Subject: [PATCH 05/11] enhance sample-cgi-script to use a sqlite database --- sample-cgi-script/Cargo.toml | 1 + sample-cgi-script/src/main.rs | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/sample-cgi-script/Cargo.toml b/sample-cgi-script/Cargo.toml index f89f8a3..5ec4abc 100644 --- a/sample-cgi-script/Cargo.toml +++ b/sample-cgi-script/Cargo.toml @@ -12,3 +12,4 @@ tokio = { version = "1", features = ["full"] } tower-sessions = "0.14.0" tower-cookies = "0.11.0" tower-sessions-file-based-store = "*" +rusqlite = "0.33.0" diff --git a/sample-cgi-script/src/main.rs b/sample-cgi-script/src/main.rs index af7b57b..f2e2679 100644 --- a/sample-cgi-script/src/main.rs +++ b/sample-cgi-script/src/main.rs @@ -1,6 +1,7 @@ use axum::{routing::get, Router}; use axum::http::StatusCode; use axum::response::Response; +use rusqlite::{named_params, Connection}; use tower_cookies::{Cookie, Cookies}; use tower_sessions::cookie::time::Duration; use tower_sessions::{MemoryStore, Session, SessionStore}; @@ -26,8 +27,29 @@ async fn main() { session.insert("foo", "bar").await.unwrap(); let value: String = session.get("foo").await.unwrap().unwrap_or("no value".to_string()); - value + let conn = Connection::open("./my_database.db").unwrap(); + conn.execute( + "CREATE TABLE IF NOT EXISTS user ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + age INTEGER + )", + [], + ).unwrap(); + let mut stmt = conn.prepare("INSERT INTO user (name, age) VALUES (?,?)").unwrap(); + stmt.execute(["Alice", "30"]).unwrap(); + + let mut stmt = conn.prepare("SELECT * FROM user").unwrap(); + let mut one: String = "".into(); + let mut rows = stmt.query([]).unwrap(); + while let Some(row) = rows.next().unwrap() { + let name: String = row.get(1).unwrap(); + one.push_str(name.as_str()) + } + + + one }), ).route( "/cgi-bin/sample-cgi-server/with/path-info", From ac316f06e808c37babbd7a2afde00dd00646b52a Mon Sep 17 00:00:00 2001 From: Marcel Koch Date: Mon, 14 Jul 2025 01:49:40 +0200 Subject: [PATCH 06/11] introduce a http-cgi-server to test CGI scripts --- http-cgi-server/Cargo.toml | 8 ++ http-cgi-server/src/main.rs | 205 ++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 http-cgi-server/Cargo.toml create mode 100644 http-cgi-server/src/main.rs diff --git a/http-cgi-server/Cargo.toml b/http-cgi-server/Cargo.toml new file mode 100644 index 0000000..5e729b0 --- /dev/null +++ b/http-cgi-server/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "http-cgi-server" +version = "0.1.0" +edition = "2021" + +[dependencies] + +[workspace] \ No newline at end of file diff --git a/http-cgi-server/src/main.rs b/http-cgi-server/src/main.rs new file mode 100644 index 0000000..02c9999 --- /dev/null +++ b/http-cgi-server/src/main.rs @@ -0,0 +1,205 @@ +use std::collections::HashMap; +use std::io::{Read, Write}; +use std::net::{TcpListener, TcpStream}; +use std::path::Path; +use std::process::{Command, Stdio}; +use std::thread; + +fn main() { + let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); + println!("CGI HTTP Server listening on http://127.0.0.1:8080"); + println!("CGI scripts should be placed in ./cgi-bin/"); + + for stream in listener.incoming() { + match stream { + Ok(stream) => { + thread::spawn(|| { + handle_connection(stream); + }); + } + Err(e) => { + eprintln!("Error accepting connection: {}", e); + } + } + } +} + +fn handle_connection(mut stream: TcpStream) { + let mut buffer = [0; 1024]; + let n = stream.read(&mut buffer).unwrap(); + let request = String::from_utf8_lossy(&buffer[..n]); + + let mut lines = request.lines(); + let first_line = lines.next().unwrap_or(""); + let parts: Vec<&str> = first_line.split_whitespace().collect(); + + if parts.len() < 3 { + send_error(stream, "400 Bad Request"); + return; + } + + let method = parts[0]; + let path = parts[1]; + + // Parse headers + let mut headers = HashMap::new(); + for line in lines { + if line.is_empty() { + break; + } + if let Some((key, value)) = line.split_once(": ") { + headers.insert(key.to_lowercase(), value.to_string()); + } + } + + // Check if this is a CGI request + if path.starts_with("/cgi-bin/") { + handle_cgi_request(stream, method, path, headers); + } else { + send_error(stream, "404 Not Found"); + } +} + +fn handle_cgi_request( + mut stream: TcpStream, + method: &str, + path: &str, + headers: HashMap, +) { + let script_name = path.trim_start_matches("/cgi-bin/"); + let script_path = format!("./cgi-bin/{}", script_name); + + let mut absolute_script_path = std::env::current_dir().unwrap(); + absolute_script_path.push(&script_path); + absolute_script_path = absolute_script_path.canonicalize().unwrap(); + + // Check if script exists + if !absolute_script_path.exists() { + send_error(stream, "404 Not Found"); + return; + } + + // Set up CGI environment variables + let mut env_vars = HashMap::new(); + let content_length = headers + .get("content-length") + .unwrap_or(&"0".to_string()) + .clone(); + let content_type = headers + .get("content-type") + .unwrap_or(&"text/plain".to_string()) + .clone(); + + env_vars.insert("REQUEST_METHOD".to_string(), method.to_string()); + env_vars.insert("CONTENT_LENGTH".to_string(), content_length); + env_vars.insert("REQUEST_URI".to_string(), path.to_string()); + env_vars.insert("QUERY_STRING".to_string(), "".to_string()); + env_vars.insert("CONTENT_TYPE".to_string(), content_type); + env_vars.insert("SERVER_PROTOCOL".to_string(), "HTTP/1.1".to_string()); + env_vars.insert("GATEWAY_INTERFACE".to_string(), "CGI/1.1".to_string()); + env_vars.insert( + "SERVER_SOFTWARE".to_string(), + "http-cgi-server/1.0".to_string(), + ); + env_vars.insert("REMOTE_ADDR".to_string(), "127.0.0.1".to_string()); + env_vars.insert("REMOTE_PORT".to_string(), "12345".to_string()); + env_vars.insert("SERVER_ADDR".to_string(), "127.0.0.1".to_string()); + env_vars.insert("SERVER_PORT".to_string(), "8080".to_string()); + env_vars.insert("SERVER_NAME".to_string(), "localhost".to_string()); + env_vars.insert("DOCUMENT_ROOT".to_string(), ".".to_string()); + env_vars.insert("SCRIPT_NAME".to_string(), path.to_string()); + env_vars.insert("PATH_INFO".to_string(), "".to_string()); + env_vars.insert("PATH_TRANSLATED".to_string(), script_path.clone()); + + // Add HTTP headers as environment variables + for (key, value) in headers { + let env_key = format!("HTTP_{}", key.to_uppercase().replace("-", "_")); + env_vars.insert(env_key, value); + } + + // Execute the CGI script + let mut command = Command::new(&absolute_script_path); + + // Set environment variables + for (key, value) in env_vars { + command.env(key, value); + } + + let output = command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn(); + + match output { + Ok(mut child) => { + // Send any input to the CGI script + if let Some(mut stdin) = child.stdin.take() { + // For POST requests, you'd read the body here + stdin.write_all(b"").unwrap(); + } + + // Read the output + let mut stdout = child.stdout.take().unwrap(); + let mut stderr = child.stderr.take().unwrap(); + + let mut output = Vec::new(); + let mut error_output = Vec::new(); + + stdout.read_to_end(&mut output).unwrap(); + stderr.read_to_end(&mut error_output).unwrap(); + + // Wait for the process to complete + let status = child.wait().unwrap(); + + // Parse and send the CGI response + let response = String::from_utf8_lossy(&output); + send_cgi_response(stream, &response); + + if !status.success() { + eprintln!("CGI script exited with status: {}", status); + } + } + Err(e) => { + eprintln!("Failed to execute CGI script: {}", e); + send_error(stream, "500 Internal Server Error"); + } + } +} + +fn send_cgi_response(mut stream: TcpStream, cgi_output: &str) { + // Split the CGI output into headers and body + let parts: Vec<&str> = cgi_output.split("\r\n\r\n").collect(); + + if parts.len() >= 2 { + // CGI output has headers and body + let headers = parts[0]; + let body = parts[1..].join("\r\n\r\n"); + + // Send HTTP status line + stream.write_all(b"HTTP/1.1 200 OK\r\n").unwrap(); + + // Send CGI headers + stream.write_all(headers.as_bytes()).unwrap(); + stream.write_all(b"\r\n\r\n").unwrap(); + + // Send body + stream.write_all(body.as_bytes()).unwrap(); + } else { + // No headers found, treat as raw content + stream + .write_all(b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n") + .unwrap(); + stream.write_all(cgi_output.as_bytes()).unwrap(); + } +} + +fn send_error(mut stream: TcpStream, status: &str) { + let response = format!( + "HTTP/1.1 {}\r\nContent-Type: text/plain\r\nContent-Length: {}\r\n\r\n{}", + status, + status.len(), + status + ); + stream.write_all(response.as_bytes()).unwrap(); +} From 8b49e709e98a1b28bb76a5d3732ca08c024d02e6 Mon Sep 17 00:00:00 2001 From: Marcel Koch Date: Wed, 10 Sep 2025 21:27:07 +0200 Subject: [PATCH 07/11] introduce support for http header `Authorization` --- cgi-rs/src/lib.rs | 2 ++ cgi-rs/src/request.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/cgi-rs/src/lib.rs b/cgi-rs/src/lib.rs index 6ad77e6..ba3b37d 100644 --- a/cgi-rs/src/lib.rs +++ b/cgi-rs/src/lib.rs @@ -105,6 +105,7 @@ pub enum MetaVariableKind { // Not in the RFC, but emitted from httpd's mod_cgi UniqueID, + HttpAuthorization, HttpHost, HttpUserAgent, HttpAccept, @@ -122,6 +123,7 @@ pub enum MetaVariableKind { impl MetaVariableKind { fn as_str(&self) -> &'static str { match self { + MetaVariableKind::HttpAuthorization => "HTTP_AUTHORIZATION", MetaVariableKind::AuthType => "AUTH_TYPE", MetaVariableKind::ContentLength => "CONTENT_LENGTH", MetaVariableKind::ContentType => "CONTENT_TYPE", diff --git a/cgi-rs/src/request.rs b/cgi-rs/src/request.rs index 0f33293..7212dcb 100644 --- a/cgi-rs/src/request.rs +++ b/cgi-rs/src/request.rs @@ -104,6 +104,7 @@ impl TryFrom> for Request where B: Body { request_builder, cgi_request, ["Content-Length", MetaVariableKind::ContentLength], + ["Authorization", MetaVariableKind::HttpAuthorization], ["Accept", MetaVariableKind::HttpAccept], ["Host", MetaVariableKind::HttpHost], ["User-Agent", MetaVariableKind::HttpUserAgent], From 32f797408d31f1b477814974a42383d83ad93bf3 Mon Sep 17 00:00:00 2001 From: Marcel Koch Date: Wed, 10 Sep 2025 23:33:32 +0200 Subject: [PATCH 08/11] run `cargo fmt` --- cgi-rs/src/lib.rs | 2 +- cgi-rs/src/request.rs | 32 +++++++------- cgi-rs/src/response.rs | 11 ++--- sample-cgi-script/src/main.rs | 79 +++++++++++++++++++---------------- tower-cgi/src/lib.rs | 12 +++--- 5 files changed, 74 insertions(+), 62 deletions(-) diff --git a/cgi-rs/src/lib.rs b/cgi-rs/src/lib.rs index ba3b37d..45eaaf2 100644 --- a/cgi-rs/src/lib.rs +++ b/cgi-rs/src/lib.rs @@ -153,7 +153,7 @@ impl MetaVariableKind { MetaVariableKind::ScriptFilename => "SCRIPT_FILENAME", MetaVariableKind::RemotePort => "REMOTE_PORT", MetaVariableKind::RequestUri => "REQUEST_URI", - MetaVariableKind::HttpCookie => "HTTP_COOKIE" + MetaVariableKind::HttpCookie => "HTTP_COOKIE", } } diff --git a/cgi-rs/src/request.rs b/cgi-rs/src/request.rs index 7212dcb..ca8da66 100644 --- a/cgi-rs/src/request.rs +++ b/cgi-rs/src/request.rs @@ -1,17 +1,20 @@ -use std::convert::Infallible; use crate::{error, CGIError, MetaVariable, MetaVariableKind, Result}; -use hyper::Request; +use http_body_util::combinators::BoxBody; +use http_body_util::Full; use hyper::body::{Body, Bytes}; +use hyper::Request; use snafu::ResultExt; +use std::convert::Infallible; use std::io::{stdin, Read}; -use http_body_util::combinators::BoxBody; -use http_body_util::Full; -pub struct CGIRequest { - pub request_body: B +pub struct CGIRequest { + pub request_body: B, } -impl CGIRequest where B: Body { +impl CGIRequest +where + B: Body, +{ pub fn from_env() -> Result>> { let content_length = MetaVariableKind::ContentLength .from_env() @@ -55,14 +58,9 @@ impl CGIRequest where B: Body { self.var(MetaVariableKind::RequestUri) .map(|uri| Ok(uri.as_str()?.to_string())) .unwrap_or_else(|| { - let path_info_str = match MetaVariableKind::PathInfo.try_from_env() { - Ok(meta_variable) => { - String::from(meta_variable.as_str().unwrap_or("")) - } - Err(_) => { - String::from("") - } + Ok(meta_variable) => String::from(meta_variable.as_str().unwrap_or("")), + Err(_) => String::from(""), }; let script_name = MetaVariableKind::ScriptName.try_from_env()?; @@ -87,11 +85,13 @@ macro_rules! try_set_headers { }; } -impl TryFrom> for Request where B: Body { +impl TryFrom> for Request +where + B: Body, +{ type Error = CGIError; fn try_from(cgi_request: CGIRequest) -> Result { - let mut request_builder = Request::builder() .method( cgi_request diff --git a/cgi-rs/src/response.rs b/cgi-rs/src/response.rs index ebdaf09..60ec09b 100644 --- a/cgi-rs/src/response.rs +++ b/cgi-rs/src/response.rs @@ -1,10 +1,10 @@ use crate::{error, CGIError, Result}; +use bytes::Bytes; +use http_body_util::Full; +use hyper::body::Body; use hyper::{http::HeaderValue, HeaderMap, Response}; use snafu::ResultExt; use std::io::Write; -use bytes::Bytes; -use http_body_util::{Full}; -use hyper::body::{Body}; #[derive(Debug)] pub struct CGIResponse { @@ -55,9 +55,10 @@ impl CGIResponse { async fn write_body(self, output: &mut impl Write) -> Result<()> { let body = self.body; - output.write(body.as_ref()).context(error::WriteResponseSnafu)?; + output + .write(body.as_ref()) + .context(error::WriteResponseSnafu)?; Ok(()) } } - diff --git a/sample-cgi-script/src/main.rs b/sample-cgi-script/src/main.rs index f2e2679..307aba9 100644 --- a/sample-cgi-script/src/main.rs +++ b/sample-cgi-script/src/main.rs @@ -1,12 +1,12 @@ -use axum::{routing::get, Router}; use axum::http::StatusCode; use axum::response::Response; +use axum::{routing::get, Router}; use rusqlite::{named_params, Connection}; +use tower_cgi::serve_cgi; use tower_cookies::{Cookie, Cookies}; use tower_sessions::cookie::time::Duration; -use tower_sessions::{MemoryStore, Session, SessionStore}; use tower_sessions::session::Record; -use tower_cgi::serve_cgi; +use tower_sessions::{MemoryStore, Session, SessionStore}; use tower_sessions_file_based_store::FileStore; @@ -19,42 +19,51 @@ async fn main() { //.with_always_save(true) .with_expiry(tower_sessions::Expiry::OnInactivity(Duration::seconds(15))); - let app = Router::new().route( - "/cgi-bin/sample-cgi-server/", - get(|cookies: Cookies, session: Session| async move { - cookies.add(Cookie::new("hello_world", "hello_world")); - session.clear().await; - session.insert("foo", "bar").await.unwrap(); - let value: String = session.get("foo").await.unwrap().unwrap_or("no value".to_string()); - - let conn = Connection::open("./my_database.db").unwrap(); - conn.execute( - "CREATE TABLE IF NOT EXISTS user ( + let app = Router::new() + .route( + "/cgi-bin/sample-cgi-server/", + get(|cookies: Cookies, session: Session| async move { + cookies.add(Cookie::new("hello_world", "hello_world")); + session.clear().await; + session.insert("foo", "bar").await.unwrap(); + let value: String = session + .get("foo") + .await + .unwrap() + .unwrap_or("no value".to_string()); + + let conn = Connection::open("./my_database.db").unwrap(); + conn.execute( + "CREATE TABLE IF NOT EXISTS user ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER )", - [], - ).unwrap(); - - let mut stmt = conn.prepare("INSERT INTO user (name, age) VALUES (?,?)").unwrap(); - stmt.execute(["Alice", "30"]).unwrap(); - - let mut stmt = conn.prepare("SELECT * FROM user").unwrap(); - let mut one: String = "".into(); - let mut rows = stmt.query([]).unwrap(); - while let Some(row) = rows.next().unwrap() { - let name: String = row.get(1).unwrap(); - one.push_str(name.as_str()) - } - - - one - }), - ).route( - "/cgi-bin/sample-cgi-server/with/path-info", - get(|| async { "Hello, PATH_INFO" }), - ).layer(session_layer); + [], + ) + .unwrap(); + + let mut stmt = conn + .prepare("INSERT INTO user (name, age) VALUES (?,?)") + .unwrap(); + stmt.execute(["Alice", "30"]).unwrap(); + + let mut stmt = conn.prepare("SELECT * FROM user").unwrap(); + let mut one: String = "".into(); + let mut rows = stmt.query([]).unwrap(); + while let Some(row) = rows.next().unwrap() { + let name: String = row.get(1).unwrap(); + one.push_str(name.as_str()) + } + + one + }), + ) + .route( + "/cgi-bin/sample-cgi-server/with/path-info", + get(|| async { "Hello, PATH_INFO" }), + ) + .layer(session_layer); if let Err(e) = serve_cgi(app).await { eprintln!("Error while serving CGI request: {}", e); diff --git a/tower-cgi/src/lib.rs b/tower-cgi/src/lib.rs index 45dc744..7339841 100644 --- a/tower-cgi/src/lib.rs +++ b/tower-cgi/src/lib.rs @@ -17,13 +17,13 @@ //! ``` use cgi_rs::{CGIError, CGIRequest, CGIResponse}; +use http_body_util::{BodyExt, Full}; +use hyper::body::{Body, Bytes}; +use hyper::{Request, Response}; use snafu::ResultExt; use std::convert::Infallible; use std::fmt::Debug; use std::io::Write; -use http_body_util::{Full, BodyExt}; -use hyper::body::{Body, Bytes}; -use hyper::{Request, Response}; use tower::{Service, ServiceExt}; /// Serve a CGI application. @@ -35,7 +35,8 @@ where + Clone + Send + 'static, - B: Body, ::Error: Debug + B: Body, + ::Error: Debug, { serve_cgi_with_output(std::io::stdout(), app).await } @@ -49,7 +50,8 @@ where + Clone + Send + 'static, - B: Body, ::Error: Debug + B: Body, + ::Error: Debug, { let request = CGIRequest::>::from_env() .and_then(Request::try_from) From 36a69185dede1a3b15e883935c317624aa56c19e Mon Sep 17 00:00:00 2001 From: Marcel Koch Date: Wed, 10 Sep 2025 23:36:57 +0200 Subject: [PATCH 09/11] run `cargo clippy --fix` --- cgi-rs/src/request.rs | 2 -- cgi-rs/src/response.rs | 6 ++---- sample-cgi-script/src/main.rs | 7 ++----- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/cgi-rs/src/request.rs b/cgi-rs/src/request.rs index ca8da66..d2ec3f9 100644 --- a/cgi-rs/src/request.rs +++ b/cgi-rs/src/request.rs @@ -1,10 +1,8 @@ use crate::{error, CGIError, MetaVariable, MetaVariableKind, Result}; -use http_body_util::combinators::BoxBody; use http_body_util::Full; use hyper::body::{Body, Bytes}; use hyper::Request; use snafu::ResultExt; -use std::convert::Infallible; use std::io::{stdin, Read}; pub struct CGIRequest { diff --git a/cgi-rs/src/response.rs b/cgi-rs/src/response.rs index 60ec09b..79c5a1c 100644 --- a/cgi-rs/src/response.rs +++ b/cgi-rs/src/response.rs @@ -1,8 +1,6 @@ -use crate::{error, CGIError, Result}; +use crate::{error, Result}; use bytes::Bytes; -use http_body_util::Full; -use hyper::body::Body; -use hyper::{http::HeaderValue, HeaderMap, Response}; +use hyper::{http::HeaderValue, HeaderMap}; use snafu::ResultExt; use std::io::Write; diff --git a/sample-cgi-script/src/main.rs b/sample-cgi-script/src/main.rs index 307aba9..729606b 100644 --- a/sample-cgi-script/src/main.rs +++ b/sample-cgi-script/src/main.rs @@ -1,12 +1,9 @@ -use axum::http::StatusCode; -use axum::response::Response; use axum::{routing::get, Router}; -use rusqlite::{named_params, Connection}; +use rusqlite::Connection; use tower_cgi::serve_cgi; use tower_cookies::{Cookie, Cookies}; use tower_sessions::cookie::time::Duration; -use tower_sessions::session::Record; -use tower_sessions::{MemoryStore, Session, SessionStore}; +use tower_sessions::Session; use tower_sessions_file_based_store::FileStore; From 024a552178dbe75ae1c5dbdb48d1b303881038af Mon Sep 17 00:00:00 2001 From: Marcel Koch Date: Wed, 10 Sep 2025 23:40:26 +0200 Subject: [PATCH 10/11] improve imports --- cgi-rs/src/request.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cgi-rs/src/request.rs b/cgi-rs/src/request.rs index d2ec3f9..58c23dc 100644 --- a/cgi-rs/src/request.rs +++ b/cgi-rs/src/request.rs @@ -1,4 +1,4 @@ -use crate::{error, CGIError, MetaVariable, MetaVariableKind, Result}; +use crate::{error, MetaVariable, MetaVariableKind, Result}; use http_body_util::Full; use hyper::body::{Body, Bytes}; use hyper::Request; @@ -87,7 +87,7 @@ impl TryFrom> for Request where B: Body, { - type Error = CGIError; + type Error = crate::CGIError; fn try_from(cgi_request: CGIRequest) -> Result { let mut request_builder = Request::builder() From 6ab0d1225475f5fc850ec32f6246908ae63f3f98 Mon Sep 17 00:00:00 2001 From: Marcel Koch Date: Wed, 10 Sep 2025 23:41:19 +0200 Subject: [PATCH 11/11] mark variable of session variable value as not used --- sample-cgi-script/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-cgi-script/src/main.rs b/sample-cgi-script/src/main.rs index 729606b..5d8e9d8 100644 --- a/sample-cgi-script/src/main.rs +++ b/sample-cgi-script/src/main.rs @@ -23,7 +23,7 @@ async fn main() { cookies.add(Cookie::new("hello_world", "hello_world")); session.clear().await; session.insert("foo", "bar").await.unwrap(); - let value: String = session + let _value: String = session .get("foo") .await .unwrap()