added crow
This commit is contained in:
321
include/crow/middlewares/cookie_parser.h
Normal file
321
include/crow/middlewares/cookie_parser.h
Normal file
@@ -0,0 +1,321 @@
|
||||
#pragma once
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include "crow/utility.h"
|
||||
#include "crow/http_request.h"
|
||||
#include "crow/http_response.h"
|
||||
|
||||
namespace crow
|
||||
{
|
||||
// Any middleware requires following 3 members:
|
||||
|
||||
// struct context;
|
||||
// storing data for the middleware; can be read from another middleware or handlers
|
||||
|
||||
// before_handle
|
||||
// called before handling the request.
|
||||
// if res.end() is called, the operation is halted.
|
||||
// (still call after_handle of this middleware)
|
||||
// 2 signatures:
|
||||
// void before_handle(request& req, response& res, context& ctx)
|
||||
// if you only need to access this middlewares context.
|
||||
// template <typename AllContext>
|
||||
// void before_handle(request& req, response& res, context& ctx, AllContext& all_ctx)
|
||||
// you can access another middlewares' context by calling `all_ctx.template get<MW>()'
|
||||
// ctx == all_ctx.template get<CurrentMiddleware>()
|
||||
|
||||
// after_handle
|
||||
// called after handling the request.
|
||||
// void after_handle(request& req, response& res, context& ctx)
|
||||
// template <typename AllContext>
|
||||
// void after_handle(request& req, response& res, context& ctx, AllContext& all_ctx)
|
||||
|
||||
struct CookieParser
|
||||
{
|
||||
// Cookie stores key, value and attributes
|
||||
struct Cookie
|
||||
{
|
||||
enum class SameSitePolicy
|
||||
{
|
||||
Strict,
|
||||
Lax,
|
||||
None
|
||||
};
|
||||
|
||||
template<typename U>
|
||||
Cookie(const std::string& key, U&& value):
|
||||
Cookie()
|
||||
{
|
||||
key_ = key;
|
||||
value_ = std::forward<U>(value);
|
||||
}
|
||||
|
||||
Cookie(const std::string& key):
|
||||
Cookie(key, "") {}
|
||||
|
||||
// format cookie to HTTP header format
|
||||
std::string dump() const
|
||||
{
|
||||
const static char* HTTP_DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT";
|
||||
|
||||
std::stringstream ss;
|
||||
ss << key_ << '=';
|
||||
ss << (value_.empty() ? "\"\"" : value_);
|
||||
dumpString(ss, !domain_.empty(), "Domain=", domain_);
|
||||
dumpString(ss, !path_.empty(), "Path=", path_);
|
||||
dumpString(ss, secure_, "Secure");
|
||||
dumpString(ss, httponly_, "HttpOnly");
|
||||
if (expires_at_)
|
||||
{
|
||||
ss << DIVIDER << "Expires="
|
||||
<< std::put_time(expires_at_.get(), HTTP_DATE_FORMAT);
|
||||
}
|
||||
if (max_age_)
|
||||
{
|
||||
ss << DIVIDER << "Max-Age=" << *max_age_;
|
||||
}
|
||||
if (same_site_)
|
||||
{
|
||||
ss << DIVIDER << "SameSite=";
|
||||
switch (*same_site_)
|
||||
{
|
||||
case SameSitePolicy::Strict:
|
||||
ss << "Strict";
|
||||
break;
|
||||
case SameSitePolicy::Lax:
|
||||
ss << "Lax";
|
||||
break;
|
||||
case SameSitePolicy::None:
|
||||
ss << "None";
|
||||
break;
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
expires_at_ = std::unique_ptr<std::tm>(new std::tm(time));
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Max-Age attribute
|
||||
Cookie& max_age(long long seconds)
|
||||
{
|
||||
max_age_ = std::unique_ptr<long long>(new long long(seconds));
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Domain attribute
|
||||
Cookie& domain(const std::string& name)
|
||||
{
|
||||
domain_ = name;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Path attribute
|
||||
Cookie& path(const std::string& path)
|
||||
{
|
||||
path_ = path;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Secured attribute
|
||||
Cookie& secure()
|
||||
{
|
||||
secure_ = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// HttpOnly attribute
|
||||
Cookie& httponly()
|
||||
{
|
||||
httponly_ = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// SameSite attribute
|
||||
Cookie& same_site(SameSitePolicy ssp)
|
||||
{
|
||||
same_site_ = std::unique_ptr<SameSitePolicy>(new SameSitePolicy(ssp));
|
||||
return *this;
|
||||
}
|
||||
|
||||
Cookie(const Cookie& c):
|
||||
key_(c.key_),
|
||||
value_(c.value_),
|
||||
domain_(c.domain_),
|
||||
path_(c.path_),
|
||||
secure_(c.secure_),
|
||||
httponly_(c.httponly_)
|
||||
{
|
||||
if (c.max_age_)
|
||||
max_age_ = std::unique_ptr<long long>(new long long(*c.max_age_));
|
||||
|
||||
if (c.expires_at_)
|
||||
expires_at_ = std::unique_ptr<std::tm>(new std::tm(*c.expires_at_));
|
||||
|
||||
if (c.same_site_)
|
||||
same_site_ = std::unique_ptr<SameSitePolicy>(new SameSitePolicy(*c.same_site_));
|
||||
}
|
||||
|
||||
private:
|
||||
Cookie() = default;
|
||||
|
||||
static void dumpString(std::stringstream& ss, bool cond, const char* prefix,
|
||||
const std::string& value = "")
|
||||
{
|
||||
if (cond)
|
||||
{
|
||||
ss << DIVIDER << prefix << value;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::string key_;
|
||||
std::string value_;
|
||||
std::unique_ptr<long long> max_age_{};
|
||||
std::string domain_ = "";
|
||||
std::string path_ = "";
|
||||
bool secure_ = false;
|
||||
bool httponly_ = false;
|
||||
std::unique_ptr<std::tm> expires_at_{};
|
||||
std::unique_ptr<SameSitePolicy> same_site_{};
|
||||
|
||||
static constexpr const char* DIVIDER = "; ";
|
||||
};
|
||||
|
||||
|
||||
struct context
|
||||
{
|
||||
std::unordered_map<std::string, std::string> jar;
|
||||
|
||||
std::string get_cookie(const std::string& key) const
|
||||
{
|
||||
auto cookie = jar.find(key);
|
||||
if (cookie != jar.end())
|
||||
return cookie->second;
|
||||
return {};
|
||||
}
|
||||
|
||||
template<typename U>
|
||||
Cookie& set_cookie(const std::string& key, U&& value)
|
||||
{
|
||||
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 struct CookieParser;
|
||||
std::vector<Cookie> cookies_to_add;
|
||||
};
|
||||
|
||||
void before_handle(request& req, response& res, context& ctx)
|
||||
{
|
||||
const int count = req.headers.count("Cookie");
|
||||
if (!count)
|
||||
return;
|
||||
if (count > 1)
|
||||
{
|
||||
res.code = 400;
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string_view cookies_sv = req.get_header_value("Cookie");
|
||||
|
||||
size_t pos = 0;
|
||||
while (pos < cookies_sv.size())
|
||||
{
|
||||
const size_t pos_equal = cookies_sv.find('=', pos);
|
||||
if (pos_equal == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
|
||||
std::string_view name_sv = cookies_sv.substr(pos, pos_equal - pos);
|
||||
name_sv = utility::trim(name_sv);
|
||||
|
||||
pos = pos_equal + 1;
|
||||
if (pos == cookies_sv.size()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const size_t pos_semicolon = cookies_sv.find(';', pos);
|
||||
std::string_view value_sv;
|
||||
|
||||
if (pos_semicolon == std::string_view::npos) {
|
||||
value_sv = cookies_sv.substr(pos);
|
||||
pos = cookies_sv.size();
|
||||
} else {
|
||||
value_sv = cookies_sv.substr(pos, pos_semicolon - pos);
|
||||
pos = pos_semicolon + 1;
|
||||
}
|
||||
|
||||
value_sv = utility::trim(value_sv);
|
||||
|
||||
if (!value_sv.empty() && value_sv.front() == '"' && value_sv.back() == '"')
|
||||
{
|
||||
if (value_sv.size() >= 2) {
|
||||
value_sv.remove_prefix(1);
|
||||
value_sv.remove_suffix(1);
|
||||
} else {
|
||||
value_sv = value_sv.substr(0,0);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.jar.emplace(std::string(name_sv), std::string(value_sv));
|
||||
}
|
||||
}
|
||||
|
||||
void after_handle(request& /*req*/, response& res, context& ctx)
|
||||
{
|
||||
for (const auto& cookie : ctx.cookies_to_add)
|
||||
{
|
||||
res.add_header("Set-Cookie", cookie.dump());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
App<CookieParser, AnotherJarMW> app;
|
||||
A B C
|
||||
A::context
|
||||
int aa;
|
||||
|
||||
ctx1 : public A::context
|
||||
ctx2 : public ctx1, public B::context
|
||||
ctx3 : public ctx2, public C::context
|
||||
|
||||
C depends on A
|
||||
|
||||
C::handle
|
||||
context.aaa
|
||||
|
||||
App::context : private CookieParser::context, ...
|
||||
{
|
||||
jar
|
||||
|
||||
}
|
||||
|
||||
SimpleApp
|
||||
*/
|
||||
} // namespace crow
|
||||
237
include/crow/middlewares/cors.h
Normal file
237
include/crow/middlewares/cors.h
Normal file
@@ -0,0 +1,237 @@
|
||||
#pragma once
|
||||
#include "crow/common.h"
|
||||
#include "crow/http_request.h"
|
||||
#include "crow/http_response.h"
|
||||
#include "crow/routing.h"
|
||||
|
||||
namespace crow
|
||||
{
|
||||
struct CORSHandler;
|
||||
|
||||
/// Used for tuning CORS policies
|
||||
struct CORSRules
|
||||
{
|
||||
friend struct crow::CORSHandler;
|
||||
|
||||
/// Set Access-Control-Allow-Origin. Default is "*"
|
||||
CORSRules& origin(const std::string& origin)
|
||||
{
|
||||
origin_ = origin;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Set Access-Control-Allow-Methods. Default is "*"
|
||||
CORSRules& methods(crow::HTTPMethod method)
|
||||
{
|
||||
add_list_item(methods_, crow::method_name(method));
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Set Access-Control-Allow-Methods. Default is "*"
|
||||
template<typename... Methods>
|
||||
CORSRules& methods(crow::HTTPMethod method, Methods... method_list)
|
||||
{
|
||||
add_list_item(methods_, crow::method_name(method));
|
||||
methods(method_list...);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Set Access-Control-Allow-Headers. Default is "*"
|
||||
CORSRules& headers(const std::string& header)
|
||||
{
|
||||
add_list_item(headers_, header);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Set Access-Control-Allow-Headers. Default is "*"
|
||||
template<typename... Headers>
|
||||
CORSRules& headers(const std::string& header, Headers... header_list)
|
||||
{
|
||||
add_list_item(headers_, header);
|
||||
headers(header_list...);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Set Access-Control-Expose-Headers. Default is none
|
||||
CORSRules& expose(const std::string& header)
|
||||
{
|
||||
add_list_item(exposed_headers_, header);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Set Access-Control-Expose-Headers. Default is none
|
||||
template<typename... Headers>
|
||||
CORSRules& expose(const std::string& header, Headers... header_list)
|
||||
{
|
||||
add_list_item(exposed_headers_, header);
|
||||
expose(header_list...);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Set Access-Control-Max-Age. Default is none
|
||||
CORSRules& max_age(int max_age)
|
||||
{
|
||||
max_age_ = std::to_string(max_age);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Enable Access-Control-Allow-Credentials
|
||||
CORSRules& allow_credentials()
|
||||
{
|
||||
allow_credentials_ = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Ignore CORS and don't send any headers
|
||||
void ignore()
|
||||
{
|
||||
ignore_ = true;
|
||||
}
|
||||
|
||||
/// Handle CORS on specific prefix path
|
||||
CORSRules& prefix(const std::string& prefix);
|
||||
|
||||
/// Handle CORS for specific blueprint
|
||||
CORSRules& blueprint(const Blueprint& bp);
|
||||
|
||||
/// Global CORS policy
|
||||
CORSRules& global();
|
||||
|
||||
private:
|
||||
CORSRules() = delete;
|
||||
CORSRules(CORSHandler* handler):
|
||||
handler_(handler) {}
|
||||
|
||||
/// build comma separated list
|
||||
void add_list_item(std::string& list, const std::string& val)
|
||||
{
|
||||
if (list == "*") list = "";
|
||||
if (list.size() > 0) list += ", ";
|
||||
list += val;
|
||||
}
|
||||
|
||||
/// Set header `key` to `value` if it is not set
|
||||
void set_header_no_override(const std::string& key, const std::string& value, crow::response& res)
|
||||
{
|
||||
if (value.size() == 0) return;
|
||||
if (!get_header_value(res.headers, key).empty()) return;
|
||||
res.add_header(key, value);
|
||||
}
|
||||
|
||||
/// Set response headers
|
||||
void apply(const request& req, response& res)
|
||||
{
|
||||
if (ignore_) return;
|
||||
|
||||
set_header_no_override("Access-Control-Allow-Methods", methods_, res);
|
||||
set_header_no_override("Access-Control-Allow-Headers", headers_, res);
|
||||
set_header_no_override("Access-Control-Expose-Headers", exposed_headers_, res);
|
||||
set_header_no_override("Access-Control-Max-Age", max_age_, res);
|
||||
|
||||
bool origin_set = false;
|
||||
|
||||
if (req.method != HTTPMethod::Options)
|
||||
{
|
||||
if (allow_credentials_)
|
||||
{
|
||||
set_header_no_override("Access-Control-Allow-Credentials", "true", res);
|
||||
if (origin_ == "*")
|
||||
{
|
||||
set_header_no_override("Access-Control-Allow-Origin", req.get_header_value("Origin"), res);
|
||||
origin_set = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( !origin_set){
|
||||
set_header_no_override("Access-Control-Allow-Origin", origin_, res);
|
||||
}
|
||||
}
|
||||
|
||||
bool ignore_ = false;
|
||||
// TODO: support multiple origins that are dynamically selected
|
||||
std::string origin_ = "*";
|
||||
std::string methods_ = "*";
|
||||
std::string headers_ = "*";
|
||||
std::string exposed_headers_;
|
||||
std::string max_age_;
|
||||
bool allow_credentials_ = false;
|
||||
|
||||
CORSHandler* handler_;
|
||||
};
|
||||
|
||||
/// CORSHandler is a global middleware for setting CORS headers.
|
||||
|
||||
///
|
||||
/// By default, it sets Access-Control-Allow-Origin/Methods/Headers to "*".
|
||||
/// The default behaviour can be changed with the `global()` cors rule.
|
||||
/// Additional rules for prexies can be added with `prefix()`.
|
||||
struct CORSHandler
|
||||
{
|
||||
struct context
|
||||
{};
|
||||
|
||||
void before_handle(crow::request& /*req*/, crow::response& /*res*/, context& /*ctx*/)
|
||||
{}
|
||||
|
||||
void after_handle(crow::request& req, crow::response& res, context& /*ctx*/)
|
||||
{
|
||||
auto& rule = find_rule(req.url);
|
||||
rule.apply(req, res);
|
||||
}
|
||||
|
||||
/// Handle CORS on a specific prefix path
|
||||
CORSRules& prefix(const std::string& prefix)
|
||||
{
|
||||
rules.emplace_back(prefix, CORSRules(this));
|
||||
return rules.back().second;
|
||||
}
|
||||
|
||||
/// Handle CORS for a specific blueprint
|
||||
CORSRules& blueprint(const Blueprint& bp)
|
||||
{
|
||||
rules.emplace_back(bp.prefix(), CORSRules(this));
|
||||
return rules.back().second;
|
||||
}
|
||||
|
||||
/// Get the global CORS policy
|
||||
CORSRules& global()
|
||||
{
|
||||
return default_;
|
||||
}
|
||||
|
||||
private:
|
||||
CORSRules& find_rule(const std::string& path)
|
||||
{
|
||||
// TODO: use a trie in case of many rules
|
||||
for (auto& rule : rules)
|
||||
{
|
||||
// Check if path starts with a rules prefix
|
||||
if (path.rfind(rule.first, 0) == 0)
|
||||
{
|
||||
return rule.second;
|
||||
}
|
||||
}
|
||||
return default_;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, CORSRules>> rules;
|
||||
CORSRules default_ = CORSRules(this);
|
||||
};
|
||||
|
||||
inline CORSRules& CORSRules::prefix(const std::string& prefix)
|
||||
{
|
||||
return handler_->prefix(prefix);
|
||||
}
|
||||
|
||||
inline CORSRules& CORSRules::blueprint(const Blueprint& bp)
|
||||
{
|
||||
return handler_->blueprint(bp);
|
||||
}
|
||||
|
||||
inline CORSRules& CORSRules::global()
|
||||
{
|
||||
return handler_->global();
|
||||
}
|
||||
|
||||
} // namespace crow
|
||||
564
include/crow/middlewares/session.h
Normal file
564
include/crow/middlewares/session.h
Normal file
@@ -0,0 +1,564 @@
|
||||
#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>
|
||||
|
||||
#include <variant>
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
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 || rv.nt() == num_type::Double_precision_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};
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
{
|
||||
using lock = std::scoped_lock<std::mutex>;
|
||||
using rc_lock = std::scoped_lock<std::recursive_mutex>;
|
||||
|
||||
struct context
|
||||
{
|
||||
// Get a mutex for locking this session
|
||||
std::recursive_mutex& mutex()
|
||||
{
|
||||
check_node();
|
||||
return node->mutex;
|
||||
}
|
||||
|
||||
// Check whether 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 multi_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 multivalue
|
||||
// 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 struct 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::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
std::string path_;
|
||||
uint64_t expiration_seconds_;
|
||||
session::ExpirationTracker expirations_;
|
||||
};
|
||||
|
||||
} // namespace crow
|
||||
25
include/crow/middlewares/utf-8.h
Normal file
25
include/crow/middlewares/utf-8.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include "crow/http_request.h"
|
||||
#include "crow/http_response.h"
|
||||
|
||||
namespace crow
|
||||
{
|
||||
|
||||
struct UTF8
|
||||
{
|
||||
struct context
|
||||
{};
|
||||
|
||||
void before_handle(request& /*req*/, response& /*res*/, context& /*ctx*/)
|
||||
{}
|
||||
|
||||
void after_handle(request& /*req*/, response& res, context& /*ctx*/)
|
||||
{
|
||||
if (get_header_value(res.headers, "Content-Type").empty())
|
||||
{
|
||||
res.set_header("Content-Type", "text/plain; charset=utf-8");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace crow
|
||||
Reference in New Issue
Block a user