static_files_module/
handler.rs1use 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#[derive(Debug)]
35pub struct StaticFilesHandler {
36 conf: StaticFilesConf,
37}
38
39impl StaticFilesHandler {
40 pub fn conf(&self) -> &StaticFilesConf {
42 &self.conf
43 }
44
45 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 }
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 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 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}