267 lines
8.2 KiB
C++
267 lines
8.2 KiB
C++
#include "Http.hpp"
|
|||
|
|
|
||
|
|
#include <cctype>
|
||
|
|
|
||
|
|
#include "Caps.hpp"
|
||
|
|
|
||
|
|
namespace molo::example::fw {
|
||
|
|
|
||
|
|
Method parse_method(std::string_view name) {
|
||
|
|
if (name == "GET") return Method::Get;
|
||
|
|
if (name == "POST") return Method::Post;
|
||
|
|
if (name == "PUT") return Method::Put;
|
||
|
|
if (name == "PATCH") return Method::Patch;
|
||
|
|
if (name == "DELETE") return Method::Delete;
|
||
|
|
if (name == "HEAD") return Method::Head;
|
||
|
|
if (name == "OPTIONS") return Method::Options;
|
||
|
|
if (name == "PROPFIND") return Method::Propfind;
|
||
|
|
if (name == "PROPPATCH") return Method::Proppatch;
|
||
|
|
if (name == "MKCOL") return Method::Mkcol;
|
||
|
|
if (name == "COPY") return Method::Copy;
|
||
|
|
if (name == "MOVE") return Method::Move;
|
||
|
|
if (name == "LOCK") return Method::Lock;
|
||
|
|
if (name == "UNLOCK") return Method::Unlock;
|
||
|
|
return Method::Unknown;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string method_name(Method method) {
|
||
|
|
switch (method) {
|
||
|
|
case Method::Get: return "GET";
|
||
|
|
case Method::Post: return "POST";
|
||
|
|
case Method::Put: return "PUT";
|
||
|
|
case Method::Patch: return "PATCH";
|
||
|
|
case Method::Delete: return "DELETE";
|
||
|
|
case Method::Head: return "HEAD";
|
||
|
|
case Method::Options: return "OPTIONS";
|
||
|
|
case Method::Propfind: return "PROPFIND";
|
||
|
|
case Method::Proppatch: return "PROPPATCH";
|
||
|
|
case Method::Mkcol: return "MKCOL";
|
||
|
|
case Method::Copy: return "COPY";
|
||
|
|
case Method::Move: return "MOVE";
|
||
|
|
case Method::Lock: return "LOCK";
|
||
|
|
case Method::Unlock: return "UNLOCK";
|
||
|
|
case Method::Unknown: return "UNKNOWN";
|
||
|
|
}
|
||
|
|
return "UNKNOWN";
|
||
|
|
}
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
bool name_eq(std::string_view a, std::string_view b) {
|
||
|
|
if (a.size() != b.size()) return false;
|
||
|
|
for (std::size_t i = 0; i < a.size(); ++i) {
|
||
|
|
if (std::tolower((unsigned char)a[i]) != std::tolower((unsigned char)b[i]))
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
void Headers::set(std::string name, std::string value) {
|
||
|
|
for (auto& e : entries_) {
|
||
|
|
if (name_eq(e.first, name)) {
|
||
|
|
e.second = value;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
entries_.emplace_back(std::move(name), std::move(value));
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string Headers::get(std::string_view name) const {
|
||
|
|
for (const auto& e : entries_) {
|
||
|
|
if (name_eq(e.first, name)) return e.second;
|
||
|
|
}
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
bool Headers::has(std::string_view name) const {
|
||
|
|
for (const auto& e : entries_) {
|
||
|
|
if (name_eq(e.first, name)) return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
const std::vector<std::pair<std::string, std::string>>& Headers::entries()
|
||
|
|
const {
|
||
|
|
return entries_;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string Request::query_or(std::string_view key,
|
||
|
|
std::string_view fallback) const {
|
||
|
|
auto it = query.find(std::string(key));
|
||
|
|
if (it == query.end()) return std::string(fallback);
|
||
|
|
return it->second;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string Request::param_or(std::string_view key,
|
||
|
|
std::string_view fallback) const {
|
||
|
|
auto it = path_params.find(std::string(key));
|
||
|
|
if (it == path_params.end()) return std::string(fallback);
|
||
|
|
return it->second;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string Request::cookie_or(std::string_view key,
|
||
|
|
std::string_view fallback) const {
|
||
|
|
auto it = cookies.find(std::string(key));
|
||
|
|
if (it == cookies.end()) return std::string(fallback);
|
||
|
|
return it->second;
|
||
|
|
}
|
||
|
|
|
||
|
|
Response::Response(int status, std::string body, std::string content_type)
|
||
|
|
: status(status), body(std::move(body)) {
|
||
|
|
headers.set("Content-Type", std::move(content_type));
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string Response::content_type() const {
|
||
|
|
return headers.get("Content-Type");
|
||
|
|
}
|
||
|
|
|
||
|
|
TextResponse::TextResponse(std::string body, int status)
|
||
|
|
: Response(status, std::move(body), "text/plain; charset=utf-8") {}
|
||
|
|
|
||
|
|
HtmlResponse::HtmlResponse(std::string body, int status)
|
||
|
|
: Response(status, std::move(body), "text/html; charset=utf-8") {}
|
||
|
|
|
||
|
|
JsonResponse::JsonResponse(std::string body, int status)
|
||
|
|
: Response(status, std::move(body), "application/json") {}
|
||
|
|
|
||
|
|
XmlResponse::XmlResponse(std::string body, int status)
|
||
|
|
: Response(status, std::move(body), "application/xml") {}
|
||
|
|
|
||
|
|
RedirectResponse::RedirectResponse(std::string location, bool permanent)
|
||
|
|
: Response(permanent ? 301 : 302, "", "text/plain; charset=utf-8") {
|
||
|
|
headers.set("Location", std::move(location));
|
||
|
|
}
|
||
|
|
|
||
|
|
FileResponse::FileResponse(std::string body, std::string content_type,
|
||
|
|
std::string filename)
|
||
|
|
: Response(200, std::move(body), std::move(content_type)) {
|
||
|
|
headers.set("Content-Disposition",
|
||
|
|
"attachment; filename=\"" + filename + "\"");
|
||
|
|
}
|
||
|
|
|
||
|
|
StreamingResponse::StreamingResponse(std::vector<std::string> chunks)
|
||
|
|
: Response(200, "", "text/plain; charset=utf-8"), chunks(std::move(chunks)) {
|
||
|
|
for (const auto& c : this->chunks) body += c;
|
||
|
|
headers.set("Transfer-Encoding", "chunked");
|
||
|
|
if (this->chunks.size() > caps::kMaxStreamChunks) {
|
||
|
|
status = 500;
|
||
|
|
body = "chunk cap exceeded";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
SseResponse::SseResponse(std::vector<std::string> events)
|
||
|
|
: Response(200, "", "text/event-stream"), events(std::move(events)) {
|
||
|
|
for (const auto& e : this->events) body += "data: " + e + "\n\n";
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string url_decode(std::string_view in) {
|
||
|
|
std::string out;
|
||
|
|
out.reserve(in.size());
|
||
|
|
for (std::size_t i = 0; i < in.size(); ++i) {
|
||
|
|
if (in[i] == '%' && i + 2 < in.size() &&
|
||
|
|
std::isxdigit((unsigned char)in[i + 1]) &&
|
||
|
|
std::isxdigit((unsigned char)in[i + 2])) {
|
||
|
|
int hi = std::isdigit((unsigned char)in[i + 1])
|
||
|
|
? in[i + 1] - '0'
|
||
|
|
: std::tolower((unsigned char)in[i + 1]) - 'a' + 10;
|
||
|
|
int lo = std::isdigit((unsigned char)in[i + 2])
|
||
|
|
? in[i + 2] - '0'
|
||
|
|
: std::tolower((unsigned char)in[i + 2]) - 'a' + 10;
|
||
|
|
out.push_back((char)(hi * 16 + lo));
|
||
|
|
i += 2;
|
||
|
|
} else if (in[i] == '+') {
|
||
|
|
out.push_back(' ');
|
||
|
|
} else {
|
||
|
|
out.push_back(in[i]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::map<std::string, std::string> parse_query(std::string_view qs) {
|
||
|
|
std::map<std::string, std::string> out;
|
||
|
|
std::size_t start = 0;
|
||
|
|
std::size_t fields = 0;
|
||
|
|
while (start <= qs.size() && fields < caps::kMaxFormFields) {
|
||
|
|
std::size_t amp = qs.find('&', start);
|
||
|
|
if (amp == std::string_view::npos) amp = qs.size();
|
||
|
|
std::string_view pair = qs.substr(start, amp - start);
|
||
|
|
std::size_t eq = pair.find('=');
|
||
|
|
if (eq == std::string_view::npos) {
|
||
|
|
if (!pair.empty()) out[url_decode(pair)] = "";
|
||
|
|
} else {
|
||
|
|
out[url_decode(pair.substr(0, eq))] = url_decode(pair.substr(eq + 1));
|
||
|
|
}
|
||
|
|
++fields;
|
||
|
|
start = amp + 1;
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::map<std::string, std::string> parse_cookies(std::string_view header) {
|
||
|
|
std::map<std::string, std::string> out;
|
||
|
|
std::size_t start = 0;
|
||
|
|
while (start < header.size()) {
|
||
|
|
std::size_t semi = header.find(';', start);
|
||
|
|
if (semi == std::string_view::npos) semi = header.size();
|
||
|
|
std::string_view pair = header.substr(start, semi - start);
|
||
|
|
std::size_t eq = pair.find('=');
|
||
|
|
if (eq != std::string_view::npos) {
|
||
|
|
std::string k(pair.substr(0, eq));
|
||
|
|
std::string v(pair.substr(eq + 1));
|
||
|
|
while (!k.empty() && k.front() == ' ') k.erase(k.begin());
|
||
|
|
while (!v.empty() && v.front() == ' ') v.erase(v.begin());
|
||
|
|
if (!k.empty()) out[k] = v;
|
||
|
|
}
|
||
|
|
start = semi + 1;
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::map<std::string, std::string> parse_form(std::string_view body) {
|
||
|
|
return parse_query(body);
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string status_text(int status) {
|
||
|
|
switch (status) {
|
||
|
|
case 200: return "OK";
|
||
|
|
case 201: return "Created";
|
||
|
|
case 207: return "Multi-Status";
|
||
|
|
case 301: return "Moved Permanently";
|
||
|
|
case 302: return "Found";
|
||
|
|
case 400: return "Bad Request";
|
||
|
|
case 401: return "Unauthorized";
|
||
|
|
case 403: return "Forbidden";
|
||
|
|
case 404: return "Not Found";
|
||
|
|
case 405: return "Method Not Allowed";
|
||
|
|
case 406: return "Not Acceptable";
|
||
|
|
case 409: return "Conflict";
|
||
|
|
case 413: return "Content Too Large";
|
||
|
|
case 422: return "Unprocessable Entity";
|
||
|
|
case 429: return "Too Many Requests";
|
||
|
|
case 500: return "Internal Server Error";
|
||
|
|
case 501: return "Not Implemented";
|
||
|
|
default: return "Unknown";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string json_escape(std::string_view in) {
|
||
|
|
std::string out;
|
||
|
|
out.reserve(in.size() + 2);
|
||
|
|
for (char c : in) {
|
||
|
|
switch (c) {
|
||
|
|
case '"': out += "\\\""; break;
|
||
|
|
case '\\': out += "\\\\"; break;
|
||
|
|
case '\n': out += "\\n"; break;
|
||
|
|
case '\r': out += "\\r"; break;
|
||
|
|
case '\t': out += "\\t"; break;
|
||
|
|
default: out.push_back(c); break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace molo::example::fw
|