Repository: thecoshman/http (language: rust)

Issue:
Cannot upload to /
So, 3c781ab introduced another issue, namely that you now cannot upload files to `/`.

This is what happens when you upload a file with no extension (`some_file`):

![no_ext](https://user-images.githubusercontent.com/28627918/106602165-963d5100-6597-11eb-970b-3104f1bae724.png)

And a bit scarier when it's a file with extension (`some.file`):

![ext](https://user-images.githubusercontent.com/28627918/106602209-a5bc9a00-6597-11eb-9577-83e0de612097.png)

Yikes. Fortunately it's caught by CORS, else my files are going somewhere.

<hr>

The root cause is that it's not safe to do this:
```js
let url = document.location.pathname; // p.s. the var really shouldn't be named "url", rather "path".
// See https://nodejs.org/api/url.html#url_url_strings_and_url_objects

upload_file(url + "/" + encodeURIComponent(file.name), file);
```
Path might very well end with `/`, and it's not safe to assume that `//` is going to behave like `/`.

The simplest and best example is of course `/` (the base path). In this case, you get `"//file_name"`, which might be interpreted as `<proto>://file_name/`, where `host == "file_name"` and `path == "/"`, leading to the aforementioned errors.

There's also the case of `//////`, `/foo/` or `path == "/bar///////"`, which should all be considered.

Which is why in #124 I wrote this code like this:
```js
let page_path = document.location.pathname.replace(/\/+$/, "") + "/"; // always leaves one and only one trailing "/"
// can't do .replace(/\/+$/, "/") because there might be no match.
upload_file(page_path + encodeURIComponent(file.name), file);
```

Should be a simple fix.


Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index ec823f7..1048410 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.11.0"
+version = "1.11.1"
 # Remember to also update in http.md
 authors = ["thecoshman <rust@thecoshman.com>",
            "nabijaczleweli <nabijaczleweli@nabijaczleweli.xyz>",
diff --git a/appveyor.yml b/appveyor.yml
index 9fea012..86b9ada 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 1.11.0-{build}
+version: 1.11.1-{build}
 
 skip_tags: false
 
@@ -32,21 +32,21 @@ build: off
 build_script:
   - git submodule update --init --recursive
   - cargo build --verbose --release
-  - cp target\release\http.exe http-v1.11.0.exe
-  - strip --strip-all --remove-section=.comment --remove-section=.note http-v1.11.0.exe
-  - makensis -DHTTP_VERSION=v1.11.0 install.nsi
+  - cp target\release\http.exe http-v1.11.1.exe
+  - strip --strip-all --remove-section=.comment --remove-section=.note http-v1.11.1.exe
+  - makensis -DHTTP_VERSION=v1.11.1 install.nsi
 
 test: off
 test_script:
   - cargo test --verbose --release
 
 artifacts:
-  - path: http-v1.11.0.exe
-  - path: http v1.11.0 installer.exe
+  - path: http-v1.11.1.exe
+  - path: http v1.11.1 installer.exe
 
 deploy:
   provider: GitHub
-  artifact: /http.*v1.11.0.*\.exe/
+  artifact: /http.*v1.11.1.*\.exe/
   auth_token:
     secure: ZTXvCrv9y01s7Hd60w8W7NaouPnPoaw9YJt9WhWQ2Pep8HLvCikt9Exjkz8SGP9P
   on:
diff --git a/assets/upload.js b/assets/upload.js
index 3d8d881..7650fe9 100644
--- a/assets/upload.js
+++ b/assets/upload.js
@@ -6,9 +6,9 @@ window.addEventListener("load", function() {
   let body = document.getElementsByTagName("body")[0];
   let file_upload = document.getElementById("file_upload");
   let remaining_files = 0;
-  let url = document.URL;
-  if(url[url.length - 1] == "/")
-    url = url.substr(0, url.length - 1);
+  let url = document.location.pathname;
+  if(!url.endsWith("/"))
+    url += "/";
 
   body.addEventListener("dragover", function(ev) {
     if(SUPPORTED_TYPES.find(function(el) {
@@ -33,7 +33,7 @@ window.addEventListener("load", function() {
       for(let i = ev.dataTransfer.files.length - 1; i >= 0; --i) {
         if(!ev.dataTransfer.items[i].webkitGetAsEntry) {
           let file = ev.dataTransfer.files[i];
-          upload_file(url + "/" + file.name, file);
+          upload_file(url + encodeURIComponent(file.name), file);
         } else
           recurse_upload(ev.dataTransfer.items[i].webkitGetAsEntry(), url);
       }
@@ -45,7 +45,7 @@ window.addEventListener("load", function() {
 
     for(let i = file_upload.files.length - 1; i >= 0; --i) {
       let file = file_upload.files[i];
-      upload_file(url + "/" + file.name, file);
+      upload_file(url + encodeURIComponent(file.name), file);
     }
   });
 
@@ -63,10 +63,10 @@ window.addEventListener("load", function() {
     if(entry.isFile) {
       if(entry.file)
         entry.file(function(f) {
-          upload_file(base_url + entry.fullPath, f);
+          upload_file(base_url + entry.fullPath.split("/").filter(function(seg) { return seg; }).map(encodeURIComponent).join("/"), f);
         });
       else
-        upload_file(base_url + entry.fullPath, entry.getFile());
+        upload_file(base_url + entry.fullPath.split("/").filter(function(seg) { return seg; }).map(encodeURIComponent).join("/"), entry.getFile());
     } else
       entry.createReader().readEntries(function(e) {
         e.forEach(function(f) {
diff --git a/src/ops/mod.rs b/src/ops/mod.rs
index d2ba555..8bccef9 100644
--- a/src/ops/mod.rs
+++ b/src/ops/mod.rs
@@ -29,8 +29,9 @@ use iron::{headers, status, method, mime, IronResult, Listening, Response, TypeM
 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,
                         response_encoding, detect_file_as_dir, encoding_extension, file_time_modified, file_time_modified_p, get_raw_fs_metadata,
-                        human_readable_size, is_nonexistent_descendant_of, USER_AGENT, ERROR_HTML, INDEX_EXTENSIONS, MIN_ENCODING_GAIN, MAX_ENCODING_SIZE,
-                        MIN_ENCODING_SIZE, DAV_LEVEL_1_METHODS, DIRECTORY_LISTING_HTML, MOBILE_DIRECTORY_LISTING_HTML, BLACKLISTED_ENCODING_EXTENSIONS};
+                        human_readable_size, encode_tail_if_trimmed, is_nonexistent_descendant_of, USER_AGENT, ERROR_HTML, INDEX_EXTENSIONS, MIN_ENCODING_GAIN,
+                        MAX_ENCODING_SIZE, MIN_ENCODING_SIZE, DAV_LEVEL_1_METHODS, DIRECTORY_LISTING_HTML, MOBILE_DIRECTORY_LISTING_HTML,
+                        BLACKLISTED_ENCODING_EXTENSIONS};
 
 
 macro_rules! log {
@@ -769,7 +770,8 @@ impl HttpHandler {
                     file_time_modified_p(req_p.parent().expect("Failed to get requested directory's parent directory"))
                         .strftime("%F %T")
                         .unwrap(),
-                    up_path = slash_idx.map(|i| &rel_noslash[0..i]).unwrap_or("").replace('%', "%25").replace('#', "%23").replace('[', "%5B").replace(']', "%5D"),
+                    up_path =
+                        slash_idx.map(|i| &rel_noslash[0..i]).unwrap_or("").replace('%', "%25").replace('#', "%23").replace('[', "%5B").replace(']', "%5D"),
                     up_path_slash = if slash_idx.is_some() { "/" } else { "" })
         };
         let list_s = req_p.read_dir()
@@ -822,7 +824,7 @@ impl HttpHandler {
                             DisplayThree("", String::new(), "")
                         },
                         path = format!("/{}", relpath).replace("//", "/").replace('%', "%25").replace('#', "%23").replace('[', "%5B").replace(']', "%5D"),
-                        fname = fname.replace('%', "%25").replace('#', "%23").replace('[', "%5B").replace(']', "%5D"))
+                        fname = encode_tail_if_trimmed(fname.replace('%', "%25").replace('#', "%23").replace('[', "%5B").replace(']', "%5D")))
             });
 
         self.handle_generated_response_encoding(req,
@@ -872,7 +874,8 @@ impl HttpHandler {
                          <td><a href=\"/{up_path}{up_path_slash}\">&nbsp;</a></td> \
                          <td><a href=\"/{up_path}{up_path_slash}\">&nbsp;</a></td></tr>",
                     file_time_modified_p(req_p.parent().expect("Failed to get requested directory's parent directory")).strftime("%F %T").unwrap(),
-                    up_path = slash_idx.map(|i| &rel_noslash[0..i]).unwrap_or("").replace('%', "%25").replace('#', "%23").replace('[', "%5B").replace(']', "%5D"),
+                    up_path =
+                        slash_idx.map(|i| &rel_noslash[0..i]).unwrap_or("").replace('%', "%25").replace('#', "%23").replace('[', "%5B").replace(']', "%5D"),
                     up_path_slash = if slash_idx.is_some() { "/" } else { "" })
         };
 
@@ -937,7 +940,7 @@ impl HttpHandler {
                             DisplayThree("", "", "")
                         },
                         path = format!("/{}", relpath).replace("//", "/").replace('%', "%25").replace('#', "%23").replace('[', "%5B").replace(']', "%5D"),
-                        fname = fname.replace('%', "%25").replace('#', "%23").replace('[', "%5B").replace(']', "%5D"))
+                        fname = encode_tail_if_trimmed(fname.replace('%', "%25").replace('#', "%23").replace('[', "%5B").replace(']', "%5D")))
             });
 
         self.handle_generated_response_encoding(req,
diff --git a/src/util/mod.rs b/src/util/mod.rs
index d8749ef..ba10286 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -216,6 +216,30 @@ pub fn uppercase_first(s: &str) -> String {
     }
 }
 
+/// Percent-encode the last character if it's white space
+///
+/// Firefox treats, e.g. `href="http://henlo/menlo   "` as `href="http://henlo/menlo"`,
+/// but that final whitespace is significant, so this turns it into `href="http://henlo/menlo  %20"`
+pub fn encode_tail_if_trimmed(mut s: String) -> String {
+    let c = s.chars().rev().next();
+    if c.map(|c| c.is_whitespace()).unwrap_or(false) {
+        let c = c.unwrap();
+
+        s.pop();
+        s.push('%');
+
+        let mut cb = [0u8; 4];
+        c.encode_utf8(&mut cb);
+        for b in cb.iter().take(c.len_utf8()) {
+            write!(s, "{:02X}", b).expect("Couldn't allocate two more characters?");
+        }
+
+        s
+    } else {
+        s
+    }
+}
+
 /// Check if the specified file is to be considered "binary".
 ///
 /// Basically checks is a file is UTF-8.


Test patch: