diff --git a/README.md b/README.md index 3b4ba359..cc62e6bf 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ app.run(host="0.0.0.0", port=8000) * [Middleware](docs/middleware.md) * [Exceptions](docs/exceptions.md) * [Blueprints](docs/blueprints.md) + * [Cookies](docs/cookies.md) * [Deploying](docs/deploying.md) * [Contributing](docs/contributing.md) * [License](LICENSE) diff --git a/docs/cookies.md b/docs/cookies.md new file mode 100644 index 00000000..ead5f157 --- /dev/null +++ b/docs/cookies.md @@ -0,0 +1,50 @@ +# Cookies + +## Request + +Request cookies can be accessed via the request.cookie dictionary + +### Example + +```python +from sanic import Sanic +from sanic.response import text + +@app.route("/cookie") +async def test(request): + test_cookie = request.cookies.get('test') + return text("Test cookie set to: {}".format(test_cookie)) +``` + +## Response + +Response cookies can be set like dictionary values and +have the following parameters available: + +* expires - datetime - Time for cookie to expire on the client's browser +* path - string - The Path attribute specifies the subset of URLs to + which this cookie applies +* comment - string - Cookie comment (metadata) +* domain - string - Specifies the domain for which the + cookie is valid. An explicitly specified domain must always + start with a dot. +* max-age - number - Number of seconds the cookie should live for +* secure - boolean - Specifies whether the cookie will only be sent via + HTTPS +* httponly - boolean - Specifies whether the cookie cannot be read + by javascript + +### Example + +```python +from sanic import Sanic +from sanic.response import text + +@app.route("/cookie") +async def test(request): + response = text("There's a cookie up in this response") + response.cookies['test'] = 'It worked!' + response.cookies['test']['domain'] = '.gotta-go-fast.com' + response.cookies['test']['httponly'] = True + return response +``` \ No newline at end of file diff --git a/sanic/request.py b/sanic/request.py index 31b73ed8..2687d86b 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -1,5 +1,6 @@ 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 ujson import loads as json_loads @@ -30,7 +31,7 @@ class Request: Properties of an HTTP request such as URL, headers, etc. """ __slots__ = ( - 'url', 'headers', 'version', 'method', + 'url', 'headers', 'version', 'method', '_cookies', 'query_string', 'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', ) @@ -52,6 +53,7 @@ class Request: self.parsed_form = None self.parsed_files = None self.parsed_args = None + self._cookies = None @property def json(self): @@ -105,6 +107,18 @@ class Request: return self.parsed_args + @property + def cookies(self): + if self._cookies is None: + if 'Cookie' in self.headers: + cookies = SimpleCookie() + cookies.load(self.headers['Cookie']) + self._cookies = {name: cookie.value + for name, cookie in cookies.items()} + else: + self._cookies = {} + return self._cookies + File = namedtuple('File', ['type', 'body', 'name']) diff --git a/sanic/response.py b/sanic/response.py index be471078..20e69eff 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,3 +1,5 @@ +from datetime import datetime +from http.cookies import SimpleCookie import ujson COMMON_STATUS_CODES = { @@ -68,7 +70,7 @@ ALL_STATUS_CODES = { class HTTPResponse: - __slots__ = ('body', 'status', 'content_type', 'headers') + __slots__ = ('body', 'status', 'content_type', 'headers', '_cookies') def __init__(self, body=None, status=200, headers=None, content_type='text/plain', body_bytes=b''): @@ -81,6 +83,7 @@ class HTTPResponse: self.status = status self.headers = headers or {} + self._cookies = None def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): # This is all returned in a kind-of funky way @@ -95,6 +98,12 @@ class HTTPResponse: b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) for name, value in self.headers.items() ) + if self._cookies: + for cookie in self._cookies.values(): + if type(cookie['expires']) is datetime: + cookie['expires'] = \ + cookie['expires'].strftime("%a, %d-%b-%Y %T GMT") + headers += (str(self._cookies) + "\r\n").encode('utf-8') # Try to pull from the common codes first # Speeds up response rate 6% over pulling from all @@ -119,6 +128,12 @@ class HTTPResponse: self.body ) + @property + def cookies(self): + if self._cookies is None: + self._cookies = SimpleCookie() + return self._cookies + def json(body, status=200, headers=None): return HTTPResponse(ujson.dumps(body), headers=headers, status=status, diff --git a/sanic/utils.py b/sanic/utils.py index c39f03ab..4c2680e9 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -5,10 +5,10 @@ HOST = '127.0.0.1' PORT = 42101 -async def local_request(method, uri, *args, **kwargs): +async def local_request(method, uri, cookies=None, *args, **kwargs): url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri) log.info(url) - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(cookies=cookies) as session: async with getattr(session, method)(url, *args, **kwargs) as response: response.text = await response.text() return response diff --git a/tests/test_cookies.py b/tests/test_cookies.py new file mode 100644 index 00000000..5b27c2e7 --- /dev/null +++ b/tests/test_cookies.py @@ -0,0 +1,44 @@ +from datetime import datetime, timedelta +from http.cookies import SimpleCookie +from sanic import Sanic +from sanic.response import json, text +from sanic.utils import sanic_endpoint_test + + +# ------------------------------------------------------------ # +# GET +# ------------------------------------------------------------ # + +def test_cookies(): + app = Sanic('test_text') + + @app.route('/') + def handler(request): + response = text('Cookies are: {}'.format(request.cookies['test'])) + response.cookies['right_back'] = 'at you' + return response + + request, response = sanic_endpoint_test(app, cookies={"test": "working!"}) + response_cookies = SimpleCookie() + response_cookies.load(response.headers.get('Set-Cookie', {})) + + assert response.text == 'Cookies are: working!' + assert response_cookies['right_back'].value == 'at you' + +def test_cookie_options(): + app = Sanic('test_text') + + @app.route('/') + def handler(request): + response = text("OK") + response.cookies['test'] = 'at you' + response.cookies['test']['httponly'] = True + response.cookies['test']['expires'] = datetime.now() + timedelta(seconds=10) + return response + + request, response = sanic_endpoint_test(app) + response_cookies = SimpleCookie() + response_cookies.load(response.headers.get('Set-Cookie', {})) + + assert response_cookies['test'].value == 'at you' + assert response_cookies['test']['httponly'] == True \ No newline at end of file