|
| 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