summaryrefslogtreecommitdiff
path: root/server/user_session.py
blob: b12d6f19ed63b4990777b97cf622458a66061cfc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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] != '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