# 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 from . import problems import db from errors.session import NoSuchSession, AuthenticationFailed import psycopg2.extras __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 get_problem_data(self, language, problem_group, problem): mod = problems.load_problem(language, problem_group, problem, 'en') conn = db.get_connection() try: cur = conn.cursor() try: cur.execute("select l.id, l.name, g.id, g.name, p.id, p.name from problem p inner join language l on l.id = p.language_id inner join problem_group g on g.id = p.problem_group_id where l.identifier = %s and g.identifier = %s and p.identifier = %s", (language, problem_group, problem)) row = cur.fetchone() problem_id = row[4] result = { 'language': {'id': row[0], 'identifier': language, 'name': row[1]}, 'problem_group': {'id': row[2], 'identifier': problem_group, 'name': row[3]}, 'problem': {'id': problem_id, 'identifier': problem, 'name': row[5], 'slug': mod.slug, 'description': mod.description, 'hint': mod.hint} } cur.execute("select content from solution where problem_id = %s and codeq_user_id = %s", (problem_id, self.uid)) row = cur.fetchone() if row: result['solution'] = row[0] else: result['solution'] = '' return result finally: cur.close() finally: conn.commit() db.return_connection(conn) def update_solution(self, problem_id, trace, solution): if (trace is None) and (solution is None): return conn = db.get_connection() try: cur = conn.cursor() try: # TODO: convert to upsert with postgresql 9.5 to eliminate the small window where it's possible for more than one concurrent insert to execute cur.execute('select id, trace, solution content from solution where codeq_user_id = %s and problem_id = %s for update', (self.uid, problem_id)) row = cur.fetchone() if row: if row[1]: new_trace = row[1] if trace: new_trace.extend(trace) else: new_trace = trace new_solution = row[2] if solution is None else solution cur.execute('update solution set content = %s, trace = %s where id = %s', (new_solution, psycopg2.extras.Json(new_trace), row[0])) else: # this is the first entry cur.execute('insert into solution (done, content, problem_id, codeq_user_id, trace) values (%s, %s, %s, %s, %s)', (False, solution, problem_id, self.uid, psycopg2.extras.Json(trace))) finally: cur.close() conn.commit() except: conn.rollback() raise finally: db.return_connection(conn) 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] != 'pbkdf2_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('pbkdf2_sha256', rounds, salt, base64.b64encode(enc).decode('utf-8')) random.seed() # TODO: add a session timeout timer