LCOV - code coverage report
Current view: top level - http/client - http_client_base.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 86.7 % 90 78
Test Date: 2026-04-21 17:49:55 Functions: 80.0 % 20 16

            Line data    Source code
       1              : #include "http_client_base.hpp"
       2              : #include "websocket_util.hpp"
       3              : #include "../../util/logger.hpp"
       4              : #include "../../asio/sockets/ssl_socket.hpp"
       5              : #include "../../asio/sockets/unix_socket.hpp"
       6              : #include "../../asio/sockets/websocket.hpp"
       7              : 
       8              : namespace thinger::http {
       9              : 
      10          809 : http_client_base::~http_client_base() {
      11          809 :     LOG_DEBUG("Destroying HTTP client base");
      12          809 :     pool_.clear();
      13          809 : }
      14              : 
      15            0 : bool http_client_base::is_same_origin(const std::string& url1, const std::string& url2) {
      16            0 :     http_request req1, req2;
      17            0 :     if (!req1.set_url(url1) || !req2.set_url(url2)) {
      18            0 :         return false;
      19              :     }
      20            0 :     return req1.get_base_path() == req2.get_base_path();
      21            0 : }
      22              : 
      23          874 : std::shared_ptr<http_request> http_client_base::create_request(method m, const std::string& url) {
      24          874 :     auto request = std::make_shared<http_request>();
      25          874 :     request->set_method(m);
      26          874 :     request->set_url(url);
      27          874 :     if (!unix_socket_.empty()) {
      28           57 :         request->set_unix_socket(unix_socket_);
      29              :     }
      30          874 :     apply_default_headers(request);
      31          874 :     return request;
      32            0 : }
      33              : 
      34          914 : void http_client_base::apply_default_headers(const std::shared_ptr<http_request>& request) {
      35          914 :     if (!request->has_header("User-Agent")) {
      36         2742 :         request->add_header("User-Agent", user_agent_);
      37              :     }
      38          914 :     if (auto_decompress_ && !request->has_header("Accept-Encoding")) {
      39         4530 :         request->add_header("Accept-Encoding", "gzip, deflate");
      40              :     }
      41          914 : }
      42              : 
      43          940 : std::shared_ptr<client_connection> http_client_base::get_or_create_connection(
      44              :     const std::shared_ptr<http_request>& request) {
      45              : 
      46          940 :     auto& io_context = get_io_context();
      47          940 :     const std::string& socket_path = request->get_unix_socket();
      48              : 
      49              :     // Try to get existing connection from pool
      50          940 :     std::shared_ptr<client_connection> connection;
      51          940 :     if (socket_path.empty()) {
      52         1766 :         connection = pool_.get_connection(request->get_host(),
      53          883 :                                           std::stoi(request->get_port()),
      54         1766 :                                           request->is_ssl());
      55              :     } else {
      56           57 :         connection = pool_.get_unix_connection(socket_path);
      57              :     }
      58              : 
      59              :     // If found in pool and still open, reuse it
      60          940 :     if (connection && connection->is_open()) {
      61           46 :         LOG_DEBUG("Reusing connection from pool for {}", request->get_host());
      62           46 :         connection->set_max_content_size(max_content_size_);
      63           46 :         return connection;
      64              :     }
      65              : 
      66              :     // Create new connection
      67          894 :     LOG_DEBUG("Creating new connection for {}", request->get_host());
      68              : 
      69          894 :     if (socket_path.empty()) {
      70          837 :         std::shared_ptr<thinger::asio::socket> sock;
      71          837 :         if (!request->is_ssl()) {
      72          780 :             sock = std::make_shared<thinger::asio::tcp_socket>("http_client", io_context);
      73              :         } else {
      74              :             auto ssl_context = std::make_shared<boost::asio::ssl::context>(
      75           57 :                 boost::asio::ssl::context::sslv23_client);
      76           57 :             ssl_context->set_default_verify_paths();
      77           57 :             if (!verify_ssl_) {
      78           54 :                 ssl_context->set_verify_mode(boost::asio::ssl::verify_none);
      79              :             }
      80           57 :             sock = std::make_shared<thinger::asio::ssl_socket>("http_client", io_context, ssl_context);
      81           57 :         }
      82          837 :         connection = std::make_shared<client_connection>(sock, timeout_);
      83          837 :         connection->set_max_content_size(max_content_size_);
      84              : 
      85              :         // Store in pool for reuse
      86         1674 :         pool_.store_connection(request->get_host(),
      87          837 :                               std::stoi(request->get_port()),
      88          837 :                               request->is_ssl(),
      89              :                               connection);
      90          837 :     } else {
      91           57 :         auto sock = std::make_shared<thinger::asio::unix_socket>("http_client", io_context);
      92           57 :         connection = std::make_shared<client_connection>(sock, socket_path, timeout_);
      93           57 :         connection->set_max_content_size(max_content_size_);
      94              : 
      95              :         // Store in pool for reuse
      96           57 :         pool_.store_unix_connection(socket_path, connection);
      97           57 :     }
      98              : 
      99          894 :     return connection;
     100            0 : }
     101              : 
     102              : // HTTP methods implementation
     103          611 : awaitable<client_response> http_client_base::get(const std::string& url, headers_map headers) {
     104              :     auto request = create_request(method::GET, url);
     105              :     for (const auto& [key, value] : headers) {
     106              :         request->add_header(key, value);
     107              :     }
     108              :     co_return co_await send(request);
     109         1222 : }
     110              : 
     111          114 : awaitable<client_response> http_client_base::post(const std::string& url, std::string body,
     112              :                                                   std::string content_type, headers_map headers) {
     113              :     auto request = create_request(method::POST, url);
     114              :     if (!body.empty()) {
     115              :         request->set_content(std::move(body), std::move(content_type));
     116              :     }
     117              :     for (const auto& [key, value] : headers) {
     118              :         request->add_header(key, value);
     119              :     }
     120              :     co_return co_await send(request);
     121          228 : }
     122              : 
     123            0 : awaitable<client_response> http_client_base::post(const std::string& url, const form& form, headers_map headers) {
     124            0 :     return post(url, form.body(), form.content_type(), std::move(headers));
     125              : }
     126              : 
     127           19 : awaitable<client_response> http_client_base::put(const std::string& url, std::string body,
     128              :                                                  std::string content_type, headers_map headers) {
     129              :     auto request = create_request(method::PUT, url);
     130              :     if (!body.empty()) {
     131              :         request->set_content(std::move(body), std::move(content_type));
     132              :     }
     133              :     for (const auto& [key, value] : headers) {
     134              :         request->add_header(key, value);
     135              :     }
     136              :     co_return co_await send(request);
     137           38 : }
     138              : 
     139           15 : awaitable<client_response> http_client_base::patch(const std::string& url, std::string body,
     140              :                                                    std::string content_type, headers_map headers) {
     141              :     auto request = create_request(method::PATCH, url);
     142              :     if (!body.empty()) {
     143              :         request->set_content(std::move(body), std::move(content_type));
     144              :     }
     145              :     for (const auto& [key, value] : headers) {
     146              :         request->add_header(key, value);
     147              :     }
     148              :     co_return co_await send(request);
     149           30 : }
     150              : 
     151            7 : awaitable<client_response> http_client_base::del(const std::string& url, headers_map headers) {
     152              :     auto request = create_request(method::DELETE, url);
     153              :     for (const auto& [key, value] : headers) {
     154              :         request->add_header(key, value);
     155              :     }
     156              :     co_return co_await send(request);
     157           14 : }
     158              : 
     159            7 : awaitable<client_response> http_client_base::head(const std::string& url, headers_map headers) {
     160              :     auto request = create_request(method::HEAD, url);
     161              :     for (const auto& [key, value] : headers) {
     162              :         request->add_header(key, value);
     163              :     }
     164              :     co_return co_await send(request);
     165           14 : }
     166              : 
     167            7 : awaitable<client_response> http_client_base::options(const std::string& url, headers_map headers) {
     168              :     auto request = create_request(method::OPTIONS, url);
     169              :     for (const auto& [key, value] : headers) {
     170              :         request->add_header(key, value);
     171              :     }
     172              :     co_return co_await send(request);
     173           14 : }
     174              : 
     175          892 : awaitable<client_response> http_client_base::send(std::shared_ptr<http_request> request) {
     176              :     auto connection = get_or_create_connection(request);
     177              :     co_return co_await send_with_redirects(std::move(request), connection, 0);
     178         1784 : }
     179              : 
     180            2 : awaitable<stream_result> http_client_base::send_streaming(std::shared_ptr<http_request> request,
     181              :                                                           stream_callback callback) {
     182              :     // Force no compression for streaming - we can't decompress chunks on the fly
     183              :     // Set this BEFORE apply_default_headers so it won't add gzip/deflate
     184              :     if (!request->has_header("Accept-Encoding")) {
     185              :         request->add_header("Accept-Encoding", "identity");
     186              :     }
     187              :     apply_default_headers(request);
     188              :     auto connection = get_or_create_connection(request);
     189              :     co_return co_await connection->send_request_streaming(std::move(request), std::move(callback));
     190            4 : }
     191              : 
     192              : awaitable<std::pair<client_response, std::shared_ptr<client_connection>>>
     193            0 : http_client_base::send_with_connection(std::shared_ptr<http_request> request) {
     194              :     auto connection = get_or_create_connection(request);
     195              :     auto response = co_await send_with_redirects(request, connection, 0);
     196              :     co_return std::make_pair(std::move(response), connection);
     197            0 : }
     198              : 
     199          938 : awaitable<client_response> http_client_base::send_with_redirects(
     200              :     std::shared_ptr<http_request> request,
     201              :     std::shared_ptr<client_connection> connection,
     202              :     unsigned int redirect_count) {
     203              : 
     204              :     // Send request
     205              :     auto response = co_await connection->send_request(request);
     206              : 
     207              :     if (!response) {
     208              :         co_return client_response(boost::asio::error::invalid_argument, nullptr);
     209              :     }
     210              : 
     211              :     // Handle redirects
     212              :     if (follow_redirects_ && response->is_redirect_response() &&
     213              :         redirect_count < max_redirects_ && response->has_header("Location")) {
     214              : 
     215              :         std::string location = response->get_header("Location");
     216              :         LOG_DEBUG("Following redirect #{} to: {}", redirect_count + 1, location);
     217              : 
     218              :         // Determine redirect method based on status code
     219              :         method redirect_method = request->get_method();
     220              :         int status = response->get_status_code();
     221              : 
     222              :         // 303 See Other always changes to GET
     223              :         if (status == 303) {
     224              :             redirect_method = method::GET;
     225              :         }
     226              :         // 301/302 traditionally change POST/PUT/DELETE to GET (browser behavior)
     227              :         else if ((status == 301 || status == 302) &&
     228              :                  (request->get_method() == method::POST ||
     229              :                   request->get_method() == method::PUT ||
     230              :                   request->get_method() == method::DELETE)) {
     231              :             redirect_method = method::GET;
     232              :         }
     233              :         // 307 and 308 preserve the original method
     234              : 
     235              :         // Create redirect request
     236              :         auto redirect_request = create_request(redirect_method, location);
     237              : 
     238              :         // Preserve body for 307/308
     239              :         if ((status == 307 || status == 308) && !request->get_body().empty()) {
     240              :             redirect_request->set_content(request->get_body());
     241              :             if (request->has_header("Content-Type")) {
     242              :                 redirect_request->add_header("Content-Type", request->get_header("Content-Type"));
     243              :             }
     244              :             if (request->has_header("Content-Length")) {
     245              :                 redirect_request->add_header("Content-Length", request->get_header("Content-Length"));
     246              :             }
     247              :         }
     248              : 
     249              :         // Copy Authorization only for same origin
     250              :         std::string original_url = request->get_url();
     251              :         if (request->has_header("Authorization") && is_same_origin(original_url, location)) {
     252              :             redirect_request->add_header("Authorization", request->get_header("Authorization"));
     253              :             LOG_DEBUG("Preserving Authorization header for same-origin redirect");
     254              :         }
     255              : 
     256              :         // Handle cookies
     257              :         if (request->get_cookie_store().update_from_headers(*response)) {
     258              :             redirect_request->set_header(header::cookie, request->get_cookie_store().get_cookie_string());
     259              :         }
     260              : 
     261              :         // Get connection for redirect (may be different host)
     262              :         auto redirect_connection = get_or_create_connection(redirect_request);
     263              : 
     264              :         co_return co_await send_with_redirects(redirect_request, redirect_connection, redirect_count + 1);
     265              :     }
     266              : 
     267              :     co_return client_response(boost::system::error_code{}, response);
     268         1876 : }
     269              : 
     270              : // Simple URL version delegates to the request version
     271           26 : awaitable<std::optional<websocket_client>> http_client_base::upgrade_websocket(
     272              :     const std::string& url, const std::string& subprotocol) {
     273              :     auto request = std::make_shared<http_request>();
     274              :     request->set_url(url);
     275              :     co_return co_await upgrade_websocket(std::move(request), subprotocol);
     276           52 : }
     277              : 
     278              : // Main implementation with request (supports custom headers)
     279           42 : awaitable<std::optional<websocket_client>> http_client_base::upgrade_websocket(
     280              :     std::shared_ptr<http_request> request, const std::string& subprotocol) {
     281              : 
     282              :     // Get URL from request
     283              :     const std::string& url = request->get_url();
     284              : 
     285              :     // Parse WebSocket URL (handle ws://, wss://, http://, https:// schemes)
     286              :     auto components = websocket_util::parse_websocket_url(url);
     287              :     if (!components) {
     288              :         // Try parsing as http/https URL
     289              :         auto ws_url = url;
     290              :         if (ws_url.starts_with("http://")) {
     291              :             ws_url = "ws://" + ws_url.substr(7);
     292              :         } else if (ws_url.starts_with("https://")) {
     293              :             ws_url = "wss://" + ws_url.substr(8);
     294              :         }
     295              :         components = websocket_util::parse_websocket_url(ws_url);
     296              :         if (!components) {
     297              :             LOG_ERROR("Invalid WebSocket URL: {}", url);
     298              :             co_return std::nullopt;
     299              :         }
     300              :     }
     301              : 
     302              :     auto& io_context = get_io_context();
     303              : 
     304              :     // Create socket based on scheme
     305              :     std::shared_ptr<asio::socket> socket;
     306              :     if (components->secure) {
     307              :         auto ssl_context = std::make_shared<boost::asio::ssl::context>(
     308              :             boost::asio::ssl::context::sslv23_client);
     309              :         ssl_context->set_default_verify_paths();
     310              :         if (!verify_ssl_) {
     311              :             ssl_context->set_verify_mode(boost::asio::ssl::verify_none);
     312              :         }
     313              :         socket = std::make_shared<asio::ssl_socket>("wss_client", io_context, ssl_context);
     314              :     } else {
     315              :         socket = std::make_shared<asio::tcp_socket>("ws_client", io_context);
     316              :     }
     317              : 
     318              :     // Connect
     319              :     auto ec = co_await socket->connect(components->host, components->port, timeout_);
     320              :     if (ec) {
     321              :         LOG_ERROR("WebSocket connect error: {}", ec.message());
     322              :         co_return std::nullopt;
     323              :     }
     324              : 
     325              :     // Create connection for HTTP upgrade
     326              :     auto connection = std::make_shared<client_connection>(socket, timeout_);
     327              : 
     328              :     // Set correct URL in request
     329              :     std::string http_url = (components->secure ? "https://" : "http://")
     330              :                           + components->host + ":" + components->port + components->path;
     331              :     request->set_url(http_url);
     332              :     request->set_method(method::GET);
     333              : 
     334              :     // Add WebSocket-specific headers (use set_header to avoid duplicates if request already has these)
     335              :     request->set_header("Upgrade", "websocket");
     336              :     request->set_header("Connection", "Upgrade");
     337              : 
     338              :     std::string ws_key = websocket_util::generate_websocket_key();
     339              :     request->add_header("Sec-WebSocket-Key", ws_key);
     340              :     request->add_header("Sec-WebSocket-Version", "13");
     341              : 
     342              :     if (!subprotocol.empty()) {
     343              :         request->add_header("Sec-WebSocket-Protocol", subprotocol);
     344              :     }
     345              : 
     346              :     apply_default_headers(request);
     347              : 
     348              :     // Send upgrade request
     349              :     auto response = co_await connection->send_request(request);
     350              : 
     351              :     if (!response || response->get_status_code() != 101) {
     352              :         LOG_ERROR("WebSocket upgrade failed: {}",
     353              :                  response ? std::to_string(response->get_status_code()) : "no response");
     354              :         co_return std::nullopt;
     355              :     }
     356              : 
     357              :     // Validate accept key
     358              :     auto accept_key = response->get_header("Sec-WebSocket-Accept");
     359              :     if (!websocket_util::validate_accept_key(accept_key, ws_key)) {
     360              :         LOG_ERROR("Invalid Sec-WebSocket-Accept key");
     361              :         co_return std::nullopt;
     362              :     }
     363              : 
     364              :     // Upgrade to WebSocket
     365              :     auto raw_socket = connection->release_socket();
     366              :     auto ws = std::make_shared<asio::websocket>(raw_socket, false, false);
     367              : 
     368              :     LOG_INFO("WebSocket connected to {}", url);
     369              : 
     370              :     co_return websocket_client(std::move(ws));
     371           84 : }
     372              : 
     373              : } // namespace thinger::http
        

Generated by: LCOV version 2.0-1