Repository: static-web-server/static-web-server (language: rust)

Issue:
CORS not working when Basic Auth enabled
### Search for duplicate issues

- [X] I already searched, and this issue is not a duplicate.

### Issue scope

Linux (E.g. Arch Linux, Debian, Alpine, etc)

### Describe the bug

CORS (Access-Control-Allow-Origin) not in response header when basic auth is enabled.

CORS works when running:
./static-web-server --port 8787 --root ./public --cors-allow-origins "*"

CORS does not work when running:
./static-web-server --port 8787 --root ./public --cors-allow-origins "*" --basic-auth 'user:$2y$10$ga2EXn12OAxgxyzRNFy7Se4OvW1fxOAogpFqJZklO0OzBzu5iuqpG'

username: user
password: bbbb


### How to reproduce it

When running with basic auth, I am getting an error:
![image](https://github.com/static-web-server/static-web-server/assets/2199825/464ed3aa-f5e2-4157-998f-9fb8828c433e)

![image](https://github.com/static-web-server/static-web-server/assets/2199825/6d49c4ae-a7b4-41af-9bcb-ca9351835f53)

When without the basic auth, Access-Control-Allow-* returned OK:
![image](https://github.com/static-web-server/static-web-server/assets/2199825/e9f474ab-c9a7-40a6-b4dd-1c0743df0182)



### Expected behavior

Access-Control-Allow-* returned in the response header.

### Complementary information

I have tried this on x86_64 and arm64, both returned the same error.

### Build target

x86_64-unknown-linux-musl

### Environment and specs

- [ ] **static-web-server:** [e.g. v2.28.0]
- [ ] **OS:** [Linux Mint 21.3]
- [ ] **Arch:** [x86_64 (64-bit), ARM64 (64-bit)]
- [ ] **Docker:** [e.g. 26.0.0]
- [ ] **Client:** [Brave 1.64.116, Chromium 123.0.6312.105]



### Additional context

_No response_


Discussion / hints:
I would like to clarify some things.

- **Basic Auth:** SWS does not allow `OPTIONS` requests missing the `Authorization` header. Therefore, the server will respond with a `401` status.  
- **CORS:** The SWS `--cors-allow-headers` option should set the `authorization` header (it is not by default). Otherwise, the server will respond with a `403` status.

To summarize (and what we need to do):

- When enabling `--basic-auth`, SWS should allow `OPTIONS` requests without the  `Authorization` header since credentials can be omitted according to the [spec](https://fetch.spec.whatwg.org/#cors-protocol-and-credentials).
-  SWS `--cors-allow-headers` should add also the `authorization` header to the list to work correctly with Basic Auth. 

This is an issue so a fix should come in the next days.

Fix patch:
diff --git a/docs/content/features/cors.md b/docs/content/features/cors.md
index 1c919015..53d3976b 100644
--- a/docs/content/features/cors.md
+++ b/docs/content/features/cors.md
@@ -26,7 +26,7 @@ The server also supports a list of [CORS allowed headers](https://developer.mozi
 This feature depends on `--cors-allow-origins` to be used along with this feature. It can be controlled by the string `-j, --cors-allow-headers` option or the equivalent [SERVER_CORS_ALLOW_HEADERS](../configuration/environment-variables.md#server_cors_allow_headers) env.
 
 !!! info "Tips"
-    - The default allowed headers value is `origin, content-type`.
+    - The default allowed headers value is `origin, content-type, authorization`.
     - The server also supports [preflight requests](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request) via the `OPTIONS` method. See [Preflighted requests in CORS](./http-methods.md#preflighted-requests-in-cors).
 
 Below is an example of how to CORS.
diff --git a/src/cors.rs b/src/cors.rs
index 4a1233be..6198ad71 100644
--- a/src/cors.rs
+++ b/src/cors.rs
@@ -15,6 +15,8 @@ use headers::{
 use http::header;
 use std::collections::HashSet;
 
+use crate::server_info;
+
 /// It defines CORS instance.
 #[derive(Clone, Debug)]
 pub struct Cors {
@@ -68,8 +70,8 @@ pub fn new(
         };
 
         if cors_res.is_some() {
-            tracing::info!(
-                    "enabled=true, allow_methods=[GET,HEAD,OPTIONS], allow_origins={}, allow_headers=[{}], expose_headers=[{}]",
+            server_info!(
+                    "cors enabled=true, allow_methods=[GET,HEAD,OPTIONS], allow_origins={}, allow_headers=[{}], expose_headers=[{}]",
                     origins_str,
                     allow_headers_str,
                     expose_headers_str,
@@ -267,16 +269,30 @@ impl Configured {
                         return Err(Forbidden::Method);
                     }
                 } else {
-                    tracing::trace!(
-                        "cors: preflight request missing access-control-request-method header"
+                    tracing::warn!(
+                        "cors: preflight request missing `access-control-request-method` header"
                     );
                     return Err(Forbidden::Method);
                 }
 
                 if let Some(req_headers) = headers.get(header::ACCESS_CONTROL_REQUEST_HEADERS) {
-                    let headers = req_headers.to_str().map_err(|_| Forbidden::Header)?;
+                    let headers = match req_headers.to_str() {
+                        Ok(val) => val,
+                        Err(err) => {
+                            tracing::error!(
+                                "cors: error parsing header `access-control-request-headers` value: {:?}",
+                                err,
+                            );
+                            return Err(Forbidden::Header);
+                        }
+                    };
+
                     for header in headers.split(',') {
-                        if !self.is_header_allowed(header.trim()) {
+                        let h = header.trim();
+                        if !self.is_header_allowed(h) {
+                            tracing::error!(
+                                "cors: header `{}` is not allowed because is missing in `cors_allow_headers` server option", h
+                            );
                             return Err(Forbidden::Header);
                         }
                     }
@@ -316,12 +332,18 @@ impl Configured {
     }
 
     fn is_header_allowed(&self, header: &str) -> bool {
+        if header.is_empty() {
+            return false;
+        }
         HeaderName::from_bytes(header.as_bytes())
             .map(|header| self.cors.allowed_headers.contains(&header))
             .unwrap_or(false)
     }
 
     fn is_origin_allowed(&self, origin: &HeaderValue) -> bool {
+        if origin.is_empty() {
+            return false;
+        }
         if let Some(ref allowed) = self.cors.origins {
             allowed.contains(origin)
         } else {
diff --git a/src/handler.rs b/src/handler.rs
index 43ff879f..214f6c17 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -267,7 +267,7 @@ impl RequestHandler {
 
             #[cfg(feature = "basic-auth")]
             // `Basic` HTTP Authorization Schema
-            if !self.opts.basic_auth.is_empty() {
+            if !self.opts.basic_auth.is_empty() && !method.is_options() {
                 if let Some((user_id, password)) = self.opts.basic_auth.split_once(':') {
                     if let Err(err) = basic_auth::check_request(headers, user_id, password) {
                         tracing::warn!("basic authentication failed {:?}", err);
diff --git a/src/settings/cli.rs b/src/settings/cli.rs
index a05cc9e2..58b7225b 100644
--- a/src/settings/cli.rs
+++ b/src/settings/cli.rs
@@ -135,7 +135,7 @@ pub struct General {
     #[arg(
         long,
         short = 'j',
-        default_value = "origin, content-type",
+        default_value = "origin, content-type, authorization",
         env = "SERVER_CORS_ALLOW_HEADERS"
     )]
     /// Specify an optional CORS list of allowed headers separated by commas. Default "origin, content-type". It requires `--cors-allow-origins` to be used along with.


Test patch: