libpromeki 1.0.0-alpha
PROfessional MEdia toolKIt
 
Loading...
Searching...
No Matches
httpserver.h
Go to the documentation of this file.
1
8#pragma once
9
10
11#include <promeki/config.h>
12#if PROMEKI_ENABLE_HTTP
13#include <functional>
14#include <promeki/function.h>
15#include <promeki/namespace.h>
16#include <promeki/objectbase.h>
17#include <promeki/error.h>
19#include <promeki/list.h>
20#include <promeki/uniqueptr.h>
21#include <promeki/httprouter.h>
22#include <promeki/httprequest.h>
25#include <promeki/variant.h>
28#include <promeki/variantspec.h>
29#include <promeki/json.h>
30#include <promeki/stringlist.h>
31#include <promeki/sslcontext.h>
32
33PROMEKI_NAMESPACE_BEGIN
34
35class TcpServer;
36class EventLoop;
37class WebSocket;
38
82class HttpServer : public ObjectBase {
83 PROMEKI_OBJECT(HttpServer, ObjectBase)
84 public:
92 explicit HttpServer(ObjectBase *parent = nullptr);
93
95 ~HttpServer() override;
96
97 // ----------------------------------------------------
98 // Listening
99 // ----------------------------------------------------
100
110 Error listen(const SocketAddress &address, int backlog = 50);
111
115 Error listen(uint16_t port, int backlog = 50);
116
118 void close();
119
121 bool isListening() const;
122
124 SocketAddress serverAddress() const;
125
126 // ----------------------------------------------------
127 // Routing
128 // ----------------------------------------------------
129
131 HttpRouter &router() { return _router; }
132
134 const HttpRouter &router() const { return _router; }
135
137 void route(const String &pattern, const HttpMethod &method, HttpHandlerFunc handler);
138
140 void route(const String &pattern, const HttpMethod &method, HttpHandler::Ptr handler);
141
143 void any(const String &pattern, HttpHandlerFunc handler);
144
146 void use(HttpMiddleware middleware);
147
165 using WebSocketHandler = Function<void(WebSocket *socket, const HttpRequest &request)>;
166
185 void routeWebSocket(const String &pattern, WebSocketHandler handler);
186
187 // ----------------------------------------------------
188 // Reflection adapters
189 // ----------------------------------------------------
190
213 template <CompiledString N>
214 void exposeDatabase(const String &mountPath, VariantDatabase<N> &db, bool readOnly = false);
215
232 template <typename T> void exposeLookup(const String &mountPath, T &target);
233
234 // ----------------------------------------------------
235 // Per-connection tuning (applied to every new connection)
236 // ----------------------------------------------------
237
239 void setIdleTimeoutMs(unsigned int ms) { _idleTimeoutMs = ms; }
240
242 void setMaxBodyBytes(int64_t bytes) { _maxBodyBytes = bytes; }
243
251 int connectionCount() const;
252
270 void setSslContext(SslContext ctx) { _sslContext = std::move(ctx); }
271
273 SslContext sslContext() const { return _sslContext; }
274
280 static bool hasTlsSupport() { return SslContext::hasTlsSupport(); }
281
283 PROMEKI_SIGNAL(requestReceived, HttpRequest);
284
286 PROMEKI_SIGNAL(responseSent, HttpRequest, HttpResponse);
287
289 PROMEKI_SIGNAL(errorOccurred, Error);
290
291 private:
292 void onNewConnection();
293 void dispatchRequest(HttpRequest &request, HttpResponse &response);
294 void reapClosedConnection(HttpConnection *conn);
295
296 EventLoop *_loop = nullptr;
297 UniquePtr<TcpServer> _tcpServer;
298 int _acceptHandle = -1;
299 HttpRouter _router;
300 HttpConnection::List _connections;
301
302 unsigned int _idleTimeoutMs = HttpConnection::DefaultIdleTimeoutMs;
303 int64_t _maxBodyBytes = HttpConnection::DefaultMaxBodyBytes;
304 SslContext _sslContext;
305
306 // Helpers for the reflection adapters; non-template
307 // because the per-key plumbing doesn't depend on the
308 // database's compile-time Name tag.
309 static JsonObject specToJson(const VariantSpec &spec);
310 static String lookupPathToKey(const String &slashPath);
311};
312
313// ============================================================
314// Reflection adapter template definitions
315// ============================================================
316
317template <CompiledString N>
318void HttpServer::exposeDatabase(const String &mountPath, VariantDatabase<N> &db, bool readOnly) {
319 using DB = VariantDatabase<N>;
320 using ID = typename DB::ID;
321
322 // Full snapshot.
323 route(mountPath, HttpMethod::Get, [&db](const HttpRequest &, HttpResponse &res) { res.setJson(db.toJson()); });
324
325 // Schema introspection. Keys not yet present in the
326 // database still appear here when a spec was declared at
327 // static-init time.
328 const String schemaPath = mountPath + "/_schema";
329 route(schemaPath, HttpMethod::Get, [](const HttpRequest &, HttpResponse &res) {
330 JsonObject out;
331 const auto specs = DB::registeredSpecs();
332 for (auto it = specs.cbegin(); it != specs.cend(); ++it) {
333 const String name = ID::fromId(it->first).name();
334 out.set(name, HttpServer::specToJson(it->second));
335 }
336 res.setJson(out);
337 });
338
339 // Per-key get.
340 const String keyPath = mountPath + "/{key}";
341 route(keyPath, HttpMethod::Get, [&db](const HttpRequest &req, HttpResponse &res) {
342 const String name = req.pathParam("key");
343 ID id = ID::find(name);
344 if (!id.isValid() || !db.contains(id)) {
345 res = HttpResponse::notFound(String("Unknown key: ") + name);
346 return;
347 }
348 JsonObject out;
349 out.setFromVariant(name, db.get(id));
350 const VariantSpec *sp = DB::spec(id);
351 if (sp != nullptr) {
352 out.set("_spec", HttpServer::specToJson(*sp));
353 }
354 res.setJson(out);
355 });
356
357 if (readOnly) return;
358
359 // Per-key set.
360 route(keyPath, HttpMethod::Put, [&db](const HttpRequest &req, HttpResponse &res) {
361 const String name = req.pathParam("key");
362 Error perr;
363 JsonObject body = req.bodyAsJson(&perr);
364 if (perr.isError() || !body.contains("value")) {
365 res = HttpResponse::badRequest("Body must be JSON object with a \"value\" field");
366 return;
367 }
368 // Snapshot the value via JsonObject::forEach,
369 // which converts the underlying nlohmann json
370 // into a typed @ref Variant — that's the path
371 // that setFromJson knows how to coerce
372 // against the registered spec.
373 ID id(name);
374 Variant captured;
375 body.forEach([&](const String &k, const Variant &val) {
376 if (k == "value") captured = val;
377 });
378 Error serr = db.setFromJson(id, captured);
379 if (serr.isError()) {
380 res = HttpResponse::badRequest(String("Validation failed for key '") + name +
381 "': " + serr.desc());
382 return;
383 }
384 JsonObject out;
385 out.setFromVariant(name, db.get(id));
386 res.setJson(out);
387 });
388
389 // Per-key delete.
390 route(keyPath, HttpMethod::Delete, [&db](const HttpRequest &req, HttpResponse &res) {
391 const String name = req.pathParam("key");
392 ID id = ID::find(name);
393 if (!id.isValid() || !db.contains(id)) {
394 res = HttpResponse::notFound(String("Unknown key: ") + name);
395 return;
396 }
397 db.remove(id);
398 res = HttpResponse::noContent();
399 });
400}
401
402template <typename T> void HttpServer::exposeLookup(const String &mountPath, T &target) {
403 const String greedy = mountPath + "/{path:*}";
404 route(greedy, HttpMethod::Get, [&target](const HttpRequest &req, HttpResponse &res) {
405 const String key = HttpServer::lookupPathToKey(req.pathParam("path"));
406 if (key.isEmpty()) {
407 res = HttpResponse::badRequest("Empty lookup key");
408 return;
409 }
410 Error err;
411 auto v = VariantLookup<T>::resolve(target, key, &err);
412 if (!v.hasValue() || err.isError()) {
413 res = HttpResponse::notFound(String("Unknown lookup: ") + key);
414 return;
415 }
416 JsonObject out;
417 out.setFromVariant("value", *v);
418 res.setJson(out);
419 });
420}
421
422PROMEKI_NAMESPACE_END
423
424#endif // PROMEKI_ENABLE_HTTP