19#include <system_error>
65 Socket(
Socket&& other) noexcept : fd_(std::exchange(other.fd_, -1)) {}
70 int fd()
const {
return fd_; }
81 throw std::system_error(errno, std::generic_category(), std::string(what));
95std::string_view
headerValue(std::string_view head, std::string_view name)
97 std::size_t lineStart = head.find(
"\r\n");
98 while (lineStart != std::string_view::npos && lineStart + 2 < head.size()) {
100 const std::size_t lineEnd = head.find(
"\r\n", lineStart);
101 const std::string_view line = head.substr(lineStart, lineEnd - lineStart);
102 const std::size_t colon = line.find(
':');
104 return trim(line.substr(colon + 1));
119 std::size_t headEnd = std::string::npos;
120 std::size_t bodyLength = 0;
122 if (headEnd == std::string::npos && (headEnd = raw.find(
"\r\n\r\n")) != std::string::npos) {
124 bodyLength =
parseInteger(
headerValue(std::string_view(raw).substr(0, headEnd),
"content-length")).value_or(0);
126 if (headEnd != std::string::npos && raw.size() >= headEnd + 4 + bodyLength) {
129 const ssize_t n = ::recv(fd, buffer,
sizeof buffer, 0);
133 raw.append(buffer,
static_cast<std::size_t
>(n));
137 if (headEnd == std::string::npos) {
141 const std::string_view line = std::string_view(raw).substr(0, raw.find(
"\r\n"));
142 const std::size_t space1 = line.find(
' ');
143 const std::size_t space2 = line.find(
' ', space1 + 1);
144 if (space1 == std::string_view::npos || space2 == std::string_view::npos) {
147 request.
method = std::string(line.substr(0, space1));
148 const std::string_view target = line.substr(space1 + 1, space2 - space1 - 1);
149 request.
path = std::string(target.substr(0, target.find(
'?')));
150 request.
body = raw.substr(headEnd + 4, bodyLength);
159 while (!data.empty()) {
160 const ssize_t n = ::send(fd, data.data(), data.size(), MSG_NOSIGNAL);
164 data.remove_prefix(
static_cast<std::size_t
>(n));
174std::string
httpResponse(std::string_view status, std::string_view contentType, std::string_view body)
176 return std::format(
"HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\n"
177 "Cache-Control: no-cache\r\nConnection: close\r\n\r\n{}",
178 status, contentType, body.size(), body);
187 return httpResponse(status,
"application/json", std::format(
"{{\"error\":\"{}\"}}", message));
195 if (name.ends_with(
".html"))
return "text/html; charset=utf-8";
196 if (name.ends_with(
".css"))
return "text/css; charset=utf-8";
197 if (name.ends_with(
".js"))
return "text/javascript; charset=utf-8";
198 if (name.ends_with(
".svg"))
return "image/svg+xml";
199 if (name.ends_with(
".ico"))
return "image/x-icon";
200 return "application/octet-stream";
209 return !name.empty() && name.front() !=
'.' && std::ranges::all_of(name, [](
unsigned char c) {
210 return std::isalnum(c) || c ==
'.' || c ==
'-' || c ==
'_';
223 std::ifstream in(config.
webDir / name, std::ios::binary);
227 std::stringstream content;
228 content << in.rdbuf();
245 std::cout << std::flush;
257 return errorResponse(
"500 Internal Server Error",
"cannot read the table catalogue");
266 char text[INET_ADDRSTRLEN] = {};
267 ::inet_ntop(AF_INET, &address.sin_addr, text,
sizeof text);
285 if (request.
method.empty()) {
292 std::string response;
293 if (request.
method ==
"GET" && request.
path ==
"/api/tables") {
296 }
else if (request.
method ==
"POST" && request.
path ==
"/api/sql") {
298 std::cout << std::format(
"[{}] POST /api/sql\n", client);
300 }
else if (request.
method ==
"GET") {
301 response =
serveFile(config, request.
path ==
"/" ?
"index.html" : std::string_view(request.
path).substr(1));
302 }
else if (request.
method ==
"POST") {
305 response =
errorResponse(
"405 Method Not Allowed",
"Only GET and POST are supported");
314 Socket server(::socket(AF_INET, SOCK_STREAM, 0));
315 if (server.fd() < 0) {
316 throwSystemError(
"socket");
319 ::setsockopt(server.fd(), SOL_SOCKET, SO_REUSEADDR, &yes,
sizeof yes);
321 sockaddr_in address{};
322 address.sin_family = AF_INET;
323 address.sin_addr.s_addr = htonl(INADDR_ANY);
324 address.sin_port = htons(config.
port);
325 if (::bind(server.fd(),
reinterpret_cast<sockaddr*
>(&address),
sizeof address) < 0) {
326 throwSystemError(std::format(
"bind to port {}", config.
port));
328 if (::listen(server.fd(), 10) < 0) {
329 throwSystemError(
"listen");
331 std::cout << std::format(
"HTTP server listening on port {}\n", config.
port) << std::flush;
334 sockaddr_in clientAddr{};
335 socklen_t length =
sizeof clientAddr;
336 Socket client(::accept(server.fd(),
reinterpret_cast<sockaddr*
>(&clientAddr), &length));
337 if (client.fd() < 0) {
338 if (errno == EINTR) {
341 throwSystemError(
"accept");
344 const timeval timeout{.tv_sec = kIdleSeconds, .tv_usec = 0};
345 ::setsockopt(client.fd(), SOL_SOCKET, SO_RCVTIMEO, &timeout,
sizeof timeout);
347 const std::string ip = clientAddress(clientAddr);
350 std::ranges::any_of(config.
allowedClients, [&](
const std::string& prefix) { return ip.starts_with(prefix); });
352 const std::scoped_lock lock(workMutex);
353 std::cout << std::format(
"[{}] rejected: not in the allowed clients\n", ip) << std::flush;
359 std::thread([config, ip, socket = std::move(client)] {
361 handleClient(config, socket.fd(), ip);
362 }
catch (
const std::exception& e) {
363 const std::scoped_lock lock(workMutex);
364 std::cerr << std::format(
"[{}] error: {}\n", ip, e.what());
The table definitions read from SYSTEM_TABLE: tables, their columns and positions.
Turning statement results into console text or JSON for the browser page.
std::string renderText(const StatementResult &result)
Formats one result for the console.
std::string renderTablesJson(const std::vector< TableSchema > &tables)
Builds the JSON answer of GET /api/tables: every table with its columns.
std::string renderJson(const std::vector< StatementResult > &results)
Builds the JSON answer of POST /api/sql: one object per statement that has something to show.
The small HTTP server behind --http: the page and its JSON API.
void runServer(const ServerConfig &config)
Serves connections until the process is stopped.
Running statements, and the results they produce.
The SqlError exception and small string helpers used everywhere.
std::optional< long long > parseInteger(std::string_view text)
Parses a whole string as a base-10 integer.
bool equalsIgnoreCase(std::string_view a, std::string_view b)
Compares two strings, ignoring the case of ASCII letters.
std::string_view trim(std::string_view text)
Removes leading and trailing spaces, tabs and line ends.
const std::vector< TableSchema > & tables() const
All tables, in the order they appear in SYSTEM_TABLE.
static Catalog load(const std::filesystem::path &dataDir)
Reads SYSTEM_TABLE from a data directory.
Runs statements against the tables in one data directory.
std::vector< StatementResult > executeScript(std::string_view script)
Runs every statement of a script, in order.
A problem with one statement: bad syntax, unknown table, value too long...
Owns a socket descriptor and closes it when it goes out of scope (RAII).
Socket(Socket &&other) noexcept
Takes over another Socket's descriptor, here to hand a connection to its thread.
int fd() const
The descriptor, for system calls.
Socket & operator=(const Socket &)=delete
Not copyable.
Socket(const Socket &)=delete
Not copyable: two copies would close the same descriptor.
Socket & operator=(Socket &&)=delete
Not needed: a Socket is only ever moved into a new Socket, never assigned.
~Socket()
Closes the descriptor, if there is one.
int fd_
The descriptor, or -1 for none.
Socket(int fd)
Takes ownership of a descriptor.
std::string runSql(const ServerConfig &config, std::string_view sql)
Runs the SQL of a POST /api/sql request, logs it, and builds the JSON answer.
constexpr std::size_t kMaxRequestSize
The largest request read (headers and body), in bytes; the rest is ignored.
std::string_view contentTypeFor(std::string_view name)
The content type of a file of the page, from its extension.
bool isSafeFileName(std::string_view name)
Tells whether a requested file name is safe to serve from the web/ directory.
std::string_view headerValue(std::string_view head, std::string_view name)
Finds a header's value in the raw request, ignoring the case of its name.
std::string httpResponse(std::string_view status, std::string_view contentType, std::string_view body)
Builds a complete HTTP response.
std::string errorResponse(std::string_view status, std::string_view message)
Builds a JSON error answer, e.g. {"error":"Not found"}.
void sendAll(int fd, std::string_view data)
Sends all of data, retrying until it is written or the client goes away.
void handleClient(const ServerConfig &config, int fd, const std::string &client)
Serves one connection: reads the request, answers it, and closes.
Request readRequest(int fd)
Reads one HTTP request: the request line, the headers and, if there is one, the body.
std::mutex workMutex
Lets only one request at a time run SQL and write to the console.
std::string clientAddress(const sockaddr_in &address)
Formats a client's IPv4 address.
std::string serveFile(const ServerConfig &config, std::string_view name)
Serves one file of the page from the web/ directory.
std::string listTables(const ServerConfig &config)
Builds the JSON answer of GET /api/tables. Call it only while holding workMutex.
void throwSystemError(std::string_view what)
Throws the error of the last failed system call.
constexpr int kIdleSeconds
How long a connection may stay silent before it is closed, in seconds.
Settings of the browser server.
std::filesystem::path dataDir
The directory holding the tables.
std::vector< std::string > allowedClients
IP prefixes such as 192.168.; empty means everyone.
std::uint16_t port
TCP port to listen on.
std::filesystem::path webDir
The directory holding the page: index.html, style.css, app.js.
What running one statement produced: a table, a message, or an error.
One HTTP request, split into the parts the server uses.
std::string path
E.g. /api/sql; any ?query part is removed.
std::string body
The body of a POST (the SQL for /api/sql); empty for a GET.
std::string method
E.g. GET or POST; empty if the request could not be read.