1use super::header::{
2 accepts_event_stream_response, accepts_json_response, get_header, is_json_media_type,
3 matches_media_type, normalize_header_name, normalize_headers, parse_content_length,
4 parse_cookie_header_values, strict_content_length_values, validate_header_name,
5 validate_header_value,
6};
7use super::method::HttpMethod;
8use super::query::{parse_query, parse_query_pairs, split_path_query};
9use crate::percent::validate_percent_encoding;
10use crate::{BootError, Result};
11use serde::{de::DeserializeOwned, Serialize};
12use std::collections::BTreeMap;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct BootRequest {
17 pub method: HttpMethod,
18 pub path: String,
19 pub query_string: Option<String>,
20 pub query: BTreeMap<String, String>,
21 pub params: BTreeMap<String, String>,
22 pub headers: BTreeMap<String, String>,
23 pub appended_headers: Vec<(String, String)>,
24 pub body: Vec<u8>,
25}
26
27impl BootRequest {
28 pub fn new(method: HttpMethod, path: impl Into<String>) -> Self {
29 let (path, query_string, query) = split_path_query(path.into());
30 Self {
31 method,
32 path,
33 query_string,
34 query,
35 params: BTreeMap::new(),
36 headers: BTreeMap::new(),
37 appended_headers: Vec::new(),
38 body: Vec::new(),
39 }
40 }
41
42 pub fn method(&self) -> HttpMethod {
43 self.method
44 }
45
46 pub fn path(&self) -> &str {
47 &self.path
48 }
49
50 pub fn query_string(&self) -> Option<&str> {
51 self.query_string.as_deref()
52 }
53
54 pub fn with_query_string(mut self, query_string: impl Into<String>) -> Self {
55 let query_string = query_string.into();
56 self.query = parse_query(&query_string);
57 self.query_string = Some(query_string);
58 self
59 }
60
61 pub fn with_path_params(mut self, params: BTreeMap<String, String>) -> Self {
62 self.params = params;
63 self
64 }
65
66 pub fn with_param(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
67 self.params.insert(name.into(), value.into());
68 self
69 }
70
71 pub fn with_query_param(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
72 self.query.insert(name.into(), value.into());
73 self.query_string = None;
74 self
75 }
76
77 pub fn with_body(mut self, body: impl Into<Vec<u8>>) -> Self {
78 self.body = body.into();
79 self
80 }
81
82 pub fn body(&self) -> &[u8] {
83 &self.body
84 }
85
86 pub fn into_body(self) -> Vec<u8> {
87 self.body
88 }
89
90 pub fn with_text(self, body: impl Into<String>) -> Self {
91 self.with_body(body.into())
92 .with_header("content-type", "text/plain; charset=utf-8")
93 }
94
95 pub fn with_json<T>(self, body: &T) -> Result<Self>
96 where
97 T: Serialize,
98 {
99 let body = serde_json::to_vec(body).map_err(|err| BootError::Internal(err.to_string()))?;
100 Ok(self
101 .with_body(body)
102 .with_header("content-type", "application/json"))
103 }
104
105 pub fn with_content_type(self, content_type: impl Into<String>) -> Self {
106 self.with_header("content-type", content_type)
107 }
108
109 pub fn with_content_length(self, content_length: u64) -> Self {
110 self.with_header("content-length", content_length.to_string())
111 }
112
113 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
114 self.headers
115 .insert(normalize_header_name(name), value.into());
116 self
117 }
118
119 pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
120 self.headers = normalize_headers(headers);
121 self
122 }
123
124 pub fn append_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
125 self.appended_headers
126 .push((normalize_header_name(name), value.into()));
127 self
128 }
129
130 pub fn header_entries(&self) -> impl Iterator<Item = (&str, &str)> {
131 self.headers
132 .iter()
133 .map(|(name, value)| (name.as_str(), value.as_str()))
134 .chain(
135 self.appended_headers
136 .iter()
137 .map(|(name, value)| (name.as_str(), value.as_str())),
138 )
139 }
140
141 pub fn validate_headers(&self) -> Result<()> {
142 for (name, value) in self.header_entries() {
143 validate_request_header(name, value)?;
144 }
145
146 Ok(())
147 }
148
149 pub fn text(&self) -> Result<String> {
150 String::from_utf8(self.body.clone()).map_err(|err| BootError::BadRequest(err.to_string()))
151 }
152
153 pub fn param(&self, name: &str) -> Option<&str> {
154 self.params.get(name).map(String::as_str)
155 }
156
157 pub fn params<T>(&self) -> Result<T>
158 where
159 T: DeserializeOwned,
160 {
161 let params = serde_urlencoded::to_string(&self.params)
162 .map_err(|err| BootError::BadRequest(err.to_string()))?;
163 serde_urlencoded::from_str(¶ms).map_err(|err| BootError::BadRequest(err.to_string()))
164 }
165
166 pub fn query_param(&self, name: &str) -> Option<&str> {
167 self.query.get(name).map(String::as_str)
168 }
169
170 pub fn query_value(&self, name: &str) -> Result<Option<String>> {
171 Ok(self
172 .query_pairs()?
173 .into_iter()
174 .find_map(|(key, value)| (key == name).then_some(value)))
175 }
176
177 pub fn query_values(&self, name: &str) -> Result<Vec<String>> {
178 Ok(self
179 .query_pairs()?
180 .into_iter()
181 .filter_map(|(key, value)| (key == name).then_some(value))
182 .collect())
183 }
184
185 pub fn query_pairs(&self) -> Result<Vec<(String, String)>> {
186 match self.query_string.as_deref() {
187 Some(query) => parse_query_pairs(query),
188 None => Ok(self
189 .query
190 .iter()
191 .map(|(key, value)| (key.clone(), value.clone()))
192 .collect()),
193 }
194 }
195
196 pub fn header(&self, name: &str) -> Option<&str> {
197 get_header(&self.headers, name)
198 }
199
200 pub fn header_values(&self, name: &str) -> Vec<&str> {
201 let mut values = self.header(name).into_iter().collect::<Vec<_>>();
202 values.extend(
203 self.appended_headers
204 .iter()
205 .filter(|(key, _)| key.eq_ignore_ascii_case(name))
206 .map(|(_, value)| value.as_str()),
207 );
208 values
209 }
210
211 pub fn authorization(&self) -> Option<&str> {
212 self.header_values("authorization").into_iter().next()
213 }
214
215 pub fn bearer_token(&self) -> Option<&str> {
216 let authorization = self.authorization()?.trim();
217 let mut parts = authorization.splitn(2, char::is_whitespace);
218 let scheme = parts.next()?;
219 let token = parts.next()?.trim();
220
221 if scheme.eq_ignore_ascii_case("bearer") && !token.is_empty() {
222 Some(token)
223 } else {
224 None
225 }
226 }
227
228 pub fn require_bearer_token(&self) -> Result<&str> {
229 self.bearer_token()
230 .ok_or_else(|| BootError::Unauthorized("missing bearer token".to_string()))
231 }
232
233 pub fn cookie_pairs(&self) -> Result<Vec<(String, String)>> {
234 parse_cookie_header_values(&self.header_values("cookie"))
235 }
236
237 pub fn cookie(&self, name: &str) -> Result<Option<String>> {
238 Ok(self
239 .cookie_pairs()?
240 .into_iter()
241 .find_map(|(key, value)| (key == name).then_some(value)))
242 }
243
244 pub fn require_cookie(&self, name: &str) -> Result<String> {
245 self.cookie(name)?
246 .ok_or_else(|| BootError::Unauthorized(format!("missing cookie: {name}")))
247 }
248
249 pub fn cookie_values(&self, name: &str) -> Result<Vec<String>> {
250 Ok(self
251 .cookie_pairs()?
252 .into_iter()
253 .filter_map(|(key, value)| (key == name).then_some(value))
254 .collect())
255 }
256
257 pub fn cookies(&self) -> Result<BTreeMap<String, String>> {
258 let mut cookies = BTreeMap::new();
259 for (name, value) in self.cookie_pairs()? {
260 cookies.entry(name).or_insert(value);
261 }
262 Ok(cookies)
263 }
264
265 pub fn content_type(&self) -> Option<&str> {
266 self.header_values("content-type").into_iter().next()
267 }
268
269 pub fn content_length(&self) -> Result<Option<u64>> {
270 let Some(content_length) = self.header_values("content-length").into_iter().next() else {
271 return Ok(None);
272 };
273
274 parse_content_length(content_length)
275 .map(Some)
276 .ok_or_else(|| {
277 BootError::BadRequest(format!("invalid content-length header: {content_length}"))
278 })
279 }
280
281 pub fn strict_content_length(&self) -> Result<Option<u64>> {
282 strict_content_length_values(
283 self.header_values("content-length"),
284 |content_length| {
285 BootError::BadRequest(format!("invalid content-length header: {content_length}"))
286 },
287 |expected_content_length, content_length| {
288 BootError::BadRequest(format!(
289 "conflicting content-length headers: {expected_content_length} != {content_length}"
290 ))
291 },
292 )
293 }
294
295 pub fn validate_content_length(&self) -> Result<()> {
296 let Some(content_length) = self.strict_content_length()? else {
297 return Ok(());
298 };
299 let actual_body_length = self.body.len() as u64;
300 if actual_body_length == content_length {
301 return Ok(());
302 }
303
304 Err(BootError::BadRequest(format!(
305 "content-length header does not match request body length: expected {content_length}, got {actual_body_length}"
306 )))
307 }
308
309 pub fn validate_body_limit(&self, body_limit: usize) -> Result<()> {
310 if self
311 .strict_content_length()?
312 .is_some_and(|content_length| content_length > body_limit as u64)
313 || self.body.len() > body_limit
314 {
315 return Err(BootError::PayloadTooLarge(format!(
316 "request body exceeds {body_limit} bytes"
317 )));
318 }
319
320 Ok(())
321 }
322
323 pub fn validate(&self) -> Result<()> {
324 self.validate_headers()?;
325 self.validate_content_length()
326 }
327
328 pub fn validate_with_body_limit(&self, body_limit: usize) -> Result<()> {
329 self.validate_headers()?;
330 self.validate_body_limit(body_limit)?;
331 self.validate_content_length()
332 }
333
334 pub fn is_content_type(&self, media_type: &str) -> bool {
335 self.content_type()
336 .is_some_and(|content_type| matches_media_type(content_type, media_type))
337 }
338
339 pub fn is_json_content_type(&self) -> bool {
340 self.content_type().is_some_and(is_json_media_type)
341 }
342
343 pub fn require_json_content_type(&self) -> Result<()> {
344 if self.is_json_content_type() {
345 return Ok(());
346 }
347
348 let message = match self.content_type() {
349 Some(content_type) => format!("expected JSON content type, got {content_type}"),
350 None => "expected JSON content type".to_string(),
351 };
352 Err(BootError::UnsupportedMediaType(message))
353 }
354
355 pub fn accepts_json(&self) -> bool {
356 accepts_json_response(&self.header_values("accept"))
357 }
358
359 pub fn require_accepts_json(&self) -> Result<()> {
360 if self.accepts_json() {
361 return Ok(());
362 }
363
364 Err(BootError::NotAcceptable(
365 "expected client to accept JSON response".to_string(),
366 ))
367 }
368
369 pub fn accepts_event_stream(&self) -> bool {
370 accepts_event_stream_response(&self.header_values("accept"))
371 }
372
373 pub fn require_accepts_event_stream(&self) -> Result<()> {
374 if self.accepts_event_stream() {
375 return Ok(());
376 }
377
378 Err(BootError::NotAcceptable(
379 "expected client to accept text/event-stream response".to_string(),
380 ))
381 }
382
383 pub fn json<T>(&self) -> Result<T>
384 where
385 T: DeserializeOwned,
386 {
387 serde_json::from_slice(&self.body).map_err(|err| BootError::BadRequest(err.to_string()))
388 }
389
390 pub fn json_with_content_type<T>(&self) -> Result<T>
391 where
392 T: DeserializeOwned,
393 {
394 self.require_json_content_type()?;
395 self.json()
396 }
397
398 pub fn query<T>(&self) -> Result<T>
399 where
400 T: DeserializeOwned,
401 {
402 let query = match self.query_string.as_deref() {
403 Some(query) => {
404 validate_percent_encoding(query)?;
405 query.to_string()
406 }
407 None => serde_urlencoded::to_string(&self.query)
408 .map_err(|err| BootError::BadRequest(err.to_string()))?,
409 };
410 serde_urlencoded::from_str(&query).map_err(|err| BootError::BadRequest(err.to_string()))
411 }
412}
413
414fn validate_request_header(name: &str, value: &str) -> Result<()> {
415 validate_header_name(name).map_err(|message| {
416 BootError::BadRequest(format!("invalid request header name {name:?}: {message}"))
417 })?;
418 validate_header_value(value).map_err(|message| {
419 BootError::BadRequest(format!(
420 "invalid request header value for {name:?}: {message}"
421 ))
422 })
423}