From 800416d91f0eddb3eecb365cb3ed291ed152258b Mon Sep 17 00:00:00 2001 From: Jose Quintana <1700322+joseluisq@users.noreply.github.com> Date: Sat, 19 Nov 2022 02:24:06 +0100 Subject: [PATCH] feat: ignore hidden files/directories via `--ignore-hidden-files` (#162) it adds the ability to ignore hidden files/directories (dotfiles), preventing them to be served and being included in auto HTML index pages (directory listing) via the new boolean `--ignore-hidden-files` option. --- docs/content/configuration/command-line-arguments.md | 3 +++ docs/content/configuration/config-file.md | 3 +++ docs/content/configuration/environment-variables.md | 3 +++ docs/content/features/directory-listing.md | 1 - docs/content/features/ignore-files.md | 19 +++++++++++++++++++ docs/mkdocs.yml | 1 + src/directory_listing.rs | 10 +++++++++- src/exts/mod.rs | 1 + src/exts/path.rs | 20 ++++++++++++++++++++ src/handler.rs | 3 +++ src/server.rs | 5 +++++ src/settings/cli.rs | 9 +++++++++ src/settings/file.rs | 2 ++ src/settings/mod.rs | 5 +++++ src/static_files.rs | 8 ++++++++ tests/compression_static.rs | 2 ++ tests/dir_listing.rs | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ tests/fixtures/public/.dotfile | 1 + tests/static_files.rs | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 19 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 docs/content/features/ignore-files.md create mode 100644 src/exts/path.rs create mode 100644 tests/fixtures/public/.dotfile diff --git a/docs/content/configuration/command-line-arguments.md b/docs/content/configuration/command-line-arguments.md index ede2217..8aaeca5 100644 --- a/docs/content/configuration/command-line-arguments.md +++ b/docs/content/configuration/command-line-arguments.md @@ -82,6 +82,9 @@ OPTIONS: --http2-tls-key Specify the file path to read the private key [env: SERVER_HTTP2_TLS_KEY=] + --ignore-hidden-files + Ignore hidden files/directories (dotfiles), preventing them to be served and being included in auto HTML + index pages (directory listing) [env: SERVER_IGNORE_HIDDEN_FILES=] [default: false] -g, --log-level Specify a logging level in lower case. Values: error, warn, info, debug or trace [env: SERVER_LOG_LEVEL=] [default: error] diff --git a/docs/content/configuration/config-file.md b/docs/content/configuration/config-file.md index cd819ab..c1819e9 100644 --- a/docs/content/configuration/config-file.md +++ b/docs/content/configuration/config-file.md @@ -74,6 +74,9 @@ redirect-trailing-slash = true #### Check for existing pre-compressed files compression-static = false +#### Ignore hidden files/directories (dotfiles) +ignore-hidden-files = false + ### Windows Only diff --git a/docs/content/configuration/environment-variables.md b/docs/content/configuration/environment-variables.md index 9fd09ec..0263627 100644 --- a/docs/content/configuration/environment-variables.md +++ b/docs/content/configuration/environment-variables.md @@ -87,6 +87,9 @@ It provides [The "Basic" HTTP Authentication Scheme](https://datatracker.ietf.or ### SERVER_REDIRECT_TRAILING_SLASH Check for a trailing slash in the requested directory URI and redirect permanent (308) to the same path with a trailing slash suffix if it is missing. Default `true` (enabled). +### SERVER_IGNORE_HIDDEN_FILES +Ignore hidden files/directories (dotfiles), preventing them to be served and being included in auto HTML index pages (directory listing). + ## Windows The following options and commands are Windows platform-specific. diff --git a/docs/content/features/directory-listing.md b/docs/content/features/directory-listing.md index ffe116d..fc630ae 100644 --- a/docs/content/features/directory-listing.md +++ b/docs/content/features/directory-listing.md @@ -23,7 +23,6 @@ However, when the *"redirect trailing slash"* feature is disabled and a director Note also that in both cases, SWS will append a trailing slash to the entry if is a directory. - ## Sorting Sorting by `Name`, `Last modified` and `Size` is enabled as clickable columns when the directory listing is activated via the `--directory-listing=true` option. diff --git a/docs/content/features/ignore-files.md b/docs/content/features/ignore-files.md new file mode 100644 index 0000000..d86067f --- /dev/null +++ b/docs/content/features/ignore-files.md @@ -0,0 +1,19 @@ +# Ignore files + +SWS provides some options to ignore files or directories from being served and displayed if the directory listing is enabled. + +## Ignore hidden files (dotfiles) + +SWS doesn't ignore dotfiles (hidden files) by default. +However, it's possible to ignore those files as shown below. As a result, SWS will respond with a `404 Not Found` status. + +This feature is disabled by default and can be controlled by the boolean `--ignore-hidden-files` option or the equivalent [SERVER_IGNORE_HIDDEN_FILES](./../configuration/environment-variables.md#server_ignore_hidden_files) env. + +Here is an example of how to ignore hidden files: + +```sh +static-web-server \ + -p=8787 -d=tests/fixtures/public -g=trace \ + --directory-listing=true \ + --ignore-hidden-files true +``` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 3b8a3ec..d7ef721 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -144,6 +144,7 @@ nav: - 'URL Redirects': 'features/url-redirects.md' - 'Windows Service': 'features/windows-service.md' - 'Trailing Slash Redirect': 'features/trailing-slash-redirect.md' + - 'Ignore Files': 'features/ignore-files.md' - 'Platforms & Architectures': 'platforms-architectures.md' - 'Migration from v1 to v2': 'migration.md' - 'Changelog v2 (latest stable)': 'https://github.com/static-web-server/static-web-server/blob/master/CHANGELOG.md' diff --git a/src/directory_listing.rs b/src/directory_listing.rs index 052b576..1193cad 100644 --- a/src/directory_listing.rs +++ b/src/directory_listing.rs @@ -35,6 +35,7 @@ pub fn auto_index<'a>( filepath: &'a Path, dir_listing_order: u8, dir_listing_format: &'a DirListFmt, + ignore_hidden_files: bool, ) -> impl Future, StatusCode>> + Send + 'a { // Note: it's safe to call `parent()` here since `filepath` // value always refer to a path with file ending and under @@ -53,6 +54,7 @@ pub fn auto_index<'a>( is_head, dir_listing_order, dir_listing_format, + ignore_hidden_files, ) .await { @@ -122,6 +124,7 @@ async fn read_dir_entries( is_head: bool, mut order_code: u8, content_format: &DirListFmt, + ignore_hidden_files: bool, ) -> Result> { let mut dirs_count: usize = 0; let mut files_count: usize = 0; @@ -134,8 +137,13 @@ async fn read_dir_entries( .file_name() .into_string() .map_err(|err| anyhow::anyhow!(err.into_string().unwrap_or_default()))?; - let mut name_encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string(); + // Check and ignore the current hidden file/directory (dotfile) if feature enabled + if ignore_hidden_files && name.starts_with('.') { + continue; + } + + let mut name_encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string(); let mut filesize = 0_u64; if meta.is_dir() { diff --git a/src/exts/mod.rs b/src/exts/mod.rs index 5f2e89e..9831796 100644 --- a/src/exts/mod.rs +++ b/src/exts/mod.rs @@ -1,3 +1,4 @@ //! Some extension traits for various use cases. pub mod http; +pub mod path; diff --git a/src/exts/path.rs b/src/exts/path.rs new file mode 100644 index 0000000..30628c9 --- /dev/null +++ b/src/exts/path.rs @@ -0,0 +1,20 @@ +//! Path-related extension traits. + +use std::path::{Component, Path}; + +/// SWS Path extensions trait. +pub trait PathExt { + fn is_hidden(&self) -> bool; +} + +impl PathExt for Path { + /// Checks if the current path is hidden (dot file). + fn is_hidden(&self) -> bool { + self.components() + .filter_map(|cmp| match cmp { + Component::Normal(s) => s.to_str(), + _ => None, + }) + .any(|s| s.starts_with('.')) + } +} diff --git a/src/handler.rs b/src/handler.rs index 7af5324..b7410b4 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -31,6 +31,7 @@ pub struct RequestHandlerOpts { pub basic_auth: String, pub log_remote_address: bool, pub redirect_trailing_slash: bool, + pub ignore_hidden_files: bool, // Advanced options pub advanced_opts: Option, @@ -61,6 +62,7 @@ impl RequestHandler { let log_remote_addr = self.opts.log_remote_address; let redirect_trailing_slash = self.opts.redirect_trailing_slash; let compression_static = self.opts.compression_static; + let ignore_hidden_files = self.opts.ignore_hidden_files; let mut cors_headers: Option = None; @@ -198,6 +200,7 @@ impl RequestHandler { dir_listing_format, redirect_trailing_slash, compression_static, + ignore_hidden_files, }) .await { diff --git a/src/server.rs b/src/server.rs index 2d931be..0ff4d52 100644 --- a/src/server.rs +++ b/src/server.rs @@ -183,6 +183,10 @@ impl Server { redirect_trailing_slash ); + // Ignore hidden files option + let ignore_hidden_files = general.ignore_hidden_files; + tracing::info!("ignore hidden files: enabled={}", ignore_hidden_files); + // Grace period option let grace_period = general.grace_period; tracing::info!("grace period before graceful shutdown: {}s", grace_period); @@ -205,6 +209,7 @@ impl Server { basic_auth, log_remote_address, redirect_trailing_slash, + ignore_hidden_files, advanced_opts, }), }); diff --git a/src/settings/cli.rs b/src/settings/cli.rs index 54802eb..8746958 100644 --- a/src/settings/cli.rs +++ b/src/settings/cli.rs @@ -219,6 +219,15 @@ pub struct General { /// Check for a trailing slash in the requested directory URI and redirect permanently (308) to the same path with a trailing slash suffix if it is missing. pub redirect_trailing_slash: bool, + #[structopt( + long, + parse(try_from_str), + default_value = "false", + env = "SERVER_IGNORE_HIDDEN_FILES" + )] + /// Ignore hidden files/directories (dotfiles), preventing them to be served and being included in auto HTML index pages (directory listing). + pub ignore_hidden_files: bool, + // // Windows specific arguments and commands // diff --git a/src/settings/file.rs b/src/settings/file.rs index e2ecc60..682573a 100644 --- a/src/settings/file.rs +++ b/src/settings/file.rs @@ -136,6 +136,8 @@ pub struct General { pub redirect_trailing_slash: Option, + pub ignore_hidden_files: Option, + #[cfg(windows)] pub windows_service: Option, } diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 39ae749..9dbf386 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -87,6 +87,7 @@ impl Settings { let mut page_fallback = opts.page_fallback; let mut log_remote_address = opts.log_remote_address; let mut redirect_trailing_slash = opts.redirect_trailing_slash; + let mut ignore_hidden_files = opts.ignore_hidden_files; // Windows-only options #[cfg(windows)] @@ -190,6 +191,9 @@ impl Settings { if let Some(v) = general.redirect_trailing_slash { redirect_trailing_slash = v } + if let Some(v) = general.ignore_hidden_files { + ignore_hidden_files = v + } // Windows-only options #[cfg(windows)] @@ -320,6 +324,7 @@ impl Settings { page_fallback, log_remote_address, redirect_trailing_slash, + ignore_hidden_files, // Windows-only options and commands #[cfg(windows)] diff --git a/src/static_files.rs b/src/static_files.rs index dcb1db7..ba217f7 100644 --- a/src/static_files.rs +++ b/src/static_files.rs @@ -23,6 +23,7 @@ use std::task::{Context, Poll}; use crate::directory_listing::DirListFmt; use crate::exts::http::{MethodExt, HTTP_SUPPORTED_METHODS}; +use crate::exts::path::PathExt; use crate::{compression_static, directory_listing, Result}; /// Defines all options needed by the static-files handler. @@ -37,6 +38,7 @@ pub struct HandleOpts<'a> { pub dir_listing_format: &'a DirListFmt, pub redirect_trailing_slash: bool, pub compression_static: bool, + pub ignore_hidden_files: bool, } /// Entry point to handle incoming requests which map to specific files @@ -58,6 +60,11 @@ pub async fn handle<'a>(opts: &HandleOpts<'a>) -> Result<(Response, bool), let (file_path, meta, is_dir, precompressed_variant) = composed_file_metadata(&mut file_path, headers_opt, compression_static_opt).await?; + // Check for a hidden file/directory (dotfile) and ignore it if feature enabled + if opts.ignore_hidden_files && file_path.is_hidden() { + return Err(StatusCode::NOT_FOUND); + } + // `is_precompressed` relates to `opts.compression_static` value let is_precompressed = precompressed_variant.is_some(); @@ -105,6 +112,7 @@ pub async fn handle<'a>(opts: &HandleOpts<'a>) -> Result<(Response, bool), file_path.as_ref(), opts.dir_listing_order, opts.dir_listing_format, + opts.ignore_hidden_files, ) .await?; diff --git a/tests/compression_static.rs b/tests/compression_static.rs index c697c6f..c15de60 100644 --- a/tests/compression_static.rs +++ b/tests/compression_static.rs @@ -43,6 +43,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: true, + ignore_hidden_files: false, }) .await .expect("unexpected error response on `handle` function"); @@ -96,6 +97,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: true, + ignore_hidden_files: false, }) .await .expect("unexpected error response on `handle` function"); diff --git a/tests/dir_listing.rs b/tests/dir_listing.rs index ed3c94e..01ad20e 100644 --- a/tests/dir_listing.rs +++ b/tests/dir_listing.rs @@ -47,6 +47,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -76,6 +77,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -115,6 +117,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: false, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -154,6 +157,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: false, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -183,6 +187,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -233,6 +238,7 @@ mod tests { dir_listing_format: &DirListFmt::Json, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: true, }) .await { @@ -301,6 +307,7 @@ mod tests { dir_listing_format: &DirListFmt::Json, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -327,4 +334,45 @@ mod tests { } } } + + #[tokio::test] + async fn dir_listing_ignore_hidden_files() { + for method in METHODS { + match static_files::handle(&HandleOpts { + method: &method, + headers: &HeaderMap::new(), + base_path: &root_dir("tests/fixtures/public"), + uri_path: "/", + uri_query: None, + dir_listing: true, + dir_listing_order: 1, + dir_listing_format: &DirListFmt::Html, + redirect_trailing_slash: true, + compression_static: false, + ignore_hidden_files: true, + }) + .await + { + Ok((mut res, _)) => { + assert_eq!(res.status(), 200); + assert_eq!(res.headers()["content-type"], "text/html; charset=utf-8"); + + let body = hyper::body::to_bytes(res.body_mut()) + .await + .expect("unexpected bytes error during `body` conversion"); + let body_str = std::str::from_utf8(&body).unwrap(); + + if method == Method::GET { + assert!(!body_str.contains(".dotfile")) + } else { + assert!(body_str.is_empty()); + } + } + Err(status) => { + assert!(method != Method::GET && method != Method::HEAD); + assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED); + } + } + } + } } diff --git a/tests/fixtures/public/.dotfile b/tests/fixtures/public/.dotfile new file mode 100644 index 0000000..5cb3ae4 --- /dev/null +++ b/tests/fixtures/public/.dotfile @@ -0,0 +1 @@ +dotfile! diff --git a/tests/static_files.rs b/tests/static_files.rs index 81c623f..dffe3a3 100644 --- a/tests/static_files.rs +++ b/tests/static_files.rs @@ -34,6 +34,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await .expect("unexpected error response on `handle` function"); @@ -75,6 +76,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await .expect("unexpected error response on `handle` function"); @@ -117,6 +119,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -143,6 +146,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await .expect("unexpected error response on `handle` function"); @@ -170,6 +174,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -196,6 +201,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: false, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -227,6 +233,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -273,6 +280,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -301,6 +309,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -332,6 +341,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -363,6 +373,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -397,6 +408,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -429,6 +441,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -459,6 +472,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -488,6 +502,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -531,6 +546,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -591,6 +607,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -654,6 +671,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -697,6 +715,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -740,6 +759,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -784,6 +804,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -820,6 +841,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -866,6 +888,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -909,6 +932,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -955,6 +979,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -999,6 +1024,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -1038,6 +1064,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -1088,6 +1115,7 @@ mod tests { dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, + ignore_hidden_files: false, }) .await { @@ -1112,4 +1140,35 @@ mod tests { } } } + + #[tokio::test] + async fn handle_ignore_hidden_files() { + let root_dir = PathBuf::from("tests/fixtures/public/"); + let headers = HeaderMap::new(); + + for method in [Method::HEAD, Method::GET] { + match static_files::handle(&HandleOpts { + method: &method, + headers: &headers, + base_path: &root_dir, + uri_path: ".dotfile", + uri_query: None, + dir_listing: false, + dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, + redirect_trailing_slash: true, + compression_static: true, + ignore_hidden_files: true, + }) + .await + { + Ok(_) => { + panic!("expected a status error 404 but not status 200") + } + Err(status) => { + assert_eq!(status, StatusCode::NOT_FOUND); + } + } + } + } } -- libgit2 1.7.2