http_server.cpp 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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. server_.set_payload_max_length(1024 * 1024 * 16); // 16MB max payload
  9. // Compression is auto-enabled when supported by client
  10. // Set keep-alive
  11. server_.set_keep_alive_max_count(100);
  12. server_.set_keep_alive_timeout(30);
  13. // Error handler - only set error content if no content already set
  14. server_.set_error_handler([](const httplib::Request& req, httplib::Response& res) {
  15. // Don't overwrite if content already set by route handler
  16. if (!res.body.empty()) {
  17. return;
  18. }
  19. nlohmann::json error;
  20. error["error"] = "Internal server error";
  21. error["status"] = res.status;
  22. res.set_content(error.dump(), "application/json");
  23. });
  24. // Exception handler
  25. server_.set_exception_handler([](const httplib::Request& req, httplib::Response& res,
  26. std::exception_ptr ep) {
  27. try {
  28. std::rethrow_exception(ep);
  29. } catch (const std::exception& e) {
  30. LOG_ERROR("Unhandled exception: {}", e.what());
  31. nlohmann::json error;
  32. error["error"] = "Internal server error";
  33. res.status = 500;
  34. res.set_content(error.dump(), "application/json");
  35. }
  36. });
  37. // Logger
  38. server_.set_logger([](const httplib::Request& req, const httplib::Response& res) {
  39. LOG_DEBUG("{} {} {} {}", req.method, req.path, res.status,
  40. res.get_header_value("Content-Length"));
  41. });
  42. }
  43. HttpServer::~HttpServer() {
  44. stop();
  45. }
  46. void HttpServer::start() {
  47. if (running_) {
  48. return;
  49. }
  50. // Setup static files
  51. setupStaticFiles();
  52. running_ = true;
  53. server_thread_ = std::thread([this] {
  54. LOG_INFO("HTTP server starting on port {}", config_.port);
  55. if (!server_.listen("0.0.0.0", config_.port)) {
  56. LOG_ERROR("Failed to start HTTP server on port {}", config_.port);
  57. }
  58. });
  59. }
  60. void HttpServer::stop() {
  61. if (!running_) {
  62. return;
  63. }
  64. running_ = false;
  65. server_.stop();
  66. if (server_thread_.joinable()) {
  67. server_thread_.join();
  68. }
  69. LOG_INFO("HTTP server stopped");
  70. }
  71. void HttpServer::registerRoute(const std::string& method,
  72. const std::string& pattern,
  73. auth::AuthMiddleware::Handler handler,
  74. auth::AuthMiddleware* auth,
  75. const std::string& required_role) {
  76. auto wrapped_handler = [handler, auth, required_role](const httplib::Request& req,
  77. httplib::Response& res) {
  78. if (auth) {
  79. if (!required_role.empty()) {
  80. auth->requireRole(req, res, required_role, handler);
  81. } else {
  82. auth->requireAuth(req, res, handler);
  83. }
  84. } else {
  85. auth::AuthContext ctx;
  86. handler(req, res, ctx);
  87. }
  88. };
  89. if (method == "GET") {
  90. server_.Get(pattern, wrapped_handler);
  91. } else if (method == "POST") {
  92. server_.Post(pattern, wrapped_handler);
  93. } else if (method == "PUT") {
  94. server_.Put(pattern, wrapped_handler);
  95. } else if (method == "PATCH") {
  96. server_.Patch(pattern, wrapped_handler);
  97. } else if (method == "DELETE") {
  98. server_.Delete(pattern, wrapped_handler);
  99. }
  100. }
  101. void HttpServer::setupStaticFiles() {
  102. std::filesystem::path static_path = config_.static_files_path;
  103. if (!std::filesystem::exists(static_path)) {
  104. LOG_WARN("Static files directory not found: {}", static_path.string());
  105. return;
  106. }
  107. // Serve static files with mount point as fallback
  108. server_.set_mount_point("/", static_path.string());
  109. // SPA fallback - catch-all route for client-side routing
  110. // Note: This handler serves index.html for SPA routes (paths without extensions)
  111. // Static files (with extensions) are handled by the mount point above
  112. std::filesystem::path index_path = static_path / "index.html";
  113. if (std::filesystem::exists(index_path)) {
  114. server_.Get(R"(/[^.]*$)", [index_path](const httplib::Request& req,
  115. httplib::Response& res) {
  116. // Don't intercept API, webhook, health, or WebSocket paths
  117. if (req.path.starts_with("/api") ||
  118. req.path.starts_with("/webhook") ||
  119. req.path.starts_with("/ws") ||
  120. req.path == "/health") {
  121. res.status = 404;
  122. res.set_content(R"({"error":"Not found"})", "application/json");
  123. return;
  124. }
  125. // Read index.html fresh on each request (no caching)
  126. std::ifstream file(index_path);
  127. if (!file.is_open()) {
  128. res.status = 500;
  129. res.set_content(R"({"error":"Failed to read index.html"})", "application/json");
  130. return;
  131. }
  132. std::string content((std::istreambuf_iterator<char>(file)),
  133. std::istreambuf_iterator<char>());
  134. // Prevent browser caching of index.html
  135. res.set_header("Cache-Control", "no-cache, no-store, must-revalidate");
  136. res.set_header("Pragma", "no-cache");
  137. res.set_header("Expires", "0");
  138. res.set_content(content, "text/html");
  139. });
  140. }
  141. LOG_INFO("Static files mounted from: {}", static_path.string());
  142. }
  143. } // namespace smartbotic::webserver