Merge pull request #448 from dranikpg/sessions

Sessions Middleware
This commit is contained in:
Farook Al-Sammarraie 2022-07-14 22:13:43 +03:00 committed by GitHub
commit e662a903a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1019 additions and 12 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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)
//{

View File

@ -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)

View File

@ -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

View File

@ -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.";

View File

@ -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.

View File

@ -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];