mirror of https://github.com/CrowCpp/Crow.git
commit
e662a903a1
|
@ -2,9 +2,69 @@ Crow contains some middlewares that are ready to be used in your application.
|
|||
<br>
|
||||
Make sure you understand how to enable and use [middleware](../middleware/).
|
||||
|
||||
## Sessions
|
||||
Include: `crow/middlewares/session.h` <br>
|
||||
Examples: `examples/middlewares/session.cpp`
|
||||
|
||||
This middleware can be used for managing sessions - small packets of data associated with a single client that persist across multiple requests. Sessions shouldn't store anything permanent, but only context that is required to easily work with the current client (is the user authenticated, what page did he visit last, etc.).
|
||||
|
||||
### Setup
|
||||
|
||||
Session data can be stored in multiple ways:
|
||||
|
||||
* `crow::InMemoryStore` - stores all data in memory
|
||||
* `crow::FileStore` - stores all all data in json files
|
||||
* A custom store
|
||||
|
||||
__Always list the CookieParser before the Session__
|
||||
```cpp
|
||||
using Session = crow::SessionMiddleware<crow::FileStore>;
|
||||
crow::App<crow::CookieParser, Session> app{Session{
|
||||
crow::FileStore{"/tmp/sessiondata"}
|
||||
}};
|
||||
```
|
||||
|
||||
Session ids are represented as random alphanumeric strings and are stored in cookies. See the examples for more customization options.
|
||||
|
||||
### Usage
|
||||
|
||||
A session is basically a key-value map with support for multiple types: strings, integers, booleans and doubles. The map is created and persisted automatically as soon it is first written to.
|
||||
|
||||
```cpp
|
||||
auto& session = app.get_context<Session>(request);
|
||||
|
||||
session.get("key", "not-found"); // get string by key and return "not-found" if not found
|
||||
session.get("int", -1);
|
||||
session.get<bool>("flag"); // returns default value(false) if not found
|
||||
|
||||
session.set("key", "new value");
|
||||
session.string("any-type"); // return any type as string representation
|
||||
session.remove("key");
|
||||
session.keys(); // return list of keys
|
||||
```
|
||||
|
||||
Session objects are shared between concurrent requests,
|
||||
this means we can perform atomic operations and even lock the object.
|
||||
```cpp
|
||||
session.apply("views", [](int v){return v + 1;}); // this operation is always atomic, no way to get a data race
|
||||
session.mutex().lock(); // manually lock session
|
||||
```
|
||||
|
||||
### Expiration
|
||||
|
||||
Expiration can happen either by the cookie expiring or the store deleting "old" data.
|
||||
|
||||
* By default, cookies expire after 30 days. This can be changed with the cookie option in the Session constructor.
|
||||
* `crow::FileStore` automatically supports deleting files that are expired (older than 30 days). The expiration age can also be changed in the constructor.
|
||||
|
||||
The session expiration can be postponed. This will make the Session issue a new cookie and make the store acknowledge the new expiration time.
|
||||
```cpp
|
||||
session.refresh_expiration()
|
||||
```
|
||||
|
||||
## Cookies
|
||||
Include: `crow/middlewares/cookie_parser.h` <br>
|
||||
Examples: `examples/middlewars/example_cookies.cpp`
|
||||
Examples: `examples/middlewares/example_cookies.cpp`
|
||||
|
||||
This middleware allows to read and write cookies by using `CookieParser`. Once enabled, it parses all incoming cookies.
|
||||
|
||||
|
@ -24,7 +84,7 @@ ctx.set_cookie("key", "value")
|
|||
|
||||
## CORS
|
||||
Include: `crow/middlewares/cors.h` <br>
|
||||
Examples: `examples/middlewars/example_cors.cpp`
|
||||
Examples: `examples/middlewares/example_cors.cpp`
|
||||
|
||||
This middleware allows to set CORS policies by using `CORSHandler`. Once enabled, it will apply the default CORS rules globally.
|
||||
|
||||
|
|
|
@ -85,6 +85,10 @@ add_executable(example_cookies middlewares/example_cookies.cpp)
|
|||
add_warnings_optimizations(example_cookies)
|
||||
target_link_libraries(example_cookies PUBLIC Crow::Crow)
|
||||
|
||||
add_executable(example_session middlewares/example_session.cpp)
|
||||
add_warnings_optimizations(example_session)
|
||||
target_link_libraries(example_session PUBLIC Crow::Crow)
|
||||
|
||||
if(MSVC)
|
||||
add_executable(example_vs example_vs.cpp)
|
||||
add_warnings_optimizations(example_vs)
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
#include "crow.h"
|
||||
#include "crow/middlewares/session.h"
|
||||
|
||||
crow::response redirect()
|
||||
{
|
||||
crow::response res;
|
||||
res.redirect("/");
|
||||
return res;
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
// Choose a storage kind for:
|
||||
// - InMemoryStore stores all entries in memory
|
||||
// - FileStore stores all entries in json files
|
||||
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
|
||||
|
||||
// Writing your own store is easy
|
||||
// Check out the existing ones for guidelines
|
||||
|
||||
// Make sure the CookieParser is registered before the Session
|
||||
crow::App<crow::CookieParser, Session> app{Session{
|
||||
// customize cookies
|
||||
crow::CookieParser::Cookie("session").max_age(/*one day*/ 24 * 60 * 60).path("/"),
|
||||
// set session id length (small value only for demonstration purposes)
|
||||
4,
|
||||
// init the store
|
||||
crow::InMemoryStore{}}};
|
||||
|
||||
// List all values
|
||||
CROW_ROUTE(app, "/")
|
||||
([&](const crow::request& req) {
|
||||
// get session as middleware context
|
||||
auto& session = app.get_context<Session>(req);
|
||||
// the session acts as a multi-type map
|
||||
// that can store string, integers, doubles and bools
|
||||
// besides get/set/remove it also supports more advanced locking operations
|
||||
|
||||
// Atomically increase number of views
|
||||
// This will not skip a view even on multithreaded applications
|
||||
// with multiple concurrent requests from a client
|
||||
// if "views" doesn't exist, it'll be default initialized
|
||||
session.apply("views", [](int v) {
|
||||
return v + 1;
|
||||
});
|
||||
|
||||
// get all currently present keys
|
||||
auto keys = session.keys();
|
||||
|
||||
std::string out;
|
||||
for (const auto& key : keys)
|
||||
// .string(key) converts a value of any type to a string
|
||||
out += "<p> " + key + " = " + session.string(key) + "</p>";
|
||||
return out;
|
||||
});
|
||||
|
||||
// Get a key
|
||||
CROW_ROUTE(app, "/get")
|
||||
([&](const crow::request& req) {
|
||||
auto& session = app.get_context<Session>(req);
|
||||
auto key = req.url_params.get("key");
|
||||
|
||||
// get a string
|
||||
// return "_NOT_FOUND_" if value is not found or of another type
|
||||
std::string string_v = session.get(key, "_NOT_FOUND_");
|
||||
// alternatively one can use
|
||||
// session.get<std::string>(key)
|
||||
// where the fallback is an empty value ""
|
||||
(void)string_v;
|
||||
|
||||
// get int
|
||||
// because supporting multiple integer types in a type bound map would be cumbersome,
|
||||
// all integral values (except uint64_t) are promoted to int64_t
|
||||
// that is why get<int>, get<uint32_t>, get<int64_t> are all accessing the same type
|
||||
int int_v = session.get(key, -1);
|
||||
(void)int_v;
|
||||
|
||||
return session.string(key);
|
||||
});
|
||||
|
||||
// Set a key
|
||||
// A session is stored as soon as it becomes non empty
|
||||
CROW_ROUTE(app, "/set")
|
||||
([&](const crow::request& req) {
|
||||
auto& session = app.get_context<Session>(req);
|
||||
|
||||
auto key = req.url_params.get("key");
|
||||
auto value = req.url_params.get("value");
|
||||
|
||||
session.set(key, value);
|
||||
|
||||
return redirect();
|
||||
});
|
||||
|
||||
// Remove a key
|
||||
CROW_ROUTE(app, "/remove")
|
||||
([&](const crow::request& req) {
|
||||
auto& session = app.get_context<Session>(req);
|
||||
auto key = req.url_params.get("key");
|
||||
session.remove(key);
|
||||
|
||||
return redirect();
|
||||
});
|
||||
|
||||
// Manually lock a session for synchronization in parallel requests
|
||||
CROW_ROUTE(app, "/lock")
|
||||
([&](const crow::request& req) {
|
||||
auto& session = app.get_context<Session>(req);
|
||||
|
||||
std::lock_guard<std::recursive_mutex> l(session.mutex());
|
||||
|
||||
if (session.get("views", 0) % 2 == 0)
|
||||
{
|
||||
session.set("even", true);
|
||||
}
|
||||
else
|
||||
{
|
||||
session.remove("even");
|
||||
}
|
||||
|
||||
return redirect();
|
||||
});
|
||||
|
||||
app.port(18080)
|
||||
//.multithreaded()
|
||||
.run();
|
||||
|
||||
return 0;
|
||||
}
|
|
@ -69,7 +69,7 @@ namespace crow
|
|||
|
||||
///
|
||||
/// This is meant to be used with requests of type "application/x-www-form-urlencoded"
|
||||
const query_string get_body_params()
|
||||
const query_string get_body_params() const
|
||||
{
|
||||
return query_string(body, false);
|
||||
}
|
||||
|
|
|
@ -1275,6 +1275,7 @@ namespace crow
|
|||
return load(str.data(), str.size());
|
||||
}
|
||||
|
||||
class wvalue_reader;
|
||||
|
||||
/// JSON write value.
|
||||
|
||||
|
@ -1284,6 +1285,7 @@ namespace crow
|
|||
class wvalue : public returnable
|
||||
{
|
||||
friend class crow::mustache::template_t;
|
||||
friend class wvalue_reader;
|
||||
|
||||
public:
|
||||
using object =
|
||||
|
@ -1950,7 +1952,38 @@ namespace crow
|
|||
}
|
||||
};
|
||||
|
||||
// Used for accessing the internals of a wvalue
|
||||
struct wvalue_reader
|
||||
{
|
||||
int64_t get(int64_t fallback)
|
||||
{
|
||||
if (ref.t() != type::Number || ref.nt == num_type::Floating_point)
|
||||
return fallback;
|
||||
return ref.num.si;
|
||||
}
|
||||
|
||||
double get(double fallback)
|
||||
{
|
||||
if (ref.t() != type::Number || ref.nt != num_type::Floating_point)
|
||||
return fallback;
|
||||
return ref.num.d;
|
||||
}
|
||||
|
||||
bool get(bool fallback)
|
||||
{
|
||||
if (ref.t() == type::True) return true;
|
||||
if (ref.t() == type::False) return false;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
std::string get(const std::string& fallback)
|
||||
{
|
||||
if (ref.t() != type::String) return fallback;
|
||||
return ref.s;
|
||||
}
|
||||
|
||||
const wvalue& ref;
|
||||
};
|
||||
|
||||
//std::vector<asio::const_buffer> dump_ref(wvalue& v)
|
||||
//{
|
||||
|
|
|
@ -50,6 +50,9 @@ namespace crow
|
|||
value_ = std::forward<U>(value);
|
||||
}
|
||||
|
||||
Cookie(const std::string& key):
|
||||
Cookie(key, "") {}
|
||||
|
||||
// format cookie to HTTP header format
|
||||
std::string dump() const
|
||||
{
|
||||
|
@ -90,6 +93,18 @@ namespace crow
|
|||
return ss.str();
|
||||
}
|
||||
|
||||
const std::string& name()
|
||||
{
|
||||
return key_;
|
||||
}
|
||||
|
||||
template<typename U>
|
||||
Cookie& value(U&& value)
|
||||
{
|
||||
value_ = std::forward<U>(value);
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Expires attribute
|
||||
Cookie& expires(const std::tm& time)
|
||||
{
|
||||
|
@ -187,7 +202,6 @@ namespace crow
|
|||
struct context
|
||||
{
|
||||
std::unordered_map<std::string, std::string> jar;
|
||||
std::vector<Cookie> cookies_to_add;
|
||||
|
||||
std::string get_cookie(const std::string& key) const
|
||||
{
|
||||
|
@ -203,6 +217,16 @@ namespace crow
|
|||
cookies_to_add.emplace_back(key, std::forward<U>(value));
|
||||
return cookies_to_add.back();
|
||||
}
|
||||
|
||||
Cookie& set_cookie(Cookie cookie)
|
||||
{
|
||||
cookies_to_add.push_back(std::move(cookie));
|
||||
return cookies_to_add.back();
|
||||
}
|
||||
|
||||
private:
|
||||
friend class CookieParser;
|
||||
std::vector<Cookie> cookies_to_add;
|
||||
};
|
||||
|
||||
void before_handle(request& req, response& res, context& ctx)
|
||||
|
|
|
@ -0,0 +1,604 @@
|
|||
#pragma once
|
||||
|
||||
#include "crow/http_request.h"
|
||||
#include "crow/http_response.h"
|
||||
#include "crow/json.h"
|
||||
#include "crow/utility.h"
|
||||
#include "crow/middlewares/cookie_parser.h"
|
||||
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <set>
|
||||
#include <queue>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <cstdio>
|
||||
#include <mutex>
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#include <type_traits>
|
||||
#include <functional>
|
||||
#include <chrono>
|
||||
|
||||
#ifdef CROW_CAN_USE_CPP17
|
||||
#include <variant>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
// convert all integer values to int64_t
|
||||
template<typename T>
|
||||
using wrap_integral_t = typename std::conditional<
|
||||
std::is_integral<T>::value && !std::is_same<bool, T>::value
|
||||
// except for uint64_t because that could lead to overflow on conversion
|
||||
&& !std::is_same<uint64_t, T>::value,
|
||||
int64_t, T>::type;
|
||||
|
||||
// convert char[]/char* to std::string
|
||||
template<typename T>
|
||||
using wrap_char_t = typename std::conditional<
|
||||
std::is_same<typename std::decay<T>::type, char*>::value,
|
||||
std::string, T>::type;
|
||||
|
||||
// Upgrade to correct type for multi_variant use
|
||||
template<typename T>
|
||||
using wrap_mv_t = wrap_char_t<wrap_integral_t<T>>;
|
||||
} // namespace
|
||||
|
||||
namespace crow
|
||||
{
|
||||
namespace session
|
||||
{
|
||||
|
||||
#ifdef CROW_CAN_USE_CPP17
|
||||
using multi_value_types = black_magic::S<bool, int64_t, double, std::string>;
|
||||
|
||||
/// A multi_value is a safe variant wrapper with json conversion support
|
||||
struct multi_value
|
||||
{
|
||||
json::wvalue json() const
|
||||
{
|
||||
// clang-format off
|
||||
return std::visit([](auto arg) {
|
||||
return json::wvalue(arg);
|
||||
}, v_);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
template<typename T, typename RT = wrap_mv_t<T>>
|
||||
RT get(const T& fallback)
|
||||
{
|
||||
if (const RT* val = std::get_if<RT>(&v_)) return *val;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
template<typename T, typename RT = wrap_mv_t<T>>
|
||||
void set(T val)
|
||||
{
|
||||
v_ = RT(std::move(val));
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
case type::Number:
|
||||
{
|
||||
if (rv.nt() == num_type::Floating_point)
|
||||
return multi_value{rv.d()};
|
||||
else if (rv.nt() == num_type::Unsigned_integer)
|
||||
return multi_value{int64_t(rv.u())};
|
||||
else
|
||||
return multi_value{rv.i()};
|
||||
}
|
||||
case type::False: return multi_value{false};
|
||||
case type::True: return multi_value{true};
|
||||
case type::String: return multi_value{std::string(rv)};
|
||||
default: return multi_value{false};
|
||||
}
|
||||
}
|
||||
#else
|
||||
// Fallback for C++11/14 that uses a raw json::wvalue internally.
|
||||
// This implementation consumes significantly more memory
|
||||
// than the variant-based version
|
||||
struct multi_value
|
||||
{
|
||||
json::wvalue json() const { return v_; }
|
||||
|
||||
static multi_value from_json(const json::rvalue&);
|
||||
|
||||
std::string string() const { return v_.dump(); }
|
||||
|
||||
template<typename T, typename RT = wrap_mv_t<T>>
|
||||
RT get(const T& fallback)
|
||||
{
|
||||
return json::wvalue_reader{v_}.get((const RT&)(fallback));
|
||||
}
|
||||
|
||||
template<typename T, typename RT = wrap_mv_t<T>>
|
||||
void set(T val)
|
||||
{
|
||||
v_ = RT(std::move(val));
|
||||
}
|
||||
|
||||
json::wvalue v_;
|
||||
};
|
||||
|
||||
inline multi_value multi_value::from_json(const json::rvalue& rv)
|
||||
{
|
||||
return {rv};
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Expiration tracker keeps track of soonest-to-expire keys
|
||||
struct ExpirationTracker
|
||||
{
|
||||
using DataPair = std::pair<uint64_t /*time*/, std::string /*key*/>;
|
||||
|
||||
/// Add key with time to tracker.
|
||||
/// If the key is already present, it will be updated
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get expiration time of soonest-to-expire entry
|
||||
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;
|
||||
}
|
||||
|
||||
using iterator = typename std::set<DataPair>::const_iterator;
|
||||
|
||||
iterator begin() const { return queue_.cbegin(); }
|
||||
|
||||
iterator end() const { return queue_.cend(); }
|
||||
|
||||
private:
|
||||
std::set<DataPair> queue_;
|
||||
std::unordered_map<std::string, uint64_t> times_;
|
||||
};
|
||||
|
||||
/// CachedSessions are shared across requests
|
||||
struct CachedSession
|
||||
{
|
||||
std::string session_id;
|
||||
std::string requested_session_id; // session hasn't been created yet, but a key was requested
|
||||
|
||||
std::unordered_map<std::string, multi_value> entries;
|
||||
std::unordered_set<std::string> dirty; // values that were changed after last load
|
||||
|
||||
void* store_data;
|
||||
bool requested_refresh;
|
||||
|
||||
// number of references held - used for correctly destroying the cache.
|
||||
// No need to be atomic, all SessionMiddleware accesses are synchronized
|
||||
int referrers;
|
||||
std::recursive_mutex mutex;
|
||||
};
|
||||
}; // namespace session
|
||||
|
||||
// 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
|
||||
bool exists() { return bool(node); }
|
||||
|
||||
// Get a value by key or fallback if it doesn't exist or is of another type
|
||||
template<typename F>
|
||||
auto get(const std::string& key, const F& fallback = F())
|
||||
// This trick lets the mutli_value deduce the return type from the fallback
|
||||
// which allows both:
|
||||
// context.get<std::string>("key")
|
||||
// context.get("key", "") -> char[] is transformed into string by mutlivalue
|
||||
// to return a string
|
||||
-> decltype(std::declval<session::multi_value>().get<F>(std::declval<F>()))
|
||||
{
|
||||
if (!node) return fallback;
|
||||
rc_lock l(node->mutex);
|
||||
|
||||
auto it = node->entries.find(key);
|
||||
if (it != node->entries.end()) return it->second.get<F>(fallback);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
bool contains(const std::string& key)
|
||||
{
|
||||
if (!node) return false;
|
||||
return node->entries.find(key) != node->entries.end();
|
||||
}
|
||||
|
||||
// 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 remove(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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
private:
|
||||
friend class SessionMiddleware;
|
||||
|
||||
void check_node()
|
||||
{
|
||||
if (!node) node = std::make_shared<session::CachedSession>();
|
||||
}
|
||||
|
||||
std::shared_ptr<session::CachedSession> node;
|
||||
};
|
||||
|
||||
template<typename... Ts>
|
||||
SessionMiddleware(
|
||||
CookieParser::Cookie cookie,
|
||||
int id_length,
|
||||
Ts... ts):
|
||||
id_length_(id_length),
|
||||
cookie_(cookie),
|
||||
store_(std::forward<Ts>(ts)...), mutex_(new std::mutex{})
|
||||
{}
|
||||
|
||||
template<typename... Ts>
|
||||
SessionMiddleware(Ts... ts):
|
||||
SessionMiddleware(
|
||||
CookieParser::Cookie("session").path("/").max_age(/*month*/ 30 * 24 * 60 * 60),
|
||||
/*id_length */ 20, // around 10^34 possible combinations, but small enough to fit into SSO
|
||||
std::forward<Ts>(ts)...)
|
||||
{}
|
||||
|
||||
template<typename AllContext>
|
||||
void before_handle(request& /*req*/, response& /*res*/, context& ctx, AllContext& all_ctx)
|
||||
{
|
||||
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;
|
||||
|
||||
auto node = std::make_shared<session::CachedSession>();
|
||||
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>
|
||||
void after_handle(request& /*req*/, response& /*res*/, context& ctx, AllContext& all_ctx)
|
||||
{
|
||||
lock l(*mutex_);
|
||||
if (!ctx.node || --ctx.node->referrers > 0) return;
|
||||
ctx.node->requested_refresh |= ctx.node->session_id == "";
|
||||
|
||||
// generate new id
|
||||
if (ctx.node->session_id == "")
|
||||
{
|
||||
// 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_);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cache_.erase(ctx.node->session_id);
|
||||
}
|
||||
|
||||
if (ctx.node->requested_refresh)
|
||||
{
|
||||
auto& cookies = all_ctx.template get<CookieParser>();
|
||||
store_id(cookies, ctx.node->session_id);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
store_.save(*ctx.node);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return cookies.get_cookie(cookie_.name());
|
||||
}
|
||||
|
||||
void store_id(CookieParser::context& cookies, const std::string& session_id)
|
||||
{
|
||||
cookie_.value(session_id);
|
||||
cookies.set_cookie(cookie_);
|
||||
}
|
||||
|
||||
private:
|
||||
int id_length_;
|
||||
|
||||
// prototype for cookie
|
||||
CookieParser::Cookie cookie_;
|
||||
|
||||
Store store_;
|
||||
|
||||
// mutexes are immovable
|
||||
std::unique_ptr<std::mutex> mutex_;
|
||||
std::unordered_map<std::string, std::shared_ptr<session::CachedSession>> cache_;
|
||||
};
|
||||
|
||||
/// 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
|
||||
void load(session::CachedSession& cn)
|
||||
{
|
||||
// load & stores happen sequentially, so moving is safe
|
||||
cn.entries = std::move(entries[cn.session_id]);
|
||||
}
|
||||
|
||||
// Persist session data
|
||||
void save(session::CachedSession& cn)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::unordered_map<std::string, session::multi_value>> entries;
|
||||
};
|
||||
|
||||
// FileStore stores all data as json files in a folder.
|
||||
// Files are deleted after expiration. Expiration refreshes are automatically picked up.
|
||||
struct FileStore
|
||||
{
|
||||
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 if (contains(key))
|
||||
{
|
||||
expirations_.add(key, time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~FileStore()
|
||||
{
|
||||
std::ofstream ofs(get_filename(".expirations", false), std::ios::trunc);
|
||||
for (const auto& p : expirations_)
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
void load(session::CachedSession& cn)
|
||||
{
|
||||
handle_expired();
|
||||
|
||||
std::ifstream file(get_filename(cn.session_id));
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf() << std::endl;
|
||||
|
||||
for (const auto& p : json::load(buffer.str()))
|
||||
cn.entries[p.key()] = session::multi_value::from_json(p);
|
||||
}
|
||||
|
||||
void save(session::CachedSession& cn)
|
||||
{
|
||||
if (cn.requested_refresh)
|
||||
expirations_.add(cn.session_id, chrono_time() + expiration_seconds_);
|
||||
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;
|
||||
}
|
||||
|
||||
std::string get_filename(const std::string& key, bool suffix = true)
|
||||
{
|
||||
return utility::join_path(path_, key + (suffix ? ".json" : ""));
|
||||
}
|
||||
|
||||
bool contains(const std::string& key)
|
||||
{
|
||||
std::ifstream file(get_filename(key));
|
||||
return file.good();
|
||||
}
|
||||
|
||||
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_;
|
||||
session::ExpirationTracker expirations_;
|
||||
};
|
||||
|
||||
} // namespace crow
|
|
@ -649,10 +649,7 @@ namespace crow
|
|||
inline std::string default_loader(const std::string& filename)
|
||||
{
|
||||
std::string path = detail::get_template_base_directory_ref();
|
||||
if (!(path.back() == '/' || path.back() == '\\'))
|
||||
path += '/';
|
||||
path += filename;
|
||||
std::ifstream inf(path);
|
||||
std::ifstream inf(utility::join_path(path, filename));
|
||||
if (!inf)
|
||||
{
|
||||
CROW_LOG_WARNING << "Template \"" << filename << "\" not found.";
|
||||
|
|
|
@ -10,9 +10,14 @@
|
|||
#include <string>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
#include <random>
|
||||
|
||||
#include "crow/settings.h"
|
||||
|
||||
#ifdef CROW_CAN_USE_CPP17
|
||||
#include <filesystem>
|
||||
#endif
|
||||
|
||||
// TODO(EDev): Adding C++20's [[likely]] and [[unlikely]] attributes might be useful
|
||||
#if defined(__GNUG__) || defined(__clang__)
|
||||
#define CROW_LIKELY(X) __builtin_expect(!!(X), 1)
|
||||
|
@ -781,6 +786,31 @@ namespace crow
|
|||
}
|
||||
}
|
||||
|
||||
inline static std::string random_alphanum(std::size_t size)
|
||||
{
|
||||
static const char alphabet[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
std::random_device dev;
|
||||
std::mt19937 rng(dev());
|
||||
std::uniform_int_distribution<std::mt19937::result_type> dist(0, sizeof(alphabet) - 2);
|
||||
std::string out;
|
||||
out.reserve(size);
|
||||
for (std::size_t i = 0; i < size; i++)
|
||||
out.push_back(alphabet[dist(rng)]);
|
||||
return out;
|
||||
}
|
||||
|
||||
inline static std::string join_path(std::string path, const std::string& fname)
|
||||
{
|
||||
#ifdef CROW_CAN_USE_CPP17
|
||||
return std::filesystem::path(path) / fname;
|
||||
#else
|
||||
if (!(path.back() == '/' || path.back() == '\\'))
|
||||
path += '/';
|
||||
path += fname;
|
||||
return path;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks two string for equality.
|
||||
* Always returns false if strings differ in size.
|
||||
|
|
|
@ -9,11 +9,13 @@
|
|||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <type_traits>
|
||||
#include <regex>
|
||||
|
||||
#include "catch.hpp"
|
||||
#include "crow.h"
|
||||
#include "crow/middlewares/cookie_parser.h"
|
||||
#include "crow/middlewares/cors.h"
|
||||
#include "crow/middlewares/session.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace crow;
|
||||
|
@ -1743,6 +1745,14 @@ TEST_CASE("middleware_cookieparser_format")
|
|||
CHECK(valid(s, 2));
|
||||
CHECK(s.find("Expires=Wed, 01 Nov 2000 23:59:59 GMT") != std::string::npos);
|
||||
}
|
||||
// prototype
|
||||
{
|
||||
auto c = Cookie("key");
|
||||
c.value("value");
|
||||
auto s = c.dump();
|
||||
CHECK(valid(s, 1));
|
||||
CHECK(s == "key=value");
|
||||
}
|
||||
} // middleware_cookieparser_format
|
||||
|
||||
TEST_CASE("middleware_cors")
|
||||
|
@ -1775,10 +1785,7 @@ TEST_CASE("middleware_cors")
|
|||
return "-";
|
||||
});
|
||||
|
||||
auto _ = async(launch::async,
|
||||
[&] {
|
||||
app.bindaddr(LOCALHOST_ADDRESS).port(45451).run();
|
||||
});
|
||||
auto _ = app.bindaddr(LOCALHOST_ADDRESS).port(45451).run_async();
|
||||
|
||||
app.wait_for_server_start();
|
||||
asio::io_service is;
|
||||
|
@ -1825,6 +1832,125 @@ TEST_CASE("middleware_cors")
|
|||
app.stop();
|
||||
} // middleware_cors
|
||||
|
||||
TEST_CASE("middleware_session")
|
||||
{
|
||||
static char buf[5012];
|
||||
|
||||
using Session = SessionMiddleware<InMemoryStore>;
|
||||
|
||||
App<crow::CookieParser, Session> app{
|
||||
Session{InMemoryStore{}}};
|
||||
|
||||
CROW_ROUTE(app, "/get")
|
||||
([&](const request& req) {
|
||||
auto& session = app.get_context<Session>(req);
|
||||
auto key = req.url_params.get("key");
|
||||
return session.string(key);
|
||||
});
|
||||
|
||||
CROW_ROUTE(app, "/set")
|
||||
([&](const request& req) {
|
||||
auto& session = app.get_context<Session>(req);
|
||||
auto key = req.url_params.get("key");
|
||||
auto value = req.url_params.get("value");
|
||||
session.set(key, value);
|
||||
return "ok";
|
||||
});
|
||||
|
||||
CROW_ROUTE(app, "/count")
|
||||
([&](const request& req) {
|
||||
auto& session = app.get_context<Session>(req);
|
||||
session.apply("counter", [](int v) {
|
||||
return v + 2;
|
||||
});
|
||||
return session.string("counter");
|
||||
});
|
||||
|
||||
CROW_ROUTE(app, "/lock")
|
||||
([&](const request& req) {
|
||||
auto& session = app.get_context<Session>(req);
|
||||
session.mutex().lock();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
session.mutex().unlock();
|
||||
return "OK";
|
||||
});
|
||||
|
||||
CROW_ROUTE(app, "/check_lock")
|
||||
([&](const request& req) {
|
||||
auto& session = app.get_context<Session>(req);
|
||||
if (session.mutex().try_lock())
|
||||
return "LOCKED";
|
||||
else
|
||||
{
|
||||
session.mutex().unlock();
|
||||
return "FAILED";
|
||||
};
|
||||
});
|
||||
|
||||
auto _ = app.bindaddr(LOCALHOST_ADDRESS).port(45451).run_async();
|
||||
|
||||
app.wait_for_server_start();
|
||||
asio::io_service is;
|
||||
|
||||
auto make_request = [&](const std::string& rq) {
|
||||
asio::ip::tcp::socket c(is);
|
||||
c.connect(asio::ip::tcp::endpoint(
|
||||
asio::ip::address::from_string(LOCALHOST_ADDRESS), 45451));
|
||||
c.send(asio::buffer(rq));
|
||||
c.receive(asio::buffer(buf, 2048));
|
||||
c.close();
|
||||
return std::string(buf);
|
||||
};
|
||||
|
||||
std::string cookie = "Cookie: session=";
|
||||
|
||||
// test = works
|
||||
{
|
||||
auto res = make_request(
|
||||
"GET /set?key=test&value=works\r\n" + cookie + "\r\n\r\n");
|
||||
|
||||
const std::regex cookiev_regex("Cookie:\\ssession=(.*?);", std::regex::icase);
|
||||
auto istart = std::sregex_token_iterator(res.begin(), res.end(), cookiev_regex, 1);
|
||||
auto iend = std::sregex_token_iterator();
|
||||
|
||||
CHECK(istart != iend);
|
||||
cookie.append(istart->str());
|
||||
cookie.push_back(';');
|
||||
}
|
||||
|
||||
// check test = works
|
||||
{
|
||||
auto res = make_request("GET /get?key=test\r\n" + cookie + "\r\n\r\n");
|
||||
CHECK(res.find("works") != std::string::npos);
|
||||
}
|
||||
|
||||
// check counter
|
||||
{
|
||||
for (int i = 1; i < 5; i++)
|
||||
{
|
||||
auto res = make_request("GET /count\r\n" + cookie + "\r\n\r\n");
|
||||
CHECK(res.find(std::to_string(2 * i)) != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
// lock
|
||||
{
|
||||
asio::ip::tcp::socket c_lock(is);
|
||||
c_lock.connect(asio::ip::tcp::endpoint(
|
||||
asio::ip::address::from_string(LOCALHOST_ADDRESS), 45451));
|
||||
c_lock.send(asio::buffer("GET /lock\r\n" + cookie + "\r\n\r\n"));
|
||||
|
||||
auto res = make_request("GET /check_lock\r\n" + cookie + "\r\n\r\n");
|
||||
CHECK(res.find("LOCKED") != std::string::npos);
|
||||
|
||||
c_lock.close();
|
||||
}
|
||||
|
||||
|
||||
app.stop();
|
||||
} // middleware_session
|
||||
|
||||
|
||||
TEST_CASE("bug_quick_repeated_request")
|
||||
{
|
||||
static char buf[2048];
|
||||
|
|
Loading…
Reference in New Issue