Skip to content

Commit 264da37

Browse files
committed
feat(middleware): add HTTP session & cookie support with tests
1 parent e2adec4 commit 264da37

7 files changed

Lines changed: 736 additions & 0 deletions

File tree

include/vix/middleware/app/presets.hpp

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
#include <vix/middleware/auth/api_key.hpp>
3939
#include <vix/middleware/auth/rbac.hpp>
4040
#include <vix/middleware/app/app_middleware.hpp>
41+
#include <vix/middleware/http/cookies.hpp>
42+
#include <vix/middleware/auth/session.hpp>
4143

4244
namespace vix::middleware::app
4345
{
@@ -347,6 +349,82 @@ namespace vix::middleware::app
347349
return adapt_ctx(vix::middleware::auth::jwt(std::move(opt)));
348350
}
349351

352+
inline vix::App::Middleware session_dev(
353+
std::string secret,
354+
std::string cookie_name = "sid",
355+
std::chrono::seconds ttl = std::chrono::hours(24 * 7),
356+
bool secure = false,
357+
std::string same_site = "Lax",
358+
bool http_only = true,
359+
std::string cookie_path = "/",
360+
bool auto_create = true)
361+
{
362+
vix::middleware::auth::SessionOptions opt{};
363+
opt.secret = std::move(secret);
364+
opt.cookie_name = std::move(cookie_name);
365+
opt.cookie_path = std::move(cookie_path);
366+
opt.secure = secure;
367+
opt.http_only = http_only;
368+
opt.same_site = std::move(same_site);
369+
opt.ttl = ttl;
370+
opt.auto_create = auto_create;
371+
372+
return adapt_ctx(vix::middleware::auth::session(std::move(opt)));
373+
}
374+
375+
inline vix::App::Middleware session_strict(
376+
std::string secret,
377+
std::string cookie_name = "sid",
378+
std::chrono::seconds ttl = std::chrono::hours(24 * 7))
379+
{
380+
vix::middleware::auth::SessionOptions opt{};
381+
opt.secret = std::move(secret);
382+
opt.cookie_name = std::move(cookie_name);
383+
opt.cookie_path = "/";
384+
385+
// strict defaults
386+
opt.secure = true;
387+
opt.http_only = true;
388+
opt.same_site = "None"; // needed for cross-site cookies, requires Secure=true
389+
opt.ttl = ttl;
390+
opt.auto_create = true;
391+
392+
return adapt_ctx(vix::middleware::auth::session(std::move(opt)));
393+
}
394+
395+
inline vix::App::Middleware set_cookie_dev(
396+
std::string name,
397+
std::string value,
398+
int max_age = 3600,
399+
bool secure = false,
400+
std::string same_site = "Lax",
401+
bool http_only = true,
402+
std::string path = "/")
403+
{
404+
return adapt_ctx(
405+
[name = std::move(name),
406+
value = std::move(value),
407+
max_age,
408+
secure,
409+
same_site = std::move(same_site),
410+
http_only,
411+
path = std::move(path)](vix::middleware::Context &ctx, vix::middleware::Next next) mutable
412+
{
413+
next();
414+
415+
vix::middleware::http::Cookie c;
416+
c.name = std::move(name);
417+
c.value = std::move(value);
418+
c.max_age = max_age;
419+
c.secure = secure;
420+
c.same_site = std::move(same_site);
421+
c.http_only = http_only;
422+
c.path = std::move(path);
423+
424+
vix::middleware::http::set(ctx.res(), c);
425+
});
426+
}
427+
350428
inline vix::App::Middleware api_key_auth(
351429
vix::middleware::auth::ApiKeyOptions opt)
352430
{
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#ifndef VIX_MIDDLEWARE_AUTH_SESSION_HPP
2+
#define VIX_MIDDLEWARE_AUTH_SESSION_HPP
3+
4+
#include <string>
5+
#include <unordered_map>
6+
#include <optional>
7+
#include <memory>
8+
#include <chrono>
9+
10+
#include <vix/middleware/middleware.hpp>
11+
12+
namespace vix::middleware::auth
13+
{
14+
struct Session
15+
{
16+
std::string id;
17+
std::unordered_map<std::string, std::string> data;
18+
19+
bool is_new{false};
20+
bool dirty{false};
21+
bool destroyed{false};
22+
23+
void set(std::string k, std::string v);
24+
std::optional<std::string> get(const std::string &k) const;
25+
void erase(const std::string &k);
26+
void destroy();
27+
};
28+
29+
class ISessionStore
30+
{
31+
public:
32+
virtual ~ISessionStore() = default;
33+
virtual std::optional<Session> load(const std::string &sid) = 0;
34+
virtual void save(const Session &s, std::chrono::seconds ttl) = 0;
35+
virtual void destroy(const std::string &sid) = 0;
36+
};
37+
38+
class InMemorySessionStore final : public ISessionStore
39+
{
40+
public:
41+
std::optional<Session> load(const std::string &sid) override;
42+
void save(const Session &s, std::chrono::seconds ttl) override;
43+
void destroy(const std::string &sid) override;
44+
45+
private:
46+
std::unordered_map<std::string, Session> map_;
47+
};
48+
49+
struct SessionOptions
50+
{
51+
std::shared_ptr<ISessionStore> store{};
52+
53+
std::string secret; // required
54+
std::string cookie_name{"sid"};
55+
std::string cookie_path{"/"};
56+
bool secure{false};
57+
bool http_only{true};
58+
std::string same_site{"Lax"};
59+
60+
std::chrono::seconds ttl{std::chrono::hours(24 * 7)};
61+
bool auto_create{true};
62+
};
63+
64+
MiddlewareFn session(SessionOptions opt);
65+
66+
} // namespace vix::middleware::auth
67+
68+
#endif
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#ifndef VIX_MIDDLEWARE_HTTP_COOKIES_HPP
2+
#define VIX_MIDDLEWARE_HTTP_COOKIES_HPP
3+
4+
#include <string>
5+
#include <string_view>
6+
#include <unordered_map>
7+
#include <optional>
8+
#include <cctype>
9+
10+
#include <boost/beast/core/string.hpp>
11+
#include <boost/beast/http/field.hpp>
12+
13+
#include <vix/middleware/middleware.hpp>
14+
15+
namespace vix::middleware::http
16+
{
17+
struct Cookie
18+
{
19+
std::string name;
20+
std::string value;
21+
22+
std::string path{"/"};
23+
std::string domain{};
24+
int max_age{-1}; // -1 => omit
25+
bool http_only{true};
26+
bool secure{false};
27+
std::string same_site{"Lax"}; // Lax | Strict | None
28+
};
29+
30+
inline std::string trim(std::string_view s)
31+
{
32+
size_t b = 0;
33+
while (b < s.size() && std::isspace(static_cast<unsigned char>(s[b])))
34+
b++;
35+
size_t e = s.size();
36+
while (e > b && std::isspace(static_cast<unsigned char>(s[e - 1])))
37+
e--;
38+
return std::string(s.substr(b, e - b));
39+
}
40+
41+
inline std::unordered_map<std::string, std::string> parse_cookie_header(std::string_view header)
42+
{
43+
std::unordered_map<std::string, std::string> out;
44+
45+
size_t i = 0;
46+
while (i < header.size())
47+
{
48+
size_t semi = header.find(';', i);
49+
if (semi == std::string_view::npos)
50+
semi = header.size();
51+
52+
auto part = header.substr(i, semi - i);
53+
i = (semi < header.size()) ? semi + 1 : semi;
54+
55+
auto eq = part.find('=');
56+
if (eq == std::string_view::npos)
57+
continue;
58+
59+
std::string k = trim(part.substr(0, eq));
60+
std::string v = trim(part.substr(eq + 1));
61+
62+
if (!k.empty())
63+
out.emplace(std::move(k), std::move(v));
64+
}
65+
66+
return out;
67+
}
68+
69+
inline std::unordered_map<std::string, std::string> parse(const vix::middleware::Request &req)
70+
{
71+
const std::string h = req.header("cookie");
72+
if (h.empty())
73+
return {};
74+
return parse_cookie_header(h);
75+
}
76+
77+
inline std::optional<std::string> get(const vix::middleware::Request &req, std::string_view name)
78+
{
79+
const std::string h = req.header("cookie");
80+
if (h.empty())
81+
return std::nullopt;
82+
83+
auto m = parse_cookie_header(h);
84+
auto it = m.find(std::string(name));
85+
if (it == m.end())
86+
return std::nullopt;
87+
return it->second;
88+
}
89+
90+
inline std::string build_set_cookie_value(const Cookie &c)
91+
{
92+
std::string s;
93+
s.reserve(128);
94+
95+
s += c.name;
96+
s += '=';
97+
s += c.value;
98+
99+
if (!c.path.empty())
100+
{
101+
s += "; Path=";
102+
s += c.path;
103+
}
104+
if (!c.domain.empty())
105+
{
106+
s += "; Domain=";
107+
s += c.domain;
108+
}
109+
if (c.max_age >= 0)
110+
{
111+
s += "; Max-Age=";
112+
s += std::to_string(c.max_age);
113+
}
114+
115+
if (c.http_only)
116+
s += "; HttpOnly";
117+
if (c.secure)
118+
s += "; Secure";
119+
120+
if (!c.same_site.empty())
121+
{
122+
s += "; SameSite=";
123+
s += c.same_site;
124+
}
125+
126+
return s;
127+
}
128+
129+
inline void set(vix::middleware::Response &res, const Cookie &c)
130+
{
131+
using boost::beast::string_view;
132+
133+
const std::string v = build_set_cookie_value(c);
134+
135+
// IMPORTANT: repeatable header
136+
res.res.insert(
137+
string_view{"Set-Cookie", 10},
138+
string_view{v.data(), v.size()});
139+
}
140+
141+
} // namespace vix::middleware::http
142+
143+
#endif

0 commit comments

Comments
 (0)