Skip to main content

actix_plus_static_files/
impl.rs

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