Repository: encode/starlette

Issue:
Get client IP address
Hi, i started using **Starlette** recently and for my application i need to log the client's IP address. It's important to note that i use **Hypercorn** as my ASGI server and it works great.
I started searching on Google how to get the IP address from **Starlette**, and i couldn't  find it anywhere, so I searched the request object. I figured out that on the "_scope" property I could access the client IP address, but it's inside a protected member of the request object. 
I don't know if there is a better way of getting it currently available on **Starlette**, but if there isn't i think it would be nice to have something similar to **Flask**'s _request.remote_addr_.

Discussion:

Patch:
diff --git a/docs/requests.md b/docs/requests.md
index e07d687..295fb85 100644
--- a/docs/requests.md
+++ b/docs/requests.md
@@ -61,6 +61,15 @@ Router path parameters are exposed as a dictionary interface.
 
 For example: `request.path_params['username']`
 
+#### Client Address
+
+The client's remote address is exposed as a named two-tuple `request.client`.
+Either item in the tuple may be `None`.
+
+The hostname or IP address: `request.client.host`
+
+The port number from which the client is connecting: `request.client.port`
+
 #### Cookies
 
 Cookies are exposed as a regular dictionary interface.
diff --git a/starlette/datastructures.py b/starlette/datastructures.py
index 50f0d5b..c6fe1e7 100644
--- a/starlette/datastructures.py
+++ b/starlette/datastructures.py
@@ -1,8 +1,11 @@
 import typing
+from collections import namedtuple
 from urllib.parse import ParseResult, parse_qsl, urlencode, urlparse
 
 from starlette.types import Scope
 
+Address = namedtuple("Address", ["host", "port"])
+
 
 class URL:
     def __init__(
diff --git a/starlette/requests.py b/starlette/requests.py
index 9fae033..2132151 100644
--- a/starlette/requests.py
+++ b/starlette/requests.py
@@ -3,7 +3,7 @@ import json
 import typing
 from collections.abc import Mapping
 
-from starlette.datastructures import URL, Headers, QueryParams
+from starlette.datastructures import URL, Address, Headers, QueryParams
 from starlette.formparsers import FormParser, MultiPartParser
 from starlette.types import Message, Receive, Scope
 
@@ -71,6 +71,11 @@ class HTTPConnection(Mapping):
             self._cookies = cookies
         return self._cookies
 
+    @property
+    def client(self) -> Address:
+        host, port = self._scope.get("client") or (None, None)
+        return Address(host=host, port=port)
+
     @property
     def session(self) -> dict:
         assert (


Test patch:
diff --git a/tests/test_requests.py b/tests/test_requests.py
index 90ad4e8..9e02c9f 100644
--- a/tests/test_requests.py
+++ b/tests/test_requests.py
@@ -63,6 +63,22 @@ def test_request_headers():
     }
 
 
+def test_request_client():
+    def app(scope):
+        async def asgi(receive, send):
+            request = Request(scope, receive)
+            response = JSONResponse(
+                {"host": request.client.host, "port": request.client.port}
+            )
+            await response(receive, send)
+
+        return asgi
+
+    client = TestClient(app)
+    response = client.get("/")
+    assert response.json() == {"host": "testclient", "port": 50000}
+
+
 def test_request_body():
     def app(scope):
         async def asgi(receive, send):