From 88c56de97b48bb5b9b8af350d0d0e0d5f080ff0e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Garc=C3=ADa?=
 <dani-garcia@users.noreply.github.com>
Date: Fri, 27 Dec 2019 18:42:39 +0100
Subject: [PATCH] Config option for client IP header

---
 .env.template |  4 ++++
 src/auth.rs   | 17 +++++++++++++----
 src/config.rs | 14 ++++++++++++--
 3 files changed, 29 insertions(+), 6 deletions(-)

diff --git a/.env.template b/.env.template
index 5b2fe2ce..6a74fa52 100644
--- a/.env.template
+++ b/.env.template
@@ -21,6 +21,10 @@
 ## Automatically reload the templates for every request, slow, use only for development
 # RELOAD_TEMPLATES=false
 
+## Client IP Header, used to identify the IP of the client, defaults to "X-Client-IP"
+## Set to the string "none" (without quotes), to disable any headers and just use the remote IP
+# IP_HEADER=X-Client-IP
+
 ## Cache time-to-live for successfully obtained icons, in seconds (0 is "forever")
 # ICON_CACHE_TTL=2592000
 ## Cache time-to-live for icons which weren't available, in seconds (0 is "forever")
diff --git a/src/auth.rs b/src/auth.rs
index a0f3f15e..4871a9fd 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -426,12 +426,21 @@ pub struct ClientIp {
 impl<'a, 'r> FromRequest<'a, 'r> for ClientIp {
     type Error = ();
 
-    fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
-        let ip = match request.client_ip() {
-            Some(addr) => addr,
-            None => "0.0.0.0".parse().unwrap(),
+    fn from_request(req: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
+        let ip = if CONFIG._ip_header_enabled() {
+            req.headers().get_one(&CONFIG.ip_header()).and_then(|ip| {
+                ip.parse()
+                    .map_err(|_| warn_!("'{}' header is malformed: {}", CONFIG.ip_header(), ip))
+                    .ok()
+            })
+        } else {
+            None
         };
 
+        let ip = ip
+            .or_else(|| req.remote().map(|r| r.ip()))
+            .unwrap_or_else(|| "0.0.0.0".parse().unwrap());
+
         Outcome::Success(ClientIp { ip })
     }
 }
diff --git a/src/config.rs b/src/config.rs
index 296f2a3a..3eed93bc 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -185,19 +185,24 @@ macro_rules! make_config {
             }
         }
     }};
+    ( @build $value:expr, $config:expr, gen, $default_fn:expr ) => {{
+        let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn;
+        f($config)
+    }};
 }
 
 //STRUCTURE:
 // /// Short description (without this they won't appear on the list)
 // group {
 //   /// Friendly Name |> Description (Optional)
-//   name: type, is_editable, none_action, <default_value (Optional)>
+//   name: type, is_editable, action, <default_value (Optional)>
 // }
 //
-// Where none_action applied when the value wasn't provided and can be:
+// Where action applied when the value wasn't provided and can be:
 //  def:    Use a default value
 //  auto:   Value is auto generated based on other values
 //  option: Value is optional
+//  gen:    Value is always autogenerated and it's original value ignored
 make_config! {
     folders {
         ///  Data folder |> Main data folder
@@ -266,6 +271,11 @@ make_config! {
 
     /// Advanced settings
     advanced {
+        /// Client IP header |> If not present, the remote IP is used.
+        /// Set to the string "none" (without quotes), to disable any headers and just use the remote IP
+        ip_header:              String, true,   def,    "X-Real-IP".to_string();
+        /// Internal IP header property, used to avoid recomputing each time
+        _ip_header_enabled:     bool,   false,  gen,    |c| &c.ip_header.trim().to_lowercase() != "none";
         /// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be redownloaded
         icon_cache_ttl:         u64,    true,   def,    2_592_000;
         /// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.