summaryrefslogtreecommitdiff
path: root/server/user_session.py
diff options
context:
space:
mode:
Diffstat (limited to 'server/user_session.py')
-rw-r--r--server/user_session.py122
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