1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
use crate::status;
use std::cell::OnceCell;
use std::collections::BTreeMap;
use std::io::{self, Write};
use std::time::Instant;
/// A FastCGI request
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Request {
pub(crate) method: String,
pub(crate) path: String,
pub(crate) query_string: String,
pub(crate) headers: BTreeMap<String, String>,
pub(crate) body: Vec<u8>,
pub(crate) created_at: Instant,
pub(crate) query: OnceCell<BTreeMap<String, String>>,
}
impl Default for Request {
fn default() -> Self {
Self {
method: String::new(),
path: String::new(),
query_string: String::new(),
headers: BTreeMap::new(),
body: Vec::new(),
created_at: Instant::now(),
query: OnceCell::new(),
}
}
}
impl Request {
/// Returns the request method
pub fn method(&self) -> &str {
self.method.as_str()
}
/// Returns the request path
pub fn path(&self) -> &str {
self.path.as_str()
}
/// Looks up the header value associated with `key`, if any
pub fn header(&self, key: &str) -> Option<&str> {
self.headers.get(key).map(String::as_str)
}
/// Returns a reference to the request body
pub fn body(&self) -> &[u8] {
self.body.as_slice()
}
/// Returns the request body as an owned `Vec`
///
/// Once the request body has been `take`n, subsequent calls return an empty `Vec`
pub fn take_body(&mut self) -> Vec<u8> {
std::mem::take(&mut self.body)
}
}
impl Request {
fn parse_query(qs: &str) -> BTreeMap<String, String> {
let mut query = BTreeMap::new();
for (k, v) in form_urlencoded::parse(qs.as_bytes()) {
query.insert(k.to_string(), v.to_string());
}
query
}
/// Returns the value of `key` from the parsed query string
pub fn query(&self, key: &str) -> Option<&str> {
let map = self
.query
.get_or_init(|| Self::parse_query(&self.query_string));
map.get(key).map(String::as_str)
}
}
/// A FastCGI response
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
pub(crate) status: u16,
pub(crate) headers: BTreeMap<String, String>,
pub(crate) body: Vec<u8>,
}
impl Default for Response {
fn default() -> Self {
Self {
// The CGI RFC says this is the default if no status is provided
status: 200,
headers: BTreeMap::new(),
body: Vec::new(),
}
}
}
impl Response {
/// Create an empty 200 OK FastCGI response
pub fn new() -> Self {
Self::default()
}
/// Sets the response header `key` to `value`
///
/// If `key` was already present in the map, the value is updated
pub fn set_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(key.into(), value.into());
self
}
/// Sets the status code of the response to `code`
pub fn set_status(mut self, code: u16) -> Self {
self.status = code;
self
}
/// Sets the response body
pub fn set_body(self, body: impl Into<String>) -> Self {
self.set_raw_body(body.into().into_bytes())
}
/// Sets the response body in bytes
pub fn set_raw_body(mut self, body: Vec<u8>) -> Self {
self.body = body;
self
}
fn of_content_type(content_type: &str, value: impl Into<String>) -> Self {
Response::default()
.set_header("Content-Type", content_type)
.set_body(value)
}
/// Returns a new json response with the given value
pub fn json(json: impl Into<String>) -> Self {
Self::of_content_type("application/json", json)
}
/// Returns a new plain-text response with the given value
pub fn text(plaintext: impl Into<String>) -> Self {
Self::of_content_type("text/plain", plaintext)
}
/// Returns a new html response with the given value
pub fn html(html: impl Into<String>) -> Self {
Self::of_content_type("text/html", html)
}
/// Returns a new response that will trigger a temporary redirect
///
/// The browser receiving the request will re-make the request with `path` as the new target
/// with method and body unchanged.
///
/// Search engines receiving this response will not attribute links to the original URL to the
/// new resource, meaning no SEO value is transferred to the new URL.
pub fn temporary_redirect(path: impl Into<String>) -> Self {
Response::default()
.set_header("Location", path)
.set_status(status::TEMPORARY_REDIRECT)
}
/// Returns a new response that will trigger a permanent redirect
///
/// The browser receiving the request will re-make the request with `path` as the new target
/// with method and body unchanged.
///
/// Search engines receiving this response will attribute links to the original URL to the
/// redirected resource, passing the SEO ranking to the new URL.
pub fn permanent_redirect(path: impl Into<String>) -> Self {
Response::default()
.set_header("Location", path)
.set_status(status::PERMANENT_REDIRECT)
}
pub(crate) fn write_stdout_bytes<W: Write>(&self, writer: &mut W) -> Result<(), io::Error> {
for (key, value) in self.headers.iter() {
writeln!(writer, "{key}: {value}")?;
}
writeln!(writer, "Status: {}", self.status)?;
writeln!(writer)?;
writer.write_all(&self.body)
}
}