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.6 % 97 84
Test Date: 2026-02-20 15:38:22 Functions: 82.6 % 23 19

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

Generated by: LCOV version 2.0-1