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