Repository: thecoshman/http (language: rust)

Issue:
Cors method configuration
Hello,

I cannot pass the parameter:
```
-H "Access-Control-Allow-Method: GET,POST"
```

due to the comma, the VALUE is detected as not correct.

Best regards,
Marc-Antoine


Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 7a743cd..fedadc5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,13 +9,14 @@ categories = ["network-programming", "web-programming::http-server"]
 license = "MIT"
 build = "build.rs"
 # Remember to also update in appveyor.yml
-version = "1.12.1"
+version = "1.12.2"
 # Remember to also update in http.md
 authors = ["thecoshman <rust@thecoshman.com>",
            "nabijaczleweli <nabijaczleweli@nabijaczleweli.xyz>",
            "pheki",
            "Adrian Herath <adrianisuru@gmail.com>",
-           "cyqsimon"]
+           "cyqsimon",
+           "jim4067"]
 
 [dependencies]
 hyper-native-tls = "0.3"
diff --git a/README.md b/README.md
index e407309..df1cb58 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# http [![Travis build status](https://travis-ci.org/thecoshman/http.svg)](https://travis-ci.org/thecoshman/http) [![AppVeyor build status](https://ci.appveyor.com/api/projects/status/7knk9lnptn1njro5?svg=true)](https://ci.appveyor.com/project/thecoshman/http) [![Licence](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) [![Crates.io version](https://meritbadge.herokuapp.com/https)](https://crates.io/crates/https)
+# http [![Travis build status](https://travis-ci.org/thecoshman/http.svg)](https://travis-ci.org/thecoshman/http) [![AppVeyor build status](https://ci.appveyor.com/api/projects/status/7knk9lnptn1njro5?svg=true)](https://ci.appveyor.com/project/thecoshman/http) [![Licence](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) [![Crates.io version](https://img.shields.io/crates/v/https.svg)](https://crates.io/crates/https)
 Host These Things Please - a basic HTTP server for hosting a folder fast and simply
 
 ## Selected features
diff --git a/appveyor.yml b/appveyor.yml
index 1d63067..c58b9f6 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 1.12.1-{build}
+version: 1.12.2-{build}
 
 skip_tags: false
 
@@ -11,11 +11,13 @@ install:
   - set PATH=C:\msys64\mingw64\bin;C:\msys64\usr\bin;%PATH%;C:\Users\appveyor\.cargo\bin
   # https://www.msys2.org/news/#2020-05-17-32-bit-msys2-no-longer-actively-supported
   - curl -SL http://repo.msys2.org/msys/x86_64/msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz -oC:\msys2-keyring.txz
-  - curl -SL http://repo.msys2.org/msys/x86_64/zstd-1.4.4-2-x86_64.pkg.tar.xz -oC:\zstd.txz
+  - curl -SL http://repo.msys2.org/msys/x86_64/msys2-keyring-1~20210904-1-any.pkg.tar.zst -oC:\msys2-keyring.tzst
+  - curl -SL http://repo.msys2.org/msys/x86_64/zstd-1.4.7-1-x86_64.pkg.tar.xz -oC:\zstd.txz
   - curl -SL http://repo.msys2.org/msys/x86_64/pacman-5.2.2-5-x86_64.pkg.tar.xz -oC:\pacman.txz
   - pacman --noconfirm -U C:\msys2-keyring.txz
   - pacman --noconfirm -U C:\zstd.txz
   - pacman --noconfirm -U C:\pacman.txz
+  - pacman --noconfirm -U C:\msys2-keyring.tzst
   - bash -lc "pacman --needed --noconfirm -Sy pacman-mirrors"
   - bash -lc "pacman --noconfirm -Sy pacman"
   - bash -lc "pacman --noconfirm -Sy"
@@ -33,21 +35,21 @@ build: off
 build_script:
   - git submodule update --init --recursive
   - cargo build --verbose --release
-  - cp target\release\http.exe http-v1.12.1.exe
-  - strip --strip-all --remove-section=.comment --remove-section=.note http-v1.12.1.exe
-  - makensis -DHTTP_VERSION=v1.12.1 install.nsi
+  - cp target\release\http.exe http-v1.12.2.exe
+  - strip --strip-all --remove-section=.comment --remove-section=.note http-v1.12.2.exe
+  - makensis -DHTTP_VERSION=v1.12.2 install.nsi
 
 test: off
 test_script:
   - cargo test --verbose --release
 
 artifacts:
-  - path: http-v1.12.1.exe
-  - path: http v1.12.1 installer.exe
+  - path: http-v1.12.2.exe
+  - path: http v1.12.2 installer.exe
 
 deploy:
   provider: GitHub
-  artifact: /http.*v1.12.1.*\.exe/
+  artifact: /http.*v1.12.2.*\.exe/
   auth_token:
     secure: ZTXvCrv9y01s7Hd60w8W7NaouPnPoaw9YJt9WhWQ2Pep8HLvCikt9Exjkz8SGP9P
   on:
diff --git a/http.md b/http.md
index 3280e9a..dc07b8f 100644
--- a/http.md
+++ b/http.md
@@ -463,7 +463,8 @@ Written by thecoshman &lt;<rust@thecoshman.com>&gt;,
            nabijaczleweli &lt;<nabijaczleweli@nabijaczleweli.xyz>&gt;,
            pheki,
            Adrian Herath &lt;<adrianisuru@gmail.com>&gt;,
-       and cyqsimon.
+           cyqsimon,
+       and jim4067.
 
 ## REPORTING BUGS
 
diff --git a/src/ops/mod.rs b/src/ops/mod.rs
index e78a3b1..fea7642 100644
--- a/src/ops/mod.rs
+++ b/src/ops/mod.rs
@@ -27,7 +27,7 @@ use iron::mime::{Mime, SubLevel as MimeSubLevel, TopLevel as MimeTopLevel};
 use std::io::{self, ErrorKind as IoErrorKind, SeekFrom, Write, Error as IoError, Read, Seek};
 use iron::{headers, status, method, mime, IronResult, Listening, Response, TypeMap, Request, Handler, Iron};
 use self::super::util::{WwwAuthenticate, DisplayThree, CommaList, Spaces, Dav, url_path, file_hash, is_symlink, encode_str, encode_file, file_length,
-                        hash_string, html_response, file_binary, client_mobile, percent_decode, file_icon_suffix, is_actually_file, is_descendant_of,
+                        hash_string, html_response, file_binary, client_mobile, percent_decode, escape_specials, file_icon_suffix, is_actually_file, is_descendant_of,
                         response_encoding, detect_file_as_dir, encoding_extension, file_time_modified, file_time_modified_p, get_raw_fs_metadata,
                         human_readable_size, encode_tail_if_trimmed, is_nonexistent_descendant_of, USER_AGENT, ERROR_HTML, INDEX_EXTENSIONS, MIN_ENCODING_GAIN,
                         MAX_ENCODING_SIZE, MIN_ENCODING_SIZE, DAV_LEVEL_1_METHODS, DIRECTORY_LISTING_HTML, MOBILE_DIRECTORY_LISTING_HTML,
@@ -352,7 +352,7 @@ impl HttpHandler {
             return self.handle_nonexistent(req, req_p);
         }
 
-        let is_file = is_actually_file(&req_p.metadata().expect("Failed to get file metadata").file_type());
+        let is_file = is_actually_file(&req_p.metadata().expect("Failed to get file metadata").file_type(), &req_p);
         let range = req.headers.get().map(|r: &headers::Range| (*r).clone());
         let raw_fs = req.headers.get().map(|r: &RawFsApiHeader| r.0).unwrap_or(false);
         if is_file {
@@ -687,7 +687,7 @@ impl HttpHandler {
                     }) || (self.follow_symlinks && self.sandbox_symlinks && symlink && !is_descendant_of(fp, &self.hosted_directory.1)))
                 })
                                                 .map(|f| {
-                    let is_file = is_actually_file(&f.file_type().expect("Failed to get file type"));
+                    let is_file = is_actually_file(&f.file_type().expect("Failed to get file type"), &f.path());
                     if is_file {
                         get_raw_fs_metadata(f.path())
                     } else {
@@ -775,8 +775,7 @@ impl HttpHandler {
                     file_time_modified_p(req_p.parent().expect("Failed to get requested directory's parent directory"))
                         .strftime("%F %T")
                         .unwrap(),
-                    up_path =
-                        slash_idx.map(|i| &rel_noslash[0..i]).unwrap_or("").replace('%', "%25").replace('#', "%23").replace('[', "%5B").replace(']', "%5D"),
+                    up_path = escape_specials(slash_idx.map(|i| &rel_noslash[0..i]).unwrap_or("")),
                     up_path_slash = if slash_idx.is_some() { "/" } else { "" })
         };
         let list_s = req_p.read_dir()
@@ -792,13 +791,13 @@ impl HttpHandler {
                 }) || (self.follow_symlinks && self.sandbox_symlinks && symlink && !is_descendant_of(fp, &self.hosted_directory.1)))
             })
             .sorted_by(|lhs, rhs| {
-                (is_actually_file(&lhs.file_type().expect("Failed to get file type")),
+                (is_actually_file(&lhs.file_type().expect("Failed to get file type"), &lhs.path()),
                  lhs.file_name().to_str().expect("Failed to get file name").to_lowercase())
-                    .cmp(&(is_actually_file(&rhs.file_type().expect("Failed to get file type")),
+                    .cmp(&(is_actually_file(&rhs.file_type().expect("Failed to get file type"), &rhs.path()),
                            rhs.file_name().to_str().expect("Failed to get file name").to_lowercase()))
             })
             .fold("".to_string(), |cur, f| {
-                let is_file = is_actually_file(&f.file_type().expect("Failed to get file type"));
+                let is_file = is_actually_file(&f.file_type().expect("Failed to get file type"), &f.path());
                 let fmeta = f.metadata().expect("Failed to get requested file metadata");
                 let fname = f.file_name().into_string().expect("Failed to get file name");
                 let path = f.path();
@@ -809,7 +808,7 @@ impl HttpHandler {
                         if is_file { "file" } else { "dir" },
                         file_icon_suffix(&path, is_file),
                         path.file_name().map(|p| p.to_str().expect("Filename not UTF-8").replace('.', "_")).as_ref().unwrap_or(&fname),
-                        fname.replace('<', "&lt;"),
+                        fname.replace('&', "&amp;").replace('<', "&lt;"),
                         if is_file { "" } else { "/" },
                         if show_file_management_controls {
                             DisplayThree("<span class=\"manage\"><span class=\"delete_file_icon\">Delete</span>",
@@ -828,8 +827,8 @@ impl HttpHandler {
                         } else {
                             DisplayThree("", String::new(), "")
                         },
-                        path = format!("/{}", relpath).replace("//", "/").replace('%', "%25").replace('#', "%23").replace('[', "%5B").replace(']', "%5D"),
-                        fname = encode_tail_if_trimmed(fname.replace('%', "%25").replace('#', "%23").replace('[', "%5B").replace(']', "%5D")))
+                        path = escape_specials(format!("/{}", relpath).replace("//", "/")),
+                        fname = encode_tail_if_trimmed(escape_specials(&fname)))
             });
 
         self.handle_generated_response_encoding(req,
@@ -879,8 +878,7 @@ impl HttpHandler {
                          <td><a href=\"/{up_path}{up_path_slash}\">&nbsp;</a></td> \
                          <td><a href=\"/{up_path}{up_path_slash}\">&nbsp;</a></td></tr>",
                     file_time_modified_p(req_p.parent().expect("Failed to get requested directory's parent directory")).strftime("%F %T").unwrap(),
-                    up_path =
-                        slash_idx.map(|i| &rel_noslash[0..i]).unwrap_or("").replace('%', "%25").replace('#', "%23").replace('[', "%5B").replace(']', "%5D"),
+                    up_path = escape_specials(slash_idx.map(|i| &rel_noslash[0..i]).unwrap_or("")),
                     up_path_slash = if slash_idx.is_some() { "/" } else { "" })
         };
 
@@ -900,13 +898,13 @@ impl HttpHandler {
                 }) || (self.follow_symlinks && self.sandbox_symlinks && symlink && !is_descendant_of(fp, &self.hosted_directory.1)))
             })
             .sorted_by(|lhs, rhs| {
-                (is_actually_file(&lhs.file_type().expect("Failed to get file type")),
+                (is_actually_file(&lhs.file_type().expect("Failed to get file type"), &lhs.path()),
                  lhs.file_name().to_str().expect("Failed to get file name").to_lowercase())
-                    .cmp(&(is_actually_file(&rhs.file_type().expect("Failed to get file type")),
+                    .cmp(&(is_actually_file(&rhs.file_type().expect("Failed to get file type"), &rhs.path()),
                            rhs.file_name().to_str().expect("Failed to get file name").to_lowercase()))
             })
             .fold("".to_string(), |cur, f| {
-                let is_file = is_actually_file(&f.file_type().expect("Failed to get file type"));
+                let is_file = is_actually_file(&f.file_type().expect("Failed to get file type"), &f.path());
                 let fmeta = f.metadata().expect("Failed to get requested file metadata");
                 let fname = f.file_name().into_string().expect("Failed to get file name");
                 let path = f.path();
@@ -919,7 +917,7 @@ impl HttpHandler {
                         path.file_name().map(|p| p.to_str().expect("Filename not UTF-8").replace('.', "_")).as_ref().unwrap_or(&fname),
                         if is_file { "file" } else { "dir" },
                         file_icon_suffix(&path, is_file),
-                        fname.replace('<', "&lt;"),
+                        fname.replace('&', "&amp;").replace('<', "&lt;"),
                         if is_file { "" } else { "/" },
                         file_time_modified(&fmeta).strftime("%F %T").unwrap(),
                         if is_file {
@@ -944,8 +942,8 @@ impl HttpHandler {
                         } else {
                             DisplayThree("", "", "")
                         },
-                        path = format!("/{}", relpath).replace("//", "/").replace('%', "%25").replace('#', "%23").replace('[', "%5B").replace(']', "%5D"),
-                        fname = encode_tail_if_trimmed(fname.replace('%', "%25").replace('#', "%23").replace('[', "%5B").replace(']', "%5D")))
+                        path = escape_specials(format!("/{}", relpath).replace("//", "/")),
+                        fname = encode_tail_if_trimmed(escape_specials(&fname)))
             });
 
         self.handle_generated_response_encoding(req,
@@ -1116,10 +1114,11 @@ impl HttpHandler {
 
     fn handle_delete_path(&self, req: &mut Request, req_p: PathBuf, symlink: bool) -> IronResult<Response> {
         let ft = req_p.metadata().expect("failed to get file metadata").file_type();
+        let is_file = is_actually_file(&ft, &req_p);
         log!(self.log,
              "{} deleted {blue}{} {magenta}{}{reset}",
              self.remote_addresses(&req),
-             if is_actually_file(&ft) {
+             if is_file {
                  "file"
              } else if symlink {
                  "symlink"
@@ -1128,7 +1127,7 @@ impl HttpHandler {
              },
              req_p.display());
 
-        if is_actually_file(&ft) {
+        if is_file {
             fs::remove_file(req_p).expect("Failed to remove requested file");
         } else {
             fs::remove_dir_all(req_p).expect(if symlink {
diff --git a/src/ops/webdav.rs b/src/ops/webdav.rs
index 7ad9864..3583766 100644
--- a/src/ops/webdav.rs
+++ b/src/ops/webdav.rs
@@ -329,7 +329,7 @@ impl HttpHandler {
                 return Ok(Response::with(status::PreconditionFailed));
             }
 
-            if !is_actually_file(&dest_p.metadata().expect("Failed to get destination file metadata").file_type()) {
+            if !is_actually_file(&dest_p.metadata().expect("Failed to get destination file metadata").file_type(), &dest_p) {
                 // NB: this disallows overwriting non-empty directories
                 if fs::remove_dir(&dest_p).is_err() {
                     return Ok(Response::with(status::Locked));
@@ -339,7 +339,7 @@ impl HttpHandler {
             overwritten = true;
         }
 
-        let source_file = is_actually_file(&req_p.metadata().expect("Failed to get requested file metadata").file_type());
+        let source_file = is_actually_file(&req_p.metadata().expect("Failed to get requested file metadata").file_type(), &dest_p);
         if let Some(sp) = source_path {
             *sp = (req_p.clone(), source_file);
         }
@@ -463,7 +463,7 @@ impl HttpHandler {
 
                 "resourcetype" => {
                     out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "resourcetype")))?;
-                    if !is_actually_file(&meta.file_type()) {
+                    if !is_actually_file(&meta.file_type(), path) {
                         out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "collection")))?;
                         out.write(XmlWEvent::end_element())?;
                     }
diff --git a/src/options.rs b/src/options.rs
index 40a3687..17d4742 100644
--- a/src/options.rs
+++ b/src/options.rs
@@ -146,15 +146,20 @@ impl Options {
             .arg(Arg::from_usage("--auth [USERNAME[:PASSWORD]] 'Data for global authentication'").validator(Options::credentials_validator))
             .arg(Arg::from_usage("--gen-auth 'Generate a one-off username:password set for global authentication'").conflicts_with("auth"))
             .arg(Arg::from_usage("--path-auth [PATH=[USERNAME[:PASSWORD]]]... 'Data for authentication under PATH'")
+                .use_delimiter(false)
                 .validator(Options::path_credentials_validator))
-            .arg(Arg::from_usage("--gen-path-auth [PATH]... 'Generate a one-off username:password set for authentication under PATH'"))
+            .arg(Arg::from_usage("--gen-path-auth [PATH]... 'Generate a one-off username:password set for authentication under PATH'").use_delimiter(false))
             .arg(Arg::from_usage("--proxy [HEADER-NAME:CIDR]... 'Treat HEADER-NAME as proxy forwarded-for header when request comes from CIDR'")
+                .use_delimiter(false)
                 .validator(|s| Options::proxy_parse(s.into()).map(|_| ())))
             .arg(Arg::from_usage("-m --mime-type [EXTENSION:MIME-TYPE]... 'Always return MIME-TYPE for files with EXTENSION'")
+                .use_delimiter(false)
                 .validator(|s| Options::mime_type_override_parse(s.into()).map(|_| ())))
             .arg(Arg::from_usage("--request-bandwidth [BYTES] 'Limit each request to returning BYTES per second, or 0 for unlimited. Default: 0'")
                 .validator(|s| Options::bandwidth_parse(s.into()).map(|_| ())))
-            .arg(Arg::from_usage("-H --header [NAME: VALUE]... 'Headers to add to every response'").validator(|s| Options::header_parse(&s).map(|_| ())))
+            .arg(Arg::from_usage("-H --header [NAME: VALUE]... 'Headers to add to every response'")
+                .use_delimiter(false)
+                .validator(|s| Options::header_parse(&s).map(|_| ())))
             .get_matches();
 
         let dir = matches.value_of("DIR").unwrap_or(".");
diff --git a/src/util/mod.rs b/src/util/mod.rs
index d93b9e1..b8c2706 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -240,6 +240,23 @@ pub fn encode_tail_if_trimmed(mut s: String) -> String {
     }
 }
 
+/// %-escape special characters in an URL
+pub fn escape_specials<S: AsRef<str>>(s: S) -> String {
+    let s = s.as_ref();
+    let mut ret = Vec::with_capacity(s.len());
+    for &b in s.as_bytes() {
+        match b {
+            b'%' => ret.extend(b"%25"),
+            b'#' => ret.extend(b"%23"),
+            b'?' => ret.extend(b"%3F"),
+            b'[' => ret.extend(b"%5B"),
+            b']' => ret.extend(b"%5D"),
+            _ => ret.push(b),
+        }
+    }
+    unsafe { String::from_utf8_unchecked(ret) }
+}
+
 /// Check if the specified file is to be considered "binary".
 ///
 /// Basically checks is a file is UTF-8.
@@ -385,8 +402,8 @@ pub fn is_symlink<P: AsRef<Path>>(p: P) -> bool {
 }
 
 /// Check if a path refers to a file in a way that includes Unix devices and Windows symlinks.
-pub fn is_actually_file(tp: &FileType) -> bool {
-    tp.is_file() || is_device(tp)
+pub fn is_actually_file<P: AsRef<Path>>(tp: &FileType, p: P) -> bool {
+    tp.is_file() || (tp.is_symlink() && fs::metadata(p).map(|m| is_actually_file(&m.file_type(), "")).unwrap_or(false)) || is_device(tp)
 }
 
 /// Check if the specified path is a direct descendant (or an equal) of the specified path.
@@ -560,7 +577,7 @@ pub fn copy_dir(from: &Path, to: &Path) -> IoResult<Vec<(IoError, String)>> {
 
         let target_path = to.join(relative_path);
 
-        if !is_actually_file(&source_metadata.file_type()) {
+        if !is_actually_file(&source_metadata.file_type(), entry.path()) {
             push_error!(errors, relative_path, fs::create_dir(&target_path));
             push_error!(errors, relative_path, fs::set_permissions(&target_path, source_metadata.permissions()));
         } else {
diff --git a/src/util/os/non_windows.rs b/src/util/os/non_windows.rs
index e443936..9ca661c 100644
--- a/src/util/os/non_windows.rs
+++ b/src/util/os/non_windows.rs
@@ -24,7 +24,7 @@ pub fn win32_file_attributes(meta: &Metadata, path: &Path) -> u32 {
         attr |= FILE_ATTRIBUTE_HIDDEN;
     }
 
-    if !is_actually_file(&meta.file_type()) {
+    if !is_actually_file(&meta.file_type(), &path) {
         attr |= FILE_ATTRIBUTE_DIRECTORY;
     } else {
         // this is the 'Archive' bit, which is set by


Test patch: