Files
AntlrLab/src/http.hpp
T
alejo ba5971a788
build / linux-clang (push) Canceled after 0s
build / linux-gcc (push) Canceled after 0s
build / macos-clang (push) Canceled after 0s
build / windows-mingw (push) Canceled after 0s
build / windows-msvc (push) Canceled after 0s
Add project files
2026-10-07 00:25:24 -03:00

395 lines
12 KiB
C++

// A minimal HTTP/1.1 server for the local web UI. It only listens on the
// loopback interface, answers one request per connection ("Connection:
// close"), and runs each connection on its own thread so that an idle
// browser pre-connection cannot block anything.
//
// POSIX sockets, with a Winsock branch for Windows.
#pragma once
#include "platform.hpp"
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <functional>
#include <map>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include "crash.hpp"
#ifndef _WIN32
#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>
#endif
namespace lab {
struct Request {
std::string method;
std::string target; // raw request target
std::string path; // decoded path, without the query
std::map<std::string, std::string> query;
std::map<std::string, std::string> headers; // names lower-cased
std::string body;
std::string header(const std::string& name) const {
const auto it = headers.find(name);
return it == headers.end() ? std::string() : it->second;
}
};
struct Response {
int status = 200;
std::string contentType = "text/plain; charset=utf-8";
std::string body;
std::vector<std::pair<std::string, std::string>> headers;
};
using Handler = std::function<Response(const Request&)>;
namespace http {
constexpr size_t kMaxHeaderBytes = 64 * 1024;
constexpr size_t kMaxBodyBytes = 24 * 1024 * 1024;
#ifdef _WIN32
using Sock = SOCKET;
inline bool validSock(Sock s) { return s != INVALID_SOCKET; }
inline void closeSock(Sock s) { closesocket(s); }
constexpr Sock kBadSock = INVALID_SOCKET;
#else
using Sock = int;
inline bool validSock(Sock s) { return s >= 0; }
inline void closeSock(Sock s) { ::close(s); }
constexpr Sock kBadSock = -1;
#endif
inline int hexVal(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
inline std::string percentDecode(const std::string& s, bool plusIsSpace) {
std::string o;
o.reserve(s.size());
for (size_t i = 0; i < s.size(); ++i) {
if (s[i] == '%' && i + 2 < s.size() && hexVal(s[i + 1]) >= 0 && hexVal(s[i + 2]) >= 0) {
o += static_cast<char>(hexVal(s[i + 1]) * 16 + hexVal(s[i + 2]));
i += 2;
} else if (s[i] == '+' && plusIsSpace) {
o += ' ';
} else {
o += s[i];
}
}
return o;
}
inline std::string lower(std::string s) {
for (char& c : s) {
if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
}
return s;
}
inline std::string trim(const std::string& s) {
size_t a = 0, b = s.size();
while (a < b && (s[a] == ' ' || s[a] == '\t')) ++a;
while (b > a && (s[b - 1] == ' ' || s[b - 1] == '\t' || s[b - 1] == '\r')) --b;
return s.substr(a, b - a);
}
inline const char* statusText(int code) {
switch (code) {
case 100: return "Continue";
case 200: return "OK";
case 204: return "No Content";
case 302: return "Found";
case 400: return "Bad Request";
case 403: return "Forbidden";
case 404: return "Not Found";
case 405: return "Method Not Allowed";
case 408: return "Request Timeout";
case 409: return "Conflict";
case 411: return "Length Required";
case 413: return "Payload Too Large";
case 415: return "Unsupported Media Type";
case 431: return "Request Header Fields Too Large";
case 500: return "Internal Server Error";
case 501: return "Not Implemented";
default: return "OK";
}
}
inline bool sendAll(Sock s, const char* data, size_t n) {
size_t sent = 0;
while (sent < n) {
#ifdef _WIN32
const int r = ::send(s, data + sent, static_cast<int>(n - sent), 0);
#elif defined(MSG_NOSIGNAL)
const ssize_t r = ::send(s, data + sent, n - sent, MSG_NOSIGNAL);
#else
const ssize_t r = ::send(s, data + sent, n - sent, 0);
#endif
if (r <= 0) return false;
sent += static_cast<size_t>(r);
}
return true;
}
inline void writeResponse(Sock s, const Response& r) {
std::string head = "HTTP/1.1 " + std::to_string(r.status) + " " + statusText(r.status) + "\r\n";
head += "Content-Type: " + r.contentType + "\r\n";
head += "Content-Length: " + std::to_string(r.body.size()) + "\r\n";
head += "Connection: close\r\n";
head += "Cache-Control: no-store\r\n";
head += "X-Content-Type-Options: nosniff\r\n";
head += "Referrer-Policy: no-referrer\r\n";
head += "X-Frame-Options: DENY\r\n";
head += "Cross-Origin-Resource-Policy: same-origin\r\n";
for (const auto& h : r.headers) head += h.first + ": " + h.second + "\r\n";
head += "\r\n";
if (!sendAll(s, head.data(), head.size())) return;
if (!r.body.empty()) sendAll(s, r.body.data(), r.body.size());
}
inline Response errorResponse(int code, const std::string& msg) {
Response r;
r.status = code;
r.contentType = "application/json; charset=utf-8";
r.body = "{\"error\":\"" + msg + "\"}";
return r;
}
// Reads and parses one request. Returns 0 on success, otherwise the HTTP
// status to answer with (-1: just drop the connection).
inline int readRequest(Sock s, Request& rq) {
std::string buf;
char chunk[8192];
size_t headerEnd = std::string::npos;
while (headerEnd == std::string::npos) {
const int n = static_cast<int>(::recv(s, chunk, static_cast<int>(sizeof chunk), 0));
if (n <= 0) return buf.empty() ? -1 : 408;
buf.append(chunk, static_cast<size_t>(n));
headerEnd = buf.find("\r\n\r\n");
if (headerEnd == std::string::npos && buf.size() > kMaxHeaderBytes) return 431;
}
const std::string head = buf.substr(0, headerEnd);
std::string body = buf.substr(headerEnd + 4);
size_t eol = head.find("\r\n");
const std::string line = head.substr(0, eol);
const size_t sp1 = line.find(' ');
const size_t sp2 = line.rfind(' ');
if (sp1 == std::string::npos || sp2 == sp1) return 400;
rq.method = line.substr(0, sp1);
rq.target = line.substr(sp1 + 1, sp2 - sp1 - 1);
if (line.compare(sp2 + 1, 5, "HTTP/") != 0) return 400;
while (eol != std::string::npos) {
const size_t start = eol + 2;
eol = head.find("\r\n", start);
const std::string h = head.substr(start, eol == std::string::npos ? std::string::npos : eol - start);
const size_t colon = h.find(':');
if (colon == std::string::npos || colon == 0) continue;
rq.headers[lower(h.substr(0, colon))] = trim(h.substr(colon + 1));
}
const size_t qm = rq.target.find('?');
rq.path = percentDecode(rq.target.substr(0, qm), false);
if (qm != std::string::npos) {
const std::string qs = rq.target.substr(qm + 1);
size_t i = 0;
while (i <= qs.size()) {
const size_t amp = qs.find('&', i);
const std::string pair = qs.substr(i, amp == std::string::npos ? std::string::npos : amp - i);
if (!pair.empty()) {
const size_t eq = pair.find('=');
const std::string k = percentDecode(pair.substr(0, eq), true);
const std::string v = eq == std::string::npos ? "" : percentDecode(pair.substr(eq + 1), true);
rq.query[k] = v;
}
if (amp == std::string::npos) break;
i = amp + 1;
}
}
if (!rq.header("transfer-encoding").empty()) return 501;
size_t want = 0;
const std::string cl = rq.header("content-length");
if (!cl.empty()) {
for (char c : cl) {
if (c < '0' || c > '9' || cl.size() > 12) return 400;
}
want = static_cast<size_t>(std::stoull(cl));
if (want > kMaxBodyBytes) return 413;
} else if (rq.method == "POST" || rq.method == "PUT") {
return 411;
}
if (want > body.size() && lower(rq.header("expect")) == "100-continue") {
const char* c100 = "HTTP/1.1 100 Continue\r\n\r\n";
sendAll(s, c100, std::strlen(c100));
}
while (body.size() < want) {
const int n = static_cast<int>(::recv(s, chunk, static_cast<int>(sizeof chunk), 0));
if (n <= 0) return 408;
body.append(chunk, static_cast<size_t>(n));
}
body.resize(want);
rq.body = std::move(body);
return 0;
}
#ifdef _WIN32
struct WinsockInit {
WinsockInit() {
WSADATA d;
WSAStartup(MAKEWORD(2, 2), &d);
}
~WinsockInit() { WSACleanup(); }
};
#endif
} // namespace http
class HttpServer {
public:
HttpServer() {
#ifdef _WIN32
static http::WinsockInit init;
#else
::signal(SIGPIPE, SIG_IGN);
#endif
}
~HttpServer() {
if (http::validSock(listen_)) http::closeSock(listen_);
}
HttpServer(const HttpServer&) = delete;
HttpServer& operator=(const HttpServer&) = delete;
// Tries each port in turn (0 = let the system choose). Loopback only.
bool listenOn(const std::vector<int>& ports, std::string& error) {
for (int p : ports) {
http::Sock s = ::socket(AF_INET, SOCK_STREAM, 0);
if (!http::validSock(s)) {
error = "cannot create a socket";
return false;
}
int yes = 1;
#ifdef _WIN32
// Without this, another program could bind the same port and share our traffic.
::setsockopt(s, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, reinterpret_cast<const char*>(&yes), sizeof yes);
::SetHandleInformation(reinterpret_cast<HANDLE>(s), HANDLE_FLAG_INHERIT, 0); // not for the Java child
#else
::setsockopt(s, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char*>(&yes), sizeof yes);
#endif
sockaddr_in addr;
std::memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_port = htons(static_cast<uint16_t>(p));
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (::bind(s, reinterpret_cast<sockaddr*>(&addr), sizeof addr) != 0 || ::listen(s, 32) != 0) {
http::closeSock(s);
continue;
}
sockaddr_in bound;
#ifdef _WIN32
int len = sizeof bound;
#else
socklen_t len = sizeof bound;
#endif
::getsockname(s, reinterpret_cast<sockaddr*>(&bound), &len);
port_ = ntohs(bound.sin_port);
listen_ = s;
return true;
}
error = ports.size() == 1 ? "port " + std::to_string(ports[0]) + " is not available"
: "no free port found";
return false;
}
int port() const { return port_; }
// Blocks until `stop` becomes true.
void serve(const Handler& handler, const std::atomic<bool>& stop) {
while (!stop.load()) {
fd_set fds;
FD_ZERO(&fds);
FD_SET(listen_, &fds);
timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 200 * 1000;
#ifdef _WIN32
const int ready = ::select(0, &fds, nullptr, nullptr, &tv);
#else
const int ready = ::select(listen_ + 1, &fds, nullptr, nullptr, &tv);
#endif
if (ready <= 0) continue;
http::Sock c = ::accept(listen_, nullptr, nullptr);
if (!http::validSock(c)) continue;
#ifdef _WIN32
::SetHandleInformation(reinterpret_cast<HANDLE>(c), HANDLE_FLAG_INHERIT, 0);
#endif
active_.fetch_add(1);
std::thread([this, c, &handler]() {
connection(c, handler);
active_.fetch_sub(1);
}).detach();
}
for (int i = 0; i < 100 && active_.load() > 0; ++i) { // let running requests finish
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
private:
http::Sock listen_ = http::kBadSock;
int port_ = 0;
std::atomic<int> active_{0};
static void connection(http::Sock c, const Handler& handler) {
crash::AltStack altStack;
#ifdef _WIN32
DWORD ms = 15000;
::setsockopt(c, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&ms), sizeof ms);
::setsockopt(c, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast<const char*>(&ms), sizeof ms);
#else
timeval tv;
tv.tv_sec = 15;
tv.tv_usec = 0;
::setsockopt(c, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
::setsockopt(c, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
#endif
Request rq;
const int rc = http::readRequest(c, rq);
if (rc > 0) {
http::writeResponse(c, http::errorResponse(rc, http::statusText(rc)));
} else if (rc == 0) {
Response r;
try {
r = handler(rq);
} catch (const std::exception& e) {
r = http::errorResponse(500, "internal error");
(void)e;
}
http::writeResponse(c, r);
}
http::closeSock(c);
}
};
} // namespace lab