index : static-web-server.git

ascending towards madness

author Jose Quintana <1700322+joseluisq@users.noreply.github.com> 2023-10-09 1:25:27.0 +00:00:00
committer GitHub <noreply@github.com> 2023-10-09 1:25:27.0 +00:00:00
commit
efb2c0cd8a14391c615cf1e6314fc82f3d8b0b39 [patch]
tree
5203dba377bccf474d048abeaea52463f39c58ae
parent
e3cd81094f400320198634cfa4bb1c189d6e873f
download
efb2c0cd8a14391c615cf1e6314fc82f3d8b0b39.tar.gz

Multiple index files support (#267)

* feat: multiple index files support

option: --index-files="a.html, b.htm, etc"
env: SERVER_INDEX_FILES
default value: "index.html"

Diff

 README.md                                            |  1 +-
 docs/content/configuration/command-line-arguments.md |  2 +-
 docs/content/configuration/config-file.md            |  3 +-
 docs/content/configuration/environment-variables.md  |  3 +-
 docs/content/features/multiple-index-files.md        | 17 ++++-
 docs/content/index.md                                |  1 +-
 docs/mkdocs.yml                                      |  1 +-
 src/handler.rs                                       |  5 +-
 src/server.rs                                        | 13 +++-
 src/settings/cli.rs                                  |  5 +-
 src/settings/file.rs                                 |  3 +-
 src/settings/mod.rs                                  |  5 +-
 src/static_files.rs                                  | 89 +++++++++++++--------
 tests/compression_static.rs                          |  3 +-
 tests/dir_listing.rs                                 | 14 ++-
 tests/fixtures/public/index.htm                      |  1 +-
 tests/static_files.rs                                | 73 +++++++++++++++++-
 tests/toml/config.toml                               |  3 +-
 18 files changed, 207 insertions(+), 35 deletions(-)

diff --git a/README.md b/README.md
index fb06318..b3144b4 100644
--- a/README.md
+++ b/README.md
@@ -65,6 +65,7 @@ Cross-platform and available for `Linux`, `macOS`, `Windows`, `FreeBSD`, `NetBSD
- Support for serving pre-compressed (Gzip/Brotli/Zstd) files directly from disk.
- Custom URL rewrites and redirects via glob patterns with replacements.
- Virtual hosting support.
- Multiple index files.
- Available as a library crate with opt-in features.
- First-class [Docker]https://docs.docker.com/get-started/overview/ support. [Scratch]https://hub.docker.com/_/scratch, latest [Alpine Linux]https://hub.docker.com/_/alpine and [Debian]https://hub.docker.com/_/alpine Docker images.
- Ability to accept a socket listener as a file descriptor for sandboxing and on-demand applications (e.g. [systemd]http://0pointer.de/blog/projects/socket-activation.html).
diff --git a/docs/content/configuration/command-line-arguments.md b/docs/content/configuration/command-line-arguments.md
index cea36a4..3d740c6 100644
--- a/docs/content/configuration/command-line-arguments.md
+++ b/docs/content/configuration/command-line-arguments.md
@@ -55,6 +55,8 @@ Options:
          HTTP host port where the redirect server will listen for requests to redirect them to HTTPS. It depends on "https_redirect" to be enabled [env: SERVER_HTTPS_REDIRECT_FROM_PORT=] [default: 80]
      --https-redirect-from-hosts <HTTPS_REDIRECT_FROM_HOSTS>
          List of host names or IPs allowed to redirect from. HTTP requests must contain the HTTP 'Host' header and match against this list. It depends on "https_redirect" to be enabled [env: SERVER_HTTPS_REDIRECT_FROM_HOSTS=] [default: localhost]
      --index-files <INDEX_FILES>
          List of files that will be used as an index for requests ending with the slash character (‘/’). Files are checked in the specified order [env: SERVER_INDEX_FILES=] [default: index.html]
  -x, --compression[=<COMPRESSION>]
          Gzip, Deflate, Brotli or Zstd compression on demand determined by the Accept-Encoding header and applied to text-based web file types only [env: SERVER_COMPRESSION=] [default: true] [possible values: true, false]
      --compression-static[=<COMPRESSION_STATIC>]
diff --git a/docs/content/configuration/config-file.md b/docs/content/configuration/config-file.md
index 0e8362e..b5c3a43 100644
--- a/docs/content/configuration/config-file.md
+++ b/docs/content/configuration/config-file.md
@@ -78,6 +78,9 @@ compression-static = true
#### Health-check endpoint (GET or HEAD `/health`)
health = false

#### List of index files
# index-files = "index.html, index.htm"

### Windows Only

#### Run the web server as a Windows Service
diff --git a/docs/content/configuration/environment-variables.md b/docs/content/configuration/environment-variables.md
index 0eb3b47..4a804e1 100644
--- a/docs/content/configuration/environment-variables.md
+++ b/docs/content/configuration/environment-variables.md
@@ -108,6 +108,9 @@ Ignore hidden files/directories (dotfiles), preventing them to be served and bei
### SERVER_HEALTH
Activate the health endpoint.

### SERVER_INDEX_FILES
List of files that will be used as an index for requests ending with the slash character (‘/’). Files are checked in the specified order. Default `index.html`.

## Windows
The following options and commands are Windows platform-specific.

diff --git a/docs/content/features/multiple-index-files.md b/docs/content/features/multiple-index-files.md
new file mode 100644
index 0000000..65cf85c
--- /dev/null
+++ b/docs/content/features/multiple-index-files.md
@@ -0,0 +1,17 @@
# Multiple index files

**`SWS`** allows to provide a list of files that will be used as an index for requests ending with the slash character (‘/’).

!!! info "Notes"
    - Files are checked in the specified order from left to right.
    - The option value can be a single index or comma-separated when multiple values.
    - The default value is `index.html`.

This feature is disabled by default and can be controlled by the string list `--index-files` option or the equivalent [SERVER_INDEX_FILES]./../configuration/environment-variables.md#server_index_files env.

Here is an example:

```sh
static-web-server -p 8787 -d ./public \
    --index-files="index.html, index.htm, default.html"
```
diff --git a/docs/content/index.md b/docs/content/index.md
index 7f6d4b5..47e1d63 100644
--- a/docs/content/index.md
+++ b/docs/content/index.md
@@ -69,6 +69,7 @@ Cross-platform and available for `Linux`, `macOS`, `Windows`, `FreeBSD`, `NetBSD
- Support for serving pre-compressed (Gzip/Brotli/Zstd) files directly from disk.
- Custom URL rewrites and redirects via glob patterns with replacements.
- Virtual hosting support.
- Multiple index files.
- Available as a library crate with opt-in features.
- First-class [Docker]https://docs.docker.com/get-started/overview/ support. [Scratch]https://hub.docker.com/_/scratch, latest [Alpine Linux]https://hub.docker.com/_/alpine and [Debian]https://hub.docker.com/_/alpine Docker images.
- Ability to accept a socket listener as a file descriptor for sandboxing and on-demand applications (e.g. [systemd]http://0pointer.de/blog/projects/socket-activation.html).
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 455f0bb..cbc05a1 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -163,6 +163,7 @@ nav:
    - 'Ignore Files': 'features/ignore-files.md'
    - 'Health endpoint': 'features/health-endpoint.md'
    - 'Virtual Hosting': 'features/virtual-hosting.md'
    - 'Multiple Index Files': 'features/multiple-index-files.md'
    - 'WebAssembly': 'features/webassembly.md'
  - 'Platforms & Architectures': 'platforms-architectures.md'
  - 'Migrating from v1 to v2': 'migration.md'
diff --git a/src/handler.rs b/src/handler.rs
index b07ed4e..fb548c5 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -70,6 +70,8 @@ pub struct RequestHandlerOpts {
    #[cfg(feature = "basic-auth")]
    #[cfg_attr(docsrs, doc(cfg(feature = "basic-auth")))]
    pub basic_auth: String,
    /// Index files feature.
    pub index_files: Vec<String>,
    /// Log remote address feature.
    pub log_remote_address: bool,
    /// Redirect trailing slash feature.
@@ -114,6 +116,7 @@ impl RequestHandler {
        let compression_static = self.opts.compression_static;
        let ignore_hidden_files = self.opts.ignore_hidden_files;
        let health = self.opts.health;
        let index_files: Vec<&str> = self.opts.index_files.iter().map(|s| s.as_str()).collect();

        let mut cors_headers: Option<http::HeaderMap> = None;

@@ -359,6 +362,7 @@ impl RequestHandler {
            }

            let uri_path = &uri_path;
            let index_files = index_files.as_ref();

            // Static files
            match static_files::handle(&HandleOpts {
@@ -376,6 +380,7 @@ impl RequestHandler {
                redirect_trailing_slash,
                compression_static,
                ignore_hidden_files,
                index_files,
            })
            .await
            {
diff --git a/src/server.rs b/src/server.rs
index f4632f3..4cae50f 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -252,6 +252,17 @@ impl Server {
        let grace_period = general.grace_period;
        server_info!("grace period before graceful shutdown: {}s", grace_period);

        // Index files option
        let index_files = general
            .index_files
            .split(',')
            .map(|s| s.trim().to_owned())
            .collect::<Vec<_>>();
        if index_files.is_empty() {
            bail!("index files list is empty, provide at least one index file")
        }
        server_info!("index files: {}", general.index_files);

        // Health endpoint option
        let health = general.health;
        server_info!("health endpoint: enabled={}", health);
@@ -280,6 +291,7 @@ impl Server {
                log_remote_address,
                redirect_trailing_slash,
                ignore_hidden_files,
                index_files,
                health,
                advanced_opts,
            }),
@@ -419,6 +431,7 @@ impl Server {
                    bail!("https redirect allowed hosts is empty, provide at least one host or IP")
                }

                // Redirect options
                let redirect_opts = Arc::new(https_redirect::RedirectOpts {
                    https_hostname: general.https_redirect_host,
                    https_port: general.port,
diff --git a/src/settings/cli.rs b/src/settings/cli.rs
index 470f58b..c1320d3 100644
--- a/src/settings/cli.rs
+++ b/src/settings/cli.rs
@@ -226,6 +226,11 @@ pub struct General {
    /// List of host names or IPs allowed to redirect from. HTTP requests must contain the HTTP 'Host' header and match against this list. It depends on "https_redirect" to be enabled.
    pub https_redirect_from_hosts: String,

    #[arg(long, default_value = "index.html", env = "SERVER_INDEX_FILES")]
    /// List of files that will be used as an index for requests ending with the slash character (‘/’).
    /// Files are checked in the specified order.
    pub index_files: String,

    #[cfg(feature = "compression")]
    #[cfg_attr(docsrs, doc(cfg(feature = "compression")))]
    #[arg(
diff --git a/src/settings/file.rs b/src/settings/file.rs
index cb2dd4e..69bc865 100644
--- a/src/settings/file.rs
+++ b/src/settings/file.rs
@@ -187,6 +187,9 @@ pub struct General {
    /// Cors expose headers feature.
    pub cors_expose_headers: Option<String>,

    /// List of files to be used as an index for requests ending with the slash character (‘/’).
    pub index_files: Option<String>,

    /// Directory listing feature.
    #[cfg(feature = "directory-listing")]
    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
diff --git a/src/settings/mod.rs b/src/settings/mod.rs
index 39023b2..4de7f7d 100644
--- a/src/settings/mod.rs
+++ b/src/settings/mod.rs
@@ -145,6 +145,7 @@ impl Settings {
        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;
        let mut index_files = opts.index_files;
        let mut health = opts.health;

        // Windows-only options
@@ -284,6 +285,9 @@ impl Settings {
                if let Some(v) = general.health {
                    health = v
                }
                if let Some(v) = general.index_files {
                    index_files = v
                }

                // Windows-only options
                #[cfg(windows)]
@@ -508,6 +512,7 @@ impl Settings {
                log_remote_address,
                redirect_trailing_slash,
                ignore_hidden_files,
                index_files,
                health,

                // Windows-only options and commands
diff --git a/src/static_files.rs b/src/static_files.rs
index 12ee3f4..1875ee7 100644
--- a/src/static_files.rs
+++ b/src/static_files.rs
@@ -39,6 +39,8 @@ use crate::{
    directory_listing::{DirListFmt, DirListOpts},
};

const DEFAULT_INDEX_FILES: &[&str; 1] = &["index.html"];

/// Defines all options needed by the static-files handler.
pub struct HandleOpts<'a> {
    /// Request method.
@@ -49,6 +51,8 @@ pub struct HandleOpts<'a> {
    pub base_path: &'a PathBuf,
    /// Request base path.
    pub uri_path: &'a str,
    /// Index files.
    pub index_files: &'a [&'a str],
    /// Request URI query.
    pub uri_query: Option<&'a str>,
    /// Directory listing feature.
@@ -91,7 +95,13 @@ pub async fn handle<'a>(opts: &HandleOpts<'a>) -> Result<(Response<Body>, bool),
        metadata,
        is_dir,
        precompressed_variant,
    } = composed_file_metadata(&mut file_path, headers_opt, compression_static_opt).await?;
    } = composed_file_metadata(
        &mut file_path,
        headers_opt,
        compression_static_opt,
        opts.index_files,
    )
    .await?;

    // Check for a hidden file/directory (dotfile) and ignore it if feature enabled
    if opts.ignore_hidden_files && file_path.is_hidden() {
@@ -215,48 +225,63 @@ async fn composed_file_metadata<'a>(
    mut file_path: &'a mut PathBuf,
    _headers: &'a HeaderMap<HeaderValue>,
    _compression_static: bool,
    mut index_files: &'a [&'a str],
) -> Result<FileMetadata<'a>, StatusCode> {
    tracing::trace!("getting metadata for file {}", file_path.display());

    match file_metadata(file_path) {
        Ok((mut metadata, is_dir)) => {
            if is_dir {
                // Append a HTML index page by default if it's a directory path (`autoindex`)
                tracing::debug!("dir: appending an index.html to the directory path");
                file_path.push("index.html");

                // Pre-compressed variant check for the autoindex
                #[cfg(feature = "compression")]
                if _compression_static {
                    if let Some(p) =
                        compression_static::precompressed_variant(file_path, _headers).await
                    {
                        return Ok(FileMetadata {
                            file_path,
                            metadata: p.metadata,
                            is_dir: false,
                            precompressed_variant: Some((p.file_path, p.extension)),
                        });
                    }
                // Try every index file variant in order
                if index_files.is_empty() {
                    index_files = DEFAULT_INDEX_FILES;
                }
                let mut index_found = false;
                for index in index_files {
                    // Append a HTML index page by default if it's a directory path (`autoindex`)
                    tracing::debug!("dir: appending {} to the directory path", index);
                    file_path.push(index);

                    // Pre-compressed variant check for the autoindex
                    #[cfg(feature = "compression")]
                    if _compression_static {
                        if let Some(p) =
                            compression_static::precompressed_variant(file_path, _headers).await
                        {
                            return Ok(FileMetadata {
                                file_path,
                                metadata: p.metadata,
                                is_dir: false,
                                precompressed_variant: Some((p.file_path, p.extension)),
                            });
                        }
                    }

                // Otherwise, just fallback to finding the index.html
                // and overwrite the current `meta`
                // Also noting that it's still a directory request
                if let Ok(meta_res) = file_metadata(file_path) {
                    (metadata, _) = meta_res
                } else {
                    // We remove the appended index.html
                    file_path.pop();
                    let new_meta: Option<Metadata>;
                    (file_path, new_meta) = suffix_file_html_metadata(file_path);
                    if let Some(new_meta) = new_meta {
                        metadata = new_meta;
                    // Otherwise, just fallback to finding the index.html
                    // and overwrite the current `meta`
                    // Also noting that it's still a directory request
                    if let Ok(meta_res) = file_metadata(file_path) {
                        (metadata, _) = meta_res;
                        index_found = true;
                        break;
                    } else {
                        // We append the index.html to preserve previous behavior
                        file_path.push("index.html");
                        // We remove only the appended index file
                        file_path.pop();
                        let new_meta: Option<Metadata>;
                        (file_path, new_meta) = suffix_file_html_metadata(file_path);
                        if let Some(new_meta) = new_meta {
                            metadata = new_meta;
                            index_found = true;
                            break;
                        }
                    }
                }

                // In case no index was found then we append the last index
                // of the list to preserve the previous behavior
                if !index_found && !index_files.is_empty() {
                    file_path.push(index_files.last().unwrap());
                }
            } else {
                // Fallback pre-compressed variant check for the specific file
                #[cfg(feature = "compression")]
diff --git a/tests/compression_static.rs b/tests/compression_static.rs
index 35f291b..f614c6c 100644
--- a/tests/compression_static.rs
+++ b/tests/compression_static.rs
@@ -48,6 +48,7 @@ mod tests {
            #[cfg(feature = "compression")]
            compression_static: true,
            ignore_hidden_files: false,
            index_files: &[],
        })
        .await
        .expect("unexpected error response on `handle` function");
@@ -106,6 +107,7 @@ mod tests {
            #[cfg(feature = "compression")]
            compression_static: true,
            ignore_hidden_files: false,
            index_files: &[],
        })
        .await
        .expect("unexpected error response on `handle` function");
@@ -158,6 +160,7 @@ mod tests {
            redirect_trailing_slash: true,
            compression_static: true,
            ignore_hidden_files: false,
            index_files: &[],
        })
        .await
        .expect("unexpected error response on `handle` function");
diff --git a/tests/dir_listing.rs b/tests/dir_listing.rs
index b1b2b27..851b089 100644
--- a/tests/dir_listing.rs
+++ b/tests/dir_listing.rs
@@ -49,6 +49,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -79,6 +80,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -119,6 +121,7 @@ mod tests {
                redirect_trailing_slash: false,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -159,6 +162,7 @@ mod tests {
                redirect_trailing_slash: false,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -189,6 +193,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -240,6 +245,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: true,
                index_files: &[],
            })
            .await
            {
@@ -254,7 +260,7 @@ mod tests {

                    if method == Method::GET {
                        let entries: Vec<FileEntry> = serde_json::from_str(body_str).unwrap();
                        assert_eq!(entries.len(), 2);
                        assert_eq!(entries.len(), 3);

                        let first_entry = entries.first().unwrap();
                        assert_eq!(first_entry.name, "spécial directöry");
@@ -263,10 +269,10 @@ mod tests {
                        assert!(first_entry.size.is_none());

                        let last_entry = entries.last().unwrap();
                        assert_eq!(last_entry.name, "index.html.gz");
                        assert_eq!(last_entry.name, "index.htm");
                        assert_eq!(last_entry.typed, "file");
                        assert!(!last_entry.mtime.is_empty());
                        assert!(last_entry.size.unwrap() > 300);
                        assert!(last_entry.size.unwrap() >= 36);
                    } else {
                        assert!(body_str.is_empty());
                    }
@@ -309,6 +315,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -351,6 +358,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: true,
                index_files: &[],
            })
            .await
            {
diff --git a/tests/fixtures/public/index.htm b/tests/fixtures/public/index.htm
new file mode 100644
index 0000000..ad3452f
--- /dev/null
+++ b/tests/fixtures/public/index.htm
@@ -0,0 +1 @@
<h1>this is a custom index file</h1>
diff --git a/tests/static_files.rs b/tests/static_files.rs
index 6007ab0..0c55a54 100644
--- a/tests/static_files.rs
+++ b/tests/static_files.rs
@@ -39,6 +39,7 @@ mod tests {
            redirect_trailing_slash: true,
            compression_static: false,
            ignore_hidden_files: false,
            index_files: &[],
        })
        .await
        .expect("unexpected error response on `handle` function");
@@ -80,6 +81,7 @@ mod tests {
            redirect_trailing_slash: true,
            compression_static: false,
            ignore_hidden_files: false,
            index_files: &[],
        })
        .await
        .expect("unexpected error response on `handle` function");
@@ -122,6 +124,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -152,6 +155,7 @@ mod tests {
            redirect_trailing_slash: true,
            compression_static: false,
            ignore_hidden_files: false,
            index_files: &[],
        })
        .await
        .expect("unexpected error response on `handle` function");
@@ -183,6 +187,7 @@ mod tests {
            redirect_trailing_slash: true,
            compression_static: false,
            ignore_hidden_files: false,
            index_files: &[],
        })
        .await
        {
@@ -213,6 +218,7 @@ mod tests {
            redirect_trailing_slash: false,
            compression_static: false,
            ignore_hidden_files: false,
            index_files: &[],
        })
        .await
        {
@@ -248,6 +254,7 @@ mod tests {
                    redirect_trailing_slash: true,
                    compression_static: false,
                    ignore_hidden_files: false,
                    index_files: &[],
                })
                .await
                {
@@ -298,6 +305,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -330,6 +338,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -365,6 +374,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -400,6 +410,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -438,6 +449,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -474,6 +486,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -508,6 +521,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -541,6 +555,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -588,6 +603,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -649,6 +665,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -712,6 +729,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -759,6 +777,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -806,6 +825,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -854,6 +874,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -894,6 +915,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -944,6 +966,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -991,6 +1014,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -1041,6 +1065,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -1089,6 +1114,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -1132,6 +1158,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -1186,6 +1213,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: false,
                ignore_hidden_files: false,
                index_files: &[],
            })
            .await
            {
@@ -1232,6 +1260,7 @@ mod tests {
                redirect_trailing_slash: true,
                compression_static: true,
                ignore_hidden_files: true,
                index_files: &[],
            })
            .await
            {
@@ -1244,4 +1273,48 @@ mod tests {
            }
        }
    }

    #[tokio::test]
    async fn handle_multiple_index_files() {
        let root_dir = PathBuf::from("tests/fixtures/public/");
        let headers = HeaderMap::new();

        let buf = fs::read(root_dir.join("index.htm"))
            .expect("unexpected error during index.htm reading");
        let buf = Bytes::from(buf);

        for method in [Method::HEAD, Method::GET] {
            match static_files::handle(&HandleOpts {
                method: &method,
                headers: &headers,
                base_path: &root_dir,
                uri_path: "/",
                uri_query: None,
                #[cfg(feature = "directory-listing")]
                dir_listing: false,
                #[cfg(feature = "directory-listing")]
                dir_listing_order: 6,
                #[cfg(feature = "directory-listing")]
                dir_listing_format: &DirListFmt::Html,
                redirect_trailing_slash: true,
                compression_static: true,
                ignore_hidden_files: true,
                index_files: &["index.html", "index.htm"],
            })
            .await
            {
                Ok((mut res, _)) => {
                    assert_eq!(res.status(), 200);
                    assert_eq!(res.headers()["content-length"], format!("{}", buf.len()));
                    let body = hyper::body::to_bytes(res.body_mut())
                        .await
                        .expect("unexpected bytes error during `body` conversion");
                    assert_eq!(body, &buf);
                }
                Err(_) => {
                    panic!("expected a normal response rather than a status error")
                }
            }
        }
    }
}
diff --git a/tests/toml/config.toml b/tests/toml/config.toml
index 3d3b3ff..3bbf5a6 100644
--- a/tests/toml/config.toml
+++ b/tests/toml/config.toml
@@ -65,6 +65,9 @@ redirect-trailing-slash = true
#### Check for existing pre-compressed files
compression-static = false

#### List of index files
index-files = "index.html, index.htm"

### Windows Only

#### Run the web server as a Windows Service