myTinySQL v0.2.0-2-g5000098
A tiny SQL interpreter over fixed-width text files
Loading...
Searching...
No Matches
Server.cpp
Go to the documentation of this file.
1
3
4#include "Server.hpp"
5
6#include <arpa/inet.h>
7#include <netinet/in.h>
8#include <sys/socket.h>
9#include <unistd.h>
10
11#include <algorithm>
12#include <cerrno>
13#include <cctype>
14#include <format>
15#include <fstream>
16#include <iostream>
17#include <mutex>
18#include <sstream>
19#include <system_error>
20#include <thread>
21#include <utility>
22
23#include "Catalog.hpp"
24#include "Output.hpp"
25#include "Session.hpp"
26#include "Util.hpp"
27
28namespace tinysql {
29
30namespace {
31
33constexpr std::size_t kMaxRequestSize = 64 * 1024;
34
38constexpr int kIdleSeconds = 30;
39
43std::mutex workMutex;
44
46class Socket {
47public:
50 explicit Socket(int fd) : fd_(fd) {}
51
54 {
55 if (fd_ >= 0) {
56 ::close(fd_);
57 }
58 }
59
60 Socket(const Socket&) = delete;
61 Socket& operator=(const Socket&) = delete;
62
65 Socket(Socket&& other) noexcept : fd_(std::exchange(other.fd_, -1)) {}
66 Socket& operator=(Socket&&) = delete;
67
70 int fd() const { return fd_; }
71
72private:
73 int fd_;
74};
75
79[[noreturn]] void throwSystemError(std::string_view what)
80{
81 throw std::system_error(errno, std::generic_category(), std::string(what));
82}
83
85struct Request {
86 std::string method;
87 std::string path;
88 std::string body;
89};
90
95std::string_view headerValue(std::string_view head, std::string_view name)
96{
97 std::size_t lineStart = head.find("\r\n");
98 while (lineStart != std::string_view::npos && lineStart + 2 < head.size()) {
99 lineStart += 2;
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(':');
103 if (colon != std::string_view::npos && equalsIgnoreCase(trim(line.substr(0, colon)), name)) {
104 return trim(line.substr(colon + 1));
105 }
106 lineStart = lineEnd;
107 }
108 return {};
109}
110
116{
117 std::string raw;
118 char buffer[4096];
119 std::size_t headEnd = std::string::npos;
120 std::size_t bodyLength = 0;
121 while (raw.size() < kMaxRequestSize) {
122 if (headEnd == std::string::npos && (headEnd = raw.find("\r\n\r\n")) != std::string::npos) {
123 // The headers are complete: from now on, read until the body has Content-Length bytes.
124 bodyLength = parseInteger(headerValue(std::string_view(raw).substr(0, headEnd), "content-length")).value_or(0);
125 }
126 if (headEnd != std::string::npos && raw.size() >= headEnd + 4 + bodyLength) {
127 break;
128 }
129 const ssize_t n = ::recv(fd, buffer, sizeof buffer, 0);
130 if (n <= 0) {
131 break;
132 }
133 raw.append(buffer, static_cast<std::size_t>(n));
134 }
135
136 Request request;
137 if (headEnd == std::string::npos) {
138 return request; // nothing, or not a complete request
139 }
140 // Request line: "POST /api/sql HTTP/1.1"
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) {
145 return request;
146 }
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);
151 return request;
152}
153
157void sendAll(int fd, std::string_view data)
158{
159 while (!data.empty()) {
160 const ssize_t n = ::send(fd, data.data(), data.size(), MSG_NOSIGNAL);
161 if (n <= 0) {
162 return; // client went away
163 }
164 data.remove_prefix(static_cast<std::size_t>(n));
165 }
166}
167
174std::string httpResponse(std::string_view status, std::string_view contentType, std::string_view body)
175{
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);
179}
180
185std::string errorResponse(std::string_view status, std::string_view message)
186{
187 return httpResponse(status, "application/json", std::format("{{\"error\":\"{}\"}}", message));
188}
189
193std::string_view contentTypeFor(std::string_view name)
194{
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";
201}
202
207bool isSafeFileName(std::string_view name)
208{
209 return !name.empty() && name.front() != '.' && std::ranges::all_of(name, [](unsigned char c) {
210 return std::isalnum(c) || c == '.' || c == '-' || c == '_';
211 });
212}
213
218std::string serveFile(const ServerConfig& config, std::string_view name)
219{
220 if (!isSafeFileName(name)) {
221 return errorResponse("404 Not Found", "Not found");
222 }
223 std::ifstream in(config.webDir / name, std::ios::binary);
224 if (!in) {
225 return errorResponse("404 Not Found", "Not found");
226 }
227 std::stringstream content;
228 content << in.rdbuf();
229 return httpResponse("200 OK", contentTypeFor(name), content.str());
230}
231
238std::string runSql(const ServerConfig& config, std::string_view sql)
239{
240 Session session(config.dataDir);
241 const auto results = session.executeScript(sql);
242 for (const StatementResult& result : results) {
243 std::cout << renderText(result);
244 }
245 std::cout << std::flush;
246 return httpResponse("200 OK", "application/json", renderJson(results));
247}
248
252std::string listTables(const ServerConfig& config)
253{
254 try {
255 return httpResponse("200 OK", "application/json", renderTablesJson(Catalog::load(config.dataDir).tables()));
256 } catch (const SqlError& e) {
257 return errorResponse("500 Internal Server Error", "cannot read the table catalogue");
258 }
259}
260
264std::string clientAddress(const sockaddr_in& address)
265{
266 char text[INET_ADDRSTRLEN] = {};
267 ::inet_ntop(AF_INET, &address.sin_addr, text, sizeof text);
268 return text;
269}
270
282void handleClient(const ServerConfig& config, int fd, const std::string& client)
283{
284 const Request request = readRequest(fd);
285 if (request.method.empty()) {
286 // An idle (preconnected) connection that never sent a request: close it without
287 // answering. Had we answered, the browser could later read that answer as the reply
288 // to the request it sends on this connection.
289 return;
290 }
291
292 std::string response;
293 if (request.method == "GET" && request.path == "/api/tables") {
294 const std::scoped_lock lock(workMutex);
295 response = listTables(config);
296 } else if (request.method == "POST" && request.path == "/api/sql") {
297 const std::scoped_lock lock(workMutex); // one request at a time runs SQL
298 std::cout << std::format("[{}] POST /api/sql\n", client);
299 response = runSql(config, request.body);
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") {
303 response = errorResponse("404 Not Found", "Not found");
304 } else {
305 response = errorResponse("405 Method Not Allowed", "Only GET and POST are supported");
306 }
307 sendAll(fd, response);
308}
309
310} // namespace
311
312void runServer(const ServerConfig& config)
313{
314 Socket server(::socket(AF_INET, SOCK_STREAM, 0));
315 if (server.fd() < 0) {
316 throwSystemError("socket");
317 }
318 const int yes = 1;
319 ::setsockopt(server.fd(), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);
320
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));
327 }
328 if (::listen(server.fd(), 10) < 0) {
329 throwSystemError("listen");
330 }
331 std::cout << std::format("HTTP server listening on port {}\n", config.port) << std::flush;
332
333 while (true) {
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) {
339 continue;
340 }
341 throwSystemError("accept");
342 }
343
344 const timeval timeout{.tv_sec = kIdleSeconds, .tv_usec = 0};
345 ::setsockopt(client.fd(), SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof timeout);
346
347 const std::string ip = clientAddress(clientAddr);
348 const bool allowed =
349 config.allowedClients.empty() ||
350 std::ranges::any_of(config.allowedClients, [&](const std::string& prefix) { return ip.starts_with(prefix); });
351 if (!allowed) {
352 const std::scoped_lock lock(workMutex);
353 std::cout << std::format("[{}] rejected: not in the allowed clients\n", ip) << std::flush;
354 continue;
355 }
356
357 // Serve the connection in its own thread, so a slow or silent client never makes the
358 // others wait. The thread owns the socket (moved in) and closes it when it ends.
359 std::thread([config, ip, socket = std::move(client)] {
360 try {
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());
365 }
366 }).detach();
367 }
368}
369
370} // namespace tinysql
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.
Definition Output.cpp:67
std::string renderTablesJson(const std::vector< TableSchema > &tables)
Builds the JSON answer of GET /api/tables: every table with its columns.
Definition Output.cpp:131
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.
Definition Output.cpp:95
The small HTTP server behind --http: the page and its JSON API.
void runServer(const ServerConfig &config)
Serves connections until the process is stopped.
Definition Server.cpp:312
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.
Definition Util.cpp:82
bool equalsIgnoreCase(std::string_view a, std::string_view b)
Compares two strings, ignoring the case of ASCII letters.
Definition Util.cpp:49
std::string_view trim(std::string_view text)
Removes leading and trailing spaces, tabs and line ends.
Definition Util.cpp:34
const std::vector< TableSchema > & tables() const
All tables, in the order they appear in SYSTEM_TABLE.
Definition Catalog.hpp:63
static Catalog load(const std::filesystem::path &dataDir)
Reads SYSTEM_TABLE from a data directory.
Definition Catalog.cpp:74
Runs statements against the tables in one data directory.
Definition Session.hpp:42
std::vector< StatementResult > executeScript(std::string_view script)
Runs every statement of a script, in order.
Definition Session.cpp:123
A problem with one statement: bad syntax, unknown table, value too long...
Definition Util.hpp:16
Owns a socket descriptor and closes it when it goes out of scope (RAII).
Definition Server.cpp:46
Socket(Socket &&other) noexcept
Takes over another Socket's descriptor, here to hand a connection to its thread.
Definition Server.cpp:65
int fd() const
The descriptor, for system calls.
Definition Server.cpp:70
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.
Definition Server.cpp:53
int fd_
The descriptor, or -1 for none.
Definition Server.cpp:73
Socket(int fd)
Takes ownership of a descriptor.
Definition Server.cpp:50
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.
Definition Server.cpp:238
constexpr std::size_t kMaxRequestSize
The largest request read (headers and body), in bytes; the rest is ignored.
Definition Server.cpp:33
std::string_view contentTypeFor(std::string_view name)
The content type of a file of the page, from its extension.
Definition Server.cpp:193
bool isSafeFileName(std::string_view name)
Tells whether a requested file name is safe to serve from the web/ directory.
Definition Server.cpp:207
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.
Definition Server.cpp:95
std::string httpResponse(std::string_view status, std::string_view contentType, std::string_view body)
Builds a complete HTTP response.
Definition Server.cpp:174
std::string errorResponse(std::string_view status, std::string_view message)
Builds a JSON error answer, e.g. {"error":"Not found"}.
Definition Server.cpp:185
void sendAll(int fd, std::string_view data)
Sends all of data, retrying until it is written or the client goes away.
Definition Server.cpp:157
void handleClient(const ServerConfig &config, int fd, const std::string &client)
Serves one connection: reads the request, answers it, and closes.
Definition Server.cpp:282
Request readRequest(int fd)
Reads one HTTP request: the request line, the headers and, if there is one, the body.
Definition Server.cpp:115
std::mutex workMutex
Lets only one request at a time run SQL and write to the console.
Definition Server.cpp:43
std::string clientAddress(const sockaddr_in &address)
Formats a client's IPv4 address.
Definition Server.cpp:264
std::string serveFile(const ServerConfig &config, std::string_view name)
Serves one file of the page from the web/ directory.
Definition Server.cpp:218
std::string listTables(const ServerConfig &config)
Builds the JSON answer of GET /api/tables. Call it only while holding workMutex.
Definition Server.cpp:252
void throwSystemError(std::string_view what)
Throws the error of the last failed system call.
Definition Server.cpp:79
constexpr int kIdleSeconds
How long a connection may stay silent before it is closed, in seconds.
Definition Server.cpp:38
Settings of the browser server.
Definition Server.hpp:14
std::filesystem::path dataDir
The directory holding the tables.
Definition Server.hpp:16
std::vector< std::string > allowedClients
IP prefixes such as 192.168.; empty means everyone.
Definition Server.hpp:18
std::uint16_t port
TCP port to listen on.
Definition Server.hpp:15
std::filesystem::path webDir
The directory holding the page: index.html, style.css, app.js.
Definition Server.hpp:17
What running one statement produced: a table, a message, or an error.
Definition Session.hpp:29
One HTTP request, split into the parts the server uses.
Definition Server.cpp:85
std::string path
E.g. /api/sql; any ?query part is removed.
Definition Server.cpp:87
std::string body
The body of a POST (the SQL for /api/sql); empty for a GET.
Definition Server.cpp:88
std::string method
E.g. GET or POST; empty if the request could not be read.
Definition Server.cpp:86