154 lines
5.6 KiB
C++
154 lines
5.6 KiB
C++
#include "routes.h"
|
|
#include "../hex.h"
|
|
#include "../config.h"
|
|
#include "../servehelper.h"
|
|
#include "../settings.h"
|
|
#include "../timeutils.h"
|
|
#include "../curlu_wrapper.h"
|
|
#include "../openssl_wrapper.h"
|
|
|
|
static inline std::string generate_csrf_token(void);
|
|
static inline bool validate_csrf_token(const httplib::Request& req, httplib::Response& res, std::string_view csrf_token, std::string_view query_csrf_token);
|
|
static void set_cookie(const httplib::Request& req, httplib::Response& res, const char* key, std::string_view value, bool session = false);
|
|
static bool safe_memcmp(const char* s1, const char* s2, size_t n);
|
|
|
|
|
|
void user_settings_route(const httplib::Request& req, httplib::Response& res) {
|
|
UserSettings settings;
|
|
Cookies cookies = parse_cookies(req);
|
|
std::string csrf_token;
|
|
|
|
if (req.method == "POST") {
|
|
if (!cookies.contains("csrf-token")) {
|
|
res.status = 400;
|
|
serve_error(req, res, "400: Bad Request", "Missing CSRF token cookie, are cookies enabled?");
|
|
return;
|
|
}
|
|
csrf_token = cookies["csrf-token"];
|
|
|
|
auto query_csrf_token = req.params.find("csrf-token");
|
|
if (query_csrf_token == req.params.end()) {
|
|
res.status = 400;
|
|
serve_error(req, res, "400: Bad Request", "Missing CSRF token query parameter");
|
|
return;
|
|
}
|
|
|
|
if (!validate_csrf_token(req, res, csrf_token, query_csrf_token->second)) {
|
|
return;
|
|
}
|
|
|
|
for (const auto& i : req.params) {
|
|
settings.set(i.first, i.second);
|
|
}
|
|
|
|
set_cookie(req, res, "auto-open-cw", settings.auto_open_cw ? "true" : "false");
|
|
} else {
|
|
for (auto &[name, value] : cookies) {
|
|
settings.set(name, value);
|
|
}
|
|
|
|
if (cookies.contains("csrf-token")) {
|
|
csrf_token = cookies["csrf-token"];
|
|
} else {
|
|
csrf_token = generate_csrf_token();
|
|
set_cookie(req, res, "csrf-token", csrf_token, true);
|
|
}
|
|
}
|
|
|
|
Element auto_open_cw_checkbox("input", {{"type", "checkbox"}, {"name", "auto-open-cw"}, {"value", "true"}}, {});
|
|
if (settings.auto_open_cw) {
|
|
auto_open_cw_checkbox.attributes.push_back({"checked", ""});
|
|
}
|
|
|
|
Element body("body", {
|
|
Element("form", {{"class", "user_settings_page-form"}, {"method", "post"}}, {
|
|
Element("label", {
|
|
std::move(auto_open_cw_checkbox),
|
|
" Automatically open Content Warnings",
|
|
}),
|
|
|
|
Element("br"),
|
|
Element("input", {{"type", "hidden"}, {"name", "csrf-token"}, {"value", csrf_token}}, {}),
|
|
Element("input", {{"type", "submit"}, {"value", "Save"}}, {}),
|
|
}),
|
|
Element("form", {{"class", "user_settings_page-form"}, {"method", "get"}, {"action", get_origin(req)}}, {
|
|
Element("input", {{"class", "cancel"}, {"type", "submit"}, {"value", "Cancel"}}, {}),
|
|
}),
|
|
});
|
|
if (req.method == "POST") {
|
|
body.nodes.insert(body.nodes.begin(), Element("div", {{"class", "success"}}, {
|
|
Element("h3", {"Settings saved!"}),
|
|
}));
|
|
}
|
|
|
|
serve(req, res, "User settings", std::move(body));
|
|
}
|
|
|
|
|
|
static inline std::string generate_csrf_token(void) {
|
|
std::vector<char> raw_token = secure_random_bytes(32);
|
|
std::array<char, 32> raw_token_hmac = hmac_sha3_256(config.hmac_key, raw_token);
|
|
|
|
return hex_encode(raw_token) + '.' + hex_encode(raw_token_hmac.data(), raw_token_hmac.size());
|
|
}
|
|
|
|
static inline bool validate_csrf_token(const httplib::Request& req, httplib::Response& res, std::string_view csrf_token, std::string_view query_csrf_token) {
|
|
if (csrf_token.size() != query_csrf_token.size() || !safe_memcmp(csrf_token.data(), query_csrf_token.data(), csrf_token.size())) {
|
|
res.status = 400;
|
|
serve_error(req, res, "400: Bad Request", "CSRF token cookie and CSRF token query parameter do not match");
|
|
return false;
|
|
}
|
|
|
|
if (csrf_token.size() != 64 + 1 + 64 || csrf_token[64] != '.') {
|
|
res.status = 400;
|
|
serve_error(req, res, "400: Bad Request", "CSRF token is in an unknown format");
|
|
return false;
|
|
}
|
|
|
|
std::vector<char> raw_token, raw_token_hmac;
|
|
try {
|
|
raw_token = hex_decode(csrf_token.substr(0, 64));
|
|
raw_token_hmac = hex_decode(csrf_token.substr(64 + 1, 64));
|
|
} catch (const std::exception& e) {
|
|
res.status = 400;
|
|
serve_error(req, res, "400: Bad Request", "Failed to parse CSRF token", e.what());
|
|
return false;
|
|
}
|
|
|
|
std::array<char, 32> our_raw_token_hmac = hmac_sha3_256(config.hmac_key, raw_token);
|
|
if (!safe_memcmp(raw_token_hmac.data(), our_raw_token_hmac.data(), 32)) {
|
|
res.status = 400;
|
|
serve_error(req, res, "400: Bad Request", "CSRF token HMAC is not correct");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void set_cookie(const httplib::Request& req, httplib::Response& res, const char* key, std::string_view value, bool session) {
|
|
CurlUrl origin;
|
|
origin.set(CURLUPART_URL, get_origin(req));
|
|
|
|
std::string header = std::string(key) + '=' + std::string(value)
|
|
+ "; HttpOnly; SameSite=Lax; Domain=" + origin.get(CURLUPART_HOST).get() + "; Path=" + origin.get(CURLUPART_PATH).get();
|
|
if (!session) {
|
|
header += "; Expires=";
|
|
header += to_web_date(current_time() + 365 * 24 * 60 * 60);
|
|
}
|
|
if (strcmp(origin.get(CURLUPART_SCHEME).get(), "https") == 0) {
|
|
header += "; Secure";
|
|
}
|
|
|
|
res.set_header("Set-Cookie", header);
|
|
}
|
|
|
|
static bool safe_memcmp(const char* s1, const char* s2, size_t n) {
|
|
bool equal = true;
|
|
|
|
for (size_t i = 0; i < n; i++) {
|
|
equal &= s1[i] == s2[i];
|
|
}
|
|
|
|
return equal;
|
|
}
|