Repository: huge-success/sanic (python)

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.

Pull request description:
Add options to control the behavior of Request.remote_addr
Closes #801 

Using `X-Forwarded-For` to determine real client ip is totally unsafe when proxy is not used, because client can fake this http header. This PR adds `PROXIES_COUNT` option to control the usage of this http header.

When nginx is used, `PROXIES_COUNT` should be `1` and `X-Forwarded-For` will be parsed. When nginx is not used (or does not override `X-Forwarded-For`), `PROXIES_COUNT` should be `0` and `X-Forwarded-For` will be ignored.

This PR also adds `X-Real-IP` that is very useful with nginx, and `FORWARDED_FOR_HEADER` and `REAL_IP_HEADER` options that allow to change the names of these headers.

There are some weird things:

* `PROXIES_COUNT` is `-1` by default, which means using the first ip from `X-Forwarded-For`. It's still totally unsafe, but backwards-compatible. It should be set to `0` for security reasons, but it changes the default behavior and probably breaks the compatibility.

* When `X-Forwarded-For` or `X-Real-IP` are unavailable or untrusted, `request.remote_addr` returns an empty string. This is compatible with the old behavior, but this is weird, I don't know why I have to use `ip = (request.remote_addr or request.ip)` boilerplate.

* English is not my native language, and I use Google Translate, so the documentation may be crappy, sorry for that D:

Patch:
diff --git a/.gitignore b/.gitignore
index 2aa0bef3..91cac370 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,5 @@ docs/_build/
 docs/_api/
 build/*
 .DS_Store
-dist/*
\ No newline at end of file
+dist/*
+pip-wheel-metadata/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0d92c563..dd866835 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,114 @@
+Version 19.3
+-------------
+19.3.1
+  - Changes:
+    - [#1497](https://github.com/huge-success/sanic/pull/1497)
+      Add support for zero-length and RFC 5987 encoded filename for
+      multipart/form-data requests.
+
+    - [#1484](https://github.com/huge-success/sanic/pull/1484)
+      The type of `expires` attribute of `sanic.cookies.Cookie` is now
+      enforced to be of type `datetime`.
+
+    - [#1482](https://github.com/huge-success/sanic/pull/1482)
+      Add support for the `stream` parameter of `sanic.Sanic.add_route()`
+      available to `sanic.Blueprint.add_route()`.
+
+    - [#1481](https://github.com/huge-success/sanic/pull/1481)
+      Accept negative values for route parameters with type `int` or `number`.
+
+    - [#1476](https://github.com/huge-success/sanic/pull/1476)
+      Deprecated the use of `sanic.request.Request.raw_args` - it has a
+      fundamental flaw in which is drops repeated query string parameters.
+      Added `sanic.request.Request.query_args` as a replacement for the
+      original use-case.
+
+    - [#1472](https://github.com/huge-success/sanic/pull/1472)
+      Remove an unwanted `None` check in Request class `repr` implementation.
+      This changes the default `repr` of a Request from `<Request>` to
+      `<Request: None />`
+
+    - [#1470](https://github.com/huge-success/sanic/pull/1470)
+      Added 2 new parameters to `sanic.app.Sanic.create_server`:
+      - `return_asyncio_server` - whether to return an asyncio.Server.
+      - `asyncio_server_kwargs` - kwargs to pass to `loop.create_server` for
+        the event loop that sanic is using.
+
+      This is a breaking change.
+
+    - [#1499](https://github.com/huge-success/sanic/pull/1499)
+      Added a set of test cases that test and benchmark route resolution.
+
+    - [#1457](https://github.com/huge-success/sanic/pull/1457)
+      The type of the `"max-age"` value in a `sanic.cookies.Cookie` is now
+      enforced to be an integer. Non-integer values are replaced with `0`.
+
+    - [#1445](https://github.com/huge-success/sanic/pull/1445)
+      Added the `endpoint` attribute to an incoming `request`, containing the
+      name of the handler function.
+
+    - [#1423](https://github.com/huge-success/sanic/pull/1423)
+      Improved request streaming. `request.stream` is now a bounded-size buffer
+      instead of an unbounded queue. Callers must now call
+      `await request.stream.read()` instead of `await request.stream.get()`
+      to read each portion of the body.
+
+      This is a breaking change.
+
+  - Fixes:
+    - [#1502](https://github.com/huge-success/sanic/pull/1502)
+      Sanic was prefetching `time.time()` and updating it once per second to
+      avoid excessive `time.time()` calls. The implementation was observed to
+      cause memory leaks in some cases. The benefit of the prefetch appeared
+      to negligible, so this has been removed. Fixes
+      [#1500](https://github.com/huge-success/sanic/pull/1500)
+
+    - [#1501](https://github.com/huge-success/sanic/pull/1501)
+      Fix a bug in the auto-reloader when the process was launched as a module
+      i.e. `python -m init0.mod1` where the sanic server is started
+      in `init0/mod1.py` with `debug` enabled and imports another module in
+      `init0`.
+
+    - [#1376](https://github.com/huge-success/sanic/pull/1376)
+      Allow sanic test client to bind to a random port by specifying
+      `port=None` when constructing a `SanicTestClient`
+
+    - [#1399](https://github.com/huge-success/sanic/pull/1399)
+      Added the ability to specify middleware on a blueprint group, so that all
+      routes produced from the blueprints in the group have the middleware
+      applied.
+
+    - [#1442](https://github.com/huge-success/sanic/pull/1442)
+      Allow the the use the `SANIC_ACCESS_LOG` environment variable to
+      enable/disable the access log when not explicitly passed to `app.run()`.
+      This allows the access log to be disabled for example when running via
+      gunicorn.
+
+  - Developer infrastructure:
+    - [#1529](https://github.com/huge-success/sanic/pull/1529) Update project PyPI credentials
+    - [#1515](https://github.com/huge-success/sanic/pull/1515) fix linter issue causing travis build failures (fix #1514)
+    - [#1490](https://github.com/huge-success/sanic/pull/1490) Fix python version in doc build
+    - [#1478](https://github.com/huge-success/sanic/pull/1478) Upgrade setuptools version and use native docutils in doc build
+    - [#1464](https://github.com/huge-success/sanic/pull/1464) Upgrade pytest, and fix caplog unit tests
+
+  - Typos and Documentation:
+    - [#1516](https://github.com/huge-success/sanic/pull/1516) Fix typo at the exception documentation
+    - [#1510](https://github.com/huge-success/sanic/pull/1510) fix typo in Asyncio example
+    - [#1486](https://github.com/huge-success/sanic/pull/1486) Documentation typo
+    - [#1477](https://github.com/huge-success/sanic/pull/1477) Fix grammar in README.md
+    - [#1489](https://github.com/huge-success/sanic/pull/1489) Added "databases" to the extensions list
+    - [#1483](https://github.com/huge-success/sanic/pull/1483) Add sanic-zipkin to extensions list
+    - [#1487](https://github.com/huge-success/sanic/pull/1487) Removed link to deleted repo, Sanic-OAuth, from the extensions list
+    - [#1460](https://github.com/huge-success/sanic/pull/1460) 18.12 changelog
+    - [#1449](https://github.com/huge-success/sanic/pull/1449) Add example of amending request object
+    - [#1446](https://github.com/huge-success/sanic/pull/1446) Update README
+    - [#1444](https://github.com/huge-success/sanic/pull/1444) Update README
+    - [#1443](https://github.com/huge-success/sanic/pull/1443) Update README, including new logo
+    - [#1440](https://github.com/huge-success/sanic/pull/1440) fix minor type and pip install instruction mismatch
+    - [#1424](https://github.com/huge-success/sanic/pull/1424) Documentation Enhancements
+
+Note: 19.3.0 was skipped for packagement purposes and not released on PyPI
+
 Version 18.12
 -------------
 18.12.0
diff --git a/docs/sanic/asyncio_python37.rst b/docs/sanic/asyncio_python37.rst
index 5bdb9e45..2fa71f2b 100644
--- a/docs/sanic/asyncio_python37.rst
+++ b/docs/sanic/asyncio_python37.rst
@@ -4,7 +4,7 @@ Python 3.7 AsyncIO examples
 With Python 3.7 AsyncIO got major update for the following types:
 
  - asyncio.AbstractEventLoop
- - asyncio.AnstractServer
+ - asyncio.AbstractServer
 
 
 This example shows how to use sanic with Python 3.7, to be precise: how to retrieve an asyncio server instance:
diff --git a/docs/sanic/changelog.md b/docs/sanic/changelog.md
index 0d92c563..dd866835 100644
--- a/docs/sanic/changelog.md
+++ b/docs/sanic/changelog.md
@@ -1,3 +1,114 @@
+Version 19.3
+-------------
+19.3.1
+  - Changes:
+    - [#1497](https://github.com/huge-success/sanic/pull/1497)
+      Add support for zero-length and RFC 5987 encoded filename for
+      multipart/form-data requests.
+
+    - [#1484](https://github.com/huge-success/sanic/pull/1484)
+      The type of `expires` attribute of `sanic.cookies.Cookie` is now
+      enforced to be of type `datetime`.
+
+    - [#1482](https://github.com/huge-success/sanic/pull/1482)
+      Add support for the `stream` parameter of `sanic.Sanic.add_route()`
+      available to `sanic.Blueprint.add_route()`.
+
+    - [#1481](https://github.com/huge-success/sanic/pull/1481)
+      Accept negative values for route parameters with type `int` or `number`.
+
+    - [#1476](https://github.com/huge-success/sanic/pull/1476)
+      Deprecated the use of `sanic.request.Request.raw_args` - it has a
+      fundamental flaw in which is drops repeated query string parameters.
+      Added `sanic.request.Request.query_args` as a replacement for the
+      original use-case.
+
+    - [#1472](https://github.com/huge-success/sanic/pull/1472)
+      Remove an unwanted `None` check in Request class `repr` implementation.
+      This changes the default `repr` of a Request from `<Request>` to
+      `<Request: None />`
+
+    - [#1470](https://github.com/huge-success/sanic/pull/1470)
+      Added 2 new parameters to `sanic.app.Sanic.create_server`:
+      - `return_asyncio_server` - whether to return an asyncio.Server.
+      - `asyncio_server_kwargs` - kwargs to pass to `loop.create_server` for
+        the event loop that sanic is using.
+
+      This is a breaking change.
+
+    - [#1499](https://github.com/huge-success/sanic/pull/1499)
+      Added a set of test cases that test and benchmark route resolution.
+
+    - [#1457](https://github.com/huge-success/sanic/pull/1457)
+      The type of the `"max-age"` value in a `sanic.cookies.Cookie` is now
+      enforced to be an integer. Non-integer values are replaced with `0`.
+
+    - [#1445](https://github.com/huge-success/sanic/pull/1445)
+      Added the `endpoint` attribute to an incoming `request`, containing the
+      name of the handler function.
+
+    - [#1423](https://github.com/huge-success/sanic/pull/1423)
+      Improved request streaming. `request.stream` is now a bounded-size buffer
+      instead of an unbounded queue. Callers must now call
+      `await request.stream.read()` instead of `await request.stream.get()`
+      to read each portion of the body.
+
+      This is a breaking change.
+
+  - Fixes:
+    - [#1502](https://github.com/huge-success/sanic/pull/1502)
+      Sanic was prefetching `time.time()` and updating it once per second to
+      avoid excessive `time.time()` calls. The implementation was observed to
+      cause memory leaks in some cases. The benefit of the prefetch appeared
+      to negligible, so this has been removed. Fixes
+      [#1500](https://github.com/huge-success/sanic/pull/1500)
+
+    - [#1501](https://github.com/huge-success/sanic/pull/1501)
+      Fix a bug in the auto-reloader when the process was launched as a module
+      i.e. `python -m init0.mod1` where the sanic server is started
+      in `init0/mod1.py` with `debug` enabled and imports another module in
+      `init0`.
+
+    - [#1376](https://github.com/huge-success/sanic/pull/1376)
+      Allow sanic test client to bind to a random port by specifying
+      `port=None` when constructing a `SanicTestClient`
+
+    - [#1399](https://github.com/huge-success/sanic/pull/1399)
+      Added the ability to specify middleware on a blueprint group, so that all
+      routes produced from the blueprints in the group have the middleware
+      applied.
+
+    - [#1442](https://github.com/huge-success/sanic/pull/1442)
+      Allow the the use the `SANIC_ACCESS_LOG` environment variable to
+      enable/disable the access log when not explicitly passed to `app.run()`.
+      This allows the access log to be disabled for example when running via
+      gunicorn.
+
+  - Developer infrastructure:
+    - [#1529](https://github.com/huge-success/sanic/pull/1529) Update project PyPI credentials
+    - [#1515](https://github.com/huge-success/sanic/pull/1515) fix linter issue causing travis build failures (fix #1514)
+    - [#1490](https://github.com/huge-success/sanic/pull/1490) Fix python version in doc build
+    - [#1478](https://github.com/huge-success/sanic/pull/1478) Upgrade setuptools version and use native docutils in doc build
+    - [#1464](https://github.com/huge-success/sanic/pull/1464) Upgrade pytest, and fix caplog unit tests
+
+  - Typos and Documentation:
+    - [#1516](https://github.com/huge-success/sanic/pull/1516) Fix typo at the exception documentation
+    - [#1510](https://github.com/huge-success/sanic/pull/1510) fix typo in Asyncio example
+    - [#1486](https://github.com/huge-success/sanic/pull/1486) Documentation typo
+    - [#1477](https://github.com/huge-success/sanic/pull/1477) Fix grammar in README.md
+    - [#1489](https://github.com/huge-success/sanic/pull/1489) Added "databases" to the extensions list
+    - [#1483](https://github.com/huge-success/sanic/pull/1483) Add sanic-zipkin to extensions list
+    - [#1487](https://github.com/huge-success/sanic/pull/1487) Removed link to deleted repo, Sanic-OAuth, from the extensions list
+    - [#1460](https://github.com/huge-success/sanic/pull/1460) 18.12 changelog
+    - [#1449](https://github.com/huge-success/sanic/pull/1449) Add example of amending request object
+    - [#1446](https://github.com/huge-success/sanic/pull/1446) Update README
+    - [#1444](https://github.com/huge-success/sanic/pull/1444) Update README
+    - [#1443](https://github.com/huge-success/sanic/pull/1443) Update README, including new logo
+    - [#1440](https://github.com/huge-success/sanic/pull/1440) fix minor type and pip install instruction mismatch
+    - [#1424](https://github.com/huge-success/sanic/pull/1424) Documentation Enhancements
+
+Note: 19.3.0 was skipped for packagement purposes and not released on PyPI
+
 Version 18.12
 -------------
 18.12.0
diff --git a/docs/sanic/config.md b/docs/sanic/config.md
index 41405597..b2552704 100644
--- a/docs/sanic/config.md
+++ b/docs/sanic/config.md
@@ -85,16 +85,19 @@ DB_USER = 'appuser'
 
 Out of the box there are just a few predefined values which can be overwritten when creating the application.
 
-    | Variable                  | Default   | Description                                               |
-    | ------------------------- | --------- | --------------------------------------------------------- |
-    | REQUEST_MAX_SIZE          | 100000000 | How big a request may be (bytes)                          |
-    | REQUEST_BUFFER_QUEUE_SIZE | 100       | Request streaming buffer queue size                    |
-    | REQUEST_TIMEOUT           | 60        | How long a request can take to arrive (sec)               |
-    | RESPONSE_TIMEOUT          | 60        | How long a response can take to process (sec)             |
-    | KEEP_ALIVE                | True      | Disables keep-alive when False                            |
-    | KEEP_ALIVE_TIMEOUT        | 5         | How long to hold a TCP connection open (sec)              |
-    | GRACEFUL_SHUTDOWN_TIMEOUT | 15.0      | How long to wait to force close non-idle connection (sec) |
-    | ACCESS_LOG                | True      | Disable or enable access log                              |
+    | Variable                  | Default           | Description                                                                 |
+    | ------------------------- | ----------------- | --------------------------------------------------------------------------- |
+    | REQUEST_MAX_SIZE          | 100000000         | How big a request may be (bytes)                                            |
+    | REQUEST_BUFFER_QUEUE_SIZE | 100               | Request streaming buffer queue size                                         |
+    | REQUEST_TIMEOUT           | 60                | How long a request can take to arrive (sec)                                 |
+    | RESPONSE_TIMEOUT          | 60                | How long a response can take to process (sec)                               |
+    | KEEP_ALIVE                | True              | Disables keep-alive when False                                              |
+    | KEEP_ALIVE_TIMEOUT        | 5                 | How long to hold a TCP connection open (sec)                                |
+    | GRACEFUL_SHUTDOWN_TIMEOUT | 15.0              | How long to wait to force close non-idle connection (sec)                   |
+    | ACCESS_LOG                | True              | Disable or enable access log                                                |
+    | PROXIES_COUNT             | -1                | The number of proxy servers in front of the app (e.g. nginx; see below)     |
+    | FORWARDED_FOR_HEADER      | "X-Forwarded-For" | The name of "X-Forwarded-For" HTTP header that contains client and proxy ip |
+    | REAL_IP_HEADER            | "X-Real-IP"       | The name of "X-Real-IP" HTTP header that contains real client ip            |
 
 ### The different Timeout variables:
 
@@ -143,3 +146,17 @@ Firefox client hard keepalive limit = 115 seconds
 Opera 11 client hard keepalive limit = 120 seconds
 Chrome 13+ client keepalive limit > 300+ seconds
 ```
+
+### About proxy servers and client ip
+
+When you use a reverse proxy server (e.g. nginx), the value of `request.ip` will contain ip of a proxy, typically `127.0.0.1`. To determine the real client ip, `X-Forwarded-For` and `X-Real-IP` HTTP headers are used. But client can fake these headers if they have not been overridden by a proxy. Sanic has a set of options to determine the level of confidence in these headers.
+
+* If you have a single proxy, set `PROXIES_COUNT` to `1`. Then Sanic will use `X-Real-IP` if available or the last ip from `X-Forwarded-For`.
+
+* If you have multiple proxies, set `PROXIES_COUNT` equal to their number to allow Sanic to select the correct ip from `X-Forwarded-For`.
+
+* If you don't use a proxy, set `PROXIES_COUNT` to `0` to ignore these headers and prevent ip falsification.
+
+* If you don't use `X-Real-IP` (e.g. your proxy sends only `X-Forwarded-For`), set `REAL_IP_HEADER` to an empty string.
+
+The real ip will be available in `request.remote_addr`. If HTTP headers are unavailable or untrusted, `request.remote_addr` will be an empty string; in this case use `request.ip` instead.
diff --git a/docs/sanic/deploying.md b/docs/sanic/deploying.md
index 23dc28b6..048def78 100644
--- a/docs/sanic/deploying.md
+++ b/docs/sanic/deploying.md
@@ -64,6 +64,26 @@ of the memory leak.
 
 See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information.
 
+## Running behind a reverse proxy
+
+Sanic can be used with a reverse proxy (e.g. nginx). There's a simple example of nginx configuration:
+
+```
+server {
+  listen 80;
+  server_name example.org;
+
+  location / {
+    proxy_pass http://127.0.0.1:8000;
+    proxy_set_header Host $host;
+    proxy_set_header X-Real-IP $remote_addr;
+    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+  }
+}
+```
+
+If you want to get real client ip, you should configure `X-Real-IP` and `X-Forwarded-For` HTTP headers and set `app.config.PROXIES_COUNT` to `1`; see the configuration page for more information.
+
 ## Disable debug logging
 
 To improve the performance add `debug=False` and `access_log=False` in the `run` arguments.
@@ -92,7 +112,7 @@ to run the app in general.
 Here is an incomplete example (please see `run_async.py` in examples for something more practical):
 
 ```python
-server = app.create_server(host="0.0.0.0", port=8000)
+server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
 loop = asyncio.get_event_loop()
 task = asyncio.ensure_future(server)
 loop.run_forever()
diff --git a/docs/sanic/examples.rst b/docs/sanic/examples.rst
index e2a7af14..a0760f38 100644
--- a/docs/sanic/examples.rst
+++ b/docs/sanic/examples.rst
@@ -160,7 +160,7 @@ execution support provided by the ``pytest-xdist`` plugin.
 Amending Request Object
 ~~~~~~~~~~~~~~~~~~~~~~~
 
-The ``request`` object in ``Sanic`` is a kind of ``dict`` object, this means that ``reqeust`` object can be manipulated as a regular ``dict`` object.
+The ``request`` object in ``Sanic`` is a kind of ``dict`` object, this means that ``request`` object can be manipulated as a regular ``dict`` object.
 
 .. literalinclude:: ../../examples/amending_request_object.py
 
diff --git a/examples/log_request_id.py b/examples/log_request_id.py
index a6dba418..687d79da 100644
--- a/examples/log_request_id.py
+++ b/examples/log_request_id.py
@@ -76,7 +76,7 @@ async def test(request):
 
 if __name__ == '__main__':
     asyncio.set_event_loop(uvloop.new_event_loop())
-    server = app.create_server(host="0.0.0.0", port=8000)
+    server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
     loop = asyncio.get_event_loop()
     loop.set_task_factory(context.task_factory)
     task = asyncio.ensure_future(server)
diff --git a/examples/run_async.py b/examples/run_async.py
index 3d8ab55a..c35da8b1 100644
--- a/examples/run_async.py
+++ b/examples/run_async.py
@@ -12,7 +12,7 @@ async def test(request):
     return response.json({"answer": "42"})
 
 asyncio.set_event_loop(uvloop.new_event_loop())
-server = app.create_server(host="0.0.0.0", port=8000)
+server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
 loop = asyncio.get_event_loop()
 task = asyncio.ensure_future(server)
 signal(SIGINT, lambda s, f: loop.stop())
diff --git a/sanic/app.py b/sanic/app.py
index a32d924c..be0d45bc 100644
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -180,27 +180,28 @@ class Sanic:
             strict_slashes = self.strict_slashes
 
         def response(handler):
-            args = [key for key in signature(handler).parameters.keys()]
-            if args:
-                if stream:
-                    handler.is_stream = stream
-
-                self.router.add(
-                    uri=uri,
-                    methods=methods,
-                    handler=handler,
-                    host=host,
-                    strict_slashes=strict_slashes,
-                    version=version,
-                    name=name,
-                )
-                return handler
-            else:
+            args = list(signature(handler).parameters.keys())
+
+            if not args:
                 raise ValueError(
                     "Required parameter `request` missing "
                     "in the {0}() route?".format(handler.__name__)
                 )
 
+            if stream:
+                handler.is_stream = stream
+
+            self.router.add(
+                uri=uri,
+                methods=methods,
+                handler=handler,
+                host=host,
+                strict_slashes=strict_slashes,
+                version=version,
+                name=name,
+            )
+            return handler
+
         return response
 
     # Shorthand method decorators
@@ -332,7 +333,7 @@ class Sanic:
         name=None,
     ):
         """
-        Add an API URL under the **DELETE** *HTTP* method
+        Add an API URL under the **PATCH** *HTTP* method
 
         :param uri: URL to be tagged to **PATCH** method of *HTTP*
         :param host: Host IP or FQDN for the service to use
@@ -880,8 +881,6 @@ class Sanic:
             # -------------------------------------------- #
             # Request Middleware
             # -------------------------------------------- #
-
-            request.app = self
             response = await self._run_request_middleware(request)
             # No middleware results
             if not response:
@@ -1287,6 +1286,7 @@ class Sanic:
             "port": port,
             "sock": sock,
             "ssl": ssl,
+            "app": self,
             "signal": Signal(),
             "debug": debug,
             "request_handler": self.handle_request,
diff --git a/sanic/config.py b/sanic/config.py
index a7183d77..97581677 100644
--- a/sanic/config.py
+++ b/sanic/config.py
@@ -1,8 +1,6 @@
 import os
 import types
 
-from distutils.util import strtobool
-
 from sanic.exceptions import PyFileError
 
 
@@ -27,6 +25,9 @@ DEFAULT_CONFIG = {
     "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",
 }
 
 
@@ -127,6 +128,23 @@ class Config(dict):
                         self[config_key] = float(v)
                     except ValueError:
                         try:
-                            self[config_key] = bool(strtobool(v))
+                            self[config_key] = strtobool(v)
                         except ValueError:
                             self[config_key] = v
+
+
+def strtobool(val):
+    """
+    This function was borrowed from distutils.utils. While distutils
+    is part of stdlib, it feels odd to use distutils in main application code.
+
+    The function was modified to walk its talk and actually return bool
+    and not int.
+    """
+    val = val.lower()
+    if val in ("y", "yes", "t", "true", "on", "1"):
+        return True
+    elif val in ("n", "no", "f", "false", "off", "0"):
+        return False
+    else:
+        raise ValueError("invalid truth value %r" % (val,))
diff --git a/sanic/request.py b/sanic/request.py
index 76975801..dfb3d1ff 100644
--- a/sanic/request.py
+++ b/sanic/request.py
@@ -95,11 +95,11 @@ class Request(dict):
         "version",
     )
 
-    def __init__(self, url_bytes, headers, version, method, transport):
+    def __init__(self, url_bytes, headers, version, method, transport, app):
         self.raw_url = url_bytes
         # TODO: Content-Encoding detection
         self._parsed_url = parse_url(url_bytes)
-        self.app = None
+        self.app = app
 
         self.headers = headers
         self.version = version
@@ -355,19 +355,38 @@ class Request(dict):
 
     @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
diff --git a/sanic/router.py b/sanic/router.py
index dcf3aeb5..4c1ea0a0 100644
--- a/sanic/router.py
+++ b/sanic/router.py
@@ -18,7 +18,7 @@ Parameter = namedtuple("Parameter", ["name", "cast"])
 REGEX_TYPES = {
     "string": (str, r"[^/]+"),
     "int": (int, r"-?\d+"),
-    "number": (float, r"-?[0-9\\.]+"),
+    "number": (float, r"-?(?:\d+(?:\.\d*)?|\.\d+)"),
     "alpha": (str, r"[A-Za-z]+"),
     "path": (str, r"[^/].*?"),
     "uuid": (
diff --git a/sanic/server.py b/sanic/server.py
index 2314938c..bd0fdf37 100644
--- a/sanic/server.py
+++ b/sanic/server.py
@@ -44,6 +44,8 @@ class HttpProtocol(asyncio.Protocol):
     """
 
     __slots__ = (
+        # app
+        "app",
         # event loop, connection
         "loop",
         "transport",
@@ -88,6 +90,7 @@ class HttpProtocol(asyncio.Protocol):
         self,
         *,
         loop,
+        app,
         request_handler,
         error_handler,
         signal=Signal(),
@@ -107,6 +110,7 @@ class HttpProtocol(asyncio.Protocol):
         **kwargs
     ):
         self.loop = loop
+        self.app = app
         self.transport = None
         self.request = None
         self.parser = None
@@ -303,6 +307,7 @@ class HttpProtocol(asyncio.Protocol):
             version=self.parser.get_http_version(),
             method=self.parser.get_method().decode(),
             transport=self.transport,
+            app=self.app,
         )
         # Remove any existing KeepAlive handler here,
         # It will be recreated if required on the new request.
@@ -607,6 +612,7 @@ def trigger_events(events, loop):
 def serve(
     host,
     port,
+    app,
     request_handler,
     error_handler,
     before_start=None,
@@ -704,6 +710,7 @@ def serve(
         loop=loop,
         connections=connections,
         signal=signal,
+        app=app,
         request_handler=request_handler,
         error_handler=error_handler,
         request_timeout=request_timeout,


Test patch:
diff --git a/tests/test_requests.py b/tests/test_requests.py
index 2f587361..9e634fd8 100644
--- a/tests/test_requests.py
+++ b/tests/test_requests.py
@@ -29,7 +29,7 @@ def test_sync(app):
     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 @@ def test_content_type(app):
     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 @@ def test_remote_addr(app):
     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>/")
diff --git a/tests/test_routes.py b/tests/test_routes.py
index f3152935..3ccef135 100644
--- a/tests/test_routes.py
+++ b/tests/test_routes.py
@@ -365,9 +365,18 @@ def test_dynamic_route_number(app):
     request, response = app.test_client.get("/weight/1234.56")
     assert response.status == 200
 
+    request, response = app.test_client.get("/weight/.12")
+    assert response.status == 200
+
+    request, response = app.test_client.get("/weight/12.")
+    assert response.status == 200
+
     request, response = app.test_client.get("/weight/1234-56")
     assert response.status == 404
 
+    request, response = app.test_client.get("/weight/12.34.56")
+    assert response.status == 404
+
 
 def test_dynamic_route_regex(app):
     @app.route("/folder/<folder_id:[A-Za-z0-9]{0,4}>")
@@ -672,9 +681,18 @@ def test_dynamic_add_route_number(app):
     request, response = app.test_client.get("/weight/1234.56")
     assert response.status == 200
 
+    request, response = app.test_client.get("/weight/.12")
+    assert response.status == 200
+
+    request, response = app.test_client.get("/weight/12.")
+    assert response.status == 200
+
     request, response = app.test_client.get("/weight/1234-56")
     assert response.status == 404
 
+    request, response = app.test_client.get("/weight/12.34.56")
+    assert response.status == 404
+
 
 def test_dynamic_add_route_regex(app):
     async def handler(request, folder_id):
diff --git a/tests/test_url_building.py b/tests/test_url_building.py
index 5e1c300a..a246aabc 100644
--- a/tests/test_url_building.py
+++ b/tests/test_url_building.py
@@ -223,7 +223,7 @@ def test_fails_with_number_message(app):
 
     expected_error = (
         'Value "foo" for parameter `some_number` '
-        "does not match pattern for type `float`: -?[0-9\\\\.]+"
+        r"does not match pattern for type `float`: -?(?:\d+(?:\.\d*)?|\.\d+)"
     )
 
     assert str(e.value) == expected_error