moirai_http/server/
response.rs1use std::io;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct HttpResponse {
8 pub(super) status: u16,
9 pub(super) headers: Vec<(String, String)>,
10 pub(super) body: Vec<u8>,
11}
12
13impl HttpResponse {
14 pub fn new<B: Into<Vec<u8>>>(status: u16, body: B) -> io::Result<Self> {
20 if !(200..=599).contains(&status) {
21 return Err(invalid_input("HTTP response status must be three digits"));
22 }
23 Ok(Self {
24 status,
25 headers: Vec::new(),
26 body: body.into(),
27 })
28 }
29
30 pub fn set_header<N: Into<String>, V: Into<String>>(
39 &mut self,
40 name: N,
41 value: V,
42 ) -> io::Result<()> {
43 let name = name.into();
44 let value = value.into();
45 validate_header_name(&name)?;
46 validate_header_value(&value)?;
47 if matches!(
48 name.to_ascii_lowercase().as_str(),
49 "content-length" | "connection" | "transfer-encoding"
50 ) {
51 return Err(invalid_input(
52 "HTTP response framing header is transport-owned",
53 ));
54 }
55 if self
56 .headers
57 .iter()
58 .any(|(existing, _)| existing.eq_ignore_ascii_case(&name))
59 {
60 return Err(invalid_input("duplicate HTTP response header"));
61 }
62 self.headers.push((name, value));
63 Ok(())
64 }
65
66 #[must_use]
68 pub const fn status(&self) -> u16 {
69 self.status
70 }
71
72 #[must_use]
74 pub fn headers(&self) -> &[(String, String)] {
75 &self.headers
76 }
77
78 #[must_use]
80 pub fn body(&self) -> &[u8] {
81 &self.body
82 }
83}
84
85pub(super) fn encode_response_head(
86 response: &HttpResponse,
87 max_response_bytes: usize,
88) -> io::Result<Vec<u8>> {
89 let status = response.status;
90 let reason = reason_phrase(status);
91 let mut head = Vec::new();
92 let status_line = format!("HTTP/1.1 {status} {reason}\r\n");
93 head.try_reserve(status_line.len())
94 .map_err(|_| invalid_data("HTTP response header allocation exceeds available memory"))?;
95 head.extend_from_slice(status_line.as_bytes());
96 for (name, value) in &response.headers {
97 let line_length = name
98 .len()
99 .checked_add(2)
100 .and_then(|length| length.checked_add(value.len()))
101 .and_then(|length| length.checked_add(2))
102 .ok_or_else(|| invalid_data("HTTP response header size overflows its bound"))?;
103 let projected = head
104 .len()
105 .checked_add(line_length)
106 .ok_or_else(|| invalid_data("HTTP response header size overflows its bound"))?;
107 if projected > max_response_bytes {
108 return Err(invalid_data("HTTP response exceeds configured byte bound"));
109 }
110 head.extend_from_slice(name.as_bytes());
111 head.extend_from_slice(b": ");
112 head.extend_from_slice(value.as_bytes());
113 head.extend_from_slice(b"\r\n");
114 }
115 let framing = format!(
116 "Content-Length: {}\r\nConnection: close\r\n\r\n",
117 response.body.len()
118 );
119 let projected = head
120 .len()
121 .checked_add(framing.len())
122 .ok_or_else(|| invalid_data("HTTP response header size overflows its bound"))?;
123 if projected > max_response_bytes {
124 return Err(invalid_data("HTTP response exceeds configured byte bound"));
125 }
126 head.extend_from_slice(framing.as_bytes());
127 Ok(head)
128}
129
130fn validate_header_name(name: &str) -> io::Result<()> {
131 if name.is_empty()
132 || !name
133 .bytes()
134 .all(|byte| byte.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&byte))
135 {
136 return Err(invalid_input("HTTP response header name is not a token"));
137 }
138 Ok(())
139}
140
141fn validate_header_value(value: &str) -> io::Result<()> {
142 if value
143 .bytes()
144 .any(|byte| byte < 0x20 && byte != b'\t' || byte == 0x7f)
145 {
146 return Err(invalid_input(
147 "HTTP response header value contains a control byte",
148 ));
149 }
150 Ok(())
151}
152
153fn reason_phrase(status: u16) -> &'static str {
154 match status {
155 200 => "OK",
156 201 => "Created",
157 202 => "Accepted",
158 204 => "No Content",
159 206 => "Partial Content",
160 300 => "Multiple Choices",
161 301 => "Moved Permanently",
162 302 => "Found",
163 304 => "Not Modified",
164 307 => "Temporary Redirect",
165 308 => "Permanent Redirect",
166 400 => "Bad Request",
167 401 => "Unauthorized",
168 403 => "Forbidden",
169 404 => "Not Found",
170 405 => "Method Not Allowed",
171 409 => "Conflict",
172 413 => "Payload Too Large",
173 415 => "Unsupported Media Type",
174 422 => "Unprocessable Content",
175 429 => "Too Many Requests",
176 500 => "Internal Server Error",
177 501 => "Not Implemented",
178 503 => "Service Unavailable",
179 _ => "Unknown",
180 }
181}
182
183fn invalid_input(message: &str) -> io::Error {
184 io::Error::new(io::ErrorKind::InvalidInput, message)
185}
186
187fn invalid_data(message: &str) -> io::Error {
188 io::Error::new(io::ErrorKind::InvalidData, message)
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn response_rejects_transport_headers_and_controls() {
197 let mut response = HttpResponse::new(200, b"ok".to_vec()).expect("valid response");
198 assert!(response.set_header("Content-Length", "2").is_err());
199 assert!(
200 response
201 .set_header("X-Test", "ok\r\nInjected: true")
202 .is_err()
203 );
204 response.set_header("X-Test", "ok").expect("valid header");
205 assert!(response.set_header("x-test", "again").is_err());
206 }
207}