Repository: thecoshman/http (language: rust)

Issue:
Per-path HTTP authentication
Adding a mechanism wherein any subpath could be (un)protected independently of the global config.


Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 3a0285d..c2f161e 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
-version = "1.5.2"
+version = "1.6.0"
 # Remember to also update in http.md
 authors = ["thecoshman <rust@thecoshman.com>",
            "nabijaczleweli <nabijaczleweli@gmail.com>",
@@ -17,23 +17,24 @@ authors = ["thecoshman <rust@thecoshman.com>",
 
 [dependencies]
 hyper-native-tls = "0.3"
-lazy_static = "1.2"
+percent-encoding = "2.1"
+lazy_static = "1.4"
 serde_json = "0.9"
 mime_guess = "1.8"
+tabwriter = "1.1"
 lazysort = "0.2"
 brotli2 = "0.3"
-unicase = "2.2"
+unicase = "2.4"
 base64 = "0.10"
 flate2 = "1.0"
 rfsapi = "0.1"
 bzip2 = "0.3"
 ctrlc = "3.1"
-regex = "1.1"
+regex = "1.2"
 serde = "0.9"
-clap = "2.32"
-rand = "0.6"
+clap = "2.33"
+rand = "0.7"
 time = "0.1"
-url = "1.7"
 md6 = "2.0"
 
 [dependencies.trivial_colours]
@@ -48,7 +49,7 @@ features = ["hyper-native-tls"]
 version = "0.2"
 
 [build-dependencies]
-embed-resource = "1.1"
+embed-resource = "1.3"
 
 [target.'cfg(not(any(target_os = "windows", target_os = "macos")))'.build-dependencies.cc]
 version = "1.0"
diff --git a/README.md b/README.md
index 05f24c6..c7e64bd 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,7 @@ See [the manpage](http.md) for full list.
   * [x] Smart encoding of generated and filesystem-originating responses (disableable via `-e` switch)
   * [x] Full Range header support
   * [x] Hosting with an <sub>(optional)</sub> optionally autogenerated TLS certificate
+  * [x] Arbitrarily nested username/password authentication
   * [x] [RFSAPI](https://github.com/nabijaczleweli/rfsapi-rs) support (explorable from commandline with [D'Oh](https://github.com/thecoshman/doh))
 
 ## [Manpage](http.md)
diff --git a/appveyor.yml b/appveyor.yml
index f941f70..62edfef 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 1.5.2-{build}
+version: 1.6.0-{build}
 
 skip_tags: false
 
@@ -22,21 +22,21 @@ build: off
 build_script:
   - git submodule update --init --recursive
   - cargo build --verbose --release
-  - cp target\release\http.exe http-v1.5.2.exe
-  - strip --strip-all --remove-section=.comment --remove-section=.note http-v1.5.2.exe
-  - makensis -DHTTP_VERSION=v1.5.2 "/X!include \"EnvVarUpdate.nsh\"" install.nsi
+  - cp target\release\http.exe http-v1.6.0.exe
+  - strip --strip-all --remove-section=.comment --remove-section=.note http-v1.6.0.exe
+  - makensis -DHTTP_VERSION=v1.6.0 "/X!include \"EnvVarUpdate.nsh\"" install.nsi
 
 test: off
 test_script:
   - cargo test --verbose --release
 
 artifacts:
-  - path: http-v1.5.2.exe
-  - path: http v1.5.2 installer.exe
+  - path: http-v1.6.0.exe
+  - path: http v1.6.0 installer.exe
 
 deploy:
   provider: GitHub
-  artifact: /http.*v1.5.2.*\.exe/
+  artifact: /http.*v1.6.0.*\.exe/
   auth_token:
     secure: ZTXvCrv9y01s7Hd60w8W7NaouPnPoaw9YJt9WhWQ2Pep8HLvCikt9Exjkz8SGP9P
   on:
diff --git a/http.md b/http.md
index cbc715b..253e256 100644
--- a/http.md
+++ b/http.md
@@ -58,24 +58,54 @@ pass parameters like what port to use.
 
   --auth [USERNAME[:PASSWORD]]
 
-    Data for authentication.
+    Data for global authentication.
 
-    If specified, will require the specified credentials to access any path
-    on the server.
+    Equivalent to --path-auth with a root path and the same crednetials.
+
+    This argument is deprecated, and will be replaced with the current version
+    of --path-auth on the next breaking release.
+    Use --path-auth in new designs to avoid surprises.
 
     Default: None.
 
   --gen-auth
 
-    Generate a one-off username:password set for authentication.
+    Generate a one-off username:password set for global authentication.
+
+    Functions as if --auth was specified with the generated credentials.
+
+    This argument is deprecated, and will be replaced with the current version
+    of --gen-path-auth on the next breaking release.
+    Use --gen-path-auth in new designs to avoid surprises.
+
+    Exclusive with --auth. Default: false.
+
+  --path-auth [PATH=[USERNAME[:PASSWORD]]]
+
+    Data for per-path authentication.
+
+    The specified PATH will require the specified credentials to access.
+    If credentials are unspecified, the path will have authentication
+    disabled, even if global or parent paths have authentication specified.
+    These can be arbitrarily nested.
+
+    PATH is slash-normalised stripped of leading and trailing slashes.
+    Specifying more than one of the same PATH is erroneous.
+
+    Default: empty.
+
+  --gen-path-auth [PATH]
+
+    Generate a one-off username:password set for authentication under PATH.
 
     The username consists of 6-12 random alphanumeric characters, whereas
     the password consists of 10-25 random characters from most of the
     ASCII printable set.
 
-    Functions as if --auth was specified with the generated credentials.
+    Functions as if --path-auth was specified with PATH
+    and the generated credentials.
 
-    Exclusive with --auth. Default: false.
+    Exclusive with --path-auth with the equivalent PATH. Default: empty.
 
   -s --no-follow-symlinks
 
@@ -198,7 +228,8 @@ pass parameters like what port to use.
     unlocked with password "pwd".
 
     Assuming password is correct, example output change:
-      Hosting "." on port 8000 TLS certificate from "cert/http8k.p12" and no authentication...
+      Hosting "." on port 8000 TLS certificate from "cert/http8k.p12"
+      and no authentication...
 
   `http --gen-ssl`
 
@@ -206,40 +237,72 @@ pass parameters like what port to use.
     identity file.
 
     Example output change:
-      Hosting "." on port 8000 TLS certificate from "$TEMP/http-P-Rust-http/tls/tls.p12" and no authentication...
+      Hosting "." on port 8000 with TLS certificate from
+      "$TEMP/http-P-Rust-http/tls/tls.p12" and no authentication...
 
-  `http --auth Pirate`
+  `http --path-auth /=Pirate`
 
     As in the first example, but require all clients to log in with the username "Pirate".
 
     Example output change:
-      Hosting "." on port 8000 without TLS and basic authentication using "Pirate" as username and no password...
+      Hosting "." on port 8000 without TLS and basic authentication...
+      Basic authentication credentials:
+      Path  Username  Password
+      /     Pirate
 
     On unauthed request:
       127.0.0.1:15141 requested to GET http://127.0.0.1:8005/ without authorisation
 
     Invalid credentials supplied:
-      127.0.0.1:15142 requested to GET http://127.0.0.1:8005/ with invalid credentials "Pirate:memelord11"
+      127.0.0.1:15142 requested to GET http://127.0.0.1:8005/ with invalid credentials
+      "Pirate:memelord11"
 
     Valid credentials supplied:
       127.0.0.1:15142 correctly authorised to GET http://127.0.0.1:8005/
       127.0.0.1:15142 was served directory listing for \\?\P:\Rust\http
 
-  `http --auth Pirate:memelord42`
+  `http --path-auth /=Pirate:memelord42`
+
+    As in the first example, but require all clients to log in
+    with the username "Pirate" and password "memelord42".
+
+    Example output change:
+      Hosting "." on port 8000 without TLS and basic authentication...
+      Basic authentication credentials:
+      Path  Username  Password
+      /     Pirate    memelord42
+
+    See above for log messages when performing requests.
+
+  `http --gen-path-auth /`
 
-    As in the first example, but require all clients to log in with the username "Pirate" and password "memelord42".
+    As in the first example, but generate a username:password pair
+    and require all clients to log in therewith.
 
     Example output change:
-      Hosting "." on port 8000 without TLS and basic authentication using "Pirate" as username and "memelord42" as password...
+      Hosting "." on port 8000 without TLS and basic authentication...
+      Basic authentication credentials:
+      Path  Username  Password
+      /     jOvm8yCp  &gK=h&$-$HElLPb9HO%
 
     See above for log messages when performing requests.
 
-  `http --gen-auth`
+  `http --path-auth /=admin:admin --gen-path-auth /target/debug --path-auth target/doc= --path-auth target/.rustc_info.json= --path-auth target/release/=releases`
 
-    As in the first example, but generate a username:password pair and require all clients to log in therewith.
+    As in the first example, but allow unauthenticated access to /target/doc and /target/.rustc_info.json,
+    require username "releases" and no password to access /target/release,
+    require a randomly-generated username:password pair to access /target/debug,
+    and lock all other paths behind "admin:admin".
 
     Example output change:
-      Hosting "." on port 8000 without TLS and basic authentication using "RQvjNUKmnD9" as username and ".TM&tb;t29,Eo" as password...
+      Hosting "." on port 8000 without TLS and basic authentication...
+      Basic authentication credentials:
+      Path                      Username  Password
+      /                         admin     admin
+      /target/.rustc_info.json
+      /target/debug             PYld448   l=Z~vdp,zAt^<uvRyU.T<F
+      /target/doc
+      /target/release           releases
 
     See above for log messages when performing requests.
 
diff --git a/src/main.rs b/src/main.rs
index 793bebe..77d7be7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,9 +1,11 @@
 extern crate hyper_native_tls;
+extern crate percent_encoding;
 extern crate trivial_colours;
 #[macro_use]
 extern crate lazy_static;
 extern crate serde_json;
 extern crate mime_guess;
+extern crate tabwriter;
 extern crate lazysort;
 extern crate brotli2;
 extern crate unicase;
@@ -21,7 +23,6 @@ extern crate iron;
 extern crate libc;
 extern crate rand;
 extern crate time;
-extern crate url;
 extern crate md6;
 
 mod error;
@@ -33,8 +34,12 @@ pub mod util;
 pub use error::Error;
 pub use options::Options;
 
+use std::mem;
 use iron::Iron;
 use std::process::exit;
+use tabwriter::TabWriter;
+use std::io::{Write, stdout};
+use std::collections::BTreeSet;
 use std::sync::{Arc, Mutex, Condvar};
 use hyper_native_tls::NativeTlsServer;
 
@@ -56,22 +61,22 @@ fn actual_main() -> i32 {
 fn result_main() -> Result<(), Error> {
     let mut opts = Options::parse();
     if opts.generate_tls {
-        opts.tls_data = Some(try!(ops::generate_tls_data(&opts.temp_directory)));
+        opts.tls_data = Some(ops::generate_tls_data(&opts.temp_directory)?);
     }
-    if opts.generate_auth {
-        opts.auth_data = Some(ops::generate_auth_data());
+    for path in mem::replace(&mut opts.generate_path_auth, BTreeSet::new()) {
+        opts.path_auth_data.insert(path, Some(ops::generate_auth_data()));
     }
 
-    let mut responder = try!(if let Some(p) = opts.port {
+    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(("0.0.0.0", p),
-                                                              try!(NativeTlsServer::new(id, pw).map_err(|err| {
-                    Error {
-                        desc: "TLS certificate",
-                        op: "open",
-                        more: err.to_string().into(),
-                    }
-                })))
+                                                              NativeTlsServer::new(id, pw).map_err(|err| {
+                        Error {
+                            desc: "TLS certificate",
+                            op: "open",
+                            more: err.to_string().into(),
+                        }
+                    })?)
             } else {
                 Iron::new(ops::HttpHandler::new(&opts)).http(("0.0.0.0", p))
             }
@@ -84,7 +89,7 @@ fn result_main() -> Result<(), Error> {
             })
     } else {
         ops::try_ports(ops::HttpHandler::new(&opts), util::PORT_SCAN_LOWEST, util::PORT_SCAN_HIGHEST, &opts.tls_data)
-    });
+    }?;
 
     print!("{}Hosting \"{}\" on port {} with",
            trivial_colours::Reset,
@@ -95,20 +100,33 @@ fn result_main() -> Result<(), Error> {
     } else {
         print!("out TLS");
     }
-    print!(" and ");
-    if let Some(ad) = opts.auth_data.as_ref() {
-        let mut itr = ad.split(':');
-        print!("basic authentication using \"{}\" as username and ", itr.next().unwrap());
-        if let Some(p) = itr.next() {
-            print!("\"{}\" as", p);
-        } else {
-            print!("no");
-        }
-        print!(" password");
+    if !opts.path_auth_data.is_empty() {
+        print!(" and basic authentication");
     } else {
-        print!("no authentication");
+        print!(" and no authentication");
     }
     println!("...");
+    if !opts.path_auth_data.is_empty() {
+        println!("Basic authentication credentials:");
+
+        let mut out = TabWriter::new(stdout());
+        writeln!(out, "Path\tUsername\tPassword").unwrap();
+
+        for (path, creds) in &opts.path_auth_data {
+            if let Some(ad) = creds {
+                let mut itr = ad.split(':');
+                write!(out, "/{}\t{}\t", path, itr.next().unwrap()).unwrap();
+                if let Some(p) = itr.next() {
+                    write!(out, "{}", p).unwrap();
+                }
+                writeln!(out).unwrap();
+            } else {
+                writeln!(out, "/{}\t\t", path).unwrap();
+            }
+        }
+
+        out.flush().unwrap();
+    }
     println!("Ctrl-C to stop.");
     println!();
 
diff --git a/src/ops.rs b/src/ops.rs
index 910fddc..36e47f8 100644
--- a/src/ops.rs
+++ b/src/ops.rs
@@ -1,7 +1,4 @@
 use md6;
-use rand::{Rng, thread_rng};
-use rand::distributions::uniform::Uniform as UniformDistribution;
-use rand::distributions::Alphanumeric as AlphanumericDistribution;
 use std::iter;
 use time::now;
 use serde_json;
@@ -14,15 +11,18 @@ use lazysort::SortedBy;
 use std::path::PathBuf;
 use std::fs::{self, File};
 use std::default::Default;
+use rand::{Rng, thread_rng};
 use iron::modifiers::Header;
-use std::collections::HashMap;
 use self::super::{Options, Error};
 use mime_guess::guess_mime_type_opt;
 use hyper_native_tls::NativeTlsServer;
+use std::collections::{BTreeMap, HashMap};
 use std::io::{self, SeekFrom, Write, Read, Seek};
 use trivial_colours::{Reset as CReset, Colour as C};
 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::{headers, status, method, mime, IronResult, Listening, Response, TypeMap, Request, Handler, Iron};
 use self::super::util::{WwwAuthenticate, 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, response_encoding, detect_file_as_dir,
@@ -70,7 +70,8 @@ pub struct HttpHandler {
     pub follow_symlinks: bool,
     pub sandbox_symlinks: bool,
     pub check_indices: bool,
-    pub auth_data: Option<(String, Option<String>)>,
+    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)>,
     cache_gen: RwLock<CacheT<Vec<u8>>>,
@@ -79,15 +80,30 @@ pub struct HttpHandler {
 
 impl HttpHandler {
     pub fn new(opts: &Options) -> HttpHandler {
+        let mut path_auth_data = BTreeMap::new();
+        let mut global_auth_data = None;
+
+        for (path, creds) in &opts.path_auth_data {
+            let creds = creds.as_ref()
+                .map(|auth| {
+                    let mut itr = auth.split_terminator(':');
+                    (itr.next().unwrap().to_string(), itr.next().map(str::to_string))
+                });
+
+            if path == "" {
+                global_auth_data = creds;
+            } else {
+                path_auth_data.insert(path.to_string(), creds);
+            }
+        }
+
         HttpHandler {
             hosted_directory: opts.hosted_directory.clone(),
             follow_symlinks: opts.follow_symlinks,
             sandbox_symlinks: opts.sandbox_symlinks,
             check_indices: opts.check_indices,
-            auth_data: opts.auth_data.as_ref().map(|auth| {
-                let mut itr = auth.split_terminator(':');
-                (itr.next().unwrap().to_string(), itr.next().map(str::to_string))
-            }),
+            global_auth_data: global_auth_data,
+            path_auth_data: path_auth_data,
             writes_temp_dir: HttpHandler::temp_subdir(&opts.temp_directory, opts.allow_writes, "writes"),
             encoded_temp_dir: HttpHandler::temp_subdir(&opts.temp_directory, opts.encode_fs, "encoded"),
             cache_gen: Default::default(),
@@ -122,8 +138,8 @@ impl HttpHandler {
 
 impl Handler for HttpHandler {
     fn handle(&self, req: &mut Request) -> IronResult<Response> {
-        if let Some(auth) = self.auth_data.as_ref() {
-            if let Some(resp) = try!(self.verify_auth(req, auth)) {
+        if self.global_auth_data.is_some() || !self.path_auth_data.is_empty() {
+            if let Some(resp) = self.verify_auth(req)? {
                 return Ok(resp);
             }
         }
@@ -146,7 +162,34 @@ impl Handler for HttpHandler {
 }
 
 impl HttpHandler {
-    fn verify_auth(&self, req: &mut Request, auth: &(String, Option<String>)) -> IronResult<Option<Response>> {
+    fn verify_auth(&self, req: &mut Request) -> IronResult<Option<Response>> {
+        let mut auth = self.global_auth_data.as_ref();
+
+        if !self.path_auth_data.is_empty() {
+            let mut path = req.url.as_ref().path();
+            if path.starts_with('/') {
+                path = &path[1..];
+            }
+            if path.ends_with('/') {
+                path = &path[..path.len() - 1];
+            }
+
+            while !path.is_empty() {
+                if let Some(pad) = self.path_auth_data.get(path) {
+                    auth = pad.as_ref();
+                    break;
+                }
+
+                path = &path[..path.rfind('/').unwrap_or(0)];
+            }
+        }
+
+        let auth = if let Some(auth) = auth {
+            auth
+        } else {
+            return Ok(None);
+        };
+
         match req.headers.get() {
             Some(headers::Authorization(headers::Basic { username, password })) => {
                 let pwd = if password == &Some(String::new()) {
@@ -521,7 +564,7 @@ impl HttpHandler {
         self.handle_raw_fs_api_response(status::Ok,
                                         &FilesetData {
                                             writes_supported: self.writes_temp_dir.is_some(),
-                                            is_root: req.url.path() == [""],
+                                            is_root: req.url.as_ref().path_segments().unwrap().count() == 1,
                                             is_file: false,
                                             files: req_p.read_dir()
                                                 .expect("Failed to read requested directory")
@@ -564,7 +607,7 @@ impl HttpHandler {
                     ((!self.follow_symlinks || !self.sandbox_symlinks) ||
                      (self.follow_symlinks && self.sandbox_symlinks && is_descendant_of(&req_p, &self.hosted_directory.1)))
                 }) {
-                if req.url.path().pop() == Some("") {
+                if req.url.as_ref().path_segments().unwrap().next_back() == Some("") {
                     let r = self.handle_get_file(req, idx);
                     log!("{} found index file for directory {magenta}{}{reset}",
                          iter::repeat(' ').take(req.remote_addr.to_string().len()).collect::<String>(),
@@ -601,7 +644,7 @@ impl HttpHandler {
 
     fn handle_get_mobile_dir_listing(&self, req: &mut Request, req_p: PathBuf) -> IronResult<Response> {
         let relpath = (url_path(&req.url) + "/").replace("//", "/");
-        let is_root = req.url.path() == [""];
+        let is_root = req.url.as_ref().path_segments().unwrap().count() == 1;
         log!("{green}{}{reset} was served mobile directory listing for {magenta}{}{reset}",
              req.remote_addr,
              req_p.display());
@@ -688,7 +731,7 @@ impl HttpHandler {
 
     fn handle_get_dir_listing(&self, req: &mut Request, req_p: PathBuf) -> IronResult<Response> {
         let relpath = (url_path(&req.url) + "/").replace("//", "/");
-        let is_root = req.url.path() == [""];
+        let is_root = req.url.as_ref().path_segments().unwrap().count() == 1;
         log!("{green}{}{reset} was served directory listing for {magenta}{}{reset}",
              req.remote_addr,
              req_p.display());
@@ -1023,22 +1066,28 @@ impl HttpHandler {
     }
 
     fn parse_requested_path_custom_symlink(&self, req: &Request, follow_symlinks: bool) -> (PathBuf, bool, bool) {
-        req.url.path().into_iter().filter(|p| !p.is_empty()).fold((self.hosted_directory.1.clone(), false, false), |(mut cur, mut sk, mut err), pp| {
-            if let Some(pp) = percent_decode(pp) {
-                cur.push(&*pp);
-            } else {
-                err = true;
-            }
-            while let Ok(newlink) = cur.read_link() {
-                sk = true;
-                if follow_symlinks {
-                    cur = newlink;
+        req.url
+            .as_ref()
+            .path_segments()
+            .unwrap()
+            .into_iter()
+            .filter(|p| !p.is_empty())
+            .fold((self.hosted_directory.1.clone(), false, false), |(mut cur, mut sk, mut err), pp| {
+                if let Some(pp) = percent_decode(pp) {
+                    cur.push(&*pp);
                 } else {
-                    break;
+                    err = true;
                 }
-            }
-            (cur, sk, err)
-        })
+                while let Ok(newlink) = cur.read_link() {
+                    sk = true;
+                    if follow_symlinks {
+                        cur = newlink;
+                    } else {
+                        break;
+                    }
+                }
+                (cur, sk, err)
+            })
     }
 
     fn create_temp_dir(&self, td: &Option<(String, PathBuf)>) {
@@ -1056,7 +1105,8 @@ impl Clone for HttpHandler {
             follow_symlinks: self.follow_symlinks,
             sandbox_symlinks: self.sandbox_symlinks,
             check_indices: self.check_indices,
-            auth_data: self.auth_data.clone(),
+            global_auth_data: self.global_auth_data.clone(),
+            path_auth_data: self.path_auth_data.clone(),
             writes_temp_dir: self.writes_temp_dir.clone(),
             encoded_temp_dir: self.encoded_temp_dir.clone(),
             cache_gen: Default::default(),
@@ -1087,13 +1137,13 @@ pub fn try_ports<H: Handler + Clone>(hndlr: H, from: u16, up_to: u16, tls_data:
         let ir = Iron::new(hndlr.clone());
         match if let Some(&((_, ref id), ref pw)) = tls_data.as_ref() {
             ir.https(("0.0.0.0", port),
-                     try!(NativeTlsServer::new(id, pw).map_err(|err| {
-                Error {
-                    desc: "TLS certificate",
-                    op: "open",
-                    more: err.to_string().into(),
-                }
-            })))
+                     NativeTlsServer::new(id, pw).map_err(|err| {
+                    Error {
+                        desc: "TLS certificate",
+                        op: "open",
+                        more: err.to_string().into(),
+                    }
+                })?)
         } else {
             ir.http(("0.0.0.0", port))
         } {
@@ -1166,15 +1216,15 @@ pub fn generate_tls_data(temp_dir: &(String, PathBuf)) -> Result<((String, PathB
         }
     }
 
-    let mut child = try!(Command::new("openssl")
-        .args(&["req", "-x509", "-newkey", "rsa:4096", "-nodes", "-keyout", "tls.key", "-out", "tls.crt", "-days", "3650", "-utf8"])
-        .current_dir(&tls_dir)
-        .stdin(Stdio::piped())
-        .stdout(Stdio::piped())
-        .stderr(Stdio::piped())
-        .spawn()
-        .map_err(|error| err(true, "spawn", error.to_string())));
-    try!(child.stdin
+    let mut child =
+        Command::new("openssl").args(&["req", "-x509", "-newkey", "rsa:4096", "-nodes", "-keyout", "tls.key", "-out", "tls.crt", "-days", "3650", "-utf8"])
+            .current_dir(&tls_dir)
+            .stdin(Stdio::piped())
+            .stdout(Stdio::piped())
+            .stderr(Stdio::piped())
+            .spawn()
+            .map_err(|error| err(true, "spawn", error.to_string()))?;
+    child.stdin
         .as_mut()
         .unwrap()
         .write_all(concat!("PL\nhttp\n",
@@ -1183,21 +1233,21 @@ pub fn generate_tls_data(temp_dir: &(String, PathBuf)) -> Result<((String, PathB
                            env!("CARGO_PKG_VERSION"),
                            "\nnabijaczleweli@gmail.com\n")
             .as_bytes())
-        .map_err(|error| err(true, "pipe", error.to_string())));
-    let es = try!(child.wait().map_err(|error| err(true, "wait", error.to_string())));
+        .map_err(|error| err(true, "pipe", error.to_string()))?;
+    let es = child.wait().map_err(|error| err(true, "wait", error.to_string()))?;
     if !es.success() {
         return Err(exit_err(true, &mut child, &es));
     }
 
-    let mut child = try!(Command::new("openssl")
-        .args(&["pkcs12", "-export", "-out", "tls.p12", "-inkey", "tls.key", "-in", "tls.crt", "-passin", "pass:", "-passout", "pass:"])
-        .current_dir(&tls_dir)
-        .stdin(Stdio::null())
-        .stdout(Stdio::null())
-        .stderr(Stdio::null())
-        .spawn()
-        .map_err(|error| err(false, "spawn", error.to_string())));
-    let es = try!(child.wait().map_err(|error| err(false, "wait", error.to_string())));
+    let mut child =
+        Command::new("openssl").args(&["pkcs12", "-export", "-out", "tls.p12", "-inkey", "tls.key", "-in", "tls.crt", "-passin", "pass:", "-passout", "pass:"])
+            .current_dir(&tls_dir)
+            .stdin(Stdio::null())
+            .stdout(Stdio::null())
+            .stderr(Stdio::null())
+            .spawn()
+            .map_err(|error| err(false, "spawn", error.to_string()))?;
+    let es = child.wait().map_err(|error| err(false, "wait", error.to_string()))?;
     if !es.success() {
         return Err(exit_err(false, &mut child, &es));
     }
diff --git a/src/options.rs b/src/options.rs
index a5d0803..a136a3e 100644
--- a/src/options.rs
+++ b/src/options.rs
@@ -11,7 +11,9 @@
 //! ```
 
 
-use clap::{AppSettings, Arg, App};
+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::path::PathBuf;
 use std::str::FromStr;
@@ -21,6 +23,7 @@ use std::fs;
 
 lazy_static! {
     static ref CREDENTIALS_REGEX: Regex = Regex::new("[^:]+(?::[^:]+)?").unwrap();
+    static ref PATH_CREDENTIALS_REGEX: Regex = Regex::new("(.+)=([^:]+(?::[^:]+)?)?").unwrap();
 }
 
 
@@ -52,10 +55,10 @@ pub struct Options {
     pub tls_data: Option<((String, PathBuf), String)>,
     /// Whether to generate a one-off certificate. Default: false
     pub generate_tls: bool,
-    /// Data for authentication, in the form `username[:password]`. Default: `None`
-    pub auth_data: Option<String>,
-    /// Whether to generate a one-off credential set. Default: false
-    pub generate_auth: bool,
+    /// Data for per-path authentication, in the form `username[:password]`, or `None` to explicitly disable
+    pub path_auth_data: BTreeMap<String, Option<String>>,
+    /// Paths for which to generate auth data
+    pub generate_path_auth: BTreeSet<String>,
 }
 
 impl Options {
@@ -80,13 +83,48 @@ impl Options {
             .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"))
-            .arg(Arg::from_usage("--auth [USERNAME[:PASSWORD]] 'Data for authentication'").validator(Options::credentials_validator))
-            .arg(Arg::from_usage("--gen-auth 'Generate a one-off username:password set for authentication'").conflicts_with("auth"))
+            .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'")
+                .validator(Options::path_credentials_validator))
+            .arg(Arg::from_usage("--gen-path-auth [PATH]... 'Generate a one-off username:password set for authentication under PATH'"))
             .get_matches();
 
         let dir = matches.value_of("DIR").unwrap_or(".");
         let dir_pb = fs::canonicalize(dir).unwrap();
         let follow_symlinks = !matches.is_present("no-follow-symlinks");
+
+        let mut path_auth_data = BTreeMap::new();
+        if let Some(root_auth) = matches.value_of("auth").map(Options::normalise_credentials) {
+            path_auth_data.insert("".to_string(), Some(root_auth));
+        }
+
+        if let Some(path_auth) = matches.values_of("path-auth") {
+            for (path, auth) in path_auth.map(Options::decode_path_credentials) {
+                match path_auth_data.entry(path) {
+                    BTreeMapEntry::Occupied(oe) => Options::path_credentials_dupe(oe.key()),
+                    BTreeMapEntry::Vacant(ve) => ve.insert(auth.map(Options::normalise_credentials)),
+                };
+            }
+        }
+
+        let mut generate_path_auth = BTreeSet::new();
+        if matches.is_present("gen-auth") {
+            generate_path_auth.insert("".to_string());
+        }
+
+        if let Some(gen_path_auth) = matches.values_of("gen-path-auth") {
+            for path in gen_path_auth.map(Options::normalise_path) {
+                if path_auth_data.contains_key(&path) {
+                    Options::path_credentials_dupe(&path);
+                }
+
+                if let Some(path) = generate_path_auth.replace(path) {
+                    Options::path_credentials_dupe(&path);
+                }
+            }
+        }
+
         Options {
             hosted_directory: (dir.to_string(), dir_pb.clone()),
             port: matches.value_of("port").map(u16::from_str).map(Result::unwrap),
@@ -116,15 +154,8 @@ impl Options {
             encode_fs: !matches.is_present("no-encode"),
             tls_data: matches.value_of("ssl").map(|id| ((id.to_string(), fs::canonicalize(id).unwrap()), env::var("HTTP_SSL_PASS").unwrap_or(String::new()))),
             generate_tls: matches.is_present("gen-ssl"),
-            auth_data: matches.value_of("auth").map(|auth| {
-                if auth.ends_with(':') {
-                        &auth[0..auth.len() - 1]
-                    } else {
-                        auth
-                    }
-                    .to_string()
-            }),
-            generate_auth: matches.is_present("gen-auth"),
+            path_auth_data: path_auth_data,
+            generate_path_auth: generate_path_auth,
         }
     }
 
@@ -148,8 +179,61 @@ impl Options {
         if CREDENTIALS_REGEX.is_match(&s) {
             Ok(())
         } else {
-            Err(format!("Authentication credentials \"{}\" need be in format \"username[:password]\"", s))
+            Err(format!("Global authentication credentials \"{}\" need be in format \"username[:password]\"", s))
+        }
+    }
+
+    fn path_credentials_validator(s: String) -> Result<(), String> {
+        if PATH_CREDENTIALS_REGEX.is_match(&s) {
+            Ok(())
+        } else {
+            Err(format!("Per-path authentication credentials \"{}\" need be in format \"path=[username[:password]]\"", s))
+        }
+    }
+
+    fn decode_path_credentials(s: &str) -> (String, Option<&str>) {
+        let creds = PATH_CREDENTIALS_REGEX.captures(s).unwrap();
+
+        (Options::normalise_path(&creds[1]), creds.get(2).map(|m| m.as_str()))
+    }
+
+    fn path_credentials_dupe(path: &str) -> ! {
+        ClapError {
+                message: format!("Credentials for path \"/{}\" already present", path),
+                kind: ClapErrorKind::ArgumentConflict,
+                info: None,
+            }
+            .exit()
+    }
+
+    fn normalise_path(path: &str) -> String {
+        let mut frags = vec![];
+        for fragment in path.split(|c| c == '/' || c == '\\') {
+            match fragment {
+                "" | "." => {}
+                ".." => {
+                    frags.pop();
+                }
+                _ => frags.push(fragment),
+            }
         }
+
+        let mut ret = String::with_capacity(frags.iter().map(|s| s.len()).sum::<usize>() + frags.len());
+        for frag in frags {
+            ret.push_str(frag);
+            ret.push('/');
+        }
+        ret.pop();
+        ret
+    }
+
+    fn normalise_credentials(creds: &str) -> String {
+        if creds.ends_with(':') {
+                &creds[0..creds.len() - 1]
+            } else {
+                creds
+            }
+            .to_string()
     }
 
     fn u16_validator(s: String) -> Result<(), String> {
diff --git a/src/util/mod.rs b/src/util/mod.rs
index dccf80c..def756a 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -6,10 +6,10 @@ mod content_encoding;
 
 use base64;
 use std::path::Path;
+use percent_encoding;
 use std::borrow::Cow;
 use rfsapi::RawFileData;
 use std::{cmp, f64, fmt};
-use url::percent_encoding;
 use iron::headers::UserAgent;
 use std::collections::HashMap;
 use time::{self, Duration, Tm};


Test patch: