coyote/routes/user_settings.cpp

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