# coding=utf-8 import uuid import threading # multiprocessing.managers.BaseManager uses threading to serve incoming requests import hashlib import base64 import random from . import prolog_session import db from errors.session import NoSuchSession, AuthenticationFailed __all__ = ['get_session_by_id', 'get_or_create_session', 'UserSession'] sessions = {} # maps session IDs to session objects module_access_lock = threading.Lock() # use this lock to access the sessions dictionary class UserSession(object): """Abstracts a user session. Only public methods are available to the outside world due to the use of multiprocessing managers. Therefore prefix any private methods with an underscore (_). No properties are accessible; use getters and setters instead. Values are passed by value instead of by reference (deep copy!). """ def __init__(self, uid, username): self._access_lock = threading.Lock() self.sid = uuid.uuid4().hex self.uid = uid self.username = username self.prolog_session = None def destroy(self): """Destroys the session.""" with self._access_lock: with module_access_lock: del sessions[self.sid] if self.prolog_session is not None: self.prolog_session.end() self.prolog_session = None # TODO: add any cleanups as features are added! def get_sid(self): return self.sid def get_uid(self): return self.uid def get_prolog(self): with self._access_lock: if self.prolog_session is None: self.prolog_session = prolog_session.PrologSession() # lazy init return self.prolog_session def __del__(self): # no locking needed if GC is removing us, as there cannot be any concurrent access by definition if hasattr(self, 'prolog_session') and (self.prolog_session is not None): self.prolog_session.end() self.prolog_session = None # TODO: add any cleanups as features are added! def get_session_by_id(sid): with module_access_lock: s = sessions.get(sid, None) if s is None: raise NoSuchSession('There is no session with SID {}'.format(sid)) return s def get_or_create_session(uid, username, sid=None): with module_access_lock: if sid is not None: s = sessions.get(sid) if s is not None: return s s = UserSession(uid, username) sessions[s.sid] = s return s def authenticate_and_create_session(username, password): conn = db.get_connection() try: cur = conn.cursor() try: cur.execute('select id, password from codeq_user where username = %s', (username,)) row = cur.fetchone() if row is None: raise AuthenticationFailed('No such user: {}'.format(username)) if verify_password(password, row[1]): return get_or_create_session(row[0], username) raise AuthenticationFailed('Password mismatch') finally: cur.close() finally: conn.commit() db.return_connection(conn) def verify_password(plain_password, encrypted_password): elts = encrypted_password.split('$') if len(elts) != 4: return False if elts[0] != 'pkbdf2_sha256': return False try: rounds = int(elts[1]) except: return False enc = hashlib.pbkdf2_hmac('sha256', plain_password.encode('utf-8'), elts[2].encode('utf-8'), rounds) return base64.b64encode(enc).decode('utf-8') == elts[3] _salt_chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' _salt_chars_len = len(_salt_chars) def encrypt_password(plain_password): rounds = 20000 chosen_chars = [] for i in range(0, 12): chosen_chars.append(_salt_chars[random.randrange(0, _salt_chars_len)]) salt = ''.join(chosen_chars) enc = hashlib.pbkdf2_hmac('sha256', plain_password.encode('utf-8'), salt.encode('utf-8'), rounds) return '{0}${1}${2}${3}'.format('pkbdf2_sha256', rounds, salt, base64.b64encode(enc).decode('utf-8')) random.seed() # TODO: add a session timeout timer