forked from mbr/flask-kvsession
-
Notifications
You must be signed in to change notification settings - Fork 0
/
kvsession.py
291 lines (227 loc) · 10.1 KB
/
kvsession.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# -*- coding: utf-8 -*-
"""
flaskext.kvsession
~~~~~~~~~~~~~~~~~~
Drop-in replacement module for Flask sessions that uses a
:class:`simplekv.KeyValueStore` as a
backend for server-side sessions.
"""
import calendar
try:
import cPickle as pickle
except ImportError:
import pickle
from datetime import datetime
import hmac
from random import SystemRandom
import re
from itsdangerous import Signer, BadSignature
from werkzeug.datastructures import CallbackDict
try:
from flask.sessions import SessionMixin, SessionInterface
except ImportError:
# pre-0.8, these are replacements for the new session interface
# see http://flask.pocoo.org/snippets/52/
# FIXME: this code should be made legacy and a dependency for
# flask >= 0.8 added once it becomes stable
class SessionInterface(object):
def get_expiration_time(self, app, session):
# copied from flask 0.8 source
if session.permanent:
return datetime.utcnow() + app.permanent_session_lifetime
def get_cookie_domain(self, app):
# copied from flask 0.8 source
if app.config['SERVER_NAME'] is not None:
return '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0]
class SessionMixin(object):
def _get_permanent(self):
return self.get('_permanent', False)
def _set_permanent(self, value):
self['_permanent'] = bool(value)
permanent = property(_get_permanent, _set_permanent)
del _get_permanent, _set_permanent
new = False
modified = True
class SessionID(object):
"""Helper class for parsing session ids.
Internally, Flask-KVSession stores session ids that are serialized as
``KEY_CREATED``, where ``KEY`` is a random number (the sessions "true" id)
and ``CREATED`` a UNIX-timestamp of when the session was created.
:param id: An integer to be used as the session key.
:param created: A :class:`~datetime.datetime` instance or None. A value of
None will result in :meth:`~datetime.datetime.utcnow()` to
be used.
"""
def __init__(self, id, created=None):
if None == created:
created = datetime.utcnow()
self.id = id
self.created = created
def has_expired(self, lifetime, now=None):
"""Report if the session key has expired.
:param lifetime: A :class:`datetime.timedelta` that specifies the
maximum age this :class:`SessionID` should be checked
against.
:param now: If specified, use this :class:`~datetime.datetime` instance
instead of :meth:`~datetime.datetime.utcnow()` as the
current time.
"""
now = now or datetime.utcnow()
return now > self.created + lifetime
def serialize(self):
"""Serializes to the standard form of ``KEY_CREATED``"""
return '%x_%x' % (self.id,
calendar.timegm(self.created.utctimetuple()))
@classmethod
def unserialize(cls, string):
"""Unserializes from a string.
:param string: A string created by :meth:`serialize`.
"""
id_s, created_s = string.split('_')
return cls(int(id_s, 16),
datetime.utcfromtimestamp(int(created_s, 16)))
class KVSession(CallbackDict, SessionMixin):
"""Replacement session class.
Instances of this class will replace the session (and thus be available
through things like :attr:`flask.session`.
The session class will save data to the store only when necessary, empty
sessions will not be stored at all."""
def __init__(self, initial=None):
def _on_update(d):
d.modified = True
CallbackDict.__init__(self, initial, _on_update)
if not initial:
self.modified = False
def destroy(self):
"""Destroys a session completely, by deleting all keys and removing it
from the internal store immediately.
This allows removing a session for security reasons, e.g. a login
stored in a session will cease to exist if the session is destroyed.
"""
for k in self.keys():
del self[k]
if self.sid_s:
self.store.delete(self.sid_s)
self.modified = False
self.new = False
def regenerate(self):
"""Generate a new session id for this session.
To avoid vulnerabilities through `session fixation attacks
<http://en.wikipedia.org/wiki/Session_fixation>`_, this function can be
called after an action like a login has taken place. The session will
be copied over to a new session id and the old one removed.
"""
self.modified = True
if getattr(self, 'sid_s', None):
# delete old session
self.store.delete(self.sid_s)
# remove sid_s, set modified
self.sid_s = None
self.modified = True
# save_session() will take care of saving the session now
class KVSessionInterface(SessionInterface):
serialization_method = pickle
def __init__(self, store, random_source=None):
self.store = store
self.random_source = random_source
def open_session(self, app, request):
key = app.secret_key
if key is not None:
session_cookie = request.cookies.get(
app.config['SESSION_COOKIE_NAME'],
None
)
if session_cookie:
try:
# restore the cookie, if it has been manipulated,
# we will find out here
sid_s = Signer(app.secret_key).unsign(session_cookie)
sid = SessionID.unserialize(sid_s)
if sid.has_expired(
app.config['PERMANENT_SESSION_LIFETIME']):
return None # the session has expired, no need to even
# check if it exists
# retrieve from store
s = KVSession(self.serialization_method.loads(
self.store.get(sid_s))
)
s.sid_s = sid_s
except (BadSignature, KeyError):
# either the cookie was manipulated or we did not find the
# session in the backend.
s = KVSession() # silently swallow errors, instead of
# of returning a NullSession
s.new = True
else:
s = KVSession() # create an empty session
s.new = True
s.store = self.store
return s
def save_session(self, app, session, response):
if session.modified:
# create a new session id only if requested
# this makes it possible to avoid session fixation, but prevents
# full cookie-highjacking if used carefully
if not getattr(session, 'sid_s', None):
session.sid_s = SessionID(self.random_source.getrandbits(
app.config['SESSION_KEY_BITS'])
).serialize()
self.store.put(session.sid_s,
self.serialization_method.dumps(dict(session)))
session.new = False
# save sid_s in session cookie
cookie_data = Signer(app.secret_key).sign(session.sid_s)
response.set_cookie(key=app.config['SESSION_COOKIE_NAME'],
value=cookie_data,
expires=self.get_expiration_time(app, session),
domain=self.get_cookie_domain(app))
class KVSessionExtension(object):
"""Activates Flask-KVSession for an application.
:param session_kvstore: An object supporting the
`simplekv.KeyValueStore` interface that session
data will be store in.
:param app: The app to activate. If not `None`, this is essentially the
same as calling :meth:`init_app` later."""
key_regex = re.compile('^[0-9a-f]+_[0-9a-f]+$')
def __init__(self, session_kvstore, app=None):
self.session_kvstore = session_kvstore
if app:
self.init_app(app)
def cleanup_sessions(self):
"""Removes all expired session from the store.
Periodically, this function should be called to remove sessions from
the backend store that have expired, as they are not removed
automatically.
This function retrieves all session keys, checks they are older than
``PERMANENT_SESSION_LIFETIME`` and if so, removes them.
Note that no distinction is made between non-permanent and permanent
sessions."""
for key in self.session_kvstore.keys():
m = self.key_regex.match(key)
now = datetime.utcnow()
if m:
# read id
sid = SessionID.unserialize(key)
# remove if expired
if sid.has_expired(
self.app.config['PERMANENT_SESSION_LIFETIME'],
now
):
self.session_kvstore.delete(key)
def init_app(self, app):
"""Initialize application and KVSession.
This will replace the session management of the application with
Flask-KVSession's.
:param app: The :class:`~flask.Flask` app to be initialized."""
self.app = app
app.config.setdefault('SESSION_KEY_BITS', 64)
app.config.setdefault('SESSION_RANDOM_SOURCE', None)
if not hasattr(app, 'session_interface'):
app.open_session = lambda r: \
app.session_interface.open_session(app, r)
app.save_session = lambda s, r: \
app.session_interface.save_session(app, s, r)
self.random_source = app.config['SESSION_RANDOM_SOURCE'] or\
SystemRandom()
app.session_interface = KVSessionInterface(self.session_kvstore,
self.random_source)