111 lines
4.0 KiB
Python
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")
|