|
1 import base64 |
|
2 import os |
|
3 import random |
|
4 import sys |
|
5 import time |
|
6 from datetime import datetime, timedelta |
|
7 try: |
|
8 import cPickle as pickle |
|
9 except ImportError: |
|
10 import pickle |
|
11 |
|
12 from django.conf import settings |
|
13 from django.core.exceptions import SuspiciousOperation |
|
14 from django.utils.hashcompat import md5_constructor |
|
15 |
|
16 # Use the system (hardware-based) random number generator if it exists. |
|
17 if hasattr(random, 'SystemRandom'): |
|
18 randrange = random.SystemRandom().randrange |
|
19 else: |
|
20 randrange = random.randrange |
|
21 MAX_SESSION_KEY = 18446744073709551616L # 2 << 63 |
|
22 |
|
23 class CreateError(Exception): |
|
24 """ |
|
25 Used internally as a consistent exception type to catch from save (see the |
|
26 docstring for SessionBase.save() for details). |
|
27 """ |
|
28 pass |
|
29 |
|
30 class SessionBase(object): |
|
31 """ |
|
32 Base class for all Session classes. |
|
33 """ |
|
34 TEST_COOKIE_NAME = 'testcookie' |
|
35 TEST_COOKIE_VALUE = 'worked' |
|
36 |
|
37 def __init__(self, session_key=None): |
|
38 self._session_key = session_key |
|
39 self.accessed = False |
|
40 self.modified = False |
|
41 |
|
42 def __contains__(self, key): |
|
43 return key in self._session |
|
44 |
|
45 def __getitem__(self, key): |
|
46 return self._session[key] |
|
47 |
|
48 def __setitem__(self, key, value): |
|
49 self._session[key] = value |
|
50 self.modified = True |
|
51 |
|
52 def __delitem__(self, key): |
|
53 del self._session[key] |
|
54 self.modified = True |
|
55 |
|
56 def keys(self): |
|
57 return self._session.keys() |
|
58 |
|
59 def items(self): |
|
60 return self._session.items() |
|
61 |
|
62 def get(self, key, default=None): |
|
63 return self._session.get(key, default) |
|
64 |
|
65 def pop(self, key, *args): |
|
66 self.modified = self.modified or key in self._session |
|
67 return self._session.pop(key, *args) |
|
68 |
|
69 def setdefault(self, key, value): |
|
70 if key in self._session: |
|
71 return self._session[key] |
|
72 else: |
|
73 self.modified = True |
|
74 self._session[key] = value |
|
75 return value |
|
76 |
|
77 def set_test_cookie(self): |
|
78 self[self.TEST_COOKIE_NAME] = self.TEST_COOKIE_VALUE |
|
79 |
|
80 def test_cookie_worked(self): |
|
81 return self.get(self.TEST_COOKIE_NAME) == self.TEST_COOKIE_VALUE |
|
82 |
|
83 def delete_test_cookie(self): |
|
84 del self[self.TEST_COOKIE_NAME] |
|
85 |
|
86 def encode(self, session_dict): |
|
87 "Returns the given session dictionary pickled and encoded as a string." |
|
88 pickled = pickle.dumps(session_dict, pickle.HIGHEST_PROTOCOL) |
|
89 pickled_md5 = md5_constructor(pickled + settings.SECRET_KEY).hexdigest() |
|
90 return base64.encodestring(pickled + pickled_md5) |
|
91 |
|
92 def decode(self, session_data): |
|
93 encoded_data = base64.decodestring(session_data) |
|
94 pickled, tamper_check = encoded_data[:-32], encoded_data[-32:] |
|
95 if md5_constructor(pickled + settings.SECRET_KEY).hexdigest() != tamper_check: |
|
96 raise SuspiciousOperation("User tampered with session cookie.") |
|
97 try: |
|
98 return pickle.loads(pickled) |
|
99 # Unpickling can cause a variety of exceptions. If something happens, |
|
100 # just return an empty dictionary (an empty session). |
|
101 except: |
|
102 return {} |
|
103 |
|
104 def update(self, dict_): |
|
105 self._session.update(dict_) |
|
106 self.modified = True |
|
107 |
|
108 def has_key(self, key): |
|
109 return self._session.has_key(key) |
|
110 |
|
111 def values(self): |
|
112 return self._session.values() |
|
113 |
|
114 def iterkeys(self): |
|
115 return self._session.iterkeys() |
|
116 |
|
117 def itervalues(self): |
|
118 return self._session.itervalues() |
|
119 |
|
120 def iteritems(self): |
|
121 return self._session.iteritems() |
|
122 |
|
123 def clear(self): |
|
124 # To avoid unnecessary persistent storage accesses, we set up the |
|
125 # internals directly (loading data wastes time, since we are going to |
|
126 # set it to an empty dict anyway). |
|
127 self._session_cache = {} |
|
128 self.accessed = True |
|
129 self.modified = True |
|
130 |
|
131 def _get_new_session_key(self): |
|
132 "Returns session key that isn't being used." |
|
133 # The random module is seeded when this Apache child is created. |
|
134 # Use settings.SECRET_KEY as added salt. |
|
135 try: |
|
136 pid = os.getpid() |
|
137 except AttributeError: |
|
138 # No getpid() in Jython, for example |
|
139 pid = 1 |
|
140 while 1: |
|
141 session_key = md5_constructor("%s%s%s%s" |
|
142 % (randrange(0, MAX_SESSION_KEY), pid, time.time(), |
|
143 settings.SECRET_KEY)).hexdigest() |
|
144 if not self.exists(session_key): |
|
145 break |
|
146 return session_key |
|
147 |
|
148 def _get_session_key(self): |
|
149 if self._session_key: |
|
150 return self._session_key |
|
151 else: |
|
152 self._session_key = self._get_new_session_key() |
|
153 return self._session_key |
|
154 |
|
155 def _set_session_key(self, session_key): |
|
156 self._session_key = session_key |
|
157 |
|
158 session_key = property(_get_session_key, _set_session_key) |
|
159 |
|
160 def _get_session(self, no_load=False): |
|
161 """ |
|
162 Lazily loads session from storage (unless "no_load" is True, when only |
|
163 an empty dict is stored) and stores it in the current instance. |
|
164 """ |
|
165 self.accessed = True |
|
166 try: |
|
167 return self._session_cache |
|
168 except AttributeError: |
|
169 if self._session_key is None or no_load: |
|
170 self._session_cache = {} |
|
171 else: |
|
172 self._session_cache = self.load() |
|
173 return self._session_cache |
|
174 |
|
175 _session = property(_get_session) |
|
176 |
|
177 def get_expiry_age(self): |
|
178 """Get the number of seconds until the session expires.""" |
|
179 expiry = self.get('_session_expiry') |
|
180 if not expiry: # Checks both None and 0 cases |
|
181 return settings.SESSION_COOKIE_AGE |
|
182 if not isinstance(expiry, datetime): |
|
183 return expiry |
|
184 delta = expiry - datetime.now() |
|
185 return delta.days * 86400 + delta.seconds |
|
186 |
|
187 def get_expiry_date(self): |
|
188 """Get session the expiry date (as a datetime object).""" |
|
189 expiry = self.get('_session_expiry') |
|
190 if isinstance(expiry, datetime): |
|
191 return expiry |
|
192 if not expiry: # Checks both None and 0 cases |
|
193 expiry = settings.SESSION_COOKIE_AGE |
|
194 return datetime.now() + timedelta(seconds=expiry) |
|
195 |
|
196 def set_expiry(self, value): |
|
197 """ |
|
198 Sets a custom expiration for the session. ``value`` can be an integer, |
|
199 a Python ``datetime`` or ``timedelta`` object or ``None``. |
|
200 |
|
201 If ``value`` is an integer, the session will expire after that many |
|
202 seconds of inactivity. If set to ``0`` then the session will expire on |
|
203 browser close. |
|
204 |
|
205 If ``value`` is a ``datetime`` or ``timedelta`` object, the session |
|
206 will expire at that specific future time. |
|
207 |
|
208 If ``value`` is ``None``, the session uses the global session expiry |
|
209 policy. |
|
210 """ |
|
211 if value is None: |
|
212 # Remove any custom expiration for this session. |
|
213 try: |
|
214 del self['_session_expiry'] |
|
215 except KeyError: |
|
216 pass |
|
217 return |
|
218 if isinstance(value, timedelta): |
|
219 value = datetime.now() + value |
|
220 self['_session_expiry'] = value |
|
221 |
|
222 def get_expire_at_browser_close(self): |
|
223 """ |
|
224 Returns ``True`` if the session is set to expire when the browser |
|
225 closes, and ``False`` if there's an expiry date. Use |
|
226 ``get_expiry_date()`` or ``get_expiry_age()`` to find the actual expiry |
|
227 date/age, if there is one. |
|
228 """ |
|
229 if self.get('_session_expiry') is None: |
|
230 return settings.SESSION_EXPIRE_AT_BROWSER_CLOSE |
|
231 return self.get('_session_expiry') == 0 |
|
232 |
|
233 def flush(self): |
|
234 """ |
|
235 Removes the current session data from the database and regenerates the |
|
236 key. |
|
237 """ |
|
238 self.clear() |
|
239 self.delete() |
|
240 self.create() |
|
241 |
|
242 def cycle_key(self): |
|
243 """ |
|
244 Creates a new session key, whilst retaining the current session data. |
|
245 """ |
|
246 data = self._session_cache |
|
247 key = self.session_key |
|
248 self.create() |
|
249 self._session_cache = data |
|
250 self.delete(key) |
|
251 |
|
252 # Methods that child classes must implement. |
|
253 |
|
254 def exists(self, session_key): |
|
255 """ |
|
256 Returns True if the given session_key already exists. |
|
257 """ |
|
258 raise NotImplementedError |
|
259 |
|
260 def create(self): |
|
261 """ |
|
262 Creates a new session instance. Guaranteed to create a new object with |
|
263 a unique key and will have saved the result once (with empty data) |
|
264 before the method returns. |
|
265 """ |
|
266 raise NotImplementedError |
|
267 |
|
268 def save(self, must_create=False): |
|
269 """ |
|
270 Saves the session data. If 'must_create' is True, a new session object |
|
271 is created (otherwise a CreateError exception is raised). Otherwise, |
|
272 save() can update an existing object with the same key. |
|
273 """ |
|
274 raise NotImplementedError |
|
275 |
|
276 def delete(self, session_key=None): |
|
277 """ |
|
278 Deletes the session data under this key. If the key is None, the |
|
279 current session key value is used. |
|
280 """ |
|
281 raise NotImplementedError |
|
282 |
|
283 def load(self): |
|
284 """ |
|
285 Loads the session data and returns a dictionary. |
|
286 """ |
|
287 raise NotImplementedError |