| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- #include "http_server.hpp"
- #include "logging/logger.hpp"
- #include <filesystem>
- namespace smartbotic::webserver {
- HttpServer::HttpServer(const HttpServerConfig& config)
- : config_(config) {
- // Configure server
- server_.set_payload_max_length(1024 * 1024 * 16); // 16MB max payload
- // Compression is auto-enabled when supported by client
- // Set keep-alive
- server_.set_keep_alive_max_count(100);
- server_.set_keep_alive_timeout(30);
- // Error handler - only set error content if no content already set
- server_.set_error_handler([](const httplib::Request& req, httplib::Response& res) {
- // Don't overwrite if content already set by route handler
- if (!res.body.empty()) {
- return;
- }
- nlohmann::json error;
- error["error"] = "Internal server error";
- error["status"] = res.status;
- res.set_content(error.dump(), "application/json");
- });
- // Exception handler
- server_.set_exception_handler([](const httplib::Request& req, httplib::Response& res,
- std::exception_ptr ep) {
- try {
- std::rethrow_exception(ep);
- } catch (const std::exception& e) {
- LOG_ERROR("Unhandled exception: {}", e.what());
- nlohmann::json error;
- error["error"] = "Internal server error";
- res.status = 500;
- res.set_content(error.dump(), "application/json");
- }
- });
- // Logger
- server_.set_logger([](const httplib::Request& req, const httplib::Response& res) {
- LOG_DEBUG("{} {} {} {}", req.method, req.path, res.status,
- res.get_header_value("Content-Length"));
- });
- }
- HttpServer::~HttpServer() {
- stop();
- }
- void HttpServer::start() {
- if (running_) {
- return;
- }
- // Setup static files
- setupStaticFiles();
- running_ = true;
- server_thread_ = std::thread([this] {
- LOG_INFO("HTTP server starting on port {}", config_.port);
- if (!server_.listen("0.0.0.0", config_.port)) {
- LOG_ERROR("Failed to start HTTP server on port {}", config_.port);
- }
- });
- }
- void HttpServer::stop() {
- if (!running_) {
- return;
- }
- running_ = false;
- server_.stop();
- if (server_thread_.joinable()) {
- server_thread_.join();
- }
- LOG_INFO("HTTP server stopped");
- }
- void HttpServer::registerRoute(const std::string& method,
- const std::string& pattern,
- auth::AuthMiddleware::Handler handler,
- auth::AuthMiddleware* auth,
- const std::string& required_role) {
- auto wrapped_handler = [handler, auth, required_role](const httplib::Request& req,
- httplib::Response& res) {
- if (auth) {
- if (!required_role.empty()) {
- auth->requireRole(req, res, required_role, handler);
- } else {
- auth->requireAuth(req, res, handler);
- }
- } else {
- auth::AuthContext ctx;
- handler(req, res, ctx);
- }
- };
- if (method == "GET") {
- server_.Get(pattern, wrapped_handler);
- } else if (method == "POST") {
- server_.Post(pattern, wrapped_handler);
- } else if (method == "PUT") {
- server_.Put(pattern, wrapped_handler);
- } else if (method == "PATCH") {
- server_.Patch(pattern, wrapped_handler);
- } else if (method == "DELETE") {
- server_.Delete(pattern, wrapped_handler);
- }
- }
- void HttpServer::setupStaticFiles() {
- std::filesystem::path static_path = config_.static_files_path;
- if (!std::filesystem::exists(static_path)) {
- LOG_WARN("Static files directory not found: {}", static_path.string());
- return;
- }
- // Serve static files with mount point as fallback
- server_.set_mount_point("/", static_path.string());
- // SPA fallback - catch-all route for client-side routing
- // Note: This handler serves index.html for SPA routes (paths without extensions)
- // Static files (with extensions) are handled by the mount point above
- std::filesystem::path index_path = static_path / "index.html";
- if (std::filesystem::exists(index_path)) {
- server_.Get(R"(/[^.]*$)", [index_path](const httplib::Request& req,
- httplib::Response& res) {
- // Don't intercept API, webhook, health, or WebSocket paths
- if (req.path.starts_with("/api") ||
- req.path.starts_with("/webhook") ||
- req.path.starts_with("/ws") ||
- req.path == "/health") {
- res.status = 404;
- res.set_content(R"({"error":"Not found"})", "application/json");
- return;
- }
- // Read index.html fresh on each request (no caching)
- std::ifstream file(index_path);
- if (!file.is_open()) {
- res.status = 500;
- res.set_content(R"({"error":"Failed to read index.html"})", "application/json");
- return;
- }
- std::string content((std::istreambuf_iterator<char>(file)),
- std::istreambuf_iterator<char>());
- // Prevent browser caching of index.html
- res.set_header("Cache-Control", "no-cache, no-store, must-revalidate");
- res.set_header("Pragma", "no-cache");
- res.set_header("Expires", "0");
- res.set_content(content, "text/html");
- });
- }
- LOG_INFO("Static files mounted from: {}", static_path.string());
- }
- } // namespace smartbotic::webserver
|