Browse Source

Added a filesystem pickle based server-side session management

Roland Haroutiounian 7 years ago
parent
commit
ac9fc07820

+ 0
- 0
lodel/session/__init__.py View File


+ 0
- 0
lodel/session/lodel_filesystem_session/__init__.py View File


+ 48
- 0
lodel/session/lodel_filesystem_session/lodel_filesystem_session.py View File

@@ -0,0 +1,48 @@
1
+from collections import MutableMapping
2
+from flask.sessions import SessionMixin
3
+import os
4
+from pickle import loads, dumps, UnpicklingError
5
+
6
+
7
+class LodelFileSystemSession(MutableMapping, SessionMixin):
8
+    
9
+    def __init__(self, directory, sid, *args, **kwargs):
10
+        self.path = os.path.join(directory, sid)
11
+        self.sid = sid
12
+        self.read()
13
+    
14
+    def __getitem__(self, key):
15
+        self.read()
16
+        return self.data[key]
17
+    
18
+    def __setitem__(self, key, value):
19
+        self.data[key] = value
20
+        self.save()
21
+    
22
+    def __delitem__(self, key):
23
+        del self.data[key]
24
+        self.save()
25
+    
26
+    def __iter__(self):
27
+        return iter(self.data)
28
+    
29
+    def __len__(self):
30
+        return len(self.data)
31
+    
32
+    ## @brief Loads a session from a file
33
+    # @todo Specify dedicated behaviour for each exception case
34
+    def read(self):
35
+        try:
36
+            with open(self.path, 'rb') as session_file:
37
+                self.data = loads(session_file.read())
38
+        except FileNotFoundError:
39
+            self.data = {}
40
+        except (ValueError, EOFError, UnpicklingError):
41
+            self.data = {}
42
+    
43
+    ## @brief Dumps a session to a file
44
+    def save(self):
45
+        new_name = '{}.new'.format(self.path)
46
+        with open(new_name, 'wb') as session_file:
47
+            session_file.write(dumps(self.data))
48
+        os.rename(new_name, self.path)

+ 76
- 0
lodel/session/lodel_filesystem_session/lodel_filesystem_session_interface.py View File

@@ -0,0 +1,76 @@
1
+from flask.sessions import SessionInterface
2
+import os
3
+import re
4
+from lodel.session.lodel_filesystem_session.lodel_filesystem_session import LodelFileSystemSession
5
+from contextlib import suppress
6
+import binascii
7
+
8
+class LodelFileSystemSessionInterface(SessionInterface):
9
+    
10
+    __sessions = dict()
11
+    
12
+    def __init__(self, directory):
13
+        self.directory = os.path.abspath(directory)
14
+        os.makedirs(self.directory, exist_ok=True)
15
+    
16
+    def open_session(self, app, request):
17
+        self.filename_template = app.config['lodel.filesystem_sessions']['filename_template']
18
+        self.session_tokensize = int(app.config['lodel.sessions']['tokensize'])
19
+        sid = request.cookies.get(app.session_cookie_name) or self.generate_token() #or '{}-{}'.format(uuid1(), os.getpid())
20
+        if LodelFileSystemSessionInterface.__sessions.get(sid, None) is None:
21
+            LodelFileSystemSessionInterface.__sessions[sid] = self.generate_file_path(sid, self.filename_template)
22
+        return LodelFileSystemSession(self.directory, sid)
23
+    
24
+    def save_session(self, app, session, response):
25
+        domain = self.get_cookie_domain(app)
26
+        
27
+        if not session:
28
+            with suppress(FileNotFoundError):
29
+                os.unlink(session.path)
30
+                del(LodelFileSystemSessionInterface.__sessions[session.sid])
31
+            response.delete_cookie(app.session_cookie_name, domain=domain)
32
+            return
33
+        
34
+        cookie_exp = self.get_expiration_time(app, session)
35
+        response.set_cookie(app.session_cookie_name, session.sid, expires=cookie_exp, httponly=False,
36
+                            domain=domain)
37
+
38
+    def generate_file_path(self, sid, filename_template):
39
+        return os.path.abspath(os.path.join(self.directory, filename_template) % sid)
40
+    
41
+    ## @brief Retrieves the token from the file system
42
+    #
43
+    # @param filepath str
44
+    # @return str|None : returns the token or None if no token was found
45
+    def get_token_from_filepath(self, filepath):
46
+        token_regex = re.compile(os.path.abspath(os.path.join(self.directory, self.filename_template % '(?P<token>.*)')))
47
+        token_search_result = token_regex.match(filepath)
48
+        if token_search_result is not None:
49
+            return token_search_result.groupdict()['token']
50
+        return None
51
+    
52
+    ## @brief generates a new session token
53
+    #
54
+    # @return str
55
+    #
56
+    # @remarks There is no valid reason for checking the generated token uniqueness:
57
+    #        - checking for uniqueness is slow ;
58
+    #        - keeping a dict with a few thousand keys of hundred bytes also is
59
+    #            memory expensive ;
60
+    #        - should the system get distributed while sharing session storage, there
61
+    #            would be no reasonable way to efficiently check for uniqueness ;
62
+    #        - sessions do have a really short life span, drastically reducing
63
+    #            even more an already close to inexistent risk of collision. A 64 bits
64
+    #            id would perfectly do the job, or to be really cautious, a 128 bits
65
+    #            one (actual size of UUIDs) ;
66
+    #        - if we are still willing to ensure uniqueness, then simply salt it
67
+    #            with a counter, or a timestamp, and hash the whole thing with a 
68
+    #            cryptographically secured method such as sha-2 if we are paranoids
69
+    #            and trying to avoid what will never happen, ever ;
70
+    #        - sure, two hexadecimal characters is one byte long. Simply go for 
71
+    #            bit length, not chars length.
72
+    def generate_token(self):
73
+        token = binascii.hexlify(os.urandom(self.session_tokensize))
74
+        if LodelFileSystemSessionInterface.__sessions.get(token, None) is not None:
75
+            token = self.generate_token()
76
+        return token.decode('utf-8')

+ 8
- 2
lodel_app/__init__.py View File

@@ -3,6 +3,7 @@ import os
3 3
 
4 4
 from lodel.lodelsites.utils import register_lodelsites
5 5
 from lodel.utils.ini_files import ini_to_dict
6
+from lodel.session.lodel_filesystem_session.lodel_filesystem_session_interface import LodelFileSystemSessionInterface
6 7
 
7 8
 
8 9
 lodel_app = Flask(__name__)
@@ -10,10 +11,15 @@ lodel_app.config['DEBUG'] = True
10 11
 lodel_app.config['sites'] = {}
11 12
 lodel_app.config.update(ini_to_dict(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.ini')))
12 13
 
14
+lodel_app.session_cookie_name = lodel_app.config['lodel.sessions']['session_cookie_name']
15
+lodel_app.permanent_session_lifetime = int(lodel_app.config['lodel.sessions']['session_lifetime'])
16
+lodel_app.session_interface = LodelFileSystemSessionInterface(lodel_app.config['lodel.filesystem_sessions']['path'])
17
+
18
+# Main Hooks
19
+from .hooks import *
20
+
13 21
 # Lodelsites
14 22
 register_lodelsites(lodel_app)
15 23
 
16
-print(lodel_app.config)
17
-
18 24
 # Main views
19 25
 from .views import *

+ 9
- 0
lodel_app/config.ini View File

@@ -19,3 +19,12 @@ groups =
19 19
 emfile = examples/em_test.pickle
20 20
 dyncode = leapi_dyncode.py
21 21
 editormode = True
22
+
23
+[lodel.sessions]
24
+session_cookie_name = lodel_session
25
+tokensize = 64
26
+session_lifetime = 500
27
+
28
+[lodel.filesystem_sessions]
29
+path = sessions
30
+filename_template = sess_%s

+ 6
- 0
lodel_app/hooks.py View File

@@ -0,0 +1,6 @@
1
+from . import lodel_app
2
+from flask.globals import session
3
+
4
+@lodel_app.before_request
5
+def make_session_permanent():
6
+    session.permanent = True

+ 1
- 1
run.py View File

@@ -2,4 +2,4 @@ from lodel_app import lodel_app
2 2
 
3 3
 
4 4
 if __name__ == "__main__":
5
-    lodel_app.run()
5
+    lodel_app.run()

Loading…
Cancel
Save