static_files_module/
handler.rs

1// Copyright 2024 Wladimir Palant
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Handler for the `request_filter` phase.
16
17use async_trait::async_trait;
18use log::{debug, info, warn};
19use module_utils::{RequestFilter, RequestFilterResult};
20use pingora_core::{Error, ErrorType};
21use pingora_http::{Method, StatusCode};
22use pingora_proxy::Session;
23use std::io::ErrorKind;
24
25use crate::compression::Compression;
26use crate::configuration::StaticFilesConf;
27use crate::file_writer::file_response;
28use crate::metadata::Metadata;
29use crate::path::{path_to_uri, resolve_uri};
30use crate::range::{extract_range, Range};
31use crate::standard_response::{error_response, redirect_response};
32
33/// Handler for Pingora’s `request_filter` phase
34#[derive(Debug)]
35pub struct StaticFilesHandler {
36    conf: StaticFilesConf,
37}
38
39impl StaticFilesHandler {
40    /// Provides read-only access to the handler’s configuration.
41    pub fn conf(&self) -> &StaticFilesConf {
42        &self.conf
43    }
44
45    /// Provides read-write access to the handler’s configuration.
46    pub fn conf_mut(&mut self) -> &mut StaticFilesConf {
47        &mut self.conf
48    }
49}
50
51#[async_trait]
52impl RequestFilter for StaticFilesHandler {
53    type Conf = StaticFilesConf;
54
55    type CTX = ();
56
57    fn new_ctx() -> Self::CTX {}
58
59    async fn request_filter(
60        &self,
61        session: &mut Session,
62        _ctx: &mut Self::CTX,
63    ) -> Result<RequestFilterResult, Box<Error>> {
64        let root = if let Some(root) = self.conf.root.as_ref() {
65            root
66        } else {
67            debug!("received request but static files handler is not configured, ignoring");
68            return Ok(RequestFilterResult::Unhandled);
69        };
70
71        let uri = &session.req_header().uri;
72        debug!("received URI path {}", uri.path());
73
74        let (mut path, not_found) = match resolve_uri(uri.path(), root) {
75            Ok(path) => (path, false),
76            Err(err) if err.kind() == ErrorKind::NotFound => {
77                debug!("canonicalizing resulted in NotFound error");
78
79                let path = self.conf.page_404.as_ref().and_then(|page_404| {
80                    debug!("error page is {page_404}");
81                    match resolve_uri(page_404, root) {
82                        Ok(path) => Some(path),
83                        Err(err) => {
84                            warn!("Failed resolving error page {page_404}: {err}");
85                            None
86                        }
87                    }
88                });
89
90                if let Some(path) = path {
91                    (path, true)
92                } else {
93                    error_response(session, StatusCode::NOT_FOUND).await?;
94                    return Ok(RequestFilterResult::ResponseSent);
95                }
96            }
97            Err(err) => {
98                let status = match err.kind() {
99                    ErrorKind::InvalidInput => {
100                        warn!("rejecting invalid path {}", uri.path());
101                        StatusCode::BAD_REQUEST
102                    }
103                    ErrorKind::InvalidData => {
104                        warn!("Requested path outside root directory: {}", uri.path());
105                        StatusCode::BAD_REQUEST
106                    }
107                    ErrorKind::PermissionDenied => {
108                        debug!("canonicalizing resulted in PermissionDenied error");
109                        StatusCode::FORBIDDEN
110                    }
111                    _ => {
112                        warn!("failed canonicalizing the path {}: {err}", uri.path());
113                        StatusCode::INTERNAL_SERVER_ERROR
114                    }
115                };
116                error_response(session, status).await?;
117                return Ok(RequestFilterResult::ResponseSent);
118            }
119        };
120
121        debug!("translated into file path {path:?}");
122
123        if self.conf.canonicalize_uri && !not_found {
124            if let Some(mut canonical) = path_to_uri(&path, root) {
125                if canonical != uri.path() {
126                    if let Some(query) = uri.query() {
127                        canonical.push('?');
128                        canonical.push_str(query);
129                    }
130                    if let Some(redirect_prefix) = &self.conf.redirect_prefix {
131                        canonical.insert_str(0, redirect_prefix);
132                    }
133                    info!("redirecting to canonical URI: {canonical}");
134                    redirect_response(session, StatusCode::PERMANENT_REDIRECT, &canonical).await?;
135                    return Ok(RequestFilterResult::ResponseSent);
136                }
137            }
138        }
139
140        if path.is_dir() {
141            for filename in &self.conf.index_file {
142                let candidate = path.join(filename);
143                if candidate.is_file() {
144                    debug!("using directory index file {filename}");
145                    path = candidate;
146                }
147            }
148        }
149
150        info!("successfully resolved request path: {path:?}");
151
152        match session.req_header().method {
153            Method::GET | Method::HEAD => {
154                // Allowed
155            }
156            _ => {
157                warn!("Denying method {}", session.req_header().method);
158                error_response(session, StatusCode::METHOD_NOT_ALLOWED).await?;
159                return Ok(RequestFilterResult::ResponseSent);
160            }
161        }
162
163        let mut compression = Compression::new(session, &self.conf.precompressed);
164
165        let (path, orig_path) =
166            if let Some(precompressed_path) = compression.rewrite_path(session, &path) {
167                (precompressed_path, Some(path))
168            } else {
169                (path, None)
170            };
171
172        let meta = match Metadata::from_path(&path, orig_path.as_ref()) {
173            Ok(meta) => meta,
174            Err(err) if err.kind() == ErrorKind::InvalidInput => {
175                warn!("Path {path:?} is not a regular file, denying access");
176                error_response(session, StatusCode::FORBIDDEN).await?;
177                return Ok(RequestFilterResult::ResponseSent);
178            }
179            Err(err) => {
180                warn!("failed retrieving metadata for path {path:?}: {err}");
181                error_response(session, StatusCode::INTERNAL_SERVER_ERROR).await?;
182                return Ok(RequestFilterResult::ResponseSent);
183            }
184        };
185
186        if meta.has_failed_precondition(session) {
187            debug!("If-Match/If-Unmodified-Since precondition failed");
188            let header = meta.to_custom_header(StatusCode::PRECONDITION_FAILED)?;
189            let header = compression.transform_header(session, header)?;
190            session.write_response_header(header).await?;
191            return Ok(RequestFilterResult::ResponseSent);
192        }
193
194        if meta.is_not_modified(session) {
195            debug!("If-None-Match/If-Modified-Since check resulted in Not Modified");
196            let header = meta.to_custom_header(StatusCode::NOT_MODIFIED)?;
197            let header = compression.transform_header(session, header)?;
198            session.write_response_header(header).await?;
199            return Ok(RequestFilterResult::ResponseSent);
200        }
201
202        let (mut header, start, end) = match extract_range(session, &meta) {
203            Some(Range::Valid(start, end)) => {
204                debug!("bytes range requested: {start}-{end}");
205                let header = meta.to_partial_content_header(start, end)?;
206                let header = compression.transform_header(session, header)?;
207                (header, start, end)
208            }
209            Some(Range::OutOfBounds) => {
210                debug!("requested bytes range is out of bounds");
211                let header = meta.to_custom_header(StatusCode::RANGE_NOT_SATISFIABLE)?;
212                let header = compression.transform_header(session, header)?;
213                session.write_response_header(header).await?;
214                return Ok(RequestFilterResult::ResponseSent);
215            }
216            None => {
217                // Range is either missing or cannot be parsed, produce the entire file.
218                let header = meta.to_response_header()?;
219                let header = compression.transform_header(session, header)?;
220                (header, 0, meta.size - 1)
221            }
222        };
223
224        if not_found {
225            header.set_status(StatusCode::NOT_FOUND)?;
226        }
227
228        session.write_response_header(header).await?;
229
230        if session.req_header().method == Method::GET {
231            // sendfile would be nice but not currently possible within pingora-proxy (see
232            // https://github.com/cloudflare/pingora/issues/160)
233            file_response(session, &path, start, end, &compression).await?;
234        }
235        Ok(RequestFilterResult::ResponseSent)
236    }
237}
238
239impl TryFrom<StaticFilesConf> for StaticFilesHandler {
240    type Error = Box<Error>;
241
242    fn try_from(mut conf: StaticFilesConf) -> Result<Self, Self::Error> {
243        conf.root = if let Some(root) = conf.root {
244            Some(root.canonicalize().map_err(|err| {
245                Error::because(
246                    ErrorType::InternalError,
247                    format!("Failed accessing root path {:?}", root),
248                    err,
249                )
250            })?)
251        } else {
252            None
253        };
254
255        debug!("Initialized static files handler, settings: {conf:#?}");
256        Ok(Self { conf })
257    }
258}