123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 |
- # -*- coding: utf-8 -*-
- r"""
- werkzeug.contrib.sessions
- ~~~~~~~~~~~~~~~~~~~~~~~~~
-
- This module contains some helper classes that help one to add session
- support to a python WSGI application. For full client-side session
- storage see :mod:`~werkzeug.contrib.securecookie` which implements a
- secure, client-side session storage.
-
-
- Application Integration
- =======================
-
- ::
-
- from werkzeug.contrib.sessions import SessionMiddleware, \
- FilesystemSessionStore
-
- app = SessionMiddleware(app, FilesystemSessionStore())
-
- The current session will then appear in the WSGI environment as
- `werkzeug.session`. However it's recommended to not use the middleware
- but the stores directly in the application. However for very simple
- scripts a middleware for sessions could be sufficient.
-
- This module does not implement methods or ways to check if a session is
- expired. That should be done by a cronjob and storage specific. For
- example to prune unused filesystem sessions one could check the modified
- time of the files. If sessions are stored in the database the new()
- method should add an expiration timestamp for the session.
-
- For better flexibility it's recommended to not use the middleware but the
- store and session object directly in the application dispatching::
-
- session_store = FilesystemSessionStore()
-
- def application(environ, start_response):
- request = Request(environ)
- sid = request.cookies.get('cookie_name')
- if sid is None:
- request.session = session_store.new()
- else:
- request.session = session_store.get(sid)
- response = get_the_response_object(request)
- if request.session.should_save:
- session_store.save(request.session)
- response.set_cookie('cookie_name', request.session.sid)
- return response(environ, start_response)
-
- :copyright: 2007 Pallets
- :license: BSD-3-Clause
- """
- import os
- import re
- import tempfile
- import warnings
- from hashlib import sha1
- from os import path
- from pickle import dump
- from pickle import HIGHEST_PROTOCOL
- from pickle import load
- from random import random
- from time import time
-
- from .._compat import PY2
- from .._compat import text_type
- from ..datastructures import CallbackDict
- from ..filesystem import get_filesystem_encoding
- from ..http import dump_cookie
- from ..http import parse_cookie
- from ..posixemulation import rename
- from ..wsgi import ClosingIterator
-
- warnings.warn(
- "'werkzeug.contrib.sessions' is deprecated as of version 0.15 and"
- " will be removed in version 1.0. It has moved to"
- " https://github.com/pallets/secure-cookie.",
- DeprecationWarning,
- stacklevel=2,
- )
-
- _sha1_re = re.compile(r"^[a-f0-9]{40}$")
-
-
- def _urandom():
- if hasattr(os, "urandom"):
- return os.urandom(30)
- return text_type(random()).encode("ascii")
-
-
- def generate_key(salt=None):
- if salt is None:
- salt = repr(salt).encode("ascii")
- return sha1(b"".join([salt, str(time()).encode("ascii"), _urandom()])).hexdigest()
-
-
- class ModificationTrackingDict(CallbackDict):
- __slots__ = ("modified",)
-
- def __init__(self, *args, **kwargs):
- def on_update(self):
- self.modified = True
-
- self.modified = False
- CallbackDict.__init__(self, on_update=on_update)
- dict.update(self, *args, **kwargs)
-
- def copy(self):
- """Create a flat copy of the dict."""
- missing = object()
- result = object.__new__(self.__class__)
- for name in self.__slots__:
- val = getattr(self, name, missing)
- if val is not missing:
- setattr(result, name, val)
- return result
-
- def __copy__(self):
- return self.copy()
-
-
- class Session(ModificationTrackingDict):
- """Subclass of a dict that keeps track of direct object changes. Changes
- in mutable structures are not tracked, for those you have to set
- `modified` to `True` by hand.
- """
-
- __slots__ = ModificationTrackingDict.__slots__ + ("sid", "new")
-
- def __init__(self, data, sid, new=False):
- ModificationTrackingDict.__init__(self, data)
- self.sid = sid
- self.new = new
-
- def __repr__(self):
- return "<%s %s%s>" % (
- self.__class__.__name__,
- dict.__repr__(self),
- "*" if self.should_save else "",
- )
-
- @property
- def should_save(self):
- """True if the session should be saved.
-
- .. versionchanged:: 0.6
- By default the session is now only saved if the session is
- modified, not if it is new like it was before.
- """
- return self.modified
-
-
- class SessionStore(object):
- """Baseclass for all session stores. The Werkzeug contrib module does not
- implement any useful stores besides the filesystem store, application
- developers are encouraged to create their own stores.
-
- :param session_class: The session class to use. Defaults to
- :class:`Session`.
- """
-
- def __init__(self, session_class=None):
- if session_class is None:
- session_class = Session
- self.session_class = session_class
-
- def is_valid_key(self, key):
- """Check if a key has the correct format."""
- return _sha1_re.match(key) is not None
-
- def generate_key(self, salt=None):
- """Simple function that generates a new session key."""
- return generate_key(salt)
-
- def new(self):
- """Generate a new session."""
- return self.session_class({}, self.generate_key(), True)
-
- def save(self, session):
- """Save a session."""
-
- def save_if_modified(self, session):
- """Save if a session class wants an update."""
- if session.should_save:
- self.save(session)
-
- def delete(self, session):
- """Delete a session."""
-
- def get(self, sid):
- """Get a session for this sid or a new session object. This method
- has to check if the session key is valid and create a new session if
- that wasn't the case.
- """
- return self.session_class({}, sid, True)
-
-
- #: used for temporary files by the filesystem session store
- _fs_transaction_suffix = ".__wz_sess"
-
-
- class FilesystemSessionStore(SessionStore):
- """Simple example session store that saves sessions on the filesystem.
- This store works best on POSIX systems and Windows Vista / Windows
- Server 2008 and newer.
-
- .. versionchanged:: 0.6
- `renew_missing` was added. Previously this was considered `True`,
- now the default changed to `False` and it can be explicitly
- deactivated.
-
- :param path: the path to the folder used for storing the sessions.
- If not provided the default temporary directory is used.
- :param filename_template: a string template used to give the session
- a filename. ``%s`` is replaced with the
- session id.
- :param session_class: The session class to use. Defaults to
- :class:`Session`.
- :param renew_missing: set to `True` if you want the store to
- give the user a new sid if the session was
- not yet saved.
- """
-
- def __init__(
- self,
- path=None,
- filename_template="werkzeug_%s.sess",
- session_class=None,
- renew_missing=False,
- mode=0o644,
- ):
- SessionStore.__init__(self, session_class)
- if path is None:
- path = tempfile.gettempdir()
- self.path = path
- if isinstance(filename_template, text_type) and PY2:
- filename_template = filename_template.encode(get_filesystem_encoding())
- assert not filename_template.endswith(_fs_transaction_suffix), (
- "filename templates may not end with %s" % _fs_transaction_suffix
- )
- self.filename_template = filename_template
- self.renew_missing = renew_missing
- self.mode = mode
-
- def get_session_filename(self, sid):
- # out of the box, this should be a strict ASCII subset but
- # you might reconfigure the session object to have a more
- # arbitrary string.
- if isinstance(sid, text_type) and PY2:
- sid = sid.encode(get_filesystem_encoding())
- return path.join(self.path, self.filename_template % sid)
-
- def save(self, session):
- fn = self.get_session_filename(session.sid)
- fd, tmp = tempfile.mkstemp(suffix=_fs_transaction_suffix, dir=self.path)
- f = os.fdopen(fd, "wb")
- try:
- dump(dict(session), f, HIGHEST_PROTOCOL)
- finally:
- f.close()
- try:
- rename(tmp, fn)
- os.chmod(fn, self.mode)
- except (IOError, OSError):
- pass
-
- def delete(self, session):
- fn = self.get_session_filename(session.sid)
- try:
- os.unlink(fn)
- except OSError:
- pass
-
- def get(self, sid):
- if not self.is_valid_key(sid):
- return self.new()
- try:
- f = open(self.get_session_filename(sid), "rb")
- except IOError:
- if self.renew_missing:
- return self.new()
- data = {}
- else:
- try:
- try:
- data = load(f)
- except Exception:
- data = {}
- finally:
- f.close()
- return self.session_class(data, sid, False)
-
- def list(self):
- """Lists all sessions in the store.
-
- .. versionadded:: 0.6
- """
- before, after = self.filename_template.split("%s", 1)
- filename_re = re.compile(
- r"%s(.{5,})%s$" % (re.escape(before), re.escape(after))
- )
- result = []
- for filename in os.listdir(self.path):
- #: this is a session that is still being saved.
- if filename.endswith(_fs_transaction_suffix):
- continue
- match = filename_re.match(filename)
- if match is not None:
- result.append(match.group(1))
- return result
-
-
- class SessionMiddleware(object):
- """A simple middleware that puts the session object of a store provided
- into the WSGI environ. It automatically sets cookies and restores
- sessions.
-
- However a middleware is not the preferred solution because it won't be as
- fast as sessions managed by the application itself and will put a key into
- the WSGI environment only relevant for the application which is against
- the concept of WSGI.
-
- The cookie parameters are the same as for the :func:`~dump_cookie`
- function just prefixed with ``cookie_``. Additionally `max_age` is
- called `cookie_age` and not `cookie_max_age` because of backwards
- compatibility.
- """
-
- def __init__(
- self,
- app,
- store,
- cookie_name="session_id",
- cookie_age=None,
- cookie_expires=None,
- cookie_path="/",
- cookie_domain=None,
- cookie_secure=None,
- cookie_httponly=False,
- cookie_samesite="Lax",
- environ_key="werkzeug.session",
- ):
- self.app = app
- self.store = store
- self.cookie_name = cookie_name
- self.cookie_age = cookie_age
- self.cookie_expires = cookie_expires
- self.cookie_path = cookie_path
- self.cookie_domain = cookie_domain
- self.cookie_secure = cookie_secure
- self.cookie_httponly = cookie_httponly
- self.cookie_samesite = cookie_samesite
- self.environ_key = environ_key
-
- def __call__(self, environ, start_response):
- cookie = parse_cookie(environ.get("HTTP_COOKIE", ""))
- sid = cookie.get(self.cookie_name, None)
- if sid is None:
- session = self.store.new()
- else:
- session = self.store.get(sid)
- environ[self.environ_key] = session
-
- def injecting_start_response(status, headers, exc_info=None):
- if session.should_save:
- self.store.save(session)
- headers.append(
- (
- "Set-Cookie",
- dump_cookie(
- self.cookie_name,
- session.sid,
- self.cookie_age,
- self.cookie_expires,
- self.cookie_path,
- self.cookie_domain,
- self.cookie_secure,
- self.cookie_httponly,
- samesite=self.cookie_samesite,
- ),
- )
- )
- return start_response(status, headers, exc_info)
-
- return ClosingIterator(
- self.app(environ, injecting_start_response),
- lambda: self.store.save_if_modified(session),
- )
|