http_server.cpp 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. #include "http_server.hpp"
  2. #include "logging/logger.hpp"
  3. #include <filesystem>
  4. namespace smartbotic::webserver {
  5. HttpServer::HttpServer(const HttpServerConfig& config)
  6. : config_(config) {
  7. // Configure server
  8. //
  9. // This line already bounded every request body before this change - it
  10. // used to be a hardcoded 16 MB literal. The bound was real, just not
  11. // configurable, and it disagreed with the (previously unused) SIZE_MAX
  12. // default that httplib itself falls back to when nothing sets this at
  13. // all. Reading the limit from config makes the two paths agree and lets
  14. // it be raised deliberately: an upload node (see docs/nodes.md) needs to
  15. // carry up to a 16 MB image field, and that field cannot fit inside a
  16. // 16 MB total body once multipart framing is added on top, so the
  17. // shipped default here goes up to 32 MB.
  18. server_.set_payload_max_length(static_cast<size_t>(config_.max_upload_mb) * 1024 * 1024);
  19. // Compression is auto-enabled when supported by client
  20. // Set keep-alive
  21. server_.set_keep_alive_max_count(100);
  22. server_.set_keep_alive_timeout(30);
  23. // Error handler - only set error content if no content already set
  24. server_.set_error_handler([](const httplib::Request& req, httplib::Response& res) {
  25. // Don't overwrite if content already set by route handler
  26. if (!res.body.empty()) {
  27. return;
  28. }
  29. nlohmann::json error;
  30. error["error"] = "Internal server error";
  31. error["status"] = res.status;
  32. res.set_content(error.dump(), "application/json");
  33. });
  34. // Exception handler
  35. server_.set_exception_handler([](const httplib::Request& req, httplib::Response& res,
  36. std::exception_ptr ep) {
  37. try {
  38. std::rethrow_exception(ep);
  39. } catch (const std::exception& e) {
  40. LOG_ERROR("Unhandled exception: {}", e.what());
  41. nlohmann::json error;
  42. error["error"] = "Internal server error";
  43. res.status = 500;
  44. res.set_content(error.dump(), "application/json");
  45. }
  46. });
  47. // Logger
  48. server_.set_logger([](const httplib::Request& req, const httplib::Response& res) {
  49. LOG_DEBUG("{} {} {} {}", req.method, req.path, res.status,
  50. res.get_header_value("Content-Length"));
  51. });
  52. }
  53. HttpServer::~HttpServer() {
  54. stop();
  55. }
  56. void HttpServer::start() {
  57. if (running_) {
  58. return;
  59. }
  60. // Setup static files
  61. setupStaticFiles();
  62. running_ = true;
  63. server_thread_ = std::thread([this] {
  64. LOG_INFO("HTTP server starting on port {}", config_.port);
  65. if (!server_.listen("0.0.0.0", config_.port)) {
  66. LOG_ERROR("Failed to start HTTP server on port {}", config_.port);
  67. }
  68. });
  69. }
  70. void HttpServer::stop() {
  71. if (!running_) {
  72. return;
  73. }
  74. running_ = false;
  75. server_.stop();
  76. if (server_thread_.joinable()) {
  77. server_thread_.join();
  78. }
  79. LOG_INFO("HTTP server stopped");
  80. }
  81. void HttpServer::registerRoute(const std::string& method,
  82. const std::string& pattern,
  83. auth::AuthMiddleware::Handler handler,
  84. auth::AuthMiddleware* auth,
  85. const std::string& required_role) {
  86. auto wrapped_handler = [handler, auth, required_role](const httplib::Request& req,
  87. httplib::Response& res) {
  88. if (auth) {
  89. if (!required_role.empty()) {
  90. auth->requireRole(req, res, required_role, handler);
  91. } else {
  92. auth->requireAuth(req, res, handler);
  93. }
  94. } else {
  95. auth::AuthContext ctx;
  96. handler(req, res, ctx);
  97. }
  98. };
  99. if (method == "GET") {
  100. server_.Get(pattern, wrapped_handler);
  101. } else if (method == "POST") {
  102. server_.Post(pattern, wrapped_handler);
  103. } else if (method == "PUT") {
  104. server_.Put(pattern, wrapped_handler);
  105. } else if (method == "PATCH") {
  106. server_.Patch(pattern, wrapped_handler);
  107. } else if (method == "DELETE") {
  108. server_.Delete(pattern, wrapped_handler);
  109. }
  110. }
  111. void HttpServer::setupStaticFiles() {
  112. std::filesystem::path static_path = config_.static_files_path;
  113. if (!std::filesystem::exists(static_path)) {
  114. LOG_WARN("Static files directory not found: {}", static_path.string());
  115. return;
  116. }
  117. // Serve static files with mount point as fallback
  118. server_.set_mount_point("/", static_path.string());
  119. // Cache headers for anything the mount point serves.
  120. //
  121. // httplib answers file requests before it looks at registered handlers, so
  122. // a Get("/") handler never runs and "/" was going out with no headers at
  123. // all. The browser then applied its own judgement to the one document that
  124. // must not be cached: index.html names the hashed bundle to load, so a
  125. // stale copy points at a file that no longer exists, and the app breaks in
  126. // a way that looks like a bug in whatever the person was doing - a missing
  127. // function on an object, say.
  128. //
  129. // Everything under /assets carries a content hash in its name, so a given
  130. // address never changes what it holds and can be kept for good.
  131. server_.set_file_request_handler([](const httplib::Request& req, httplib::Response& res) {
  132. if (req.path.starts_with("/assets/")) {
  133. res.set_header("Cache-Control", "public, max-age=31536000, immutable");
  134. return;
  135. }
  136. if (req.path == "/" || req.path.ends_with(".html")) {
  137. res.set_header("Cache-Control", "no-cache, no-store, must-revalidate");
  138. res.set_header("Pragma", "no-cache");
  139. res.set_header("Expires", "0");
  140. }
  141. });
  142. // SPA fallback - catch-all route for client-side routing
  143. // Note: This handler serves index.html for SPA routes (paths without extensions)
  144. // Static files (with extensions) are handled by the mount point above
  145. std::filesystem::path index_path = static_path / "index.html";
  146. if (std::filesystem::exists(index_path)) {
  147. server_.Get(R"(/[^.]*$)", [index_path](const httplib::Request& req,
  148. httplib::Response& res) {
  149. // Don't intercept API, webhook, health, or WebSocket paths
  150. if (req.path.starts_with("/api") ||
  151. req.path.starts_with("/webhook") ||
  152. req.path.starts_with("/ws") ||
  153. req.path == "/health") {
  154. res.status = 404;
  155. res.set_content(R"({"error":"Not found"})", "application/json");
  156. return;
  157. }
  158. // Read index.html fresh on each request (no caching)
  159. std::ifstream file(index_path);
  160. if (!file.is_open()) {
  161. res.status = 500;
  162. res.set_content(R"({"error":"Failed to read index.html"})", "application/json");
  163. return;
  164. }
  165. std::string content((std::istreambuf_iterator<char>(file)),
  166. std::istreambuf_iterator<char>());
  167. // Prevent browser caching of index.html
  168. res.set_header("Cache-Control", "no-cache, no-store, must-revalidate");
  169. res.set_header("Pragma", "no-cache");
  170. res.set_header("Expires", "0");
  171. res.set_content(content, "text/html");
  172. });
  173. }
  174. LOG_INFO("Static files mounted from: {}", static_path.string());
  175. }
  176. } // namespace smartbotic::webserver