Repository: thecoshman/http (language: rust)

Issue:
Create directory button for non-WebDAV
Currently the only way (at least that I found) to create directories without WebDAV enabled (i.e. without -d) is to drag&drop a folder with some file in it, which is quite annoying, especially when you don't have any graphical file manager.


Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 78f873f..98b8676 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,7 +9,7 @@ categories = ["network-programming", "web-programming::http-server"]
 license = "MIT"
 build = "build.rs"
 # Remember to also update in appveyor.yml and the http-crates.io branch
-version = "2.0.2"
+version = "2.1.0"
 # Remember to also update in http.md
 authors = ["thecoshman <rust@thecoshman.com>",
            "nabijaczleweli <nabijaczleweli@nabijaczleweli.xyz>",
@@ -27,7 +27,6 @@ percent-encoding = "2.1"
 serde_json = "1.0"
 mime_guess = "1.8"
 tabwriter = "1.1"
-itertools = "0.8"
 arrayvec = "0.7"
 walkdir = "2.2"
 blake3 = "1.3"
@@ -52,7 +51,7 @@ default-features = false
 
 [dependencies.brotli]
 version = "6.0"
-features = ["simd"]  # comment this out for release
+features = ["simd"]
 
 [dependencies.iron]
 path = "vendor/iron-0.6.1"
diff --git a/appveyor.yml b/appveyor.yml
index 0139118..c6b087b 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,7 +1,7 @@
 image:
   - Visual Studio 2022
 
-version: 2.0.2-{build}
+version: 2.1.0-{build}
 
 skip_tags: false
 
@@ -27,21 +27,21 @@ build: off
 build_script:
   - git submodule update --init --recursive
   - cargo build --verbose --release
-  - cp target\release\http.exe http-v2.0.2.exe
-  - strip --strip-all --remove-section=.comment --remove-section=.note http-v2.0.2.exe
-  - makensis -DHTTP_VERSION=v2.0.2 install.nsi
+  - cp target\release\http.exe http-v2.1.0.exe
+  - strip --strip-all --remove-section=.comment --remove-section=.note http-v2.1.0.exe
+  - makensis -DHTTP_VERSION=v2.1.0 install.nsi
 
 test: off
 test_script:
   - cargo test --verbose --release
 
 artifacts:
-  - path: http-v2.0.2.exe
-  - path: http v2.0.2 installer.exe
+  - path: http-v2.1.0.exe
+  - path: http v2.1.0 installer.exe
 
 deploy:
   provider: GitHub
-  artifact: /http.*v2.0.2.*\.exe/
+  artifact: /http.*v2.1.0.*\.exe/
   auth_token:
     secure: ZTXvCrv9y01s7Hd60w8W7NaouPnPoaw9YJt9WhWQ2Pep8HLvCikt9Exjkz8SGP9P
   on:
diff --git a/assets/directory_listing.html b/assets/directory_listing.html
index 58aa583..15d9927 100644
--- a/assets/directory_listing.html
+++ b/assets/directory_listing.html
@@ -19,6 +19,10 @@
         border-collapse: collapse;
       }
 
+      td:nth-child(2) {
+        white-space: pre-wrap;
+      }
+
       td:nth-child(2), td:nth-child(3) {
         padding-right: 5pt;
       }
diff --git a/assets/directory_listing_mobile.html b/assets/directory_listing_mobile.html
index ff02784..d15ac5a 100644
--- a/assets/directory_listing_mobile.html
+++ b/assets/directory_listing_mobile.html
@@ -38,6 +38,10 @@
         text-decoration: none;
       }
 
+      div:nth-child(1) {
+        white-space: pre-wrap;
+      }
+
       span {
         overflow-wrap: break-word;
         word-wrap: break-word;
diff --git a/http.md b/http.md
index 3cc202e..ab7032c 100644
--- a/http.md
+++ b/http.md
@@ -188,6 +188,18 @@ pass parameters like what port to use.
     This is false by default because it's most likely not something you
     want to do.
 
+  -D --convenient-webdav
+
+    Allow MKCOL (create directory) and MOVE (rename) WebDAV requests.
+
+    These are write requests so this is a no-op unless -w. False by default.
+
+  -d --webdav
+
+    Handle WebDAV requests.
+
+    False by default. Supersedes -D.
+
   -l --no-listings
 
     Do not generate directory listings.
@@ -261,12 +273,6 @@ pass parameters like what port to use.
 
     Don't colourise log output.
 
-  -d --webdav
-
-    Handle WebDAV requests.
-
-    False by default.
-
 ## NOTES
 
 When returning files from the filesystem, the `ETag` returned
diff --git a/src/main.rs b/src/main.rs
index d97b19b..a6d7aa3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,7 +5,6 @@ extern crate percent_encoding;
 extern crate trivial_colours;
 extern crate serde_json;
 extern crate mime_guess;
-extern crate itertools;
 extern crate tabwriter;
 extern crate arrayvec;
 extern crate walkdir;
diff --git a/src/ops/mod.rs b/src/ops/mod.rs
index 42bab90..cee97bf 100644
--- a/src/ops/mod.rs
+++ b/src/ops/mod.rs
@@ -120,6 +120,13 @@ pub use self::bandwidth::{LimitBandwidthMiddleware, SimpleChain};
 
 type CacheT<Cnt> = HashMap<(blake3::Hash, EncodingType), (Cnt, AtomicU64)>;
 
+#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+pub enum WebDavLevel {
+    No,
+    MkColMoveOnly,
+    All,
+}
+
 pub struct HttpHandler {
     pub hosted_directory: (String, PathBuf),
     pub follow_symlinks: bool,
@@ -129,7 +136,7 @@ pub struct HttpHandler {
     pub strip_extensions: bool,
     /// (at all, log_time, log_colour)
     pub log: (bool, bool, bool),
-    pub webdav: bool,
+    pub webdav: WebDavLevel,
     pub global_auth_data: Option<(String, Option<String>)>,
     pub path_auth_data: BTreeMap<String, Option<(String, Option<String>)>>,
     pub writes_temp_dir: Option<(String, PathBuf)>,
@@ -171,7 +178,9 @@ impl HttpHandler {
 
         let allowed_methods = [method::Options, method::Get, method::Head, method::Trace]
             .iter()
-            .chain(dav_level_1_methods(opts.allow_writes).iter().filter(|_| opts.webdav))
+            .chain(dav_level_1_methods(opts.allow_writes)
+                .iter()
+                .filter(|method| opts.webdav == WebDavLevel::All || (opts.webdav == WebDavLevel::MkColMoveOnly && matches!(**method, method::DavMkcol | method::DavMove))))
             .chain([method::Put, method::Delete].iter().filter(|_| opts.allow_writes))
             .cloned()
             .collect::<Vec<_>>()
@@ -258,15 +267,15 @@ impl Handler for &'static HttpHandler {
             }
             method::Trace => self.handle_trace(req),
 
-            method::DavCopy if self.webdav => self.handle_webdav_copy(req),
-            method::DavMkcol if self.webdav => self.handle_webdav_mkcol(req),
-            method::DavMove if self.webdav => self.handle_webdav_move(req),
-            method::DavPropfind if self.webdav => self.handle_webdav_propfind(req),
-            method::DavProppatch if self.webdav => self.handle_webdav_proppatch(req),
+            method::DavCopy if self.webdav >= WebDavLevel::All => self.handle_webdav_copy(req),
+            method::DavMkcol if self.webdav >= WebDavLevel::MkColMoveOnly => self.handle_webdav_mkcol(req),
+            method::DavMove if self.webdav >= WebDavLevel::MkColMoveOnly => self.handle_webdav_move(req),
+            method::DavPropfind if self.webdav >= WebDavLevel::All => self.handle_webdav_propfind(req),
+            method::DavProppatch if self.webdav >= WebDavLevel::All => self.handle_webdav_proppatch(req),
 
             _ => self.handle_bad_method(req),
         }?;
-        if self.webdav {
+        if self.webdav >= WebDavLevel::All {
             resp.headers.set(Dav::LEVEL_1);
         }
         for (h, v) in &self.additional_headers {
@@ -678,12 +687,12 @@ impl HttpHandler {
                                                           Header(headers::ContentEncoding([encoding].into())),
                                                           resp,
                                                           mt)));
-                            },
+                            }
                             Err(err) if err.kind() == IoErrorKind::NotFound => true,
                             e @ Err(_) => {
                                 e.expect("Failed to get encoded file metadata");
                                 unsafe { std::hint::unreachable_unchecked() }
-                            },
+                            }
                         }
                     }
                     Some(&((_, false, _), _)) => {
@@ -699,7 +708,7 @@ impl HttpHandler {
             if forgor {
                 self.cache_fs_files.write().expect("Filesystem file cache write lock poisoned").retain(|_, v| *v == hash);
                 self.cache_fs.write().expect("Filesystem cache write lock poisoned").remove(&cache_key);
-                return self.handle_get_file_encoded(req, req_p, mt, headers, etag)
+                return self.handle_get_file_encoded(req, req_p, mt, headers, etag);
             }
 
             let mut resp_p = self.encoded_temp_dir.as_ref().unwrap().1.join(cache_key.0.to_hex().as_str());
@@ -945,7 +954,7 @@ impl HttpHandler {
                                  if is_file { "" } else { "/" },
                                  if show_file_management_controls {
                                      DisplayThree(r#"<span class="manage"><span class="delete_file_icon" onclick="delete_onclick(arguments[0])">Delete</span>"#,
-                                                  if self.webdav {
+                                                  if self.webdav >= WebDavLevel::MkColMoveOnly {
                                                       r#" <span class="rename_icon" onclick="rename_onclick(arguments[0])">Rename</span>"#
                                                   } else {
                                                       ""
@@ -993,7 +1002,7 @@ impl HttpHandler {
                                                                               } else {
                                                                                   ""
                                                                               },
-                                                                              if show_file_management_controls && self.webdav {
+                                                                              if show_file_management_controls && self.webdav >= WebDavLevel::MkColMoveOnly {
                                                                                   r#"<a id='new"directory' href><span class="new_dir_icon">Create directory</span></a>"#
                                                                               } else {
                                                                                   ""
@@ -1095,7 +1104,7 @@ impl HttpHandler {
                                if is_file { "</abbr>" } else { "" },
                                if show_file_management_controls {
                                    DisplayThree("<td><a href class=\"delete_file_icon\" onclick=\"delete_onclick(arguments[0])\">Delete</a>",
-                                                if self.webdav {
+                                                if self.webdav >= WebDavLevel::MkColMoveOnly {
                                                     " <a href class=\"rename_icon\" onclick=\"rename_onclick(arguments[0])\">Rename</a>"
                                                 } else {
                                                     ""
@@ -1140,7 +1149,7 @@ impl HttpHandler {
                                                                        } else {
                                                                            ""
                                                                        },
-                                                                       if show_file_management_controls && self.webdav {
+                                                                       if show_file_management_controls && self.webdav >= WebDavLevel::MkColMoveOnly {
                                                                            "<tr id=\'new\"directory\'><td><a tabindex=\"-1\" href class=\"new_dir_icon\"></a></td>\
                                                                                                       <td colspan=3><a href>Create directory</a></td>\
                                                                                                       <td><a tabindex=\"-1\" href>&nbsp;</a></td></tr>"
@@ -1664,11 +1673,14 @@ pub fn try_ports<H: Handler + Copy>(hndlr: H, addr: IpAddr, from: u16, up_to: u1
 /// ```
 pub fn generate_tls_data(temp_dir: &(String, PathBuf)) -> Result<((String, PathBuf), String), Error> {
     fn err<M: fmt::Display>(which: bool, op: &'static str, more: M) -> Error {
-        Error(format!("{} {}: {}", op, if which {
-                "TLS key generation process"
-            } else {
-                "TLS identity generation process"
-            }, more))
+        Error(format!("{} {}: {}",
+                      op,
+                      if which {
+                          "TLS key generation process"
+                      } else {
+                          "TLS identity generation process"
+                      },
+                      more))
     }
     fn exit_err(which: bool, process: &mut Child, exitc: &ExitStatus) -> Error {
         let mut stdout = String::new();
diff --git a/src/ops/webdav.rs b/src/ops/webdav.rs
index 7afc261..07c4091 100644
--- a/src/ops/webdav.rs
+++ b/src/ops/webdav.rs
@@ -21,10 +21,10 @@ use xml::common::{TextPosition as XmlTextPosition, XmlVersion, Position};
 use xml::name::{OwnedName as OwnedXmlName, Name as XmlName};
 use iron::{status, IronResult, Response, Request};
 use iron::url::Url as GenericUrl;
+use std::collections::BTreeSet;
 use std::path::{PathBuf, Path};
 use std::fs::{self, Metadata};
 use self::super::HttpHandler;
-use itertools::Itertools;
 use std::{fmt, mem};
 use time::strptime;
 
@@ -819,15 +819,13 @@ fn intialise_xml_output() -> Result<XmlWriter<Vec<u8>>, XmlWError> {
 }
 
 fn namespaces_for_props<'n, N: 'n + BorrowXmlName<'n>, Ni: Iterator<Item = &'n N>>(elem_name: &str, props: Ni) -> XmlWEventStartElementBuilder {
-    let mut bldr = XmlWEvent::start_element(elem_name).ns(WEBDAV_XML_NAMESPACES[0].0, WEBDAV_XML_NAMESPACES[0].1);
-
-    for prop_namespace in props.map(|p| p.borrow_xml_name()).flat_map(|p| p.namespace).unique() {
-        if let Some((prefix, namespace)) = WEBDAV_XML_NAMESPACES[1..].iter().find(|(_, ns)| *ns == prop_namespace) {
-            bldr = bldr.ns(*prefix, *namespace);
-        }
-    }
-
-    bldr
+    props.map(|p| p.borrow_xml_name())
+        .flat_map(|p| p.namespace)
+        .collect::<BTreeSet<_>>()
+        .into_iter()
+        .flat_map(|prop_namespace| WEBDAV_XML_NAMESPACES[1..].iter().find(|(_, ns)| *ns == prop_namespace))
+        .fold(XmlWEvent::start_element(elem_name).ns(WEBDAV_XML_NAMESPACES[0].0, WEBDAV_XML_NAMESPACES[0].1),
+              |bldr, (prefix, namespace)| bldr.ns(*prefix, *namespace))
 }
 
 /// text/xml; charset=utf-8
diff --git a/src/options.rs b/src/options.rs
index 8f5440f..8075f05 100644
--- a/src/options.rs
+++ b/src/options.rs
@@ -13,16 +13,17 @@
 
 use clap::{AppSettings, ErrorKind as ClapErrorKind, Error as ClapError, Arg, App};
 use std::collections::btree_map::{BTreeMap, Entry as BTreeMapEntry};
+use self::super::ops::WebDavLevel;
 use std::ffi::{OsString, OsStr};
 use std::collections::BTreeSet;
 use std::env::{self, temp_dir};
 use std::num::NonZeroU64;
+use std::{cmp, str, fs};
 use std::path::PathBuf;
 use std::str::FromStr;
 use std::borrow::Cow;
 use iron::mime::Mime;
 use std::net::IpAddr;
-use std::{str, fs};
 use cidr::IpCidr;
 use blake3;
 
@@ -98,7 +99,7 @@ pub struct Options {
     /// Whether to colourise the log output. Default: `true`
     pub log_colour: bool,
     /// Whether to handle WebDAV requests. Default: false
-    pub webdav: bool,
+    pub webdav: WebDavLevel,
     /// Data for HTTPS, identity file and password. Default: `None`
     pub tls_data: Option<((String, PathBuf), String)>,
     /// Whether to generate a one-off certificate. Default: false
@@ -151,6 +152,7 @@ impl Options {
             .arg(Arg::from_usage("-Q --quiet-time 'Don't prefix logs with the timestamp'"))
             .arg(Arg::from_usage("-c --no-colour 'Don't colourise the log output'"))
             .arg(Arg::from_usage("-d --webdav 'Handle WebDAV requests. Default: false'"))
+            .arg(Arg::from_usage("-D --convenient-webdav 'Allow WebDAV MKCOL and MOVE only. Default: false'"))
             .arg(Arg::from_usage("--ssl [TLS_IDENTITY] 'Data for HTTPS, identity file. Password in HTTP_SSL_PASS env var, otherwise empty'")
                 .validator(Options::identity_validator))
             .arg(Arg::from_usage("--gen-ssl 'Generate a one-off TLS certificate'").conflicts_with("ssl"))
@@ -259,7 +261,16 @@ impl Options {
             loglevel: matches.occurrences_of("quiet").into(),
             log_time: !matches.is_present("quiet-time"),
             log_colour: !matches.is_present("no-colour"),
-            webdav: matches.is_present("webdav"),
+            webdav: cmp::max(if matches.is_present("webdav") {
+                            WebDavLevel::All
+                        } else {
+                            WebDavLevel::No
+                        },
+                        if matches.is_present("convenient-webdav") {
+                            WebDavLevel::MkColMoveOnly
+                        } else {
+                            WebDavLevel::No
+                        }),
             tls_data: matches.value_of("ssl").map(|id| ((id.to_string(), fs::canonicalize(id).unwrap()), env::var("HTTP_SSL_PASS").unwrap_or_default())),
             generate_tls: matches.is_present("gen-ssl"),
             path_auth_data: path_auth_data,
diff --git a/src/util/mod.rs b/src/util/mod.rs
index c318b25..01bba9a 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -20,7 +20,7 @@ use iron::headers::{HeaderFormat, UserAgent, Header};
 use xml::name::{OwnedName as OwnedXmlName, Name as XmlName};
 use iron::error::{HttpResult as HyperResult, HttpError as HyperError};
 use iron::mime::{Mime, SubLevel as MimeSubLevel, TopLevel as MimeTopLevel};
-use std::io::{ErrorKind as IoErrorKind, BufReader, BufRead, Result as IoResult, Error as IoError, Write};
+use std::io::{ErrorKind as IoErrorKind, Result as IoResult, Error as IoError, Write, Read};
 
 pub use self::os::*;
 pub use self::webdav::*;
@@ -288,7 +288,33 @@ pub fn file_binary<P: AsRef<Path>>(path: P) -> bool {
 
 fn file_binary_impl(path: &Path) -> bool {
     path.metadata()
-        .map(|m| is_device(&m.file_type()) || File::open(path).and_then(|f| BufReader::new(f).read_line(&mut String::new())).is_err())
+        .map(|m| {
+            is_device(&m.file_type()) ||
+            File::open(path)
+                .map_err(|_| ())
+                .and_then(|mut f| {
+                    #[allow(invalid_value)]
+                    let mut buf: [u8; 2048] = unsafe { mem::MaybeUninit::uninit().assume_init() }; // 2k matches LINE_MAX
+                    let mut remaining = &mut buf[..];
+                    while let Ok(rd) = f.read(remaining) {
+                        if rd == 0 || remaining[0..rd].contains(&b'\0') {
+                            return Err(());
+                        }
+                        if let Some(idx) = remaining[0..rd].iter().position(|&b| b== b'\n') {
+                            remaining = &mut remaining[idx..];
+                            let remaining_len = remaining.len();
+                            let _ = remaining;
+                            return str::from_utf8(&buf[0..buf.len() - remaining_len]).map(|_|()).map_err(|_|());
+                        }
+                        remaining = &mut remaining[rd..];
+                        if remaining.len() == 0 {
+                            break;
+                        }
+                    }
+                    Err(())
+                })
+                .is_err()
+        })
         .unwrap_or(true)
 }
 


Test patch: