Repository: thecoshman/http (language: rust)

Issue:
Handle some proxying header?
A common one is `X-Forwarded-For`, and it'd be useful for running this behind a reverse proxy like `nginx` (like I do, on, e.g. https://foreign.nabijaczleweli.xyz/debian); right now my journal looks like this:

```
Feb 15 19:47:51 tarta http[5689]: [2020-02-15 19:47:51] 127.0.0.1:54280 was served file /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/README as text/plain
Feb 15 20:11:46 tarta http[5689]: [2020-02-15 20:11:46] 127.0.0.1:54448 was served file /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/README as text/plain
Feb 15 20:14:23 tarta http[5689]: [2020-02-15 20:14:23] 127.0.0.1:54474 was served file /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/dists/buster/InRelease as text/plain
Feb 15 20:14:23 tarta http[5689]: [2020-02-15 20:14:23] 127.0.0.1:54476 was served file /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/dists/sid/InRelease as text/plain
Feb 15 20:14:23 tarta http[5689]: [2020-02-15 20:14:23] 127.0.0.1:54480 was served file /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/dists/buster/main/source/Sources.gz as application/x-gzip
Feb 15 20:14:23 tarta http[5689]: [2020-02-15 20:14:23] 127.0.0.1:54482 was served file /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/dists/buster/main/binary-amd64/Packages.gz as application/x-gzip
Feb 15 20:14:40 tarta http[5689]: [2020-02-15 20:14:40] 127.0.0.1:54492 was served file /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/pool/main/h/http/http_1.8.0-1_amd64.deb as application/octet-stream
Feb 15 20:16:26 tarta http[5689]: [2020-02-15 20:16:26] 127.0.0.1:54506 was served directory listing for /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian
Feb 15 20:16:26 tarta http[5689]: [2020-02-15 20:16:26]                 encoded as gzip for 345.6% ratio
Feb 15 20:16:30 tarta http[5689]: [2020-02-15 20:16:30] 127.0.0.1:54512 was served directory listing for /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/pool
Feb 15 20:16:30 tarta http[5689]: [2020-02-15 20:16:30]                 encoded as gzip for 338.1% ratio
Feb 15 20:16:31 tarta http[5689]: [2020-02-15 20:16:31] 127.0.0.1:54514 was served directory listing for /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/pool/patches
Feb 15 20:16:31 tarta http[5689]: [2020-02-15 20:16:31]                 encoded as gzip for 338.7% ratio
Feb 15 20:16:32 tarta http[5689]: [2020-02-15 20:16:32] 127.0.0.1:54516 was served directory listing for /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/pool/patches/h
Feb 15 20:16:32 tarta http[5689]: [2020-02-15 20:16:32]                 encoded as gzip for 337.8% ratio
Feb 15 20:16:34 tarta http[5689]: [2020-02-15 20:16:34] 127.0.0.1:54518 was served directory listing for /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/pool/patches/h/http
Feb 15 20:16:34 tarta http[5689]: [2020-02-15 20:16:34]                 encoded as gzip for 338.2% ratio
Feb 15 20:21:37 tarta http[5689]: [2020-02-15 20:21:37] 127.0.0.1:54552 was served file /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/README as text/plain
Feb 15 20:25:28 tarta http[5689]: [2020-02-15 20:25:28] 127.0.0.1:54574 was served file /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/README as text/plain
Feb 15 20:27:53 tarta http[5689]: [2020-02-15 20:27:53] 127.0.0.1:54600 was served file /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/README as text/plain
Feb 15 20:36:38 tarta http[5689]: [2020-02-15 20:36:38] 127.0.0.1:54662 was served file /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/README as text/plain
Feb 15 20:36:42 tarta http[5689]: [2020-02-15 20:36:42] 127.0.0.1:54664 was served file /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/README as text/plain
Feb 15 20:37:06 tarta http[5689]: [2020-02-15 20:37:06] 127.0.0.1:54666 was served file /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/README as text/plain
```

Where, and of this I'm quite sure, *none* of the requests originated from the server.

I propose using the value of `X-Forwarded-For`, if present, instead of the source address, with some marker, like, e.g.
```
Feb 15 20:21:37 tarta http[5689]: [2020-02-15 20:21:37] 127.0.0.1:54552 (for 192.168.1.167:6969) was served file /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/README as text/plain
```
or
```
Feb 15 20:21:37 tarta http[5689]: [2020-02-15 20:21:37] 127.0.0.1:54552 for 192.168.1.167:6969 was served file /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/README as text/plain
```
or
```
Feb 15 20:21:37 tarta http[5689]: [2020-02-15 20:21:37] 127.0.0.1:54552 [for 192.168.1.167:6969] was served file /home/nabijaczleweli/foreign.nabijaczleweli.xyz/debian/README as text/plain
```


Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 78d02e8..03151c3 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.8.0"
+version = "1.9.0"
 # Remember to also update in http.md
 authors = ["thecoshman <rust@thecoshman.com>",
            "nabijaczleweli <nabijaczleweli@gmail.com>",
@@ -44,6 +44,10 @@ md6 = "2.0"
 version = "0.3"
 default-features = false
 
+[dependencies.cidr]
+version = "0.1"
+default-features = false
+
 [dependencies.iron]
 version = "0.6"
 features = ["hyper-native-tls"]
diff --git a/appveyor.yml b/appveyor.yml
index 6e6fa01..08ef7be 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 1.8.0-{build}
+version: 1.9.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.8.0.exe
-  - strip --strip-all --remove-section=.comment --remove-section=.note http-v1.8.0.exe
-  - makensis -DHTTP_VERSION=v1.8.0 "/X!include \"EnvVarUpdate.nsh\"" install.nsi
+  - cp target\release\http.exe http-v1.9.0.exe
+  - strip --strip-all --remove-section=.comment --remove-section=.note http-v1.9.0.exe
+  - makensis -DHTTP_VERSION=v1.9.0 "/X!include \"EnvVarUpdate.nsh\"" install.nsi
 
 test: off
 test_script:
   - cargo test --verbose --release
 
 artifacts:
-  - path: http-v1.8.0.exe
-  - path: http v1.8.0 installer.exe
+  - path: http-v1.9.0.exe
+  - path: http v1.9.0 installer.exe
 
 deploy:
   provider: GitHub
-  artifact: /http.*v1.8.0.*\.exe/
+  artifact: /http.*v1.9.0.*\.exe/
   auth_token:
     secure: ZTXvCrv9y01s7Hd60w8W7NaouPnPoaw9YJt9WhWQ2Pep8HLvCikt9Exjkz8SGP9P
   on:
diff --git a/assets/adjust_tz.js b/assets/adjust_tz.js
index 326358e..83c1054 100644
--- a/assets/adjust_tz.js
+++ b/assets/adjust_tz.js
@@ -1,3 +1,5 @@
+"use strict";
+
 window.addEventListener("load", function() {
   const FORMAT = "yyyy-MM-dd HH:mm:ss";
 
diff --git a/assets/directory_listing.html b/assets/directory_listing.html
index 2026489..934262c 100644
--- a/assets/directory_listing.html
+++ b/assets/directory_listing.html
@@ -28,7 +28,7 @@
         padding-right: 5pt;
       }
 
-      td:nth-child(3) a, td:nth-child(4) a {
+      td:nth-child(3) a, td:nth-child(4) a, td:nth-child(5) a {
         color: inherit;
       }
 
@@ -36,11 +36,23 @@
         text-align: right;
       }
 
+      td:nth-child(5) {
+        padding-left: 5pt;
+      }
+
+      tr:nth-child(n+4) td:nth-child(5) a {
+        display: inline-block;
+      }
+
       td > a {
         display: block;
         text-decoration: none;
       }
 
+      td > input {
+        height: 1em;
+      }
+
       tr:hover {
         background-color: #eee;
       }
@@ -76,14 +88,39 @@
       .back_arrow_icon:before {
         content: url('{back_arrow_icon}');
       }
+
+      .new_dir_icon:before {
+        content: url('{new_dir_icon}');
+      }
+
+      .delete_file_icon:before {
+        content: url('{delete_file_icon}');
+      }
+
+      .rename_icon:before {
+        content: url('{rename_icon}');
+      }
+
+      .confirm_icon:before {
+        content: url('{confirm_icon}');
+      }
+
+      .delete_file_icon, .rename_icon, .confirm_icon {
+        font-size: 0;
+      }
+
+      .has-log {
+        font-size: inherit;
+      }
     </style>
   </head>
   <body>
     <div>
       <p class="pre-list">The requested directory {0} contains the following files:</p>
       <table>
-        <tr><th></th> <th>Name</th> <th>Last modified (UTC)</th> <th>Size</th></tr>
+        <tr><th></th> <th>Name</th> <th>Last modified (UTC)</th> <th>Size</th> {5}</tr>
         {2}
+        {6}
         {3}
       </table>
     </div>
diff --git a/assets/directory_listing_mobile.html b/assets/directory_listing_mobile.html
index aee7d1b..ba8de97 100644
--- a/assets/directory_listing_mobile.html
+++ b/assets/directory_listing_mobile.html
@@ -30,6 +30,10 @@
         padding-top: 7px;
       }
 
+      .manage {
+        float: right;
+      }
+
       .bottom {
         padding-bottom: 7px;
       }
@@ -59,6 +63,10 @@
         word-wrap: break-word;
       }
 
+      #newdir_input > input[type="text"] {
+        margin-left: 0.5em;
+      }
+
       .dir_icon:before {
         content: url('{dir_icon}');
         margin-right: 0.5em;
@@ -88,12 +96,38 @@
         content: url('{back_arrow_icon}');
         margin-right: 0.5em;
       }
+
+      .new_dir_icon:before {
+        content: url('{new_dir_icon}');
+        margin-right: 0.5em;
+      }
+
+      .delete_file_icon:before {
+        content: url('{delete_file_icon}');
+      }
+
+      .rename_icon:before {
+        content: url('{rename_icon}');
+      }
+
+      .confirm_icon:before {
+        content: url('{confirm_icon}');
+      }
+
+      .delete_file_icon, .rename_icon, .confirm_icon {
+        font-size: 0;
+      }
+
+      .has-log {
+        font-size: inherit;
+      }
     </style>
   </head>
   <body>
     <div>
       <a href="{1}{0}" class="list heading top bottom">{0}</a>
       {3}
+      {6}
       {4}
     </div>
     {5}
diff --git a/assets/icons/LICENSE b/assets/icons/LICENSE
new file mode 100644
index 0000000..6394969
--- /dev/null
+++ b/assets/icons/LICENSE
@@ -0,0 +1,5 @@
+Unless otherwise specified, all files in this directory come from the Apache Public Domain Icons repository as seen under https://web.archive.org/web/20160303193836/http://www.apache.org/icons/
+
+delete_file.png based on https://commons.wikimedia.org/wiki/File:Ballot_x_no_small.png by Mankash licensed under CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0/)
+new_directory.gif and new_directory_icon.psd based on dir.png and small/burst.png from the Apache Public Domain Icons repository and released into the public domain
+confirm.png based on https://commons.wikimedia.org/wiki/File:True-Symbol.gif by Abnormaal and released into the public domain
diff --git a/assets/icons/back_arrow_icon.gif b/assets/icons/back_arrow.gif
similarity index 100%
rename from assets/icons/back_arrow_icon.gif
rename to assets/icons/back_arrow.gif
diff --git a/assets/icons/confirm.png b/assets/icons/confirm.png
new file mode 100644
index 0000000..5037607
Binary files /dev/null and b/assets/icons/confirm.png differ
diff --git a/assets/icons/delete_file.png b/assets/icons/delete_file.png
new file mode 100644
index 0000000..1452517
Binary files /dev/null and b/assets/icons/delete_file.png differ
diff --git a/assets/icons/directory_icon.gif b/assets/icons/directory.gif
similarity index 100%
rename from assets/icons/directory_icon.gif
rename to assets/icons/directory.gif
diff --git a/assets/icons/file_icon.gif b/assets/icons/file.gif
similarity index 100%
rename from assets/icons/file_icon.gif
rename to assets/icons/file.gif
diff --git a/assets/icons/file_binary_icon.gif b/assets/icons/file_binary.gif
similarity index 100%
rename from assets/icons/file_binary_icon.gif
rename to assets/icons/file_binary.gif
diff --git a/assets/icons/file_image_icon.gif b/assets/icons/file_image.gif
similarity index 100%
rename from assets/icons/file_image_icon.gif
rename to assets/icons/file_image.gif
diff --git a/assets/icons/file_text_icon.gif b/assets/icons/file_text.gif
similarity index 100%
rename from assets/icons/file_text_icon.gif
rename to assets/icons/file_text.gif
diff --git a/assets/icons/new_directory.gif b/assets/icons/new_directory.gif
new file mode 100644
index 0000000..6f765fe
Binary files /dev/null and b/assets/icons/new_directory.gif differ
diff --git a/assets/icons/new_directory.psd b/assets/icons/new_directory.psd
new file mode 100644
index 0000000..7e092d0
Binary files /dev/null and b/assets/icons/new_directory.psd differ
diff --git a/assets/icons/rename.png b/assets/icons/rename.png
new file mode 100644
index 0000000..51f1dd8
Binary files /dev/null and b/assets/icons/rename.png differ
diff --git a/assets/manage.js b/assets/manage.js
new file mode 100644
index 0000000..561dcb6
--- /dev/null
+++ b/assets/manage.js
@@ -0,0 +1,114 @@
+"use strict";
+
+window.addEventListener("load", function() {
+  let delete_file_links = document.getElementsByClassName("delete_file_icon");
+  let rename_links = document.getElementsByClassName("rename_icon");
+
+
+  for(let i = delete_file_links.length - 1; i >= 0; --i) {
+    let link = delete_file_links[i];
+
+    link.addEventListener("click", function(ev) {
+      ev.preventDefault();
+
+      let line = link.parentElement.parentElement;
+      make_request("DELETE", get_href_for_line(line), link);
+    });
+  }
+
+  let first_rename_onclick = function(link, first_onclick, ev) {
+    ev.preventDefault();
+
+    let line = link.parentElement.parentElement;
+    let filename_cell = get_filename_cell_for_line(line);
+    let original_name = filename_cell.innerText;
+
+    let submit_callback = function() {
+      rename(original_name, new_name_input.value, link);
+    };
+    let new_name_input = make_filename_input(filename_cell, original_name, submit_callback);
+
+    link.removeEventListener("click", first_onclick);
+    make_confirm_icon(link, submit_callback);
+  };
+  for(let i = rename_links.length - 1; i >= 0; --i) {
+    let link = rename_links[i];
+
+    let first_onclick = function(ev) {
+      first_rename_onclick(link, first_onclick, ev);
+    };
+    link.addEventListener("click", first_onclick);
+  }
+
+
+  function rename(fname_from, fname_to, status_out) {
+    let root_url = window.location.origin + window.location.pathname;
+    if(!root_url.endsWith("/"))
+      root_url += "/";
+
+    if(fname_from.endsWith("/"))
+      fname_from = fname_from.substr(0, fname_from.length - 1);
+    if(fname_to.endsWith("/"))
+      fname_to = fname_to.substr(0, fname_to.length - 1);
+
+    make_request("MOVE", root_url + encodeURI(fname_from), status_out, function(request) {
+      request.setRequestHeader("Destination", root_url + encodeURI(fname_to));
+    });
+  };
+});
+
+
+function make_filename_input(input_container, initial, callback) {
+  input_container.innerHTML = "<input type=\"text\"></input>";
+  let input_elem = input_container.children[0];
+  input_elem.value = initial;
+
+  input_elem.addEventListener("keypress", function(ev) {
+    if(ev.keyCode === 13) {  // Enter
+      ev.preventDefault();
+      callback();
+    }
+  });
+  input_container.addEventListener("click", function(ev) {
+    ev.preventDefault();
+  });
+
+  input_elem.focus();
+
+  return input_elem;
+};
+
+function make_confirm_icon(element, callback) {
+  element.classList.add("confirm_icon");
+  element.href = "#confirm";
+  element.innerText = "Confirm";
+  element.addEventListener("click", function(ev) {
+    ev.preventDefault();
+    callback();
+  });
+};
+
+function create_new_directory(fname, status_out) {
+  let req_url = window.location.origin + window.location.pathname;
+  if(!req_url.endsWith("/"))
+    req_url += "/";
+  req_url += encodeURI(fname);
+
+  make_request("MKCOL", req_url, status_out);
+};
+
+function make_request(verb, url, status_out, request_modifier) {
+  let request = new XMLHttpRequest();
+  request.addEventListener("loadend", function() {
+    if(request.status >= 200 && request.status < 300)
+      window.location.reload();
+    else {
+      status_out.innerHTML = request.status + " " + request.statusText + (request.response ? " — " : "") + request.response;
+      status_out.classList.add("has-log");
+    }
+  });
+  request.open(verb, url);
+  if(request_modifier)
+    request_modifier(request);
+  request.send();
+};
diff --git a/assets/manage_desktop.js b/assets/manage_desktop.js
new file mode 100644
index 0000000..65f1f00
--- /dev/null
+++ b/assets/manage_desktop.js
@@ -0,0 +1,40 @@
+"use strict";
+
+window.addEventListener("load", function() {
+  let new_directory_line = document.getElementById("new_directory");
+
+  if(new_directory_line) {
+    let new_directory_filename_cell = new_directory_line.children[1];
+    let new_directory_status_output = new_directory_line.children[4].children[0];
+    let new_directory_filename_input = null;
+
+    new_directory_line.addEventListener("click", function(ev) {
+      if(new_directory_filename_input === null)
+        ev.preventDefault();
+      else if(ev.target === new_directory_status_output)
+        ;
+      else if(ev.target !== new_directory_filename_input) {
+        ev.preventDefault();
+        new_directory_filename_input.focus();
+      }
+
+      if(new_directory_filename_input === null) {
+        let submit_callback = function() {
+          create_new_directory(new_directory_filename_input.value, new_directory_status_output);
+        };
+
+        new_directory_filename_input = make_filename_input(new_directory_filename_cell, "", submit_callback);
+        make_confirm_icon(new_directory_status_output, submit_callback);
+      }
+    }, true);
+  }
+});
+
+
+function get_href_for_line(line) {
+  return line.children[0].children[0].href;
+}
+
+function get_filename_cell_for_line(line) {
+  return line.children[1];
+}
diff --git a/assets/manage_mobile.js b/assets/manage_mobile.js
new file mode 100644
index 0000000..cc76513
--- /dev/null
+++ b/assets/manage_mobile.js
@@ -0,0 +1,48 @@
+"use strict";
+
+window.addEventListener("load", function() {
+  let new_directory_line = document.getElementById("new_directory");
+
+  if(new_directory_line) {
+    let new_directory_status_output = new_directory_line.children[0];
+    let new_directory_filename_input = null;
+
+    new_directory_line.addEventListener("click", function(ev) {
+      if(new_directory_filename_input === null || ev.target === new_directory_filename_input)
+        ev.preventDefault();
+      else if(ev.target === new_directory_status_output)
+        ;
+      else if(ev.target !== new_directory_filename_input) {
+        ev.preventDefault();
+        new_directory_filename_input.focus();
+      }
+
+      if(new_directory_filename_input === null) {
+        let new_directory_filename_cell = document.createElement("span");
+        new_directory_filename_cell.id = "newdir_input";
+        new_directory_line.append(new_directory_filename_cell);
+
+        let first_onclick = true;
+        let submit_callback = function() {
+          if(first_onclick) {
+            first_onclick = false;
+            return;
+          }
+          create_new_directory(new_directory_filename_input.value, new_directory_status_output);
+        };
+
+        new_directory_filename_input = make_filename_input(new_directory_filename_cell, "", submit_callback);
+        make_confirm_icon(new_directory_status_output, submit_callback);
+      }
+    }, true);
+  }
+});
+
+
+function get_href_for_line(line) {
+  return line.href;
+}
+
+function get_filename_cell_for_line(line) {
+  return line.children[0];
+}
diff --git a/assets/upload.js b/assets/upload.js
index 6aacd70..3d8d881 100644
--- a/assets/upload.js
+++ b/assets/upload.js
@@ -1,3 +1,5 @@
+"use strict";
+
 window.addEventListener("load", function() {
   const SUPPORTED_TYPES = ["Files", "application/x-moz-file"];
 
diff --git a/http.md b/http.md
index f3da3de..75edbcd 100644
--- a/http.md
+++ b/http.md
@@ -113,6 +113,15 @@ pass parameters like what port to use.
 
     Exclusive with --path-auth with the equivalent PATH. Default: empty.
 
+  --proxy [HEADER-NAME:CIDR]
+
+    Treat HEADER-NAME as a proxy forwarded-for header when the request
+    originates from an address inside the network specified by the CIDR.
+
+    If the header is set but the request isn't in the network, it's ignored.
+
+    Can be specified any amount of times. Default: none.
+
   -s --no-follow-symlinks
 
     Don't follow symlinks when requesting file access.
@@ -184,7 +193,7 @@ pass parameters like what port to use.
       127.0.0.1:47880 was served directory listing for \\?\P:\Rust\http
       127.0.0.1:47902 was served file \\?\P:\Rust\http\http.1.html as text/html
       127.0.0.1:47916 was served file S:\Rust-target\doc\main.css as text/css
-      127.0.0.1:48049 asked for options
+      127.0.0.1:48049 asked for OPTIONS
       127.0.0.1:47936 used disabled request method DELETE
       127.0.0.1:48222 used disabled request method PUT
       ^C
@@ -206,7 +215,7 @@ pass parameters like what port to use.
     As in the first example, but don't follow symlinks.
 
     Example output change:
-      127.0.0.1:47916 requested to GET nonexistant entity S:\Rust-target\doc\main.css
+      127.0.0.1:47916 requested to GET nonexistent entity S:\Rust-target\doc\main.css
 
   `http -w`
 
@@ -346,7 +355,30 @@ pass parameters like what port to use.
     to direct descendants of the hosted directory.
 
     Example output change:
-      127.0.0.1:47916 requested to GET nonexistant entity S:\Rust-target\doc\main.css
+      127.0.0.1:47916 requested to GET nonexistent entity S:\Rust-target\doc\main.css
+
+  `http --proxy X-Forwarded-For:127.0.0.1 --proxy X-Proxied-For:192.168.1.0/24`
+
+    As in the first example, but treat the X-Forwarded-For and X-Proxied-For
+    as proxy headers for requests from localhost and the 192.168.1.0/24 network,
+    respectively.
+
+    Given, that requests from 127.0.0.1, 192.168.1.109, and 93.184.216.34
+    have the following headers set:
+      X-Forwarded-For: OwO
+      X-Proxied-For: UwU
+
+    Then the output will be as follows:
+      Hosting "." on port 8000 without TLS and no authentication...
+      Trusted proxies:
+      Header           Network
+      X-Forwarded-For  127.0.0.1
+      X-Proxied-For    192.168.1.0/24
+      Ctrl-C to stop.
+
+      [2020-02-17 17:48:41] 127.0.0.1:1392 for OwO was served directory listing for \\?\P:\Rust\http
+      [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
 
 ## AUTHOR
 
diff --git a/src/main.rs b/src/main.rs
index da4d213..d18838e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -22,6 +22,7 @@ extern crate bzip2;
 extern crate ctrlc;
 extern crate serde;
 extern crate regex;
+extern crate cidr;
 #[macro_use]
 extern crate clap;
 extern crate iron;
@@ -39,7 +40,7 @@ pub mod ops;
 pub mod util;
 
 pub use error::Error;
-pub use options::Options;
+pub use options::{LogLevel, Options};
 
 use std::mem;
 use iron::Iron;
@@ -103,7 +104,7 @@ fn result_main() -> Result<(), Error> {
                        &opts.tls_data)
     }?;
 
-    if opts.loglevel < ops::LogLevel::NoStartup {
+    if opts.loglevel < options::LogLevel::NoStartup {
         print!("{}Hosting \"{}\" on port {}",
                trivial_colours::Reset,
                opts.hosted_directory.0,
@@ -123,8 +124,19 @@ fn result_main() -> Result<(), Error> {
             print!(" and no authentication");
         }
         println!("...");
+
+        if !opts.proxies.is_empty()  {
+            println!("Trusted proxies:");
+
+            let mut out = TabWriter::new(stdout());
+            writeln!(out, "Header\tNetwork").unwrap();
+            for (network, header) in &opts.proxies {
+                writeln!(out, "{}\t{}", header, network).unwrap();
+            }
+            out.flush().unwrap();
+        }
     }
-    if !opts.path_auth_data.is_empty() && opts.loglevel < ops::LogLevel::NoAuth {
+    if !opts.path_auth_data.is_empty() && opts.loglevel < options::LogLevel::NoAuth {
         println!("Basic authentication credentials:");
 
         let mut out = TabWriter::new(stdout());
@@ -145,7 +157,7 @@ fn result_main() -> Result<(), Error> {
 
         out.flush().unwrap();
     }
-    if opts.loglevel < ops::LogLevel::NoStartup {
+    if opts.loglevel < options::LogLevel::NoStartup {
         println!("Ctrl-C to stop.");
         println!();
     }
diff --git a/src/ops/mod.rs b/src/ops/mod.rs
index 97b1fb3..b7ccc2e 100644
--- a/src/ops/mod.rs
+++ b/src/ops/mod.rs
@@ -1,5 +1,5 @@
 use md6;
-use std::iter;
+use std::fmt;
 use serde_json;
 use std::borrow::Cow;
 use std::net::IpAddr;
@@ -9,26 +9,27 @@ 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 iron::url::Url as GenericUrl;
-use self::super::{Options, Error};
 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 iron::{headers, status, method, mime, IronResult, Listening, Response, TypeMap, Request, Handler, Iron};
-use self::super::util::{WwwAuthenticate, CommaList, 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_nonexistant_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};
+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};
 
 
 macro_rules! log {
@@ -88,6 +89,7 @@ pub struct HttpHandler {
     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>,
     cache_gen: RwLock<CacheT<Vec<u8>>>,
     cache_fs: RwLock<CacheT<(PathBuf, bool)>>,
 }
@@ -124,6 +126,7 @@ impl HttpHandler {
             encoded_temp_dir: HttpHandler::temp_subdir(&opts.temp_directory, opts.encode_fs, "encoded"),
             cache_gen: Default::default(),
             cache_fs: Default::default(),
+            proxies: opts.proxies.clone(),
         }
     }
 
@@ -235,16 +238,16 @@ impl HttpHandler {
 
                 if &auth.0 == username && &auth.1 == pwd {
                     log!(self.log,
-                         "{green}{}{reset} correctly authorised to {red}{}{reset} {yellow}{}{reset}",
-                         req.remote_addr,
+                         "{} correctly authorised to {red}{}{reset} {yellow}{}{reset}",
+                         self.remote_addresses(&req),
                          req.method,
                          req.url);
 
                     Ok(None)
                 } else {
                     log!(self.log,
-                         "{green}{}{reset} requested to {red}{}{reset} {yellow}{}{reset} with invalid credentials \"{}{}{}\"",
-                         req.remote_addr,
+                         "{} requested to {red}{}{reset} {yellow}{}{reset} with invalid credentials \"{}{}{}\"",
+                         self.remote_addresses(&req),
                          req.method,
                          req.url,
                          username,
@@ -256,8 +259,8 @@ impl HttpHandler {
             }
             None => {
                 log!(self.log,
-                     "{green}{}{reset} requested to {red}{}{reset} {yellow}{}{reset} without authorisation",
-                     req.remote_addr,
+                     "{} requested to {red}{}{reset} {yellow}{}{reset} without authorisation",
+                     self.remote_addresses(&req),
                      req.method,
                      req.url);
 
@@ -267,7 +270,7 @@ impl HttpHandler {
     }
 
     fn handle_options(&self, req: &mut Request) -> IronResult<Response> {
-        log!(self.log, "{green}{}{reset} asked for {red}OPTIONS{reset}", req.remote_addr);
+        log!(self.log, "{} asked for {red}OPTIONS{reset}", self.remote_addresses(&req));
 
         let mut allowed_methods = Vec::with_capacity(6 +
                                                      if self.webdav {
@@ -292,7 +295,7 @@ impl HttpHandler {
 
         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_nonexistant(req, req_p);
+            return self.handle_nonexistent(req, req_p);
         }
 
         let is_file = is_actually_file(&req_p.metadata().expect("Failed to get file metadata").file_type());
@@ -317,8 +320,8 @@ impl HttpHandler {
 
     fn handle_invalid_url(&self, req: &mut Request, cause: &str) -> IronResult<Response> {
         log!(self.log,
-             "{green}{}{reset} requested to {red}{}{reset} {yellow}{}{reset} with invalid URL -- {}",
-             req.remote_addr,
+             "{} requested to {red}{}{reset} {yellow}{}{reset} with invalid URL -- {}",
+             self.remote_addresses(&req),
              req.method,
              req.url,
              &cause[3..cause.len() - 4]); // Strip <p> tags
@@ -329,14 +332,14 @@ impl HttpHandler {
     }
 
     #[inline(always)]
-    fn handle_nonexistant(&self, req: &mut Request, req_p: PathBuf) -> IronResult<Response> {
-        self.handle_nonexistant_status(req, req_p, status::NotFound)
+    fn handle_nonexistent(&self, req: &mut Request, req_p: PathBuf) -> IronResult<Response> {
+        self.handle_nonexistent_status(req, req_p, status::NotFound)
     }
 
-    fn handle_nonexistant_status(&self, req: &mut Request, req_p: PathBuf, status: status::Status) -> IronResult<Response> {
+    fn handle_nonexistent_status(&self, req: &mut Request, req_p: PathBuf, status: status::Status) -> IronResult<Response> {
         log!(self.log,
-             "{green}{}{reset} requested to {red}{}{reset} nonexistant entity {magenta}{}{reset}",
-             req.remote_addr,
+             "{} requested to {red}{}{reset} nonexistent entity {magenta}{}{reset}",
+             self.remote_addresses(&req),
              req.method,
              req_p.display());
 
@@ -349,8 +352,8 @@ impl HttpHandler {
 
     fn handle_get_raw_fs_file(&self, req: &mut Request, req_p: PathBuf) -> IronResult<Response> {
         log!(self.log,
-             "{green}{}{reset} was served metadata for file {magenta}{}{reset}",
-             req.remote_addr,
+             "{} was served metadata for file {magenta}{}{reset}",
+             self.remote_addresses(&req),
              req_p.display());
         self.handle_raw_fs_api_response(status::Ok,
                                         &FilesetData {
@@ -399,8 +402,8 @@ impl HttpHandler {
             "text/plain".parse().unwrap()
         });
         log!(self.log,
-             "{green}{}{reset} was served byte range {}-{} of file {magenta}{}{reset} as {blue}{}{reset}",
-             req.remote_addr,
+             "{} was served byte range {}-{} of file {magenta}{}{reset} as {blue}{}{reset}",
+             self.remote_addresses(&req),
              from,
              to,
              req_p.display(),
@@ -430,8 +433,8 @@ impl HttpHandler {
             "text/plain".parse().unwrap()
         });
         log!(self.log,
-             "{green}{}{reset} was served file {magenta}{}{reset} from byte {} as {blue}{}{reset}",
-             req.remote_addr,
+             "{} was served file {magenta}{}{reset} from byte {} as {blue}{}{reset}",
+             self.remote_addresses(&req),
              req_p.display(),
              from,
              mime_type);
@@ -447,8 +450,8 @@ impl HttpHandler {
             "text/plain".parse().unwrap()
         });
         log!(self.log,
-             "{green}{}{reset} was served last {} bytes of file {magenta}{}{reset} as {blue}{}{reset}",
-             req.remote_addr,
+             "{} was served last {} bytes of file {magenta}{}{reset} as {blue}{}{reset}",
+             self.remote_addresses(&req),
              from,
              req_p.display(),
              mime_type);
@@ -481,7 +484,7 @@ impl HttpHandler {
                                                 status::RangeNotSatisfiable,
                                                 html_response(ERROR_HTML,
                                                               &["416 Range Not Satisfiable",
-                                                                &format!("Requested range <samp>{}</samp> could not be fullfilled for file {}.",
+                                                                &format!("Requested range <samp>{}</samp> could not be fulfilled for file {}.",
                                                                          range,
                                                                          req_p.display()),
                                                                 reason]))
@@ -494,8 +497,8 @@ impl HttpHandler {
             "text/plain".parse().unwrap()
         });
         log!(self.log,
-             "{green}{}{reset} was served an empty range from file {magenta}{}{reset} as {blue}{}{reset}",
-             req.remote_addr,
+             "{} was served an empty range from file {magenta}{}{reset} as {blue}{}{reset}",
+             self.remote_addresses(&req),
              req_p.display(),
              mime_type);
 
@@ -517,8 +520,8 @@ impl HttpHandler {
             "text/plain".parse().unwrap()
         });
         log!(self.log,
-             "{green}{}{reset} was served file {magenta}{}{reset} as {blue}{}{reset}",
-             req.remote_addr,
+             "{} was served file {magenta}{}{reset} as {blue}{}{reset}",
+             self.remote_addresses(&req),
              req_p.display(),
              mime_type);
 
@@ -548,7 +551,7 @@ impl HttpHandler {
                     Some(&(ref resp_p, true)) => {
                         log!(self.log,
                              "{} encoded as {} for {:.1}% ratio (cached)",
-                             iter::repeat(' ').take(req.remote_addr.to_string().len()).collect::<String>(),
+                             Spaces(self.remote_addresses(req).to_string().len()),
                              encoding,
                              ((file_length(&req_p.metadata().expect("Failed to get requested file metadata"), &req_p) as f64) /
                               (file_length(&resp_p.metadata().expect("Failed to get encoded file metadata"), &resp_p) as f64)) *
@@ -591,7 +594,7 @@ impl HttpHandler {
                 } else {
                     log!(self.log,
                          "{} encoded as {} for {:.1}% ratio",
-                         iter::repeat(' ').take(req.remote_addr.to_string().len()).collect::<String>(),
+                         Spaces(self.remote_addresses(req).to_string().len()),
                          encoding,
                          gain * 100f64);
 
@@ -608,7 +611,7 @@ impl HttpHandler {
             } else {
                 log!(self.log,
                      "{} failed to encode as {}, sending identity",
-                     iter::repeat(' ').take(req.remote_addr.to_string().len()).collect::<String>(),
+                     Spaces(self.remote_addresses(req).to_string().len()),
                      encoding);
             }
         }
@@ -624,8 +627,8 @@ impl HttpHandler {
 
     fn handle_get_raw_fs_dir(&self, req: &mut Request, req_p: PathBuf) -> IronResult<Response> {
         log!(self.log,
-             "{green}{}{reset} was served metadata for directory {magenta}{}{reset}",
-             req.remote_addr,
+             "{} was served metadata for directory {magenta}{}{reset}",
+             self.remote_addresses(&req),
              req_p.display());
         self.handle_raw_fs_api_response(status::Ok,
                                         &FilesetData {
@@ -676,7 +679,7 @@ impl HttpHandler {
                     let r = self.handle_get_file(req, idx);
                     log!(self.log,
                          "{} found index file for directory {magenta}{}{reset}",
-                         iter::repeat(' ').take(req.remote_addr.to_string().len()).collect::<String>(),
+                         Spaces(self.remote_addresses(req).to_string().len()),
                          req_p.display());
                     return r;
                 } else {
@@ -695,8 +698,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!(self.log,
-             "Redirecting {green}{}{reset} to {yellow}{}{reset} - found index file {magenta}index.{}{reset}",
-             req.remote_addr,
+             "Redirecting {} to {yellow}{}{reset} - found index file {magenta}index.{}{reset}",
+             self.remote_addresses(&req),
              new_url,
              idx_ext);
 
@@ -711,10 +714,11 @@ 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;
+        let is_root = req.url.as_ref().path_segments().unwrap().count() + !req.url.as_ref().as_str().ends_with('/') as usize == 1;
+        let show_file_management_controls = self.writes_temp_dir.is_some();
         log!(self.log,
-             "{green}{}{reset} was served mobile directory listing for {magenta}{}{reset}",
-             req.remote_addr,
+             "{} was served mobile directory listing for {magenta}{}{reset}",
+             self.remote_addresses(&req),
              req_p.display());
 
         let parent_s = if is_root {
@@ -755,23 +759,31 @@ impl HttpHandler {
                 let fname = f.file_name().into_string().expect("Failed to get file name");
                 let path = f.path();
 
-                format!("{}<a href=\"{path}{fname}\" class=\"list entry top\"><span class=\"{}{}_icon\" id=\"{}\">{}{}</span></a> \
-                         <a href=\"{path}{fname}\" class=\"list entry bottom\"><span class=\"marker\">@</span><span class=\"datetime\">{} UTC</span> \
-                         {}{}{}</a>\n",
+                format!("{}<a href=\"{path}{fname}\" class=\"list entry top\"><span class=\"{}{}_icon\" id=\"{}\">{}{}</span>{}</a> \
+                           <a href=\"{path}{fname}\" class=\"list entry bottom\"><span class=\"marker\">@</span><span class=\"datetime\">{} UTC</span>{}</a>\n",
                         cur,
                         if is_file { "file" } else { "dir" },
                         file_icon_suffix(&path, is_file),
                         path.file_name().map(|p| p.to_str().expect("Filename not UTF-8").replace('.', "_")).as_ref().unwrap_or(&fname),
                         fname,
                         if is_file { "" } else { "/" },
+                        if show_file_management_controls {
+                            DisplayThree("<span class=\"manage\"><span class=\"delete_file_icon\">Delete</span>",
+                                         if self.webdav {
+                                             " <span class=\"rename_icon\">Rename</span>"
+                                         } else {
+                                             ""
+                                         },
+                                         "</span>")
+                        } else {
+                            DisplayThree("", "", "")
+                        },
                         file_time_modified(&fmeta).strftime("%F %T").unwrap(),
-                        if is_file { "<span class=\"size\">" } else { "" },
                         if is_file {
-                            human_readable_size(file_length(&fmeta, &path))
+                            DisplayThree("<span class=\"size\">", human_readable_size(file_length(&fmeta, &path)), "</span>")
                         } else {
-                            String::new()
+                            DisplayThree("", String::new(), "")
                         },
-                        if is_file { "</span>" } else { "" },
                         path = format!("/{}", relpath).replace("//", "/").replace('%', "%25").replace('#', "%23"),
                         fname = fname.replace('%', "%25").replace('#', "%23"))
             });
@@ -781,28 +793,35 @@ impl HttpHandler {
                                                 html_response(MOBILE_DIRECTORY_LISTING_HTML,
                                                               &[&relpath[..],
                                                                 if is_root { "" } else { "/" },
-                                                                if self.writes_temp_dir.is_some() {
-                                                                    r#"<script type="text/javascript">{upload}</script>"#
+                                                                if show_file_management_controls {
+                                                                    r#"<script type="text/javascript">{upload}{manage_mobile}{manage}</script>"#
                                                                 } else {
                                                                     ""
                                                                 },
                                                                 &parent_s[..],
                                                                 &list_s[..],
-                                                                if self.writes_temp_dir.is_some() {
+                                                                if show_file_management_controls {
                                                                     "<span class=\"list heading top top-border bottom\"> \
                                                                        Upload files: <input id=\"file_upload\" type=\"file\" multiple /> \
                                                                      </span>"
                                                                 } else {
                                                                     ""
+                                                                },
+                                                                if show_file_management_controls && self.webdav {
+                                                                    "<a id=\"new_directory\" href=\"#new_directory\" class=\"list entry top bottom\">
+                                                                         <span class=\"new_dir_icon\">Create directory</span></a>"
+                                                                } else {
+                                                                    ""
                                                                 }]))
     }
 
     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;
+        let is_root = req.url.as_ref().path_segments().unwrap().count() + !req.url.as_ref().as_str().ends_with('/') as usize == 1;
+        let show_file_management_controls = self.writes_temp_dir.is_some();
         log!(self.log,
-             "{green}{}{reset} was served directory listing for {magenta}{}{reset}",
-             req.remote_addr,
+             "{} was served directory listing for {magenta}{}{reset}",
+             self.remote_addresses(&req),
              req_p.display());
 
         let parent_s = if is_root {
@@ -813,6 +832,7 @@ impl HttpHandler {
             format!("<tr><td><a href=\"/{up_path}{up_path_slash}\" id=\"parent_dir\" class=\"back_arrow_icon\"></a></td> \
                          <td><a href=\"/{up_path}{up_path_slash}\">Parent directory</a></td> \
                          <td><a href=\"/{up_path}{up_path_slash}\" class=\"datetime\">{}</a></td> \
+                         <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(""),
@@ -844,8 +864,8 @@ impl HttpHandler {
                 let len = file_length(&fmeta, &path);
 
                 format!("{}<tr><td><a href=\"{path}{fname}\" id=\"{}\" class=\"{}{}_icon\"></a></td> \
-                           <td><a href=\"{path}{fname}\">{}{}</a></td> <td><a href=\"{path}{fname}\" class=\"datetime\">{}</a></td> \
-                           <td><a href=\"{path}{fname}\">{}{}{}{}{}</a></td></tr>\n",
+                               <td><a href=\"{path}{fname}\">{}{}</a></td> <td><a href=\"{path}{fname}\" class=\"datetime\">{}</a></td> \
+                               <td><a href=\"{path}{fname}\">{}{}{}</a></td> {}</tr>\n",
                         cur,
                         path.file_name().map(|p| p.to_str().expect("Filename not UTF-8").replace('.', "_")).as_ref().unwrap_or(&fname),
                         if is_file { "file" } else { "dir" },
@@ -853,19 +873,28 @@ impl HttpHandler {
                         fname,
                         if is_file { "" } else { "/" },
                         file_time_modified(&fmeta).strftime("%F %T").unwrap(),
-                        if is_file { "<abbr title=\"" } else { "&nbsp;" },
                         if is_file {
-                            len.to_string()
+                            DisplayThree("<abbr title=\"", len.to_string(), " B\">")
                         } else {
-                            String::new()
+                            DisplayThree("&nbsp;", String::new(), "")
                         },
-                        if is_file { " B\">" } else { "" },
                         if is_file {
                             human_readable_size(len)
                         } else {
                             String::new()
                         },
                         if is_file { "</abbr>" } else { "" },
+                        if show_file_management_controls {
+                            DisplayThree("<td><a href=\"#delete_file\" class=\"delete_file_icon\">Delete</a>",
+                                         if self.webdav {
+                                             " <a href=\"#rename\" class=\"rename_icon\">Rename</a>"
+                                         } else {
+                                             ""
+                                         },
+                                         "</td>")
+                        } else {
+                            DisplayThree("", "", "")
+                        },
                         path = format!("/{}", relpath).replace("//", "/").replace('%', "%25").replace('#', "%23"),
                         fname = fname.replace('%', "%25").replace('#', "%23"))
             });
@@ -874,20 +903,34 @@ impl HttpHandler {
                                                 status::Ok,
                                                 html_response(DIRECTORY_LISTING_HTML,
                                                               &[&relpath[..],
-                                                                if self.writes_temp_dir.is_some() {
-                                                                    r#"<script type="text/javascript">{upload}</script>"#
+                                                                if show_file_management_controls {
+                                                                    r#"<script type="text/javascript">{upload}{manage_desktop}{manage}</script>"#
                                                                 } else {
                                                                     ""
                                                                 },
                                                                 &parent_s[..],
                                                                 &list_s[..],
-                                                                if self.writes_temp_dir.is_some() {
+                                                                if show_file_management_controls {
                                                                     "<hr /> \
                                                                      <p> \
                                                                        Drag&amp;Drop to upload or <input id=\"file_upload\" type=\"file\" multiple />. \
                                                                      </p>"
                                                                 } else {
                                                                     ""
+                                                                },
+                                                                if show_file_management_controls {
+                                                                    "<th>Manage</th>"
+                                                                } else {
+                                                                    ""
+                                                                },
+                                                                if show_file_management_controls && self.webdav {
+                                                                    "<tr id=\"new_directory\"><td><a href=\"#new_directory\" class=\"new_dir_icon\"></a></td> \
+                                                                                              <td><a href=\"#new_directory\">Create directory</a></td> \
+                                                                                              <td><a href=\"#new_directory\">&nbsp;</a></td> \
+                                                                                              <td><a href=\"#new_directory\">&nbsp;</a></td> \
+                                                                                              <td><a href=\"#new_directory\">&nbsp;</a></td></tr>"
+                                                                } else {
+                                                                    ""
                                                                 }]))
     }
 
@@ -914,7 +957,7 @@ impl HttpHandler {
         } else if req.headers.has::<headers::ContentRange>() {
             self.handle_put_partial_content(req)
         } else if (symlink && !self.follow_symlinks) ||
-                  (symlink && self.follow_symlinks && self.sandbox_symlinks && !is_nonexistant_descendant_of(&req_p, &self.hosted_directory.1)) {
+                  (symlink && self.follow_symlinks && self.sandbox_symlinks && !is_nonexistent_descendant_of(&req_p, &self.hosted_directory.1)) {
             self.create_temp_dir(&self.writes_temp_dir);
             self.handle_put_file(req, req_p, false)
         } else {
@@ -940,8 +983,8 @@ impl HttpHandler {
             .to_string();
 
         log!(self.log,
-             "{green}{}{reset} tried to {red}{}{reset} on {magenta}{}{reset} ({blue}{}{reset}) but only {red}{}{reset} are allowed",
-             req.remote_addr,
+             "{} tried to {red}{}{reset} on {magenta}{}{reset} ({blue}{}{reset}) but only {red}{}{reset} are allowed",
+             self.remote_addresses(&req),
              req.method,
              url_path(&req.url),
              tpe,
@@ -959,8 +1002,8 @@ impl HttpHandler {
 
     fn handle_put_partial_content(&self, req: &mut Request) -> IronResult<Response> {
         log!(self.log,
-             "{green}{}{reset} tried to {red}PUT{reset} partial content to {yellow}{}{reset}",
-             req.remote_addr,
+             "{} tried to {red}PUT{reset} partial content to {yellow}{}{reset}",
+             self.remote_addresses(&req),
              url_path(&req.url));
 
         self.handle_generated_response_encoding(req,
@@ -975,8 +1018,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!(self.log,
-             "{green}{}{reset} {} {magenta}{}{reset}, size: {}B",
-             req.remote_addr,
+             "{} {} {magenta}{}{reset}, size: {}B",
+             self.remote_addresses(&req),
              if !legal {
                  "tried to illegally create"
              } else if existant {
@@ -1016,7 +1059,7 @@ impl HttpHandler {
             self.handle_invalid_url(req, "<p>Percent-encoding decoded to invalid UTF-8.</p>")
         } else if !req_p.exists() || (symlink && !self.follow_symlinks) ||
                   (symlink && self.follow_symlinks && self.sandbox_symlinks && !is_descendant_of(&req_p, &self.hosted_directory.1)) {
-            self.handle_nonexistant(req, req_p)
+            self.handle_nonexistent(req, req_p)
         } else {
             self.handle_delete_path(req, req_p, symlink)
         }
@@ -1025,8 +1068,8 @@ impl HttpHandler {
     fn handle_delete_path(&self, req: &mut Request, req_p: PathBuf, symlink: bool) -> IronResult<Response> {
         let ft = req_p.metadata().expect("failed to get file metadata").file_type();
         log!(self.log,
-             "{green}{}{reset} deleted {blue}{} {magenta}{}{reset}",
-             req.remote_addr,
+             "{} deleted {blue}{} {magenta}{}{reset}",
+             self.remote_addresses(&req),
              if is_actually_file(&ft) {
                  "file"
              } else if symlink {
@@ -1051,8 +1094,8 @@ impl HttpHandler {
 
     fn handle_trace(&self, req: &mut Request) -> IronResult<Response> {
         log!(self.log,
-             "{green}{}{reset} requested {red}TRACE{reset} for {magenta}{}{reset}",
-             req.remote_addr,
+             "{} requested {red}TRACE{reset} for {magenta}{}{reset}",
+             self.remote_addresses(&req),
              url_path(&req.url));
 
         let mut hdr = req.headers.clone();
@@ -1068,8 +1111,8 @@ impl HttpHandler {
 
     fn handle_forbidden_method(&self, req: &mut Request, switch: &str, desc: &str) -> IronResult<Response> {
         log!(self.log,
-             "{green}{}{reset} used disabled request method {red}{}{reset} grouped under {}",
-             req.remote_addr,
+             "{} used disabled request method {red}{}{reset} grouped under {}",
+             self.remote_addresses(&req),
              req.method,
              desc);
 
@@ -1086,8 +1129,8 @@ impl HttpHandler {
 
     fn handle_bad_method(&self, req: &mut Request) -> IronResult<Response> {
         log!(self.log,
-             "{green}{}{reset} used invalid request method {red}{}{reset}",
-             req.remote_addr,
+             "{} used invalid request method {red}{}{reset}",
+             self.remote_addresses(&req),
              req.method);
 
         let last_p = format!("<p>Unsupported request method: {}.<br />\nSupported methods: {}{}OPTIONS, GET, PUT, DELETE, HEAD, and TRACE.</p>",
@@ -1113,7 +1156,7 @@ impl HttpHandler {
                 if let Some(enc_resp) = self.cache_gen.read().expect("Generated file cache read lock poisoned").get(&cache_key) {
                     log!(self.log,
                          "{} encoded as {} for {:.1}% ratio (cached)",
-                         iter::repeat(' ').take(req.remote_addr.to_string().len()).collect::<String>(),
+                         Spaces(self.remote_addresses(req).to_string().len()),
                          encoding,
                          ((resp.len() as f64) / (enc_resp.len() as f64)) * 100f64);
 
@@ -1128,7 +1171,7 @@ impl HttpHandler {
             if let Some(enc_resp) = encode_str(&resp, &encoding) {
                 log!(self.log,
                      "{} encoded as {} for {:.1}% ratio",
-                     iter::repeat(' ').take(req.remote_addr.to_string().len()).collect::<String>(),
+                     Spaces(self.remote_addresses(req).to_string().len()),
                      encoding,
                      ((resp.len() as f64) / (enc_resp.len() as f64)) * 100f64);
 
@@ -1143,7 +1186,7 @@ impl HttpHandler {
             } else {
                 log!(self.log,
                      "{} failed to encode as {}, sending identity",
-                     iter::repeat(' ').take(req.remote_addr.to_string().len()).collect::<String>(),
+                     Spaces(self.remote_addresses(req).to_string().len()),
                      encoding);
             }
         }
@@ -1164,10 +1207,11 @@ impl HttpHandler {
     }
 
     fn parse_requested_path_custom_symlink(&self, req_url: &GenericUrl, follow_symlinks: bool) -> (PathBuf, bool, bool) {
-        req_url.path_segments()
+        let (mut cur, sk, err, abs) = req_url.path_segments()
             .unwrap()
             .filter(|p| !p.is_empty())
-            .fold((self.hosted_directory.1.clone(), false, false), |(mut cur, mut sk, mut err), pp| {
+            .fold((self.hosted_directory.1.clone(), false, false, true),
+                  |(mut cur, mut sk, mut err, mut abs), pp| {
                 if let Some(pp) = percent_decode(pp) {
                     cur.push(&*pp);
                 } else {
@@ -1176,13 +1220,27 @@ impl HttpHandler {
                 while let Ok(newlink) = cur.read_link() {
                     sk = true;
                     if follow_symlinks {
-                        cur = newlink;
+                        if newlink.is_absolute() {
+                            cur = newlink;
+                        } else {
+                            abs = false;
+                            cur.pop();
+                            cur.push(newlink);
+                        }
                     } else {
                         break;
                     }
                 }
-                (cur, sk, err)
-            })
+                (cur, sk, err, abs)
+            });
+
+        if !abs {
+            if let Ok(full) = cur.canonicalize() {
+                cur = full;
+            }
+        }
+
+        (cur, sk, err)
     }
 
     fn create_temp_dir(&self, td: &Option<(String, PathBuf)>) {
@@ -1191,6 +1249,14 @@ impl HttpHandler {
             log!(self.log, "Created temp dir {magenta}{}{reset}", temp_name);
         }
     }
+
+    #[inline(always)]
+    fn remote_addresses<'s, 'r, 'ra, 'rb: 'ra>(&'s self, req: &'r Request<'ra, 'rb>) -> AddressWriter<'r, 's, 'ra, 'rb> {
+        AddressWriter {
+            request: req,
+            proxies: &self.proxies,
+        }
+    }
 }
 
 impl Clone for HttpHandler {
@@ -1206,6 +1272,7 @@ impl Clone for HttpHandler {
             path_auth_data: self.path_auth_data.clone(),
             writes_temp_dir: self.writes_temp_dir.clone(),
             encoded_temp_dir: self.encoded_temp_dir.clone(),
+            proxies: self.proxies.clone(),
             cache_gen: Default::default(),
             cache_fs: Default::default(),
         }
@@ -1213,26 +1280,28 @@ 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,
+pub struct AddressWriter<'r, 'p, 'ra, 'rb: 'ra> {
+    pub request: &'r Request<'ra, 'rb>,
+    pub proxies: &'p BTreeMap<IpCidr, String>,
 }
 
-impl From<u64> for LogLevel {
-    fn from(raw: u64) -> LogLevel {
-        match raw {
-            0 => LogLevel::All,
-            1 => LogLevel::NoServeStatus,
-            2 => LogLevel::NoStartup,
-            _ => LogLevel::NoAuth,
+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)?;
+
+        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)?;
+                    }
+                }
+            }
         }
+
+        Ok(())
     }
 }
 
diff --git a/src/ops/webdav.rs b/src/ops/webdav.rs
index 3fdc098..3304160 100644
--- a/src/ops/webdav.rs
+++ b/src/ops/webdav.rs
@@ -45,7 +45,7 @@ impl HttpHandler {
 
         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_nonexistant(req, req_p);
+            return self.handle_nonexistent(req, req_p);
         }
 
 
@@ -66,8 +66,8 @@ impl HttpHandler {
                 } {
                     Ok(e) => {
                         log!(self.log,
-                             "{green}{}{reset} tried to {red}PROPFIND{reset} {yellow}{}{reset} with invalid XML",
-                             req.remote_addr,
+                             "{} tried to {red}PROPFIND{reset} {yellow}{}{reset} with invalid XML",
+                             self.remote_addresses(&req),
                              req_p.display());
                         return self.handle_generated_response_encoding(req,
                                                                        status::BadRequest,
@@ -79,8 +79,8 @@ impl HttpHandler {
         };
 
         log!(self.log,
-             "{green}{}{reset} requested {red}PROPFIND{reset} of {} on {yellow}{}{reset} at depth {}",
-             req.remote_addr,
+             "{} requested {red}PROPFIND{reset} of {} on {yellow}{}{reset} at depth {}",
+             self.remote_addresses(&req),
              props,
              req_p.display(),
              depth);
@@ -146,7 +146,12 @@ impl HttpHandler {
                 let mut symlink = false;
                 while let Ok(newlink) = path.read_link() {
                     symlink = true;
-                    path = newlink;
+                    if newlink.is_absolute() {
+                        path = newlink;
+                    } else {
+                        path.pop();
+                        path.push(newlink);
+                    }
                 }
 
                 if !(!path.exists() || (symlink && !self.follow_symlinks) ||
@@ -179,15 +184,15 @@ impl HttpHandler {
 
         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_nonexistant(req, req_p);
+            return self.handle_nonexistent(req, req_p);
         }
 
         let props = match parse_proppatch(req) {
             Ok(props) => props,
             Err(e) => {
                 log!(self.log,
-                     "{green}{}{reset} tried to {red}PROPPATCH{reset} {yellow}{}{reset} with invalid XML",
-                     req.remote_addr,
+                     "{} tried to {red}PROPPATCH{reset} {yellow}{}{reset} with invalid XML",
+                     self.remote_addresses(&req),
                      req_p.display());
                 return self.handle_generated_response_encoding(req,
                                                                status::BadRequest,
@@ -196,8 +201,8 @@ impl HttpHandler {
         };
 
         log!(self.log,
-             "{green}{}{reset} requested {red}PROPPATCH{reset} of {} on {yellow}{}{reset}",
-             req.remote_addr,
+             "{} requested {red}PROPPATCH{reset} of {} on {yellow}{}{reset}",
+             self.remote_addresses(&req),
              CommaList(props.iter().map(|p| &p.0.local_name)),
              req_p.display());
 
@@ -211,8 +216,8 @@ impl HttpHandler {
         let (req_p, symlink, url_err) = self.parse_requested_path(req);
 
         log!(self.log,
-             "{green}{}{reset} requested to {red}MKCOL{reset} at {yellow}{}{reset}",
-             req.remote_addr,
+             "{} requested to {red}MKCOL{reset} at {yellow}{}{reset}",
+             self.remote_addresses(&req),
              req_p.display());
 
         if url_err {
@@ -225,7 +230,7 @@ impl HttpHandler {
 
         if !req_p.parent().map(|pp| pp.exists()).unwrap_or(true) || (symlink && !self.follow_symlinks) ||
            (symlink && self.follow_symlinks && self.sandbox_symlinks && !is_descendant_of(&req_p, &self.hosted_directory.1)) {
-            return self.handle_nonexistant_status(req, req_p, status::Conflict);
+            return self.handle_nonexistent_status(req, req_p, status::Conflict);
         }
 
         if req.body.read_exact(&mut [0]).is_ok() {
@@ -236,7 +241,7 @@ impl HttpHandler {
             Ok(()) => Ok(Response::with(status::Created)),
             Err(e) => {
                 match e.kind() {
-                    IoErrorKind::NotFound => self.handle_nonexistant_status(req, req_p, status::Conflict),
+                    IoErrorKind::NotFound => self.handle_nonexistent_status(req, req_p, status::Conflict),
                     IoErrorKind::AlreadyExists => Ok(Response::with((status::MethodNotAllowed, "File exists"))),
                     _ => Ok(Response::with(status::Forbidden)),
                 }
@@ -286,15 +291,15 @@ impl HttpHandler {
 
                 (dest_p, dest_symlink)
             }
-            None => return self.handle_invalid_url(req, "<p>Destination URL invalid or nonexistant.</p>"),
+            None => return self.handle_invalid_url(req, "<p>Destination URL invalid or nonexistent.</p>"),
         };
 
         let depth = req.headers.get::<Depth>().copied().unwrap_or(Depth::Infinity);
         let overwrite = req.headers.get::<Overwrite>().copied().unwrap_or_default().0;
 
         log!(self.log,
-             "{green}{}{reset} requested to {}{red}{}{reset} {yellow}{}{reset} to {yellow}{}{reset} at depth {}",
-             req.remote_addr,
+             "{} requested to {}{red}{}{reset} {yellow}{}{reset} to {yellow}{}{reset} at depth {}",
+             self.remote_addresses(&req),
              if overwrite { "overwrite-" } else { "" },
              if !is_move { "COPY" } else { "MOVE" },
              req_p.display(),
@@ -311,7 +316,7 @@ impl HttpHandler {
 
         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_nonexistant(req, req_p);
+            return self.handle_nonexistent(req, req_p);
         }
 
         if !dest_p.parent().map(|pp| pp.exists()).unwrap_or(true) || (dest_symlink && !self.follow_symlinks) ||
diff --git a/src/options.rs b/src/options.rs
index 63d3397..b5702a7 100644
--- a/src/options.rs
+++ b/src/options.rs
@@ -14,12 +14,13 @@
 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::borrow::Cow;
 use std::net::IpAddr;
 use regex::Regex;
+use cidr::IpCidr;
 use std::fs;
 
 
@@ -29,6 +30,30 @@ lazy_static! {
 }
 
 
+#[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,
+        }
+    }
+}
+
+
 /// Representation of the application's all configurable values.
 #[derive(Debug, Clone, Hash, PartialEq, Eq)]
 pub struct Options {
@@ -71,6 +96,8 @@ pub struct Options {
     pub path_auth_data: BTreeMap<String, Option<String>>,
     /// Paths for which to generate auth data
     pub generate_path_auth: BTreeSet<String>,
+    /// Header names and who we trust them from in `HEADER-NAME:CIDR` format
+    pub proxies: BTreeMap<IpCidr, String>,
 }
 
 impl Options {
@@ -103,6 +130,8 @@ impl Options {
             .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'"))
+            .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(|_| ())))
             .get_matches();
 
         let dir = matches.value_of("DIR").unwrap_or(".");
@@ -174,6 +203,7 @@ impl Options {
             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(),
         }
     }
 
@@ -181,7 +211,7 @@ impl Options {
         fs::canonicalize(&s).map_err(|_| format!("{} \"{}\" not found", prefix, s)).and_then(|f| if f.is_dir() {
             Ok(())
         } else {
-            Err(format!("{} \"{}\" not actualy a directory", prefix, s))
+            Err(format!("{} \"{}\" not actually a directory", prefix, s))
         })
     }
 
@@ -189,7 +219,7 @@ impl Options {
         fs::canonicalize(&s).map_err(|_| format!("TLS identity file \"{}\" not found", s)).and_then(|f| if f.is_file() {
             Ok(())
         } else {
-            Err(format!("TLS identity file \"{}\" not actualy a file", s))
+            Err(format!("TLS identity file \"{}\" not actually a file", s))
         })
     }
 
@@ -261,4 +291,18 @@ impl Options {
     fn u16_validator(s: String) -> Result<(), String> {
         u16::from_str(&s).map(|_| ()).map_err(|_| format!("{} is not a valid port number", s))
     }
+
+    fn proxy_parse<'s>(s: Cow<'s, str>) -> Result<(IpCidr, String), String> {
+        match s.find(":") {
+            None => Err(format!("{} not in HEADER-NAME:CIDR format", s)),
+            Some(0) => Err(format!("{} sets invalid zero-length header", s)),
+            Some(col_idx) => {
+                let cidr = s[col_idx + 1..].parse().map_err(|e| format!("{} not a valid CIDR: {}", &s[col_idx + 1..], e))?;
+
+                let mut s = s.into_owned();
+                s.truncate(col_idx);
+                Ok((cidr, s))
+            }
+        }
+    }
 }
diff --git a/src/util/mod.rs b/src/util/mod.rs
index 5c746b5..d0adad0 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -11,12 +11,13 @@ use percent_encoding;
 use walkdir::WalkDir;
 use std::borrow::Cow;
 use rfsapi::RawFileData;
+use std::{cmp, f64, str};
 use std::time::SystemTime;
-use std::{cmp, f64, fmt, str};
 use std::collections::HashMap;
 use time::{self, Duration, Tm};
 use iron::{mime, Headers, Url};
 use base64::display::Base64Display;
+use std::fmt::{self, Write as FmtWrite};
 use iron::error::HttpResult as HyperResult;
 use std::fs::{self, FileType, Metadata, File};
 use iron::headers::{HeaderFormat, UserAgent, Header};
@@ -49,28 +50,47 @@ lazy_static! {
         ass.insert("dir_icon",
             Cow::Owned(format!("data:{};base64,{}",
                                get_mime_type_str("gif").unwrap(),
-                               Base64Display::with_config(&include_bytes!("../../assets/icons/directory_icon.gif")[..], base64::STANDARD))));
+                               Base64Display::with_config(&include_bytes!("../../assets/icons/directory.gif")[..], base64::STANDARD))));
         ass.insert("file_icon",
             Cow::Owned(format!("data:{};base64,{}",
                                get_mime_type_str("gif").unwrap(),
-                               Base64Display::with_config(&include_bytes!("../../assets/icons/file_icon.gif")[..], base64::STANDARD))));
+                               Base64Display::with_config(&include_bytes!("../../assets/icons/file.gif")[..], base64::STANDARD))));
         ass.insert("file_binary_icon",
             Cow::Owned(format!("data:{};base64,{}",
                                get_mime_type_str("gif").unwrap(),
-                               Base64Display::with_config(&include_bytes!("../../assets/icons/file_binary_icon.gif")[..], base64::STANDARD))));
+                               Base64Display::with_config(&include_bytes!("../../assets/icons/file_binary.gif")[..], base64::STANDARD))));
         ass.insert("file_image_icon",
             Cow::Owned(format!("data:{};base64,{}",
                                get_mime_type_str("gif").unwrap(),
-                               Base64Display::with_config(&include_bytes!("../../assets/icons/file_image_icon.gif")[..], base64::STANDARD))));
+                               Base64Display::with_config(&include_bytes!("../../assets/icons/file_image.gif")[..], base64::STANDARD))));
         ass.insert("file_text_icon",
             Cow::Owned(format!("data:{};base64,{}",
                                get_mime_type_str("gif").unwrap(),
-                               Base64Display::with_config(&include_bytes!("../../assets/icons/file_text_icon.gif")[..], base64::STANDARD))));
+                               Base64Display::with_config(&include_bytes!("../../assets/icons/file_text.gif")[..], base64::STANDARD))));
         ass.insert("back_arrow_icon",
             Cow::Owned(format!("data:{};base64,{}",
                                get_mime_type_str("gif").unwrap(),
-                               Base64Display::with_config(&include_bytes!("../../assets/icons/back_arrow_icon.gif")[..], base64::STANDARD))));
+                               Base64Display::with_config(&include_bytes!("../../assets/icons/back_arrow.gif")[..], base64::STANDARD))));
+        ass.insert("new_dir_icon",
+            Cow::Owned(format!("data:{};base64,{}",
+                               get_mime_type_str("gif").unwrap(),
+                               Base64Display::with_config(&include_bytes!("../../assets/icons/new_directory.gif")[..], base64::STANDARD))));
+        ass.insert("delete_file_icon",
+            Cow::Owned(format!("data:{};base64,{}",
+                               get_mime_type_str("png").unwrap(),
+                               Base64Display::with_config(&include_bytes!("../../assets/icons/delete_file.png")[..], base64::STANDARD))));
+        ass.insert("rename_icon",
+            Cow::Owned(format!("data:{};base64,{}",
+                               get_mime_type_str("png").unwrap(),
+                               Base64Display::with_config(&include_bytes!("../../assets/icons/rename.png")[..], base64::STANDARD))));
+        ass.insert("confirm_icon",
+            Cow::Owned(format!("data:{};base64,{}",
+                               get_mime_type_str("png").unwrap(),
+                               Base64Display::with_config(&include_bytes!("../../assets/icons/confirm.png")[..], base64::STANDARD))));
         ass.insert("date", Cow::Borrowed(include_str!("../../assets/date.js")));
+        ass.insert("manage", Cow::Borrowed(include_str!("../../assets/manage.js")));
+        ass.insert("manage_mobile", Cow::Borrowed(include_str!("../../assets/manage_mobile.js")));
+        ass.insert("manage_desktop", Cow::Borrowed(include_str!("../../assets/manage_desktop.js")));
         ass.insert("upload", Cow::Borrowed(include_str!("../../assets/upload.js")));
         ass.insert("adjust_tz", Cow::Borrowed(include_str!("../../assets/adjust_tz.js")));
         ass
@@ -132,6 +152,18 @@ impl<D: fmt::Display, I: Iterator<Item = D> + Clone> fmt::Display for CommaList<
     }
 }
 
+#[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
+pub struct DisplayThree<Df: fmt::Display, Ds: fmt::Display, Dt: fmt::Display>(pub Df, pub Ds, pub Dt);
+
+impl<Df: fmt::Display, Ds: fmt::Display, Dt: fmt::Display> fmt::Display for DisplayThree<Df, Ds, Dt> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.0.fmt(f)?;
+        self.1.fmt(f)?;
+        self.2.fmt(f)?;
+        Ok(())
+    }
+}
+
 
 /// `xml`'s `OwnedName::borrow()` returns a value not a reference, so it cannot be used with the libstd `Borrow` trait
 pub trait BorrowXmlName<'n> {
@@ -152,6 +184,18 @@ impl<'n> BorrowXmlName<'n> for OwnedXmlName {
     }
 }
 
+#[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
+pub struct Spaces(pub usize);
+
+impl fmt::Display for Spaces {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        for _ in 0..self.0 {
+            f.write_char(' ')?;
+        }
+        Ok(())
+    }
+}
+
 
 
 /// Uppercase the first character of the supplied string.
@@ -346,7 +390,7 @@ pub fn is_descendant_of<Pw: AsRef<Path>, Po: AsRef<Path>>(who: Pw, of_whom: Po)
 
 /// Check if the specified path is a direct descendant (or an equal) of the specified path, without without requiring it to
 /// exist in the first place.
-pub fn is_nonexistant_descendant_of<Pw: AsRef<Path>, Po: AsRef<Path>>(who: Pw, of_whom: Po) -> bool {
+pub fn is_nonexistent_descendant_of<Pw: AsRef<Path>, Po: AsRef<Path>>(who: Pw, of_whom: Po) -> bool {
     let mut who = fs::canonicalize(&who).unwrap_or_else(|_| who.as_ref().to_path_buf());
     let of_whom = if let Ok(p) = fs::canonicalize(of_whom) {
         p


Test patch: