1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
//! Hyper integration.
//! See examples/docs/main.rs for usage sample.
//!
//! Entry point for this module is `Responder`.
//! Create `Responder` providing reference to `Loader`.
//! Use `request_respond()` method to serve file in response to request.

use super::loader::Loader;
use http::{header, HeaderMap, Method, StatusCode, Uri};
use hyper::{Body, Request, Response};

/// Possible errors during `Responder` handling
pub enum ResponderError {
    /// Not supported HTTP Method, this maps to HTTP `METHOD_NOT_ALLOWED`.
    HttpMethodNotSupported,

    /// Request URI was not found in `Loader`. This maps to HTTP `NOT_FOUND`.
    LoaderPathNotFound,

    /// Error while parsing HTTP `Accept-Encoding`. This maps to HTTP `BAD_REQUEST`.
    UnparsableAcceptEncoding,
}
impl ResponderError {
    /// Converts error into best matching HTTP error code
    pub fn as_http_status_code(&self) -> StatusCode {
        match self {
            ResponderError::HttpMethodNotSupported => StatusCode::METHOD_NOT_ALLOWED,
            ResponderError::LoaderPathNotFound => StatusCode::NOT_FOUND,
            ResponderError::UnparsableAcceptEncoding => StatusCode::BAD_REQUEST,
        }
    }

    /// Creates default response (status code + empty body) for this error.
    pub fn as_default_response(&self) -> Response<Body> {
        Response::builder()
            .status(self.as_http_status_code())
            .body(Body::default())
            .unwrap()
    }
}

/// Main class for hyper integration.
/// Given `Loader`, responds to incoming requests serving files from `Loader`.
pub struct Responder<'l> {
    loader: &'l Loader,
}
impl<'l> Responder<'l> {
    /// Creates instance, using provided `Loader`.
    pub fn new(loader: &'l Loader) -> Self {
        Self { loader }
    }

    /// Given basic hyper request, responds to it, or returns `ResponderError`.
    /// To automatically cast `ResponderError` to response, use `request_respond` instead.
    pub fn request_respond_or_error(
        &self,
        request: &Request<Body>,
    ) -> Result<Response<Body>, ResponderError> {
        self.parts_respond_or_error(request.method(), request.uri(), request.headers())
    }

    /// Given set of parts (`method`, `uri` and `headers`), responds to it, or returns `ResponderError`.
    /// To automatically cast `ResponderError` to response, use `parts_respond` instead.
    pub fn parts_respond_or_error(
        &self,
        method: &Method,
        uri: &Uri,
        headers: &HeaderMap,
    ) -> Result<Response<Body>, ResponderError> {
        // Only GET requests are allowed.
        // TODO: Handle HEAD requests.
        match *method {
            Method::GET => (),
            _ => {
                return Err(ResponderError::HttpMethodNotSupported);
            }
        };

        // Find file for given request.
        let file_descriptor = match self.loader.get(uri.path()) {
            Some(file_descriptor) => file_descriptor,
            None => {
                return Err(ResponderError::LoaderPathNotFound);
            }
        };

        // Check for possible ETag.
        // If ETag exists and matches current file, return 304.
        if let Some(ref etag_request) = headers.get(header::IF_NONE_MATCH) {
            if etag_request.as_bytes() == file_descriptor.etag().as_bytes() {
                return Ok(Response::builder()
                    .status(StatusCode::NOT_MODIFIED)
                    .body(Body::default())
                    .unwrap());
            }
        };

        // Check accepted encodings
        let mut accepted_encoding_gzip = false;
        if let Some(accept_encoding) = headers.get(header::ACCEPT_ENCODING) {
            let accept_encoding = match accept_encoding.to_str() {
                Ok(accept_encoding) => accept_encoding,
                Err(_) => {
                    return Err(ResponderError::UnparsableAcceptEncoding);
                }
            };

            #[allow(clippy::single_match)]
            accept_encoding
                .split(", ")
                .for_each(|accept_encoding| match accept_encoding {
                    "gzip" => {
                        accepted_encoding_gzip = true;
                    }
                    _ => {}
                });
        }

        // Select data based on accepted encoding
        // (chunk, content_encoding)
        let (content, content_encoding) =
            if accepted_encoding_gzip && file_descriptor.content_gzip().is_some() {
                (file_descriptor.content_gzip().unwrap(), "gzip")
            } else {
                (file_descriptor.content(), "identity")
            };

        // Provide response.
        let response = Response::builder()
            .header(header::CONTENT_TYPE, file_descriptor.content_type())
            .header(header::CONTENT_LENGTH, content.len())
            .header(header::CONTENT_ENCODING, content_encoding)
            .header(header::ETAG, file_descriptor.etag())
            .body(Body::from(content))
            .unwrap();

        Ok(response)
    }

    /// Given basic hyper request, responds to it.
    /// In case of error creates default http response. If specific error control is needed, use `request_respond_or_error` instead.
    pub fn request_respond(
        &self,
        request: &Request<Body>,
    ) -> Response<Body> {
        match self.request_respond_or_error(request) {
            Ok(response) => response,
            Err(error) => error.as_default_response(),
        }
    }

    /// Given set of parts (`method`, `uri` and `headers`), responds to it.
    /// In case of error creates default http response. If specific error control is needed, use `parts_respond_or_error` instead.
    pub fn parts_respond(
        &self,
        method: &Method,
        uri: &Uri,
        headers: &HeaderMap,
    ) -> Response<Body> {
        match self.parts_respond_or_error(method, uri, headers) {
            Ok(response) => response,
            Err(error) => error.as_default_response(),
        }
    }
}