Repository: sanic-org/sanic

Issue:
sanic behind proxy - option to replace IP of request.ip with X-Forwarded-For value
Dear Devs,
Please consider adding an option to replace request.ip with X-Forwarded-For value, it will make life a lot easier for those of us who running sanic behind nginx or load balancer.

I can get real IP with request.headers.get('X-Forwarded-For') but would be good to have an option to set custom header for sanic to get ip address from so it can get added in sanic logs, console etc.
It's quite common and as an example Flask has ProxyFix to address this.


Discussion:
#853 looks unconfigurable and unsafe, something like [werkzeug ProxyFix](https://github.com/pallets/werkzeug/blob/64d1d2117cc177b9caf18bb571f32133492978c3/werkzeug/contrib/fixers.py#L97) would be better
...but this is still unconfigurable and unsafe :/
Perhaps, but the original need was addressed, reviewed, and merged. @andreymal perhaps you can submit a new PR to address your concerns?

Resolving patch:
diff --git a/sanic/config.py b/sanic/config.py
--- a/sanic/config.py
+++ b/sanic/config.py
@@ -27,6 +27,9 @@
     "WEBSOCKET_WRITE_LIMIT": 2 ** 16,
     "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0,  # 15 sec
     "ACCESS_LOG": True,
+    "PROXIES_COUNT": -1,
+    "FORWARDED_FOR_HEADER": "X-Forwarded-For",
+    "REAL_IP_HEADER": "X-Real-IP",
 }
 
 
diff --git a/sanic/request.py b/sanic/request.py
--- a/sanic/request.py
+++ b/sanic/request.py
@@ -355,19 +355,38 @@ def _get_address(self):
 
     @property
     def remote_addr(self):
-        """Attempt to return the original client ip based on X-Forwarded-For.
+        """Attempt to return the original client ip based on X-Forwarded-For
+        or X-Real-IP. If HTTP headers are unavailable or untrusted, returns
+        an empty string.
 
         :return: original client ip.
         """
         if not hasattr(self, "_remote_addr"):
-            forwarded_for = self.headers.get("X-Forwarded-For", "").split(",")
-            remote_addrs = [
-                addr
-                for addr in [addr.strip() for addr in forwarded_for]
-                if addr
-            ]
-            if len(remote_addrs) > 0:
-                self._remote_addr = remote_addrs[0]
+            if self.app.config.PROXIES_COUNT == 0:
+                self._remote_addr = ""
+            elif self.app.config.REAL_IP_HEADER and self.headers.get(
+                self.app.config.REAL_IP_HEADER
+            ):
+                self._remote_addr = self.headers[
+                    self.app.config.REAL_IP_HEADER
+                ]
+            elif self.app.config.FORWARDED_FOR_HEADER:
+                forwarded_for = self.headers.get(
+                    self.app.config.FORWARDED_FOR_HEADER, ""
+                ).split(",")
+                remote_addrs = [
+                    addr
+                    for addr in [addr.strip() for addr in forwarded_for]
+                    if addr
+                ]
+                if self.app.config.PROXIES_COUNT == -1:
+                    self._remote_addr = remote_addrs[0]
+                elif len(remote_addrs) >= self.app.config.PROXIES_COUNT:
+                    self._remote_addr = remote_addrs[
+                        -self.app.config.PROXIES_COUNT
+                    ]
+                else:
+                    self._remote_addr = ""
             else:
                 self._remote_addr = ""
         return self._remote_addr


Test patch:
diff --git a/tests/test_requests.py b/tests/test_requests.py
--- a/tests/test_requests.py
+++ b/tests/test_requests.py
@@ -29,7 +29,7 @@ def handler(request):
     assert response.text == "Hello"
 
 
-def test_remote_address(app):
+def test_ip(app):
     @app.route("/")
     def handler(request):
         return text("{}".format(request.ip))
@@ -203,11 +203,23 @@ async def handler(request):
     assert response.text == "application/json"
 
 
-def test_remote_addr(app):
+def test_remote_addr_with_two_proxies(app):
+    app.config.PROXIES_COUNT = 2
+
     @app.route("/")
     async def handler(request):
         return text(request.remote_addr)
 
+    headers = {"X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1"}
+    request, response = app.test_client.get("/", headers=headers)
+    assert request.remote_addr == "127.0.0.2"
+    assert response.text == "127.0.0.2"
+
+    headers = {"X-Forwarded-For": "127.0.1.1"}
+    request, response = app.test_client.get("/", headers=headers)
+    assert request.remote_addr == ""
+    assert response.text == ""
+
     headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"}
     request, response = app.test_client.get("/", headers=headers)
     assert request.remote_addr == "127.0.0.1"
@@ -222,6 +234,86 @@ async def handler(request):
     assert request.remote_addr == "127.0.0.1"
     assert response.text == "127.0.0.1"
 
+    headers = {
+        "X-Forwarded-For": ", 127.0.2.2, ,  ,127.0.0.1, ,   ,,127.0.1.2"
+    }
+    request, response = app.test_client.get("/", headers=headers)
+    assert request.remote_addr == "127.0.0.1"
+    assert response.text == "127.0.0.1"
+
+
+def test_remote_addr_with_infinite_number_of_proxies(app):
+    app.config.PROXIES_COUNT = -1
+
+    @app.route("/")
+    async def handler(request):
+        return text(request.remote_addr)
+
+    headers = {"X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1"}
+    request, response = app.test_client.get("/", headers=headers)
+    assert request.remote_addr == "127.0.0.2"
+    assert response.text == "127.0.0.2"
+
+    headers = {"X-Forwarded-For": "127.0.1.1"}
+    request, response = app.test_client.get("/", headers=headers)
+    assert request.remote_addr == "127.0.1.1"
+    assert response.text == "127.0.1.1"
+
+    headers = {
+        "X-Forwarded-For": "127.0.0.5, 127.0.0.4, 127.0.0.3, 127.0.0.2, 127.0.0.1"
+    }
+    request, response = app.test_client.get("/", headers=headers)
+    assert request.remote_addr == "127.0.0.5"
+    assert response.text == "127.0.0.5"
+
+
+def test_remote_addr_without_proxy(app):
+    app.config.PROXIES_COUNT = 0
+
+    @app.route("/")
+    async def handler(request):
+        return text(request.remote_addr)
+
+    headers = {"X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1"}
+    request, response = app.test_client.get("/", headers=headers)
+    assert request.remote_addr == ""
+    assert response.text == ""
+
+    headers = {"X-Forwarded-For": "127.0.1.1"}
+    request, response = app.test_client.get("/", headers=headers)
+    assert request.remote_addr == ""
+    assert response.text == ""
+
+    headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"}
+    request, response = app.test_client.get("/", headers=headers)
+    assert request.remote_addr == ""
+    assert response.text == ""
+
+
+def test_remote_addr_custom_headers(app):
+    app.config.PROXIES_COUNT = 1
+    app.config.REAL_IP_HEADER = "Client-IP"
+    app.config.FORWARDED_FOR_HEADER = "Forwarded"
+
+    @app.route("/")
+    async def handler(request):
+        return text(request.remote_addr)
+
+    headers = {"X-Real-IP": "127.0.0.2", "Forwarded": "127.0.1.1"}
+    request, response = app.test_client.get("/", headers=headers)
+    assert request.remote_addr == "127.0.1.1"
+    assert response.text == "127.0.1.1"
+
+    headers = {"X-Forwarded-For": "127.0.1.1"}
+    request, response = app.test_client.get("/", headers=headers)
+    assert request.remote_addr == ""
+    assert response.text == ""
+
+    headers = {"Client-IP": "127.0.0.2", "Forwarded": "127.0.1.1"}
+    request, response = app.test_client.get("/", headers=headers)
+    assert request.remote_addr == "127.0.0.2"
+    assert response.text == "127.0.0.2"
+
 
 def test_match_info(app):
     @app.route("/api/v1/user/<user_id>/")