eggserve_core/primitives/
http.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum ReadOnlyMethod {
10 Get,
11 Head,
12}
13
14impl ReadOnlyMethod {
15 pub fn as_str(&self) -> &'static str {
16 match self {
17 Self::Get => "GET",
18 Self::Head => "HEAD",
19 }
20 }
21}
22
23impl std::fmt::Display for ReadOnlyMethod {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 f.write_str(self.as_str())
26 }
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum RequestValidationError {
32 MethodNotAllowed,
34 InvalidContentLength,
37 BodyTooLarge,
39 UnsupportedTransferEncoding,
41 ConflictingBodyHeaders,
43 InvalidRequestTarget,
45}
46
47impl std::fmt::Display for RequestValidationError {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 Self::MethodNotAllowed => write!(f, "method not allowed for static serving"),
51 Self::InvalidContentLength => write!(f, "malformed Content-Length"),
52 Self::BodyTooLarge => write!(f, "request body too large"),
53 Self::UnsupportedTransferEncoding => {
54 write!(f, "unsupported Transfer-Encoding on read-only request")
55 }
56 Self::ConflictingBodyHeaders => {
57 write!(f, "both Content-Length and Transfer-Encoding present")
58 }
59 Self::InvalidRequestTarget => write!(f, "request target is not valid origin-form"),
60 }
61 }
62}
63
64impl std::error::Error for RequestValidationError {}
65
66pub fn validate_method(method: &str) -> Result<ReadOnlyMethod, RequestValidationError> {
68 match method {
69 "GET" => Ok(ReadOnlyMethod::Get),
70 "HEAD" => Ok(ReadOnlyMethod::Head),
71 _ => Err(RequestValidationError::MethodNotAllowed),
72 }
73}
74
75pub fn validate_request_body(
82 content_length: Option<&str>,
83 transfer_encoding: Option<&str>,
84 max_body_bytes: u64,
85) -> Result<(), RequestValidationError> {
86 if content_length.is_some() && transfer_encoding.is_some() {
87 return Err(RequestValidationError::ConflictingBodyHeaders);
88 }
89
90 if transfer_encoding.is_some() {
91 return Err(RequestValidationError::UnsupportedTransferEncoding);
92 }
93
94 if let Some(cl) = content_length {
95 let trimmed = cl.trim();
96 if trimmed.is_empty() {
97 return Err(RequestValidationError::InvalidContentLength);
98 }
99 if !trimmed.chars().all(|c| c.is_ascii_digit()) {
100 return Err(RequestValidationError::InvalidContentLength);
101 }
102 let len: u64 = trimmed
103 .parse()
104 .map_err(|_| RequestValidationError::InvalidContentLength)?;
105 if len > max_body_bytes {
106 return Err(RequestValidationError::BodyTooLarge);
107 }
108 }
109
110 Ok(())
111}
112
113pub fn validate_request_target(target: &str) -> Result<(), RequestValidationError> {
115 if target.is_empty() || !target.starts_with('/') {
116 return Err(RequestValidationError::InvalidRequestTarget);
117 }
118 if target.contains(char::is_whitespace) {
119 return Err(RequestValidationError::InvalidRequestTarget);
120 }
121 Ok(())
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn get_is_allowed() {
130 assert_eq!(validate_method("GET").unwrap(), ReadOnlyMethod::Get);
131 }
132
133 #[test]
134 fn head_is_allowed() {
135 assert_eq!(validate_method("HEAD").unwrap(), ReadOnlyMethod::Head);
136 }
137
138 #[test]
139 fn post_is_rejected() {
140 assert_eq!(
141 validate_method("POST").unwrap_err(),
142 RequestValidationError::MethodNotAllowed
143 );
144 }
145
146 #[test]
147 fn put_is_rejected() {
148 assert_eq!(
149 validate_method("PUT").unwrap_err(),
150 RequestValidationError::MethodNotAllowed
151 );
152 }
153
154 #[test]
155 fn delete_is_rejected() {
156 assert_eq!(
157 validate_method("DELETE").unwrap_err(),
158 RequestValidationError::MethodNotAllowed
159 );
160 }
161
162 #[test]
163 fn patch_is_rejected() {
164 assert_eq!(
165 validate_method("PATCH").unwrap_err(),
166 RequestValidationError::MethodNotAllowed
167 );
168 }
169
170 #[test]
171 fn options_is_rejected() {
172 assert_eq!(
173 validate_method("OPTIONS").unwrap_err(),
174 RequestValidationError::MethodNotAllowed
175 );
176 }
177
178 #[test]
179 fn zero_content_length_allowed() {
180 assert!(validate_request_body(Some("0"), None, 0).is_ok());
181 }
182
183 #[test]
184 fn positive_content_length_rejected() {
185 assert_eq!(
186 validate_request_body(Some("1024"), None, 0).unwrap_err(),
187 RequestValidationError::BodyTooLarge
188 );
189 }
190
191 #[test]
192 fn invalid_content_length_rejected() {
193 assert_eq!(
194 validate_request_body(Some("not-a-number"), None, 0).unwrap_err(),
195 RequestValidationError::InvalidContentLength
196 );
197 }
198
199 #[test]
200 fn negative_content_length_rejected() {
201 assert_eq!(
202 validate_request_body(Some("-1"), None, 0).unwrap_err(),
203 RequestValidationError::InvalidContentLength
204 );
205 }
206
207 #[test]
208 fn overflowing_content_length_rejected() {
209 assert_eq!(
210 validate_request_body(Some("99999999999999999999"), None, 0).unwrap_err(),
211 RequestValidationError::InvalidContentLength
212 );
213 }
214
215 #[test]
216 fn nonempty_transfer_encoding_rejected() {
217 assert_eq!(
218 validate_request_body(None, Some("chunked"), 0).unwrap_err(),
219 RequestValidationError::UnsupportedTransferEncoding
220 );
221 }
222
223 #[test]
224 fn content_length_and_transfer_encoding_rejected() {
225 assert_eq!(
226 validate_request_body(Some("0"), Some("chunked"), 0).unwrap_err(),
227 RequestValidationError::ConflictingBodyHeaders
228 );
229 }
230
231 #[test]
232 fn empty_transfer_encoding_rejected() {
233 assert_eq!(
234 validate_request_body(None, Some(""), 0).unwrap_err(),
235 RequestValidationError::UnsupportedTransferEncoding
236 );
237 }
238
239 #[test]
240 fn whitespace_transfer_encoding_rejected() {
241 assert_eq!(
242 validate_request_body(None, Some(" "), 0).unwrap_err(),
243 RequestValidationError::UnsupportedTransferEncoding
244 );
245 }
246
247 #[test]
248 fn no_headers_allowed() {
249 assert!(validate_request_body(None, None, 0).is_ok());
250 }
251
252 #[test]
253 fn empty_content_length_rejected() {
254 assert_eq!(
255 validate_request_body(Some(""), None, 0).unwrap_err(),
256 RequestValidationError::InvalidContentLength
257 );
258 }
259
260 #[test]
261 fn whitespace_only_content_length_rejected() {
262 assert_eq!(
263 validate_request_body(Some(" "), None, 0).unwrap_err(),
264 RequestValidationError::InvalidContentLength
265 );
266 }
267
268 #[test]
269 fn content_length_with_max_body_bytes() {
270 assert!(validate_request_body(Some("100"), None, 100).is_ok());
271 assert_eq!(
272 validate_request_body(Some("101"), None, 100).unwrap_err(),
273 RequestValidationError::BodyTooLarge
274 );
275 }
276
277 #[test]
278 fn valid_request_target() {
279 assert!(validate_request_target("/").is_ok());
280 assert!(validate_request_target("/foo").is_ok());
281 assert!(validate_request_target("/foo/bar").is_ok());
282 assert!(validate_request_target("/file.txt").is_ok());
283 }
284
285 #[test]
286 fn empty_request_target_rejected() {
287 assert_eq!(
288 validate_request_target("").unwrap_err(),
289 RequestValidationError::InvalidRequestTarget
290 );
291 }
292
293 #[test]
294 fn absolute_uri_request_target_rejected() {
295 assert_eq!(
296 validate_request_target("http://example.com/").unwrap_err(),
297 RequestValidationError::InvalidRequestTarget
298 );
299 }
300
301 #[test]
302 fn asterisk_request_target_rejected() {
303 assert_eq!(
304 validate_request_target("*").unwrap_err(),
305 RequestValidationError::InvalidRequestTarget
306 );
307 }
308
309 #[test]
310 fn whitespace_in_request_target_rejected() {
311 assert_eq!(
312 validate_request_target("/foo bar").unwrap_err(),
313 RequestValidationError::InvalidRequestTarget
314 );
315 }
316
317 #[test]
318 fn read_only_method_as_str() {
319 assert_eq!(ReadOnlyMethod::Get.as_str(), "GET");
320 assert_eq!(ReadOnlyMethod::Head.as_str(), "HEAD");
321 }
322
323 #[test]
324 fn read_only_method_display() {
325 assert_eq!(format!("{}", ReadOnlyMethod::Get), "GET");
326 assert_eq!(format!("{}", ReadOnlyMethod::Head), "HEAD");
327 }
328
329 #[test]
330 fn request_validation_error_is_display() {
331 let err = RequestValidationError::MethodNotAllowed;
332 assert!(!err.to_string().is_empty());
333 }
334
335 #[test]
336 fn request_validation_error_is_error() {
337 let err: &dyn std::error::Error = &RequestValidationError::BodyTooLarge;
338 assert!(!err.to_string().is_empty());
339 }
340}