LCOV - code coverage report
Current view: top level - http/server - http_server_base.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 80.6 % 248 200
Test Date: 2026-04-21 17:49:55 Functions: 79.3 % 58 46

            Line data    Source code
       1              : #include "http_server_base.hpp"
       2              : #include "server_connection.hpp"
       3              : #include "request.hpp"
       4              : #include "response.hpp"
       5              : #include "../../util/logger.hpp"
       6              : #include "../../util/base64.hpp"
       7              : #include <filesystem>
       8              : 
       9              : namespace thinger::http {
      10              : 
      11              : // Route registration methods - GET
      12          214 : route& http_server_base::get(const std::string& path, route_callback_response_only handler) {
      13          214 :     return router_[method::GET][path] = handler;
      14              : }
      15              : 
      16            0 : route& http_server_base::get(const std::string& path, route_callback_json_response handler) {
      17            0 :     return router_[method::GET][path] = handler;
      18              : }
      19              : 
      20         4323 : route& http_server_base::get(const std::string& path, route_callback_request_response handler) {
      21         4323 :     return router_[method::GET][path] = handler;
      22              : }
      23              : 
      24            0 : route& http_server_base::get(const std::string& path, route_callback_request_json_response handler) {
      25            0 :     return router_[method::GET][path] = handler;
      26              : }
      27              : 
      28              : // Route registration methods - POST
      29           35 : route& http_server_base::post(const std::string& path, route_callback_response_only handler) {
      30           35 :     return router_[method::POST][path] = handler;
      31              : }
      32              : 
      33          227 : route& http_server_base::post(const std::string& path, route_callback_json_response handler) {
      34          227 :     return router_[method::POST][path] = handler;
      35              : }
      36              : 
      37          824 : route& http_server_base::post(const std::string& path, route_callback_request_response handler) {
      38          824 :     return router_[method::POST][path] = handler;
      39              : }
      40              : 
      41           54 : route& http_server_base::post(const std::string& path, route_callback_request_json_response handler) {
      42           54 :     return router_[method::POST][path] = handler;
      43              : }
      44              : 
      45              : // Route registration methods - PUT
      46           10 : route& http_server_base::put(const std::string& path, route_callback_response_only handler) {
      47           10 :     return router_[method::PUT][path] = handler;
      48              : }
      49              : 
      50           54 : route& http_server_base::put(const std::string& path, route_callback_json_response handler) {
      51           54 :     return router_[method::PUT][path] = handler;
      52              : }
      53              : 
      54          320 : route& http_server_base::put(const std::string& path, route_callback_request_response handler) {
      55          320 :     return router_[method::PUT][path] = handler;
      56              : }
      57              : 
      58           11 : route& http_server_base::put(const std::string& path, route_callback_request_json_response handler) {
      59           11 :     return router_[method::PUT][path] = handler;
      60              : }
      61              : 
      62              : // Route registration methods - DELETE
      63           31 : route& http_server_base::del(const std::string& path, route_callback_response_only handler) {
      64           31 :     return router_[method::DELETE][path] = handler;
      65              : }
      66              : 
      67            0 : route& http_server_base::del(const std::string& path, route_callback_json_response handler) {
      68            0 :     return router_[method::DELETE][path] = handler;
      69              : }
      70              : 
      71          295 : route& http_server_base::del(const std::string& path, route_callback_request_response handler) {
      72          295 :     return router_[method::DELETE][path] = handler;
      73              : }
      74              : 
      75            2 : route& http_server_base::del(const std::string& path, route_callback_request_json_response handler) {
      76            2 :     return router_[method::DELETE][path] = handler;
      77              : }
      78              : 
      79              : // Route registration methods - PATCH
      80           10 : route& http_server_base::patch(const std::string& path, route_callback_response_only handler) {
      81           10 :     return router_[method::PATCH][path] = handler;
      82              : }
      83              : 
      84           54 : route& http_server_base::patch(const std::string& path, route_callback_json_response handler) {
      85           54 :     return router_[method::PATCH][path] = handler;
      86              : }
      87              : 
      88          314 : route& http_server_base::patch(const std::string& path, route_callback_request_response handler) {
      89          314 :     return router_[method::PATCH][path] = handler;
      90              : }
      91              : 
      92            0 : route& http_server_base::patch(const std::string& path, route_callback_request_json_response handler) {
      93            0 :     return router_[method::PATCH][path] = handler;
      94              : }
      95              : 
      96              : // Route registration methods - HEAD
      97           25 : route& http_server_base::head(const std::string& path, route_callback_response_only handler) {
      98           25 :     return router_[method::HEAD][path] = handler;
      99              : }
     100              : 
     101            4 : route& http_server_base::head(const std::string& path, route_callback_request_response handler) {
     102            4 :     return router_[method::HEAD][path] = handler;
     103              : }
     104              : 
     105              : // Route registration methods - OPTIONS
     106           23 : route& http_server_base::options(const std::string& path, route_callback_response_only handler) {
     107           23 :     return router_[method::OPTIONS][path] = handler;
     108              : }
     109              : 
     110            0 : route& http_server_base::options(const std::string& path, route_callback_request_response handler) {
     111            0 :     return router_[method::OPTIONS][path] = handler;
     112              : }
     113              : 
     114              : // Middleware
     115           38 : void http_server_base::use(middleware_function middleware) {
     116           38 :     middlewares_.push_back(std::move(middleware));
     117           38 : }
     118              : 
     119              : // Basic Auth helpers
     120           22 : void http_server_base::set_basic_auth(const std::string& path_prefix, 
     121              :                                  const std::string& realm,
     122              :                                  auth_verify_function verify) {
     123           22 :     use([path_prefix, realm, verify](request& req, response& res, std::function<void()> next) {
     124              :         // Get the request path
     125           22 :         auto http_request = req.get_http_request();
     126           22 :         if (!http_request) {
     127            0 :             next();
     128            0 :             return;
     129              :         }
     130              :         
     131           22 :         const std::string& path = http_request->get_uri();
     132              :         
     133              :         // Check if this path requires auth
     134           22 :         if (!path.starts_with(path_prefix)) {
     135            2 :             next();
     136            2 :             return;
     137              :         }
     138              :         
     139              :         // Check for Authorization header
     140           20 :         if (!http_request->has_header("Authorization")) {
     141            4 :             res.status(http_response::status::unauthorized);
     142           12 :             res.header("WWW-Authenticate", "Basic realm=\"" + realm + "\"");
     143           12 :             res.send("Authentication required");
     144            4 :             return;
     145              :         }
     146              :         
     147           16 :         auto auth_header = http_request->get_header("Authorization");
     148           16 :         if (!auth_header.starts_with("Basic ")) {
     149            2 :             res.status(http_response::status::unauthorized);
     150            6 :             res.header("WWW-Authenticate", "Basic realm=\"" + realm + "\"");
     151            6 :             res.send("Invalid authentication");
     152            2 :             return;
     153              :         }
     154              :         
     155              :         // Decode base64 credentials
     156           14 :         auto encoded = auth_header.substr(6);
     157           14 :         std::string decoded;
     158              :         try {
     159           14 :             decoded = ::thinger::util::base64::decode(encoded);
     160            0 :         } catch (...) {
     161            0 :             res.status(http_response::status::unauthorized);
     162            0 :             res.send("Invalid credentials format");
     163            0 :             return;
     164            0 :         }
     165              :         
     166              :         // Parse username:password
     167           14 :         auto colon_pos = decoded.find(':');
     168           14 :         if (colon_pos == std::string::npos) {
     169            0 :             res.status(http_response::status::unauthorized);
     170            0 :             res.send("Invalid credentials format");
     171            0 :             return;
     172              :         }
     173              :         
     174           14 :         std::string username = decoded.substr(0, colon_pos);
     175           14 :         std::string password = decoded.substr(colon_pos + 1);
     176              :         
     177              :         // Verify credentials
     178           14 :         if (verify(username, password)) {
     179            8 :             req.set_auth_user(username);
     180            8 :             next();
     181              :         } else {
     182            6 :             res.status(http_response::status::unauthorized);
     183           18 :             res.header("WWW-Authenticate", "Basic realm=\"" + realm + "\"");
     184           24 :             res.send("Invalid username or password");
     185              :         }
     186           24 :     });
     187           22 : }
     188              : 
     189           12 : void http_server_base::set_basic_auth(const std::string& path_prefix,
     190              :                                  const std::string& realm,
     191              :                                  const std::string& username,
     192              :                                  const std::string& password) {
     193           12 :     set_basic_auth(path_prefix, realm, 
     194           24 :         [username, password](const std::string& u, const std::string& p) {
     195            4 :             return u == username && p == password;
     196              :         });
     197           12 : }
     198              : 
     199            6 : void http_server_base::set_basic_auth(const std::string& path_prefix,
     200              :                                  const std::string& realm,
     201              :                                  const std::map<std::string, std::string>& users) {
     202            6 :     set_basic_auth(path_prefix, realm,
     203           12 :         [users](const std::string& u, const std::string& p) {
     204            6 :             auto it = users.find(u);
     205            6 :             return it != users.end() && it->second == p;
     206              :         });
     207            6 : }
     208              : 
     209              : // Fallback handlers
     210            0 : void http_server_base::set_not_found_handler(route_callback_response_only handler) {
     211            0 :     router_.set_fallback_handler([handler](request& req, response& res) {
     212            0 :         handler(res);
     213            0 :     });
     214            0 : }
     215              : 
     216           10 : void http_server_base::set_not_found_handler(route_callback_request_response handler) {
     217           10 :     router_.set_fallback_handler(handler);
     218           10 : }
     219              : 
     220              : // Configuration
     221           16 : void http_server_base::enable_cors(bool enabled) {
     222           16 :     cors_enabled_ = enabled;
     223           16 : }
     224              : 
     225           68 : void http_server_base::enable_ssl(bool enabled) {
     226           68 :     ssl_enabled_ = enabled;
     227           68 : }
     228              : 
     229           14 : void http_server_base::set_connection_timeout(std::chrono::seconds timeout) {
     230           14 :     connection_timeout_ = timeout;
     231           14 : }
     232              : 
     233           12 : void http_server_base::set_max_body_size(size_t size) {
     234           12 :     max_body_size_ = size;
     235           12 : }
     236              : 
     237           20 : void http_server_base::set_max_listening_attempts(int attempts) {
     238           20 :     max_listening_attempts_ = attempts;
     239           20 : }
     240              : 
     241              : // Static file serving
     242           20 : void http_server_base::serve_static(const std::string& url_prefix,
     243              :                                const std::string& directory,
     244              :                                const std::string& fallback) {
     245              :     namespace fs = std::filesystem;
     246              : 
     247              :     // Normalize route: avoid double slash when prefix is "/"
     248           20 :     std::string route = url_prefix;
     249           20 :     if (!route.empty() && route.back() == '/') route.pop_back();
     250           20 :     route += "/:path(.*)";
     251              : 
     252           34 :     get(route, [directory, fallback](request& req, response& res) {
     253           14 :         std::string path = req["path"];
     254              : 
     255           14 :         auto canonical_dir = fs::canonical(directory);
     256           14 :         bool has_fallback = !fallback.empty();
     257              : 
     258              :         // Empty path means root request — try fallback file directly
     259           14 :         if (path.empty()) {
     260            4 :             if (has_fallback) {
     261            2 :                 auto fallback_file = canonical_dir / fallback;
     262            2 :                 if (fs::exists(fallback_file) && fs::is_regular_file(fallback_file)) {
     263            2 :                     res.send_file(fallback_file);
     264            2 :                     return;
     265              :                 }
     266            2 :             }
     267            2 :             res.status(http_response::status::not_found);
     268            6 :             res.send("Not found");
     269            2 :             return;
     270              :         }
     271              : 
     272              :         // Construct full file path
     273           10 :         fs::path file_path = fs::path(directory) / path;
     274           10 :         auto canonical_file = fs::weakly_canonical(file_path);
     275              : 
     276              :         // Security: ensure the resolved path is within the directory
     277           10 :         if (!canonical_file.string().starts_with(canonical_dir.string())) {
     278            0 :             res.status(http_response::status::forbidden);
     279            0 :             res.send("Access denied");
     280            0 :             return;
     281              :         }
     282              : 
     283              :         // Serve file if it exists
     284           10 :         if (fs::exists(canonical_file)) {
     285            6 :             if (fs::is_regular_file(canonical_file)) {
     286            4 :                 res.send_file(canonical_file);
     287            4 :                 return;
     288              :             }
     289            2 :             if (fs::is_directory(canonical_file) && has_fallback) {
     290            2 :                 auto fallback_file = canonical_file / fallback;
     291            2 :                 if (fs::exists(fallback_file) && fs::is_regular_file(fallback_file)) {
     292            2 :                     res.send_file(fallback_file);
     293            2 :                     return;
     294              :                 }
     295            2 :             }
     296              :         }
     297              : 
     298              :         // SPA fallback: serve root fallback file for non-existent paths
     299            4 :         if (has_fallback) {
     300            2 :             auto fallback_file = canonical_dir / fallback;
     301            2 :             if (fs::exists(fallback_file) && fs::is_regular_file(fallback_file)) {
     302            2 :                 res.send_file(fallback_file);
     303            2 :                 return;
     304              :             }
     305            2 :         }
     306              : 
     307            2 :         res.status(http_response::status::not_found);
     308            6 :         res.send("Not found");
     309           42 :     });
     310           20 : }
     311              : 
     312              : // Server control
     313          708 : bool http_server_base::listen(const std::string& host, uint16_t port) {
     314          708 :     host_ = host;
     315          708 :     port_ = std::to_string(port);
     316          708 :     use_unix_socket_ = false;
     317              :     
     318              :     // Create socket server using virtual method
     319          708 :     socket_server_ = create_socket_server(host, port_);
     320          708 :     if (!socket_server_) {
     321            0 :         LOG_ERROR("Failed to create socket server");
     322            0 :         return false;
     323              :     }
     324              :     
     325              :     // Configure socket server
     326          708 :     socket_server_->set_max_listening_attempts(max_listening_attempts_);
     327              :     
     328              :     // Setup connection handler
     329          708 :     setup_connection_handler();
     330              :     
     331              :     // Start listening
     332          708 :     return socket_server_->start();
     333              : }
     334              : 
     335           48 : bool http_server_base::listen_unix(const std::string& unix_path) {
     336           48 :     unix_path_ = unix_path;
     337           48 :     use_unix_socket_ = true;
     338              :     
     339              :     // Create Unix socket server using virtual method
     340           48 :     socket_server_ = create_unix_socket_server(unix_path);
     341           48 :     if (!socket_server_) {
     342            0 :         LOG_ERROR("Failed to create Unix socket server");
     343            0 :         return false;
     344              :     }
     345              :     
     346              :     // Configure socket server
     347           48 :     socket_server_->set_max_listening_attempts(max_listening_attempts_);
     348              :     
     349              :     // Setup connection handler
     350           48 :     setup_connection_handler();
     351              :     
     352              :     // Start listening
     353           48 :     return socket_server_->start();
     354              : }
     355              : 
     356            0 : bool http_server_base::start(uint16_t port) {
     357            0 :     return start("0.0.0.0", port);
     358              : }
     359              : 
     360            0 : bool http_server_base::start(uint16_t port, const std::function<void()> &on_listening) {
     361            0 :     return start("0.0.0.0", port, on_listening);
     362              : }
     363              : 
     364            0 : bool http_server_base::start(const std::string& host, uint16_t port) {
     365            0 :     return start(host, port, nullptr);
     366              : }
     367              : 
     368           54 : bool http_server_base::start(const std::string& host, uint16_t port, const std::function<void()> &on_listening) {
     369           54 :     if (!listen(host, port)) {
     370            0 :         return false;
     371              :     }
     372           54 :     if (on_listening) {
     373           54 :         on_listening();
     374              :     }
     375           54 :     wait();
     376           54 :     return true;
     377              : }
     378              : 
     379            0 : bool http_server_base::start_unix(const std::string& unix_path) {
     380            0 :     return start_unix(unix_path, nullptr);
     381              : }
     382              : 
     383            0 : bool http_server_base::start_unix(const std::string& unix_path, const std::function<void()> &on_listening) {
     384            0 :     if (!listen_unix(unix_path)) {
     385            0 :         return false;
     386              :     }
     387            0 :     if (on_listening) {
     388            0 :         on_listening();
     389              :     }
     390            0 :     wait();
     391            0 :     return true;
     392              : }
     393              : 
     394          798 : bool http_server_base::stop() {
     395          798 :     if (socket_server_) {
     396          750 :         bool result = socket_server_->stop();
     397          750 :         socket_server_.reset();
     398          750 :         return result;
     399              :     }
     400           48 :     return false;
     401              : }
     402              : 
     403           78 : bool http_server_base::is_listening() const {
     404           78 :     return socket_server_ != nullptr && socket_server_->is_running();
     405              : }
     406              : 
     407          677 : uint16_t http_server_base::local_port() const {
     408          677 :     return socket_server_ ? socket_server_->local_port() : 0;
     409              : }
     410              : 
     411              : 
     412              : // Private methods
     413          756 : void http_server_base::setup_connection_handler() {
     414          756 :     socket_server_->set_handler([this](std::shared_ptr<asio::socket> socket) {
     415              :         // Create HTTP connection
     416          992 :         auto connection = std::make_shared<server_connection>(socket);
     417              : 
     418              :         // Set request handler — awaitable coroutine with three-way dispatch
     419         2044 :         connection->set_handler([this](std::shared_ptr<request> req) -> awaitable<void> {
     420              :             auto http_connection = req->get_http_connection();
     421              :             auto stream = req->get_http_stream();
     422              :             auto http_request = req->get_http_request();
     423              : 
     424              :             if (!http_connection || !stream) {
     425              :                 LOG_ERROR("Invalid connection or stream");
     426              :                 co_return;
     427              :             }
     428              : 
     429              :             // 1. Match route
     430              :             auto* matched_route = router_.find_route(req);
     431              : 
     432              :             // 2. Run middlewares (synchronous)
     433              :             bool passed = false;
     434         1038 :             execute_middlewares(*req, stream, 0, [&passed]() { passed = true; });
     435              :             if (!passed) co_return;
     436              : 
     437              :             // 3. Three-way dispatch
     438              :             response res(http_connection, stream, http_request, cors_enabled_);
     439              : 
     440              :             if (!matched_route) {
     441              :                 // No route matched → fallback / 404
     442              :                 router_.handle_unmatched(req);
     443              :             } else if (matched_route->is_deferred_body()) {
     444              :                 // DEFERRED: handler reads body at its discretion
     445              :                 co_await matched_route->handle_request_coro(*req, res);
     446              :             } else if (http_request->has_pending_body()) {
     447              :                 // PENDING BODY: check size limit, read, then dispatch
     448              :                 if (!http_request->is_chunked_transfer() && req->content_length() > max_body_size_) {
     449              :                     res.error(http_response::status::payload_too_large, "Payload Too Large");
     450              :                     co_return;
     451              :                 }
     452              :                 req->set_max_body_size(max_body_size_);
     453              :                 bool ok = co_await req->read_body();
     454              :                 if (!ok) {
     455              :                     res.error(http_response::status::payload_too_large, "Payload Too Large");
     456              :                     co_return;
     457              :                 }
     458              :                 matched_route->handle_request(*req, res);
     459              :             } else {
     460              :                 // NO BODY: dispatch directly
     461              :                 matched_route->handle_request(*req, res);
     462              :             }
     463              : 
     464              :             co_return;
     465         2104 :         });
     466              : 
     467              :         // Start handling the connection with configured timeout
     468          992 :         connection->start(connection_timeout_);
     469          992 :     });
     470          756 : }
     471              : 
     472         1070 : void http_server_base::execute_middlewares(request& req, std::shared_ptr<http_stream> stream, 
     473              :                                       size_t index, std::function<void()> final_handler) {
     474         1070 :     if (index >= middlewares_.size()) {
     475              :         // All middlewares executed, call final handler
     476         1038 :         final_handler();
     477         1038 :         return;
     478              :     }
     479              :     
     480              :     // Create response object for middleware
     481           32 :     auto connection = req.get_http_connection();
     482           32 :     auto http_request = req.get_http_request();
     483              :     
     484           32 :     if (connection) {
     485           32 :         response res(connection, stream, http_request, cors_enabled_);
     486              :         
     487              :         // Execute current middleware
     488           32 :         middlewares_[index](req, res, [this, &req, stream, index, final_handler]() {
     489              :             // Middleware called next(), execute next middleware
     490           18 :             execute_middlewares(req, stream, index + 1, final_handler);
     491           18 :         });
     492           32 :     }
     493           32 : }
     494              : 
     495              : } // namespace thinger::http
        

Generated by: LCOV version 2.0-1