Repository: thecoshman/http (language: rust)

Issue:
serve .html on extensionless paths?
I'm using this to test a site locally, and (as many webservers do) going to e.g. `/some-file` serves `/some-file.html` whereas this crate serves a 404

Is there an option for this that I missed? and if not, could one be added?


Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 3976745..6a63c61 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,10 +9,10 @@ categories = ["network-programming", "web-programming::http-server"]
 license = "MIT"
 build = "build.rs"
 # Remember to also update in appveyor.yml
-version = "1.9.3"
+version = "1.10.0"
 # Remember to also update in http.md
 authors = ["thecoshman <rust@thecoshman.com>",
-           "nabijaczleweli <nabijaczleweli@gmail.com>",
+           "nabijaczleweli <nabijaczleweli@nabijaczleweli.xyz>",
            "pheki",
            "Adrian Herath <adrianisuru@gmail.com>"]
 
diff --git a/README.md b/README.md
index 0f66d33..27555c9 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,8 @@ See [the manpage](http.md) for full list.
   * [x] Full Range header support
   * [x] Hosting with an <sub>(optional)</sub> optionally autogenerated TLS certificate
   * [x] Arbitrarily nested username/password authentication
+  * [x] Per-request bandwidth cap
+  * [x] Per-extension-overridable MIME-types with reasonable guesses
   * [x] [WebDAV/RFC2518](https://tools.ietf.org/html/rfc2518) support, tested with the Linux [`davfs2`](http://savannah.nongnu.org/projects/davfs2) helper, Windows network filesystem support (out-of-box), and the Total Commander [WebDAV plugin](https://www.ghisler.com/plugins.htm)
   * [x] [RFSAPI](https://github.com/nabijaczleweli/rfsapi-rs) support ([format spec](https://rawcdn.githack.com/nabijaczleweli/rfsapi-rs/doc/rfsapi/index.html#format-spec)) (explorable from commandline with [D'Oh](https://github.com/thecoshman/doh))
 
diff --git a/appveyor.yml b/appveyor.yml
index 63d7351..000ab6d 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 1.9.3-{build}
+version: 1.10.0-{build}
 
 skip_tags: false
 
@@ -9,7 +9,12 @@ clone_folder: C:\http
 
 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
+  - pacman --noconfirm -U C:\msys2-keyring.txz
   - bash -lc "pacman --needed --noconfirm -Sy pacman-mirrors"
+  - bash -lc "pacman --noconfirm -Sy pacman"
+  - bash -lc "pacman --noconfirm -Su"
   - bash -lc "pacman --noconfirm -Sy"
   - bash -lc "pacman --noconfirm -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-nsis unzip"
   -
@@ -23,21 +28,21 @@ build: off
 build_script:
   - git submodule update --init --recursive
   - cargo build --verbose --release
-  - cp target\release\http.exe http-v1.9.3.exe
-  - strip --strip-all --remove-section=.comment --remove-section=.note http-v1.9.3.exe
-  - makensis -DHTTP_VERSION=v1.9.3 install.nsi
+  - cp target\release\http.exe http-v1.10.0.exe
+  - strip --strip-all --remove-section=.comment --remove-section=.note http-v1.10.0.exe
+  - makensis -DHTTP_VERSION=v1.10.0 install.nsi
 
 test: off
 test_script:
   - cargo test --verbose --release
 
 artifacts:
-  - path: http-v1.9.3.exe
-  - path: http v1.9.3 installer.exe
+  - path: http-v1.10.0.exe
+  - path: http v1.10.0 installer.exe
 
 deploy:
   provider: GitHub
-  artifact: /http.*v1.9.3.*\.exe/
+  artifact: /http.*v1.10.0.*\.exe/
   auth_token:
     secure: ZTXvCrv9y01s7Hd60w8W7NaouPnPoaw9YJt9WhWQ2Pep8HLvCikt9Exjkz8SGP9P
   on:
diff --git a/http.md b/http.md
index bce35e9..8d85a54 100644
--- a/http.md
+++ b/http.md
@@ -122,6 +122,26 @@ pass parameters like what port to use.
 
     Can be specified any amount of times. Default: none.
 
+  -m --mime-type [EXTENSION:MIME-TYPE]
+
+    Return MIME-TYPE for files with EXTENSION.
+
+    If EXTENSION is the empty string, return that MIME-TYPE for files with no extension.
+
+    The default MIME type is as returned by the mime_guess crate, if any,
+    otherwise "application/octet-stream" for binary files or "text/plain".
+
+    Can be specified any amount of times. Default: none.
+
+  --request-bandwidth [BYTES]
+
+    Limit the band for each request to BYTES/second wide.
+
+    Can be suffixed with [KMGTPE] binary prefixes or [kmgtpe] SI prefixes.
+    Zero disables capping.
+
+    Default: 0.
+
   -s --no-follow-symlinks
 
     Don't follow symlinks when requesting file access.
@@ -162,6 +182,13 @@ pass parameters like what port to use.
 
     This is false by default because it's useful for reducing bandwidth usage.
 
+  -x --strip-extensions
+
+    Allow stripping index extentions from served paths:
+    a request to /file might get served by /file.[s]htm[l].
+
+    This is false by default
+
   -q --quiet...
 
     Suppress increasing amounts of output.
@@ -172,6 +199,10 @@ pass parameters like what port to use.
       N >= 2 – suppress startup except for auth data, if present
       N >= 3 – suppress all startup messages
 
+  -c --no-colour
+
+    Don't colourise log output.
+
   -d --webdav
 
     Handle WebDAV requests.
@@ -380,10 +411,33 @@ pass parameters like what port to use.
       [2020-02-17 17:49:12] 192.168.1.109:1403 for UwU was served directory listing for \\?\P:\Rust\http
       [2020-02-17 17:49:29] 93.184.216.34:1397 was served directory listing for \\?\P:\Rust\http
 
+  `http --mime-type css:text/css;charset=utf-8 --mime-type :image/jpeg`
+
+    As in the first example, but send .css with the charset=utf8 attribute and
+    treat files with no extension as JPEGs.
+
+    Then the output will be as follows:
+      Hosting "." on port 8000 without TLS and no authentication...
+      Serving files with no extension as image/jpeg.
+      Serving files with .css extension as text/css; charset=utf-8.
+      Ctrl-C to stop.
+
+      [2020-07-20 12:32:24] 127.0.0.1:47916 was served file \\?\S:\Rust-target\doc\main.css as text/css; charset=utf-8
+      [2020-07-20 12:32:25] 127.0.0.1:2803 was served file \\?\P:\121D800E\http\DSC_6505 as image/jpeg
+
+  `http --request-bandwidth 4K`
+
+    As in the first example, but limit each request to 4096 bytes per second.
+
+    Example output change:
+      Hosting "." on port 8000 without TLS and no authentication...
+      Requests limited to 4096B/s.
+      Ctrl-C to stop.
+
 ## AUTHOR
 
 Written by thecoshman &lt;<rust@thecoshman.com>&gt;,
-           nabijaczleweli &lt;<nabijaczleweli@gmail.com>&gt;,
+           nabijaczleweli &lt;<nabijaczleweli@nabijaczleweli.xyz>&gt;,
            pheki,
        and Adrian Herath &lt;<adrianisuru@gmail.com>&gt;.
 
diff --git a/src/main.rs b/src/main.rs
index d18838e..124533c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -76,10 +76,14 @@ fn result_main() -> Result<(), Error> {
         opts.path_auth_data.insert(path, Some(ops::generate_auth_data()));
     }
 
+    let handler = ops::SimpleChain {
+        handler: ops::HttpHandler::new(&opts),
+        after: opts.request_bandwidth.map(ops::LimitBandwidthMiddleware::new),
+    };
     let mut responder = if let Some(p) = opts.port {
         if let Some(&((ref id, _), ref pw)) = opts.tls_data.as_ref() {
-                Iron::new(ops::HttpHandler::new(&opts)).https((opts.bind_address, p),
-                                                              NativeTlsServer::new(id, pw).map_err(|err| {
+                Iron::new(handler).https((opts.bind_address, p),
+                                         NativeTlsServer::new(id, pw).map_err(|err| {
                         Error {
                             desc: "TLS certificate",
                             op: "open",
@@ -87,7 +91,7 @@ fn result_main() -> Result<(), Error> {
                         }
                     })?)
             } else {
-                Iron::new(ops::HttpHandler::new(&opts)).http((opts.bind_address, p))
+                Iron::new(handler).http((opts.bind_address, p))
             }
             .map_err(|_| {
                 Error {
@@ -97,16 +101,14 @@ fn result_main() -> Result<(), Error> {
                 }
             })
     } else {
-        ops::try_ports(ops::HttpHandler::new(&opts),
-                       opts.bind_address,
-                       util::PORT_SCAN_LOWEST,
-                       util::PORT_SCAN_HIGHEST,
-                       &opts.tls_data)
+        ops::try_ports(handler, opts.bind_address, util::PORT_SCAN_LOWEST, util::PORT_SCAN_HIGHEST, &opts.tls_data)
     }?;
 
     if opts.loglevel < options::LogLevel::NoStartup {
-        print!("{}Hosting \"{}\" on port {}",
-               trivial_colours::Reset,
+        if opts.log_colour {
+            print!("{}", trivial_colours::Reset);
+        }
+        print!("Hosting \"{}\" on port {}",
                opts.hosted_directory.0,
                responder.socket.port());
         if responder.socket.ip() != IpAddr::from([0, 0, 0, 0]) {
@@ -125,7 +127,18 @@ fn result_main() -> Result<(), Error> {
         }
         println!("...");
 
-        if !opts.proxies.is_empty()  {
+        if let Some(band) = opts.request_bandwidth {
+            println!("Requests limited to {}B/s.", band);
+        }
+
+        for (ext, mime_type) in opts.mime_type_overrides {
+            match &ext[..] {
+                "" => println!("Serving files with no extension as {}.", mime_type),
+                _ => println!("Serving files with .{} extension as {}.", ext, mime_type),
+            }
+        }
+
+        if !opts.proxies.is_empty() {
             println!("Trusted proxies:");
 
             let mut out = TabWriter::new(stdout());
@@ -173,7 +186,7 @@ fn result_main() -> Result<(), Error> {
     responder.close().unwrap();
 
     // This is necessary because the server isn't Drop::drop()ped when the responder is
-    ops::HttpHandler::clean_temp_dirs(&opts.temp_directory, opts.loglevel);
+    ops::HttpHandler::clean_temp_dirs(&opts.temp_directory, opts.loglevel, opts.log_colour);
 
     Ok(())
 }
diff --git a/src/ops/bandwidth.rs b/src/ops/bandwidth.rs
new file mode 100644
index 0000000..c82117e
--- /dev/null
+++ b/src/ops/bandwidth.rs
@@ -0,0 +1,102 @@
+use iron::{AfterMiddleware, IronResult, Response, Handler, Request};
+use std::num::{NonZeroUsize, NonZeroU64};
+use std::io::{Result as IoResult, Write};
+use iron::response::WriteBody;
+use std::time::Duration;
+use std::thread;
+
+
+pub const DEFAULT_SLEEP: Duration = Duration::from_millis(1);
+
+
+#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+pub struct SimpleChain<H: Handler, Am: AfterMiddleware> {
+    pub handler: H,
+    pub after: Option<Am>,
+}
+
+impl<H: Handler, Am: AfterMiddleware> Handler for SimpleChain<H, Am> {
+    fn handle(&self, req: &mut Request) -> IronResult<Response> {
+        let resp = self.handler.handle(req)?;
+        match self.after.as_ref() {
+            Some(am) => am.after(req, resp),
+            None => Ok(resp),
+        }
+    }
+}
+
+
+
+#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+pub struct LimitBandwidthMiddleware {
+    pub bandwidth: NonZeroU64,
+}
+
+impl LimitBandwidthMiddleware {
+    pub fn new(bandwidth: NonZeroU64) -> LimitBandwidthMiddleware {
+        LimitBandwidthMiddleware { bandwidth: bandwidth }
+    }
+}
+
+impl AfterMiddleware for LimitBandwidthMiddleware {
+    fn after(&self, _: &mut Request, res: Response) -> IronResult<Response> {
+        Ok(Response {
+            body: res.body.map(|body| {
+                Box::new(LimitBandwidthWriteBody {
+                    bandwidth: self.bandwidth,
+                    underlying: body,
+                }) as Box<dyn WriteBody>
+            }),
+            ..res
+        })
+    }
+}
+
+
+struct LimitBandwidthWriteBody {
+    bandwidth: NonZeroU64,
+    underlying: Box<dyn WriteBody>,
+}
+
+impl WriteBody for LimitBandwidthWriteBody {
+    fn write_body(&mut self, res: &mut dyn Write) -> IoResult<()> {
+        self.underlying.write_body(&mut LimitBandwidthWriter::new(self.bandwidth, res))
+    }
+}
+
+
+struct LimitBandwidthWriter<'o> {
+    chunk_len: NonZeroUsize,
+    output: &'o mut dyn Write,
+}
+
+impl<'o> LimitBandwidthWriter<'o> {
+    fn new(bandwidth: NonZeroU64, output: &'o mut dyn Write) -> LimitBandwidthWriter<'o> {
+        LimitBandwidthWriter {
+            // bandwidth / (1000 / DEFAULT_SLEEP_MS)
+            chunk_len: NonZeroUsize::new(bandwidth.get() as usize * DEFAULT_SLEEP.as_millis() as usize / 1000).unwrap_or(NonZeroUsize::new(1).unwrap()),
+            output: output,
+        }
+    }
+}
+
+impl<'o> Write for LimitBandwidthWriter<'o> {
+    fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
+        self.write_all(buf)?;
+        Ok(buf.len())
+    }
+
+    fn flush(&mut self) -> IoResult<()> {
+        self.output.flush()
+    }
+
+    fn write_all(&mut self, buf: &[u8]) -> IoResult<()> {
+        for chunk in buf.chunks(self.chunk_len.get()) {
+            self.output.write_all(chunk)?;
+            self.output.flush()?;
+            thread::sleep(DEFAULT_SLEEP);
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/ops/mod.rs b/src/ops/mod.rs
index 037250d..1221f96 100644
--- a/src/ops/mod.rs
+++ b/src/ops/mod.rs
@@ -1,21 +1,21 @@
 use md6;
 use std::fmt;
 use serde_json;
+use std::ffi::OsStr;
 use std::borrow::Cow;
 use std::net::IpAddr;
 use serde::Serialize;
 use unicase::UniCase;
-use iron::mime::Mime;
 use std::sync::RwLock;
 use lazysort::SortedBy;
-use std::path::PathBuf;
 use cidr::{Cidr, IpCidr};
 use std::fs::{self, File};
 use std::default::Default;
 use rand::{Rng, thread_rng};
 use iron::modifiers::Header;
+use std::path::{PathBuf, Path};
 use iron::url::Url as GenericUrl;
-use mime_guess::guess_mime_type_opt;
+use mime_guess::get_mime_type_opt;
 use hyper_native_tls::NativeTlsServer;
 use std::collections::{BTreeMap, HashMap};
 use self::super::{LogLevel, Options, Error};
@@ -23,6 +23,7 @@ use std::process::{ExitStatus, Command, Child, Stdio};
 use rfsapi::{RawFsApiHeader, FilesetData, RawFileData};
 use rand::distributions::uniform::Uniform as UniformDistribution;
 use rand::distributions::Alphanumeric as AlphanumericDistribution;
+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,
@@ -33,46 +34,78 @@ use self::super::util::{WwwAuthenticate, DisplayThree, CommaList, Spaces, Dav, u
 
 
 macro_rules! log {
-    ($do_log:expr, $fmt:expr) => {
+    ($logcfg:expr, $fmt:expr) => {
         use time::now;
         use trivial_colours::{Reset as CReset, Colour as C};
 
-        if $do_log {
-            print!("{}[{}]{} ", C::Cyan, now().strftime("%F %T").unwrap(), CReset);
-            println!(concat!($fmt, "{black:.0}{red:.0}{green:.0}{yellow:.0}{blue:.0}{magenta:.0}{cyan:.0}{white:.0}{reset:.0}"),
-                     black = C::Black,
-                     red = C::Red,
-                     green = C::Green,
-                     yellow = C::Yellow,
-                     blue = C::Blue,
-                     magenta = C::Magenta,
-                     cyan = C::Cyan,
-                     white = C::White,
-                     reset = CReset);
+        if $logcfg.0 {
+            if $logcfg.1 {
+                print!("{}[{}]{} ", C::Cyan, now().strftime("%F %T").unwrap(), CReset);
+                println!(concat!($fmt, "{black:.0}{red:.0}{green:.0}{yellow:.0}{blue:.0}{magenta:.0}{cyan:.0}{white:.0}{reset:.0}"),
+                         black = C::Black,
+                         red = C::Red,
+                         green = C::Green,
+                         yellow = C::Yellow,
+                         blue = C::Blue,
+                         magenta = C::Magenta,
+                         cyan = C::Cyan,
+                         white = C::White,
+                         reset = CReset);
+            } else {
+                print!("[{}] ", now().strftime("%F %T").unwrap());
+                println!(concat!($fmt, "{black:.0}{red:.0}{green:.0}{yellow:.0}{blue:.0}{magenta:.0}{cyan:.0}{white:.0}{reset:.0}"),
+                         black = "",
+                         red = "",
+                         green = "",
+                         yellow = "",
+                         blue = "",
+                         magenta = "",
+                         cyan = "",
+                         white = "",
+                         reset = "");
+            }
         }
     };
-    ($do_log:expr, $fmt:expr, $($arg:tt)*) => {
+    ($logcfg:expr, $fmt:expr, $($arg:tt)*) => {
         use time::now;
         use trivial_colours::{Reset as CReset, Colour as C};
 
-        if $do_log {
-            print!("{}[{}]{} ", C::Cyan, now().strftime("%F %T").unwrap(), CReset);
-            println!(concat!($fmt, "{black:.0}{red:.0}{green:.0}{yellow:.0}{blue:.0}{magenta:.0}{cyan:.0}{white:.0}{reset:.0}"),
-                     $($arg)*,
-                     black = C::Black,
-                     red = C::Red,
-                     green = C::Green,
-                     yellow = C::Yellow,
-                     blue = C::Blue,
-                     magenta = C::Magenta,
-                     cyan = C::Cyan,
-                     white = C::White,
-                     reset = CReset);
+        if $logcfg.0 {
+            if $logcfg.1 {
+                print!("{}[{}]{} ", C::Cyan, now().strftime("%F %T").unwrap(), CReset);
+                println!(concat!($fmt, "{black:.0}{red:.0}{green:.0}{yellow:.0}{blue:.0}{magenta:.0}{cyan:.0}{white:.0}{reset:.0}"),
+                         $($arg)*,
+                         black = C::Black,
+                         red = C::Red,
+                         green = C::Green,
+                         yellow = C::Yellow,
+                         blue = C::Blue,
+                         magenta = C::Magenta,
+                         cyan = C::Cyan,
+                         white = C::White,
+                         reset = CReset);
+            } else {
+                print!("[{}] ", now().strftime("%F %T").unwrap());
+                println!(concat!($fmt, "{black:.0}{red:.0}{green:.0}{yellow:.0}{blue:.0}{magenta:.0}{cyan:.0}{white:.0}{reset:.0}"),
+                         $($arg)*,
+                         black = "",
+                         red = "",
+                         green = "",
+                         yellow = "",
+                         blue = "",
+                         magenta = "",
+                         cyan = "",
+                         white = "",
+                         reset = "");
+            }
         }
     };
 }
 
 mod webdav;
+mod bandwidth;
+
+pub use self::bandwidth::{LimitBandwidthMiddleware, SimpleChain};
 
 
 // TODO: ideally this String here would be Encoding instead but hyper is bad
@@ -83,13 +116,16 @@ pub struct HttpHandler {
     pub follow_symlinks: bool,
     pub sandbox_symlinks: bool,
     pub check_indices: bool,
-    pub log: bool,
+    pub strip_extensions: bool,
+    /// (at all, log_colour)
+    pub log: (bool, bool),
     pub webdav: bool,
     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)>,
     pub encoded_temp_dir: Option<(String, PathBuf)>,
     pub proxies: BTreeMap<IpCidr, String>,
+    pub mime_type_overrides: BTreeMap<String, Mime>,
     cache_gen: RwLock<CacheT<Vec<u8>>>,
     cache_fs: RwLock<CacheT<(PathBuf, bool)>>,
 }
@@ -118,7 +154,8 @@ impl HttpHandler {
             follow_symlinks: opts.follow_symlinks,
             sandbox_symlinks: opts.sandbox_symlinks,
             check_indices: opts.check_indices,
-            log: opts.loglevel < LogLevel::NoServeStatus,
+            strip_extensions: opts.strip_extensions,
+            log: (opts.loglevel < LogLevel::NoServeStatus, opts.log_colour),
             webdav: opts.webdav,
             global_auth_data: global_auth_data,
             path_auth_data: path_auth_data,
@@ -127,13 +164,16 @@ impl HttpHandler {
             cache_gen: Default::default(),
             cache_fs: Default::default(),
             proxies: opts.proxies.clone(),
+            mime_type_overrides: opts.mime_type_overrides.clone(),
         }
     }
 
-    pub fn clean_temp_dirs(temp_dir: &(String, PathBuf), loglevel: LogLevel) {
+    pub fn clean_temp_dirs(temp_dir: &(String, PathBuf), loglevel: LogLevel, log_colour: bool) {
         for (temp_name, temp_dir) in ["writes", "encoded", "tls"].iter().flat_map(|tn| HttpHandler::temp_subdir(temp_dir, true, tn)) {
             if temp_dir.exists() && fs::remove_dir_all(&temp_dir).is_ok() {
-                log!(loglevel < LogLevel::NoServeStatus, "Deleted temp dir {magenta}{}{reset}", temp_name);
+                log!((loglevel < LogLevel::NoServeStatus, log_colour),
+                     "Deleted temp dir {magenta}{}{reset}",
+                     temp_name);
             }
         }
     }
@@ -287,12 +327,18 @@ impl HttpHandler {
     }
 
     fn handle_get(&self, req: &mut Request) -> IronResult<Response> {
-        let (req_p, symlink, url_err) = self.parse_requested_path(req);
+        let (mut req_p, symlink, url_err) = self.parse_requested_path(req);
 
         if url_err {
             return self.handle_invalid_url(req, "<p>Percent-encoding decoded to invalid UTF-8.</p>");
         }
 
+        if !req_p.exists() && req_p.extension().is_none() && self.strip_extensions {
+            if let Some(rp) = INDEX_EXTENSIONS.iter().map(|ext| req_p.with_extension(ext)).find(|rp| rp.exists()) {
+                req_p = rp;
+            }
+        }
+
         if !req_p.exists() || (symlink && !self.follow_symlinks) ||
            (symlink && self.follow_symlinks && self.sandbox_symlinks && !is_descendant_of(&req_p, &self.hosted_directory.1)) {
             return self.handle_nonexistent(req, req_p);
@@ -396,11 +442,7 @@ impl HttpHandler {
     }
 
     fn handle_get_file_closed_range(&self, req: &mut Request, req_p: PathBuf, from: u64, to: u64) -> IronResult<Response> {
-        let mime_type = guess_mime_type_opt(&req_p).unwrap_or_else(|| if file_binary(&req_p) {
-            "application/octet-stream".parse().unwrap()
-        } else {
-            "text/plain".parse().unwrap()
-        });
+        let mime_type = self.guess_mime_type(&req_p);
         log!(self.log,
              "{} was served byte range {}-{} of file {magenta}{}{reset} as {blue}{}{reset}",
              self.remote_addresses(&req),
@@ -427,11 +469,7 @@ impl HttpHandler {
     }
 
     fn handle_get_file_right_opened_range(&self, req: &mut Request, req_p: PathBuf, from: u64) -> IronResult<Response> {
-        let mime_type = guess_mime_type_opt(&req_p).unwrap_or_else(|| if file_binary(&req_p) {
-            "application/octet-stream".parse().unwrap()
-        } else {
-            "text/plain".parse().unwrap()
-        });
+        let mime_type = self.guess_mime_type(&req_p);
         log!(self.log,
              "{} was served file {magenta}{}{reset} from byte {} as {blue}{}{reset}",
              self.remote_addresses(&req),
@@ -444,11 +482,7 @@ impl HttpHandler {
     }
 
     fn handle_get_file_left_opened_range(&self, req: &mut Request, req_p: PathBuf, from: u64) -> IronResult<Response> {
-        let mime_type = guess_mime_type_opt(&req_p).unwrap_or_else(|| if file_binary(&req_p) {
-            "application/octet-stream".parse().unwrap()
-        } else {
-            "text/plain".parse().unwrap()
-        });
+        let mime_type = self.guess_mime_type(&req_p);
         log!(self.log,
              "{} was served last {} bytes of file {magenta}{}{reset} as {blue}{}{reset}",
              self.remote_addresses(&req),
@@ -491,11 +525,7 @@ impl HttpHandler {
     }
 
     fn handle_get_file_empty_range(&self, req: &mut Request, req_p: PathBuf, from: u64, to: u64) -> IronResult<Response> {
-        let mime_type = guess_mime_type_opt(&req_p).unwrap_or_else(|| if file_binary(&req_p) {
-            "application/octet-stream".parse().unwrap()
-        } else {
-            "text/plain".parse().unwrap()
-        });
+        let mime_type = self.guess_mime_type(&req_p);
         log!(self.log,
              "{} was served an empty range from file {magenta}{}{reset} as {blue}{}{reset}",
              self.remote_addresses(&req),
@@ -514,11 +544,7 @@ impl HttpHandler {
     }
 
     fn handle_get_file(&self, req: &mut Request, req_p: PathBuf) -> IronResult<Response> {
-        let mime_type = guess_mime_type_opt(&req_p).unwrap_or_else(|| if file_binary(&req_p) {
-            "application/octet-stream".parse().unwrap()
-        } else {
-            "text/plain".parse().unwrap()
-        });
+        let mime_type = self.guess_mime_type(&req_p);
         log!(self.log,
              "{} was served file {magenta}{}{reset} as {blue}{}{reset}",
              self.remote_addresses(&req),
@@ -1276,8 +1302,22 @@ impl HttpHandler {
         AddressWriter {
             request: req,
             proxies: &self.proxies,
+            log: self.log,
         }
     }
+
+    fn guess_mime_type(&self, req_p: &Path) -> Mime {
+        // Based on mime_guess::guess_mime_type_opt(); that one does to_str() instead of to_string_lossy()
+        let ext = req_p.extension().map(OsStr::to_string_lossy).unwrap_or("".into());
+
+        (self.mime_type_overrides.get(&*ext).cloned())
+            .or_else(|| get_mime_type_opt(&*ext))
+            .unwrap_or_else(|| if file_binary(req_p) {
+                Mime(MimeTopLevel::Application, MimeSubLevel::OctetStream, Default::default()) // "application/octet-stream"
+            } else {
+                Mime(MimeTopLevel::Text, MimeSubLevel::Plain, Default::default()) // "text/plain"
+            })
+    }
 }
 
 impl Clone for HttpHandler {
@@ -1287,6 +1327,7 @@ impl Clone for HttpHandler {
             follow_symlinks: self.follow_symlinks,
             sandbox_symlinks: self.sandbox_symlinks,
             check_indices: self.check_indices,
+            strip_extensions: self.strip_extensions,
             log: self.log,
             webdav: self.webdav,
             global_auth_data: self.global_auth_data.clone(),
@@ -1294,6 +1335,7 @@ impl Clone for HttpHandler {
             writes_temp_dir: self.writes_temp_dir.clone(),
             encoded_temp_dir: self.encoded_temp_dir.clone(),
             proxies: self.proxies.clone(),
+            mime_type_overrides: self.mime_type_overrides.clone(),
             cache_gen: Default::default(),
             cache_fs: Default::default(),
         }
@@ -1304,19 +1346,29 @@ impl Clone for HttpHandler {
 pub struct AddressWriter<'r, 'p, 'ra, 'rb: 'ra> {
     pub request: &'r Request<'ra, 'rb>,
     pub proxies: &'p BTreeMap<IpCidr, String>,
+    /// (at all, log_colour)
+    pub log: (bool, bool),
 }
 
 impl<'r, 'p, 'ra, 'rb: 'ra> fmt::Display for AddressWriter<'r, 'p, 'ra, 'rb> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         use trivial_colours::{Reset as CReset, Colour as C};
 
-        write!(f, "{green}{}{reset}", self.request.remote_addr, green = C::Green, reset = CReset)?;
+        if self.log.1 {
+            write!(f, "{green}{}{reset}", self.request.remote_addr, green = C::Green, reset = CReset)?;
+        } else {
+            write!(f, "{}", self.request.remote_addr)?;
+        }
 
         for (network, header) in self.proxies {
             if network.contains(&self.request.remote_addr.ip()) {
                 if let Some(saddrs) = self.request.headers.get_raw(header) {
                     for saddr in saddrs {
-                        write!(f, " for {green}{}{reset}", String::from_utf8_lossy(saddr), green = C::Green, reset = CReset)?;
+                        if self.log.1 {
+                            write!(f, " for {green}{}{reset}", String::from_utf8_lossy(saddr), green = C::Green, reset = CReset)?;
+                        } else {
+                            write!(f, " for {}", String::from_utf8_lossy(saddr))?;
+                        }
                     }
                 }
             }
diff --git a/src/ops/webdav.rs b/src/ops/webdav.rs
index 3304160..7ad9864 100644
--- a/src/ops/webdav.rs
+++ b/src/ops/webdav.rs
@@ -7,8 +7,8 @@
 
 
 use self::super::super::util::{BorrowXmlName, Destination, CommaList, Overwrite, Depth, win32_file_attributes, file_time_accessed, file_time_modified,
-                               file_time_created, client_microsoft, is_actually_file, is_descendant_of, file_executable, html_response, file_length,
-                               file_binary, copy_dir, WEBDAV_ALLPROP_PROPERTIES_NON_WINDOWS, WEBDAV_ALLPROP_PROPERTIES_WINDOWS, WEBDAV_XML_NAMESPACE_MICROSOFT,
+                               file_time_created, client_microsoft, is_actually_file, is_descendant_of, file_executable, html_response, file_length, copy_dir,
+                               WEBDAV_ALLPROP_PROPERTIES_NON_WINDOWS, WEBDAV_ALLPROP_PROPERTIES_WINDOWS, WEBDAV_XML_NAMESPACE_MICROSOFT,
                                WEBDAV_XML_NAMESPACE_APACHE, WEBDAV_PROPNAME_PROPERTIES, WEBDAV_XML_NAMESPACE_DAV, WEBDAV_XML_NAMESPACES, ERROR_HTML};
 use std::io::{ErrorKind as IoErrorKind, Result as IoResult, Error as IoError, Write, Read};
 use xml::reader::{EventReader as XmlReader, XmlEvent as XmlREvent, Error as XmlRError};
@@ -18,7 +18,6 @@ use xml::writer::events::StartElementBuilder as XmlWEventStartElementBuilder;
 use xml::common::{TextPosition as XmlTextPosition, XmlVersion, Position};
 use xml::name::{OwnedName as OwnedXmlName, Name as XmlName};
 use iron::{status, IronResult, Response, Request};
-use mime_guess::guess_mime_type_opt;
 use iron::url::Url as GenericUrl;
 use std::path::{PathBuf, Path};
 use std::fs::{self, Metadata};
@@ -118,7 +117,7 @@ impl HttpHandler {
         out.write(namespaces_for_props("D:multistatus", props.iter().flat_map(|pp| pp.iter())))?;
 
         let meta = path.metadata().expect("Failed to get requested file metadata");
-        handle_propfind_path(&mut out, &url, &path, &meta, props, just_names)?;
+        self.handle_propfind_path(&mut out, &url, &path, &meta, props, just_names)?;
 
         if meta.is_dir() {
             if let Some(ir) = self.handle_webdav_propfind_path_recursive(req, &mut out, url, &path, props, just_names, depth)? {
@@ -156,12 +155,12 @@ impl HttpHandler {
 
                 if !(!path.exists() || (symlink && !self.follow_symlinks) ||
                      (symlink && self.follow_symlinks && self.sandbox_symlinks && !is_descendant_of(&path, &self.hosted_directory.1))) {
-                    handle_propfind_path(out,
-                                         &url,
-                                         &path,
-                                         &path.metadata().expect("Failed to get requested file metadata"),
-                                         props,
-                                         just_names)?;
+                    self.handle_propfind_path(out,
+                                              &url,
+                                              &path,
+                                              &path.metadata().expect("Failed to get requested file metadata"),
+                                              props,
+                                              just_names)?;
                     self.handle_webdav_propfind_path_recursive(req, out, url, &path, props, just_names, next_depth)?;
                 }
             }
@@ -370,6 +369,150 @@ impl HttpHandler {
             }
         }
     }
+
+    /// Adapted from
+    /// https://github.com/tylerwhall/hyperdav-server/blob/415f512ac030478593ad389a3267aeed7441d826/src/lib.rs#L306
+    fn handle_propfind_path<'n, W: Write, N: BorrowXmlName<'n>>(&self, out: &mut XmlWriter<W>, url: &str, path: &Path, meta: &Metadata, props: &[&'n [N]],
+                                                                just_names: bool)
+                                                                -> Result<(), XmlWError> {
+        out.write(XmlWEvent::start_element("D:response"))?;
+
+        out.write(XmlWEvent::start_element("D:href"))?;
+        out.write(XmlWEvent::characters(url))?;
+        out.write(XmlWEvent::end_element())?; // href
+
+        let prop_count = props.iter().map(|pp| pp.len()).sum();
+        let mut failed_props = Vec::with_capacity(prop_count);
+        out.write(XmlWEvent::start_element("D:propstat"))?;
+        out.write(XmlWEvent::start_element("D:prop"))?;
+        for prop in props.iter().flat_map(|pp| pp.iter()) {
+            let prop = prop.borrow_xml_name();
+
+            let mut write_name = false;
+            if !just_names && !self.handle_prop_path(out, path, meta, prop)? {
+                failed_props.push(prop);
+                write_name = true;
+            }
+
+            if just_names || write_name {
+                start_client_prop_element(out, prop)?;
+                out.write(XmlWEvent::end_element())?;
+            }
+        }
+        out.write(XmlWEvent::end_element())?; // prop
+        out.write(XmlWEvent::start_element("D:status"))?;
+        if failed_props.len() >= prop_count {
+            // If they all failed, make this a failure response and return
+            out.write(XmlWEvent::characters("HTTP/1.1 404 Not Found"))?;
+            out.write(XmlWEvent::end_element())?; // status
+            out.write(XmlWEvent::end_element())?; // propstat
+            out.write(XmlWEvent::end_element())?; // response
+            return Ok(());
+        }
+
+        out.write(XmlWEvent::characters("HTTP/1.1 200 OK"))?;
+        out.write(XmlWEvent::end_element())?; // status
+        out.write(XmlWEvent::end_element())?; // propstat
+
+        if !failed_props.is_empty() {
+            // Handle the failed properties
+            out.write(XmlWEvent::start_element("D:propstat"))?;
+            out.write(XmlWEvent::start_element("D:prop"))?;
+            for prop in failed_props {
+                start_client_prop_element(out, prop)?;
+                out.write(XmlWEvent::end_element())?;
+            }
+            out.write(XmlWEvent::end_element())?; // prop
+            out.write(XmlWEvent::start_element("D:status"))?;
+            out.write(XmlWEvent::characters("HTTP/1.1 404 Not Found"))?;
+            out.write(XmlWEvent::end_element())?; // status
+            out.write(XmlWEvent::end_element())?; // propstat
+        }
+
+        out.write(XmlWEvent::end_element())?; // response
+
+        Ok(())
+    }
+
+    /// Adapted from
+    /// https://github.com/tylerwhall/hyperdav-server/blob/415f512ac030478593ad389a3267aeed7441d826/src/lib.rs#L245
+    /// extended properties adapted from
+    /// https://github.com/miquels/webdav-handler-rs/blob/02433c1acfccd848a7de26889f6857cbad559076/src/handle_props.rs#L655
+    fn handle_prop_path<W: Write>(&self, out: &mut XmlWriter<W>, path: &Path, meta: &Metadata, prop: XmlName) -> Result<bool, XmlWError> {
+        if prop.namespace == Some(WEBDAV_XML_NAMESPACE_DAV.1) {
+            match prop.local_name {
+                "creationdate" => {
+                    out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "creationdate")))?;
+                    out.write(XmlWEvent::characters(&file_time_created(meta).rfc3339().to_string()))?;
+                }
+
+                "getcontentlength" => {
+                    out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "getcontentlength")))?;
+                    out.write(XmlWEvent::characters(&file_length(&meta, &path).to_string()))?;
+                }
+
+                "getcontenttype" => {
+                    out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "getcontenttype")))?;
+                    out.write(XmlWEvent::characters(&self.guess_mime_type(&path).to_string()))?;
+                }
+
+                "getlastmodified" => {
+                    out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "getlastmodified")))?;
+                    out.write(XmlWEvent::characters(&file_time_modified(meta).rfc3339().to_string()))?;
+                }
+
+                "resourcetype" => {
+                    out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "resourcetype")))?;
+                    if !is_actually_file(&meta.file_type()) {
+                        out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "collection")))?;
+                        out.write(XmlWEvent::end_element())?;
+                    }
+                }
+
+                _ => return Ok(false),
+            }
+        } else if prop.namespace == Some(WEBDAV_XML_NAMESPACE_MICROSOFT.1) {
+            match prop.local_name {
+                "Win32CreationTime" => {
+                    out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_MICROSOFT.0, "Win32CreationTime")))?;
+                    out.write(XmlWEvent::characters(&file_time_created(meta).rfc3339().to_string()))?;
+                }
+
+                "Win32FileAttributes" => {
+                    out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_MICROSOFT.0, "Win32FileAttributes")))?;
+
+                    let attr = win32_file_attributes(meta, path);
+                    out.write(XmlWEvent::characters(&format!("{:08x}", attr)))?;
+                }
+
+                "Win32LastAccessTime" => {
+                    out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_MICROSOFT.0, "Win32FileAttributes")))?;
+                    out.write(XmlWEvent::characters(&file_time_accessed(meta).rfc3339().to_string()))?;
+                }
+
+                "Win32LastModifiedTime" => {
+                    out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_MICROSOFT.0, "Win32LastModifiedTime")))?;
+                    out.write(XmlWEvent::characters(&file_time_modified(meta).rfc3339().to_string()))?;
+                }
+
+                _ => return Ok(false),
+            }
+        } else if prop.namespace == Some(WEBDAV_XML_NAMESPACE_APACHE.1) {
+            match prop.local_name {
+                "executable" => {
+                    out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_APACHE.0, "executable")))?;
+                    out.write(XmlWEvent::characters(if file_executable(&meta) { "T" } else { "F" }))?;
+                }
+
+                _ => return Ok(false),
+            }
+        } else {
+            return Ok(false);
+        }
+
+        out.write(XmlWEvent::end_element())?;
+        Ok(true)
+    }
 }
 
 
@@ -446,155 +589,6 @@ fn parse_propfind(req: &mut Request) -> Result<PropfindVariant, Result<String, X
     }
 }
 
-/// Adapted from
-/// https://github.com/tylerwhall/hyperdav-server/blob/415f512ac030478593ad389a3267aeed7441d826/src/lib.rs#L306
-fn handle_propfind_path<'n, W: Write, N: BorrowXmlName<'n>>(out: &mut XmlWriter<W>, url: &str, path: &Path, meta: &Metadata, props: &[&'n [N]],
-                                                            just_names: bool)
-                                                            -> Result<(), XmlWError> {
-    out.write(XmlWEvent::start_element("D:response"))?;
-
-    out.write(XmlWEvent::start_element("D:href"))?;
-    out.write(XmlWEvent::characters(url))?;
-    out.write(XmlWEvent::end_element())?; // href
-
-    let prop_count = props.iter().map(|pp| pp.len()).sum();
-    let mut failed_props = Vec::with_capacity(prop_count);
-    out.write(XmlWEvent::start_element("D:propstat"))?;
-    out.write(XmlWEvent::start_element("D:prop"))?;
-    for prop in props.iter().flat_map(|pp| pp.iter()) {
-        let prop = prop.borrow_xml_name();
-
-        let mut write_name = false;
-        if !just_names && !handle_prop_path(out, path, meta, prop)? {
-            failed_props.push(prop);
-            write_name = true;
-        }
-
-        if just_names || write_name {
-            start_client_prop_element(out, prop)?;
-            out.write(XmlWEvent::end_element())?;
-        }
-    }
-    out.write(XmlWEvent::end_element())?; // prop
-    out.write(XmlWEvent::start_element("D:status"))?;
-    if failed_props.len() >= prop_count {
-        // If they all failed, make this a failure response and return
-        out.write(XmlWEvent::characters("HTTP/1.1 404 Not Found"))?;
-        out.write(XmlWEvent::end_element())?; // status
-        out.write(XmlWEvent::end_element())?; // propstat
-        out.write(XmlWEvent::end_element())?; // response
-        return Ok(());
-    }
-
-    out.write(XmlWEvent::characters("HTTP/1.1 200 OK"))?;
-    out.write(XmlWEvent::end_element())?; // status
-    out.write(XmlWEvent::end_element())?; // propstat
-
-    if !failed_props.is_empty() {
-        // Handle the failed properties
-        out.write(XmlWEvent::start_element("D:propstat"))?;
-        out.write(XmlWEvent::start_element("D:prop"))?;
-        for prop in failed_props {
-            start_client_prop_element(out, prop)?;
-            out.write(XmlWEvent::end_element())?;
-        }
-        out.write(XmlWEvent::end_element())?; // prop
-        out.write(XmlWEvent::start_element("D:status"))?;
-        out.write(XmlWEvent::characters("HTTP/1.1 404 Not Found"))?;
-        out.write(XmlWEvent::end_element())?; // status
-        out.write(XmlWEvent::end_element())?; // propstat
-    }
-
-    out.write(XmlWEvent::end_element())?; // response
-
-    Ok(())
-}
-
-/// Adapted from
-/// https://github.com/tylerwhall/hyperdav-server/blob/415f512ac030478593ad389a3267aeed7441d826/src/lib.rs#L245
-/// extended properties adapted from
-/// https://github.com/miquels/webdav-handler-rs/blob/02433c1acfccd848a7de26889f6857cbad559076/src/handle_props.rs#L655
-fn handle_prop_path<W: Write>(out: &mut XmlWriter<W>, path: &Path, meta: &Metadata, prop: XmlName) -> Result<bool, XmlWError> {
-    if prop.namespace == Some(WEBDAV_XML_NAMESPACE_DAV.1) {
-        match prop.local_name {
-            "creationdate" => {
-                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "creationdate")))?;
-                out.write(XmlWEvent::characters(&file_time_created(meta).rfc3339().to_string()))?;
-            }
-
-            "getcontentlength" => {
-                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "getcontentlength")))?;
-                out.write(XmlWEvent::characters(&file_length(&meta, &path).to_string()))?;
-            }
-
-            "getcontenttype" => {
-                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "getcontenttype")))?;
-                let mime_type = guess_mime_type_opt(&path).unwrap_or_else(|| if file_binary(&path) {
-                    "application/octet-stream".parse().unwrap()
-                } else {
-                    "text/plain".parse().unwrap()
-                });
-                out.write(XmlWEvent::characters(&mime_type.to_string()))?;
-            }
-
-            "getlastmodified" => {
-                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "getlastmodified")))?;
-                out.write(XmlWEvent::characters(&file_time_modified(meta).rfc3339().to_string()))?;
-            }
-
-            "resourcetype" => {
-                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "resourcetype")))?;
-                if !is_actually_file(&meta.file_type()) {
-                    out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_DAV.0, "collection")))?;
-                    out.write(XmlWEvent::end_element())?;
-                }
-            }
-
-            _ => return Ok(false),
-        }
-    } else if prop.namespace == Some(WEBDAV_XML_NAMESPACE_MICROSOFT.1) {
-        match prop.local_name {
-            "Win32CreationTime" => {
-                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_MICROSOFT.0, "Win32CreationTime")))?;
-                out.write(XmlWEvent::characters(&file_time_created(meta).rfc3339().to_string()))?;
-            }
-
-            "Win32FileAttributes" => {
-                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_MICROSOFT.0, "Win32FileAttributes")))?;
-
-                let attr = win32_file_attributes(meta, path);
-                out.write(XmlWEvent::characters(&format!("{:08x}", attr)))?;
-            }
-
-            "Win32LastAccessTime" => {
-                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_MICROSOFT.0, "Win32FileAttributes")))?;
-                out.write(XmlWEvent::characters(&file_time_accessed(meta).rfc3339().to_string()))?;
-            }
-
-            "Win32LastModifiedTime" => {
-                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_MICROSOFT.0, "Win32LastModifiedTime")))?;
-                out.write(XmlWEvent::characters(&file_time_modified(meta).rfc3339().to_string()))?;
-            }
-
-            _ => return Ok(false),
-        }
-    } else if prop.namespace == Some(WEBDAV_XML_NAMESPACE_APACHE.1) {
-        match prop.local_name {
-            "executable" => {
-                out.write(XmlWEvent::start_element((WEBDAV_XML_NAMESPACE_APACHE.0, "executable")))?;
-                out.write(XmlWEvent::characters(if file_executable(&meta) { "T" } else { "F" }))?;
-            }
-
-            _ => return Ok(false),
-        }
-    } else {
-        return Ok(false);
-    }
-
-    out.write(XmlWEvent::end_element())?;
-    Ok(true)
-}
-
 /// Adapted from
 /// https://github.com/tylerwhall/hyperdav-server/blob/415f512ac030478593ad389a3267aeed7441d826/src/lib.rs#L214
 fn start_client_prop_element<W: Write>(out: &mut XmlWriter<W>, prop: XmlName) -> Result<(), XmlWError> {
diff --git a/src/options.rs b/src/options.rs
index b5702a7..06026ee 100644
--- a/src/options.rs
+++ b/src/options.rs
@@ -15,9 +15,11 @@ use clap::{AppSettings, ErrorKind as ClapErrorKind, Error as ClapError, Arg, App
 use std::collections::btree_map::{BTreeMap, Entry as BTreeMapEntry};
 use std::collections::BTreeSet;
 use std::env::{self, temp_dir};
+use std::num::NonZeroU64;
 use std::path::PathBuf;
 use std::str::FromStr;
 use std::borrow::Cow;
+use iron::mime::Mime;
 use std::net::IpAddr;
 use regex::Regex;
 use cidr::IpCidr;
@@ -76,6 +78,8 @@ pub struct Options {
     pub temp_directory: (String, PathBuf),
     /// Whether to check for index files in served directories before serving a listing. Default: true
     pub check_indices: bool,
+    /// Whether to allow requests to `/file` to return `/file.{INDEX_EXTENSIONS`. Default: false
+    pub strip_extensions: bool,
     /// Whether to allow write operations. Default: false
     pub allow_writes: bool,
     /// Whether to encode filesystem files. Default: true
@@ -86,6 +90,8 @@ pub struct Options {
     ///   * >= 2 – suppress startup except for auth data, if present
     ///   * >= 3 – suppress all startup messages
     pub loglevel: LogLevel,
+    /// Whether to colourise the log output. Default: `true`
+    pub log_colour: bool,
     /// Whether to handle WebDAV requests. Default: false
     pub webdav: bool,
     /// Data for HTTPS, identity file and password. Default: `None`
@@ -98,6 +104,10 @@ pub struct Options {
     pub generate_path_auth: BTreeSet<String>,
     /// Header names and who we trust them from in `HEADER-NAME:CIDR` format
     pub proxies: BTreeMap<IpCidr, String>,
+    /// Extension -> MIME type mapping overrides; empty string for no extension
+    pub mime_type_overrides: BTreeMap<String, Mime>,
+    /// Max amount of data per second each request is allowed to return. Default: `None`
+    pub request_bandwidth: Option<NonZeroU64>,
 }
 
 impl Options {
@@ -120,7 +130,9 @@ impl Options {
             .arg(Arg::from_usage("-w --allow-write 'Allow for write operations. Default: false'"))
             .arg(Arg::from_usage("-i --no-indices 'Always generate dir listings even if index files are available. Default: false'"))
             .arg(Arg::from_usage("-e --no-encode 'Do not encode filesystem files. Default: false'"))
+            .arg(Arg::from_usage("-x --strip-extensions 'Allow stripping index extentions from served paths. Default: false'"))
             .arg(Arg::from_usage("-q --quiet... 'Suppress increasing amounts of output'"))
+            .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("--ssl [TLS_IDENTITY] 'Data for HTTPS, identity file. Password in HTTP_SSL_PASS env var, otherwise empty'")
                 .validator(Options::identity_validator))
@@ -132,6 +144,10 @@ impl Options {
             .arg(Arg::from_usage("--gen-path-auth [PATH]... 'Generate a one-off username:password set for authentication under PATH'"))
             .arg(Arg::from_usage("--proxy [HEADER-NAME:CIDR]... 'Treat HEADER-NAME as proxy forwarded-for header when request comes from CIDR'")
                 .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'")
+                .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(|_| ())))
             .get_matches();
 
         let dir = matches.value_of("DIR").unwrap_or(".");
@@ -195,15 +211,24 @@ impl Options {
                  temp_pb.join(suffix))
             },
             check_indices: !matches.is_present("no-indices"),
+            strip_extensions: matches.is_present("strip-extensions"),
             allow_writes: matches.is_present("allow-write"),
             encode_fs: !matches.is_present("no-encode"),
             loglevel: matches.occurrences_of("quiet").into(),
+            log_colour: !matches.is_present("no-colour"),
             webdav: matches.is_present("webdav"),
             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,
             generate_path_auth: generate_path_auth,
             proxies: matches.values_of("proxy").unwrap_or_default().map(Cow::from).map(Options::proxy_parse).map(Result::unwrap).collect(),
+            mime_type_overrides: matches.values_of("mime-type")
+                .unwrap_or_default()
+                .map(Cow::from)
+                .map(Options::mime_type_override_parse)
+                .map(Result::unwrap)
+                .collect(),
+            request_bandwidth: matches.value_of("request-bandwidth").map(Cow::from).map(Options::bandwidth_parse).map(Result::unwrap).unwrap_or_default(),
         }
     }
 
@@ -305,4 +330,45 @@ impl Options {
             }
         }
     }
+
+    fn bandwidth_parse<'s>(s_orig: Cow<'s, str>) -> Result<Option<NonZeroU64>, String> {
+        let s = s_orig.trim();
+        let multiplier_b = s.as_bytes().get(s.len() - 1).ok_or_else(|| format!("\"{}\" bandwidth specifier empty", s_orig))?;
+        let multiplier_order = match multiplier_b {
+            b'k' | b'K' => 1,
+            b'm' | b'M' => 2,
+            b'g' | b'G' => 3,
+            b't' | b'T' => 4,
+            b'p' | b'P' => 5,
+            b'e' | b'E' => 6,
+            _ => 0,
+        };
+        let (multiplier, s) = match multiplier_order {
+            0 => (1, s),
+            mo => {
+                let base: u64 = if (*multiplier_b as char).is_uppercase() {
+                    1024
+                } else {
+                    1000
+                };
+                (base.pow(mo), &s[..s.len() - 1]) // No need to check, E is 2^60
+            }
+        };
+
+        let number = u64::from_str(s).map_err(|e| format!("\"{}\" not band width size: {}", s, e))?;
+        Ok(NonZeroU64::new(number.checked_mul(multiplier).ok_or_else(|| format!("{} * {} too big", number, multiplier))?))
+    }
+
+    fn mime_type_override_parse<'s>(s: Cow<'s, str>) -> Result<(String, Mime), String> {
+        match s.find(":") {
+            None => Err(format!("{} not in EXTENSION:MIME-TYPE format", s)),
+            Some(col_idx) => {
+                let mt = s[col_idx + 1..].parse().map_err(|()| format!("{} not a valid MIME type", &s[col_idx + 1..]))?;
+
+                let mut s = s.into_owned();
+                s.truncate(col_idx);
+                Ok((s, mt))
+            }
+        }
+    }
 }
diff --git a/src/util/mod.rs b/src/util/mod.rs
index d0adad0..d8749ef 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -106,7 +106,7 @@ pub static PORT_SCAN_HIGHEST: u16 = 9999;
 /// The app name and version to use with User-Agent or Server response header.
 pub static USER_AGENT: &str = concat!("http/", env!("CARGO_PKG_VERSION"));
 
-/// Index file extensions to look for if `-i` was not specified.
+/// Index file extensions to look for if `-i` was not specified and strippable extensions to look for if `-x` was specified.
 pub static INDEX_EXTENSIONS: &[&str] = &["html", "htm", "shtml"];
 
 


Test patch: