211 lines
6.5 KiB
C++
211 lines
6.5 KiB
C++
#include <memory>
|
|
#include <exception>
|
|
#include <stdexcept>
|
|
#include <FastHash.h>
|
|
#include <curl/curl.h>
|
|
|
|
#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 = "<!DOCTYPE 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<std::string> subtitle, std::optional<std::string> 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;
|
|
}
|