forked from orbit-oss/flask
Merge pull request #2288 from davidism/vary-cookies
Set "Vary: Cookie" header when session is accessed
This commit is contained in:
commit
b11f7354d1
2 changed files with 97 additions and 29 deletions
|
|
@ -49,6 +49,13 @@ class SessionMixin(object):
|
||||||
#: The default mixin implementation just hardcodes ``True`` in.
|
#: The default mixin implementation just hardcodes ``True`` in.
|
||||||
modified = True
|
modified = True
|
||||||
|
|
||||||
|
#: the accessed variable indicates whether or not the session object has
|
||||||
|
#: been accessed in that request. This allows flask to append a `Vary:
|
||||||
|
#: Cookie` header to the response if the session is being accessed. This
|
||||||
|
#: allows caching proxy servers, like Varnish, to use both the URL and the
|
||||||
|
#: session cookie as keys when caching pages, preventing multiple users
|
||||||
|
#: from being served the same cache.
|
||||||
|
accessed = True
|
||||||
|
|
||||||
def _tag(value):
|
def _tag(value):
|
||||||
if isinstance(value, tuple):
|
if isinstance(value, tuple):
|
||||||
|
|
@ -117,8 +124,23 @@ class SecureCookieSession(CallbackDict, SessionMixin):
|
||||||
def __init__(self, initial=None):
|
def __init__(self, initial=None):
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
self.modified = True
|
self.modified = True
|
||||||
CallbackDict.__init__(self, initial, on_update)
|
self.accessed = True
|
||||||
|
|
||||||
|
super(SecureCookieSession, self).__init__(initial, on_update)
|
||||||
self.modified = False
|
self.modified = False
|
||||||
|
self.accessed = False
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
self.accessed = True
|
||||||
|
return super(SecureCookieSession, self).__getitem__(key)
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
self.accessed = True
|
||||||
|
return super(SecureCookieSession, self).get(key, default)
|
||||||
|
|
||||||
|
def setdefault(self, key, default=None):
|
||||||
|
self.accessed = True
|
||||||
|
return super(SecureCookieSession, self).setdefault(key, default)
|
||||||
|
|
||||||
|
|
||||||
class NullSession(SecureCookieSession):
|
class NullSession(SecureCookieSession):
|
||||||
|
|
@ -290,22 +312,20 @@ class SessionInterface(object):
|
||||||
return datetime.utcnow() + app.permanent_session_lifetime
|
return datetime.utcnow() + app.permanent_session_lifetime
|
||||||
|
|
||||||
def should_set_cookie(self, app, session):
|
def should_set_cookie(self, app, session):
|
||||||
"""Indicates whether a cookie should be set now or not. This is
|
"""Used by session backends to determine if a ``Set-Cookie`` header
|
||||||
used by session backends to figure out if they should emit a
|
should be set for this session cookie for this response. If the session
|
||||||
set-cookie header or not. The default behavior is controlled by
|
has been modified, the cookie is set. If the session is permanent and
|
||||||
the ``SESSION_REFRESH_EACH_REQUEST`` config variable. If
|
the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is
|
||||||
it's set to ``False`` then a cookie is only set if the session is
|
always set.
|
||||||
modified, if set to ``True`` it's always set if the session is
|
|
||||||
permanent.
|
|
||||||
|
|
||||||
This check is usually skipped if sessions get deleted.
|
This check is usually skipped if the session was deleted.
|
||||||
|
|
||||||
.. versionadded:: 0.11
|
.. versionadded:: 0.11
|
||||||
"""
|
"""
|
||||||
if session.modified:
|
|
||||||
return True
|
return session.modified or (
|
||||||
save_each = app.config['SESSION_REFRESH_EACH_REQUEST']
|
session.permanent and app.config['SESSION_REFRESH_EACH_REQUEST']
|
||||||
return save_each and session.permanent
|
)
|
||||||
|
|
||||||
def open_session(self, app, request):
|
def open_session(self, app, request):
|
||||||
"""This method has to be implemented and must either return ``None``
|
"""This method has to be implemented and must either return ``None``
|
||||||
|
|
@ -371,22 +391,22 @@ class SecureCookieSessionInterface(SessionInterface):
|
||||||
domain = self.get_cookie_domain(app)
|
domain = self.get_cookie_domain(app)
|
||||||
path = self.get_cookie_path(app)
|
path = self.get_cookie_path(app)
|
||||||
|
|
||||||
# Delete case. If there is no session we bail early.
|
# If the session is modified to be empty, remove the cookie.
|
||||||
# If the session was modified to be empty we remove the
|
# If the session is empty, return without setting the cookie.
|
||||||
# whole cookie.
|
|
||||||
if not session:
|
if not session:
|
||||||
if session.modified:
|
if session.modified:
|
||||||
response.delete_cookie(app.session_cookie_name,
|
response.delete_cookie(
|
||||||
domain=domain, path=path)
|
app.session_cookie_name,
|
||||||
|
domain=domain,
|
||||||
|
path=path
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Modification case. There are upsides and downsides to
|
# Add a "Vary: Cookie" header if the session was accessed at all.
|
||||||
# emitting a set-cookie header each request. The behavior
|
if session.accessed:
|
||||||
# is controlled by the :meth:`should_set_cookie` method
|
response.headers.add('Vary', 'Cookie')
|
||||||
# which performs a quick check to figure out if the cookie
|
|
||||||
# should be set or not. This is controlled by the
|
|
||||||
# SESSION_REFRESH_EACH_REQUEST config flag as well as
|
|
||||||
# the permanent flag on the session itself.
|
|
||||||
if not self.should_set_cookie(app, session):
|
if not self.should_set_cookie(app, session):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -394,6 +414,12 @@ class SecureCookieSessionInterface(SessionInterface):
|
||||||
secure = self.get_cookie_secure(app)
|
secure = self.get_cookie_secure(app)
|
||||||
expires = self.get_expiration_time(app, session)
|
expires = self.get_expiration_time(app, session)
|
||||||
val = self.get_signing_serializer(app).dumps(dict(session))
|
val = self.get_signing_serializer(app).dumps(dict(session))
|
||||||
response.set_cookie(app.session_cookie_name, val,
|
response.set_cookie(
|
||||||
expires=expires, httponly=httponly,
|
app.session_cookie_name,
|
||||||
domain=domain, path=path, secure=secure)
|
val,
|
||||||
|
expires=expires,
|
||||||
|
httponly=httponly,
|
||||||
|
domain=domain,
|
||||||
|
path=path,
|
||||||
|
secure=secure
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -529,6 +529,48 @@ def test_session_cookie_setting():
|
||||||
run_test(expect_header=False)
|
run_test(expect_header=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_vary_cookie():
|
||||||
|
app = flask.Flask(__name__)
|
||||||
|
app.secret_key = 'testkey'
|
||||||
|
|
||||||
|
@app.route('/set')
|
||||||
|
def set_session():
|
||||||
|
flask.session['test'] = 'test'
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@app.route('/get')
|
||||||
|
def get():
|
||||||
|
return flask.session.get('test')
|
||||||
|
|
||||||
|
@app.route('/getitem')
|
||||||
|
def getitem():
|
||||||
|
return flask.session['test']
|
||||||
|
|
||||||
|
@app.route('/setdefault')
|
||||||
|
def setdefault():
|
||||||
|
return flask.session.setdefault('test', 'default')
|
||||||
|
|
||||||
|
@app.route('/no-vary-header')
|
||||||
|
def no_vary_header():
|
||||||
|
return ''
|
||||||
|
|
||||||
|
c = app.test_client()
|
||||||
|
|
||||||
|
def expect(path, header=True):
|
||||||
|
rv = c.get(path)
|
||||||
|
|
||||||
|
if header:
|
||||||
|
assert rv.headers['Vary'] == 'Cookie'
|
||||||
|
else:
|
||||||
|
assert 'Vary' not in rv.headers
|
||||||
|
|
||||||
|
expect('/set')
|
||||||
|
expect('/get')
|
||||||
|
expect('/getitem')
|
||||||
|
expect('/setdefault')
|
||||||
|
expect('/no-vary-header', False)
|
||||||
|
|
||||||
|
|
||||||
def test_flashes():
|
def test_flashes():
|
||||||
app = flask.Flask(__name__)
|
app = flask.Flask(__name__)
|
||||||
app.secret_key = 'testkey'
|
app.secret_key = 'testkey'
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue