Repository: thecoshman/http (language: rust)

Issue:
.deb installer
Pirate said he wanted them, so a definite addition, but I need a feature that's currently not in and am e-mailing the owner with feature requests, so don't hold up #55 with this, since it could take a long time.


Discussion / hints:

Fix patch:
diff --git a/.travis.yml b/.travis.yml
index 6c18457..e116719 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,6 +13,9 @@ matrix:
     - env: LANGUAGE=Rust CLIPPY=true
       language: rust
       rust: nightly
+    - env: LANGUAGE=Rust-deb DEPLOY=true DEPLOY_FILE="$TRAVIS_BUILD_DIR/target/debian"
+      language: rust
+      rust: stable
     - env: LANGUAGE=Ruby DEPLOY=true DEPLOY_FILE="$TRAVIS_BUILD_DIR/../http-man-$TRAVIS_TAG.tbz2"
       language: ruby
       rust: []
@@ -23,6 +26,10 @@ matrix:
 
 install:
   - if [ "$LANGUAGE" == "Ruby" ]; then gem install ronn; fi
+  - if [ "$LANGUAGE" == "Rust-deb" ]; then
+      export DEPLOY_FILE="$DEPLOY_FILE/http_$(awk '/version/ {print(gensub(/"/, "", "g", $3)); exit}' Cargo.toml)_amd64.deb";
+      cargo install cargo-deb || :;
+    fi
 
 script:
   - if [ "$LANGUAGE" == "Rust" ]; then cargo build --verbose; fi
@@ -34,6 +41,8 @@ script:
       cargo clippy;
     fi
   -
+  - if [ "$LANGUAGE" == "Rust-deb" ]; then cargo deb; fi
+  -
   - if [ "$LANGUAGE" == "Ruby" ]; then
       mkdir man; pushd man;
       cp ../http.md .;
diff --git a/Cargo.toml b/Cargo.toml
index c2f161e..e054936 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.6.0"
+version = "1.7.0"
 # Remember to also update in http.md
 authors = ["thecoshman <rust@thecoshman.com>",
            "nabijaczleweli <nabijaczleweli@gmail.com>",
@@ -66,3 +66,9 @@ name = "httplz"
 path = "src/main.rs"
 test = false
 doc = false
+
+
+[package.metadata.deb]
+name = "http"
+maintainer = "nabijaczleweli <nabijaczleweli@gmail.com>"
+section = "web"
diff --git a/README.md b/README.md
index c7e64bd..73048ec 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,8 @@ set PREFIX=D:\Akces
 curl -SsL https://cdn.rawgit.com/thecoshman/http/master/install.sh | sh
 ```
 
+If you're on a Debian-based amd64 machine, you can also grab a `.deb` package from the [latest release page](https://github.com/thecoshman/http/releases/latest).
+
 If you're on Windows and prefer a more guided installation (or you don't have a shell),
 you can download the Windows installer from the [latest release's page](https://github.com/thecoshman/http/releases/latest).
 (Note: you can add /D *INSTALLDIR* to installer command line to change the installation directory.)
diff --git a/appveyor.yml b/appveyor.yml
index 62edfef..00eece9 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 1.6.0-{build}
+version: 1.7.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.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
+  - cp target\release\http.exe http-v1.7.0.exe
+  - strip --strip-all --remove-section=.comment --remove-section=.note http-v1.7.0.exe
+  - makensis -DHTTP_VERSION=v1.7.0 "/X!include \"EnvVarUpdate.nsh\"" install.nsi
 
 test: off
 test_script:
   - cargo test --verbose --release
 
 artifacts:
-  - path: http-v1.6.0.exe
-  - path: http v1.6.0 installer.exe
+  - path: http-v1.7.0.exe
+  - path: http v1.7.0 installer.exe
 
 deploy:
   provider: GitHub
-  artifact: /http.*v1.6.0.*\.exe/
+  artifact: /http.*v1.7.0.*\.exe/
   auth_token:
     secure: ZTXvCrv9y01s7Hd60w8W7NaouPnPoaw9YJt9WhWQ2Pep8HLvCikt9Exjkz8SGP9P
   on:
diff --git a/http.md b/http.md
index 253e256..690f816 100644
--- a/http.md
+++ b/http.md
@@ -32,6 +32,12 @@ pass parameters like what port to use.
 
     Default: first free port from 8000 up.
 
+  -a --address [ADDRESS]
+
+    IP to bind the server to.
+
+    Default: 0.0.0.0.
+
   -t --temp-dir [TEMP]
 
     Temporary directory to use to store data to write.
@@ -147,6 +153,16 @@ pass parameters like what port to use.
 
     This is false by default because it's useful for reducing bandwidth usage.
 
+  -q --quiet...
+
+    Suppress increasing amounts of output.
+
+    Specifying this flag N times will, for:
+      N == 0 – show all output
+      N >= 1 – suppress serving status lines ("IP was served something")
+      N >= 2 – suppress startup except for auth data, if present
+      N >= 3 – suppress all startup messages
+
 ## EXAMPLES
 
   `http`
@@ -222,6 +238,18 @@ pass parameters like what port to use.
       Starting server failed: port taken.
       <EOF>
 
+  `http -a 192.168.65.1`
+
+    As in the first example, but listen on address 192.168.65.21.
+
+    Assuming the address can be used, example output change:
+      Hosting "." on port 8000 under address 192.168.65.1 without TLS and no authentication...
+
+    If the address is inaccessible or doesn't exist, example output change:
+      Starting server failed: The requested address is not valid in its context.
+      (os error 10049).
+      <EOF>
+
   `HTTP_SSL_PASS=pwd http --ssl cert/http8k.p12`
 
     As in the first example, but encrypt with the identity file cert/http8k.p12
diff --git a/src/main.rs b/src/main.rs
index 77d7be7..d4b8a5a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -36,6 +36,7 @@ pub use options::Options;
 
 use std::mem;
 use iron::Iron;
+use std::net::IpAddr;
 use std::process::exit;
 use tabwriter::TabWriter;
 use std::io::{Write, stdout};
@@ -69,7 +70,7 @@ fn result_main() -> Result<(), Error> {
 
     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),
+                Iron::new(ops::HttpHandler::new(&opts)).https((opts.bind_address, p),
                                                               NativeTlsServer::new(id, pw).map_err(|err| {
                         Error {
                             desc: "TLS certificate",
@@ -78,7 +79,7 @@ fn result_main() -> Result<(), Error> {
                         }
                     })?)
             } else {
-                Iron::new(ops::HttpHandler::new(&opts)).http(("0.0.0.0", p))
+                Iron::new(ops::HttpHandler::new(&opts)).http((opts.bind_address, p))
             }
             .map_err(|_| {
                 Error {
@@ -88,25 +89,35 @@ fn result_main() -> Result<(), Error> {
                 }
             })
     } else {
-        ops::try_ports(ops::HttpHandler::new(&opts), util::PORT_SCAN_LOWEST, util::PORT_SCAN_HIGHEST, &opts.tls_data)
+        ops::try_ports(ops::HttpHandler::new(&opts),
+                       opts.bind_address,
+                       util::PORT_SCAN_LOWEST,
+                       util::PORT_SCAN_HIGHEST,
+                       &opts.tls_data)
     }?;
 
-    print!("{}Hosting \"{}\" on port {} with",
-           trivial_colours::Reset,
-           opts.hosted_directory.0,
-           responder.socket.port());
-    if let Some(&((ref id, _), _)) = opts.tls_data.as_ref() {
-        print!(" TLS certificate from \"{}\"", id);
-    } else {
-        print!("out TLS");
-    }
-    if !opts.path_auth_data.is_empty() {
-        print!(" and basic authentication");
-    } else {
-        print!(" and no authentication");
+    if opts.loglevel < ops::LogLevel::NoStartup {
+        print!("{}Hosting \"{}\" on port {}",
+               trivial_colours::Reset,
+               opts.hosted_directory.0,
+               responder.socket.port());
+        if responder.socket.ip() != IpAddr::from([0, 0, 0, 0]) {
+            print!(" under address {}", responder.socket.ip());
+        }
+        print!(" with");
+        if let Some(&((ref id, _), _)) = opts.tls_data.as_ref() {
+            print!(" TLS certificate from \"{}\"", id);
+        } else {
+            print!("out TLS");
+        }
+        if !opts.path_auth_data.is_empty() {
+            print!(" and basic authentication");
+        } else {
+            print!(" and no authentication");
+        }
+        println!("...");
     }
-    println!("...");
-    if !opts.path_auth_data.is_empty() {
+    if !opts.path_auth_data.is_empty() && opts.loglevel < ops::LogLevel::NoAuth {
         println!("Basic authentication credentials:");
 
         let mut out = TabWriter::new(stdout());
@@ -127,8 +138,10 @@ fn result_main() -> Result<(), Error> {
 
         out.flush().unwrap();
     }
-    println!("Ctrl-C to stop.");
-    println!();
+    if opts.loglevel < ops::LogLevel::NoStartup {
+        println!("Ctrl-C to stop.");
+        println!();
+    }
 
     let end_handler = Arc::new(Condvar::new());
     ctrlc::set_handler({
@@ -141,7 +154,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);
+    ops::HttpHandler::clean_temp_dirs(&opts.temp_directory, opts.loglevel);
 
     Ok(())
 }
diff --git a/src/ops.rs b/src/ops.rs
index 36e47f8..9a9e690 100644
--- a/src/ops.rs
+++ b/src/ops.rs
@@ -3,6 +3,7 @@ use std::iter;
 use time::now;
 use serde_json;
 use std::borrow::Cow;
+use std::net::IpAddr;
 use serde::Serialize;
 use unicase::UniCase;
 use iron::mime::Mime;
@@ -32,32 +33,36 @@ use self::super::util::{WwwAuthenticate, url_path, file_hash, is_symlink, encode
 
 
 macro_rules! log {
-    ($fmt:expr) => {
-        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);
+    ($do_log:expr, $fmt:expr) => {
+        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);
+        }
     };
-    ($fmt:expr, $($arg:tt)*) => {
-        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);
+    ($do_log:expr, $fmt:expr, $($arg:tt)*) => {
+        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);
+        }
     };
 }
 
@@ -70,6 +75,7 @@ pub struct HttpHandler {
     pub follow_symlinks: bool,
     pub sandbox_symlinks: bool,
     pub check_indices: bool,
+    pub log: 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)>,
@@ -102,6 +108,7 @@ impl HttpHandler {
             follow_symlinks: opts.follow_symlinks,
             sandbox_symlinks: opts.sandbox_symlinks,
             check_indices: opts.check_indices,
+            log: opts.loglevel < LogLevel::NoServeStatus,
             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"),
@@ -111,10 +118,10 @@ impl HttpHandler {
         }
     }
 
-    pub fn clean_temp_dirs(temp_dir: &(String, PathBuf)) {
+    pub fn clean_temp_dirs(temp_dir: &(String, PathBuf), loglevel: LogLevel) {
         for (temp_name, temp_dir) in ["writes", "encoded", "tls"].into_iter().flat_map(|tn| HttpHandler::temp_subdir(temp_dir, true, tn)) {
             if temp_dir.exists() && fs::remove_dir_all(&temp_dir).is_ok() {
-                log!("Deleted temp dir {magenta}{}{reset}", temp_name);
+                log!(loglevel < LogLevel::NoServeStatus, "Deleted temp dir {magenta}{}{reset}", temp_name);
             }
         }
     }
@@ -199,14 +206,16 @@ impl HttpHandler {
                 };
 
                 if &auth.0 == username && &auth.1 == pwd {
-                    log!("{green}{}{reset} correctly authorised to {red}{}{reset} {yellow}{}{reset}",
+                    log!(self.log,
+                         "{green}{}{reset} correctly authorised to {red}{}{reset} {yellow}{}{reset}",
                          req.remote_addr,
                          req.method,
                          req.url);
 
                     Ok(None)
                 } else {
-                    log!("{green}{}{reset} requested to {red}{}{reset} {yellow}{}{reset} with invalid credentials \"{}{}{}\"",
+                    log!(self.log,
+                         "{green}{}{reset} requested to {red}{}{reset} {yellow}{}{reset} with invalid credentials \"{}{}{}\"",
                          req.remote_addr,
                          req.method,
                          req.url,
@@ -218,7 +227,8 @@ impl HttpHandler {
                 }
             }
             None => {
-                log!("{green}{}{reset} requested to {red}{}{reset} {yellow}{}{reset} without authorisation",
+                log!(self.log,
+                     "{green}{}{reset} requested to {red}{}{reset} {yellow}{}{reset} without authorisation",
                      req.remote_addr,
                      req.method,
                      req.url);
@@ -229,7 +239,7 @@ impl HttpHandler {
     }
 
     fn handle_options(&self, req: &mut Request) -> IronResult<Response> {
-        log!("{green}{}{reset} asked for {red}OPTIONS{reset}", req.remote_addr);
+        log!(self.log, "{green}{}{reset} asked for {red}OPTIONS{reset}", req.remote_addr);
         Ok(Response::with((status::NoContent,
                            Header(headers::Server(USER_AGENT.to_string())),
                            Header(headers::Allow(vec![method::Options, method::Get, method::Put, method::Delete, method::Head, method::Trace])))))
@@ -268,11 +278,12 @@ impl HttpHandler {
     }
 
     fn handle_invalid_url(&self, req: &mut Request, cause: &str) -> IronResult<Response> {
-        log!("{green}{}{reset} requested to {red}{}{reset} {yellow}{}{reset} with invalid URL -- {}",
+        log!(self.log,
+             "{green}{}{reset} requested to {red}{}{reset} {yellow}{}{reset} with invalid URL -- {}",
              req.remote_addr,
              req.method,
              req.url,
-             cause.replace("<p>", "").replace("</p>", ""));
+             &cause[3..cause.len() - 4]); // Strip <p> tags
 
         self.handle_generated_response_encoding(req,
                                                 status::BadRequest,
@@ -280,7 +291,8 @@ impl HttpHandler {
     }
 
     fn handle_nonexistant(&self, req: &mut Request, req_p: PathBuf) -> IronResult<Response> {
-        log!("{green}{}{reset} requested to {red}{}{reset} nonexistant entity {magenta}{}{reset}",
+        log!(self.log,
+             "{green}{}{reset} requested to {red}{}{reset} nonexistant entity {magenta}{}{reset}",
              req.remote_addr,
              req.method,
              req_p.display());
@@ -293,7 +305,8 @@ impl HttpHandler {
     }
 
     fn handle_get_raw_fs_file(&self, req: &mut Request, req_p: PathBuf) -> IronResult<Response> {
-        log!("{green}{}{reset} was served metadata for file {magenta}{}{reset}",
+        log!(self.log,
+             "{green}{}{reset} was served metadata for file {magenta}{}{reset}",
              req.remote_addr,
              req_p.display());
         self.handle_raw_fs_api_response(status::Ok,
@@ -342,7 +355,8 @@ impl HttpHandler {
         } else {
             "text/plain".parse().unwrap()
         });
-        log!("{green}{}{reset} was served byte range {}-{} of file {magenta}{}{reset} as {blue}{}{reset}",
+        log!(self.log,
+             "{green}{}{reset} was served byte range {}-{} of file {magenta}{}{reset} as {blue}{}{reset}",
              req.remote_addr,
              from,
              to,
@@ -372,7 +386,8 @@ impl HttpHandler {
         } else {
             "text/plain".parse().unwrap()
         });
-        log!("{green}{}{reset} was served file {magenta}{}{reset} from byte {} as {blue}{}{reset}",
+        log!(self.log,
+             "{green}{}{reset} was served file {magenta}{}{reset} from byte {} as {blue}{}{reset}",
              req.remote_addr,
              req_p.display(),
              from,
@@ -388,7 +403,8 @@ impl HttpHandler {
         } else {
             "text/plain".parse().unwrap()
         });
-        log!("{green}{}{reset} was served last {} bytes of file {magenta}{}{reset} as {blue}{}{reset}",
+        log!(self.log,
+             "{green}{}{reset} was served last {} bytes of file {magenta}{}{reset} as {blue}{}{reset}",
              req.remote_addr,
              from,
              req_p.display(),
@@ -433,7 +449,8 @@ impl HttpHandler {
         } else {
             "text/plain".parse().unwrap()
         });
-        log!("{green}{}{reset} was served an empty range from file {magenta}{}{reset} as {blue}{}{reset}",
+        log!(self.log,
+             "{green}{}{reset} was served an empty range from file {magenta}{}{reset} as {blue}{}{reset}",
              req.remote_addr,
              req_p.display(),
              mime_type);
@@ -455,7 +472,8 @@ impl HttpHandler {
         } else {
             "text/plain".parse().unwrap()
         });
-        log!("{green}{}{reset} was served file {magenta}{}{reset} as {blue}{}{reset}",
+        log!(self.log,
+             "{green}{}{reset} was served file {magenta}{}{reset} as {blue}{}{reset}",
              req.remote_addr,
              req_p.display(),
              mime_type);
@@ -484,7 +502,8 @@ impl HttpHandler {
             {
                 match self.cache_fs.read().expect("Filesystem cache read lock poisoned").get(&cache_key) {
                     Some(&(ref resp_p, true)) => {
-                        log!("{} encoded as {} for {:.1}% ratio (cached)",
+                        log!(self.log,
+                             "{} encoded as {} for {:.1}% ratio (cached)",
                              iter::repeat(' ').take(req.remote_addr.to_string().len()).collect::<String>(),
                              encoding,
                              ((file_length(&req_p.metadata().expect("Failed to get requested file metadata"), &req_p) as f64) /
@@ -526,7 +545,8 @@ impl HttpHandler {
                     cache.insert(cache_key, (req_p.clone(), false));
                     fs::remove_file(resp_p).expect("Failed to remove too big encoded file");
                 } else {
-                    log!("{} encoded as {} for {:.1}% ratio",
+                    log!(self.log,
+                         "{} encoded as {} for {:.1}% ratio",
                          iter::repeat(' ').take(req.remote_addr.to_string().len()).collect::<String>(),
                          encoding,
                          gain * 100f64);
@@ -542,7 +562,8 @@ impl HttpHandler {
                                               mt)));
                 }
             } else {
-                log!("{} failed to encode as {}, sending identity",
+                log!(self.log,
+                     "{} failed to encode as {}, sending identity",
                      iter::repeat(' ').take(req.remote_addr.to_string().len()).collect::<String>(),
                      encoding);
             }
@@ -558,7 +579,8 @@ impl HttpHandler {
     }
 
     fn handle_get_raw_fs_dir(&self, req: &mut Request, req_p: PathBuf) -> IronResult<Response> {
-        log!("{green}{}{reset} was served metadata for directory {magenta}{}{reset}",
+        log!(self.log,
+             "{green}{}{reset} was served metadata for directory {magenta}{}{reset}",
              req.remote_addr,
              req_p.display());
         self.handle_raw_fs_api_response(status::Ok,
@@ -609,7 +631,8 @@ impl HttpHandler {
                 }) {
                 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}",
+                    log!(self.log,
+                         "{} found index file for directory {magenta}{}{reset}",
                          iter::repeat(' ').take(req.remote_addr.to_string().len()).collect::<String>(),
                          req_p.display());
                     return r;
@@ -628,7 +651,8 @@ impl HttpHandler {
 
     fn handle_get_dir_index_no_slash(&self, req: &mut Request, idx_ext: &str) -> IronResult<Response> {
         let new_url = req.url.to_string() + "/";
-        log!("Redirecting {green}{}{reset} to {yellow}{}{reset} - found index file {magenta}index.{}{reset}",
+        log!(self.log,
+             "Redirecting {green}{}{reset} to {yellow}{}{reset} - found index file {magenta}index.{}{reset}",
              req.remote_addr,
              new_url,
              idx_ext);
@@ -645,7 +669,8 @@ 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.as_ref().path_segments().unwrap().count() == 1;
-        log!("{green}{}{reset} was served mobile directory listing for {magenta}{}{reset}",
+        log!(self.log,
+             "{green}{}{reset} was served mobile directory listing for {magenta}{}{reset}",
              req.remote_addr,
              req_p.display());
 
@@ -732,7 +757,8 @@ 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.as_ref().path_segments().unwrap().count() == 1;
-        log!("{green}{}{reset} was served directory listing for {magenta}{}{reset}",
+        log!(self.log,
+             "{green}{}{reset} was served directory listing for {magenta}{}{reset}",
              req.remote_addr,
              req_p.display());
 
@@ -861,7 +887,8 @@ impl HttpHandler {
             })
             .to_string();
 
-        log!("{green}{}{reset} tried to {red}{}{reset} on {magenta}{}{reset} ({blue}{}{reset}) but only {red}{}{reset} are allowed",
+        log!(self.log,
+             "{green}{}{reset} tried to {red}{}{reset} on {magenta}{}{reset} ({blue}{}{reset}) but only {red}{}{reset} are allowed",
              req.remote_addr,
              req.method,
              url_path(&req.url),
@@ -879,7 +906,8 @@ impl HttpHandler {
     }
 
     fn handle_put_partial_content(&self, req: &mut Request) -> IronResult<Response> {
-        log!("{green}{}{reset} tried to {red}PUT{reset} partial content to {yellow}{}{reset}",
+        log!(self.log,
+             "{green}{}{reset} tried to {red}PUT{reset} partial content to {yellow}{}{reset}",
              req.remote_addr,
              url_path(&req.url));
 
@@ -894,7 +922,8 @@ impl HttpHandler {
 
     fn handle_put_file(&self, req: &mut Request, req_p: PathBuf, legal: bool) -> IronResult<Response> {
         let existant = !legal || req_p.exists();
-        log!("{green}{}{reset} {} {magenta}{}{reset}, size: {}B",
+        log!(self.log,
+             "{green}{}{reset} {} {magenta}{}{reset}, size: {}B",
              req.remote_addr,
              if !legal {
                  "tried to illegally create"
@@ -942,9 +971,11 @@ impl HttpHandler {
     }
 
     fn handle_delete_path(&self, req: &mut Request, req_p: PathBuf, symlink: bool) -> IronResult<Response> {
-        log!("{green}{}{reset} deleted {blue}{} {magenta}{}{reset}",
+        let file = is_actually_file(&req_p.metadata().expect("failed to get file metadata").file_type());
+        log!(self.log,
+             "{green}{}{reset} deleted {blue}{} {magenta}{}{reset}",
              req.remote_addr,
-             if is_actually_file(&req_p.metadata().expect("failed to get file metadata").file_type()) {
+             if file {
                  "file"
              } else if symlink {
                  "symlink"
@@ -953,7 +984,7 @@ impl HttpHandler {
              },
              req_p.display());
 
-        if is_actually_file(&req_p.metadata().expect("failed to get file metadata").file_type()) {
+        if file {
             fs::remove_file(req_p).expect("Failed to remove requested file");
         } else {
             fs::remove_dir_all(req_p).expect(if symlink {
@@ -967,7 +998,8 @@ impl HttpHandler {
     }
 
     fn handle_trace(&self, req: &mut Request) -> IronResult<Response> {
-        log!("{green}{}{reset} requested {red}TRACE{reset} for {magenta}{}{reset}",
+        log!(self.log,
+             "{green}{}{reset} requested {red}TRACE{reset} for {magenta}{}{reset}",
              req.remote_addr,
              url_path(&req.url));
 
@@ -983,7 +1015,8 @@ impl HttpHandler {
     }
 
     fn handle_forbidden_method(&self, req: &mut Request, switch: &str, desc: &str) -> IronResult<Response> {
-        log!("{green}{}{reset} used disabled request method {red}{}{reset} grouped under {}",
+        log!(self.log,
+             "{green}{}{reset} used disabled request method {red}{}{reset} grouped under {}",
              req.remote_addr,
              req.method,
              desc);
@@ -1000,7 +1033,10 @@ impl HttpHandler {
     }
 
     fn handle_bad_method(&self, req: &mut Request) -> IronResult<Response> {
-        log!("{green}{}{reset} used invalid request method {red}{}{reset}", req.remote_addr, req.method);
+        log!(self.log,
+             "{green}{}{reset} used invalid request method {red}{}{reset}",
+             req.remote_addr,
+             req.method);
 
         let last_p = format!("<p>Unsupported request method: {}.<br />\nSupported methods: OPTIONS, GET, PUT, DELETE, HEAD and TRACE.</p>",
                              req.method);
@@ -1016,7 +1052,8 @@ impl HttpHandler {
 
             {
                 if let Some(enc_resp) = self.cache_gen.read().expect("Generated file cache read lock poisoned").get(&cache_key) {
-                    log!("{} encoded as {} for {:.1}% ratio (cached)",
+                    log!(self.log,
+                         "{} encoded as {} for {:.1}% ratio (cached)",
                          iter::repeat(' ').take(req.remote_addr.to_string().len()).collect::<String>(),
                          encoding,
                          ((resp.len() as f64) / (enc_resp.len() as f64)) * 100f64);
@@ -1030,7 +1067,8 @@ impl HttpHandler {
             }
 
             if let Some(enc_resp) = encode_str(&resp, &encoding) {
-                log!("{} encoded as {} for {:.1}% ratio",
+                log!(self.log,
+                     "{} encoded as {} for {:.1}% ratio",
                      iter::repeat(' ').take(req.remote_addr.to_string().len()).collect::<String>(),
                      encoding,
                      ((resp.len() as f64) / (enc_resp.len() as f64)) * 100f64);
@@ -1044,7 +1082,8 @@ impl HttpHandler {
                                           "text/html;charset=utf-8".parse::<mime::Mime>().unwrap(),
                                           &cache[&cache_key][..])));
             } else {
-                log!("{} failed to encode as {}, sending identity",
+                log!(self.log,
+                     "{} failed to encode as {}, sending identity",
                      iter::repeat(' ').take(req.remote_addr.to_string().len()).collect::<String>(),
                      encoding);
             }
@@ -1093,7 +1132,7 @@ impl HttpHandler {
     fn create_temp_dir(&self, td: &Option<(String, PathBuf)>) {
         let &(ref temp_name, ref temp_dir) = td.as_ref().unwrap();
         if !temp_dir.exists() && fs::create_dir_all(&temp_dir).is_ok() {
-            log!("Created temp dir {magenta}{}{reset}", temp_name);
+            log!(self.log, "Created temp dir {magenta}{}{reset}", temp_name);
         }
     }
 }
@@ -1105,6 +1144,7 @@ impl Clone for HttpHandler {
             follow_symlinks: self.follow_symlinks,
             sandbox_symlinks: self.sandbox_symlinks,
             check_indices: self.check_indices,
+            log: self.log,
             global_auth_data: self.global_auth_data.clone(),
             path_auth_data: self.path_auth_data.clone(),
             writes_temp_dir: self.writes_temp_dir.clone(),
@@ -1116,6 +1156,30 @@ impl Clone for HttpHandler {
 }
 
 
+#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+pub enum LogLevel {
+    /// Write everything
+    All,
+    /// No serving messages
+    NoServeStatus,
+    /// No startup messages, but yes auth data
+    NoStartup,
+    /// No auth data
+    NoAuth,
+}
+
+impl From<u64> for LogLevel {
+    fn from(raw: u64) -> LogLevel {
+        match raw {
+            0 => LogLevel::All,
+            1 => LogLevel::NoServeStatus,
+            2 => LogLevel::NoStartup,
+            _ => LogLevel::NoAuth,
+        }
+    }
+}
+
+
 /// Attempt to start a server on ports from `from` to `up_to`, inclusive, with the specified handler.
 ///
 /// If an error other than the port being full is encountered it is returned.
@@ -1131,12 +1195,13 @@ impl Clone for HttpHandler {
 /// # use iron::{status, Response};
 /// let server = try_ports(|req| Ok(Response::with((status::Ok, "Abolish the burgeoisie!"))), 8000, 8100, None).unwrap();
 /// ```
-pub fn try_ports<H: Handler + Clone>(hndlr: H, from: u16, up_to: u16, tls_data: &Option<((String, PathBuf), String)>) -> Result<Listening, Error> {
+pub fn try_ports<H: Handler + Clone>(hndlr: H, addr: IpAddr, from: u16, up_to: u16, tls_data: &Option<((String, PathBuf), String)>)
+                                     -> Result<Listening, Error> {
     let hndlr = hndlr;
     for port in from..up_to + 1 {
         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),
+            ir.https((addr, port),
                      NativeTlsServer::new(id, pw).map_err(|err| {
                     Error {
                         desc: "TLS certificate",
@@ -1145,7 +1210,7 @@ pub fn try_ports<H: Handler + Clone>(hndlr: H, from: u16, up_to: u16, tls_data:
                     }
                 })?)
         } else {
-            ir.http(("0.0.0.0", port))
+            ir.http((addr, port))
         } {
             Ok(server) => return Ok(server),
             Err(error) => {
diff --git a/src/options.rs b/src/options.rs
index a136a3e..7f0aa8b 100644
--- a/src/options.rs
+++ b/src/options.rs
@@ -14,9 +14,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 self::super::ops::LogLevel;
 use std::env::{self, temp_dir};
 use std::path::PathBuf;
 use std::str::FromStr;
+use std::net::IpAddr;
 use regex::Regex;
 use std::fs;
 
@@ -34,6 +36,8 @@ pub struct Options {
     pub hosted_directory: (String, PathBuf),
     /// The port to host on. Default: first free port from 8000 up
     pub port: Option<u16>,
+    /// The address to bind to. Default: 0.0.0.0
+    pub bind_address: IpAddr,
     /// Whether to allow symlinks to be requested. Default: true
     pub follow_symlinks: bool,
     /// Whether to disallow going out of the descendants of the hosted directory (via symlinks)
@@ -51,6 +55,12 @@ pub struct Options {
     pub allow_writes: bool,
     /// Whether to encode filesystem files. Default: true
     pub encode_fs: bool,
+    /// How much to suppress output
+    ///
+    ///   * >= 1 – suppress serving status lines ("IP was served something")
+    ///   * >= 2 – suppress startup except for auth data, if present
+    ///   * >= 3 – suppress all startup messages
+    pub loglevel: LogLevel,
     /// 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
@@ -72,6 +82,7 @@ impl Options {
             .arg(Arg::from_usage("[DIR] 'Directory to host. Default: current working directory'")
                 .validator(|s| Options::filesystem_dir_validator(s, "Directory to host")))
             .arg(Arg::from_usage("-p --port [port] 'Port to use. Default: first free port from 8000 up'").validator(Options::u16_validator))
+            .arg(Arg::from_usage("-a --address [address] 'Address to bind to. Default: 0.0.0.0'").validator(Options::ipaddr_validator))
             .arg(Arg::from_usage("-t --temp-dir [temp] 'Temporary directory. Default: $TEMP'")
                 .validator(|s| Options::filesystem_dir_validator(s, "Temporary directory")))
             .arg(Arg::from_usage("-s --no-follow-symlinks 'Don't follow symlinks. Default: false'"))
@@ -80,6 +91,7 @@ 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("-q --quiet... 'Suppress increasing amounts of output'"))
             .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"))
@@ -128,6 +140,7 @@ impl Options {
         Options {
             hosted_directory: (dir.to_string(), dir_pb.clone()),
             port: matches.value_of("port").map(u16::from_str).map(Result::unwrap),
+            bind_address: matches.value_of("address").map(IpAddr::from_str).map(Result::unwrap).unwrap_or_else(|| "0.0.0.0".parse().unwrap()),
             follow_symlinks: follow_symlinks,
             sandbox_symlinks: follow_symlinks && matches.is_present("sandbox-symlinks"),
             temp_directory: {
@@ -152,6 +165,7 @@ impl Options {
             check_indices: !matches.is_present("no-indices"),
             allow_writes: matches.is_present("allow-write"),
             encode_fs: !matches.is_present("no-encode"),
+            loglevel: matches.occurrences_of("quiet").into(),
             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"),
             path_auth_data: path_auth_data,
@@ -236,6 +250,10 @@ impl Options {
             .to_string()
     }
 
+    fn ipaddr_validator(s: String) -> Result<(), String> {
+        IpAddr::from_str(&s).map(|_| ()).map_err(|_| format!("{} is not a valid IP address", s))
+    }
+
     fn u16_validator(s: String) -> Result<(), String> {
         u16::from_str(&s).map(|_| ()).map_err(|_| format!("{} is not a valid port number", s))
     }


Test patch: