Skip to content
This repository was archived by the owner on Oct 26, 2022. It is now read-only.

Commit 915d7af

Browse files
committed
Hyper CGI
first implementation
0 parents  commit 915d7af

File tree

3 files changed

+210
-0
lines changed

3 files changed

+210
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/target
2+
Cargo.lock

Cargo.toml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "hyper_cgi"
3+
version = "0.1.0"
4+
authors = ["Christian Schilling <[email protected]>", "Louis-Marie Givel <[email protected]>"]
5+
edition = "2018"
6+
7+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8+
9+
[dependencies]
10+
futures = "*"
11+
tokio = {version = "*", features = ["full"] }
12+
tokio-util = { version = "0.3.1", features=["compat"] }
13+
hyper = "*"
14+
15+
[lib]
16+
name = "hyper_cgi"
17+
path = "src/hyper_cgi.rs"

src/hyper_cgi.rs

+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
//! This module implements a do_cgi function, to run CGI scripts with hyper
2+
use futures::TryStreamExt;
3+
use hyper::{Request, Response};
4+
use std::process::Stdio;
5+
use std::str::FromStr;
6+
use tokio::io::AsyncBufReadExt;
7+
use tokio::io::AsyncReadExt;
8+
use tokio::io::AsyncWriteExt;
9+
use tokio::io::BufReader;
10+
use tokio::process::Command;
11+
use tokio::stream::StreamExt;
12+
13+
/// do_cgi is an async function that takes an hyper request and a CGI compatible
14+
/// command, and passes the request to be executed to the command.
15+
/// It then returns an hyper response and the stderr output of the command.
16+
pub async fn do_cgi(
17+
req: Request<hyper::Body>,
18+
cmd: Command,
19+
) -> (hyper::http::Response<hyper::Body>, Vec<u8>) {
20+
let mut cmd = cmd;
21+
cmd.stdout(Stdio::piped());
22+
cmd.stderr(Stdio::piped());
23+
cmd.stdin(Stdio::piped());
24+
cmd.env("SERVER_SOFTWARE", "hyper")
25+
.env("SERVER_NAME", "localhost") // TODO
26+
.env("GATEWAY_INTERFACE", "CGI/1.1")
27+
.env("SERVER_PROTOCOL", "HTTP/1.1") // TODO
28+
.env("SERVER_PORT", "80") // TODO
29+
.env("REQUEST_METHOD", format!("{}", req.method()))
30+
.env("SCRIPT_NAME", "") // TODO
31+
.env("QUERY_STRING", req.uri().query().unwrap_or(""))
32+
.env("REMOTE_ADDR", "") // TODO
33+
.env("AUTH_TYPE", "") // TODO
34+
.env("REMOTE_USER", "") // TODO
35+
.env(
36+
"CONTENT_TYPE",
37+
req.headers()
38+
.get(hyper::header::CONTENT_TYPE)
39+
.map(|x| x.to_str().ok())
40+
.flatten()
41+
.unwrap_or(""),
42+
)
43+
.env(
44+
"HTTP_CONTENT_ENCODING",
45+
req.headers()
46+
.get(hyper::header::CONTENT_ENCODING)
47+
.map(|x| x.to_str().ok())
48+
.flatten()
49+
.unwrap_or(""),
50+
)
51+
.env(
52+
"CONTENT_LENGTH",
53+
req.headers()
54+
.get(hyper::header::CONTENT_LENGTH)
55+
.map(|x| x.to_str().ok())
56+
.flatten()
57+
.unwrap_or(""),
58+
);
59+
60+
let mut child = cmd.spawn().expect("can't spawn CGI command");
61+
let mut stdin = child.stdin.as_mut().expect("Failed to open stdin");
62+
let mut stdout = child.stdout.as_mut().expect("Failed to open stdout");
63+
let mut stderr = child.stderr.as_mut().expect("Failed to open stderr");
64+
65+
let req_body = req
66+
.into_body()
67+
.map(|result| {
68+
result.map_err(|_error| std::io::Error::new(std::io::ErrorKind::Other, "Error!"))
69+
})
70+
.into_async_read();
71+
72+
let mut req_body = to_tokio_async_read(req_body);
73+
let mut err_output = vec![];
74+
75+
let res = tokio::try_join!(
76+
async {
77+
println!("copy");
78+
tokio::io::copy(&mut req_body, &mut stdin).await?;
79+
println!("copy done");
80+
stdin.shutdown().await?;
81+
println!("done");
82+
Ok(())
83+
},
84+
{
85+
//tokio::io::copy(&mut stdout, &mut tokio::io::stdout()).await;
86+
println!("build response");
87+
88+
build_response(&mut stdout, &mut stderr, &mut err_output)
89+
}
90+
);
91+
92+
let (_, r2) = res.unwrap_or((
93+
(),
94+
Response::builder()
95+
.status(hyper::StatusCode::INTERNAL_SERVER_ERROR)
96+
.body(hyper::Body::empty())
97+
.unwrap(),
98+
));
99+
100+
(r2, err_output)
101+
}
102+
103+
fn to_tokio_async_read(r: impl futures::io::AsyncRead) -> impl tokio::io::AsyncRead {
104+
tokio_util::compat::FuturesAsyncReadCompatExt::compat(r)
105+
}
106+
107+
async fn build_response(
108+
stdout: &mut &mut tokio::process::ChildStdout,
109+
stderr: &mut &mut tokio::process::ChildStderr,
110+
err_output: &mut Vec<u8>,
111+
) -> Result<Response<hyper::Body>, std::io::Error> {
112+
let mut response = Response::builder();
113+
114+
let mut stdout = BufReader::new(stdout);
115+
let mut line = String::new();
116+
while stdout.read_line(&mut line).await.unwrap_or(0) > 0 {
117+
line = line
118+
.trim_end_matches("\n")
119+
.trim_end_matches("\r")
120+
.to_owned();
121+
122+
let l: Vec<&str> = line.splitn(2, ": ").collect();
123+
if l.len() < 2 {
124+
break;
125+
}
126+
if l[0] == "Status" {
127+
response = response.status(
128+
hyper::StatusCode::from_u16(
129+
u16::from_str(l[1].split(" ").next().unwrap_or("500")).unwrap_or(500),
130+
)
131+
.unwrap_or(hyper::StatusCode::INTERNAL_SERVER_ERROR),
132+
);
133+
} else {
134+
response = response.header(l[0], l[1]);
135+
}
136+
line = String::new();
137+
}
138+
139+
stderr.read_to_end(err_output).await.unwrap_or(0);
140+
141+
let mut data = vec![];
142+
stdout.read_to_end(&mut data).await.unwrap_or(0);
143+
144+
let body = response.body(hyper::Body::from(data));
145+
146+
convert_error_io_hyper(body)
147+
}
148+
149+
fn convert_error_io_hyper<T>(res: Result<T, hyper::http::Error>) -> Result<T, std::io::Error> {
150+
match res {
151+
Ok(res) => Ok(res),
152+
Err(_) => Err(std::io::Error::new(std::io::ErrorKind::Other, "Error!")),
153+
}
154+
}
155+
156+
#[cfg(test)]
157+
mod tests {
158+
use futures::TryStreamExt;
159+
160+
#[tokio::test]
161+
async fn run_cmd() {
162+
let body_content = "a body";
163+
164+
let req = hyper::Request::builder()
165+
.method("GET")
166+
.uri("/some/file?query=aquery")
167+
.version(hyper::Version::HTTP_11)
168+
.header("Host", "localhost:8001")
169+
.header("User-Agent", "test/2.25.1")
170+
.header("Accept", "*/*")
171+
.header("Accept-Encoding", "deflate, gzip, br")
172+
.header("Accept-Language", "en-US, *;q=0.9")
173+
.header("Pragma", "no-cache")
174+
.body(hyper::Body::from(body_content))
175+
.unwrap();
176+
177+
let mut cmd = tokio::process::Command::new("echo");
178+
cmd.arg("-n");
179+
cmd.arg("blabl:bl\r\na body");
180+
181+
let (rep, stderr) = super::do_cgi(req, cmd).await;
182+
let output = rep
183+
.into_body()
184+
.try_fold(String::new(), |mut acc, elt| async move {
185+
acc.push_str(std::str::from_utf8(&elt).unwrap());
186+
Ok(acc)
187+
}).await.unwrap();
188+
assert_eq!("", std::str::from_utf8(&stderr).unwrap());
189+
assert_eq!(body_content, output);
190+
}
191+
}

0 commit comments

Comments
 (0)