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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
//! 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 std::cmp;
use std::convert::Infallible;
use std::pin::Pin;
use std::task::{Context, Poll};

/// Represents hyper HttpBody compatible response based on static memory chunk.
/// This is used as a body handler for [u8] content, directly from loader.
pub struct StaticBody {
    /// Slice of pending (unsent) data.
    /// After every successful transmission this is moved forward.
    pending_content: &'static [u8],
}
impl StaticBody {
    /// Maximal number of bytes to be sent at once.
    const CHUNK_SIZE: usize = 8 * 1024;

    /// Constructor.
    /// Creates StaticBody from static memory slice.
    /// Whole `content` will be sent.
    pub fn new(content: &'static [u8]) -> Self {
        Self {
            pending_content: content,
        }
    }
}
impl Default for StaticBody {
    /// Creates StaticBody initialized with empty chunk, therefore yielding empty body.
    fn default() -> Self {
        Self {
            pending_content: &[],
        }
    }
}
impl hyper::body::HttpBody for StaticBody {
    type Data = &'static [u8];
    type Error = Infallible;

    /// Sends up to CHUNK_SIZE data at once.
    fn poll_data(
        self: Pin<&mut Self>,
        _cx: &mut Context,
    ) -> Poll<Option<Result<Self::Data, Self::Error>>> {
        let self_ = self.get_mut();

        if self_.is_end_stream() {
            return Poll::Ready(None);
        }

        let pending_content_end = cmp::min(Self::CHUNK_SIZE, self_.pending_content.len());
        let chunk = &self_.pending_content[..pending_content_end];
        self_.pending_content = &self_.pending_content[pending_content_end..];

        Poll::Ready(Some(Ok(chunk)))
    }

    /// We don't use any trailers, so this always returns Ready-No-Headers value.
    fn poll_trailers(
        self: Pin<&mut Self>,
        _cx: &mut Context,
    ) -> Poll<Result<Option<hyper::HeaderMap>, Self::Error>> {
        Poll::Ready(Ok(None))
    }

    // We know where we are, so we can override this method and provide good hints.
    fn is_end_stream(&self) -> bool {
        self.pending_content.len() <= 0
    }

    // Chunks size is known, so we can always provide exact hint.
    fn size_hint(&self) -> http_body::SizeHint {
        http_body::SizeHint::with_exact(self.pending_content.len() as u64)
    }
}

/// 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) -> http::StatusCode {
        match self {
            ResponderError::HttpMethodNotSupported => http::StatusCode::METHOD_NOT_ALLOWED,
            ResponderError::LoaderPathNotFound => http::StatusCode::NOT_FOUND,
            ResponderError::UnparsableAcceptEncoding => http::StatusCode::BAD_REQUEST,
        }
    }

    /// Creates default response (status code + empty body) for this error.
    pub fn as_default_response(&self) -> hyper::Response<StaticBody> {
        return hyper::Response::builder()
            .status(self.as_http_status_code())
            .body(StaticBody::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: &hyper::Request<hyper::Body>,
    ) -> Result<hyper::Response<StaticBody>, ResponderError> {
        return 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: &http::Method,
        uri: &http::Uri,
        headers: &http::HeaderMap,
    ) -> Result<hyper::Response<StaticBody>, ResponderError> {
        // Only GET requests are allowed.
        // TODO: Handle HEAD requests.
        match *method {
            http::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(http::header::IF_NONE_MATCH) {
            if etag_request.as_bytes() == file_descriptor.etag().as_bytes() {
                return Ok(hyper::Response::builder()
                    .status(http::StatusCode::NOT_MODIFIED)
                    .body(StaticBody::default())
                    .unwrap());
            }
        };

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

            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 chunk_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 = hyper::Response::builder()
            .header(http::header::CONTENT_TYPE, file_descriptor.content_type())
            .header(http::header::CONTENT_LENGTH, chunk_encoding.0.len())
            .header(http::header::CONTENT_ENCODING, chunk_encoding.1)
            .header(http::header::ETAG, file_descriptor.etag())
            .body(StaticBody::new(chunk_encoding.0))
            .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: &hyper::Request<hyper::Body>,
    ) -> hyper::Response<StaticBody> {
        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: &http::Method,
        uri: &http::Uri,
        headers: &http::HeaderMap,
    ) -> hyper::Response<StaticBody> {
        match self.parts_respond_or_error(method, uri, headers) {
            Ok(response) => response,
            Err(error) => error.as_default_response(),
        }
    }
}