#include #include #include #include #include #include "config.h" #include "servehelper.h" #include "curlu_wrapper.h" #include "routes/routes.h" static inline void parse_cookies(std::string_view str, Cookies& cookies); static inline bool lowercase_compare(std::string_view lhs, std::string_view rhs); void serve(const httplib::Request& req, httplib::Response& res, std::string title, Element element, Nodes extra_head) { using namespace std::string_literals; std::string css_url = get_origin(req) + "/style.css"; res.set_header("Content-Security-Policy", "default-src 'none'; img-src https:; media-src https:; style-src "s + css_url); Element head("head", { Element("meta", {{"charset", "utf-8"}}, {}), Element("title", {std::move(title)}), Element("link", {{"rel", "stylesheet"}, {"href", std::move(css_url) + "?v=" + std::to_string(css_hash)}}, {}), Element("meta", {{"name", "viewport"}, {"content", "width=device-width,initial-scale=1"}}, {}) }); head.nodes.reserve(head.nodes.size() + extra_head.size()); head.nodes.insert(head.nodes.end(), extra_head.begin(), extra_head.end()); std::string html = ""s + Element("html", { std::move(head), std::move(element) }).serialize(); uint64_t hash = FastHash(html.data(), html.size(), 0); res.set_header("ETag", std::string(1, '"') + std::to_string(hash) + '"'); if (should_send_304(req, hash)) { res.status = 304; res.set_header("Content-Length", std::to_string(html.size())); res.set_header("Content-Type", "text/html"); } else { res.set_content(std::move(html), "text/html"); } } void serve_error(const httplib::Request& req, httplib::Response& res, std::string title, std::optional subtitle, std::optional info) { Element error_div("div", {{"class", "error"}}, { Element("h2", {title}) }); if (subtitle) { error_div.nodes.push_back(Element("p", { std::move(*subtitle) })); } if (info) { error_div.nodes.push_back(Element("pre", { Element("code", {std::move(*info)}) })); } Element body("body", {std::move(error_div)}); serve(req, res, std::move(title), std::move(body)); } void serve_redirect(const httplib::Request& req, httplib::Response& res, std::string url, bool permanent) { using namespace std::string_literals; Element body("body", { "Redirecting to ", Element("a", {{"href", url}}, {url}), "…" }); res.set_redirect(url, permanent ? 301 : 302); serve(req, res, "Redirecting to "s + std::move(url) + "…", std::move(body)); } bool starts_with(const CurlUrl& url, const CurlUrl& base) { if (strcmp(url.get(CURLUPART_SCHEME).get(), base.get(CURLUPART_SCHEME).get()) != 0) { return false; } if (strcmp(url.get(CURLUPART_HOST).get(), base.get(CURLUPART_HOST).get()) != 0) { return false; } CurlStr url_path = url.get(CURLUPART_PATH); CurlStr base_path = base.get(CURLUPART_PATH); size_t base_path_len = strlen(base_path.get()); return memcmp(url_path.get(), base_path.get(), base_path_len) == 0 && (base_path.get()[base_path_len - 1] == '/' || url_path.get()[base_path_len] == '/' || url_path.get()[base_path_len] == '\0'); } std::string get_origin(const httplib::Request& req) { if (req.has_header("X-Canonical-Origin")) { return req.get_header_value("X-Canonical-Origin"); } if (config.canonical_origin) { return *config.canonical_origin; } std::string origin = "http://"; if (req.has_header("Host")) { origin += req.get_header_value("Host"); } else { origin += config.bind_host; if (config.bind_port != 80) { origin += ':' + std::to_string(config.bind_port); } } return origin; } std::string proxy_mastodon_url(const httplib::Request& req, const std::string& url_str) { CurlUrl url; url.set(CURLUPART_URL, url_str.c_str()); std::string new_url = get_origin(req) + '/' + url.get(CURLUPART_HOST).get() + url.get(CURLUPART_PATH).get(); try { CurlStr query = url.get(CURLUPART_QUERY); new_url += '?'; new_url += query.get(); } catch (const CurlUrlException& e) { if (e.code != CURLUE_NO_QUERY) { throw; } } try { CurlStr fragment = url.get(CURLUPART_FRAGMENT); new_url += '#'; new_url += fragment.get(); } catch (const CurlUrlException& e) { if (e.code != CURLUE_NO_FRAGMENT) { throw; } } return new_url; } bool should_send_304(const httplib::Request& req, uint64_t hash) { std::string header = req.get_header_value("If-None-Match"); if (header == "*") { return true; } size_t pos = header.find(std::string(1, '"') + std::to_string(hash) + '"'); return pos != std::string::npos && (pos == 0 || header[pos - 1] != '/'); } Cookies parse_cookies(const httplib::Request& req) { Cookies cookies; for (const auto& i : req.headers) { if (lowercase_compare(i.first, "cookie")) { parse_cookies(i.second, cookies); } } return cookies; } static inline void parse_cookies(std::string_view str, Cookies& cookies) { using namespace std::string_literals; size_t offset = 0; size_t new_offset = 0; const char* delimiter = "; "; size_t delimiter_len = strlen(delimiter); while (offset < str.size()) { new_offset = str.find(delimiter, offset); std::string_view item = str.substr(offset, new_offset != std::string_view::npos ? new_offset - offset : std::string_view::npos); size_t equal_offset = item.find('='); if (equal_offset == std::string_view::npos) { throw std::invalid_argument("invalid user setting item: "s + std::string(item)); } cookies.insert({std::string(item.substr(0, equal_offset)), std::string(item.substr(equal_offset + 1))}); if (new_offset == std::string_view::npos) { break; } offset = new_offset + delimiter_len; } } static inline bool lowercase_compare(std::string_view lhs, std::string_view rhs) { if (lhs.size() != rhs.size()) { return false; } auto lower = [](char c) { return c >= 'A' && c <= 'Z' ? c - 'A' + 'a' : c; }; for (size_t i = 0; i < lhs.size(); i++) { if (lower(lhs[i]) != lower(rhs[i])) { return false; } } return true; }