#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 raw_token = secure_random_bytes(32); std::array 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 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 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; }