Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

sessions.py 9.8KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import datetime
  2. import time
  3. from importlib import import_module
  4. from django.conf import settings
  5. from django.contrib.sessions.backends.base import UpdateError
  6. from django.core.exceptions import SuspiciousOperation
  7. from django.http import parse_cookie
  8. from django.http.cookie import SimpleCookie
  9. from django.utils import timezone
  10. from django.utils.encoding import force_str
  11. from django.utils.functional import LazyObject
  12. from channels.db import database_sync_to_async
  13. try:
  14. from django.utils.http import http_date
  15. except ImportError:
  16. from django.utils.http import cookie_date as http_date
  17. class CookieMiddleware:
  18. """
  19. Extracts cookies from HTTP or WebSocket-style scopes and adds them as a
  20. scope["cookies"] entry with the same format as Django's request.COOKIES.
  21. """
  22. def __init__(self, inner):
  23. self.inner = inner
  24. async def __call__(self, scope, receive, send):
  25. # Check this actually has headers. They're a required scope key for HTTP and WS.
  26. if "headers" not in scope:
  27. raise ValueError(
  28. "CookieMiddleware was passed a scope that did not have a headers key "
  29. + "(make sure it is only passed HTTP or WebSocket connections)"
  30. )
  31. # Go through headers to find the cookie one
  32. for name, value in scope.get("headers", []):
  33. if name == b"cookie":
  34. cookies = parse_cookie(value.decode("latin1"))
  35. break
  36. else:
  37. # No cookie header found - add an empty default.
  38. cookies = {}
  39. # Return inner application
  40. return await self.inner(dict(scope, cookies=cookies), receive, send)
  41. @classmethod
  42. def set_cookie(
  43. cls,
  44. message,
  45. key,
  46. value="",
  47. max_age=None,
  48. expires=None,
  49. path="/",
  50. domain=None,
  51. secure=False,
  52. httponly=False,
  53. samesite="lax",
  54. ):
  55. """
  56. Sets a cookie in the passed HTTP response message.
  57. ``expires`` can be:
  58. - a string in the correct format,
  59. - a naive ``datetime.datetime`` object in UTC,
  60. - an aware ``datetime.datetime`` object in any time zone.
  61. If it is a ``datetime.datetime`` object then ``max_age`` will be calculated.
  62. """
  63. value = force_str(value)
  64. cookies = SimpleCookie()
  65. cookies[key] = value
  66. if expires is not None:
  67. if isinstance(expires, datetime.datetime):
  68. if timezone.is_aware(expires):
  69. expires = timezone.make_naive(expires, timezone.utc)
  70. delta = expires - expires.utcnow()
  71. # Add one second so the date matches exactly (a fraction of
  72. # time gets lost between converting to a timedelta and
  73. # then the date string).
  74. delta = delta + datetime.timedelta(seconds=1)
  75. # Just set max_age - the max_age logic will set expires.
  76. expires = None
  77. max_age = max(0, delta.days * 86400 + delta.seconds)
  78. else:
  79. cookies[key]["expires"] = expires
  80. else:
  81. cookies[key]["expires"] = ""
  82. if max_age is not None:
  83. cookies[key]["max-age"] = max_age
  84. # IE requires expires, so set it if hasn't been already.
  85. if not expires:
  86. cookies[key]["expires"] = http_date(time.time() + max_age)
  87. if path is not None:
  88. cookies[key]["path"] = path
  89. if domain is not None:
  90. cookies[key]["domain"] = domain
  91. if secure:
  92. cookies[key]["secure"] = True
  93. if httponly:
  94. cookies[key]["httponly"] = True
  95. if samesite is not None:
  96. assert samesite.lower() in [
  97. "strict",
  98. "lax",
  99. "none",
  100. ], "samesite must be either 'strict', 'lax' or 'none'"
  101. cookies[key]["samesite"] = samesite
  102. # Write out the cookies to the response
  103. for c in cookies.values():
  104. message.setdefault("headers", []).append(
  105. (b"Set-Cookie", bytes(c.output(header=""), encoding="utf-8"))
  106. )
  107. @classmethod
  108. def delete_cookie(cls, message, key, path="/", domain=None):
  109. """
  110. Deletes a cookie in a response.
  111. """
  112. return cls.set_cookie(
  113. message,
  114. key,
  115. max_age=0,
  116. path=path,
  117. domain=domain,
  118. expires="Thu, 01-Jan-1970 00:00:00 GMT",
  119. )
  120. class InstanceSessionWrapper:
  121. """
  122. Populates the session in application instance scope, and wraps send to save
  123. the session.
  124. """
  125. # Message types that trigger a session save if it's modified
  126. save_message_types = ["http.response.start"]
  127. # Message types that can carry session cookies back
  128. cookie_response_message_types = ["http.response.start"]
  129. def __init__(self, scope, send):
  130. self.cookie_name = settings.SESSION_COOKIE_NAME
  131. self.session_store = import_module(settings.SESSION_ENGINE).SessionStore
  132. self.scope = dict(scope)
  133. if "session" in self.scope:
  134. # There's already session middleware of some kind above us, pass
  135. # that through
  136. self.activated = False
  137. else:
  138. # Make sure there are cookies in the scope
  139. if "cookies" not in self.scope:
  140. raise ValueError(
  141. "No cookies in scope - SessionMiddleware needs to run "
  142. "inside of CookieMiddleware."
  143. )
  144. # Parse the headers in the scope into cookies
  145. self.scope["session"] = LazyObject()
  146. self.activated = True
  147. # Override send
  148. self.real_send = send
  149. async def resolve_session(self):
  150. session_key = self.scope["cookies"].get(self.cookie_name)
  151. self.scope["session"]._wrapped = await database_sync_to_async(
  152. self.session_store
  153. )(session_key)
  154. async def send(self, message):
  155. """
  156. Overridden send that also does session saves/cookies.
  157. """
  158. # Only save session if we're the outermost session middleware
  159. if self.activated:
  160. modified = self.scope["session"].modified
  161. empty = self.scope["session"].is_empty()
  162. # If this is a message type that we want to save on, and there's
  163. # changed data, save it. We also save if it's empty as we might
  164. # not be able to send a cookie-delete along with this message.
  165. if (
  166. message["type"] in self.save_message_types
  167. and message.get("status", 200) != 500
  168. and (modified or settings.SESSION_SAVE_EVERY_REQUEST)
  169. ):
  170. await database_sync_to_async(self.save_session)()
  171. # If this is a message type that can transport cookies back to the
  172. # client, then do so.
  173. if message["type"] in self.cookie_response_message_types:
  174. if empty:
  175. # Delete cookie if it's set
  176. if settings.SESSION_COOKIE_NAME in self.scope["cookies"]:
  177. CookieMiddleware.delete_cookie(
  178. message,
  179. settings.SESSION_COOKIE_NAME,
  180. path=settings.SESSION_COOKIE_PATH,
  181. domain=settings.SESSION_COOKIE_DOMAIN,
  182. )
  183. else:
  184. # Get the expiry data
  185. if self.scope["session"].get_expire_at_browser_close():
  186. max_age = None
  187. expires = None
  188. else:
  189. max_age = self.scope["session"].get_expiry_age()
  190. expires_time = time.time() + max_age
  191. expires = http_date(expires_time)
  192. # Set the cookie
  193. CookieMiddleware.set_cookie(
  194. message,
  195. self.cookie_name,
  196. self.scope["session"].session_key,
  197. max_age=max_age,
  198. expires=expires,
  199. domain=settings.SESSION_COOKIE_DOMAIN,
  200. path=settings.SESSION_COOKIE_PATH,
  201. secure=settings.SESSION_COOKIE_SECURE or None,
  202. httponly=settings.SESSION_COOKIE_HTTPONLY or None,
  203. samesite=settings.SESSION_COOKIE_SAMESITE,
  204. )
  205. # Pass up the send
  206. return await self.real_send(message)
  207. def save_session(self):
  208. """
  209. Saves the current session.
  210. """
  211. try:
  212. self.scope["session"].save()
  213. except UpdateError:
  214. raise SuspiciousOperation(
  215. "The request's session was deleted before the "
  216. "request completed. The user may have logged "
  217. "out in a concurrent request, for example."
  218. )
  219. class SessionMiddleware:
  220. """
  221. Class that adds Django sessions (from HTTP cookies) to the
  222. scope. Works with HTTP or WebSocket protocol types (or anything that
  223. provides a "headers" entry in the scope).
  224. Requires the CookieMiddleware to be higher up in the stack.
  225. """
  226. def __init__(self, inner):
  227. self.inner = inner
  228. async def __call__(self, scope, receive, send):
  229. """
  230. Instantiate a session wrapper for this scope, resolve the session and
  231. call the inner application.
  232. """
  233. wrapper = InstanceSessionWrapper(scope, send)
  234. await wrapper.resolve_session()
  235. return await self.inner(wrapper.scope, receive, wrapper.send)
  236. # Shortcut to include cookie middleware
  237. def SessionMiddlewareStack(inner):
  238. return CookieMiddleware(SessionMiddleware(inner))