Crow  1.1
A C++ microframework for the web
 
Loading...
Searching...
No Matches
session.h
1#pragma once
2
3#include "crow/http_request.h"
4#include "crow/http_response.h"
5#include "crow/json.h"
6#include "crow/utility.h"
7#include "crow/middlewares/cookie_parser.h"
8
9#include <unordered_map>
10#include <unordered_set>
11#include <set>
12#include <queue>
13
14#include <memory>
15#include <string>
16#include <cstdio>
17#include <mutex>
18
19#include <fstream>
20#include <sstream>
21
22#include <type_traits>
23#include <functional>
24#include <chrono>
25
26#include <variant>
27
28namespace
29{
30 // convert all integer values to int64_t
31 template<typename T>
32 using wrap_integral_t = typename std::conditional<
33 std::is_integral<T>::value && !std::is_same<bool, T>::value
34 // except for uint64_t because that could lead to overflow on conversion
35 && !std::is_same<uint64_t, T>::value,
36 int64_t, T>::type;
37
38 // convert char[]/char* to std::string
39 template<typename T>
40 using wrap_char_t = typename std::conditional<
41 std::is_same<typename std::decay<T>::type, char*>::value,
42 std::string, T>::type;
43
44 // Upgrade to correct type for multi_variant use
45 template<typename T>
46 using wrap_mv_t = wrap_char_t<wrap_integral_t<T>>;
47} // namespace
48
49namespace crow
50{
51 namespace session
52 {
53
54 using multi_value_types = black_magic::S<bool, int64_t, double, std::string>;
55
56 /// A multi_value is a safe variant wrapper with json conversion support
58 {
59 json::wvalue json() const
60 {
61 // clang-format off
62 return std::visit([](auto arg) {
63 return json::wvalue(arg);
64 }, v_);
65 // clang-format on
66 }
67
68 static multi_value from_json(const json::rvalue&);
69
70 std::string string() const
71 {
72 // clang-format off
73 return std::visit([](auto arg) {
74 if constexpr (std::is_same_v<decltype(arg), std::string>)
75 return arg;
76 else
77 return std::to_string(arg);
78 }, v_);
79 // clang-format on
80 }
81
82 template<typename T, typename RT = wrap_mv_t<T>>
83 RT get(const T& fallback)
84 {
85 if (const RT* val = std::get_if<RT>(&v_)) return *val;
86 return fallback;
87 }
88
89 template<typename T, typename RT = wrap_mv_t<T>>
90 void set(T val)
91 {
92 v_ = RT(std::move(val));
93 }
94
95 typename multi_value_types::rebind<std::variant> v_;
96 };
97
98 inline multi_value multi_value::from_json(const json::rvalue& rv)
99 {
100 using namespace json;
101 switch (rv.t())
102 {
103 case type::Number:
104 {
105 if (rv.nt() == num_type::Floating_point || rv.nt() == num_type::Double_precision_floating_point)
106 return multi_value{rv.d()};
107 else if (rv.nt() == num_type::Unsigned_integer)
108 return multi_value{int64_t(rv.u())};
109 else
110 return multi_value{rv.i()};
111 }
112 case type::False: return multi_value{false};
113 case type::True: return multi_value{true};
114 case type::String: return multi_value{std::string(rv)};
115 default: return multi_value{false};
116 }
117 }
118
119 /// Expiration tracker keeps track of soonest-to-expire keys
121 {
122 using DataPair = std::pair<uint64_t /*time*/, std::string /*key*/>;
123
124 /// Add key with time to tracker.
125 /// If the key is already present, it will be updated
126 void add(std::string key, uint64_t time)
127 {
128 auto it = times_.find(key);
129 if (it != times_.end()) remove(key);
130 times_[key] = time;
131 queue_.insert({time, std::move(key)});
132 }
133
134 void remove(const std::string& key)
135 {
136 auto it = times_.find(key);
137 if (it != times_.end())
138 {
139 queue_.erase({it->second, key});
140 times_.erase(it);
141 }
142 }
143
144 /// Get expiration time of soonest-to-expire entry
145 uint64_t peek_first() const
146 {
147 if (queue_.empty()) return std::numeric_limits<uint64_t>::max();
148 return queue_.begin()->first;
149 }
150
151 /// Get expiration time of key, or the maximum value if it is not tracked
152 uint64_t get(const std::string& key) const
153 {
154 auto it = times_.find(key);
155 if (it == times_.end()) return std::numeric_limits<uint64_t>::max();
156 return it->second;
157 }
158
159 std::string pop_first()
160 {
161 auto it = times_.find(queue_.begin()->second);
162 auto key = it->first;
163 times_.erase(it);
164 queue_.erase(queue_.begin());
165 return key;
166 }
167
168 using iterator = typename std::set<DataPair>::const_iterator;
169
170 iterator begin() const { return queue_.cbegin(); }
171
172 iterator end() const { return queue_.cend(); }
173
174 private:
175 std::set<DataPair> queue_;
176 std::unordered_map<std::string, uint64_t> times_;
177 };
178
179 /// CachedSessions are shared across requests
181 {
182 std::string session_id;
183 std::string requested_session_id; // session hasn't been created yet, but a key was requested
184
185 std::unordered_map<std::string, multi_value> entries;
186 std::unordered_set<std::string> dirty; // values that were changed after last load
187
188 void* store_data;
189 bool requested_refresh;
190
191 // number of references held - used for correctly destroying the cache.
192 // No need to be atomic, all SessionMiddleware accesses are synchronized
193 int referrers;
194 std::recursive_mutex mutex;
195 };
196 } // namespace session
197
198 // SessionMiddleware allows storing securely and easily small snippets of user information
199 template<typename Store>
201 {
202 using lock = std::scoped_lock<std::mutex>;
203 using rc_lock = std::scoped_lock<std::recursive_mutex>;
204
205 struct context
206 {
207 // Get a mutex for locking this session
208 std::recursive_mutex& mutex()
209 {
210 check_node();
211 return node->mutex;
212 }
213
214 // Check whether this session is already present
215 bool exists() { return bool(node); }
216
217 // Get a value by key or fallback if it doesn't exist or is of another type
218 template<typename F>
219 auto get(const std::string& key, const F& fallback = F())
220 // This trick lets the multi_value deduce the return type from the fallback
221 // which allows both:
222 // context.get<std::string>("key")
223 // context.get("key", "") -> char[] is transformed into string by multivalue
224 // to return a string
225 -> decltype(std::declval<session::multi_value>().get<F>(std::declval<F>()))
226 {
227 if (!node) return fallback;
228 rc_lock l(node->mutex);
229
230 auto it = node->entries.find(key);
231 if (it != node->entries.end()) return it->second.get<F>(fallback);
232 return fallback;
233 }
234
235 // Set a value by key
236 template<typename T>
237 void set(const std::string& key, T value)
238 {
239 check_node();
240 rc_lock l(node->mutex);
241
242 node->dirty.insert(key);
243 node->entries[key].set(std::move(value));
244 }
245
246 bool contains(const std::string& key)
247 {
248 if (!node) return false;
249 return node->entries.find(key) != node->entries.end();
250 }
251
252 // Atomically mutate a value with a function
253 template<typename Func>
254 void apply(const std::string& key, const Func& f)
255 {
256 using traits = utility::function_traits<Func>;
257 using arg = typename std::decay<typename traits::template arg<0>>::type;
258 using retv = typename std::decay<typename traits::result_type>::type;
259 check_node();
260 rc_lock l(node->mutex);
261 node->dirty.insert(key);
262 node->entries[key].set<retv>(f(node->entries[key].get(arg{})));
263 }
264
265 // Remove a value from the session
266 void remove(const std::string& key)
267 {
268 if (!node) return;
269 rc_lock l(node->mutex);
270 node->dirty.insert(key);
271 node->entries.erase(key);
272 }
273
274 // Format value by key as a string
275 std::string string(const std::string& key)
276 {
277 if (!node) return "";
278 rc_lock l(node->mutex);
279
280 auto it = node->entries.find(key);
281 if (it != node->entries.end()) return it->second.string();
282 return "";
283 }
284
285 // Get a list of keys present in session
286 std::vector<std::string> keys()
287 {
288 if (!node) return {};
289 rc_lock l(node->mutex);
290
291 std::vector<std::string> out;
292 for (const auto& p : node->entries)
293 out.push_back(p.first);
294 return out;
295 }
296
297 // Delay expiration by issuing another cookie with an updated expiration time
298 // and notifying the store
299 void refresh_expiration()
300 {
301 if (!node) return;
302 node->requested_refresh = true;
303 }
304
305 private:
306 friend struct SessionMiddleware;
307
308 void check_node()
309 {
310 if (!node) node = std::make_shared<session::CachedSession>();
311 }
312
313 std::shared_ptr<session::CachedSession> node;
314 };
315
316 template<typename... Ts>
319 int id_length,
320 Ts... ts):
321 id_length_(id_length),
322 cookie_(cookie),
323 store_(std::forward<Ts>(ts)...), mutex_(new std::mutex{})
324 {}
325
326 template<typename... Ts>
327 SessionMiddleware(Ts... ts):
329 CookieParser::Cookie("session")
330 .path("/")
331 .max_age(/*month*/ 30 * 24 * 60 * 60)
332 .httponly()
333 .same_site(CookieParser::Cookie::SameSitePolicy::Lax),
334 /*id_length */ 20, // around 10^34 possible combinations, but small enough to fit into SSO
335 std::forward<Ts>(ts)...)
336 {}
337
338 template<typename AllContext>
339 void before_handle(request& /*req*/, response& /*res*/, context& ctx, AllContext& all_ctx)
340 {
341 lock l(*mutex_);
342
343 auto& cookies = all_ctx.template get<CookieParser>();
344 auto session_id = load_id(cookies);
345 if (session_id == "") return;
346
347 // search entry in cache
348 auto it = cache_.find(session_id);
349 if (it != cache_.end())
350 {
351 it->second->referrers++;
352 ctx.node = it->second;
353 return;
354 }
355
356 // check this is a valid entry before loading
357 if (!store_.contains(session_id)) return;
358
359 auto node = std::make_shared<session::CachedSession>();
360 node->session_id = session_id;
361 node->referrers = 1;
362
363 try
364 {
365 store_.load(*node);
366 }
367 catch (...)
368 {
369 CROW_LOG_ERROR << "Exception occurred during session load";
370 return;
371 }
372
373 ctx.node = node;
374 cache_[session_id] = node;
375 }
376
377 template<typename AllContext>
378 void after_handle(request& /*req*/, response& /*res*/, context& ctx, AllContext& all_ctx)
379 {
380 lock l(*mutex_);
381 if (!ctx.node || --ctx.node->referrers > 0) return;
382 ctx.node->requested_refresh |= ctx.node->session_id == "";
383
384 // generate new id
385 if (ctx.node->session_id == "")
386 {
387 // check for requested id
388 ctx.node->session_id = std::move(ctx.node->requested_session_id);
389 if (ctx.node->session_id == "")
390 {
391 ctx.node->session_id = utility::random_alphanum(id_length_);
392 }
393 }
394 else
395 {
396 cache_.erase(ctx.node->session_id);
397 }
398
399 if (ctx.node->requested_refresh)
400 {
401 auto& cookies = all_ctx.template get<CookieParser>();
402 store_id(cookies, ctx.node->session_id);
403 }
404
405 try
406 {
407 store_.save(*ctx.node);
408 }
409 catch (...)
410 {
411 CROW_LOG_ERROR << "Exception occurred during session save";
412 return;
413 }
414 }
415
416 private:
417 std::string next_id()
418 {
419 std::string id;
420 do
421 {
422 id = utility::random_alphanum(id_length_);
423 } while (store_.contains(id));
424 return id;
425 }
426
427 std::string load_id(const CookieParser::context& cookies)
428 {
429 return cookies.get_cookie(cookie_.name());
430 }
431
432 void store_id(CookieParser::context& cookies, const std::string& session_id)
433 {
434 cookie_.value(session_id);
435 cookies.set_cookie(cookie_);
436 }
437
438 private:
439 int id_length_;
440
441 // prototype for cookie
442 CookieParser::Cookie cookie_;
443
444 Store store_;
445
446 // mutexes are immovable
447 std::unique_ptr<std::mutex> mutex_;
448 std::unordered_map<std::string, std::shared_ptr<session::CachedSession>> cache_;
449 };
450
451 /// InMemoryStore stores all entries in memory
453 {
454 // Load a value into the session cache.
455 // A load is always followed by a save, no loads happen consecutively
456 void load(session::CachedSession& cn)
457 {
458 // load & stores happen sequentially, so moving is safe
459 cn.entries = std::move(entries[cn.session_id]);
460 }
461
462 // Persist session data
463 void save(session::CachedSession& cn)
464 {
465 entries[cn.session_id] = std::move(cn.entries);
466 // cn.dirty is a list of changed keys since the last load
467 }
468
469 bool contains(const std::string& key)
470 {
471 return entries.count(key) > 0;
472 }
473
474 std::unordered_map<std::string, std::unordered_map<std::string, session::multi_value>> entries;
475 };
476
477 // FileStore stores all data as json files in a folder.
478 // Files are deleted after expiration. Expiration refreshes are automatically picked up.
480 {
481 FileStore(const std::string& folder, uint64_t expiration_seconds = /*month*/ 30 * 24 * 60 * 60):
482 path_(folder), expiration_seconds_(expiration_seconds)
483 {
484 std::ifstream ifs(get_filename(".expirations", false));
485
486 auto current_ts = chrono_time();
487 std::string key;
488 uint64_t time;
489 while (ifs >> key >> time)
490 {
491 if (current_ts > time)
492 {
493 evict(key);
494 }
495 else if (contains(key))
496 {
497 expirations_.add(key, time);
498 }
499 }
500 }
501
502 ~FileStore()
503 {
504 std::ofstream ofs(get_filename(".expirations", false), std::ios::trunc);
505 for (const auto& p : expirations_)
506 ofs << p.second << " " << p.first << "\n";
507 }
508
509 // Delete expired entries
510 // At most 3 to prevent freezes
511 void handle_expired()
512 {
513 int deleted = 0;
514 auto current_ts = chrono_time();
515 while (current_ts > expirations_.peek_first() && deleted < 3)
516 {
517 evict(expirations_.pop_first());
518 deleted++;
519 }
520 }
521
522 void load(session::CachedSession& cn)
523 {
524 handle_expired();
525
526 std::ifstream file(get_filename(cn.session_id));
527
528 std::stringstream buffer;
529 buffer << file.rdbuf() << std::endl;
530
531 for (const auto& p : json::load(buffer.str()))
532 cn.entries[p.key()] = session::multi_value::from_json(p);
533 }
534
535 void save(session::CachedSession& cn)
536 {
537 if (cn.requested_refresh)
538 expirations_.add(cn.session_id, chrono_time() + expiration_seconds_);
539 if (cn.dirty.empty()) return;
540
541 std::ofstream file(get_filename(cn.session_id));
542 json::wvalue jw;
543 for (const auto& p : cn.entries)
544 jw[p.first] = p.second.json();
545 file << jw.dump() << std::flush;
546 }
547
548 std::string get_filename(const std::string& key, bool suffix = true)
549 {
550 std::string filename{key};
551 // we have to sanitize here to prevent file traversal by ".." or similar
552 utility::sanitize_filename(filename);
553 if (suffix)
554 {
555 filename.append(".json");
556 }
557 return (path_/filename).string();
558 }
559
560 bool contains(const std::string& key)
561 {
562 // handle_expired() only evicts a few entries per load, so check this key explicitly
563 if (chrono_time() > expirations_.get(key))
564 {
565 expirations_.remove(key);
566 evict(key);
567 return false;
568 }
569
570 std::ifstream file(get_filename(key));
571 return file.good();
572 }
573
574 void evict(const std::string& key)
575 {
576 std::remove(get_filename(key).c_str());
577 }
578
579 // expirations are persisted across restarts, so they need a clock with a fixed epoch
580 uint64_t chrono_time() const
581 {
582 return std::chrono::duration_cast<std::chrono::seconds>(
583 std::chrono::system_clock::now().time_since_epoch())
584 .count();
585 }
586
587 std::filesystem::path path_;
588 uint64_t expiration_seconds_;
589 session::ExpirationTracker expirations_;
590 };
591
592} // namespace crow
JSON read value.
Definition json.h:322
int64_t i() const
The integer value.
Definition json.h:444
double d() const
The double precision floating-point number value.
Definition json.h:478
uint64_t u() const
The unsigned integer value.
Definition json.h:462
num_type nt() const
The number type of the JSON value.
Definition json.h:432
type t() const
The type of the JSON value.
Definition json.h:420
JSON write value.
Definition json.h:1378
The main namespace of the library. In this namespace is defined the most important classes and functi...
Definition session.h:480
InMemoryStore stores all entries in memory.
Definition session.h:453
Definition session.h:206
Definition session.h:201
CachedSessions are shared across requests.
Definition session.h:181
Expiration tracker keeps track of soonest-to-expire keys.
Definition session.h:121
uint64_t peek_first() const
Get expiration time of soonest-to-expire entry.
Definition session.h:145
uint64_t get(const std::string &key) const
Get expiration time of key, or the maximum value if it is not tracked.
Definition session.h:152
void add(std::string key, uint64_t time)
Definition session.h:126
A multi_value is a safe variant wrapper with json conversion support.
Definition session.h:58