actix_web_static_files/
resource_files.rs

1use actix_web::{
2    dev::{
3        always_ready, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory,
4        ServiceRequest, ServiceResponse,
5    },
6    error::Error,
7    guard::{Guard, GuardContext},
8    http::{
9        header::{self, ContentType},
10        Method, StatusCode,
11    },
12    HttpMessage, HttpRequest, HttpResponse, ResponseError,
13};
14use derive_more::{Deref, Display, Error};
15use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready};
16use static_files::Resource;
17use std::{collections::HashMap, ops::Deref, rc::Rc};
18
19/// Static resource files handling
20///
21/// `ResourceFiles` service must be registered with `App::service` method.
22///
23/// ```rust
24/// use std::collections::HashMap;
25///
26/// use actix_web::App;
27///
28/// fn main() {
29/// // serve root directory with default options:
30/// // - resolve index.html
31///     let files: HashMap<&'static str, static_files::Resource> = HashMap::new();
32///     let app = App::new()
33///         .service(actix_web_static_files::ResourceFiles::new("/", files));
34/// // or subpath with additional option to not resolve index.html
35///     let files: HashMap<&'static str, static_files::Resource> = HashMap::new();
36///     let app = App::new()
37///         .service(actix_web_static_files::ResourceFiles::new("/imgs", files)
38///             .do_not_resolve_defaults());
39/// }
40/// ```
41#[allow(clippy::needless_doctest_main)]
42pub struct ResourceFiles {
43    not_resolve_defaults: bool,
44    use_guard: bool,
45    not_found_resolves_to: Option<String>,
46    inner: Rc<ResourceFilesInner>,
47}
48
49pub struct ResourceFilesInner {
50    path: String,
51    files: HashMap<&'static str, Resource>,
52}
53
54const INDEX_HTML: &str = "index.html";
55
56impl ResourceFiles {
57    pub fn new(path: &str, files: HashMap<&'static str, Resource>) -> Self {
58        let inner = ResourceFilesInner {
59            path: path.into(),
60            files,
61        };
62        Self {
63            inner: Rc::new(inner),
64            not_resolve_defaults: false,
65            not_found_resolves_to: None,
66            use_guard: false,
67        }
68    }
69
70    /// By default trying to resolve '.../' to '.../index.html' if it exists.
71    /// Turn off this resolution by calling this function.
72    pub fn do_not_resolve_defaults(mut self) -> Self {
73        self.not_resolve_defaults = true;
74        self
75    }
76
77    /// Resolves not found references to this path.
78    ///
79    /// This can be useful for angular-like applications.
80    pub fn resolve_not_found_to<S: ToString>(mut self, path: S) -> Self {
81        self.not_found_resolves_to = Some(path.to_string());
82        self
83    }
84
85    /// Resolves not found references to root path.
86    ///
87    /// This can be useful for angular-like applications.
88    pub fn resolve_not_found_to_root(self) -> Self {
89        self.resolve_not_found_to(INDEX_HTML)
90    }
91
92    /// If this is called, we will use an [actix_web::guard::Guard] to check if this request should be handled.
93    /// If set to true, we skip using the handler for files that haven't been found, instead of sending 404s.
94    /// Would be ignored, if `resolve_not_found_to` or `resolve_not_found_to_root` is used.
95    ///
96    /// Can be useful if you want to share files on a (sub)path that's also used by a different route handler.
97    pub fn skip_handler_when_not_found(mut self) -> Self {
98        self.use_guard = true;
99        self
100    }
101
102    fn select_guard(&self) -> Box<dyn Guard> {
103        if self.not_resolve_defaults {
104            Box::new(NotResolveDefaultsGuard::from(self))
105        } else {
106            Box::new(ResolveDefaultsGuard::from(self))
107        }
108    }
109}
110
111impl Deref for ResourceFiles {
112    type Target = ResourceFilesInner;
113
114    fn deref(&self) -> &Self::Target {
115        &self.inner
116    }
117}
118
119struct NotResolveDefaultsGuard {
120    inner: Rc<ResourceFilesInner>,
121}
122
123impl Guard for NotResolveDefaultsGuard {
124    fn check(&self, ctx: &GuardContext<'_>) -> bool {
125        self.inner
126            .files
127            .contains_key(ctx.head().uri.path().trim_start_matches('/'))
128    }
129}
130
131impl From<&ResourceFiles> for NotResolveDefaultsGuard {
132    fn from(files: &ResourceFiles) -> Self {
133        Self {
134            inner: files.inner.clone(),
135        }
136    }
137}
138
139struct ResolveDefaultsGuard {
140    inner: Rc<ResourceFilesInner>,
141}
142
143impl Guard for ResolveDefaultsGuard {
144    fn check(&self, ctx: &GuardContext<'_>) -> bool {
145        let path = ctx.head().uri.path().trim_start_matches('/');
146        self.inner.files.contains_key(path)
147            || ((path.is_empty() || path.ends_with('/'))
148                && self
149                    .inner
150                    .files
151                    .contains_key((path.to_string() + INDEX_HTML).as_str()))
152    }
153}
154
155impl From<&ResourceFiles> for ResolveDefaultsGuard {
156    fn from(files: &ResourceFiles) -> Self {
157        Self {
158            inner: files.inner.clone(),
159        }
160    }
161}
162
163impl HttpServiceFactory for ResourceFiles {
164    fn register(self, config: &mut AppService) {
165        let prefix = self.path.trim_start_matches('/');
166        let rdef = if config.is_root() {
167            ResourceDef::root_prefix(prefix)
168        } else {
169            ResourceDef::prefix(prefix)
170        };
171        let guards = if self.use_guard && self.not_found_resolves_to.is_none() {
172            Some(vec![self.select_guard()])
173        } else {
174            None
175        };
176        config.register_service(rdef, guards, self, None);
177    }
178}
179
180impl ServiceFactory<ServiceRequest> for ResourceFiles {
181    type Config = ();
182    type Response = ServiceResponse;
183    type Error = Error;
184    type Service = ResourceFilesService;
185    type InitError = ();
186    type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
187
188    fn new_service(&self, _: ()) -> Self::Future {
189        ok(ResourceFilesService {
190            resolve_defaults: !self.not_resolve_defaults,
191            not_found_resolves_to: self.not_found_resolves_to.clone(),
192            inner: self.inner.clone(),
193        })
194        .boxed_local()
195    }
196}
197
198#[derive(Deref)]
199pub struct ResourceFilesService {
200    resolve_defaults: bool,
201    not_found_resolves_to: Option<String>,
202    #[deref]
203    inner: Rc<ResourceFilesInner>,
204}
205
206impl Service<ServiceRequest> for ResourceFilesService {
207    type Response = ServiceResponse;
208    type Error = Error;
209    type Future = Ready<Result<Self::Response, Self::Error>>;
210
211    always_ready!();
212
213    fn call(&self, req: ServiceRequest) -> Self::Future {
214        match *req.method() {
215            Method::HEAD | Method::GET => (),
216            _ => {
217                return ok(ServiceResponse::new(
218                    req.into_parts().0,
219                    HttpResponse::MethodNotAllowed()
220                        .insert_header(ContentType::plaintext())
221                        .insert_header((header::ALLOW, "GET, HEAD"))
222                        .body("This resource only supports GET and HEAD."),
223                ));
224            }
225        }
226
227        let req_path = req.match_info().unprocessed();
228        let mut item = self.files.get(req_path);
229
230        if item.is_none()
231            && self.resolve_defaults
232            && (req_path.is_empty() || req_path.ends_with('/'))
233        {
234            let index_req_path = req_path.to_string() + INDEX_HTML;
235            item = self.files.get(index_req_path.trim_start_matches('/'));
236        }
237
238        let (req, response) = if item.is_some() {
239            let (req, _) = req.into_parts();
240            let response = respond_to(&req, item);
241            (req, response)
242        } else {
243            let real_path = match get_pathbuf(req_path) {
244                Ok(item) => item,
245                Err(e) => return ok(req.error_response(e)),
246            };
247
248            let (req, _) = req.into_parts();
249
250            let mut item = self.files.get(real_path.as_str());
251
252            if item.is_none() && self.not_found_resolves_to.is_some() {
253                let not_found_path = self.not_found_resolves_to.as_ref().unwrap();
254                item = self.files.get(not_found_path.as_str());
255            }
256
257            let response = respond_to(&req, item);
258            (req, response)
259        };
260
261        ok(ServiceResponse::new(req, response))
262    }
263}
264
265fn respond_to(req: &HttpRequest, item: Option<&Resource>) -> HttpResponse {
266    if let Some(file) = item {
267        let etag = Some(header::EntityTag::new_strong(format!(
268            "{:x}:{:x}",
269            file.data.len(),
270            file.modified
271        )));
272
273        let precondition_failed = !any_match(etag.as_ref(), req);
274
275        let not_modified = !none_match(etag.as_ref(), req);
276
277        let mut resp = HttpResponse::build(StatusCode::OK);
278        resp.insert_header((header::CONTENT_TYPE, file.mime_type));
279
280        if let Some(etag) = etag {
281            resp.insert_header(header::ETag(etag));
282        }
283
284        if precondition_failed {
285            return resp.status(StatusCode::PRECONDITION_FAILED).finish();
286        } else if not_modified {
287            return resp.status(StatusCode::NOT_MODIFIED).finish();
288        }
289
290        resp.body(file.data)
291    } else {
292        HttpResponse::NotFound().body("Not found")
293    }
294}
295
296/// Returns true if `req` has no `If-Match` header or one which matches `etag`.
297fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
298    match req.get_header::<header::IfMatch>() {
299        None | Some(header::IfMatch::Any) => true,
300        Some(header::IfMatch::Items(ref items)) => {
301            if let Some(some_etag) = etag {
302                for item in items {
303                    if item.strong_eq(some_etag) {
304                        return true;
305                    }
306                }
307            }
308            false
309        }
310    }
311}
312
313/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`.
314fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
315    match req.get_header::<header::IfNoneMatch>() {
316        Some(header::IfNoneMatch::Any) => false,
317        Some(header::IfNoneMatch::Items(ref items)) => {
318            if let Some(some_etag) = etag {
319                for item in items {
320                    if item.weak_eq(some_etag) {
321                        return false;
322                    }
323                }
324            }
325            true
326        }
327        None => true,
328    }
329}
330
331#[derive(Debug, PartialEq, Display, Error)]
332pub enum UriSegmentError {
333    /// The segment started with the wrapped invalid character.
334    #[display(fmt = "The segment started with the wrapped invalid character")]
335    BadStart(#[error(not(source))] char),
336
337    /// The segment contained the wrapped invalid character.
338    #[display(fmt = "The segment contained the wrapped invalid character")]
339    BadChar(#[error(not(source))] char),
340
341    /// The segment ended with the wrapped invalid character.
342    #[display(fmt = "The segment ended with the wrapped invalid character")]
343    BadEnd(#[error(not(source))] char),
344}
345
346#[cfg(test)]
347mod tests_error_impl {
348    use super::*;
349
350    fn assert_send_and_sync<T: Send + Sync + 'static>() {}
351
352    #[test]
353    fn test_error_impl() {
354        // ensure backwards compatibility when migrating away from failure
355        assert_send_and_sync::<UriSegmentError>();
356    }
357}
358
359/// Return `BadRequest` for `UriSegmentError`
360impl ResponseError for UriSegmentError {
361    fn error_response(&self) -> HttpResponse {
362        HttpResponse::new(StatusCode::BAD_REQUEST)
363    }
364}
365
366fn get_pathbuf(path: &str) -> Result<String, UriSegmentError> {
367    let mut buf = Vec::new();
368    for segment in path.split('/') {
369        if segment == ".." {
370            buf.pop();
371        } else if segment.starts_with('.') {
372            return Err(UriSegmentError::BadStart('.'));
373        } else if segment.starts_with('*') {
374            return Err(UriSegmentError::BadStart('*'));
375        } else if segment.ends_with(':') {
376            return Err(UriSegmentError::BadEnd(':'));
377        } else if segment.ends_with('>') {
378            return Err(UriSegmentError::BadEnd('>'));
379        } else if segment.ends_with('<') {
380            return Err(UriSegmentError::BadEnd('<'));
381        } else if segment.is_empty() {
382            continue;
383        } else if cfg!(windows) && segment.contains('\\') {
384            return Err(UriSegmentError::BadChar('\\'));
385        } else {
386            buf.push(segment)
387        }
388    }
389
390    Ok(buf.join("/"))
391}