diff --git a/docs/sanic/request_data.md b/docs/sanic/request_data.md index f411ed8d..a86a0f21 100644 --- a/docs/sanic/request_data.md +++ b/docs/sanic/request_data.md @@ -85,6 +85,12 @@ The following variables are accessible as properties on `Request` objects: return json({'status': 'production'}) ``` +- `url`: The full URL of the request, ie: `http://localhost:8000/posts/1/?foo=bar` +- `scheme`: The URL scheme associated with the request: `http` or `https` +- `host`: The host associated with the request: `localhost:8080` +- `path`: The path of the request: `/posts/1/` +- `query_string`: The query string of the request: `foo=bar` or a blank string `''` + ## Accessing values using `get` and `getlist` diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 4f01d64e..e30c178c 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -70,7 +70,7 @@ TRACEBACK_WRAPPER_HTML = ''' {frame_html}

{exc_name}: {exc_value} - while handling uri {uri} + while handling path {path}

diff --git a/sanic/handlers.py b/sanic/handlers.py index d5a5d0dd..0b1ee89c 100644 --- a/sanic/handlers.py +++ b/sanic/handlers.py @@ -34,7 +34,7 @@ class ErrorHandler: exc_name=exc_type.__name__, exc_value=exc_value, frame_html=''.join(frame_html), - uri=request.url) + path=request.path) def add(self, exception, handler): self.handlers[exception] = handler diff --git a/sanic/request.py b/sanic/request.py index ac1b78d6..68743c79 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -2,7 +2,7 @@ from cgi import parse_header from collections import namedtuple from http.cookies import SimpleCookie from httptools import parse_url -from urllib.parse import parse_qs +from urllib.parse import parse_qs, urlunparse try: from ujson import loads as json_loads @@ -36,24 +36,20 @@ class RequestParameters(dict): class Request(dict): """Properties of an HTTP request such as URL, headers, etc.""" __slots__ = ( - 'app', 'url', 'headers', 'version', 'method', '_cookies', 'transport', - 'query_string', 'body', - 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', - '_ip', + 'app', 'headers', 'version', 'method', '_cookies', 'transport', + 'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', + '_ip', '_parsed_url', ) def __init__(self, url_bytes, headers, version, method, transport): # TODO: Content-Encoding detection - url_parsed = parse_url(url_bytes) + self._parsed_url = parse_url(url_bytes) self.app = None - self.url = url_parsed.path.decode('utf-8') + self.headers = headers self.version = version self.method = method self.transport = transport - self.query_string = None - if url_parsed.query: - self.query_string = url_parsed.query.decode('utf-8') # Init but do not inhale self.body = [] @@ -144,6 +140,40 @@ class Request(dict): self._ip = self.transport.get_extra_info('peername') return self._ip + @property + def scheme(self): + if self.transport.get_extra_info('sslcontext'): + return 'https' + + return 'http' + + @property + def host(self): + # it appears that httptools doesn't return the host + # so pull it from the headers + return self.headers.get('Host', '') + + @property + def path(self): + return self._parsed_url.path.decode('utf-8') + + @property + def query_string(self): + if self._parsed_url.query: + return self._parsed_url.query.decode('utf-8') + else: + return '' + + @property + def url(self): + return urlunparse(( + self.scheme, + self.host, + self.path, + None, + self.query_string, + None)) + File = namedtuple('File', ['type', 'body', 'name']) diff --git a/sanic/router.py b/sanic/router.py index f17ff23d..b7c7fdb3 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -281,14 +281,14 @@ class Router: """ # No virtual hosts specified; default behavior if not self.hosts: - return self._get(request.url, request.method, '') + return self._get(request.path, request.method, '') # virtual hosts specified; try to match route to the host header try: - return self._get(request.url, request.method, + return self._get(request.path, request.method, request.headers.get("Host", '')) # try default hosts except NotFound: - return self._get(request.url, request.method, '') + return self._get(request.path, request.method, '') @lru_cache(maxsize=ROUTER_CACHE_SIZE) def _get(self, url, method, host): diff --git a/sanic/testing.py b/sanic/testing.py index 77f4ff85..4fde428c 100644 --- a/sanic/testing.py +++ b/sanic/testing.py @@ -17,7 +17,9 @@ class SanicTestClient: host=HOST, port=PORT, uri=uri) log.info(url) - async with aiohttp.ClientSession(cookies=cookies) as session: + conn = aiohttp.TCPConnector(verify_ssl=False) + async with aiohttp.ClientSession( + cookies=cookies, connector=conn) as session: async with getattr( session, method.lower())(url, *args, **kwargs) as response: response.text = await response.text() diff --git a/tests/certs/selfsigned.cert b/tests/certs/selfsigned.cert new file mode 100644 index 00000000..0dc7b914 --- /dev/null +++ b/tests/certs/selfsigned.cert @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIJAO6wb0FSc/rNMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTcwMzAzMTUyODAzWhcNMTkxMTI4MTUyODAzWjBF +MQswCQYDVQQGEwJVUzETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAsy7Zb3p4yCEnUtPLwqeJrwj9u/ZmcFCrMAktFBx9hG6rY2r7mdB6Bflh +V5cUJXxnsNiDpYcxGhA8kry7pEork1vZ05DyZC9ulVlvxBouVShBcLLwdpaoTGqE +vYtejv6x7ogwMXOjkWWb1WpOv4CVhpeXJ7O/d1uAiYgcUpTpPp4ONG49IAouBHq3 +h+o4nVvNfB0J8gaCtTsTZqi1Wt8WYs3XjxGJaKh//ealfRe1kuv40CWQ8gjaC8/1 +w9pHdom3Wi/RwfDM3+dVGV6M5lAbPXMB4RK17Hk9P3hlJxJOpKBdgcBJPXtNrTwf +qEWWxk2mB/YVyB84AxjkkNoYyi2ggQIDAQABo4GnMIGkMB0GA1UdDgQWBBRa46Ix +9s9tmMqu+Zz1mocHghm4NTB1BgNVHSMEbjBsgBRa46Ix9s9tmMqu+Zz1mocHghm4 +NaFJpEcwRTELMAkGA1UEBhMCVVMxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV +BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAO6wb0FSc/rNMAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQELBQADggEBACdrnM8zb7abxAJsU5WLn1IR0f2+EFA7 +ezBEJBM4bn0IZrXuP5ThZ2wieJlshG0C16XN9+zifavHci+AtQwWsB0f/ppHdvWQ +7wt7JN88w+j0DNIYEadRCjWxR3gRAXPgKu3sdyScKFq8MvB49A2EdXRmQSTIM6Fj +teRbE+poxewFT0mhurf3xrtGiSALmv7uAzhRDqpYUzcUlbOGgkyFLYAOOdvZvei+ +mfXDi4HKYxgyv53JxBARMdajnCHXM7zQ6Tjc8j1HRtmDQ3XapUB559KfxfODGQq5 +zmeoZWU4duxcNXJM0Eiz1CJ39JoWwi8sqaGi/oskuyAh7YKyVTn8xa8= +-----END CERTIFICATE----- diff --git a/tests/certs/selfsigned.key b/tests/certs/selfsigned.key new file mode 100644 index 00000000..504ef7da --- /dev/null +++ b/tests/certs/selfsigned.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAsy7Zb3p4yCEnUtPLwqeJrwj9u/ZmcFCrMAktFBx9hG6rY2r7 +mdB6BflhV5cUJXxnsNiDpYcxGhA8kry7pEork1vZ05DyZC9ulVlvxBouVShBcLLw +dpaoTGqEvYtejv6x7ogwMXOjkWWb1WpOv4CVhpeXJ7O/d1uAiYgcUpTpPp4ONG49 +IAouBHq3h+o4nVvNfB0J8gaCtTsTZqi1Wt8WYs3XjxGJaKh//ealfRe1kuv40CWQ +8gjaC8/1w9pHdom3Wi/RwfDM3+dVGV6M5lAbPXMB4RK17Hk9P3hlJxJOpKBdgcBJ +PXtNrTwfqEWWxk2mB/YVyB84AxjkkNoYyi2ggQIDAQABAoIBAFgVasxTf3aaXbNo +7JzXMWb7W4iAG2GRNmZZzHA7hTSKFvS7jc3SX3n6WvDtEvlOi8ay2RyRNgEjBDP6 +VZ/w2jUJjS5k7dN0Qb9nhPr5B9fS/0CAppcVfsx5/KEVFzniWOPyzQYyW7FJKu8h +4G5hrp/Ie4UH5tKtB6YUZB/wliyyQUkAZdBcoy1hfkOZLAXb1oofArKsiQUHIRA5 +th1yyS4cZP8Upngd1EE+d95dFHM2F6iI2lj6DHuu+JxUZ+wKXoNimdG7JniRtIf4 +56GoDov83Ey+XbIS6FSQc9nY0ijBDcubl/yP3roCQpE+MZ9BNEo5uj7YmCtAMYLW +TXTNBGUCgYEA4wdkH1NLdub2NcpqwmSA0AtbRvDkt0XTDWWwmuMr/+xPVa4sUKHs +80THQEX/WAZroP6IPbMP6BJhzb53vECukgC65qPxu6M9D1lBGtglxgen4AMu1bKK +gnM8onwARGIo/2ay6qRRZZCxg0TvBky3hbTcIM2zVrnKU6VVyGKHSV8CgYEAygxs +WQYrACv3XN6ZEzyxy08JgjbcnkPWK/m3VPcyHgdEkDu8+nDdUVdbF/js2JWMMx5g +vrPhZ7jVLOXGcLr5mVU4dG5tW5lU0bMy+YYxpEQDiBKlpXgfOsQnakHj7cCZ6bay +mKjJck2oEAQS9bqOJN/Ts5vhOmc8rmhkO7hnAh8CgYEArhVDy9Vl/1WYo6SD+m1w +bJbYtewPpQzwicxZAFuDqKk+KDf3GRkhBWTO2FUUOB4sN3YVaCI+5zf5MPeE/qAm +fCP9LM+3k6bXMkbBamEljdTfACHQruJJ3T+Z1gn5dnZCc5z/QncfRx8NTtfz5MO8 +0dTeGnVAuBacs0kLHy2WCUcCgYALNBkl7pOf1NBIlAdE686oCV/rmoMtO3G6yoQB +8BsVUy3YGZfnAy8ifYeNkr3/XHuDsiGHMY5EJBmd/be9NID2oaUZv63MsHnljtw6 +vdgu1Z6kgvQwcrK4nXvaBoFPA6kFLp5EnMde0TOKf89VVNzg6pBgmzon9OWGfj9g +mF8N3QKBgQCeoLwxUxpzEA0CPHm7DWF0LefVGllgZ23Eqncdy0QRku5zwwibszbL +sWaR3uDCc3oYcbSGCDVx3cSkvMAJNalc5ZHPfoV9W0+v392/rrExo5iwD8CSoCb2 +gFWkeR7PBrD3NzFzFAWyiudzhBKHfRsB0MpCXbJV/WLqTlGIbEypjg== +-----END RSA PRIVATE KEY----- diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py index fac8ea85..aca3345d 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -75,7 +75,7 @@ def test_html_traceback_output_in_debug_mode(): summary_text = " ".join(soup.select('.summary')[0].text.split()) assert ( "NameError: name 'bar' " - "is not defined while handling uri /4") == summary_text + "is not defined while handling path /4") == summary_text def test_inherited_exception_handler(): diff --git a/tests/test_requests.py b/tests/test_requests.py index 38954038..7b453fc1 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -1,10 +1,15 @@ from json import loads as json_loads, dumps as json_dumps +from urllib.parse import urlparse +import os +import ssl import pytest from sanic import Sanic from sanic.exceptions import ServerError -from sanic.response import json, text, redirect +from sanic.response import json, text + +from sanic.testing import HOST, PORT # ------------------------------------------------------------ # @@ -200,3 +205,61 @@ def test_post_form_multipart_form_data(): request, response = app.test_client.post(data=payload, headers=headers) assert request.form.get('test') == 'OK' + + +@pytest.mark.parametrize( + 'path,query,expected_url', [ + ('/foo', '', 'http://{}:{}/foo'), + ('/bar/baz', '', 'http://{}:{}/bar/baz'), + ('/moo/boo', 'arg1=val1', 'http://{}:{}/moo/boo?arg1=val1') + ]) +def test_url_attributes_no_ssl(path, query, expected_url): + app = Sanic('test_url_attrs_no_ssl') + + async def handler(request): + return text('OK') + + app.add_route(handler, path) + + request, response = app.test_client.get(path + '?{}'.format(query)) + assert request.url == expected_url.format(HOST, PORT) + + parsed = urlparse(request.url) + + assert parsed.scheme == request.scheme + assert parsed.path == request.path + assert parsed.query == request.query_string + assert parsed.netloc == request.host + + +@pytest.mark.parametrize( + 'path,query,expected_url', [ + ('/foo', '', 'https://{}:{}/foo'), + ('/bar/baz', '', 'https://{}:{}/bar/baz'), + ('/moo/boo', 'arg1=val1', 'https://{}:{}/moo/boo?arg1=val1') + ]) +def test_url_attributes_with_ssl(path, query, expected_url): + app = Sanic('test_url_attrs_with_ssl') + + current_dir = os.path.dirname(os.path.realpath(__file__)) + context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) + context.load_cert_chain( + os.path.join(current_dir, 'certs/selfsigned.cert'), + keyfile=os.path.join(current_dir, 'certs/selfsigned.key')) + + async def handler(request): + return text('OK') + + app.add_route(handler, path) + + request, response = app.test_client.get( + 'https://{}:{}'.format(HOST, PORT) + path + '?{}'.format(query), + server_kwargs={'ssl': context}) + assert request.url == expected_url.format(HOST, PORT) + + parsed = urlparse(request.url) + + assert parsed.scheme == request.scheme + assert parsed.path == request.path + assert parsed.query == request.query_string + assert parsed.netloc == request.host