cgi/
lib.rs

1//! Easily create CGI (RFC 3875) programmes in Rust based on hyper's [`http`](https://github.com/hyperium/http) types.
2//!
3//! # Installation & Usage
4//!
5//! `Cargo.toml`:
6//!
7//! ```cargo,ignore
8//! [dependencies]
9//! cgi2 = "0.7"
10//! ```
11//!
12//!
13//! Use the [`cgi::main`] macro on your `main` function, taking in a [`Request`] and returning a [`Response`].
14//!
15//! ```rust,no_run
16//! #[cgi::main]
17//! fn main(request: cgi::Request) -> cgi::Response {
18//!      cgi::text_response(200, "Hello World")
19//! }
20//! ```
21//!
22//! This also works if you return a `Result`
23//! If your function returns a `Result` the error is printed to `stderr`
24//! and an HTTP 500 error is returned.
25//!
26//! ```rust,no_run
27//! #[cgi::main]
28//! fn main(request: cgi::Request) -> Result<cgi::Response, String> {
29//!     let greeting = std::fs::read_to_string("greeting.txt").map_err(|_| "Couldn't open file")?;
30//!
31//!     Ok(cgi::text_response(200, greeting))
32//! }
33//! ```
34//!
35//! It will parse & extract the CGI environmental variables and the HTTP request body to create
36//! an `Request`, call your function to create a response, and convert your `Response` into the
37//! correct format and print to stdout. If this programme is not called as CGI (e.g. missing
38//! required environmental variables), it will panic.
39//!
40//! It is also possible to call the `cgi::handle` function directly inside your `main` function:
41//!
42//! ```rust,ignore
43//! fn main() {
44//!     cgi::handle(|request: cgi::Request| -> cgi::Response {
45//!         cgi::empty_response(404)
46//!     });
47//! }
48//! ```
49//!
50//! Several shortcut functions are provided (such as [`html_response`]/[`binary_response`]).
51
52
53use std::io::{Read, Write, stdin};
54use std::collections::HashMap;
55use std::convert::TryFrom;
56
57pub extern crate http;
58
59/// A `Vec<u8>` Request from http
60pub type Request = http::Request<Vec<u8>>;
61
62/// A `Vec<u8>` Response from http
63pub type Response = http::Response<Vec<u8>>;
64
65/// Call a function as a CGI programme.
66///
67/// This should be called from a `main` function.
68/// Parse & extract the CGI environmental variables, and HTTP request body,
69/// to create `Request`, and convert your `Response` into the correct format and
70/// print to stdout. If this programme is not called as CGI (e.g. missing required
71/// environmental variables), it will panic.
72pub fn handle<F>(func: F)
73    where F: FnOnce(Request) -> Response
74{
75    let env_vars: HashMap<String, String> = std::env::vars().collect();
76
77    // How many bytes do we have to read for request body
78    // A general stdin().read_to_end() can block if the webserver doesn't close things
79    let content_length: usize = env_vars.get("CONTENT_LENGTH")
80        .and_then(|cl| cl.parse::<usize>().ok()).unwrap_or(0);
81
82    let mut stdin_contents = vec![0; content_length];
83    stdin().read_exact(&mut stdin_contents).unwrap();
84
85    let request = parse_request(env_vars, stdin_contents);
86
87    let response = func(request);
88
89    let output = serialize_response(response);
90
91    std::io::stdout().write_all(&output).unwrap();
92}
93
94#[doc(inline)]
95pub use cgi_attributes::main;
96
97pub fn err_to_500<E>(res: Result<Response, E>) -> Response {
98    res.unwrap_or(empty_response(500))
99}
100
101/// A HTTP Reponse with no body and that HTTP status code, e.g. `return cgi::empty_response(404);`
102/// to return a [HTTP 404 Not Found](https://en.wikipedia.org/wiki/HTTP_404).
103pub fn empty_response<T>(status_code: T) -> Response
104    where http::StatusCode: TryFrom<T>,
105          <http::StatusCode as TryFrom<T>>::Error: Into<http::Error>
106{
107    http::response::Builder::new().status(status_code).body(vec![]).unwrap()
108}
109
110/// Converts `text` to bytes (UTF8) and sends that as the body with that `status_code` and HTML
111/// `Content-Type` header (`text/html`)
112pub fn html_response<T, S>(status_code: T, body: S) -> Response
113    where http::StatusCode: TryFrom<T>,
114          <http::StatusCode as TryFrom<T>>::Error: Into<http::Error>,
115          S: Into<String>
116{
117    let body: Vec<u8> = body.into().into_bytes();
118    http::response::Builder::new()
119        .status(status_code)
120        .header(http::header::CONTENT_TYPE, "text/html; charset=utf-8")
121        .header(http::header::CONTENT_LENGTH, format!("{}", body.len()).as_str())
122        .body(body)
123        .unwrap()
124}
125
126/// Convert to a string and return that with the status code
127pub fn string_response<T, S>(status_code: T, body: S) -> Response
128    where http::StatusCode: TryFrom<T>,
129          <http::StatusCode as TryFrom<T>>::Error: Into<http::Error>,
130          S: Into<String>
131{
132    let body: Vec<u8> = body.into().into_bytes();
133    http::response::Builder::new()
134        .status(status_code)
135        .header(http::header::CONTENT_LENGTH, format!("{}", body.len()).as_str())
136        .body(body)
137        .unwrap()
138}
139
140
141/// Serves this content as `text/plain` text response, with that status code
142///
143/// ```rust,ignore
144/// extern crate cgi;
145///
146/// cgi::cgi_main! { |request: cgi::Request| -> cgi::Response {
147///   cgi::text_response(200, "Hello world");
148/// } }
149/// ```
150pub fn text_response<T, S>(status_code: T, body: S) -> Response
151    where http::StatusCode: TryFrom<T>,
152          <http::StatusCode as TryFrom<T>>::Error: Into<http::Error>,
153          S: Into<String>
154{
155    let body: Vec<u8> = body.into().into_bytes();
156    http::response::Builder::new()
157        .status(status_code)
158        .header(http::header::CONTENT_LENGTH, format!("{}", body.len()).as_str())
159        .header(http::header::CONTENT_TYPE, "text/plain; charset=utf-8")
160        .body(body)
161        .unwrap()
162}
163
164
165/// Sends  `blob` with that status code, and optional content type, `None` for no `Content-Type`
166/// header to be set.
167///
168/// No `Content-Type` header:
169///
170/// ```rust,ignore
171/// cgi::binary_response(200, None, vec![1, 2]);
172/// ```
173///
174/// Send an image:
175///
176/// ```rust,ignore
177/// cgi::binary_response(200, "image/png", vec![1, 2]);
178/// ```
179///
180/// Send a generic binary blob:
181///
182/// ```rust,ignore
183/// cgi::binary_response(200, "application/octet-stream", vec![1, 2]);
184/// ```
185pub fn binary_response<'a, T>(status_code: T, content_type: impl Into<Option<&'a str>>, body: Vec<u8>) -> Response
186    where http::StatusCode: TryFrom<T>,
187          <http::StatusCode as TryFrom<T>>::Error: Into<http::Error>,
188{
189    let content_type: Option<&str> = content_type.into();
190
191    let mut response = http::response::Builder::new()
192        .status(status_code)
193        .header(http::header::CONTENT_LENGTH, format!("{}", body.len()).as_str());
194
195    if let Some(ct)  = content_type {
196        response = response.header(http::header::CONTENT_TYPE, ct);
197    }
198
199    response.body(body).unwrap()
200}
201
202
203fn parse_request(env_vars: HashMap<String, String>, stdin: Vec<u8>) -> Request {
204    let mut req = http::Request::builder();
205
206    let method = env_vars.get("REQUEST_METHOD").expect("no REQUEST_METHOD set");
207    req = req.method(method.as_str());
208    let uri = if env_vars.get("QUERY_STRING").unwrap_or(&"".to_owned()) != "" {
209        format!("{}?{}", env_vars["SCRIPT_NAME"], env_vars["QUERY_STRING"])
210    } else {
211        env_vars["SCRIPT_NAME"].to_owned()
212    };
213    req = req.uri(uri.as_str());
214
215    if let Some(v) = env_vars.get("SERVER_PROTOCOL") {
216        if v == "HTTP/0.9" {
217            req = req.version(http::version::Version::HTTP_09);
218        } else if v == "HTTP/1.0" {
219            req = req.version(http::version::Version::HTTP_10);
220        } else if v == "HTTP/1.1" {
221            req = req.version(http::version::Version::HTTP_11);
222        } else if v == "HTTP/2.0" {
223            req = req.version(http::version::Version::HTTP_2);
224        } else {
225            unimplemented!("Unsupport HTTP SERVER_PROTOCOL {:?}", v);
226        }
227    }
228
229    for key in env_vars.keys().filter(|k| k.starts_with("HTTP_")) {
230        let header: String = key.chars().skip(5).map(|c| if c == '_' { '-' } else { c }).collect();
231        req = req.header(header.as_str(), env_vars[key].as_str().trim());
232    }
233
234
235    req = add_header(req, &env_vars, "AUTH_TYPE", "X-CGI-Auth-Type");
236    req = add_header(req, &env_vars, "CONTENT_LENGTH", "X-CGI-Content-Length");
237    req = add_header(req, &env_vars, "CONTENT_TYPE", "X-CGI-Content-Type");
238    req = add_header(req, &env_vars, "GATEWAY_INTERFACE", "X-CGI-Gateway-Interface");
239    req = add_header(req, &env_vars, "PATH_INFO", "X-CGI-Path-Info");
240    req = add_header(req, &env_vars, "PATH_TRANSLATED", "X-CGI-Path-Translated");
241    req = add_header(req, &env_vars, "QUERY_STRING", "X-CGI-Query-String");
242    req = add_header(req, &env_vars, "REMOTE_ADDR", "X-CGI-Remote-Addr");
243    req = add_header(req, &env_vars, "REMOTE_HOST", "X-CGI-Remote-Host");
244    req = add_header(req, &env_vars, "REMOTE_IDENT", "X-CGI-Remote-Ident");
245    req = add_header(req, &env_vars, "REMOTE_USER", "X-CGI-Remote-User");
246    req = add_header(req, &env_vars, "REQUEST_METHOD", "X-CGI-Request-Method");
247    req = add_header(req, &env_vars, "SCRIPT_NAME", "X-CGI-Script-Name");
248    req = add_header(req, &env_vars, "SERVER_PORT", "X-CGI-Server-Port");
249    req = add_header(req, &env_vars, "SERVER_PROTOCOL", "X-CGI-Server-Protocol");
250    req = add_header(req, &env_vars, "SERVER_SOFTWARE", "X-CGI-Server-Software");
251
252    req.body(stdin).unwrap()
253
254}
255
256// add the CGI request meta-variables as X-CGI- headers
257fn add_header(req: http::request::Builder, env_vars: &HashMap<String, String>, meta_var: &str, target_header: &str) -> http::request::Builder {
258    if let Some(var) = env_vars.get(meta_var) {
259        req.header(target_header, var.as_str())
260    } else {
261        req
262    }
263}
264
265/// Convert the Request into the appropriate stdout format
266fn serialize_response(response: Response) -> Vec<u8> {
267    let mut output = String::new();
268    output.push_str("Status: ");
269    output.push_str(response.status().as_str());
270    if let Some(reason) = response.status().canonical_reason() {
271        output.push_str(" ");
272        output.push_str(reason);
273    }
274    output.push_str("\n");
275
276    {
277        let headers = response.headers();
278        let mut keys: Vec<&http::header::HeaderName> = headers.keys().collect();
279        keys.sort_by_key(|h| h.as_str());
280        for key in keys {
281            output.push_str(key.as_str());
282            output.push_str(": ");
283            output.push_str(headers.get(key).unwrap().to_str().unwrap());
284            output.push_str("\n");
285        }
286    }
287
288    output.push_str("\n");
289
290    let mut output = output.into_bytes();
291
292    let (_, mut body) = response.into_parts();
293
294    output.append(&mut body);
295
296    output
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    fn env(input: Vec<(&str, &str)>) -> HashMap<String, String> {
304        input.into_iter().map(|(a, b)| (a.to_owned(), b.to_owned())).collect()
305    }
306
307    #[test]
308    fn test_parse_request() {
309        let env_vars = env(vec![
310           ("REQUEST_METHOD", "GET"), ("SCRIPT_NAME", "/my/path/script"),
311           ("SERVER_PROTOCOL", "HTTP/1.0"), ("HTTP_USER_AGENT", "MyBrowser/1.0"),
312           ("QUERY_STRING", "foo=bar&baz=bop"),
313           ]);
314        let stdin = Vec::new();
315        let req = parse_request(env_vars, stdin);
316        assert_eq!(req.method(), &http::method::Method::GET);
317        assert_eq!(req.uri(), "/my/path/script?foo=bar&baz=bop");
318        assert_eq!(req.uri().path(), "/my/path/script");
319        assert_eq!(req.uri().query(), Some("foo=bar&baz=bop"));
320        assert_eq!(req.version(), http::version::Version::HTTP_10);
321        assert_eq!(req.headers()[http::header::USER_AGENT], "MyBrowser/1.0");
322        assert_eq!(req.body(), &vec![] as &Vec<u8>);
323    }
324
325    fn test_serialized_response(resp: http::response::Builder, body: &str, expected_output: &str) {
326        let resp: Response = resp.body(String::from(body).into_bytes()).unwrap();
327        let output = serialize_response(resp);
328        let expected_output = String::from(expected_output).into_bytes();
329
330        if output != expected_output {
331            println!("output: {}\nexptected: {}", std::str::from_utf8(&output).unwrap(), std::str::from_utf8(&expected_output).unwrap());
332        }
333
334        assert_eq!(output, expected_output);
335    }
336
337    #[test]
338    fn test_serialized_response1() {
339        test_serialized_response(
340            http::Response::builder().status(200),
341            "Hello World",
342            "Status: 200 OK\n\nHello World"
343        );
344
345        test_serialized_response(
346            http::Response::builder().status(200)
347                .header("Content-Type", "text/html")
348                .header("Content-Language", "en")
349                .header("Cache-Control", "max-age=3600"),
350            "<html><body><h1>Hello</h1></body></html>",
351            "Status: 200 OK\ncache-control: max-age=3600\ncontent-language: en\ncontent-type: text/html\n\n<html><body><h1>Hello</h1></body></html>"
352        );
353    }
354
355    #[test]
356    fn test_shortcuts1() {
357        assert_eq!(std::str::from_utf8(&serialize_response(html_response(200, "<html><body><h1>Hello World</h1></body></html>"))).unwrap(),
358            "Status: 200 OK\ncontent-length: 46\ncontent-type: text/html; charset=utf-8\n\n<html><body><h1>Hello World</h1></body></html>"
359        );
360    }
361
362    #[test]
363    fn test_shortcuts2() {
364        assert_eq!(std::str::from_utf8(&serialize_response(binary_response(200, None, vec![65, 66, 67]))).unwrap(),
365            "Status: 200 OK\ncontent-length: 3\n\nABC"
366        );
367
368        assert_eq!(std::str::from_utf8(&serialize_response(binary_response(200, "application/octet-stream", vec![65, 66, 67]))).unwrap(),
369            "Status: 200 OK\ncontent-length: 3\ncontent-type: application/octet-stream\n\nABC"
370        );
371
372        let ct: String = "image/png".to_string();
373        assert_eq!(std::str::from_utf8(&serialize_response(binary_response(200, ct.as_str(), vec![65, 66, 67]))).unwrap(),
374            "Status: 200 OK\ncontent-length: 3\ncontent-type: image/png\n\nABC"
375        );
376    }
377
378}