1use crate::parse::{is_field_value, is_token_str};
4
5#[derive(Debug, Default)]
7pub enum Body {
8 #[default]
10 Empty,
11 Static(&'static [u8]),
14 Owned(Vec<u8>),
16}
17
18impl Body {
19 fn as_bytes(&self) -> &[u8] {
20 match self {
21 Body::Empty => &[],
22 Body::Static(bytes) => bytes,
23 Body::Owned(bytes) => bytes,
24 }
25 }
26}
27
28impl From<&'static str> for Body {
29 fn from(s: &'static str) -> Self {
30 Body::Static(s.as_bytes())
31 }
32}
33
34impl From<&'static [u8]> for Body {
35 fn from(s: &'static [u8]) -> Self {
36 Body::Static(s)
37 }
38}
39
40impl From<String> for Body {
41 fn from(s: String) -> Self {
42 Body::Owned(s.into_bytes())
43 }
44}
45
46impl From<Vec<u8>> for Body {
47 fn from(v: Vec<u8>) -> Self {
48 Body::Owned(v)
49 }
50}
51
52#[derive(Debug)]
62pub struct Response {
63 status: u16,
64 headers: Vec<u8>,
66 body: Body,
67}
68
69impl Response {
70 pub fn new(status: u16) -> Self {
77 assert!(
78 (200..=599).contains(&status),
79 "giong-http: invalid status code {status}"
80 );
81 Self {
82 status,
83 headers: Vec::new(),
84 body: Body::Empty,
85 }
86 }
87
88 pub fn text(body: impl Into<Body>) -> Self {
90 Self::new(200)
91 .header("Content-Type", "text/plain; charset=utf-8")
92 .body(body)
93 }
94
95 pub fn json(body: impl Into<Body>) -> Self {
97 Self::new(200)
98 .header("Content-Type", "application/json")
99 .body(body)
100 }
101
102 pub fn header(mut self, name: &str, value: &str) -> Self {
113 assert!(
114 is_token_str(name.as_bytes()),
115 "giong-http: invalid header name {name:?}"
116 );
117 assert!(
118 is_field_value(value.as_bytes()),
119 "giong-http: invalid value for header {name}: {value:?}"
120 );
121 let managed = ["content-length", "transfer-encoding", "connection", "date"];
122 if !managed.iter().any(|m| name.eq_ignore_ascii_case(m)) {
123 self.headers.extend_from_slice(name.as_bytes());
124 self.headers.extend_from_slice(b": ");
125 self.headers.extend_from_slice(value.trim().as_bytes());
126 self.headers.extend_from_slice(b"\r\n");
127 }
128 self
129 }
130
131 pub fn body(mut self, body: impl Into<Body>) -> Self {
133 self.body = body.into();
134 self
135 }
136
137 pub fn status(&self) -> u16 {
139 self.status
140 }
141
142 pub(crate) fn write_to(&self, out: &mut Vec<u8>, minor: u8, keep_alive: bool, head_only: bool) {
144 let body = self.body.as_bytes();
145 out.reserve(128 + self.headers.len() + body.len());
146 out.extend_from_slice(if minor == 0 {
147 b"HTTP/1.0 "
148 } else {
149 b"HTTP/1.1 "
150 });
151 push_number(out, self.status as usize);
152 out.push(b' ');
153 out.extend_from_slice(reason(self.status).as_bytes());
154 out.extend_from_slice(b"\r\nDate: ");
155 out.extend_from_slice(&crate::date::now());
156 out.extend_from_slice(b"\r\n");
157 let bodiless = matches!(self.status, 204 | 304);
159 if !bodiless {
160 out.extend_from_slice(b"Content-Length: ");
161 push_number(out, body.len());
162 out.extend_from_slice(b"\r\n");
163 }
164 out.extend_from_slice(&self.headers);
165 if !keep_alive {
166 out.extend_from_slice(b"Connection: close\r\n");
167 } else if minor == 0 {
168 out.extend_from_slice(b"Connection: keep-alive\r\n");
169 }
170 out.extend_from_slice(b"\r\n");
171 if !head_only && !bodiless {
172 out.extend_from_slice(body);
173 }
174 }
175}
176
177fn push_number(out: &mut Vec<u8>, mut n: usize) {
178 let mut digits = [0u8; 20];
179 let mut at = digits.len();
180 loop {
181 at -= 1;
182 digits[at] = b'0' + (n % 10) as u8;
183 n /= 10;
184 if n == 0 {
185 break;
186 }
187 }
188 out.extend_from_slice(&digits[at..]);
189}
190
191pub(crate) fn reason(status: u16) -> &'static str {
192 match status {
193 200 => "OK",
194 201 => "Created",
195 202 => "Accepted",
196 204 => "No Content",
197 206 => "Partial Content",
198 301 => "Moved Permanently",
199 302 => "Found",
200 303 => "See Other",
201 304 => "Not Modified",
202 307 => "Temporary Redirect",
203 308 => "Permanent Redirect",
204 400 => "Bad Request",
205 401 => "Unauthorized",
206 403 => "Forbidden",
207 404 => "Not Found",
208 405 => "Method Not Allowed",
209 406 => "Not Acceptable",
210 408 => "Request Timeout",
211 409 => "Conflict",
212 410 => "Gone",
213 411 => "Length Required",
214 412 => "Precondition Failed",
215 413 => "Content Too Large",
216 414 => "URI Too Long",
217 415 => "Unsupported Media Type",
218 417 => "Expectation Failed",
219 422 => "Unprocessable Content",
220 429 => "Too Many Requests",
221 431 => "Request Header Fields Too Large",
222 500 => "Internal Server Error",
223 501 => "Not Implemented",
224 502 => "Bad Gateway",
225 503 => "Service Unavailable",
226 504 => "Gateway Timeout",
227 505 => "HTTP Version Not Supported",
228 _ => match status / 100 {
229 2 => "OK",
230 3 => "Redirection",
231 4 => "Client Error",
232 _ => "Server Error",
233 },
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 fn wire(resp: &Response, minor: u8, keep_alive: bool, head_only: bool) -> String {
242 let mut out = Vec::new();
243 resp.write_to(&mut out, minor, keep_alive, head_only);
244 let text = String::from_utf8(out).unwrap();
245 let start = text.find("Date: ").unwrap();
247 let end = start + text[start..].find("\r\n").unwrap() + 2;
248 assert_eq!(end - start, 6 + 29 + 2);
249 format!("{}{}", &text[..start], &text[end..])
250 }
251
252 #[test]
253 fn serialises_status_headers_and_body() {
254 let resp = Response::json(r#"{"a":1}"#).header("X-Id", " 7 ");
255 assert_eq!(
256 wire(&resp, 1, true, false),
257 "HTTP/1.1 200 OK\r\nContent-Length: 7\r\nContent-Type: application/json\r\nX-Id: 7\r\n\r\n{\"a\":1}"
258 );
259 assert_eq!(
260 wire(&Response::new(404), 1, false, false),
261 "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
262 );
263 assert_eq!(
264 wire(&Response::text(vec![b'x'; 12345]), 0, true, true),
265 "HTTP/1.0 200 OK\r\nContent-Length: 12345\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: keep-alive\r\n\r\n"
266 );
267 assert_eq!(
268 wire(&Response::new(204).body("ignored"), 1, true, false),
269 "HTTP/1.1 204 No Content\r\n\r\n"
270 );
271 assert_eq!(
272 wire(&Response::new(599), 1, true, false),
273 "HTTP/1.1 599 Server Error\r\nContent-Length: 0\r\n\r\n"
274 );
275 }
276
277 #[test]
278 fn managed_headers_cannot_be_overridden() {
279 let resp = Response::new(200)
280 .header("content-length", "999")
281 .header("Connection", "keep-alive")
282 .header("Transfer-Encoding", "chunked")
283 .header("Date", "yesterday")
284 .body("ok");
285 assert_eq!(
286 wire(&resp, 1, true, false),
287 "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok"
288 );
289 }
290
291 #[test]
292 #[should_panic(expected = "invalid value for header")]
293 fn header_values_cannot_split_the_response() {
294 let _ = Response::new(200).header("X-User", "a\r\nSet-Cookie: stolen=1");
295 }
296
297 #[test]
298 #[should_panic(expected = "invalid header name")]
299 fn header_names_must_be_tokens() {
300 let _ = Response::new(200).header("Bad Name", "1");
301 }
302
303 #[test]
304 #[should_panic(expected = "invalid status code")]
305 fn interim_status_codes_are_rejected() {
306 let _ = Response::new(100);
307 }
308}