Repository: thecoshman/http (language: rust)

Issue:
Exception when when accessing a folder that is missing permissions
This issue ties in with #94 .
This has been tested under x86_64 Linux, but is probably also the case under other platforms.

## General Info
- Kernel: `Linux hzd 5.6.4-arch1-1 #1 SMP PREEMPT Mon, 13 Apr 2020 12:21:19 +0000 x86_64 GNU/Linux`
- http: `http 1.9.1`

## Error
given a folder tree similar to this

```
.
|-- [-rw-r--r-- aloy    ]  a.txt
|-- [drwx------ root    ]  not_work
|   `-- [-rw-r--r-- root    ]  c.txt
`-- [drwxr-xr-x aloy    ]  works
    `-- [-rw-r--r-- aloy    ]  b.txt
```

when running http as user `aloy`, it will error when attempting to access the `not_work` folder instead of returning a sensible status code like `403: Forbidden`.

```
Hosting "." on port 8000 without TLS and no authentication...
Ctrl-C to stop.

[2020-04-22 12:35:19] 127.0.0.1:51948 was served directory listing for /tmp/example
[2020-04-22 12:35:19]                           encoded as gzip for 336.6% ratio
[2020-04-22 12:35:24] 127.0.0.1:51948 was served file /tmp/example/a.txt as text/plain
[2020-04-22 12:35:24] 127.0.0.1:51948 requested to GET nonexistent entity /tmp/example/favicon.ico
[2020-04-22 12:35:24]                           encoded as gzip for 519.4% ratio
[2020-04-22 12:35:27] 127.0.0.1:51948 was served directory listing for /tmp/example/not_work
thread '<unnamed>' panicked at 'Failed to read requested directory: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }', /home/aloy/.cargo/registry/src/github.com-1ecc6299db9ec823/https-1.9.1/src/ops/mod.rs:841:22
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

## Reproducing

1. Take any folder structure which contains a folder/file which the executing user has no access permissions for.
2. Run http in that directory with no arguments
3. Navigate over a browser or other tools to the folder with the permission issue

EDIT: Clarified example


Discussion / hints:
Yeah, I remember a similar thing happening some time in past on Windows 10 with its C:/ protections; I think I fixed that particular case but this codebase has a nasty trend of panicking instead of handling errors right, mistakes of my youth and all that.

Thanks for the test-case, I'll look into it!

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index d8fd6c9..6fb4049 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.9.1"
+version = "1.9.2"
 # Remember to also update in http.md
 authors = ["thecoshman <rust@thecoshman.com>",
            "nabijaczleweli <nabijaczleweli@gmail.com>",
diff --git a/README.md b/README.md
index acf5625..0f66d33 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ See [the manpage](http.md) for full list.
   * [x] Hosting with an <sub>(optional)</sub> optionally autogenerated TLS certificate
   * [x] Arbitrarily nested username/password authentication
   * [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 (explorable from commandline with [D'Oh](https://github.com/thecoshman/doh))
+  * [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))
 
 ## [Manpage](http.md)
 
diff --git a/appveyor.yml b/appveyor.yml
index 31a41d3..cc0026b 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 1.9.1-{build}
+version: 1.9.2-{build}
 
 skip_tags: false
 
@@ -23,21 +23,21 @@ build: off
 build_script:
   - git submodule update --init --recursive
   - cargo build --verbose --release
-  - cp target\release\http.exe http-v1.9.1.exe
-  - strip --strip-all --remove-section=.comment --remove-section=.note http-v1.9.1.exe
-  - makensis -DHTTP_VERSION=v1.9.1 install.nsi
+  - cp target\release\http.exe http-v1.9.2.exe
+  - strip --strip-all --remove-section=.comment --remove-section=.note http-v1.9.2.exe
+  - makensis -DHTTP_VERSION=v1.9.2 install.nsi
 
 test: off
 test_script:
   - cargo test --verbose --release
 
 artifacts:
-  - path: http-v1.9.1.exe
-  - path: http v1.9.1 installer.exe
+  - path: http-v1.9.2.exe
+  - path: http v1.9.2 installer.exe
 
 deploy:
   provider: GitHub
-  artifact: /http.*v1.9.1.*\.exe/
+  artifact: /http.*v1.9.2.*\.exe/
   auth_token:
     secure: ZTXvCrv9y01s7Hd60w8W7NaouPnPoaw9YJt9WhWQ2Pep8HLvCikt9Exjkz8SGP9P
   on:
diff --git a/src/ops/mod.rs b/src/ops/mod.rs
index b7ccc2e..037250d 100644
--- a/src/ops/mod.rs
+++ b/src/ops/mod.rs
@@ -19,11 +19,11 @@ use mime_guess::guess_mime_type_opt;
 use hyper_native_tls::NativeTlsServer;
 use std::collections::{BTreeMap, HashMap};
 use self::super::{LogLevel, Options, Error};
-use std::io::{self, SeekFrom, Write, Read, Seek};
 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 std::io::{self, ErrorKind as IoErrorKind, SeekFrom, Write, Error as IoError, Read, Seek};
 use iron::{headers, status, method, mime, IronResult, Listening, Response, TypeMap, Request, Handler, Iron};
 use self::super::util::{WwwAuthenticate, DisplayThree, CommaList, Spaces, Dav, url_path, file_hash, is_symlink, encode_str, encode_file, file_length,
                         hash_string, html_response, file_binary, client_mobile, percent_decode, file_icon_suffix, is_actually_file, is_descendant_of,
@@ -531,11 +531,15 @@ impl HttpHandler {
            req_p.extension().and_then(|s| s.to_str()).map(|s| !BLACKLISTED_ENCODING_EXTENSIONS.contains(&UniCase::new(s))).unwrap_or(true) {
             self.handle_get_file_encoded(req, req_p, mime_type)
         } else {
+            let file = match File::open(&req_p) {
+                Ok(file) => file,
+                Err(err) => return self.handle_requested_entity_unopenable(req, err, "file"),
+            };
             Ok(Response::with((status::Ok,
                                (Header(headers::Server(USER_AGENT.to_string())),
                                 Header(headers::LastModified(headers::HttpDate(file_time_modified(&metadata)))),
                                 Header(headers::AcceptRanges(vec![headers::RangeUnit::Bytes]))),
-                               req_p.as_path(),
+                               file,
                                Header(headers::ContentLength(file_length(&metadata, &req_p))),
                                mime_type)))
         }
@@ -633,7 +637,8 @@ impl HttpHandler {
         self.handle_raw_fs_api_response(status::Ok,
                                         &FilesetData {
                                             writes_supported: self.writes_temp_dir.is_some(),
-                                            is_root: req.url.as_ref().path_segments().unwrap().count() == 1,
+                                            is_root: req.url.as_ref().path_segments().unwrap().count() + !req.url.as_ref().as_str().ends_with('/') as usize ==
+                                                     1,
                                             is_file: false,
                                             files: req_p.read_dir()
                                                 .expect("Failed to read requested directory")
@@ -838,9 +843,13 @@ impl HttpHandler {
                     up_path = slash_idx.map(|i| &rel_noslash[0..i]).unwrap_or(""),
                     up_path_slash = if slash_idx.is_some() { "/" } else { "" })
         };
-        let list_s = req_p.read_dir()
-            .expect("Failed to read requested directory")
-            .map(|p| p.expect("Failed to iterate over requested directory"))
+
+
+        let rd = match req_p.read_dir() {
+            Ok(rd) => rd,
+            Err(err) => return self.handle_requested_entity_unopenable(req, err, "directory"),
+        };
+        let list_s = rd.map(|p| p.expect("Failed to iterate over requested directory"))
             .filter(|f| {
                 let fp = f.path();
                 let mut symlink = false;
@@ -1194,6 +1203,18 @@ impl HttpHandler {
         Ok(Response::with((st, Header(headers::Server(USER_AGENT.to_string())), "text/html;charset=utf-8".parse::<mime::Mime>().unwrap(), resp)))
     }
 
+    fn handle_requested_entity_unopenable(&self, req: &mut Request, e: IoError, entity_type: &str) -> IronResult<Response> {
+        if e.kind() == IoErrorKind::PermissionDenied {
+            self.handle_generated_response_encoding(req,
+                                                    status::Forbidden,
+                                                    html_response(ERROR_HTML, &["403 Forbidden", &format!("Can't access {}.", url_path(&req.url)), ""]))
+        } else {
+            // The ops that get here (File::open(), fs::read_dir()) can't return any other errors by the time they're run
+            // (and even if it could, there isn't much we can do about them)
+            panic!("Failed to read requested {}: {:?}", entity_type, e)
+        }
+    }
+
     fn handle_raw_fs_api_response<R: Serialize>(&self, st: status::Status, resp: &R) -> IronResult<Response> {
         Ok(Response::with((st,
                            Header(headers::Server(USER_AGENT.to_string())),


Test patch: