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