1use super::cookie::CookieOptions;
2use super::header::{
3 get_header, is_json_media_type, matches_media_type, normalize_header_name, normalize_headers,
4 parse_content_length, strict_content_length_values, validate_header_name,
5 validate_header_value,
6};
7use crate::{BootError, Result, SseEvent, SseStream};
8use futures_core::Stream;
9use serde::de::DeserializeOwned;
10use serde::Serialize;
11use std::collections::BTreeMap;
12use std::fmt;
13use std::sync::{Arc, Mutex};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct BootResponse {
18 pub status: u16,
19 pub headers: BTreeMap<String, String>,
20 pub appended_headers: Vec<(String, String)>,
21 pub body: Vec<u8>,
22 stream: Option<SharedSseStream>,
23}
24
25#[derive(Clone)]
26struct SharedSseStream {
27 inner: Arc<Mutex<Option<SseStream>>>,
28}
29
30impl SharedSseStream {
31 fn new(stream: SseStream) -> Self {
32 Self {
33 inner: Arc::new(Mutex::new(Some(stream))),
34 }
35 }
36
37 fn take(&self) -> Option<SseStream> {
38 self.inner.lock().ok()?.take()
39 }
40}
41
42impl fmt::Debug for SharedSseStream {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 f.debug_struct("SharedSseStream").finish_non_exhaustive()
45 }
46}
47
48impl PartialEq for SharedSseStream {
49 fn eq(&self, other: &Self) -> bool {
50 Arc::ptr_eq(&self.inner, &other.inner)
51 }
52}
53
54impl Eq for SharedSseStream {}
55
56impl Default for BootResponse {
57 fn default() -> Self {
58 Self::new(200, Vec::<u8>::new())
59 }
60}
61
62impl BootResponse {
63 pub fn new(status: u16, body: impl Into<Vec<u8>>) -> Self {
64 Self {
65 status,
66 headers: BTreeMap::new(),
67 appended_headers: Vec::new(),
68 body: body.into(),
69 stream: None,
70 }
71 }
72
73 pub fn status(&self) -> u16 {
74 self.status
75 }
76
77 pub fn body(&self) -> &[u8] {
78 &self.body
79 }
80
81 pub fn into_body(self) -> Vec<u8> {
82 self.body
83 }
84
85 pub fn empty(status: u16) -> Self {
86 Self::new(status, Vec::<u8>::new())
87 }
88
89 pub fn no_content() -> Self {
90 Self::empty(204)
91 }
92
93 pub fn redirect(location: impl Into<String>) -> Self {
94 Self::redirect_with_status(302, location)
95 }
96
97 pub fn see_other(location: impl Into<String>) -> Self {
98 Self::redirect_with_status(303, location)
99 }
100
101 pub fn temporary_redirect(location: impl Into<String>) -> Self {
102 Self::redirect_with_status(307, location)
103 }
104
105 pub fn permanent_redirect(location: impl Into<String>) -> Self {
106 Self::redirect_with_status(308, location)
107 }
108
109 pub fn redirect_with_status(status: u16, location: impl Into<String>) -> Self {
110 Self::empty(status).with_location(location)
111 }
112
113 pub fn text(body: impl Into<String>) -> Self {
114 Self::text_with_status(200, body)
115 }
116
117 pub fn text_with_status(status: u16, body: impl Into<String>) -> Self {
118 Self::new(status, body.into()).with_header("content-type", "text/plain; charset=utf-8")
119 }
120
121 pub fn json<T>(body: &T) -> Result<Self>
122 where
123 T: Serialize,
124 {
125 Self::json_with_status(200, body)
126 }
127
128 pub fn json_with_status<T>(status: u16, body: &T) -> Result<Self>
129 where
130 T: Serialize,
131 {
132 let body = serde_json::to_vec(body).map_err(|err| BootError::Internal(err.to_string()))?;
133 Ok(Self::new(status, body).with_header("content-type", "application/json"))
134 }
135
136 pub fn sse<S>(stream: S) -> Self
137 where
138 S: Stream<Item = Result<SseEvent>> + Send + 'static,
139 {
140 Self::empty(200)
141 .with_header("content-type", "text/event-stream; charset=utf-8")
142 .with_header("cache-control", "no-cache")
143 .with_header("connection", "keep-alive")
144 .with_sse_stream(stream)
145 }
146
147 pub fn from_error(error: &BootError) -> Self {
148 Self::text_with_status(error.http_status_code(), error.http_response_message())
149 }
150
151 pub fn body_text(&self) -> Result<String> {
152 if self.is_streaming() {
153 return Err(BootError::Internal(
154 "streaming response body cannot be read as text".to_string(),
155 ));
156 }
157 String::from_utf8(self.body.clone()).map_err(|err| BootError::Internal(err.to_string()))
158 }
159
160 pub fn body_json<T>(&self) -> Result<T>
161 where
162 T: DeserializeOwned,
163 {
164 if self.is_streaming() {
165 return Err(BootError::Internal(
166 "streaming response body cannot be read as JSON".to_string(),
167 ));
168 }
169 serde_json::from_slice(&self.body).map_err(|err| BootError::Internal(err.to_string()))
170 }
171
172 pub fn with_status(mut self, status: u16) -> Self {
173 self.status = status;
174 self
175 }
176
177 pub fn with_content_type(self, content_type: impl Into<String>) -> Self {
178 self.with_header("content-type", content_type)
179 }
180
181 pub fn with_content_length(self, content_length: u64) -> Self {
182 self.with_header("content-length", content_length.to_string())
183 }
184
185 pub fn with_location(self, location: impl Into<String>) -> Self {
186 self.with_header("location", location)
187 }
188
189 pub fn with_www_authenticate(self, challenge: impl Into<String>) -> Self {
190 self.with_header("www-authenticate", challenge)
191 }
192
193 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
194 self.headers
195 .insert(normalize_header_name(name), value.into());
196 self
197 }
198
199 pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
200 self.headers = normalize_headers(headers);
201 self
202 }
203
204 pub fn append_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
205 self.appended_headers
206 .push((normalize_header_name(name), value.into()));
207 self
208 }
209
210 pub fn append_www_authenticate(self, challenge: impl Into<String>) -> Self {
211 self.append_header("www-authenticate", challenge)
212 }
213
214 pub fn with_cookie(
215 self,
216 name: impl AsRef<str>,
217 value: impl AsRef<str>,
218 options: CookieOptions,
219 ) -> Result<Self> {
220 let header = options.set_cookie_header(name.as_ref(), value.as_ref())?;
221 Ok(self.append_header("set-cookie", header))
222 }
223
224 pub fn delete_cookie(self, name: impl AsRef<str>, options: CookieOptions) -> Result<Self> {
225 let header = options.delete_cookie_header(name.as_ref())?;
226 Ok(self.append_header("set-cookie", header))
227 }
228
229 pub fn is_streaming(&self) -> bool {
230 self.stream.is_some()
231 }
232
233 pub fn is_event_stream(&self) -> bool {
234 self.is_content_type("text/event-stream")
235 }
236
237 pub fn into_sse_stream(self) -> Option<SseStream> {
238 self.stream.and_then(|stream| stream.take())
239 }
240
241 pub fn header_entries(&self) -> impl Iterator<Item = (&str, &str)> {
242 self.headers
243 .iter()
244 .map(|(name, value)| (name.as_str(), value.as_str()))
245 .chain(
246 self.appended_headers
247 .iter()
248 .map(|(name, value)| (name.as_str(), value.as_str())),
249 )
250 }
251
252 pub fn validate_headers(&self) -> Result<()> {
253 for (name, value) in self.header_entries() {
254 validate_response_header(name, value)?;
255 }
256
257 Ok(())
258 }
259
260 pub fn header(&self, name: &str) -> Option<&str> {
261 get_header(&self.headers, name)
262 }
263
264 pub fn header_values(&self, name: &str) -> Vec<&str> {
265 let mut values = self.header(name).into_iter().collect::<Vec<_>>();
266 values.extend(
267 self.appended_headers
268 .iter()
269 .filter(|(key, _)| key.eq_ignore_ascii_case(name))
270 .map(|(_, value)| value.as_str()),
271 );
272 values
273 }
274
275 pub fn content_type(&self) -> Option<&str> {
276 self.header_values("content-type").into_iter().next()
277 }
278
279 pub fn location(&self) -> Option<&str> {
280 self.header_values("location").into_iter().next()
281 }
282
283 pub fn www_authenticate(&self) -> Option<&str> {
284 self.header_values("www-authenticate").into_iter().next()
285 }
286
287 pub fn www_authenticate_values(&self) -> Vec<&str> {
288 self.header_values("www-authenticate")
289 }
290
291 pub fn content_length(&self) -> Result<Option<u64>> {
292 let Some(content_length) = self.header_values("content-length").into_iter().next() else {
293 return Ok(None);
294 };
295
296 parse_content_length(content_length)
297 .map(Some)
298 .ok_or_else(|| {
299 BootError::Internal(format!("invalid content-length header: {content_length}"))
300 })
301 }
302
303 pub fn strict_content_length(&self) -> Result<Option<u64>> {
304 strict_content_length_values(
305 self.header_values("content-length"),
306 |content_length| {
307 BootError::Internal(format!(
308 "invalid response content-length header: {content_length}"
309 ))
310 },
311 |expected_content_length, content_length| {
312 BootError::Internal(format!(
313 "conflicting response content-length headers: {expected_content_length} != {content_length}"
314 ))
315 },
316 )
317 }
318
319 pub fn validate_content_length(&self) -> Result<()> {
320 let Some(content_length) = self.strict_content_length()? else {
321 return Ok(());
322 };
323 if self.is_streaming() {
324 return Err(BootError::Internal(
325 "streaming responses must not include a content-length header".to_string(),
326 ));
327 }
328
329 let actual_body_length = self.body.len() as u64;
330 if actual_body_length == content_length {
331 return Ok(());
332 }
333
334 Err(BootError::Internal(format!(
335 "response content-length header does not match response body length: expected {content_length}, got {actual_body_length}"
336 )))
337 }
338
339 pub fn is_content_type(&self, media_type: &str) -> bool {
340 self.content_type()
341 .is_some_and(|content_type| matches_media_type(content_type, media_type))
342 }
343
344 pub fn is_json_content_type(&self) -> bool {
345 self.content_type().is_some_and(is_json_media_type)
346 }
347
348 pub fn has_body(&self) -> bool {
349 self.is_streaming() || !self.body.is_empty()
350 }
351
352 pub fn allows_body(&self) -> bool {
353 !(self.is_informational() || self.status == 204 || self.status == 304)
354 }
355
356 pub fn validate_body_allowed(&self) -> Result<()> {
357 if !self.has_body() || self.allows_body() {
358 return Ok(());
359 }
360
361 Err(BootError::Internal(format!(
362 "response status {} must not include a body",
363 self.status
364 )))
365 }
366
367 pub fn is_informational(&self) -> bool {
368 (100..200).contains(&self.status)
369 }
370
371 pub fn is_success(&self) -> bool {
372 (200..300).contains(&self.status)
373 }
374
375 pub fn is_redirection(&self) -> bool {
376 (300..400).contains(&self.status)
377 }
378
379 pub fn is_client_error(&self) -> bool {
380 (400..500).contains(&self.status)
381 }
382
383 pub fn is_server_error(&self) -> bool {
384 (500..600).contains(&self.status)
385 }
386
387 pub fn is_error(&self) -> bool {
388 self.is_client_error() || self.is_server_error()
389 }
390
391 pub fn is_valid_status(&self) -> bool {
392 (100..1000).contains(&self.status)
393 }
394
395 pub fn validate_status(&self) -> Result<()> {
396 if self.is_valid_status() {
397 return Ok(());
398 }
399
400 Err(BootError::Internal(format!(
401 "invalid response status {}",
402 self.status
403 )))
404 }
405
406 pub fn validate(&self) -> Result<()> {
407 self.validate_status()?;
408 self.validate_content_length()?;
409 self.validate_body_allowed()?;
410 self.validate_headers()
411 }
412
413 fn with_sse_stream<S>(mut self, stream: S) -> Self
414 where
415 S: Stream<Item = Result<SseEvent>> + Send + 'static,
416 {
417 self.stream = Some(SharedSseStream::new(Box::pin(stream)));
418 self
419 }
420}
421
422fn validate_response_header(name: &str, value: &str) -> Result<()> {
423 validate_header_name(name).map_err(|message| {
424 BootError::Internal(format!("invalid response header name {name:?}: {message}"))
425 })?;
426 validate_header_value(value).map_err(|message| {
427 BootError::Internal(format!(
428 "invalid response header value for {name:?}: {message}"
429 ))
430 })
431}