123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352 |
- # -*- 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. It 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: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
- :license: BSD, see LICENSE for more details.
- """
- import re
- import os
- import tempfile
- from os import path
- from time import time
- from random import random
- from hashlib import sha1
- from pickle import dump, load, HIGHEST_PROTOCOL
- from werkzeug.datastructures import CallbackDict
- from werkzeug.utils import dump_cookie, parse_cookie
- from werkzeug.wsgi import ClosingIterator
- from werkzeug.posixemulation import rename
- from werkzeug._compat import PY2, text_type
- from werkzeug.filesystem import get_filesystem_encoding
- _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),
- self.should_save and '*' or ''
- )
- @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, 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.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)))
- return start_response(status, headers, exc_info)
- return ClosingIterator(self.app(environ, injecting_start_response),
- lambda: self.store.save_if_modified(session))
|