kopia lustrzana https://github.com/simonw/datasette
response.set_cookie(), closes #795
rodzic
f240970b83
commit
008e2f63c2
|
@ -1,6 +1,5 @@
|
||||||
from datasette import hookimpl
|
from datasette import hookimpl
|
||||||
from itsdangerous import BadSignature
|
from itsdangerous import BadSignature
|
||||||
from http.cookies import SimpleCookie
|
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
|
|
|
@ -3,7 +3,6 @@ import asgi_csrf
|
||||||
import collections
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
from http.cookies import SimpleCookie
|
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
@ -442,19 +441,9 @@ class Datasette:
|
||||||
def _write_messages_to_response(self, request, response):
|
def _write_messages_to_response(self, request, response):
|
||||||
if getattr(request, "_messages", None):
|
if getattr(request, "_messages", None):
|
||||||
# Set those messages
|
# Set those messages
|
||||||
cookie = SimpleCookie()
|
response.set_cookie("ds_messages", self.sign(request._messages, "messages"))
|
||||||
cookie["ds_messages"] = self.sign(request._messages, "messages")
|
|
||||||
cookie["ds_messages"]["path"] = "/"
|
|
||||||
# TODO: Co-exist with existing set-cookie headers
|
|
||||||
assert "set-cookie" not in response.headers
|
|
||||||
response.headers["set-cookie"] = cookie.output(header="").lstrip()
|
|
||||||
elif getattr(request, "_messages_should_clear", False):
|
elif getattr(request, "_messages_should_clear", False):
|
||||||
cookie = SimpleCookie()
|
response.set_cookie("ds_messages", "", expires=0, max_age=0)
|
||||||
cookie["ds_messages"] = ""
|
|
||||||
cookie["ds_messages"]["path"] = "/"
|
|
||||||
# TODO: Co-exist with existing set-cookie headers
|
|
||||||
assert "set-cookie" not in response.headers
|
|
||||||
response.headers["set-cookie"] = cookie.output(header="").lstrip()
|
|
||||||
|
|
||||||
def _show_messages(self, request):
|
def _show_messages(self, request):
|
||||||
if getattr(request, "_messages", None):
|
if getattr(request, "_messages", None):
|
||||||
|
|
|
@ -4,10 +4,15 @@ from mimetypes import guess_type
|
||||||
from urllib.parse import parse_qs, urlunparse, parse_qsl
|
from urllib.parse import parse_qs, urlunparse, parse_qsl
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from html import escape
|
from html import escape
|
||||||
from http.cookies import SimpleCookie
|
from http.cookies import SimpleCookie, Morsel
|
||||||
import re
|
import re
|
||||||
import aiofiles
|
import aiofiles
|
||||||
|
|
||||||
|
# Workaround for adding samesite support to pre 3.8 python
|
||||||
|
Morsel._reserved["samesite"] = "SameSite"
|
||||||
|
# Thanks, Starlette:
|
||||||
|
# https://github.com/encode/starlette/blob/519f575/starlette/responses.py#L17
|
||||||
|
|
||||||
|
|
||||||
class NotFound(Exception):
|
class NotFound(Exception):
|
||||||
pass
|
pass
|
||||||
|
@ -17,6 +22,9 @@ class Forbidden(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
SAMESITE_VALUES = ("strict", "lax", "none")
|
||||||
|
|
||||||
|
|
||||||
class Request:
|
class Request:
|
||||||
def __init__(self, scope, receive):
|
def __init__(self, scope, receive):
|
||||||
self.scope = scope
|
self.scope = scope
|
||||||
|
@ -370,20 +378,24 @@ class Response:
|
||||||
self.body = body
|
self.body = body
|
||||||
self.status = status
|
self.status = status
|
||||||
self.headers = headers or {}
|
self.headers = headers or {}
|
||||||
|
self._set_cookie_headers = []
|
||||||
self.content_type = content_type
|
self.content_type = content_type
|
||||||
|
|
||||||
async def asgi_send(self, send):
|
async def asgi_send(self, send):
|
||||||
headers = {}
|
headers = {}
|
||||||
headers.update(self.headers)
|
headers.update(self.headers)
|
||||||
headers["content-type"] = self.content_type
|
headers["content-type"] = self.content_type
|
||||||
|
raw_headers = [
|
||||||
|
[key.encode("utf-8"), value.encode("utf-8")]
|
||||||
|
for key, value in headers.items()
|
||||||
|
]
|
||||||
|
for set_cookie in self._set_cookie_headers:
|
||||||
|
raw_headers.append([b"set-cookie", set_cookie.encode("utf-8")])
|
||||||
await send(
|
await send(
|
||||||
{
|
{
|
||||||
"type": "http.response.start",
|
"type": "http.response.start",
|
||||||
"status": self.status,
|
"status": self.status,
|
||||||
"headers": [
|
"headers": raw_headers,
|
||||||
[key.encode("utf-8"), value.encode("utf-8")]
|
|
||||||
for key, value in headers.items()
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
body = self.body
|
body = self.body
|
||||||
|
@ -391,6 +403,37 @@ class Response:
|
||||||
body = body.encode("utf-8")
|
body = body.encode("utf-8")
|
||||||
await send({"type": "http.response.body", "body": body})
|
await send({"type": "http.response.body", "body": body})
|
||||||
|
|
||||||
|
def set_cookie(
|
||||||
|
self,
|
||||||
|
key,
|
||||||
|
value="",
|
||||||
|
max_age=None,
|
||||||
|
expires=None,
|
||||||
|
path="/",
|
||||||
|
domain=None,
|
||||||
|
secure=False,
|
||||||
|
httponly=False,
|
||||||
|
samesite="lax",
|
||||||
|
):
|
||||||
|
assert samesite in SAMESITE_VALUES, "samesite should be one of {}".format(
|
||||||
|
SAMESITE_VALUES
|
||||||
|
)
|
||||||
|
cookie = SimpleCookie()
|
||||||
|
cookie[key] = value
|
||||||
|
for prop_name, prop_value in (
|
||||||
|
("max_age", max_age),
|
||||||
|
("expires", expires),
|
||||||
|
("path", path),
|
||||||
|
("domain", domain),
|
||||||
|
("samesite", samesite),
|
||||||
|
):
|
||||||
|
if prop_value is not None:
|
||||||
|
cookie[key][prop_name.replace("_", "-")] = prop_value
|
||||||
|
for prop_name, prop_value in (("secure", secure), ("httponly", httponly)):
|
||||||
|
if prop_value:
|
||||||
|
cookie[key][prop_name] = True
|
||||||
|
self._set_cookie_headers.append(cookie.output(header="").strip())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def html(cls, body, status=200, headers=None):
|
def html(cls, body, status=200, headers=None):
|
||||||
return cls(
|
return cls(
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import json
|
import json
|
||||||
from datasette.utils.asgi import Response
|
from datasette.utils.asgi import Response
|
||||||
from .base import BaseView
|
from .base import BaseView
|
||||||
from http.cookies import SimpleCookie
|
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,17 +61,8 @@ class AuthTokenView(BaseView):
|
||||||
return Response("Root token has already been used", status=403)
|
return Response("Root token has already been used", status=403)
|
||||||
if secrets.compare_digest(token, self.ds._root_token):
|
if secrets.compare_digest(token, self.ds._root_token):
|
||||||
self.ds._root_token = None
|
self.ds._root_token = None
|
||||||
cookie = SimpleCookie()
|
response = Response.redirect("/")
|
||||||
cookie["ds_actor"] = self.ds.sign({"id": "root"}, "actor")
|
response.set_cookie("ds_actor", self.ds.sign({"id": "root"}, "actor"))
|
||||||
cookie["ds_actor"]["path"] = "/"
|
|
||||||
response = Response(
|
|
||||||
body="",
|
|
||||||
status=302,
|
|
||||||
headers={
|
|
||||||
"Location": "/",
|
|
||||||
"set-cookie": cookie.output(header="").lstrip(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
return Response("Invalid token", status=403)
|
return Response("Invalid token", status=403)
|
||||||
|
|
|
@ -131,6 +131,36 @@ Each of these responses will use the correct corresponding content-type - ``text
|
||||||
|
|
||||||
Each of the helper methods take optional ``status=`` and ``headers=`` arguments, documented above.
|
Each of the helper methods take optional ``status=`` and ``headers=`` arguments, documented above.
|
||||||
|
|
||||||
|
.. _internals_response_set_cookie:
|
||||||
|
|
||||||
|
Setting cookies with response.set_cookie()
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
To set cookies on the response, use the ``response.set_cookie(...)`` method. The method signature looks like this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def set_cookie(
|
||||||
|
self,
|
||||||
|
key,
|
||||||
|
value="",
|
||||||
|
max_age=None,
|
||||||
|
expires=None,
|
||||||
|
path="/",
|
||||||
|
domain=None,
|
||||||
|
secure=False,
|
||||||
|
httponly=False,
|
||||||
|
samesite="lax",
|
||||||
|
):
|
||||||
|
|
||||||
|
You can use this with :ref:`datasette.sign() <datasette_sign>` to set signed cookies. Here's how you would set the ``ds_actor`` cookie for use with Datasette :ref:`authentication <authentication>`:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
response = Response.redirect("/")
|
||||||
|
response.set_cookie("ds_actor", datasette.sign({"id": "cleopaws"}, "actor"))
|
||||||
|
return response
|
||||||
|
|
||||||
.. _internals_datasette:
|
.. _internals_datasette:
|
||||||
|
|
||||||
Datasette class
|
Datasette class
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from datasette.utils.asgi import Response
|
from datasette.utils.asgi import Response
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def test_response_html():
|
def test_response_html():
|
||||||
|
@ -26,3 +27,28 @@ def test_response_redirect():
|
||||||
response = Response.redirect("/foo")
|
response = Response.redirect("/foo")
|
||||||
assert 302 == response.status
|
assert 302 == response.status
|
||||||
assert "/foo" == response.headers["Location"]
|
assert "/foo" == response.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_response_set_cookie():
|
||||||
|
events = []
|
||||||
|
|
||||||
|
async def send(event):
|
||||||
|
events.append(event)
|
||||||
|
|
||||||
|
response = Response.redirect("/foo")
|
||||||
|
response.set_cookie("foo", "bar", max_age=10, httponly=True)
|
||||||
|
await response.asgi_send(send)
|
||||||
|
|
||||||
|
assert [
|
||||||
|
{
|
||||||
|
"type": "http.response.start",
|
||||||
|
"status": 302,
|
||||||
|
"headers": [
|
||||||
|
[b"Location", b"/foo"],
|
||||||
|
[b"content-type", b"text/plain"],
|
||||||
|
[b"set-cookie", b"foo=bar; HttpOnly; Max-Age=10; Path=/; SameSite=lax"],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{"type": "http.response.body", "body": b""},
|
||||||
|
] == events
|
||||||
|
|
Ładowanie…
Reference in New Issue