# coding=utf-8 import uuid import threading # multiprocessing.managers.BaseManager uses threading to serve incoming requests import hashlib import base64 import random import db import server from errors.session import NoSuchSession, AuthenticationFailed, PasswordChangeFailed, UserExists, SignupFailed import psycopg2.extras import datetime __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, settings): self._access_lock = threading.Lock() self.sid = uuid.uuid4().hex self.uid = uid self.username = username self._lang_session = None self.settings = settings def destroy(self): """Destroys the session.""" with self._access_lock: with module_access_lock: del sessions[self.sid] if self._lang_session is not None: self._lang_session.destroy() self._lang_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_settings(self): return self.settings def update_settings(self, newSettings): self.settings.update(newSettings) def write_settings_to_db(self): conn = db.get_connection() try: cur = conn.cursor() try: cur.execute("UPDATE codeq_user SET gui_lang='" + self.settings['lang'] + "' WHERE id="+str(self.uid)) finally: cur.close() finally: conn.commit() db.return_connection(conn) def load_language_session(self, problem_id): with self._access_lock: if self._lang_session is not None: self._lang_session.destroy() self._lang_session = None conn = db.get_connection() try: cur = conn.cursor() try: cur.execute("select l.identifier, g.identifier, p.identifier 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 p.id = %s", (problem_id,)) row = cur.fetchone() if not row: return None language_identifier = row[0] group_identifier = row[1] problem_identifier = row[2] handler = server.language_session_handlers.get(language_identifier) if not handler: return None self._lang_session = handler(self, problem_id, language_identifier, group_identifier, problem_identifier) return self._lang_session finally: cur.close() finally: conn.commit() db.return_connection(conn) def end_language_session(self): with self._access_lock: if self._lang_session is not None: self._lang_session.destroy() self._lang_session = None def current_language_session(self): with self._access_lock: return self._lang_session def get_problem_data(self, language, problem_group, problem): mod = server.problems.load_problem(language, problem_group, problem, 'sl') mod_language = server.problems.load_language(language, 'sl') # Get generic and problem-specific hints. hint = dict(mod_language.hint) hint.update(mod.hint) plan = mod.plan if hasattr(mod, 'plan') else [] 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': hint, 'plan': plan} } 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, 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 change_password(self, password): conn = db.get_connection() try: cur = conn.cursor() try: cur.execute('update codeq_user set password = %s where id = %s', (encrypt_password(password), self.uid,)) affected = cur.rowcount if affected is None: raise PasswordChangeFailed('Password change failed') finally: cur.close() finally: conn.commit() db.return_connection(conn) def send(self, json_obj): """Sends a message to the user. This method may be used only for messages that are not replies to requests. For replies use the reply() method on the Request object. :param json_obj: a dict representing the json message :return: None """ json_obj['sid'] = self.sid server.handlers.send(None, self.sid, json_obj) def __del__(self): # no locking needed if GC is removing us, as there cannot be any concurrent access by definition if hasattr(self, '_lang_session') and (self._lang_session is not None): self._lang_session.destroy() self._lang_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, lan=None): with module_access_lock: if sid is not None: s = sessions.get(sid) if s is not None: return s settings = {} if lan is not None: #settings['lan'] or settings['lang'] ???? settings['lan'] = lan s = UserSession(uid, username, settings) sessions[s.sid] = s return s def authenticate_and_create_session(username, password): now = datetime.datetime.now() conn = db.get_connection() try: cur = conn.cursor() try: cur.execute('select id, password, gui_lang 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]): cur.execute('update codeq_user set last_login = %s where id = %s', (str(now), row[0],)) return get_or_create_session(row[0], username, None, row[2]) raise AuthenticationFailed('Password mismatch') finally: cur.close() finally: conn.commit() db.return_connection(conn) def signup(username, password): now = datetime.datetime.now() conn = db.get_connection() try: cur = conn.cursor() try: cur.execute('select id from codeq_user where username = %s', (username,)) row = cur.fetchone() if row: raise UserExists('User exists') else: cur.execute('insert into codeq_user (username, password, name, email, is_admin, is_active, date_joined, last_login, gui_lang) values (%s, %s, %s, %s, %s, %s, %s, %s, %s) returning id', (username, encrypt_password(password),None ,'', False, True, str(now), str(now), None )) affected = cur.rowcount if affected is None: raise SignupFailed('Signn up failed') 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