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.
Diff
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(-)
@@ -82,6 +82,9 @@ OPTIONS:
--http2-tls-key <http2-tls-key>
Specify the file path to read the private key [env: SERVER_HTTP2_TLS_KEY=]
--ignore-hidden-files <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 <log-level>
Specify a logging level in lower case. Values: error, warn, info, debug or trace [env: SERVER_LOG_LEVEL=]
[default: error]
@@ -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
@@ -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.
@@ -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.
@@ -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
```
@@ -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'
@@ -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<Output = Result<Response<Body>, StatusCode>> + Send + 'a {
@@ -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<Response<Body>> {
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();
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() {
@@ -1,3 +1,4 @@
pub mod http;
pub mod path;
@@ -0,0 +1,20 @@
use std::path::{Component, Path};
pub trait PathExt {
fn is_hidden(&self) -> bool;
}
impl PathExt for Path {
fn is_hidden(&self) -> bool {
self.components()
.filter_map(|cmp| match cmp {
Component::Normal(s) => s.to_str(),
_ => None,
})
.any(|s| s.starts_with('.'))
}
}
@@ -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,
pub advanced_opts: Option<Advanced>,
@@ -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<http::HeaderMap> = None;
@@ -198,6 +200,7 @@ impl RequestHandler {
dir_listing_format,
redirect_trailing_slash,
compression_static,
ignore_hidden_files,
})
.await
{
@@ -183,6 +183,10 @@ impl Server {
redirect_trailing_slash
);
let ignore_hidden_files = general.ignore_hidden_files;
tracing::info!("ignore hidden files: enabled={}", ignore_hidden_files);
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,
}),
});
@@ -219,6 +219,15 @@ pub struct General {
pub redirect_trailing_slash: bool,
#[structopt(
long,
parse(try_from_str),
default_value = "false",
env = "SERVER_IGNORE_HIDDEN_FILES"
)]
pub ignore_hidden_files: bool,
@@ -136,6 +136,8 @@ pub struct General {
pub redirect_trailing_slash: Option<bool>,
pub ignore_hidden_files: Option<bool>,
#[cfg(windows)]
pub windows_service: Option<bool>,
}
@@ -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;
#[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
}
#[cfg(windows)]
@@ -320,6 +324,7 @@ impl Settings {
page_fallback,
log_remote_address,
redirect_trailing_slash,
ignore_hidden_files,
#[cfg(windows)]
@@ -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};
@@ -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,
}
@@ -58,6 +60,11 @@ pub async fn handle<'a>(opts: &HandleOpts<'a>) -> Result<(Response<Body>, bool),
let (file_path, meta, is_dir, precompressed_variant) =
composed_file_metadata(&mut file_path, headers_opt, compression_static_opt).await?;
if opts.ignore_hidden_files && file_path.is_hidden() {
return Err(StatusCode::NOT_FOUND);
}
let is_precompressed = precompressed_variant.is_some();
@@ -105,6 +112,7 @@ pub async fn handle<'a>(opts: &HandleOpts<'a>) -> Result<(Response<Body>, bool),
file_path.as_ref(),
opts.dir_listing_order,
opts.dir_listing_format,
opts.ignore_hidden_files,
)
.await?;
@@ -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");
@@ -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);
}
}
}
}
}
@@ -0,0 +1 @@
dotfile!
@@ -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);
}
}
}
}
}