Skip to content

support variable PATH_INFO in routes #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cgi-rs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
8 changes: 6 additions & 2 deletions cgi-rs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -11,15 +11,17 @@
//! ## 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.
//! std::env::set_var("REQUEST_METHOD", "GET");
//! std::env::set_var("CONTENT_LENGTH", "0");
//! std::env::set_var("REQUEST_URI", "/");
//!
//! let cgi_request: Request<Body> = CGIRequest::from_env()
//! let cgi_request: Request<Full<Bytes>> = CGIRequest::<Full<Bytes>>::from_env()
//! .and_then(Request::try_from).unwrap();
//!
//! assert_eq!(cgi_request.method(), "GET");
@@ -106,6 +108,7 @@ pub enum MetaVariableKind {
HttpHost,
HttpUserAgent,
HttpAccept,
HttpCookie,
ServerSignature,
DocumentRoot,
RequestScheme,
@@ -148,6 +151,7 @@ impl MetaVariableKind {
MetaVariableKind::ScriptFilename => "SCRIPT_FILENAME",
MetaVariableKind::RemotePort => "REMOTE_PORT",
MetaVariableKind::RequestUri => "REQUEST_URI",
MetaVariableKind::HttpCookie => "HTTP_COOKIE"
}
}

44 changes: 34 additions & 10 deletions cgi-rs/src/request.rs
Original file line number Diff line number Diff line change
@@ -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<B> {
pub request_body: B
}

impl CGIRequest {
pub fn from_env() -> Result<Self> {
impl <B> CGIRequest<B> where B: Body {
pub fn from_env() -> Result<CGIRequest<Full<Bytes>>> {
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<MetaVariable> {
@@ -44,11 +55,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()?
))
})
@@ -65,10 +87,11 @@ macro_rules! try_set_headers {
};
}

impl TryFrom<CGIRequest> for Request<HttpBody> {
impl <B>TryFrom<CGIRequest<B>> for Request<B> where B: Body {
type Error = CGIError;

fn try_from(cgi_request: CGIRequest) -> Result<Self> {
fn try_from(cgi_request: CGIRequest<B>) -> Result<Self> {

let mut request_builder = Request::builder()
.method(
cgi_request
@@ -84,6 +107,7 @@ impl TryFrom<CGIRequest> for Request<HttpBody> {
["Accept", MetaVariableKind::HttpAccept],
["Host", MetaVariableKind::HttpHost],
["User-Agent", MetaVariableKind::HttpUserAgent],
["Cookie", MetaVariableKind::HttpCookie],
);

request_builder
37 changes: 11 additions & 26 deletions cgi-rs/src/response.rs
Original file line number Diff line number Diff line change
@@ -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<B: hyper::body::HttpBody> {
headers: HeaderMap<HeaderValue>,
status: String,
reason: Option<String>,
body: B,
pub struct CGIResponse {
pub headers: HeaderMap<HeaderValue>,
pub status: String,
pub reason: Option<String>,
pub body: Bytes,
}

impl<B: hyper::body::HttpBody> CGIResponse<B> {
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<B: hyper::body::HttpBody> CGIResponse<B> {
}

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<B: hyper::body::HttpBody> TryFrom<Response<B>> for CGIResponse<B> {
type Error = CGIError;

fn try_from(response: Response<B>) -> Result<Self> {
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,
})
}
}
8 changes: 8 additions & 0 deletions http-cgi-server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "http-cgi-server"
version = "0.1.0"
edition = "2021"

[dependencies]

[workspace]
205 changes: 205 additions & 0 deletions http-cgi-server/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<String, String>,
) {
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();
}
6 changes: 5 additions & 1 deletion sample-cgi-script/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
54 changes: 51 additions & 3 deletions sample-cgi-script/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,60 @@
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};
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());

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);
5 changes: 3 additions & 2 deletions tower-cgi/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
31 changes: 24 additions & 7 deletions tower-cgi/src/lib.rs
Original file line number Diff line number Diff line change
@@ -17,22 +17,25 @@
//! ```
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.
///
/// Responses are emitted to stdout per the CGI RFC3875
pub async fn serve_cgi<S, B>(app: S) -> Result<()>
where
S: Service<Request<HttpBody>, Response = Response<B>, Error = Infallible>
S: Service<Request<Full<Bytes>>, Response = Response<B>, Error = Infallible>
+ Clone
+ Send
+ 'static,
B: hyper::body::HttpBody,
B: Body, <B as 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<S, B>(output: impl Write, app: S) -> Result<()>
where
S: Service<Request<HttpBody>, Response = Response<B>, Error = Infallible>
S: Service<Request<Full<Bytes>>, Response = Response<B>, Error = Infallible>
+ Clone
+ Send
+ 'static,
B: hyper::body::HttpBody,
B: Body, <B as Body>::Error: Debug
{
let request = CGIRequest::from_env()
let request = CGIRequest::<Full<Bytes>>::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<B> = 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