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.5KB

5 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  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. def __call__(self, scope):
  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("ascii"))
  35. break
  36. else:
  37. # No cookie header found - add an empty default.
  38. cookies = {}
  39. # Return inner application
  40. return self.inner(dict(scope, cookies=cookies))
  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. ):
  54. """
  55. Sets a cookie in the passed HTTP response message.
  56. ``expires`` can be:
  57. - a string in the correct format,
  58. - a naive ``datetime.datetime`` object in UTC,
  59. - an aware ``datetime.datetime`` object in any time zone.
  60. If it is a ``datetime.datetime`` object then ``max_age`` will be calculated.
  61. """
  62. value = force_str(value)
  63. cookies = SimpleCookie()
  64. cookies[key] = value
  65. if expires is not None:
  66. if isinstance(expires, datetime.datetime):
  67. if timezone.is_aware(expires):
  68. expires = timezone.make_naive(expires, timezone.utc)
  69. delta = expires - expires.utcnow()
  70. # Add one second so the date matches exactly (a fraction of
  71. # time gets lost between converting to a timedelta and
  72. # then the date string).
  73. delta = delta + datetime.timedelta(seconds=1)
  74. # Just set max_age - the max_age logic will set expires.
  75. expires = None
  76. max_age = max(0, delta.days * 86400 + delta.seconds)
  77. else:
  78. cookies[key]["expires"] = expires
  79. else:
  80. cookies[key]["expires"] = ""
  81. if max_age is not None:
  82. cookies[key]["max-age"] = max_age
  83. # IE requires expires, so set it if hasn't been already.
  84. if not expires:
  85. cookies[key]["expires"] = http_date(time.time() + max_age)
  86. if path is not None:
  87. cookies[key]["path"] = path
  88. if domain is not None:
  89. cookies[key]["domain"] = domain
  90. if secure:
  91. cookies[key]["secure"] = True
  92. if httponly:
  93. cookies[key]["httponly"] = True
  94. # Write out the cookies to the response
  95. for c in cookies.values():
  96. message.setdefault("headers", []).append(
  97. (b"Set-Cookie", bytes(c.output(header=""), encoding="utf-8"))
  98. )
  99. @classmethod
  100. def delete_cookie(cls, message, key, path="/", domain=None):
  101. """
  102. Deletes a cookie in a response.
  103. """
  104. return cls.set_cookie(
  105. message,
  106. key,
  107. max_age=0,
  108. path=path,
  109. domain=domain,
  110. expires="Thu, 01-Jan-1970 00:00:00 GMT",
  111. )
  112. class SessionMiddleware:
  113. """
  114. Class that adds Django sessions (from HTTP cookies) to the
  115. scope. Works with HTTP or WebSocket protocol types (or anything that
  116. provides a "headers" entry in the scope).
  117. Requires the CookieMiddleware to be higher up in the stack.
  118. """
  119. # Message types that trigger a session save if it's modified
  120. save_message_types = ["http.response.start"]
  121. # Message types that can carry session cookies back
  122. cookie_response_message_types = ["http.response.start"]
  123. def __init__(self, inner):
  124. self.inner = inner
  125. self.cookie_name = settings.SESSION_COOKIE_NAME
  126. self.session_store = import_module(settings.SESSION_ENGINE).SessionStore
  127. def __call__(self, scope):
  128. return SessionMiddlewareInstance(scope, self)
  129. class SessionMiddlewareInstance:
  130. """
  131. Inner class that is instantiated once per scope.
  132. """
  133. def __init__(self, scope, middleware):
  134. self.middleware = middleware
  135. self.scope = dict(scope)
  136. if "session" in self.scope:
  137. # There's already session middleware of some kind above us, pass that through
  138. self.activated = False
  139. else:
  140. # Make sure there are cookies in the scope
  141. if "cookies" not in self.scope:
  142. raise ValueError(
  143. "No cookies in scope - SessionMiddleware needs to run inside of CookieMiddleware."
  144. )
  145. # Parse the headers in the scope into cookies
  146. self.scope["session"] = LazyObject()
  147. self.activated = True
  148. # Instantiate our inner application
  149. self.inner = self.middleware.inner(self.scope)
  150. async def __call__(self, receive, send):
  151. """
  152. We intercept the send() callable so we can do session saves and
  153. add session cookie overrides to send back.
  154. """
  155. # Resolve the session now we can do it in a blocking way
  156. session_key = self.scope["cookies"].get(self.middleware.cookie_name)
  157. self.scope["session"]._wrapped = await database_sync_to_async(
  158. self.middleware.session_store
  159. )(session_key)
  160. # Override send
  161. self.real_send = send
  162. return await self.inner(receive, self.send)
  163. async def send(self, message):
  164. """
  165. Overridden send that also does session saves/cookies.
  166. """
  167. # Only save session if we're the outermost session middleware
  168. if self.activated:
  169. modified = self.scope["session"].modified
  170. empty = self.scope["session"].is_empty()
  171. # If this is a message type that we want to save on, and there's
  172. # changed data, save it. We also save if it's empty as we might
  173. # not be able to send a cookie-delete along with this message.
  174. if (
  175. message["type"] in self.middleware.save_message_types
  176. and message.get("status", 200) != 500
  177. and (modified or settings.SESSION_SAVE_EVERY_REQUEST)
  178. ):
  179. self.save_session()
  180. # If this is a message type that can transport cookies back to the
  181. # client, then do so.
  182. if message["type"] in self.middleware.cookie_response_message_types:
  183. if empty:
  184. # Delete cookie if it's set
  185. if settings.SESSION_COOKIE_NAME in self.scope["cookies"]:
  186. CookieMiddleware.delete_cookie(
  187. message,
  188. settings.SESSION_COOKIE_NAME,
  189. path=settings.SESSION_COOKIE_PATH,
  190. domain=settings.SESSION_COOKIE_DOMAIN,
  191. )
  192. else:
  193. # Get the expiry data
  194. if self.scope["session"].get_expire_at_browser_close():
  195. max_age = None
  196. expires = None
  197. else:
  198. max_age = self.scope["session"].get_expiry_age()
  199. expires_time = time.time() + max_age
  200. expires = http_date(expires_time)
  201. # Set the cookie
  202. CookieMiddleware.set_cookie(
  203. message,
  204. self.middleware.cookie_name,
  205. self.scope["session"].session_key,
  206. max_age=max_age,
  207. expires=expires,
  208. domain=settings.SESSION_COOKIE_DOMAIN,
  209. path=settings.SESSION_COOKIE_PATH,
  210. secure=settings.SESSION_COOKIE_SECURE or None,
  211. httponly=settings.SESSION_COOKIE_HTTPONLY or None,
  212. )
  213. # Pass up the send
  214. return await self.real_send(message)
  215. def save_session(self):
  216. """
  217. Saves the current session.
  218. """
  219. try:
  220. self.scope["session"].save()
  221. except UpdateError:
  222. raise SuspiciousOperation(
  223. "The request's session was deleted before the "
  224. "request completed. The user may have logged "
  225. "out in a concurrent request, for example."
  226. )
  227. # Shortcut to include cookie middleware
  228. SessionMiddlewareStack = lambda inner: CookieMiddleware(SessionMiddleware(inner))