Generateurv2/backend/env/lib/python3.10/site-packages/channels/sessions.py
2022-06-24 17:14:37 +02:00

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))