2022-05-28 13:39:33 +00:00
|
|
|
#pragma once
|
|
|
|
|
|
|
|
#include "crow/http_request.h"
|
|
|
|
#include "crow/http_response.h"
|
|
|
|
#include "crow/TinySHA1.hpp"
|
|
|
|
#include "crow/json.h"
|
|
|
|
#include "crow/utility.h"
|
|
|
|
#include "crow/middlewares/cookie_parser.h"
|
|
|
|
|
|
|
|
#include <unordered_map>
|
|
|
|
#include <unordered_set>
|
2022-06-22 12:18:32 +00:00
|
|
|
#include <set>
|
|
|
|
#include <queue>
|
|
|
|
|
2022-05-28 13:39:33 +00:00
|
|
|
#include <memory>
|
|
|
|
#include <string>
|
2022-06-22 12:18:32 +00:00
|
|
|
#include <cstdio>
|
|
|
|
#include <mutex>
|
|
|
|
|
2022-05-28 13:39:33 +00:00
|
|
|
#include <fstream>
|
|
|
|
#include <sstream>
|
2022-06-22 12:18:32 +00:00
|
|
|
|
2022-05-28 13:39:33 +00:00
|
|
|
#include <type_traits>
|
|
|
|
#include <functional>
|
2022-06-21 20:52:21 +00:00
|
|
|
#include <chrono>
|
2022-05-28 13:39:33 +00:00
|
|
|
|
2022-06-21 14:05:31 +00:00
|
|
|
// REQUIRE C++17
|
2022-05-28 13:39:33 +00:00
|
|
|
#include <variant>
|
|
|
|
|
2022-06-22 12:18:32 +00:00
|
|
|
namespace {
|
|
|
|
// change all integral values to int64_t
|
|
|
|
template<typename T>
|
|
|
|
using wrap_integral_t = std::conditional_t<std::is_integral_v<T>, int64_t, T>;
|
|
|
|
}
|
|
|
|
|
2022-05-28 13:39:33 +00:00
|
|
|
namespace crow
|
|
|
|
{
|
2022-06-22 12:18:32 +00:00
|
|
|
namespace session
|
2022-05-28 13:39:33 +00:00
|
|
|
{
|
2022-06-21 14:05:31 +00:00
|
|
|
using multi_value_types = black_magic::S<bool, int64_t, double, std::string>;
|
|
|
|
|
2022-06-21 12:38:22 +00:00
|
|
|
/// A multi_value is a safe variant wrapper with json conversion support
|
2022-05-28 13:39:33 +00:00
|
|
|
struct multi_value
|
|
|
|
{
|
|
|
|
json::wvalue json() const
|
|
|
|
{
|
2022-05-28 14:59:33 +00:00
|
|
|
// clang-format off
|
2022-05-28 13:39:33 +00:00
|
|
|
return std::visit([](auto arg) {
|
|
|
|
return json::wvalue(arg);
|
2022-05-28 14:59:33 +00:00
|
|
|
}, v_);
|
|
|
|
// clang-format on
|
2022-05-28 13:39:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static multi_value from_json(const json::rvalue&);
|
|
|
|
|
|
|
|
std::string string() const
|
|
|
|
{
|
|
|
|
// clang-format off
|
|
|
|
return std::visit([](auto arg) {
|
|
|
|
if constexpr (std::is_same_v<decltype(arg), std::string>)
|
|
|
|
return arg;
|
|
|
|
else
|
|
|
|
return std::to_string(arg);
|
|
|
|
}, v_);
|
|
|
|
// clang-format on
|
|
|
|
}
|
|
|
|
|
2022-06-21 14:05:31 +00:00
|
|
|
template<typename T, typename RT = wrap_integral_t<T>>
|
|
|
|
RT get(const T& fallback)
|
2022-05-28 13:39:33 +00:00
|
|
|
{
|
2022-06-21 14:05:31 +00:00
|
|
|
if (const RT* val = std::get_if<RT>(&v_)) return *val;
|
2022-05-28 13:39:33 +00:00
|
|
|
return fallback;
|
|
|
|
}
|
|
|
|
|
2022-06-21 14:05:31 +00:00
|
|
|
template<typename T, typename RT = wrap_integral_t<T>>
|
2022-05-28 13:39:33 +00:00
|
|
|
void set(T val)
|
|
|
|
{
|
2022-06-21 14:05:31 +00:00
|
|
|
v_ = RT(std::move(val));
|
2022-05-28 13:39:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
typename multi_value_types::rebind<std::variant> v_;
|
|
|
|
};
|
|
|
|
|
|
|
|
inline multi_value multi_value::from_json(const json::rvalue& rv)
|
|
|
|
{
|
|
|
|
using namespace json;
|
|
|
|
switch (rv.t())
|
|
|
|
{
|
2022-05-28 14:59:33 +00:00
|
|
|
case type::Number:
|
|
|
|
{
|
|
|
|
if (rv.nt() == num_type::Floating_point)
|
|
|
|
return multi_value{rv.d()};
|
2022-06-21 12:38:22 +00:00
|
|
|
else if (rv.nt() == num_type::Unsigned_integer)
|
2022-06-21 14:05:31 +00:00
|
|
|
return multi_value{int64_t(rv.u())};
|
2022-05-28 14:59:33 +00:00
|
|
|
else
|
|
|
|
return multi_value{rv.i()};
|
|
|
|
}
|
2022-05-28 13:39:33 +00:00
|
|
|
case type::False: return multi_value{false};
|
|
|
|
case type::True: return multi_value{true};
|
2022-05-28 14:59:33 +00:00
|
|
|
case type::String: return multi_value{std::string(rv)};
|
|
|
|
default: return multi_value{false};
|
2022-05-28 13:39:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-22 12:18:32 +00:00
|
|
|
/// Expiration tracker keeps track of soonest-to-expire keys
|
2022-06-21 20:52:21 +00:00
|
|
|
struct ExpirationTracker
|
|
|
|
{
|
|
|
|
using DataPair = std::pair<uint64_t /*time*/, std::string /*key*/>;
|
|
|
|
|
2022-06-22 12:18:32 +00:00
|
|
|
/// Add key with time to tracker.
|
|
|
|
/// If the key is already present, it will be updated
|
2022-06-21 20:52:21 +00:00
|
|
|
void add(std::string key, uint64_t time)
|
|
|
|
{
|
|
|
|
auto it = times_.find(key);
|
|
|
|
if (it != times_.end()) remove(key);
|
|
|
|
times_[key] = time;
|
|
|
|
queue_.insert({time, std::move(key)});
|
|
|
|
}
|
|
|
|
|
|
|
|
void remove(const std::string& key)
|
|
|
|
{
|
|
|
|
auto it = times_.find(key);
|
|
|
|
if (it != times_.end())
|
|
|
|
{
|
|
|
|
queue_.erase({it->second, key});
|
|
|
|
times_.erase(it);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-22 12:18:32 +00:00
|
|
|
/// Get expiration time of soonest-to-expire entry
|
2022-06-21 20:52:21 +00:00
|
|
|
uint64_t peek_first() const
|
|
|
|
{
|
|
|
|
if (queue_.empty()) return std::numeric_limits<uint64_t>::max();
|
|
|
|
return queue_.begin()->first;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string pop_first()
|
|
|
|
{
|
|
|
|
auto it = times_.find(queue_.begin()->second);
|
|
|
|
auto key = it->first;
|
|
|
|
times_.erase(it);
|
|
|
|
queue_.erase(queue_.begin());
|
|
|
|
return key;
|
|
|
|
}
|
|
|
|
|
2022-06-22 12:18:32 +00:00
|
|
|
auto begin() const { return queue_.cbegin(); }
|
|
|
|
|
|
|
|
auto end() const { return queue_.cend(); }
|
|
|
|
|
|
|
|
private:
|
2022-06-21 20:52:21 +00:00
|
|
|
std::set<DataPair> queue_;
|
|
|
|
std::unordered_map<std::string, uint64_t> times_;
|
|
|
|
};
|
|
|
|
|
2022-05-28 13:39:33 +00:00
|
|
|
/// CachedSessions are shared across requests
|
|
|
|
struct CachedSession
|
|
|
|
{
|
|
|
|
std::string session_id;
|
2022-06-22 12:18:32 +00:00
|
|
|
std::string requested_session_id; // session hasn't been created yet, but a key was requested
|
2022-05-28 13:39:33 +00:00
|
|
|
|
2022-06-22 12:18:32 +00:00
|
|
|
std::unordered_map<std::string, multi_value> entries;
|
|
|
|
std::unordered_set<std::string> dirty; // values that were changed after last load
|
2022-05-28 13:39:33 +00:00
|
|
|
|
|
|
|
void* store_data;
|
2022-06-21 20:52:21 +00:00
|
|
|
bool requested_refresh;
|
2022-05-28 13:39:33 +00:00
|
|
|
|
2022-06-22 12:18:32 +00:00
|
|
|
// number of references held - used for correctly destroying the cache.
|
|
|
|
// No need to be atomic, all SessionMiddleware accesses are synchronized
|
2022-05-28 13:39:33 +00:00
|
|
|
int referrers;
|
|
|
|
std::recursive_mutex mutex;
|
|
|
|
};
|
2022-06-22 12:18:32 +00:00
|
|
|
}; // namespace session
|
2022-05-28 13:39:33 +00:00
|
|
|
|
|
|
|
// SessionMiddleware allows storing securely and easily small snippets of user information
|
|
|
|
template<typename Store>
|
|
|
|
struct SessionMiddleware
|
|
|
|
{
|
|
|
|
#ifdef CROW_CAN_USE_CPP17
|
|
|
|
using lock = std::scoped_lock<std::mutex>;
|
|
|
|
using rc_lock = std::scoped_lock<std::recursive_mutex>;
|
|
|
|
#else
|
|
|
|
using lock = std::lock_guard<std::mutex>;
|
|
|
|
using rc_lock = std::lock_guard<std::recursive_mutex>;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
struct context
|
|
|
|
{
|
|
|
|
// Get a mutex for locking this session
|
|
|
|
std::recursive_mutex& mutex()
|
|
|
|
{
|
|
|
|
check_node();
|
|
|
|
return node->mutex;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check wheter this session is already present
|
2022-06-21 12:38:22 +00:00
|
|
|
bool exists() { return bool(node); }
|
2022-05-28 13:39:33 +00:00
|
|
|
|
|
|
|
// Get a value by key or fallback if it doesn't exist or is of another type
|
|
|
|
template<typename T>
|
|
|
|
T get(const std::string& key, const T& fallback = T())
|
|
|
|
{
|
|
|
|
if (!node) return fallback;
|
|
|
|
rc_lock l(node->mutex);
|
|
|
|
|
|
|
|
auto it = node->entries.find(key);
|
|
|
|
if (it != node->entries.end()) return it->second.get<T>(fallback);
|
|
|
|
return fallback;
|
|
|
|
}
|
|
|
|
|
2022-06-21 12:38:22 +00:00
|
|
|
// Request a special session id for the store
|
|
|
|
// WARNING: it does not check for collisions!
|
|
|
|
void preset_id(std::string id)
|
|
|
|
{
|
|
|
|
check_node();
|
|
|
|
node->requested_session_id = std::move(id);
|
|
|
|
}
|
|
|
|
|
2022-06-21 20:52:21 +00:00
|
|
|
// Delay expiration by issuing another cookie with an updated expiration time
|
|
|
|
// and notifying the store
|
|
|
|
void refresh_expiration()
|
|
|
|
{
|
|
|
|
if (!node) return;
|
|
|
|
node->requested_refresh = true;
|
|
|
|
}
|
|
|
|
|
2022-05-28 13:39:33 +00:00
|
|
|
// Set a value by key
|
|
|
|
template<typename T>
|
|
|
|
void set(const std::string& key, T value)
|
|
|
|
{
|
|
|
|
check_node();
|
|
|
|
rc_lock l(node->mutex);
|
|
|
|
|
|
|
|
node->dirty.insert(key);
|
|
|
|
node->entries[key].set(std::move(value));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Atomically mutate a value with a function
|
|
|
|
template<typename Func>
|
|
|
|
void apply(const std::string& key, const Func& f)
|
|
|
|
{
|
|
|
|
using traits = utility::function_traits<Func>;
|
|
|
|
using arg = typename std::decay<typename traits::template arg<0>>::type;
|
|
|
|
using retv = typename std::decay<typename traits::result_type>::type;
|
|
|
|
check_node();
|
|
|
|
rc_lock l(node->mutex);
|
|
|
|
node->dirty.insert(key);
|
|
|
|
node->entries[key].set<retv>(f(node->entries[key].get(arg{})));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove a value from the session
|
|
|
|
void evict(const std::string& key)
|
|
|
|
{
|
|
|
|
if (!node) return;
|
|
|
|
rc_lock l(node->mutex);
|
|
|
|
node->dirty.insert(key);
|
|
|
|
node->entries.erase(key);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Format value by key as a string
|
|
|
|
std::string string(const std::string& key)
|
|
|
|
{
|
|
|
|
if (!node) return "";
|
|
|
|
rc_lock l(node->mutex);
|
|
|
|
|
|
|
|
auto it = node->entries.find(key);
|
|
|
|
if (it != node->entries.end()) return it->second.string();
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get a list of keys present in session
|
|
|
|
std::vector<std::string> keys()
|
|
|
|
{
|
|
|
|
if (!node) return {};
|
|
|
|
rc_lock l(node->mutex);
|
|
|
|
|
|
|
|
std::vector<std::string> out;
|
|
|
|
for (const auto& p : node->entries)
|
|
|
|
out.push_back(p.first);
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
friend class SessionMiddleware;
|
|
|
|
|
|
|
|
void check_node()
|
|
|
|
{
|
2022-06-22 12:18:32 +00:00
|
|
|
if (!node) node = std::make_shared<session::CachedSession>();
|
2022-05-28 13:39:33 +00:00
|
|
|
}
|
|
|
|
|
2022-06-22 12:18:32 +00:00
|
|
|
std::shared_ptr<session::CachedSession> node;
|
2022-05-28 13:39:33 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
template<typename... Ts>
|
2022-06-21 20:52:21 +00:00
|
|
|
SessionMiddleware(const std::string& secret_key,
|
|
|
|
CookieParser::Cookie cookie,
|
|
|
|
int id_length,
|
|
|
|
Ts... ts):
|
|
|
|
secret_key_(secret_key),
|
|
|
|
id_length_(id_length), cookie_(cookie),
|
|
|
|
store_(std::forward<Ts>(ts)...), mutex_(new std::mutex{})
|
2022-05-28 13:39:33 +00:00
|
|
|
{}
|
|
|
|
|
|
|
|
template<typename... Ts>
|
|
|
|
SessionMiddleware(const std::string& secret_key, Ts... ts):
|
|
|
|
SessionMiddleware(secret_key,
|
|
|
|
CookieParser::Cookie("session").path("/").max_age(/*month*/ 30 * 24 * 60 * 60),
|
2022-06-21 20:52:21 +00:00
|
|
|
/*id_length */ 10,
|
|
|
|
std::forward<Ts>(ts)...)
|
2022-05-28 13:39:33 +00:00
|
|
|
{}
|
|
|
|
|
|
|
|
template<typename AllContext>
|
2022-05-28 14:59:33 +00:00
|
|
|
void before_handle(request& /*req*/, response& /*rsp*/, context& ctx, AllContext& all_ctx)
|
2022-05-28 13:39:33 +00:00
|
|
|
{
|
|
|
|
lock l(*mutex_);
|
|
|
|
|
|
|
|
auto& cookies = all_ctx.template get<CookieParser>();
|
|
|
|
auto session_id = load_id(cookies);
|
|
|
|
if (session_id == "") return;
|
|
|
|
|
|
|
|
// search entry in cache
|
|
|
|
auto it = cache_.find(session_id);
|
|
|
|
if (it != cache_.end())
|
|
|
|
{
|
|
|
|
it->second->referrers++;
|
|
|
|
ctx.node = it->second;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// check this is a valid entry before loading
|
|
|
|
if (!store_.contains(session_id)) return;
|
|
|
|
|
2022-06-22 12:18:32 +00:00
|
|
|
auto node = std::make_shared<session::CachedSession>();
|
2022-05-28 13:39:33 +00:00
|
|
|
node->session_id = session_id;
|
|
|
|
node->referrers = 1;
|
|
|
|
|
|
|
|
try
|
|
|
|
{
|
|
|
|
store_.load(*node);
|
|
|
|
}
|
|
|
|
catch (...)
|
|
|
|
{
|
|
|
|
CROW_LOG_ERROR << "Exception occurred during session load";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.node = node;
|
|
|
|
cache_[session_id] = node;
|
|
|
|
}
|
|
|
|
|
|
|
|
template<typename AllContext>
|
2022-05-28 14:59:33 +00:00
|
|
|
void after_handle(request& /*req*/, response& /*rsp*/, context& ctx, AllContext& all_ctx)
|
2022-05-28 13:39:33 +00:00
|
|
|
{
|
|
|
|
lock l(*mutex_);
|
|
|
|
if (!ctx.node || --ctx.node->referrers > 0) return;
|
2022-06-21 20:52:21 +00:00
|
|
|
ctx.node->requested_refresh |= ctx.node->session_id == "";
|
2022-05-28 13:39:33 +00:00
|
|
|
|
|
|
|
// generate new id
|
|
|
|
if (ctx.node->session_id == "")
|
|
|
|
{
|
2022-06-21 12:38:22 +00:00
|
|
|
// check for requested id
|
|
|
|
ctx.node->session_id = std::move(ctx.node->requested_session_id);
|
|
|
|
if (ctx.node->session_id == "")
|
|
|
|
{
|
|
|
|
ctx.node->session_id = utility::random_alphanum(id_length_);
|
|
|
|
}
|
2022-05-28 13:39:33 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
cache_.erase(ctx.node->session_id);
|
|
|
|
}
|
|
|
|
|
2022-06-21 20:52:21 +00:00
|
|
|
if (ctx.node->requested_refresh)
|
|
|
|
{
|
|
|
|
auto& cookies = all_ctx.template get<CookieParser>();
|
|
|
|
store_id(cookies, ctx.node->session_id);
|
|
|
|
}
|
|
|
|
|
2022-05-28 13:39:33 +00:00
|
|
|
try
|
|
|
|
{
|
|
|
|
store_.save(*ctx.node);
|
|
|
|
}
|
|
|
|
catch (...)
|
|
|
|
{
|
|
|
|
// TODO(dranikpg): better handling?
|
|
|
|
CROW_LOG_ERROR << "Exception occurred during session save";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
std::string next_id()
|
|
|
|
{
|
|
|
|
std::string id;
|
|
|
|
do
|
|
|
|
{
|
|
|
|
id = utility::random_alphanum(id_length_);
|
|
|
|
} while (store_.contains(id));
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string load_id(const CookieParser::context& cookies)
|
|
|
|
{
|
|
|
|
auto session_raw = cookies.get_cookie(cookie_.name());
|
|
|
|
|
|
|
|
auto pos = session_raw.find(":");
|
|
|
|
if (pos <= 0 || pos >= session_raw.size() - 1) return "";
|
|
|
|
|
|
|
|
auto session_id = session_raw.substr(0, pos);
|
|
|
|
auto session_hmac = session_raw.substr(pos + 1, session_raw.size() - pos - 1);
|
|
|
|
|
|
|
|
return hash_id(session_id) == session_hmac ? session_id : "";
|
|
|
|
}
|
|
|
|
|
|
|
|
void store_id(CookieParser::context& cookies, const std::string& session_id)
|
|
|
|
{
|
|
|
|
cookie_.value(session_id + ":" + hash_id(session_id));
|
|
|
|
cookies.set_cookie(cookie_);
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string hash_id(const std::string& key) const
|
|
|
|
{
|
|
|
|
sha1::SHA1 s;
|
|
|
|
std::string input = key + "/" + secret_key_;
|
|
|
|
s.processBytes(input.data(), input.size());
|
|
|
|
uint8_t digest[20];
|
|
|
|
s.getDigestBytes(digest);
|
|
|
|
return utility::base64encode((unsigned char*)digest, 20);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
std::string secret_key_;
|
2022-05-28 14:59:33 +00:00
|
|
|
int id_length_;
|
2022-05-28 13:39:33 +00:00
|
|
|
|
|
|
|
// prototype for cookie
|
|
|
|
CookieParser::Cookie cookie_;
|
|
|
|
|
|
|
|
Store store_;
|
|
|
|
|
|
|
|
// mutexes are immovable
|
|
|
|
std::unique_ptr<std::mutex> mutex_;
|
2022-06-22 12:18:32 +00:00
|
|
|
std::unordered_map<std::string, std::shared_ptr<session::CachedSession>> cache_;
|
2022-05-28 13:39:33 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/// InMemoryStore stores all entries in memory
|
|
|
|
struct InMemoryStore
|
|
|
|
{
|
|
|
|
// Load a value into the session cache.
|
|
|
|
// A load is always followed by a save, no loads happen consecutively
|
2022-06-22 12:18:32 +00:00
|
|
|
void load(session::CachedSession& cn)
|
2022-05-28 13:39:33 +00:00
|
|
|
{
|
|
|
|
// load & stores happen sequentially, so moving is safe
|
|
|
|
cn.entries = std::move(entries[cn.session_id]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Persist session data
|
2022-06-22 12:18:32 +00:00
|
|
|
void save(session::CachedSession& cn)
|
2022-05-28 13:39:33 +00:00
|
|
|
{
|
|
|
|
entries[cn.session_id] = std::move(cn.entries);
|
|
|
|
// cn.dirty is a list of changed keys since the last load
|
|
|
|
}
|
|
|
|
|
|
|
|
bool contains(const std::string& key)
|
|
|
|
{
|
|
|
|
return entries.count(key) > 0;
|
|
|
|
}
|
|
|
|
|
2022-06-22 12:18:32 +00:00
|
|
|
std::unordered_map<std::string, std::unordered_map<std::string, session::multi_value>> entries;
|
2022-05-28 13:39:33 +00:00
|
|
|
};
|
|
|
|
|
2022-06-22 12:18:32 +00:00
|
|
|
// FileStore stores all data as json files in a folder.
|
|
|
|
// Files are deleted after expiration. Expiration refreshes are automatically picked up.
|
2022-05-28 13:39:33 +00:00
|
|
|
struct FileStore
|
|
|
|
{
|
2022-06-21 20:52:21 +00:00
|
|
|
FileStore(const std::string& folder, uint64_t expiration_seconds = /*month*/ 30 * 24 * 60 * 60):
|
|
|
|
path_(folder), expiration_seconds_(expiration_seconds)
|
|
|
|
{
|
|
|
|
std::ifstream ifs(get_filename(".expirations", false));
|
|
|
|
|
|
|
|
auto current_ts = chrono_time();
|
|
|
|
std::string key;
|
|
|
|
uint64_t time;
|
|
|
|
while (ifs >> key >> time)
|
|
|
|
{
|
|
|
|
if (current_ts > time)
|
|
|
|
{
|
|
|
|
evict(key);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
expirations_.add(key, time);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
~FileStore()
|
|
|
|
{
|
|
|
|
std::ofstream ofs(get_filename(".expirations", false), std::ios::trunc);
|
2022-06-22 12:18:32 +00:00
|
|
|
for (const auto& p : expirations_)
|
2022-06-21 20:52:21 +00:00
|
|
|
ofs << p.second << " " << p.first << "\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete expired entries
|
|
|
|
// At most 3 to prevent freezes
|
|
|
|
void handle_expired()
|
|
|
|
{
|
|
|
|
int deleted = 0;
|
|
|
|
auto current_ts = chrono_time();
|
|
|
|
while (current_ts > expirations_.peek_first() && deleted < 3)
|
|
|
|
{
|
|
|
|
evict(expirations_.pop_first());
|
|
|
|
deleted++;
|
|
|
|
}
|
|
|
|
}
|
2022-05-28 13:39:33 +00:00
|
|
|
|
2022-06-22 12:18:32 +00:00
|
|
|
void load(session::CachedSession& cn)
|
2022-05-28 13:39:33 +00:00
|
|
|
{
|
2022-06-21 20:52:21 +00:00
|
|
|
handle_expired();
|
|
|
|
|
2022-05-28 13:39:33 +00:00
|
|
|
std::ifstream file(get_filename(cn.session_id));
|
|
|
|
|
|
|
|
std::stringstream buffer;
|
|
|
|
buffer << file.rdbuf() << std::endl;
|
|
|
|
|
|
|
|
for (const auto& p : json::load(buffer.str()))
|
2022-06-22 12:18:32 +00:00
|
|
|
cn.entries[p.key()] = session::multi_value::from_json(p);
|
2022-05-28 13:39:33 +00:00
|
|
|
}
|
|
|
|
|
2022-06-22 12:18:32 +00:00
|
|
|
void save(session::CachedSession& cn)
|
2022-05-28 13:39:33 +00:00
|
|
|
{
|
2022-06-21 20:52:21 +00:00
|
|
|
if (cn.requested_refresh)
|
|
|
|
expirations_.add(cn.session_id, chrono_time() + expiration_seconds_);
|
2022-05-28 13:39:33 +00:00
|
|
|
if (cn.dirty.empty()) return;
|
|
|
|
|
|
|
|
std::ofstream file(get_filename(cn.session_id));
|
|
|
|
json::wvalue jw;
|
|
|
|
for (const auto& p : cn.entries)
|
|
|
|
jw[p.first] = p.second.json();
|
|
|
|
file << jw.dump() << std::flush;
|
|
|
|
}
|
|
|
|
|
2022-06-21 20:52:21 +00:00
|
|
|
std::string get_filename(const std::string& key, bool suffix = true)
|
2022-05-28 13:39:33 +00:00
|
|
|
{
|
2022-06-21 20:52:21 +00:00
|
|
|
return utility::join_path(path_, key + (suffix ? ".json" : ""));
|
2022-05-28 13:39:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool contains(const std::string& key)
|
|
|
|
{
|
|
|
|
std::ifstream file(get_filename(key));
|
|
|
|
return file.good();
|
|
|
|
}
|
|
|
|
|
2022-06-21 20:52:21 +00:00
|
|
|
void evict(const std::string& key)
|
|
|
|
{
|
|
|
|
std::remove(get_filename(key).c_str());
|
|
|
|
}
|
|
|
|
|
|
|
|
uint64_t chrono_time() const
|
|
|
|
{
|
|
|
|
return std::chrono::duration_cast<std::chrono::seconds>(
|
|
|
|
std::chrono::system_clock::now().time_since_epoch())
|
|
|
|
.count();
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string path_;
|
|
|
|
uint64_t expiration_seconds_;
|
2022-06-22 12:18:32 +00:00
|
|
|
session::ExpirationTracker expirations_;
|
2022-05-28 13:39:33 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
} // namespace crow
|