diff --git a/helpers/pagetop-statics/src/lib.rs b/helpers/pagetop-statics/src/lib.rs index d2f147e3..f0135695 100644 --- a/helpers/pagetop-statics/src/lib.rs +++ b/helpers/pagetop-statics/src/lib.rs @@ -47,10 +47,7 @@ pub mod resource; pub use resource::Resource as StaticResource; mod resource_dir; -pub use resource_dir::{resource_dir, ResourceDir}; - -mod resource_files; -pub use resource_files::{ResourceFiles, UriSegmentError}; +pub use resource_dir::{ResourceDir, resource_dir}; /// Support for module based generations. Use it for large data sets (more than 128 Mb). pub mod sets; diff --git a/helpers/pagetop-statics/src/resource_files.rs b/helpers/pagetop-statics/src/resource_files.rs deleted file mode 100644 index b487bca9..00000000 --- a/helpers/pagetop-statics/src/resource_files.rs +++ /dev/null @@ -1,396 +0,0 @@ -use super::resource::Resource; -use actix_web::{ - dev::{ - always_ready, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory, - ServiceRequest, ServiceResponse, - }, - error::Error, - guard::{Guard, GuardContext}, - http::{ - header::{self, ContentType}, - Method, StatusCode, - }, - HttpMessage, HttpRequest, HttpResponse, ResponseError, -}; -use derive_more::{Deref, Display, Error}; -use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready}; -use std::{collections::HashMap, ops::Deref, rc::Rc}; - -/// Static resource files handling -/// -/// `ResourceFiles` service must be registered with `App::service` method. -/// -/// ```rust -/// use std::collections::HashMap; -/// -/// use actix_web::App; -/// -/// fn main() { -/// // serve root directory with default options: -/// // - resolve index.html -/// let files: HashMap<&'static str, pagetop_statics::StaticResource> = HashMap::new(); -/// let app = App::new() -/// .service(pagetop_statics::ResourceFiles::new("/", files)); -/// // or subpath with additional option to not resolve index.html -/// let files: HashMap<&'static str, pagetop_statics::StaticResource> = HashMap::new(); -/// let app = App::new() -/// .service(pagetop_statics::ResourceFiles::new("/imgs", files) -/// .do_not_resolve_defaults()); -/// } -/// ``` -#[allow(clippy::needless_doctest_main)] -pub struct ResourceFiles { - not_resolve_defaults: bool, - use_guard: bool, - not_found_resolves_to: Option, - inner: Rc, -} - -pub struct ResourceFilesInner { - path: String, - files: HashMap<&'static str, Resource>, -} - -const INDEX_HTML: &str = "index.html"; - -impl ResourceFiles { - pub fn new(path: &str, files: HashMap<&'static str, Resource>) -> Self { - let inner = ResourceFilesInner { - path: path.into(), - files, - }; - Self { - inner: Rc::new(inner), - not_resolve_defaults: false, - not_found_resolves_to: None, - use_guard: false, - } - } - - /// By default trying to resolve '.../' to '.../index.html' if it exists. - /// Turn off this resolution by calling this function. - pub fn do_not_resolve_defaults(mut self) -> Self { - self.not_resolve_defaults = true; - self - } - - /// Resolves not found references to this path. - /// - /// This can be useful for angular-like applications. - pub fn resolve_not_found_to(mut self, path: S) -> Self { - self.not_found_resolves_to = Some(path.to_string()); - self - } - - /// Resolves not found references to root path. - /// - /// This can be useful for angular-like applications. - pub fn resolve_not_found_to_root(self) -> Self { - self.resolve_not_found_to(INDEX_HTML) - } - - /// If this is called, we will use an [actix_web::guard::Guard] to check if this request should be handled. - /// If set to true, we skip using the handler for files that haven't been found, instead of sending 404s. - /// Would be ignored, if `resolve_not_found_to` or `resolve_not_found_to_root` is used. - /// - /// Can be useful if you want to share files on a (sub)path that's also used by a different route handler. - pub fn skip_handler_when_not_found(mut self) -> Self { - self.use_guard = true; - self - } - - fn select_guard(&self) -> Box { - if self.not_resolve_defaults { - Box::new(NotResolveDefaultsGuard::from(self)) - } else { - Box::new(ResolveDefaultsGuard::from(self)) - } - } -} - -impl Deref for ResourceFiles { - type Target = ResourceFilesInner; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -struct NotResolveDefaultsGuard { - inner: Rc, -} - -impl Guard for NotResolveDefaultsGuard { - fn check(&self, ctx: &GuardContext<'_>) -> bool { - self.inner - .files - .contains_key(ctx.head().uri.path().trim_start_matches('/')) - } -} - -impl From<&ResourceFiles> for NotResolveDefaultsGuard { - fn from(files: &ResourceFiles) -> Self { - Self { - inner: files.inner.clone(), - } - } -} - -struct ResolveDefaultsGuard { - inner: Rc, -} - -impl Guard for ResolveDefaultsGuard { - fn check(&self, ctx: &GuardContext<'_>) -> bool { - let path = ctx.head().uri.path().trim_start_matches('/'); - self.inner.files.contains_key(path) - || ((path.is_empty() || path.ends_with('/')) - && self - .inner - .files - .contains_key((path.to_string() + INDEX_HTML).as_str())) - } -} - -impl From<&ResourceFiles> for ResolveDefaultsGuard { - fn from(files: &ResourceFiles) -> Self { - Self { - inner: files.inner.clone(), - } - } -} - -impl HttpServiceFactory for ResourceFiles { - fn register(self, config: &mut AppService) { - let prefix = self.path.trim_start_matches('/'); - let rdef = if config.is_root() { - ResourceDef::root_prefix(prefix) - } else { - ResourceDef::prefix(prefix) - }; - let guards = if self.use_guard && self.not_found_resolves_to.is_none() { - Some(vec![self.select_guard()]) - } else { - None - }; - config.register_service(rdef, guards, self, None); - } -} - -impl ServiceFactory for ResourceFiles { - type Config = (); - type Response = ServiceResponse; - type Error = Error; - type Service = ResourceFilesService; - type InitError = (); - type Future = LocalBoxFuture<'static, Result>; - - fn new_service(&self, _: ()) -> Self::Future { - ok(ResourceFilesService { - resolve_defaults: !self.not_resolve_defaults, - not_found_resolves_to: self.not_found_resolves_to.clone(), - inner: self.inner.clone(), - }) - .boxed_local() - } -} - -#[derive(Deref)] -pub struct ResourceFilesService { - resolve_defaults: bool, - not_found_resolves_to: Option, - #[deref] - inner: Rc, -} - -impl Service for ResourceFilesService { - type Response = ServiceResponse; - type Error = Error; - type Future = Ready>; - - always_ready!(); - - fn call(&self, req: ServiceRequest) -> Self::Future { - match *req.method() { - Method::HEAD | Method::GET => (), - _ => { - return ok(ServiceResponse::new( - req.into_parts().0, - HttpResponse::MethodNotAllowed() - .insert_header(ContentType::plaintext()) - .insert_header((header::ALLOW, "GET, HEAD")) - .body("This resource only supports GET and HEAD."), - )); - } - } - - let req_path = req.match_info().unprocessed(); - let mut item = self.files.get(req_path); - - if item.is_none() - && self.resolve_defaults - && (req_path.is_empty() || req_path.ends_with('/')) - { - let index_req_path = req_path.to_string() + INDEX_HTML; - item = self.files.get(index_req_path.trim_start_matches('/')); - } - - let (req, response) = if item.is_some() { - let (req, _) = req.into_parts(); - let response = respond_to(&req, item); - (req, response) - } else { - let real_path = match get_pathbuf(req_path) { - Ok(item) => item, - Err(e) => return ok(req.error_response(e)), - }; - - let (req, _) = req.into_parts(); - - let mut item = self.files.get(real_path.as_str()); - - if item.is_none() && self.not_found_resolves_to.is_some() { - let not_found_path = self.not_found_resolves_to.as_ref().unwrap(); - item = self.files.get(not_found_path.as_str()); - } - - let response = respond_to(&req, item); - (req, response) - }; - - ok(ServiceResponse::new(req, response)) - } -} - -fn respond_to(req: &HttpRequest, item: Option<&Resource>) -> HttpResponse { - if let Some(file) = item { - let etag = Some(header::EntityTag::new_strong(format!( - "{:x}:{:x}", - file.data.len(), - file.modified - ))); - - let precondition_failed = !any_match(etag.as_ref(), req); - - let not_modified = !none_match(etag.as_ref(), req); - - let mut resp = HttpResponse::build(StatusCode::OK); - resp.insert_header((header::CONTENT_TYPE, file.mime_type)); - - if let Some(etag) = etag { - resp.insert_header(header::ETag(etag)); - } - - if precondition_failed { - return resp.status(StatusCode::PRECONDITION_FAILED).finish(); - } else if not_modified { - return resp.status(StatusCode::NOT_MODIFIED).finish(); - } - - resp.body(file.data) - } else { - HttpResponse::NotFound().body("Not found") - } -} - -/// Returns true if `req` has no `If-Match` header or one which matches `etag`. -fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { - match req.get_header::() { - None | Some(header::IfMatch::Any) => true, - Some(header::IfMatch::Items(ref items)) => { - if let Some(some_etag) = etag { - for item in items { - if item.strong_eq(some_etag) { - return true; - } - } - } - false - } - } -} - -/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`. -fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { - match req.get_header::() { - Some(header::IfNoneMatch::Any) => false, - Some(header::IfNoneMatch::Items(ref items)) => { - if let Some(some_etag) = etag { - for item in items { - if item.weak_eq(some_etag) { - return false; - } - } - } - true - } - None => true, - } -} - -/// Error type representing invalid characters in a URI path segment. -/// -/// This enum is used to report specific formatting errors in individual segments of a URI path, -/// such as starting, ending, or containing disallowed characters. Each variant wraps the offending -/// character that caused the error. -#[derive(Debug, PartialEq, Display, Error)] -pub enum UriSegmentError { - /// The segment started with the wrapped invalid character. - #[display(fmt = "The segment started with the wrapped invalid character")] - BadStart(#[error(not(source))] char), - - /// The segment contained the wrapped invalid character. - #[display(fmt = "The segment contained the wrapped invalid character")] - BadChar(#[error(not(source))] char), - - /// The segment ended with the wrapped invalid character. - #[display(fmt = "The segment ended with the wrapped invalid character")] - BadEnd(#[error(not(source))] char), -} - -#[cfg(test)] -mod tests_error_impl { - use super::*; - - fn assert_send_and_sync() {} - - #[test] - fn test_error_impl() { - // ensure backwards compatibility when migrating away from failure - assert_send_and_sync::(); - } -} - -/// Return `BadRequest` for `UriSegmentError` -impl ResponseError for UriSegmentError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -fn get_pathbuf(path: &str) -> Result { - let mut buf = Vec::new(); - for segment in path.split('/') { - if segment == ".." { - buf.pop(); - } else if segment.starts_with('.') { - return Err(UriSegmentError::BadStart('.')); - } else if segment.starts_with('*') { - return Err(UriSegmentError::BadStart('*')); - } else if segment.ends_with(':') { - return Err(UriSegmentError::BadEnd(':')); - } else if segment.ends_with('>') { - return Err(UriSegmentError::BadEnd('>')); - } else if segment.ends_with('<') { - return Err(UriSegmentError::BadEnd('<')); - } else if segment.is_empty() { - continue; - } else if cfg!(windows) && segment.contains('\\') { - return Err(UriSegmentError::BadChar('\\')); - } else { - buf.push(segment) - } - } - - Ok(buf.join("/")) -}