1# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) 2# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 3 4""" 5Creates a session object in your WSGI environment. 6 7Use like: 8 9..code-block:: Python 10 11 environ['paste.session.factory']() 12 13This will return a dictionary. The contents of this dictionary will 14be saved to disk when the request is completed. The session will be 15created when you first fetch the session dictionary, and a cookie will 16be sent in that case. There's current no way to use sessions without 17cookies, and there's no way to delete a session except to clear its 18data. 19 20@@: This doesn't do any locking, and may cause problems when a single 21session is accessed concurrently. Also, it loads and saves the 22session for each request, with no caching. Also, sessions aren't 23expired. 24""" 25 26try: 27 # Python 3 28 from http.cookies import SimpleCookie 29except ImportError: 30 # Python 2 31 from Cookie import SimpleCookie 32import time 33import random 34import os 35import datetime 36import six 37import threading 38import tempfile 39 40try: 41 import cPickle 42except ImportError: 43 import pickle as cPickle 44try: 45 from hashlib import md5 46except ImportError: 47 from md5 import md5 48from paste import wsgilib 49from paste import request 50 51class SessionMiddleware(object): 52 53 def __init__(self, application, global_conf=None, **factory_kw): 54 self.application = application 55 self.factory_kw = factory_kw 56 57 def __call__(self, environ, start_response): 58 session_factory = SessionFactory(environ, **self.factory_kw) 59 environ['paste.session.factory'] = session_factory 60 remember_headers = [] 61 62 def session_start_response(status, headers, exc_info=None): 63 if not session_factory.created: 64 remember_headers[:] = [status, headers] 65 return start_response(status, headers) 66 headers.append(session_factory.set_cookie_header()) 67 return start_response(status, headers, exc_info) 68 69 app_iter = self.application(environ, session_start_response) 70 def start(): 71 if session_factory.created and remember_headers: 72 # Tricky bastard used the session after start_response 73 status, headers = remember_headers 74 headers.append(session_factory.set_cookie_header()) 75 exc = ValueError( 76 "You cannot get the session after content from the " 77 "app_iter has been returned") 78 start_response(status, headers, (exc.__class__, exc, None)) 79 def close(): 80 if session_factory.used: 81 session_factory.close() 82 return wsgilib.add_start_close(app_iter, start, close) 83 84 85class SessionFactory(object): 86 87 88 def __init__(self, environ, cookie_name='_SID_', 89 session_class=None, 90 session_expiration=60*12, # in minutes 91 **session_class_kw): 92 93 self.created = False 94 self.used = False 95 self.environ = environ 96 self.cookie_name = cookie_name 97 self.session = None 98 self.session_class = session_class or FileSession 99 self.session_class_kw = session_class_kw 100 101 self.expiration = session_expiration 102 103 def __call__(self): 104 self.used = True 105 if self.session is not None: 106 return self.session.data() 107 cookies = request.get_cookies(self.environ) 108 session = None 109 if self.cookie_name in cookies: 110 self.sid = cookies[self.cookie_name].value 111 try: 112 session = self.session_class(self.sid, create=False, 113 **self.session_class_kw) 114 except KeyError: 115 # Invalid SID 116 pass 117 if session is None: 118 self.created = True 119 self.sid = self.make_sid() 120 session = self.session_class(self.sid, create=True, 121 **self.session_class_kw) 122 session.clean_up() 123 self.session = session 124 return session.data() 125 126 def has_session(self): 127 if self.session is not None: 128 return True 129 cookies = request.get_cookies(self.environ) 130 if cookies.has_key(self.cookie_name): 131 return True 132 return False 133 134 def make_sid(self): 135 # @@: need better algorithm 136 return (''.join(['%02d' % x for x in time.localtime(time.time())[:6]]) 137 + '-' + self.unique_id()) 138 139 def unique_id(self, for_object=None): 140 """ 141 Generates an opaque, identifier string that is practically 142 guaranteed to be unique. If an object is passed, then its 143 id() is incorporated into the generation. Relies on md5 and 144 returns a 32 character long string. 145 """ 146 r = [time.time(), random.random()] 147 if hasattr(os, 'times'): 148 r.append(os.times()) 149 if for_object is not None: 150 r.append(id(for_object)) 151 content = str(r) 152 if six.PY3: 153 content = content.encode('utf8') 154 md5_hash = md5(content) 155 try: 156 return md5_hash.hexdigest() 157 except AttributeError: 158 # Older versions of Python didn't have hexdigest, so we'll 159 # do it manually 160 hexdigest = [] 161 for char in md5_hash.digest(): 162 hexdigest.append('%02x' % ord(char)) 163 return ''.join(hexdigest) 164 165 def set_cookie_header(self): 166 c = SimpleCookie() 167 c[self.cookie_name] = self.sid 168 c[self.cookie_name]['path'] = '/' 169 170 gmt_expiration_time = time.gmtime(time.time() + (self.expiration * 60)) 171 c[self.cookie_name]['expires'] = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT", gmt_expiration_time) 172 173 name, value = str(c).split(': ', 1) 174 return (name, value) 175 176 def close(self): 177 if self.session is not None: 178 self.session.close() 179 180 181last_cleanup = None 182cleaning_up = False 183cleanup_cycle = datetime.timedelta(seconds=15*60) #15 min 184 185class FileSession(object): 186 187 def __init__(self, sid, create=False, session_file_path=tempfile.gettempdir(), 188 chmod=None, 189 expiration=2880, # in minutes: 48 hours 190 ): 191 if chmod and isinstance(chmod, (six.binary_type, six.text_type)): 192 chmod = int(chmod, 8) 193 self.chmod = chmod 194 if not sid: 195 # Invalid... 196 raise KeyError 197 self.session_file_path = session_file_path 198 self.sid = sid 199 if not create: 200 if not os.path.exists(self.filename()): 201 raise KeyError 202 self._data = None 203 204 self.expiration = expiration 205 206 207 def filename(self): 208 return os.path.join(self.session_file_path, self.sid) 209 210 def data(self): 211 if self._data is not None: 212 return self._data 213 if os.path.exists(self.filename()): 214 f = open(self.filename(), 'rb') 215 self._data = cPickle.load(f) 216 f.close() 217 else: 218 self._data = {} 219 return self._data 220 221 def close(self): 222 if self._data is not None: 223 filename = self.filename() 224 exists = os.path.exists(filename) 225 if not self._data: 226 if exists: 227 os.unlink(filename) 228 else: 229 f = open(filename, 'wb') 230 cPickle.dump(self._data, f) 231 f.close() 232 if not exists and self.chmod: 233 os.chmod(filename, self.chmod) 234 235 def _clean_up(self): 236 global cleaning_up 237 try: 238 exp_time = datetime.timedelta(seconds=self.expiration*60) 239 now = datetime.datetime.now() 240 241 #Open every session and check that it isn't too old 242 for root, dirs, files in os.walk(self.session_file_path): 243 for f in files: 244 self._clean_up_file(f, exp_time=exp_time, now=now) 245 finally: 246 cleaning_up = False 247 248 def _clean_up_file(self, f, exp_time, now): 249 t = f.split("-") 250 if len(t) != 2: 251 return 252 t = t[0] 253 try: 254 sess_time = datetime.datetime( 255 int(t[0:4]), 256 int(t[4:6]), 257 int(t[6:8]), 258 int(t[8:10]), 259 int(t[10:12]), 260 int(t[12:14])) 261 except ValueError: 262 # Probably not a session file at all 263 return 264 265 if sess_time + exp_time < now: 266 os.remove(os.path.join(self.session_file_path, f)) 267 268 def clean_up(self): 269 global last_cleanup, cleanup_cycle, cleaning_up 270 now = datetime.datetime.now() 271 272 if cleaning_up: 273 return 274 275 if not last_cleanup or last_cleanup + cleanup_cycle < now: 276 if not cleaning_up: 277 cleaning_up = True 278 try: 279 last_cleanup = now 280 t = threading.Thread(target=self._clean_up) 281 t.start() 282 except: 283 # Normally _clean_up should set cleaning_up 284 # to false, but if something goes wrong starting 285 # it... 286 cleaning_up = False 287 raise 288 289class _NoDefault(object): 290 def __repr__(self): 291 return '<dynamic default>' 292NoDefault = _NoDefault() 293 294def make_session_middleware( 295 app, global_conf, 296 session_expiration=NoDefault, 297 expiration=NoDefault, 298 cookie_name=NoDefault, 299 session_file_path=NoDefault, 300 chmod=NoDefault): 301 """ 302 Adds a middleware that handles sessions for your applications. 303 The session is a peristent dictionary. To get this dictionary 304 in your application, use ``environ['paste.session.factory']()`` 305 which returns this persistent dictionary. 306 307 Configuration: 308 309 session_expiration: 310 The time each session lives, in minutes. This controls 311 the cookie expiration. Default 12 hours. 312 313 expiration: 314 The time each session lives on disk. Old sessions are 315 culled from disk based on this. Default 48 hours. 316 317 cookie_name: 318 The cookie name used to track the session. Use different 319 names to avoid session clashes. 320 321 session_file_path: 322 Sessions are put in this location, default /tmp. 323 324 chmod: 325 The octal chmod you want to apply to new sessions (e.g., 660 326 to make the sessions group readable/writable) 327 328 Each of these also takes from the global configuration. cookie_name 329 and chmod take from session_cookie_name and session_chmod 330 """ 331 if session_expiration is NoDefault: 332 session_expiration = global_conf.get('session_expiration', 60*12) 333 session_expiration = int(session_expiration) 334 if expiration is NoDefault: 335 expiration = global_conf.get('expiration', 60*48) 336 expiration = int(expiration) 337 if cookie_name is NoDefault: 338 cookie_name = global_conf.get('session_cookie_name', '_SID_') 339 if session_file_path is NoDefault: 340 session_file_path = global_conf.get('session_file_path', '/tmp') 341 if chmod is NoDefault: 342 chmod = global_conf.get('session_chmod', None) 343 return SessionMiddleware( 344 app, session_expiration=session_expiration, 345 expiration=expiration, cookie_name=cookie_name, 346 session_file_path=session_file_path, chmod=chmod) 347