diff options
Diffstat (limited to 'server/user_session.py')
-rw-r--r-- | server/user_session.py | 122 |
1 files changed, 122 insertions, 0 deletions
diff --git a/server/user_session.py b/server/user_session.py new file mode 100644 index 0000000..e418f8d --- /dev/null +++ b/server/user_session.py @@ -0,0 +1,122 @@ +# 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 |