1# -*- coding: utf-8 -*- 2r""" 3 werkzeug.contrib.sessions 4 ~~~~~~~~~~~~~~~~~~~~~~~~~ 5 6 This module contains some helper classes that help one to add session 7 support to a python WSGI application. For full client-side session 8 storage see :mod:`~werkzeug.contrib.securecookie` which implements a 9 secure, client-side session storage. 10 11 12 Application Integration 13 ======================= 14 15 :: 16 17 from werkzeug.contrib.sessions import SessionMiddleware, \ 18 FilesystemSessionStore 19 20 app = SessionMiddleware(app, FilesystemSessionStore()) 21 22 The current session will then appear in the WSGI environment as 23 `werkzeug.session`. However it's recommended to not use the middleware 24 but the stores directly in the application. However for very simple 25 scripts a middleware for sessions could be sufficient. 26 27 This module does not implement methods or ways to check if a session is 28 expired. That should be done by a cronjob and storage specific. For 29 example to prune unused filesystem sessions one could check the modified 30 time of the files. If sessions are stored in the database the new() 31 method should add an expiration timestamp for the session. 32 33 For better flexibility it's recommended to not use the middleware but the 34 store and session object directly in the application dispatching:: 35 36 session_store = FilesystemSessionStore() 37 38 def application(environ, start_response): 39 request = Request(environ) 40 sid = request.cookies.get('cookie_name') 41 if sid is None: 42 request.session = session_store.new() 43 else: 44 request.session = session_store.get(sid) 45 response = get_the_response_object(request) 46 if request.session.should_save: 47 session_store.save(request.session) 48 response.set_cookie('cookie_name', request.session.sid) 49 return response(environ, start_response) 50 51 :copyright: 2007 Pallets 52 :license: BSD-3-Clause 53""" 54import os 55import re 56import tempfile 57import warnings 58from hashlib import sha1 59from os import path 60from pickle import dump 61from pickle import HIGHEST_PROTOCOL 62from pickle import load 63from random import random 64from time import time 65 66from .._compat import PY2 67from .._compat import text_type 68from ..datastructures import CallbackDict 69from ..filesystem import get_filesystem_encoding 70from ..http import dump_cookie 71from ..http import parse_cookie 72from ..posixemulation import rename 73from ..wsgi import ClosingIterator 74 75warnings.warn( 76 "'werkzeug.contrib.sessions' is deprecated as of version 0.15 and" 77 " will be removed in version 1.0. It has moved to" 78 " https://github.com/pallets/secure-cookie.", 79 DeprecationWarning, 80 stacklevel=2, 81) 82 83_sha1_re = re.compile(r"^[a-f0-9]{40}$") 84 85 86def _urandom(): 87 if hasattr(os, "urandom"): 88 return os.urandom(30) 89 return text_type(random()).encode("ascii") 90 91 92def generate_key(salt=None): 93 if salt is None: 94 salt = repr(salt).encode("ascii") 95 return sha1(b"".join([salt, str(time()).encode("ascii"), _urandom()])).hexdigest() 96 97 98class ModificationTrackingDict(CallbackDict): 99 __slots__ = ("modified",) 100 101 def __init__(self, *args, **kwargs): 102 def on_update(self): 103 self.modified = True 104 105 self.modified = False 106 CallbackDict.__init__(self, on_update=on_update) 107 dict.update(self, *args, **kwargs) 108 109 def copy(self): 110 """Create a flat copy of the dict.""" 111 missing = object() 112 result = object.__new__(self.__class__) 113 for name in self.__slots__: 114 val = getattr(self, name, missing) 115 if val is not missing: 116 setattr(result, name, val) 117 return result 118 119 def __copy__(self): 120 return self.copy() 121 122 123class Session(ModificationTrackingDict): 124 """Subclass of a dict that keeps track of direct object changes. Changes 125 in mutable structures are not tracked, for those you have to set 126 `modified` to `True` by hand. 127 """ 128 129 __slots__ = ModificationTrackingDict.__slots__ + ("sid", "new") 130 131 def __init__(self, data, sid, new=False): 132 ModificationTrackingDict.__init__(self, data) 133 self.sid = sid 134 self.new = new 135 136 def __repr__(self): 137 return "<%s %s%s>" % ( 138 self.__class__.__name__, 139 dict.__repr__(self), 140 "*" if self.should_save else "", 141 ) 142 143 @property 144 def should_save(self): 145 """True if the session should be saved. 146 147 .. versionchanged:: 0.6 148 By default the session is now only saved if the session is 149 modified, not if it is new like it was before. 150 """ 151 return self.modified 152 153 154class SessionStore(object): 155 """Baseclass for all session stores. The Werkzeug contrib module does not 156 implement any useful stores besides the filesystem store, application 157 developers are encouraged to create their own stores. 158 159 :param session_class: The session class to use. Defaults to 160 :class:`Session`. 161 """ 162 163 def __init__(self, session_class=None): 164 if session_class is None: 165 session_class = Session 166 self.session_class = session_class 167 168 def is_valid_key(self, key): 169 """Check if a key has the correct format.""" 170 return _sha1_re.match(key) is not None 171 172 def generate_key(self, salt=None): 173 """Simple function that generates a new session key.""" 174 return generate_key(salt) 175 176 def new(self): 177 """Generate a new session.""" 178 return self.session_class({}, self.generate_key(), True) 179 180 def save(self, session): 181 """Save a session.""" 182 183 def save_if_modified(self, session): 184 """Save if a session class wants an update.""" 185 if session.should_save: 186 self.save(session) 187 188 def delete(self, session): 189 """Delete a session.""" 190 191 def get(self, sid): 192 """Get a session for this sid or a new session object. This method 193 has to check if the session key is valid and create a new session if 194 that wasn't the case. 195 """ 196 return self.session_class({}, sid, True) 197 198 199#: used for temporary files by the filesystem session store 200_fs_transaction_suffix = ".__wz_sess" 201 202 203class FilesystemSessionStore(SessionStore): 204 """Simple example session store that saves sessions on the filesystem. 205 This store works best on POSIX systems and Windows Vista / Windows 206 Server 2008 and newer. 207 208 .. versionchanged:: 0.6 209 `renew_missing` was added. Previously this was considered `True`, 210 now the default changed to `False` and it can be explicitly 211 deactivated. 212 213 :param path: the path to the folder used for storing the sessions. 214 If not provided the default temporary directory is used. 215 :param filename_template: a string template used to give the session 216 a filename. ``%s`` is replaced with the 217 session id. 218 :param session_class: The session class to use. Defaults to 219 :class:`Session`. 220 :param renew_missing: set to `True` if you want the store to 221 give the user a new sid if the session was 222 not yet saved. 223 """ 224 225 def __init__( 226 self, 227 path=None, 228 filename_template="werkzeug_%s.sess", 229 session_class=None, 230 renew_missing=False, 231 mode=0o644, 232 ): 233 SessionStore.__init__(self, session_class) 234 if path is None: 235 path = tempfile.gettempdir() 236 self.path = path 237 if isinstance(filename_template, text_type) and PY2: 238 filename_template = filename_template.encode(get_filesystem_encoding()) 239 assert not filename_template.endswith(_fs_transaction_suffix), ( 240 "filename templates may not end with %s" % _fs_transaction_suffix 241 ) 242 self.filename_template = filename_template 243 self.renew_missing = renew_missing 244 self.mode = mode 245 246 def get_session_filename(self, sid): 247 # out of the box, this should be a strict ASCII subset but 248 # you might reconfigure the session object to have a more 249 # arbitrary string. 250 if isinstance(sid, text_type) and PY2: 251 sid = sid.encode(get_filesystem_encoding()) 252 return path.join(self.path, self.filename_template % sid) 253 254 def save(self, session): 255 fn = self.get_session_filename(session.sid) 256 fd, tmp = tempfile.mkstemp(suffix=_fs_transaction_suffix, dir=self.path) 257 f = os.fdopen(fd, "wb") 258 try: 259 dump(dict(session), f, HIGHEST_PROTOCOL) 260 finally: 261 f.close() 262 try: 263 rename(tmp, fn) 264 os.chmod(fn, self.mode) 265 except (IOError, OSError): 266 pass 267 268 def delete(self, session): 269 fn = self.get_session_filename(session.sid) 270 try: 271 os.unlink(fn) 272 except OSError: 273 pass 274 275 def get(self, sid): 276 if not self.is_valid_key(sid): 277 return self.new() 278 try: 279 f = open(self.get_session_filename(sid), "rb") 280 except IOError: 281 if self.renew_missing: 282 return self.new() 283 data = {} 284 else: 285 try: 286 try: 287 data = load(f) 288 except Exception: 289 data = {} 290 finally: 291 f.close() 292 return self.session_class(data, sid, False) 293 294 def list(self): 295 """Lists all sessions in the store. 296 297 .. versionadded:: 0.6 298 """ 299 before, after = self.filename_template.split("%s", 1) 300 filename_re = re.compile( 301 r"%s(.{5,})%s$" % (re.escape(before), re.escape(after)) 302 ) 303 result = [] 304 for filename in os.listdir(self.path): 305 #: this is a session that is still being saved. 306 if filename.endswith(_fs_transaction_suffix): 307 continue 308 match = filename_re.match(filename) 309 if match is not None: 310 result.append(match.group(1)) 311 return result 312 313 314class SessionMiddleware(object): 315 """A simple middleware that puts the session object of a store provided 316 into the WSGI environ. It automatically sets cookies and restores 317 sessions. 318 319 However a middleware is not the preferred solution because it won't be as 320 fast as sessions managed by the application itself and will put a key into 321 the WSGI environment only relevant for the application which is against 322 the concept of WSGI. 323 324 The cookie parameters are the same as for the :func:`~dump_cookie` 325 function just prefixed with ``cookie_``. Additionally `max_age` is 326 called `cookie_age` and not `cookie_max_age` because of backwards 327 compatibility. 328 """ 329 330 def __init__( 331 self, 332 app, 333 store, 334 cookie_name="session_id", 335 cookie_age=None, 336 cookie_expires=None, 337 cookie_path="/", 338 cookie_domain=None, 339 cookie_secure=None, 340 cookie_httponly=False, 341 cookie_samesite="Lax", 342 environ_key="werkzeug.session", 343 ): 344 self.app = app 345 self.store = store 346 self.cookie_name = cookie_name 347 self.cookie_age = cookie_age 348 self.cookie_expires = cookie_expires 349 self.cookie_path = cookie_path 350 self.cookie_domain = cookie_domain 351 self.cookie_secure = cookie_secure 352 self.cookie_httponly = cookie_httponly 353 self.cookie_samesite = cookie_samesite 354 self.environ_key = environ_key 355 356 def __call__(self, environ, start_response): 357 cookie = parse_cookie(environ.get("HTTP_COOKIE", "")) 358 sid = cookie.get(self.cookie_name, None) 359 if sid is None: 360 session = self.store.new() 361 else: 362 session = self.store.get(sid) 363 environ[self.environ_key] = session 364 365 def injecting_start_response(status, headers, exc_info=None): 366 if session.should_save: 367 self.store.save(session) 368 headers.append( 369 ( 370 "Set-Cookie", 371 dump_cookie( 372 self.cookie_name, 373 session.sid, 374 self.cookie_age, 375 self.cookie_expires, 376 self.cookie_path, 377 self.cookie_domain, 378 self.cookie_secure, 379 self.cookie_httponly, 380 samesite=self.cookie_samesite, 381 ), 382 ) 383 ) 384 return start_response(status, headers, exc_info) 385 386 return ClosingIterator( 387 self.app(environ, injecting_start_response), 388 lambda: self.store.save_if_modified(session), 389 ) 390