Files
bongo/src/www/bongo_web/session.py
T
2026-07-16 21:44:21 +02:00

111 lines
4.0 KiB
Python

# /****************************************************************************
# * <Novell-copyright>
# * 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.
# * </Novell-copyright>
# ****************************************************************************/
"""Opaque, server-side web sessions.
Only a hash of the browser token is stored. The Bongo Store cookie remains
server-side and the user's password is never retained after login.
"""
import hashlib
import os
import secrets
import sqlite3
import time
COOKIE_NAME = "__Host-bongo-session"
class SessionStore:
def __init__(self, path, lifetime=3600):
self.path = path
self.lifetime = int(lifetime)
directory = os.path.dirname(path)
os.makedirs(directory, mode=0o700, exist_ok=True)
os.chmod(directory, 0o700)
self._initialize()
def _connect(self):
connection = sqlite3.connect(self.path, timeout=5)
connection.execute("PRAGMA busy_timeout=5000")
connection.execute("PRAGMA journal_mode=WAL")
connection.execute("PRAGMA foreign_keys=ON")
return connection
def _initialize(self):
with self._connect() as database:
database.execute(
"CREATE TABLE IF NOT EXISTS sessions ("
"id_hash BLOB PRIMARY KEY, user TEXT NOT NULL, "
"store_cookie TEXT NOT NULL, csrf_token TEXT NOT NULL, "
"created INTEGER NOT NULL, expires INTEGER NOT NULL)")
database.execute(
"CREATE INDEX IF NOT EXISTS sessions_expiry ON sessions(expires)")
os.chmod(self.path, 0o600)
@staticmethod
def _hash(token):
return hashlib.sha256(token.encode("ascii")).digest()
def create(self, user, store_cookie):
token = secrets.token_urlsafe(32)
csrf_token = secrets.token_urlsafe(32)
now = int(time.time())
with self._connect() as database:
database.execute("DELETE FROM sessions WHERE expires <= ?", (now,))
database.execute(
"INSERT INTO sessions VALUES (?, ?, ?, ?, ?, ?)",
(self._hash(token), user, store_cookie, csrf_token,
now, now + self.lifetime))
return token, csrf_token
def get(self, token):
if not token:
return None
now = int(time.time())
with self._connect() as database:
row = database.execute(
"SELECT user, store_cookie, csrf_token, expires "
"FROM sessions WHERE id_hash = ? AND expires > ?",
(self._hash(token), now)).fetchone()
if row is None:
return None
return {"user": row[0], "store_cookie": row[1],
"csrf_token": row[2], "expires": row[3]}
def delete(self, token):
if not token:
return
with self._connect() as database:
database.execute("DELETE FROM sessions WHERE id_hash = ?",
(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 expired_session_cookie():
return (f"{COOKIE_NAME}=; Path=/; Max-Age=0; "
"Secure; HttpOnly; SameSite=Strict")