Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sessions Middleware #448

Merged
merged 24 commits into from
Jul 14, 2022
Merged

Sessions Middleware #448

merged 24 commits into from
Jul 14, 2022

Conversation

dranikpg
Copy link
Member

@dranikpg dranikpg commented May 28, 2022

Sessions

This is the main implementation for #406 . I ended up focusing on backend storage only, making the design less flexible, but easier to use and implement.

What made it into this version

  • First and foremost, it allows associating data with a client. The session is created on first write and is automatically loaded and stored. At the simplest level it works just like a map.
    * Session keys are stored in cookies and are signed with a HMAC (Hash-based message authentication code), so custom keys and/or short keys are safe to use.
  • Data can be stored in-memory or on-disk with a default store. The on-disk store support expiration tracking and should be a simple all-round solution for simple projects. It's also easy to create a custom data store
  • Active sessions are stored in a common pool, so that two concurrent requests from a single client will share the session object. This keeps the data always synchronized in multithreaded applications and even allows locking the session for operations, where multiple values have to be changed "at once".
  • The session allows storing not only strings, but also primitives

What it looks like

// choose a storage kind for sessions (in-memory, json files, custom)
using Session = crow::SessionMiddleware<crow::FileStore>; 
crow::App<crow::CookieParser, Session> app {Session{crow::FileStore{"./sessions"}};

// get the session from a request
auto& session = app.get_context<Session>(req);

// use it like a map
auto views = session.get<int>("views");
string user_country = session.get("country", "US"); // get with fallback
session.set("ad_score", 10.0);

// atomic operations & locking
session.apply("views", [](int v){ return v + 1; }); // no page view will be skipped 
session.mutex().lock(); // guaranteed to be the same mutex for all threads handling the same client

How it works

++++++++++++++++++++++++++++++++++++++
|            client                  |
++++++++++++++++++++++++++++++++++++++
    ^ |
    | | <-- session cookie
    | v
++++++++++++++++++++++
| session middleware |
++++++++++++++++++++++
    |
    |
+++++++++++++++++            ++++++++++++++
| session cache | <------->  |    store   |
+++++++++++++++++     |      ++++++++++++++
    |                 |
    |                 |- load and save `CachedSession`s
    |
    |
    | <--- store shared_ptr to
    V 
+++++++++++++++++++++++
|  CachedSession      |        
|                     | <------ mw context on thread1
| - id                |           
| - entries           |
| - changed keys      |
| - mutex, ref count, | <------ mw context on thread2
|   etc.              | 
|                     |
+++++++++++++++++++++++

1. Cookies.
Each client stores its session id in the cookies. Each id is signed, so that it can be changed only by the server. The Session middleware reads the cookie in read_id and verifies the signature.

2. Session cache.
If a valid session id was found, the middleware looks in the cache for a CachedSession. The cache holds all currently active session objects. A CachedSession contains not only data, but also a mutex for locking, its id, a list of keys that changed since last load and some auxiliary values. Because of the cache, all concurrent requests share literally the same object, so no "data race" can happen and all modifications are instantly visible to all requests.

3. Stores.
If no CachedSession was found, it has to be loaded from the store and put into the cache. First the SessionMiddleware checks for validity with contains() and then calls load() on the store. As soon as there are no requests referring to a CachedSession, it is released back into the store and saved.

4. Fresh sessions.
Sessions are initialized on first write (no need to expiclitly create one for the user) and have no id until they are persisted. If no special id was requested (via preset_id), a random id will be generated and stored in the clients cookies.

Its imporant to understand, that session initialization does not work with concurrent requests, because the client receives it cookies only with the response. Only requests that already have a valid session will share one session object.

5. Expiration.
It'd be silly to store all data forever. That's why the FileStore deletes files after X seconds (by default a whole month). When the file is gone, the session will no longer be valid and a new one will be issued.
Cookies are ought to expire as well. By default they're valid for one month.

The expiration stamps can be prolonged by calling refresh_expiration() on a session object. This will make the middleware issue a new cookie and the store to postpone its deletions.

6. Multi-typed map.
Session data is represented as a unordered_map<string, mutli_value>. A multi_value allows storing strings, integers, doubles and booleans. Because dealing with multiple integer types would be cumbersome, it "promotes" all integer values to int64_t. Besides, it has helpers to convert the value from/to json and represent it as a string.
On C++17 and above multi_value is implemented on top of std::variant, otherwise it uses json::wvalue with some quirks for reading the value back.

7. Default stores. The InMemoryStore is really simple, it stores all entries just in a hashmap. FileStore is a little bit more complex. It stores data in json files and keeps track of soon-to-expire keys. Generally, a custom store can be implemented quite easily.

Current issues

  • Expiration tracking Added general purpose ExpirationTracker that is used by the FileStore
  • There is no alternative for the C++17 variant (as Crow is now deboostified) for C++14/11 Solved with a crooked wrapper around json::wvalue
  • Exception handling during load/store, what to do with crashes in handlers

@The-EDev The-EDev linked an issue May 28, 2022 that may be closed by this pull request
4 tasks
@crow-clang-format
Copy link

--- include/crow/middlewares/cookie_parser.h	(before formatting)
+++ include/crow/middlewares/cookie_parser.h	(after formatting)
@@ -50,7 +50,9 @@
                 value_ = std::forward<U>(value);
             }
 
-            Cookie(const std::string& key): Cookie() {
+            Cookie(const std::string& key):
+              Cookie()
+            {
                 key_ = key;
             }
 
@@ -94,12 +96,14 @@
                 return ss.str();
             }
 
-            const std::string& name() {
+            const std::string& name()
+            {
                 return key_;
             }
 
             template<typename U>
-            void value(U&& value) {
+            void value(U&& value)
+            {
                 value_ = std::forward<U>(value);
             }
 
@@ -216,7 +220,8 @@
                 return cookies_to_add.back();
             }
 
-            void set_cookie(Cookie cookie) {
+            void set_cookie(Cookie cookie)
+            {
                 cookies_to_add.push_back(std::move(cookie));
             }
 

@dranikpg
Copy link
Member Author

I'd say this is ready to have a look at and experiment with. You can also check the examples or run the small demo I left in gitter.

@dranikpg dranikpg marked this pull request as ready for review June 25, 2022 14:19
@The-EDev
Copy link
Member

@dranikpg I should mention regarding the unittest you added, @luca-schlecker is working on separating the unit tests into multiple source files so I'm just letting you know in case his PR gets merged before this one.

@The-EDev The-EDev changed the title WIP Sessions Sessions MIddleware Jun 27, 2022
@The-EDev The-EDev changed the title Sessions MIddleware Sessions Middleware Jun 27, 2022
Copy link
Member

@The-EDev The-EDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have much other than the comments, everything seems alright. Although I would like a second opinion since the added code is rather large.

include/crow/json.h Show resolved Hide resolved
tests/unittest.cpp Outdated Show resolved Hide resolved
include/crow/middlewares/session.h Outdated Show resolved Hide resolved
examples/middlewares/example_session.cpp Outdated Show resolved Hide resolved
include/crow/middlewares/session.h Outdated Show resolved Hide resolved
include/crow/middlewares/session.h Outdated Show resolved Hide resolved
include/crow/middlewares/session.h Show resolved Hide resolved
@dranikpg
Copy link
Member Author

dranikpg commented Jul 4, 2022

I've stripped out custom keys. Even if this feature might be handy in certain cases, using it correctly is quite tricky. Supporting it really-really securely would require adding another secure hasing algorithm more powerfult than sha1.
I've also added some more tests, specifically locking.

@crow-clang-format
Copy link

--- include/crow/middlewares/cookie_parser.h	(before formatting)
+++ include/crow/middlewares/cookie_parser.h	(after formatting)
@@ -51,7 +51,7 @@
             }
 
             Cookie(const std::string& key):
-              Cookie(key, ""){}
+              Cookie(key, "") {}
 
             // format cookie to HTTP header format
             std::string dump() const

mrozigor
mrozigor previously approved these changes Jul 12, 2022
Copy link
Collaborator

@luca-schlecker luca-schlecker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work. 👍
Except for the comments, this is looking quite ready.

include/crow/middlewares/cookie_parser.h Outdated Show resolved Hide resolved
include/crow/middlewares/cookie_parser.h Outdated Show resolved Hide resolved
include/crow/utility.h Outdated Show resolved Hide resolved
include/crow/middlewares/session.h Show resolved Hide resolved
Copy link
Collaborator

@luca-schlecker luca-schlecker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. 🚀

@The-EDev
Copy link
Member

Merging now, Thank you @dranikpg for all the work you've done and your patience during the review process!

@The-EDev The-EDev merged commit e662a90 into CrowCpp:master Jul 14, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Sessions middleware
4 participants