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/lib.rs b/cgi-rs/src/lib.rs index e4d9fa6..45eaaf2 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"); @@ -103,9 +105,11 @@ pub enum MetaVariableKind { // Not in the RFC, but emitted from httpd's mod_cgi UniqueID, + HttpAuthorization, HttpHost, HttpUserAgent, HttpAccept, + HttpCookie, ServerSignature, DocumentRoot, RequestScheme, @@ -119,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", @@ -148,6 +153,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 63c8128..58c23dc 100644 --- a/cgi-rs/src/request.rs +++ b/cgi-rs/src/request.rs @@ -1,14 +1,19 @@ -use crate::{error, CGIError, MetaVariable, MetaVariableKind, Result}; -use hyper::{Body as HttpBody, Request}; +use crate::{error, MetaVariable, MetaVariableKind, Result}; +use http_body_util::Full; +use hyper::body::{Body, Bytes}; +use hyper::Request; use snafu::ResultExt; use std::io::{stdin, Read}; -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 +24,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 { @@ -44,11 +56,17 @@ 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()? )) }) @@ -65,10 +83,13 @@ macro_rules! try_set_headers { }; } -impl TryFrom for Request { - type Error = CGIError; +impl TryFrom> for Request +where + B: Body, +{ + type Error = crate::CGIError; - fn try_from(cgi_request: CGIRequest) -> Result { + fn try_from(cgi_request: CGIRequest) -> Result { let mut request_builder = Request::builder() .method( cgi_request @@ -81,9 +102,11 @@ impl TryFrom for Request { request_builder, cgi_request, ["Content-Length", MetaVariableKind::ContentLength], + ["Authorization", MetaVariableKind::HttpAuthorization], ["Accept", MetaVariableKind::HttpAccept], ["Host", MetaVariableKind::HttpHost], ["User-Agent", MetaVariableKind::HttpUserAgent], + ["Cookie", MetaVariableKind::HttpCookie], ); request_builder diff --git a/cgi-rs/src/response.rs b/cgi-rs/src/response.rs index 616fff0..79c5a1c 100644 --- a/cgi-rs/src/response.rs +++ b/cgi-rs/src/response.rs @@ -1,17 +1,18 @@ -use crate::{error, CGIError, Result}; -use hyper::{http::HeaderValue, HeaderMap, Response}; +use crate::{error, Result}; +use bytes::Bytes; +use hyper::{http::HeaderValue, HeaderMap}; use snafu::ResultExt; use std::io::Write; #[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 +51,12 @@ 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/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(); +} diff --git a/sample-cgi-script/Cargo.toml b/sample-cgi-script/Cargo.toml index 12561c6..5ec4abc 100644 --- a/sample-cgi-script/Cargo.toml +++ b/sample-cgi-script/Cargo.toml @@ -6,6 +6,10 @@ license = "Apache-2.0" publish = false [dependencies] -axum = "0.6" +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 = "*" +rusqlite = "0.33.0" diff --git a/sample-cgi-script/src/main.rs b/sample-cgi-script/src/main.rs index 4495f83..5d8e9d8 100644 --- a/sample-cgi-script/src/main.rs +++ b/sample-cgi-script/src/main.rs @@ -1,12 +1,66 @@ use axum::{routing::get, Router}; +use rusqlite::Connection; use tower_cgi::serve_cgi; +use tower_cookies::{Cookie, Cookies}; +use tower_sessions::cookie::time::Duration; +use tower_sessions::Session; + +use tower_sessions_file_based_store::FileStore; #[tokio::main] async fn main() { - let app = Router::new().route( - "/cgi-bin/sample-cgi-server", - get(|| async { "Hello, World!" }), - ); + 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(|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); if let Err(e) = serve_cgi(app).await { eprintln!("Error while serving CGI request: {}", e); 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..7339841 100644 --- a/tower-cgi/src/lib.rs +++ b/tower-cgi/src/lib.rs @@ -17,9 +17,12 @@ //! ``` use cgi_rs::{CGIError, CGIRequest, CGIResponse}; -use hyper::{Body as HttpBody, Request, Response}; +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 tower::{Service, ServiceExt}; @@ -28,11 +31,12 @@ 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 +46,14 @@ 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 +62,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