servlin/
response.rs

1use futures_io::AsyncWrite;
2use futures_lite::{AsyncReadExt, AsyncWriteExt};
3use std::convert::TryFrom;
4use std::io::ErrorKind;
5use std::io::Write;
6
7#[cfg(any(feature = "include_dir", feature = "json"))]
8use crate::Error;
9#[cfg(feature = "include_dir")]
10use crate::Request;
11use crate::event::EventReceiver;
12use crate::http_error::HttpError;
13use crate::util::{copy_async, copy_chunked_async};
14use crate::{AsciiString, ContentType, Cookie, EventSender, HeaderList, ResponseBody};
15use safina::sync::sync_channel;
16use std::fmt::Debug;
17use std::sync::Mutex;
18
19#[allow(clippy::module_name_repetitions)]
20#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
21pub enum ResponseKind {
22    DropConnection,
23    /// `GetBodyAndReprocess(max_len: u64)`<br>
24    /// Read the body from the client, but only up to the specified `u64` bytes.
25    GetBodyAndReprocess(u64),
26    Normal,
27}
28
29#[derive(Eq, PartialEq)]
30pub struct Response {
31    pub kind: ResponseKind,
32    pub code: u16,
33    pub content_type: ContentType,
34    pub headers: HeaderList,
35    pub body: ResponseBody,
36}
37impl Response {
38    #[must_use]
39    pub fn new(code: u16) -> Self {
40        Self {
41            kind: ResponseKind::Normal,
42            code,
43            content_type: ContentType::None,
44            headers: HeaderList::new(),
45            body: ResponseBody::empty(),
46        }
47    }
48
49    /// Return this and the server will drop the connection.
50    #[must_use]
51    pub fn drop_connection() -> Self {
52        Self {
53            kind: ResponseKind::DropConnection,
54            code: 0,
55            content_type: ContentType::None,
56            headers: HeaderList::new(),
57            body: ResponseBody::empty(),
58        }
59    }
60
61    /// Return this and the server will read the request body from the client
62    /// and call the request handler again.
63    ///
64    /// If the request body is larger than `max_len` bytes, it sends 413 Payload Too Large.
65    #[must_use]
66    pub fn get_body_and_reprocess(max_len: u64) -> Self {
67        Self {
68            kind: ResponseKind::GetBodyAndReprocess(max_len),
69            code: 0,
70            content_type: ContentType::None,
71            headers: HeaderList::new(),
72            body: ResponseBody::empty(),
73        }
74    }
75
76    /// Looks for the requested file in included `dir`.
77    ///
78    /// Determines the content-type from the file extension.
79    /// For the list of supported content types, see [`ContentType`].
80    ///
81    /// When the request path is `"/"`, tries to return the file `/index.html`.
82    ///
83    /// # Errors
84    /// Returns a 404 Not Found response if the file is not found in the included dir.
85    #[cfg(feature = "include_dir")]
86    // TODO: Change this to accept only GET and HEAD requests.
87    // TODO: Change this to handle HEAD requests properly.
88    // TODO: Honor Accept request header.
89    pub fn include_dir(req: &Request, dir: &'static include_dir::Dir) -> Result<Response, Error> {
90        let path = &req.url.path;
91        let path = path.strip_prefix('/').unwrap_or(path);
92        let file = if path.is_empty() {
93            dir.get_file("index.html")
94        } else if let Some(file) = dir.get_file(path) {
95            Some(file)
96        } else if path.ends_with('/') {
97            let dir_path = path.trim_end_matches('/');
98            dir.get_file(format!("{dir_path}/index.html"))
99        } else if let Some(_dir) = dir.get_dir(path) {
100            return Ok(Response::redirect_301(format!("/{path}/")));
101        } else {
102            None
103        }
104        .ok_or_else(|| Error::client_error(Response::not_found_404()))?;
105        let extension = std::path::Path::new(path)
106            .extension()
107            .map_or("", |os_str| os_str.to_str().unwrap_or(""));
108        let content_type = match extension {
109            "css" => ContentType::Css,
110            "csv" => ContentType::Csv,
111            "gif" => ContentType::Gif,
112            "htm" | "html" => ContentType::Html,
113            "js" => ContentType::JavaScript,
114            "jpg" | "jpeg" => ContentType::Jpeg,
115            "json" => ContentType::Json,
116            "md" => ContentType::Markdown,
117            "pdf" => ContentType::Pdf,
118            "txt" => ContentType::PlainText,
119            "png" => ContentType::Png,
120            "svg" => ContentType::Svg,
121            _ => ContentType::None,
122        };
123        Ok(Response::new(200)
124            .with_type(content_type)
125            .with_body(ResponseBody::StaticBytes(file.contents())))
126    }
127
128    #[must_use]
129    pub fn html(code: u16, body: impl Into<ResponseBody>) -> Self {
130        Self::new(code).with_type(ContentType::Html).with_body(body)
131    }
132
133    /// # Errors
134    /// Returns an error when it fails to serialize `v`.
135    #[cfg(feature = "json")]
136    pub fn json(code: u16, v: impl serde::Serialize) -> Result<Response, Error> {
137        let body_vec = serde_json::to_vec(&v)
138            .map_err(|e| Error::server_error(format!("error serializing response to json: {e}")))?;
139        Ok(Self::new(code)
140            .with_type(ContentType::Json)
141            .with_body(body_vec))
142    }
143
144    #[must_use]
145    pub fn event_stream() -> (EventSender, Response) {
146        let (sender, receiver) = sync_channel(50);
147        (
148            EventSender(Some(sender)),
149            Self::new(200)
150                .with_type(ContentType::EventStream)
151                .with_body(ResponseBody::EventStream(Mutex::new(EventReceiver(
152                    receiver,
153                )))),
154        )
155    }
156
157    #[must_use]
158    pub fn text(code: u16, body: impl Into<ResponseBody>) -> Self {
159        Self::new(code)
160            .with_type(ContentType::PlainText)
161            .with_body(body)
162    }
163
164    #[must_use]
165    pub fn ok_200() -> Self {
166        Response::new(200)
167    }
168
169    #[must_use]
170    pub fn no_content_204() -> Self {
171        Response::new(204)
172    }
173
174    /// Tell the client to GET `location`.
175    ///
176    /// The client should store this redirect.
177    ///
178    /// # Panics
179    /// Panics when `location` is not US-ASCII.
180    #[must_use]
181    pub fn redirect_301(location: impl AsRef<str>) -> Self {
182        Response::new(301).with_header("location", location.as_ref().try_into().unwrap())
183    }
184
185    /// Tell the client to GET `location`.
186    ///
187    /// The client should not store this redirect.
188    ///
189    /// A PUT or POST handler usually returns this.
190    ///
191    /// # Panics
192    /// Panics when `location` is not US-ASCII.
193    #[must_use]
194    pub fn redirect_303(location: impl AsRef<str>) -> Self {
195        Response::new(303).with_header("location", location.as_ref().try_into().unwrap())
196    }
197
198    #[must_use]
199    pub fn unauthorized_401() -> Self {
200        Response::new(401)
201    }
202
203    #[must_use]
204    pub fn forbidden_403() -> Self {
205        Response::new(401)
206    }
207
208    #[must_use]
209    pub fn not_found_404() -> Self {
210        Response::text(404, "not found")
211    }
212
213    /// # Panics
214    /// Panics when any of `allowed_methods` are not US-ASCII.
215    #[must_use]
216    pub fn method_not_allowed_405(allowed_methods: &[&'static str]) -> Self {
217        Self::new(405).with_header("allow", allowed_methods.join(",").try_into().unwrap())
218    }
219
220    #[must_use]
221    pub fn length_required_411() -> Self {
222        Response::text(411, "not accepting streaming uploads")
223    }
224
225    #[must_use]
226    pub fn payload_too_large_413() -> Self {
227        Response::text(413, "Uploaded data is too big.")
228    }
229
230    #[must_use]
231    pub fn unprocessable_entity_422(body: impl Into<String>) -> Self {
232        let body: String = body.into();
233        Response::text(422, body)
234    }
235
236    #[must_use]
237    pub fn too_many_requests_429() -> Self {
238        Response::text(429, "Too many requests.")
239    }
240
241    #[must_use]
242    pub fn internal_server_error_500() -> Self {
243        Response::new(500)
244    }
245
246    #[must_use]
247    pub fn not_implemented_501() -> Self {
248        Response::new(501)
249    }
250
251    #[must_use]
252    pub fn service_unavailable_503() -> Self {
253        Response::new(503)
254    }
255
256    #[must_use]
257    pub fn with_body(mut self, b: impl Into<ResponseBody>) -> Self {
258        self.body = b.into();
259        self
260    }
261
262    /// Adds a `Cache-Control: max-age=N` header.
263    ///
264    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control>
265    #[allow(clippy::missing_panics_doc)]
266    #[must_use]
267    pub fn with_max_age_seconds(mut self, seconds: u32) -> Self {
268        self.headers.add(
269            "cache-control",
270            format!("max-age={seconds}").try_into().unwrap(),
271        );
272        self
273    }
274
275    /// Adds a `Cache-Control: no-store` header.
276    ///
277    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control>
278    #[allow(clippy::missing_panics_doc)]
279    #[must_use]
280    pub fn with_no_store(mut self) -> Self {
281        self.headers
282            .add("cache-control", "no-store".try_into().unwrap());
283        self
284    }
285
286    /// Adds a `Set-Cookie` header.
287    ///
288    /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie>
289    #[must_use]
290    pub fn with_set_cookie(mut self, cookie: Cookie) -> Self {
291        self.headers.add("set-cookie", cookie.into());
292        self
293    }
294
295    /// Adds a header.
296    ///
297    /// You can call this multiple times to add multiple headers with the same name.
298    ///
299    /// The [HTTP spec](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4)
300    /// limits header names to US-ASCII and header values to US-ASCII or ISO-8859-1.
301    ///
302    /// # Panics
303    /// Panics when `name` is not US-ASCII.
304    ///
305    /// # Example
306    /// ```
307    /// use servlin::Response;
308    ///
309    /// # fn example() -> Response {
310    /// return Response::new(200)
311    ///     .with_header("header1", "value1".to_string().try_into().unwrap());
312    /// # }
313    /// ```
314    #[must_use]
315    pub fn with_header(mut self, name: impl AsRef<str>, value: AsciiString) -> Self {
316        self.headers.add(name, value);
317        self
318    }
319
320    #[must_use]
321    pub fn with_status(mut self, c: u16) -> Self {
322        self.code = c;
323        self
324    }
325
326    #[must_use]
327    pub fn with_type(mut self, t: ContentType) -> Self {
328        self.content_type = t;
329        self
330    }
331
332    #[must_use]
333    pub fn is_1xx(&self) -> bool {
334        self.code / 100 == 1
335    }
336
337    #[must_use]
338    pub fn is_2xx(&self) -> bool {
339        self.code / 100 == 2
340    }
341
342    #[must_use]
343    pub fn is_3xx(&self) -> bool {
344        self.code / 100 == 3
345    }
346
347    #[must_use]
348    pub fn is_4xx(&self) -> bool {
349        self.code / 100 == 4
350    }
351
352    #[must_use]
353    pub fn is_5xx(&self) -> bool {
354        self.code / 100 == 5
355    }
356
357    #[must_use]
358    pub fn is_normal(&self) -> bool {
359        self.kind == ResponseKind::Normal
360    }
361
362    #[must_use]
363    pub fn is_get_body_and_reprocess(&self) -> bool {
364        matches!(self.kind, ResponseKind::GetBodyAndReprocess(..))
365    }
366}
367impl From<std::io::Error> for Response {
368    fn from(e: std::io::Error) -> Self {
369        match e.kind() {
370            ErrorKind::InvalidData => Response::text(400, "Bad request"),
371            _ => Response::text(500, "Internal server error"),
372        }
373    }
374}
375impl Debug for Response {
376    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
377        match self.kind {
378            ResponseKind::DropConnection => write!(f, "Response(kind=Drop)"),
379            ResponseKind::GetBodyAndReprocess(max_len) => {
380                write!(f, "Response(kind=GetBodyAndReprocess({max_len}))")
381            }
382            ResponseKind::Normal => {
383                write!(
384                    f,
385                    "Response({} {}, {:?}, {:?}, {:?})",
386                    self.code,
387                    reason_phrase(self.code),
388                    self.content_type,
389                    self.headers,
390                    self.body
391                )
392            }
393        }
394    }
395}
396
397#[must_use]
398pub fn reason_phrase(code: u16) -> &'static str {
399    // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
400    match code {
401        100 => "Continue",
402        101 => "Switching Protocols",
403        102 => "Processing",
404        103 => "Early Hints",
405        200 => "OK",
406        201 => "Created",
407        202 => "Accepted",
408        203 => "Non-Authoritative Information",
409        204 => "No Content",
410        205 => "Reset Content",
411        206 => "Partial Content",
412        207 => "Multi-Status",
413        208 => "Already Reported",
414        226 => "IM Used",
415        300 => "Multiple Choice",
416        301 => "Moved Permanently",
417        302 => "Found",
418        303 => "See Other",
419        304 => "Not Modified",
420        307 => "Temporary Redirect",
421        308 => "Permanent Redirect",
422        400 => "Bad Request",
423        401 => "Unauthorized",
424        402 => "Payment Required ",
425        403 => "Forbidden",
426        404 => "Not Found",
427        405 => "Method Not Allowed",
428        406 => "Not Acceptable",
429        407 => "Proxy Authentication Required",
430        408 => "Request Timeout",
431        409 => "Conflict",
432        410 => "Gone",
433        411 => "Length Required",
434        412 => "Precondition Failed",
435        413 => "Payload Too Large",
436        414 => "URI Too Long",
437        415 => "Unsupported Media Type",
438        416 => "Range Not Satisfiable",
439        417 => "Expectation Failed",
440        418 => "I'm a teapot",
441        421 => "Misdirected Request",
442        422 => "Unprocessable Entity",
443        423 => "Locked",
444        424 => "Failed Dependency",
445        425 => "Too Early ",
446        426 => "Upgrade Required",
447        428 => "Precondition Required",
448        429 => "Too Many Requests",
449        431 => "Request Header Fields Too Large",
450        451 => "Unavailable For Legal Reasons",
451        500 => "Internal Server Error",
452        501 => "Not Implemented",
453        502 => "Bad Gateway",
454        503 => "Service Unavailable",
455        504 => "Gateway Timeout",
456        505 => "HTTP Version Not Supported",
457        506 => "Variant Also Negotiates",
458        507 => "Insufficient Storage",
459        508 => "Loop Detected",
460        510 => "Not Extended",
461        511 => "Network Authentication Required",
462        _ => "Response",
463    }
464}
465
466/// # Errors
467/// Returns an error when:
468/// - `response` is not `Response::Normal`
469/// - the connection is closed
470/// - we fail to send the response on the connection
471/// - the response body is saved in a file and we fail to read the file
472#[allow(clippy::module_name_repetitions)]
473pub async fn write_http_response(
474    mut writer: impl AsyncWrite + Unpin,
475    response: &Response,
476    close: bool,
477) -> Result<(), HttpError> {
478    //dbg!("write_http_response", &response);
479    if !response.is_normal() {
480        return Err(HttpError::UnwritableResponse);
481    }
482    // https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2
483    //     status-line = HTTP-version SP status-code SP reason-phrase CRLF
484    //     status-code    = 3DIGIT
485    //     reason-phrase  = *( HTAB / SP / VCHAR )
486    let mut head_bytes: Vec<u8> = format!(
487        "HTTP/1.1 {} {}\r\n",
488        response.code,
489        reason_phrase(response.code)
490    )
491    .into_bytes();
492    if response.content_type != ContentType::None {
493        if response.headers.get_only("content-type").is_some() {
494            return Err(HttpError::DuplicateContentTypeHeader);
495        }
496        write!(
497            head_bytes,
498            "content-type: {}\r\n",
499            response.content_type.as_str()
500        )
501        .unwrap();
502    }
503    if close {
504        write!(head_bytes, "connection: close\r\n",).unwrap();
505    }
506    if let Some(body_len) = response.body.len() {
507        if response.headers.get_only("content-length").is_some() {
508            return Err(HttpError::DuplicateContentLengthHeader);
509        }
510        write!(head_bytes, "content-length: {body_len}\r\n").unwrap();
511    } else {
512        if response.headers.get_only("transfer-encoding").is_some() {
513            return Err(HttpError::DuplicateTransferEncodingHeader);
514        }
515        write!(head_bytes, "transfer-encoding: chunked\r\n").unwrap();
516    }
517    for header in &response.headers {
518        // Convert headers from UTF-8 back to ISO-8859-1, with 0xFF for a replacement byte.
519        write!(head_bytes, "{}: ", header.name).unwrap();
520        head_bytes.extend(header.value.chars().map(|c| u8::try_from(c).unwrap_or(255)));
521        head_bytes.extend(b"\r\n");
522    }
523    head_bytes.extend(b"\r\n");
524    //dbg!(escape_ascii(head_bytes.as_slice()));
525    writer
526        .write_all(head_bytes.as_slice())
527        .await
528        .map_err(|_| HttpError::Disconnected)?;
529    drop(head_bytes);
530    match response.body.len() {
531        Some(0) => {}
532        Some(body_len) => {
533            let mut reader = AsyncReadExt::take(
534                response
535                    .body
536                    .async_reader()
537                    .await
538                    .map_err(HttpError::error_reading_file)?,
539                body_len,
540            );
541            let num_copied = copy_async(&mut reader, &mut writer, body_len)
542                .await
543                .map_errs(HttpError::error_reading_response_body, |_| {
544                    HttpError::Disconnected
545                })?;
546            if num_copied != body_len {
547                return Err(HttpError::ErrorReadingResponseBody(
548                    ErrorKind::UnexpectedEof,
549                    "body is smaller than expected".to_string(),
550                ));
551            }
552        }
553        None => {
554            let mut reader = response
555                .body
556                .async_reader()
557                .await
558                .map_err(HttpError::error_reading_response_body)?;
559            copy_chunked_async(&mut reader, &mut writer)
560                .await
561                .map_errs(HttpError::error_reading_response_body, |_| {
562                    HttpError::Disconnected
563                })?;
564        }
565    }
566    writer.flush().await.map_err(|_| HttpError::Disconnected)
567}