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, sync::Arc};
18
19pub type DefaultResourceFiles = HashMap<&'static str, Resource>;
20
21pub trait ResourceFile {
23 fn data(&self) -> &'static [u8];
24 fn modified(&self) -> u64;
25 fn mime_type(&self) -> &str;
26}
27
28pub trait ResourceFilesCollection {
30 type Resource: ResourceFile;
31 fn get_resource(&self, path: &str) -> Option<&Self::Resource>;
33 fn contains_key(&self, path: &str) -> bool;
35}
36
37impl<R> ResourceFilesCollection for Rc<R>
38where
39 R: ResourceFilesCollection,
40{
41 type Resource = R::Resource;
42 fn get_resource(&self, path: &str) -> Option<&Self::Resource> {
43 let r: &R = self;
44 r.get_resource(path)
45 }
46
47 fn contains_key(&self, path: &str) -> bool {
48 let r: &R = self;
49 r.contains_key(path)
50 }
51}
52
53impl<R> ResourceFilesCollection for Arc<R>
54where
55 R: ResourceFilesCollection,
56{
57 type Resource = R::Resource;
58 fn get_resource(&self, path: &str) -> Option<&Self::Resource> {
59 let r: &R = self;
60 r.get_resource(path)
61 }
62
63 fn contains_key(&self, path: &str) -> bool {
64 let r: &R = self;
65 r.contains_key(path)
66 }
67}
68
69mod legacy_static_files {
70 use super::*;
71
72 impl ResourceFile for Resource {
73 fn data(&self) -> &'static [u8] {
74 self.data
75 }
76
77 fn modified(&self) -> u64 {
78 self.modified
79 }
80
81 fn mime_type(&self) -> &str {
82 self.mime_type
83 }
84 }
85
86 impl ResourceFilesCollection for DefaultResourceFiles {
87 type Resource = Resource;
88 fn get_resource(&self, path: &str) -> Option<&Self::Resource> {
89 self.get(path)
90 }
91
92 fn contains_key(&self, path: &str) -> bool {
93 self.contains_key(path)
94 }
95 }
96}
97
98#[allow(clippy::needless_doctest_main)]
121pub struct ResourceFiles<C = DefaultResourceFiles> {
122 not_resolve_defaults: bool,
123 use_guard: bool,
124 not_found_resolves_to: Option<String>,
125 inner: Rc<ResourceFilesInner<C>>,
126}
127
128pub struct ResourceFilesInner<C> {
129 path: String,
130 files: C,
131}
132
133const INDEX_HTML: &str = "index.html";
134
135impl<F> ResourceFiles<F>
136where
137 F: ResourceFilesCollection + 'static,
138{
139 #[must_use]
140 pub fn new(path: &str, files: F) -> Self {
141 let inner = ResourceFilesInner {
142 path: path.into(),
143 files,
144 };
145 Self {
146 inner: Rc::new(inner),
147 not_resolve_defaults: false,
148 not_found_resolves_to: None,
149 use_guard: false,
150 }
151 }
152
153 #[must_use]
156 pub fn do_not_resolve_defaults(mut self) -> Self {
157 self.not_resolve_defaults = true;
158 self
159 }
160
161 #[must_use]
165 pub fn resolve_not_found_to<S: ToString>(mut self, path: S) -> Self {
166 self.not_found_resolves_to = Some(path.to_string());
167 self
168 }
169
170 #[must_use]
174 pub fn resolve_not_found_to_root(self) -> Self {
175 self.resolve_not_found_to(INDEX_HTML)
176 }
177
178 #[must_use]
184 pub fn skip_handler_when_not_found(mut self) -> Self {
185 self.use_guard = true;
186 self
187 }
188
189 fn select_guard(&self) -> Box<dyn Guard> {
190 if self.not_resolve_defaults {
191 Box::new(NotResolveDefaultsGuard::from(self))
192 } else {
193 Box::new(ResolveDefaultsGuard::from(self))
194 }
195 }
196}
197
198impl<C> Deref for ResourceFiles<C> {
199 type Target = ResourceFilesInner<C>;
200
201 fn deref(&self) -> &Self::Target {
202 &self.inner
203 }
204}
205
206struct NotResolveDefaultsGuard<C> {
207 inner: Rc<ResourceFilesInner<C>>,
208}
209
210impl<C> Guard for NotResolveDefaultsGuard<C>
211where
212 C: ResourceFilesCollection,
213{
214 fn check(&self, ctx: &GuardContext<'_>) -> bool {
215 self.inner
216 .files
217 .contains_key(ctx.head().uri.path().trim_start_matches('/'))
218 }
219}
220
221impl<C> From<&ResourceFiles<C>> for NotResolveDefaultsGuard<C> {
222 fn from(files: &ResourceFiles<C>) -> Self {
223 Self {
224 inner: files.inner.clone(),
225 }
226 }
227}
228
229struct ResolveDefaultsGuard<C> {
230 inner: Rc<ResourceFilesInner<C>>,
231}
232
233impl<C> Guard for ResolveDefaultsGuard<C>
234where
235 C: ResourceFilesCollection,
236{
237 fn check(&self, ctx: &GuardContext<'_>) -> bool {
238 let path = ctx.head().uri.path().trim_start_matches('/');
239 self.inner.files.contains_key(path)
240 || ((path.is_empty() || path.ends_with('/'))
241 && self
242 .inner
243 .files
244 .contains_key((path.to_string() + INDEX_HTML).as_str()))
245 }
246}
247
248impl<C> From<&ResourceFiles<C>> for ResolveDefaultsGuard<C> {
249 fn from(files: &ResourceFiles<C>) -> Self {
250 Self {
251 inner: files.inner.clone(),
252 }
253 }
254}
255
256impl<C> HttpServiceFactory for ResourceFiles<C>
257where
258 C: ResourceFilesCollection + 'static,
259{
260 fn register(self, config: &mut AppService) {
261 let prefix = self.path.trim_start_matches('/');
262 let rdef = if config.is_root() {
263 ResourceDef::root_prefix(prefix)
264 } else {
265 ResourceDef::prefix(prefix)
266 };
267 let guards = if self.use_guard && self.not_found_resolves_to.is_none() {
268 Some(vec![self.select_guard()])
269 } else {
270 None
271 };
272 config.register_service(rdef, guards, self, None);
273 }
274}
275
276impl<C> ServiceFactory<ServiceRequest> for ResourceFiles<C>
277where
278 C: ResourceFilesCollection + 'static,
279{
280 type Response = ServiceResponse;
281 type Error = Error;
282 type Config = ();
283 type Service = ResourceFilesService<C>;
284 type InitError = ();
285 type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
286
287 fn new_service(&self, _: ()) -> Self::Future {
288 ok(ResourceFilesService {
289 resolve_defaults: !self.not_resolve_defaults,
290 not_found_resolves_to: self.not_found_resolves_to.clone(),
291 inner: self.inner.clone(),
292 })
293 .boxed_local()
294 }
295}
296
297#[derive(Deref)]
298pub struct ResourceFilesService<C> {
299 resolve_defaults: bool,
300 not_found_resolves_to: Option<String>,
301 #[deref]
302 inner: Rc<ResourceFilesInner<C>>,
303}
304
305impl<C> Service<ServiceRequest> for ResourceFilesService<C>
306where
307 C: ResourceFilesCollection,
308{
309 type Response = ServiceResponse;
310 type Error = Error;
311 type Future = Ready<Result<Self::Response, Self::Error>>;
312
313 always_ready!();
314
315 fn call(&self, req: ServiceRequest) -> Self::Future {
316 match *req.method() {
317 Method::HEAD | Method::GET => (),
318 _ => {
319 return ok(ServiceResponse::new(
320 req.into_parts().0,
321 HttpResponse::MethodNotAllowed()
322 .insert_header(ContentType::plaintext())
323 .insert_header((header::ALLOW, "GET, HEAD"))
324 .body("This resource only supports GET and HEAD."),
325 ));
326 }
327 }
328
329 let req_path = req.match_info().unprocessed();
330 let mut item = self.files.get_resource(req_path);
331
332 if item.is_none()
333 && self.resolve_defaults
334 && (req_path.is_empty() || req_path.ends_with('/'))
335 {
336 let index_req_path = req_path.to_string() + INDEX_HTML;
337 item = self
338 .files
339 .get_resource(index_req_path.trim_start_matches('/'));
340 }
341
342 let (req, response) = if item.is_some() {
343 let (req, _) = req.into_parts();
344 let response = respond_to(&req, item);
345 (req, response)
346 } else {
347 let real_path = match get_pathbuf(req_path) {
348 Ok(item) => item,
349 Err(e) => return ok(req.error_response(e)),
350 };
351
352 let (req, _) = req.into_parts();
353
354 let mut item = self.files.get_resource(real_path.as_str());
355
356 if item.is_none() && self.not_found_resolves_to.is_some() {
357 let not_found_path = self.not_found_resolves_to.as_ref().unwrap();
358 item = self.files.get_resource(not_found_path.as_str());
359 }
360
361 let response = respond_to(&req, item);
362 (req, response)
363 };
364
365 ok(ServiceResponse::new(req, response))
366 }
367}
368
369fn respond_to<Resource: ResourceFile>(req: &HttpRequest, item: Option<&Resource>) -> HttpResponse {
370 if let Some(file) = item {
371 let etag = Some(header::EntityTag::new_strong(format!(
372 "{:x}:{:x}",
373 file.data().len(),
374 file.modified()
375 )));
376
377 let precondition_failed = !any_match(etag.as_ref(), req);
378
379 let not_modified = !none_match(etag.as_ref(), req);
380
381 let mut resp = HttpResponse::build(StatusCode::OK);
382 resp.insert_header((header::CONTENT_TYPE, file.mime_type()));
383
384 if let Some(etag) = etag {
385 resp.insert_header(header::ETag(etag));
386 }
387
388 if precondition_failed {
389 return resp.status(StatusCode::PRECONDITION_FAILED).finish();
390 } else if not_modified {
391 return resp.status(StatusCode::NOT_MODIFIED).finish();
392 }
393
394 resp.body(file.data())
395 } else {
396 HttpResponse::NotFound().body("Not found")
397 }
398}
399
400fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
402 match req.get_header::<header::IfMatch>() {
403 None | Some(header::IfMatch::Any) => true,
404 Some(header::IfMatch::Items(ref items)) => {
405 if let Some(some_etag) = etag {
406 for item in items {
407 if item.strong_eq(some_etag) {
408 return true;
409 }
410 }
411 }
412 false
413 }
414 }
415}
416
417fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
419 match req.get_header::<header::IfNoneMatch>() {
420 Some(header::IfNoneMatch::Any) => false,
421 Some(header::IfNoneMatch::Items(ref items)) => {
422 if let Some(some_etag) = etag {
423 for item in items {
424 if item.weak_eq(some_etag) {
425 return false;
426 }
427 }
428 }
429 true
430 }
431 None => true,
432 }
433}
434
435#[derive(Debug, PartialEq, Display, Error)]
436pub enum UriSegmentError {
437 #[display(fmt = "The segment started with the wrapped invalid character")]
439 BadStart(#[error(not(source))] char),
440
441 #[display(fmt = "The segment contained the wrapped invalid character")]
443 BadChar(#[error(not(source))] char),
444
445 #[display(fmt = "The segment ended with the wrapped invalid character")]
447 BadEnd(#[error(not(source))] char),
448}
449
450impl ResponseError for UriSegmentError {
452 fn error_response(&self) -> HttpResponse {
453 HttpResponse::new(StatusCode::BAD_REQUEST)
454 }
455}
456
457fn get_pathbuf(path: &str) -> Result<String, UriSegmentError> {
458 let mut buf = Vec::new();
459 for segment in path.split('/') {
460 if segment == ".." {
461 buf.pop();
462 } else if segment.starts_with('.') {
463 return Err(UriSegmentError::BadStart('.'));
464 } else if segment.starts_with('*') {
465 return Err(UriSegmentError::BadStart('*'));
466 } else if segment.ends_with(':') {
467 return Err(UriSegmentError::BadEnd(':'));
468 } else if segment.ends_with('>') {
469 return Err(UriSegmentError::BadEnd('>'));
470 } else if segment.ends_with('<') {
471 return Err(UriSegmentError::BadEnd('<'));
472 } else if segment.is_empty() {
473 continue;
474 } else if cfg!(windows) && segment.contains('\\') {
475 return Err(UriSegmentError::BadChar('\\'));
476 } else {
477 buf.push(segment);
478 }
479 }
480
481 Ok(buf.join("/"))
482}
483
484#[cfg(test)]
485mod tests_error_impl {
486 use super::*;
487
488 fn assert_send_and_sync<T: Send + Sync + 'static>() {}
489
490 #[test]
491 fn test_error_impl() {
492 assert_send_and_sync::<UriSegmentError>();
494 }
495}