269 lines
9.8 KiB
Python
269 lines
9.8 KiB
Python
import datetime
|
|
import time
|
|
from importlib import import_module
|
|
|
|
from django.conf import settings
|
|
from django.contrib.sessions.backends.base import UpdateError
|
|
from django.core.exceptions import SuspiciousOperation
|
|
from django.http import parse_cookie
|
|
from django.http.cookie import SimpleCookie
|
|
from django.utils import timezone
|
|
from django.utils.encoding import force_str
|
|
from django.utils.functional import LazyObject
|
|
|
|
from channels.db import database_sync_to_async
|
|
|
|
try:
|
|
from django.utils.http import http_date
|
|
except ImportError:
|
|
from django.utils.http import cookie_date as http_date
|
|
|
|
|
|
class CookieMiddleware:
|
|
"""
|
|
Extracts cookies from HTTP or WebSocket-style scopes and adds them as a
|
|
scope["cookies"] entry with the same format as Django's request.COOKIES.
|
|
"""
|
|
|
|
def __init__(self, inner):
|
|
self.inner = inner
|
|
|
|
async def __call__(self, scope, receive, send):
|
|
# Check this actually has headers. They're a required scope key for HTTP and WS.
|
|
if "headers" not in scope:
|
|
raise ValueError(
|
|
"CookieMiddleware was passed a scope that did not have a headers key "
|
|
+ "(make sure it is only passed HTTP or WebSocket connections)"
|
|
)
|
|
# Go through headers to find the cookie one
|
|
for name, value in scope.get("headers", []):
|
|
if name == b"cookie":
|
|
cookies = parse_cookie(value.decode("latin1"))
|
|
break
|
|
else:
|
|
# No cookie header found - add an empty default.
|
|
cookies = {}
|
|
# Return inner application
|
|
return await self.inner(dict(scope, cookies=cookies), receive, send)
|
|
|
|
@classmethod
|
|
def set_cookie(
|
|
cls,
|
|
message,
|
|
key,
|
|
value="",
|
|
max_age=None,
|
|
expires=None,
|
|
path="/",
|
|
domain=None,
|
|
secure=False,
|
|
httponly=False,
|
|
samesite="lax",
|
|
):
|
|
"""
|
|
Sets a cookie in the passed HTTP response message.
|
|
|
|
``expires`` can be:
|
|
- a string in the correct format,
|
|
- a naive ``datetime.datetime`` object in UTC,
|
|
- an aware ``datetime.datetime`` object in any time zone.
|
|
If it is a ``datetime.datetime`` object then ``max_age`` will be calculated.
|
|
"""
|
|
value = force_str(value)
|
|
cookies = SimpleCookie()
|
|
cookies[key] = value
|
|
if expires is not None:
|
|
if isinstance(expires, datetime.datetime):
|
|
if timezone.is_aware(expires):
|
|
expires = timezone.make_naive(expires, timezone.utc)
|
|
delta = expires - expires.utcnow()
|
|
# Add one second so the date matches exactly (a fraction of
|
|
# time gets lost between converting to a timedelta and
|
|
# then the date string).
|
|
delta = delta + datetime.timedelta(seconds=1)
|
|
# Just set max_age - the max_age logic will set expires.
|
|
expires = None
|
|
max_age = max(0, delta.days * 86400 + delta.seconds)
|
|
else:
|
|
cookies[key]["expires"] = expires
|
|
else:
|
|
cookies[key]["expires"] = ""
|
|
if max_age is not None:
|
|
cookies[key]["max-age"] = max_age
|
|
# IE requires expires, so set it if hasn't been already.
|
|
if not expires:
|
|
cookies[key]["expires"] = http_date(time.time() + max_age)
|
|
if path is not None:
|
|
cookies[key]["path"] = path
|
|
if domain is not None:
|
|
cookies[key]["domain"] = domain
|
|
if secure:
|
|
cookies[key]["secure"] = True
|
|
if httponly:
|
|
cookies[key]["httponly"] = True
|
|
if samesite is not None:
|
|
assert samesite.lower() in [
|
|
"strict",
|
|
"lax",
|
|
"none",
|
|
], "samesite must be either 'strict', 'lax' or 'none'"
|
|
cookies[key]["samesite"] = samesite
|
|
# Write out the cookies to the response
|
|
for c in cookies.values():
|
|
message.setdefault("headers", []).append(
|
|
(b"Set-Cookie", bytes(c.output(header=""), encoding="utf-8"))
|
|
)
|
|
|
|
@classmethod
|
|
def delete_cookie(cls, message, key, path="/", domain=None):
|
|
"""
|
|
Deletes a cookie in a response.
|
|
"""
|
|
return cls.set_cookie(
|
|
message,
|
|
key,
|
|
max_age=0,
|
|
path=path,
|
|
domain=domain,
|
|
expires="Thu, 01-Jan-1970 00:00:00 GMT",
|
|
)
|
|
|
|
|
|
class InstanceSessionWrapper:
|
|
"""
|
|
Populates the session in application instance scope, and wraps send to save
|
|
the session.
|
|
"""
|
|
|
|
# Message types that trigger a session save if it's modified
|
|
save_message_types = ["http.response.start"]
|
|
|
|
# Message types that can carry session cookies back
|
|
cookie_response_message_types = ["http.response.start"]
|
|
|
|
def __init__(self, scope, send):
|
|
self.cookie_name = settings.SESSION_COOKIE_NAME
|
|
self.session_store = import_module(settings.SESSION_ENGINE).SessionStore
|
|
|
|
self.scope = dict(scope)
|
|
|
|
if "session" in self.scope:
|
|
# There's already session middleware of some kind above us, pass
|
|
# that through
|
|
self.activated = False
|
|
else:
|
|
# Make sure there are cookies in the scope
|
|
if "cookies" not in self.scope:
|
|
raise ValueError(
|
|
"No cookies in scope - SessionMiddleware needs to run "
|
|
"inside of CookieMiddleware."
|
|
)
|
|
# Parse the headers in the scope into cookies
|
|
self.scope["session"] = LazyObject()
|
|
self.activated = True
|
|
|
|
# Override send
|
|
self.real_send = send
|
|
|
|
async def resolve_session(self):
|
|
session_key = self.scope["cookies"].get(self.cookie_name)
|
|
self.scope["session"]._wrapped = await database_sync_to_async(
|
|
self.session_store
|
|
)(session_key)
|
|
|
|
async def send(self, message):
|
|
"""
|
|
Overridden send that also does session saves/cookies.
|
|
"""
|
|
# Only save session if we're the outermost session middleware
|
|
if self.activated:
|
|
modified = self.scope["session"].modified
|
|
empty = self.scope["session"].is_empty()
|
|
# If this is a message type that we want to save on, and there's
|
|
# changed data, save it. We also save if it's empty as we might
|
|
# not be able to send a cookie-delete along with this message.
|
|
if (
|
|
message["type"] in self.save_message_types
|
|
and message.get("status", 200) != 500
|
|
and (modified or settings.SESSION_SAVE_EVERY_REQUEST)
|
|
):
|
|
await database_sync_to_async(self.save_session)()
|
|
# If this is a message type that can transport cookies back to the
|
|
# client, then do so.
|
|
if message["type"] in self.cookie_response_message_types:
|
|
if empty:
|
|
# Delete cookie if it's set
|
|
if settings.SESSION_COOKIE_NAME in self.scope["cookies"]:
|
|
CookieMiddleware.delete_cookie(
|
|
message,
|
|
settings.SESSION_COOKIE_NAME,
|
|
path=settings.SESSION_COOKIE_PATH,
|
|
domain=settings.SESSION_COOKIE_DOMAIN,
|
|
)
|
|
else:
|
|
# Get the expiry data
|
|
if self.scope["session"].get_expire_at_browser_close():
|
|
max_age = None
|
|
expires = None
|
|
else:
|
|
max_age = self.scope["session"].get_expiry_age()
|
|
expires_time = time.time() + max_age
|
|
expires = http_date(expires_time)
|
|
# Set the cookie
|
|
CookieMiddleware.set_cookie(
|
|
message,
|
|
self.cookie_name,
|
|
self.scope["session"].session_key,
|
|
max_age=max_age,
|
|
expires=expires,
|
|
domain=settings.SESSION_COOKIE_DOMAIN,
|
|
path=settings.SESSION_COOKIE_PATH,
|
|
secure=settings.SESSION_COOKIE_SECURE or None,
|
|
httponly=settings.SESSION_COOKIE_HTTPONLY or None,
|
|
samesite=settings.SESSION_COOKIE_SAMESITE,
|
|
)
|
|
# Pass up the send
|
|
return await self.real_send(message)
|
|
|
|
def save_session(self):
|
|
"""
|
|
Saves the current session.
|
|
"""
|
|
try:
|
|
self.scope["session"].save()
|
|
except UpdateError:
|
|
raise SuspiciousOperation(
|
|
"The request's session was deleted before the "
|
|
"request completed. The user may have logged "
|
|
"out in a concurrent request, for example."
|
|
)
|
|
|
|
|
|
class SessionMiddleware:
|
|
"""
|
|
Class that adds Django sessions (from HTTP cookies) to the
|
|
scope. Works with HTTP or WebSocket protocol types (or anything that
|
|
provides a "headers" entry in the scope).
|
|
|
|
Requires the CookieMiddleware to be higher up in the stack.
|
|
"""
|
|
|
|
def __init__(self, inner):
|
|
self.inner = inner
|
|
|
|
async def __call__(self, scope, receive, send):
|
|
"""
|
|
Instantiate a session wrapper for this scope, resolve the session and
|
|
call the inner application.
|
|
"""
|
|
wrapper = InstanceSessionWrapper(scope, send)
|
|
|
|
await wrapper.resolve_session()
|
|
|
|
return await self.inner(wrapper.scope, receive, wrapper.send)
|
|
|
|
|
|
# Shortcut to include cookie middleware
|
|
def SessionMiddlewareStack(inner):
|
|
return CookieMiddleware(SessionMiddleware(inner))
|