From 4cf08a3fd2b5805e6792c0d1a228362575341804 Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Wed, 22 Jul 2026 03:30:30 +0200 Subject: [PATCH] Allow web sessions in direct HTTP mode --- src/www/bongo-web.py.in | 16 +++++++---- src/www/bongo_web/session.py | 23 ++++++++++----- src/www/tests/test_session.py | 53 +++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 src/www/tests/test_session.py diff --git a/src/www/bongo-web.py.in b/src/www/bongo-web.py.in index 2078f4b..0cf7c3f 100644 --- a/src/www/bongo-web.py.in +++ b/src/www/bongo-web.py.in @@ -59,7 +59,7 @@ from bongo_web.card_store import (CardNotFound, CardPathError, from bongo_web.dav import (card_multistatus, event_multistatus, multistatus, options_headers, parse_card_report, parse_report, requested_properties) -from bongo_web.session import (COOKIE_NAME, SessionStore, +from bongo_web.session import (SessionStore, cookie_name, expired_session_cookie, session_cookie) from bongo_web.tasks import TaskStore from bongo_web.identities import IdentityStore @@ -375,9 +375,12 @@ class BongoWebHandler(BaseHTTPRequestHandler): cookies = SimpleCookie(self.headers.get("Cookie", "")) except Exception: return None - morsel = cookies.get(COOKIE_NAME) + morsel = cookies.get(cookie_name(self._secure_cookies())) return morsel.value if morsel else None + def _secure_cookies(self): + return bool(self.server.config.get("external_https", False)) + def _session(self): return self.server.sessions.get(self._session_token()) @@ -743,7 +746,8 @@ class BongoWebHandler(BaseHTTPRequestHandler): token, csrf_token = self.server.sessions.create(user, store_cookie) self._json(HTTPStatus.CREATED, {"user": user, "csrf_token": csrf_token}, - {"Set-Cookie": session_cookie(token, lifetime), + {"Set-Cookie": session_cookie( + token, lifetime, self._secure_cookies()), "Vary": "Cookie"}) return user = str(data.get("user", "")).strip() @@ -789,7 +793,8 @@ class BongoWebHandler(BaseHTTPRequestHandler): token, csrf_token = self.server.sessions.create(user, store_cookie) self._json(HTTPStatus.CREATED, {"user": user, "csrf_token": csrf_token}, - {"Set-Cookie": session_cookie(token, lifetime), + {"Set-Cookie": session_cookie( + token, lifetime, self._secure_cookies()), "Vary": "Cookie"}) def _security_session(self): @@ -1113,7 +1118,8 @@ class BongoWebHandler(BaseHTTPRequestHandler): self.server.sessions.delete(token) revoke(session["user"], session["store_cookie"]) self._empty(HTTPStatus.NO_CONTENT, - {"Set-Cookie": expired_session_cookie(), "Vary": "Cookie"}) + {"Set-Cookie": expired_session_cookie( + self._secure_cookies()), "Vary": "Cookie"}) def do_PUT(self): path = self._path() diff --git a/src/www/bongo_web/session.py b/src/www/bongo_web/session.py index 5fb41b3..2d6ca66 100644 --- a/src/www/bongo_web/session.py +++ b/src/www/bongo_web/session.py @@ -33,7 +33,10 @@ import sqlite3 import time -COOKIE_NAME = "__Host-bongo-session" +SECURE_COOKIE_NAME = "__Host-bongo-session" +INSECURE_COOKIE_NAME = "bongo-session" +# Retain the historic public name for callers which assume the secure mode. +COOKIE_NAME = SECURE_COOKIE_NAME class SessionStore: @@ -106,11 +109,17 @@ class SessionStore: (self._hash(token),)) -def session_cookie(token, max_age): - return (f"{COOKIE_NAME}={token}; Path=/; Max-Age={int(max_age)}; " - "Secure; HttpOnly; SameSite=Strict") +def cookie_name(secure=True): + return SECURE_COOKIE_NAME if secure else INSECURE_COOKIE_NAME -def expired_session_cookie(): - return (f"{COOKIE_NAME}=; Path=/; Max-Age=0; " - "Secure; HttpOnly; SameSite=Strict") +def session_cookie(token, max_age, secure=True): + secure_attribute = " Secure;" if secure else "" + return (f"{cookie_name(secure)}={token}; Path=/; Max-Age={int(max_age)};" + f"{secure_attribute} HttpOnly; SameSite=Strict") + + +def expired_session_cookie(secure=True): + secure_attribute = " Secure;" if secure else "" + return (f"{cookie_name(secure)}=; Path=/; Max-Age=0;" + f"{secure_attribute} HttpOnly; SameSite=Strict") diff --git a/src/www/tests/test_session.py b/src/www/tests/test_session.py new file mode 100644 index 0000000..1bc4cc7 --- /dev/null +++ b/src/www/tests/test_session.py @@ -0,0 +1,53 @@ +# /**************************************************************************** +# * +# * Copyright (c) 2001 Novell, Inc. All Rights Reserved. +# * +# * This program is free software; you can redistribute it and/or +# * modify it under the terms of version 2 of the GNU General Public License +# * as published by the Free Software Foundation. +# * +# * This program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# * You should have received a copy of the GNU General Public License +# * along with this program; if not, contact Novell, Inc. +# * +# * To contact Novell about this file by physical or electronic mail, you +# * may find current contact information at www.novell.com. +# * +# ****************************************************************************/ + +import unittest + +from bongo_web.session import (expired_session_cookie, session_cookie) + + +class SessionCookieTests(unittest.TestCase): + def test_https_cookie_uses_host_prefix_and_secure_attribute(self): + cookie = session_cookie("token", 3600) + + self.assertTrue(cookie.startswith("__Host-bongo-session=token;")) + self.assertIn(" Secure;", cookie) + self.assertIn(" HttpOnly;", cookie) + self.assertIn(" SameSite=Strict", cookie) + + def test_direct_http_cookie_has_separate_name(self): + cookie = session_cookie("token", 3600, secure=False) + + self.assertTrue(cookie.startswith("bongo-session=token;")) + self.assertNotIn(" Secure;", cookie) + self.assertIn(" HttpOnly;", cookie) + self.assertIn(" SameSite=Strict", cookie) + + def test_expired_cookie_matches_transport_mode(self): + self.assertTrue(expired_session_cookie().startswith( + "__Host-bongo-session=;")) + insecure = expired_session_cookie(secure=False) + self.assertTrue(insecure.startswith("bongo-session=;")) + self.assertNotIn(" Secure;", insecure) + + +if __name__ == "__main__": + unittest.main()