From d1d835c02302884b2db1cab099b3ea6a84f41d32 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 30 Jul 2013 16:43:54 +0200 Subject: [PATCH] Added SESSION_REFRESH_EACH_REQUEST config option. This also changes how sessions are being refreshed. With the new behavior set-cookie is only emitted if the session is modified or if the session is permanent. Permanent sessions can be set to not refresh automatically through the SESSION_REFRESH_EACH_REQUEST config key. This fixes #798. --- CHANGES | 7 +++++++ docs/config.rst | 12 +++++++++++ flask/app.py | 1 + flask/sessions.py | 33 ++++++++++++++++++++++++++++++ flask/testsuite/basic.py | 43 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 96 insertions(+) diff --git a/CHANGES b/CHANGES index fe3e2235..912c6aaf 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,13 @@ Version 1.0 (release date to be announced, codename to be selected) +- Added ``SESSION_REFRESH_EACH_REQUEST`` config key that controls the + set-cookie behavior. If set to `True` a permanent session will be + refreshed each request and get their lifetime extended, if set to + `False` it will only be modified if the session actually modifies. + Non permanent sessions are not affected by this and will always + expire if the browser window closes. + Version 0.10.2 -------------- diff --git a/docs/config.rst b/docs/config.rst index ced2ad82..1bc46afa 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -88,6 +88,15 @@ The following configuration values are used internally by Flask: :class:`datetime.timedelta` object. Starting with Flask 0.8 this can also be an integer representing seconds. +``SESSION_REFRESH_EACH_REQUEST`` this flag controls how permanent + sessions are refresh. If set to `True` + (which is the default) then the cookie + is refreshed each request which + automatically bumps the lifetime. If + set to `False` a `set-cookie` header is + only sent if the session is modified. + Non permanent sessions are not affected + by this. ``USE_X_SENDFILE`` enable/disable x-sendfile ``LOGGER_NAME`` the name of the logger ``SERVER_NAME`` the name and port number of the server. @@ -210,6 +219,9 @@ The following configuration values are used internally by Flask: .. versionadded:: 0.10 ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_PRETTYPRINT_REGULAR`` +.. versionadded:: 1.0 + ``SESSION_REFRESH_EACH_REQUEST`` + Configuring from Files ---------------------- diff --git a/flask/app.py b/flask/app.py index addc40b4..652c1809 100644 --- a/flask/app.py +++ b/flask/app.py @@ -285,6 +285,7 @@ class Flask(_PackageBoundObject): 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, + 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': 12 * 60 * 60, # 12 hours 'TRAP_BAD_REQUEST_ERRORS': False, diff --git a/flask/sessions.py b/flask/sessions.py index 3246eb83..d6b7e5ae 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -252,6 +252,24 @@ class SessionInterface(object): if session.permanent: return datetime.utcnow() + app.permanent_session_lifetime + def should_set_cookie(self, app, session): + """Indicates weather a cookie should be set now or not. This is + used by session backends to figure out if they should emit a + set-cookie header or not. The default behavior is controlled by + the ``SESSION_REFRESH_EACH_REQUEST`` config variable. If + it's set to `False` then a cookie is only set if the session is + modified, if set to `True` it's always set if the session is + permanent. + + This check is usually skipped if sessions get deleted. + + .. versionadded:: 1.0 + """ + if session.modified: + return True + save_each = app.config['SESSION_REFRESH_EACH_REQUEST'] + return save_each and session.permanent + def open_session(self, app, request): """This method has to be implemented and must either return `None` in case the loading failed because of a configuration error or an @@ -315,11 +333,26 @@ class SecureCookieSessionInterface(SessionInterface): def save_session(self, app, session, response): domain = self.get_cookie_domain(app) path = self.get_cookie_path(app) + + # Delete case. If there is no session we bail early. + # If the session was modified to be empty we remove the + # whole cookie. if not session: if session.modified: response.delete_cookie(app.session_cookie_name, domain=domain, path=path) return + + # Modification case. There are upsides and downsides to + # emitting a set-cookie header each request. The behavior + # is controlled by the :meth:`should_set_cookie` method + # 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): + return + httponly = self.get_cookie_httponly(app) secure = self.get_cookie_secure(app) expires = self.get_expiration_time(app, session) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 51fd46f2..0fffd4ee 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -345,6 +345,49 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(type(rv['b']), bytes) self.assert_equal(rv['t'], (1, 2, 3)) + def test_session_cookie_setting(self): + app = flask.Flask(__name__) + app.testing = True + app.secret_key = 'dev key' + is_permanent = True + + @app.route('/bump') + def bump(): + rv = flask.session['foo'] = flask.session.get('foo', 0) + 1 + flask.session.permanent = is_permanent + return str(rv) + + @app.route('/read') + def read(): + return str(flask.session.get('foo', 0)) + + def run_test(expect_header): + with app.test_client() as c: + self.assert_equal(c.get('/bump').data, '1') + self.assert_equal(c.get('/bump').data, '2') + self.assert_equal(c.get('/bump').data, '3') + + rv = c.get('/read') + set_cookie = rv.headers.get('set-cookie') + self.assert_equal(set_cookie is not None, expect_header) + self.assert_equal(rv.data, '3') + + is_permanent = True + app.config['SESSION_REFRESH_EACH_REQUEST'] = True + run_test(expect_header=True) + + is_permanent = True + app.config['SESSION_REFRESH_EACH_REQUEST'] = False + run_test(expect_header=False) + + is_permanent = False + app.config['SESSION_REFRESH_EACH_REQUEST'] = True + run_test(expect_header=False) + + is_permanent = False + app.config['SESSION_REFRESH_EACH_REQUEST'] = False + run_test(expect_header=False) + def test_flashes(self): app = flask.Flask(__name__) app.secret_key = 'testkey'