|
0
|
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 |