diff --git a/libhttp/Makefile b/libhttp/Makefile index 3152312..41a1bb0 100644 --- a/libhttp/Makefile +++ b/libhttp/Makefile @@ -1,49 +1,51 @@ VERSION := 0.0.1 FRAMEWORK_NAME := us LIB_NAME := libus_http +LIBC_NAME := libus_httpclient INSTL_TDIR := /lib INCLU_TDIR := /usr/include INCLU_SDIR := inc CPP := g++ INCS := -I inc -I/opt/homebrew/include CPPFLAGS := --std=c++20 -O2 -Wall -Werror -LIBS := -L/opt/homebrew/lib -lssl -lcrypto -lcurl -lpthread +SERVER_LIBS := -L/opt/homebrew/lib -lssl -lcrypto -lpthread +CLIENT_LIBS := -L/opt/homebrew/lib -lssl -lcrypto -lcurl -lpthread BUILD_TDIR := build $(shell mkdir -p $(BUILD_TDIR)) #-------- -all: $(BUILD_TDIR)/$(LIB_NAME).so +all: $(BUILD_TDIR)/$(LIB_NAME).so $(BUILD_TDIR)/$(LIBC_NAME).so -$(BUILD_TDIR)/$(LIB_NAME).so: $(BUILD_TDIR)/HttpServer.o #$(BUILD_TDIR)/WebSocketServer.o $(BUILD_TDIR)/WebSocketClient.o - $(CPP) -shared $^ $(LIBS) -o $@ +# ---- Server library (HttpServer, no curl) ---- +$(BUILD_TDIR)/$(LIB_NAME).so: $(BUILD_TDIR)/HttpServer.o + $(CPP) -shared $^ $(SERVER_LIBS) -o $@ $(BUILD_TDIR)/HttpServer.o: src/HttpServer.cpp $(CPP) $(CPPFLAGS) -fPIC -c $^ $(INCS) -o $@ -$(BUILD_TDIR)/WebSocketServer.o: src/WebSocketServer.cpp - $(CPP) $(CPPFLAGS) -fPIC -c $^ $(INCS) -o $@ +# ---- Client library (HttpClient, depends on curl + beast websocket) ---- +$(BUILD_TDIR)/$(LIBC_NAME).so: $(BUILD_TDIR)/HttpClient.o + $(CPP) -shared $^ $(CLIENT_LIBS) -o $@ -$(BUILD_TDIR)/WebSocketClient.o: src/WebSocketClient.cpp +$(BUILD_TDIR)/HttpClient.o: src/HttpClient.cpp $(CPP) $(CPPFLAGS) -fPIC -c $^ $(INCS) -o $@ clear: @rm -rvf $(BUILD_TDIR) -install: - @echo "install UniServe_libth..." +install: all + @echo "install UniServe_libhttp..." @mkdir -vp $(INCLU_TDIR)/$(FRAMEWORK_NAME) @cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/HttpServer.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/ @cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/HttpClient.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/ - #@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/WebSocketServer.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/ - #@cp -vf $(INCLU_SDIR)/$(FRAMEWORK_NAME)/WebSocketClient.hpp $(INCLU_TDIR)/$(FRAMEWORK_NAME)/ @cp -vf $(BUILD_TDIR)/$(LIB_NAME).so $(INSTL_TDIR)/ + @cp -vf $(BUILD_TDIR)/$(LIBC_NAME).so $(INSTL_TDIR)/ @echo "done." uninstall: - @echo "uninstall UniServe_libth..." + @echo "uninstall UniServe_libhttp..." @rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/HttpServer.hpp @rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/HttpClient.hpp - @rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/WebSocketServer.hpp - @rm -vf $(INCLU_TDIR)/$(FRAMEWORK_NAME)/WebSocketClient.hpp @rm -vf $(INSTL_TDIR)/$(LIB_NAME).so + @rm -vf $(INSTL_TDIR)/$(LIBC_NAME).so @echo "done." diff --git a/libhttp/README.md b/libhttp/README.md index 0f01707..4813da3 100644 --- a/libhttp/README.md +++ b/libhttp/README.md @@ -27,17 +27,24 @@ C++20 network library for the UniServe microservice framework, providing unified - C++20 compiler - Boost.Beast / Boost.Asio (header-only) - OpenSSL (for TLS) -- libcurl (for HTTP client) +- libcurl (for HTTP client only — `libus_httpclient.so`) - pthread ## Build ```bash -make # builds build/libus_http.so -make install # installs to /usr/lib and /usr/include/us (requires root) +make # builds build/libus_http.so and build/libus_httpclient.so +make install # installs both .so to /lib and headers to /usr/include/us (requires root) ``` -Makefile links: `-lssl -lcrypto -lcurl -lpthread` with Homebrew paths for macOS. +Two shared libraries are produced: + +| Library | SONAME | Contains | Links | +|---|---|---|---| +| Server | `libus_http.so` | `HttpServer` (HTTP + WebSocket server + WS proxy) | `-lssl -lcrypto -lpthread` | +| Client | `libus_httpclient.so` | `HttpClient` (HTTP via curl + WebSocket via Beast) | `-lssl -lcrypto -lcurl -lpthread` | + +`libus_http.so` does **not** depend on libcurl. Link against `libus_httpclient.so` only when you need the client, and link both when you need both server and client (e.g. a proxy/gateway). ## Quick Start @@ -253,6 +260,7 @@ using WebSocketClientBase = HttpClient; ## Version History +- **v3.2.0**: Split into two shared libraries — `libus_http.so` (server only, no curl) and `libus_httpclient.so` (HttpClient, depends on curl). HttpClient implementation moved to `src/HttpClient.cpp`; `HttpClient.hpp` is declaration-only. - **v3.1.0**: Added WebSocket reverse proxy (`ws_proxy()`) with bidirectional async bridging, ws:// and wss:// backend support, handshake-time authorization callback. - **v3.0.0**: Merged WebSocketServer into HttpServer and WebSocketClient into HttpClient. Single-port HTTP+WebSocket support. - **v2.x**: Separate HttpServer, WebSocketServer, and HttpClient modules. diff --git a/libhttp/inc/us/HttpClient.hpp b/libhttp/inc/us/HttpClient.hpp index c054a01..6d1df70 100644 --- a/libhttp/inc/us/HttpClient.hpp +++ b/libhttp/inc/us/HttpClient.hpp @@ -4,8 +4,6 @@ #include #include #include -#include -#include #include #include @@ -37,307 +35,42 @@ enum class HttpMethod { class HttpClient { public: - HttpClient() { - curl_ = curl_easy_init(); - if (!curl_) { - throw std::runtime_error("Failed to initialize CURL"); - } - } - - ~HttpClient() { - ws_close(); - if (curl_) { - curl_easy_cleanup(curl_); - } - if (headers_) { - curl_slist_free_all(headers_); - } - } + HttpClient(); + ~HttpClient(); HttpClient(const HttpClient&) = delete; HttpClient& operator=(const HttpClient&) = delete; - - void set_header(const std::string& header) { - headers_ = curl_slist_append(headers_, header.c_str()); - } - - void clear_headers() { - if (headers_) { - curl_slist_free_all(headers_); - headers_ = nullptr; - } - } - - const std::map& get_response_headers() const { - return response_headers_; - } - - long get_response_code() const { - return response_code_; - } - + + void set_header(const std::string& header); + void clear_headers(); + + const std::map& get_response_headers() const; + long get_response_code() const; + // ===== HTTP Methods (Synchronous, libcurl-based) ===== void request_stream_sync( HttpMethod method, const std::string& url, const std::string& data, - std::function callback - ) { - if (!curl_) { - throw std::runtime_error("CURL not initialized"); - } - - response_headers_.clear(); - response_code_ = 0; - - curl_easy_reset(curl_); - curl_easy_setopt(curl_, CURLOPT_URL, url.c_str()); - set_http_method(method, data); - curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers_); - curl_easy_setopt(curl_, CURLOPT_HEADERFUNCTION, header_callback); - curl_easy_setopt(curl_, CURLOPT_HEADERDATA, this); - - std::string full_response; - curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, write_callback); - curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &full_response); - - curl_easy_setopt(curl_, CURLOPT_TIMEOUT, 30L); - curl_easy_setopt(curl_, CURLOPT_CONNECTTIMEOUT, 10L); - - CURLcode res = curl_easy_perform(curl_); - - if (res != CURLE_OK) { - throw std::runtime_error(std::string("CURL error: ") + curl_easy_strerror(res)); - } - - curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &response_code_); - - if (!full_response.empty()) { - callback(full_response); - } - } - - void get_stream_sync(const std::string& url, std::function callback) { - request_stream_sync(HttpMethod::GET, url, "", callback); - } - - void post_stream_sync(const std::string& url, const std::string& data, std::function callback) { - request_stream_sync(HttpMethod::POST, url, data, callback); - } - - void put_stream_sync(const std::string& url, const std::string& data, std::function callback) { - request_stream_sync(HttpMethod::PUT, url, data, callback); - } - - void delete_stream_sync(const std::string& url, std::function callback) { - request_stream_sync(HttpMethod::DELETE, url, "", callback); - } + std::function callback); + + void get_stream_sync(const std::string& url, std::function callback); + void post_stream_sync(const std::string& url, const std::string& data, std::function callback); + void put_stream_sync(const std::string& url, const std::string& data, std::function callback); + void delete_stream_sync(const std::string& url, std::function callback); // ===== WebSocket Methods (Synchronous, Boost.Beast-based) ===== - - /** - * Connect to WebSocket server (ws:// or wss://) - * @param url Full URL like "ws://host:port/path" or "wss://host:port/path" - * @param timeout_ms Connection timeout in milliseconds - * @return true on success - */ - bool ws_connect(const std::string& url, int timeout_ms = 5000) { - std::string host, port, path; - bool ssl = false; - - if (url.size() >= 6 && url.substr(0, 6) == "wss://") { - ssl = true; - parse_ws_url(url.substr(6), host, port, path, "443"); - } else if (url.size() >= 5 && url.substr(0, 5) == "ws://") { - ssl = false; - parse_ws_url(url.substr(5), host, port, path, "80"); - } else { - return false; - } - - return ws_connect(host, port, path, ssl, timeout_ms); - } - - /** - * Connect to WebSocket server with explicit parameters - */ + bool ws_connect(const std::string& url, int timeout_ms = 5000); bool ws_connect(const std::string& host, const std::string& port, - const std::string& path = "/", bool ssl = false, int timeout_ms = 5000) { - ws_close(); - ws_state_ = std::make_unique(); - ws_state_->ssl_enabled = ssl; - ws_state_->host = host; - ws_state_->port = port; - ws_state_->path = path.empty() ? "/" : path; - - try { - tcp::resolver resolver(ws_state_->ioc); - auto const results = resolver.resolve(host, port); - - if (ssl) { - // wss:// path: tcp::socket → asio::ssl::stream → websocket::stream - ws_state_->ssl_ctx = std::make_unique(asio::ssl::context::tlsv12_client); - ws_state_->ssl_ctx->set_verify_mode(asio::ssl::verify_none); - - // Create socket wrapped in beast::tcp_stream for timeout support - ws_state_->tcp_stream = std::make_unique(ws_state_->ioc); - - // Set connect timeout - ws_state_->tcp_stream->expires_after(std::chrono::milliseconds(timeout_ms)); - asio::connect(ws_state_->tcp_stream->socket(), results); - - // Now build SSL stream on top of the socket - ws_state_->ssl_stream = std::make_unique>( - ws_state_->tcp_stream->socket(), *ws_state_->ssl_ctx); - - // Set SNI - if (!SSL_set_tlsext_host_name(ws_state_->ssl_stream->native_handle(), host.c_str())) { - throw std::runtime_error("Failed to set SNI hostname"); - } - - // SSL handshake - ws_state_->ssl_stream->handshake(asio::ssl::stream_base::client); - - // Build websocket stream on top of SSL stream - ws_state_->wss_stream = std::make_unique&>>( - *ws_state_->ssl_stream); - - // WebSocket handshake - ws_state_->tcp_stream->expires_never(); - ws_state_->wss_stream->set_option(websocket::stream_base::timeout::suggested(beast::role_type::client)); - ws_state_->wss_stream->set_option(websocket::stream_base::decorator( - [](websocket::request_type& req) { - req.set(beast::http::field::user_agent, "UniServe-HttpClient"); - })); - ws_state_->wss_stream->handshake(host, ws_state_->path); - } else { - // ws:// path: tcp::socket wrapped in beast::tcp_stream → websocket::stream - ws_state_->tcp_stream = std::make_unique(ws_state_->ioc); - - ws_state_->tcp_stream->expires_after(std::chrono::milliseconds(timeout_ms)); - asio::connect(ws_state_->tcp_stream->socket(), results); - - // Build websocket stream on top of tcp_stream's socket (by reference) - ws_state_->ws_stream = std::make_unique>( - ws_state_->tcp_stream->socket()); - - ws_state_->tcp_stream->expires_never(); - ws_state_->ws_stream->set_option(websocket::stream_base::timeout::suggested(beast::role_type::client)); - ws_state_->ws_stream->set_option(websocket::stream_base::decorator( - [](websocket::request_type& req) { - req.set(beast::http::field::user_agent, "UniServe-HttpClient"); - })); - ws_state_->ws_stream->handshake(host, ws_state_->path); - } - - ws_state_->connected = true; - return true; - } catch (const std::exception& e) { - std::cerr << "[HttpClient::ws_connect] " << e.what() << std::endl; - ws_state_.reset(); - return false; - } - } - - /** - * Send a text message over WebSocket - */ - bool ws_send(const std::string& message) { - if (!ws_state_ || !ws_state_->connected) return false; - try { - if (ws_state_->ssl_enabled && ws_state_->wss_stream) { - ws_state_->wss_stream->write(asio::buffer(message)); - } else if (ws_state_->ws_stream) { - ws_state_->ws_stream->write(asio::buffer(message)); - } - return true; - } catch (const std::exception& e) { - std::cerr << "[HttpClient::ws_send] " << e.what() << std::endl; - ws_state_->connected = false; - return false; - } - } - - /** - * Send binary data over WebSocket - */ - bool ws_send_binary(const void* data, size_t size) { - if (!ws_state_ || !ws_state_->connected) return false; - try { - if (ws_state_->ssl_enabled && ws_state_->wss_stream) { - ws_state_->wss_stream->binary(true); - ws_state_->wss_stream->write(asio::buffer(data, size)); - ws_state_->wss_stream->text(true); - } else if (ws_state_->ws_stream) { - ws_state_->ws_stream->binary(true); - ws_state_->ws_stream->write(asio::buffer(data, size)); - ws_state_->ws_stream->text(true); - } - return true; - } catch (const std::exception& e) { - std::cerr << "[HttpClient::ws_send_binary] " << e.what() << std::endl; - ws_state_->connected = false; - return false; - } - } - - /** - * Receive a message from WebSocket (blocking) - * @param timeout_ms Timeout in milliseconds (0 = wait forever) - * @return Received message, or empty string on error/timeout/close - */ - std::string ws_recv(int timeout_ms = 0) { - if (!ws_state_ || !ws_state_->connected) return ""; - try { - beast::flat_buffer buffer; - - // For synchronous recv with timeout, we use websocket stream's own timeout options - // since we set suggested timeouts during handshake - if (ws_state_->ssl_enabled && ws_state_->wss_stream) { - ws_state_->wss_stream->read(buffer); - } else if (ws_state_->ws_stream) { - ws_state_->ws_stream->read(buffer); - } - return beast::buffers_to_string(buffer.data()); - } catch (const beast::system_error& e) { - if (e.code() == websocket::error::closed) { - ws_state_->connected = false; - } else { - std::cerr << "[HttpClient::ws_recv] " << e.what() << std::endl; - ws_state_->connected = false; - } - return ""; - } catch (const std::exception& e) { - std::cerr << "[HttpClient::ws_recv] " << e.what() << std::endl; - ws_state_->connected = false; - return ""; - } - } - - bool ws_is_connected() const { - return ws_state_ && ws_state_->connected; - } - - void ws_close() { - if (!ws_state_) return; - try { - if (ws_state_->connected) { - beast::error_code ec; - if (ws_state_->ssl_enabled && ws_state_->wss_stream) { - ws_state_->wss_stream->close(websocket::close_code::normal, ec); - } else if (ws_state_->ws_stream) { - ws_state_->ws_stream->close(websocket::close_code::normal, ec); - } - } - } catch (...) { - // Ignore close errors - } - ws_state_.reset(); - } - - std::string ws_get_host() const { return ws_state_ ? ws_state_->host : ""; } - std::string ws_get_port() const { return ws_state_ ? ws_state_->port : ""; } - std::string ws_get_path() const { return ws_state_ ? ws_state_->path : ""; } + const std::string& path = "/", bool ssl = false, int timeout_ms = 5000); + bool ws_send(const std::string& message); + bool ws_send_binary(const void* data, size_t size); + std::string ws_recv(int timeout_ms = 0); + bool ws_is_connected() const; + void ws_close(); + std::string ws_get_host() const; + std::string ws_get_port() const; + std::string ws_get_path() const; private: CURL* curl_ = nullptr; @@ -345,14 +78,13 @@ private: std::map response_headers_; long response_code_ = 0; - // WebSocket state - layered streams with reference ownership struct WsState { asio::io_context ioc; - std::unique_ptr tcp_stream; // owns the socket - std::unique_ptr ssl_ctx; // SSL context - std::unique_ptr> ssl_stream; // references tcp_stream->socket() - std::unique_ptr> ws_stream; // ws:// references tcp_stream->socket() - std::unique_ptr&>> wss_stream; // wss:// references ssl_stream + std::unique_ptr tcp_stream; + std::unique_ptr ssl_ctx; + std::unique_ptr> ssl_stream; + std::unique_ptr> ws_stream; + std::unique_ptr&>> wss_stream; bool ssl_enabled = false; bool connected = false; std::string host; @@ -361,89 +93,11 @@ private: }; std::unique_ptr ws_state_; - void set_http_method(HttpMethod method, const std::string& data) { - switch (method) { - case HttpMethod::GET: - curl_easy_setopt(curl_, CURLOPT_HTTPGET, 1L); - break; - case HttpMethod::POST: - curl_easy_setopt(curl_, CURLOPT_POST, 1L); - if (!data.empty()) { - curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, data.c_str()); - curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, data.size()); - } - break; - case HttpMethod::PUT: - curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "PUT"); - if (!data.empty()) { - curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, data.c_str()); - curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, data.size()); - } - break; - case HttpMethod::DELETE: - curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "DELETE"); - break; - case HttpMethod::PATCH: - curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "PATCH"); - if (!data.empty()) { - curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, data.c_str()); - curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, data.size()); - } - break; - case HttpMethod::HEAD: - curl_easy_setopt(curl_, CURLOPT_NOBODY, 1L); - break; - case HttpMethod::OPTIONS: - curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "OPTIONS"); - break; - } - } - - static size_t header_callback(char* buffer, size_t size, size_t nitems, void* userdata) { - size_t total_size = size * nitems; - HttpClient* client = static_cast(userdata); - std::string header_line(buffer, total_size); - size_t colon_pos = header_line.find(':'); - if (colon_pos != std::string::npos) { - std::string key = header_line.substr(0, colon_pos); - std::string value = header_line.substr(colon_pos + 1); - key.erase(0, key.find_first_not_of(" \t")); - key.erase(key.find_last_not_of(" \t") + 1); - value.erase(0, value.find_first_not_of(" \t")); - value.erase(value.find_last_not_of(" \t\r\n") + 1); - if (!key.empty() && !value.empty()) { - client->response_headers_[key] = value; - } - } - return total_size; - } - - static size_t write_callback(char* ptr, size_t size, size_t nmemb, void* userdata) { - size_t total_size = size * nmemb; - std::string* response = static_cast(userdata); - response->append(ptr, total_size); - return total_size; - } - - static void parse_ws_url(const std::string& url_part, std::string& host, std::string& port, std::string& path, const char* default_port) { - size_t path_pos = url_part.find('/'); - std::string host_port; - if (path_pos != std::string::npos) { - host_port = url_part.substr(0, path_pos); - path = url_part.substr(path_pos); - } else { - host_port = url_part; - path = "/"; - } - size_t port_pos = host_port.find(':'); - if (port_pos != std::string::npos) { - host = host_port.substr(0, port_pos); - port = host_port.substr(port_pos + 1); - } else { - host = host_port; - port = default_port; - } - } + void set_http_method(HttpMethod method, const std::string& data); + static size_t header_callback(char* buffer, size_t size, size_t nitems, void* userdata); + static size_t write_callback(char* ptr, size_t size, size_t nmemb, void* userdata); + static void parse_ws_url(const std::string& url_part, std::string& host, std::string& port, + std::string& path, const char* default_port); }; // Backward compatibility aliases diff --git a/libhttp/src/HttpClient.cpp b/libhttp/src/HttpClient.cpp new file mode 100644 index 0000000..3b31389 --- /dev/null +++ b/libhttp/src/HttpClient.cpp @@ -0,0 +1,357 @@ +#include +#include +#include + +namespace pmc::net { + +HttpClient::HttpClient() { + curl_ = curl_easy_init(); + if (!curl_) { + throw std::runtime_error("Failed to initialize CURL"); + } +} + +HttpClient::~HttpClient() { + ws_close(); + if (curl_) { + curl_easy_cleanup(curl_); + } + if (headers_) { + curl_slist_free_all(headers_); + } +} + +void HttpClient::set_header(const std::string& header) { + headers_ = curl_slist_append(headers_, header.c_str()); +} + +void HttpClient::clear_headers() { + if (headers_) { + curl_slist_free_all(headers_); + headers_ = nullptr; + } +} + +const std::map& HttpClient::get_response_headers() const { + return response_headers_; +} + +long HttpClient::get_response_code() const { + return response_code_; +} + +void HttpClient::request_stream_sync( + HttpMethod method, + const std::string& url, + const std::string& data, + std::function callback) +{ + if (!curl_) { + throw std::runtime_error("CURL not initialized"); + } + + response_headers_.clear(); + response_code_ = 0; + + curl_easy_reset(curl_); + curl_easy_setopt(curl_, CURLOPT_URL, url.c_str()); + set_http_method(method, data); + curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers_); + curl_easy_setopt(curl_, CURLOPT_HEADERFUNCTION, header_callback); + curl_easy_setopt(curl_, CURLOPT_HEADERDATA, this); + + std::string full_response; + curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &full_response); + + curl_easy_setopt(curl_, CURLOPT_TIMEOUT, 30L); + curl_easy_setopt(curl_, CURLOPT_CONNECTTIMEOUT, 10L); + + CURLcode res = curl_easy_perform(curl_); + + if (res != CURLE_OK) { + throw std::runtime_error(std::string("CURL error: ") + curl_easy_strerror(res)); + } + + curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &response_code_); + + if (!full_response.empty()) { + callback(full_response); + } +} + +void HttpClient::get_stream_sync(const std::string& url, std::function callback) { + request_stream_sync(HttpMethod::GET, url, "", callback); +} + +void HttpClient::post_stream_sync(const std::string& url, const std::string& data, std::function callback) { + request_stream_sync(HttpMethod::POST, url, data, callback); +} + +void HttpClient::put_stream_sync(const std::string& url, const std::string& data, std::function callback) { + request_stream_sync(HttpMethod::PUT, url, data, callback); +} + +void HttpClient::delete_stream_sync(const std::string& url, std::function callback) { + request_stream_sync(HttpMethod::DELETE, url, "", callback); +} + +bool HttpClient::ws_connect(const std::string& url, int timeout_ms) { + std::string host, port, path; + bool ssl = false; + + if (url.size() >= 6 && url.substr(0, 6) == "wss://") { + ssl = true; + parse_ws_url(url.substr(6), host, port, path, "443"); + } else if (url.size() >= 5 && url.substr(0, 5) == "ws://") { + ssl = false; + parse_ws_url(url.substr(5), host, port, path, "80"); + } else { + return false; + } + + return ws_connect(host, port, path, ssl, timeout_ms); +} + +bool HttpClient::ws_connect(const std::string& host, const std::string& port, + const std::string& path, bool ssl, int timeout_ms) { + ws_close(); + ws_state_ = std::make_unique(); + ws_state_->ssl_enabled = ssl; + ws_state_->host = host; + ws_state_->port = port; + ws_state_->path = path.empty() ? "/" : path; + + try { + tcp::resolver resolver(ws_state_->ioc); + auto const results = resolver.resolve(host, port); + + if (ssl) { + ws_state_->ssl_ctx = std::make_unique(asio::ssl::context::tlsv12_client); + ws_state_->ssl_ctx->set_verify_mode(asio::ssl::verify_none); + + ws_state_->tcp_stream = std::make_unique(ws_state_->ioc); + + ws_state_->tcp_stream->expires_after(std::chrono::milliseconds(timeout_ms)); + asio::connect(ws_state_->tcp_stream->socket(), results); + + ws_state_->ssl_stream = std::make_unique>( + ws_state_->tcp_stream->socket(), *ws_state_->ssl_ctx); + + if (!SSL_set_tlsext_host_name(ws_state_->ssl_stream->native_handle(), host.c_str())) { + throw std::runtime_error("Failed to set SNI hostname"); + } + + ws_state_->ssl_stream->handshake(asio::ssl::stream_base::client); + + ws_state_->wss_stream = std::make_unique&>>( + *ws_state_->ssl_stream); + + ws_state_->tcp_stream->expires_never(); + ws_state_->wss_stream->set_option(websocket::stream_base::timeout::suggested(beast::role_type::client)); + ws_state_->wss_stream->set_option(websocket::stream_base::decorator( + [](websocket::request_type& req) { + req.set(beast::http::field::user_agent, "UniServe-HttpClient"); + })); + ws_state_->wss_stream->handshake(host, ws_state_->path); + } else { + ws_state_->tcp_stream = std::make_unique(ws_state_->ioc); + + ws_state_->tcp_stream->expires_after(std::chrono::milliseconds(timeout_ms)); + asio::connect(ws_state_->tcp_stream->socket(), results); + + ws_state_->ws_stream = std::make_unique>( + ws_state_->tcp_stream->socket()); + + ws_state_->tcp_stream->expires_never(); + ws_state_->ws_stream->set_option(websocket::stream_base::timeout::suggested(beast::role_type::client)); + ws_state_->ws_stream->set_option(websocket::stream_base::decorator( + [](websocket::request_type& req) { + req.set(beast::http::field::user_agent, "UniServe-HttpClient"); + })); + ws_state_->ws_stream->handshake(host, ws_state_->path); + } + + ws_state_->connected = true; + return true; + } catch (const std::exception& e) { + std::cerr << "[HttpClient::ws_connect] " << e.what() << std::endl; + ws_state_.reset(); + return false; + } +} + +bool HttpClient::ws_send(const std::string& message) { + if (!ws_state_ || !ws_state_->connected) return false; + try { + if (ws_state_->ssl_enabled && ws_state_->wss_stream) { + ws_state_->wss_stream->write(asio::buffer(message)); + } else if (ws_state_->ws_stream) { + ws_state_->ws_stream->write(asio::buffer(message)); + } + return true; + } catch (const std::exception& e) { + std::cerr << "[HttpClient::ws_send] " << e.what() << std::endl; + ws_state_->connected = false; + return false; + } +} + +bool HttpClient::ws_send_binary(const void* data, size_t size) { + if (!ws_state_ || !ws_state_->connected) return false; + try { + if (ws_state_->ssl_enabled && ws_state_->wss_stream) { + ws_state_->wss_stream->binary(true); + ws_state_->wss_stream->write(asio::buffer(data, size)); + ws_state_->wss_stream->text(true); + } else if (ws_state_->ws_stream) { + ws_state_->ws_stream->binary(true); + ws_state_->ws_stream->write(asio::buffer(data, size)); + ws_state_->ws_stream->text(true); + } + return true; + } catch (const std::exception& e) { + std::cerr << "[HttpClient::ws_send_binary] " << e.what() << std::endl; + ws_state_->connected = false; + return false; + } +} + +std::string HttpClient::ws_recv(int timeout_ms) { + (void)timeout_ms; + if (!ws_state_ || !ws_state_->connected) return ""; + try { + beast::flat_buffer buffer; + + if (ws_state_->ssl_enabled && ws_state_->wss_stream) { + ws_state_->wss_stream->read(buffer); + } else if (ws_state_->ws_stream) { + ws_state_->ws_stream->read(buffer); + } + return beast::buffers_to_string(buffer.data()); + } catch (const beast::system_error& e) { + if (e.code() == websocket::error::closed) { + ws_state_->connected = false; + } else { + std::cerr << "[HttpClient::ws_recv] " << e.what() << std::endl; + ws_state_->connected = false; + } + return ""; + } catch (const std::exception& e) { + std::cerr << "[HttpClient::ws_recv] " << e.what() << std::endl; + ws_state_->connected = false; + return ""; + } +} + +bool HttpClient::ws_is_connected() const { + return ws_state_ && ws_state_->connected; +} + +void HttpClient::ws_close() { + if (!ws_state_) return; + try { + if (ws_state_->connected) { + beast::error_code ec; + if (ws_state_->ssl_enabled && ws_state_->wss_stream) { + ws_state_->wss_stream->close(websocket::close_code::normal, ec); + } else if (ws_state_->ws_stream) { + ws_state_->ws_stream->close(websocket::close_code::normal, ec); + } + } + } catch (...) { + // Ignore close errors + } + ws_state_.reset(); +} + +std::string HttpClient::ws_get_host() const { return ws_state_ ? ws_state_->host : ""; } +std::string HttpClient::ws_get_port() const { return ws_state_ ? ws_state_->port : ""; } +std::string HttpClient::ws_get_path() const { return ws_state_ ? ws_state_->path : ""; } + +void HttpClient::set_http_method(HttpMethod method, const std::string& data) { + switch (method) { + case HttpMethod::GET: + curl_easy_setopt(curl_, CURLOPT_HTTPGET, 1L); + break; + case HttpMethod::POST: + curl_easy_setopt(curl_, CURLOPT_POST, 1L); + if (!data.empty()) { + curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, data.c_str()); + curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, data.size()); + } + break; + case HttpMethod::PUT: + curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "PUT"); + if (!data.empty()) { + curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, data.c_str()); + curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, data.size()); + } + break; + case HttpMethod::DELETE: + curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "DELETE"); + break; + case HttpMethod::PATCH: + curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "PATCH"); + if (!data.empty()) { + curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, data.c_str()); + curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, data.size()); + } + break; + case HttpMethod::HEAD: + curl_easy_setopt(curl_, CURLOPT_NOBODY, 1L); + break; + case HttpMethod::OPTIONS: + curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "OPTIONS"); + break; + } +} + +size_t HttpClient::header_callback(char* buffer, size_t size, size_t nitems, void* userdata) { + size_t total_size = size * nitems; + HttpClient* client = static_cast(userdata); + std::string header_line(buffer, total_size); + size_t colon_pos = header_line.find(':'); + if (colon_pos != std::string::npos) { + std::string key = header_line.substr(0, colon_pos); + std::string value = header_line.substr(colon_pos + 1); + key.erase(0, key.find_first_not_of(" \t")); + key.erase(key.find_last_not_of(" \t") + 1); + value.erase(0, value.find_first_not_of(" \t")); + value.erase(value.find_last_not_of(" \t\r\n") + 1); + if (!key.empty() && !value.empty()) { + client->response_headers_[key] = value; + } + } + return total_size; +} + +size_t HttpClient::write_callback(char* ptr, size_t size, size_t nmemb, void* userdata) { + size_t total_size = size * nmemb; + std::string* response = static_cast(userdata); + response->append(ptr, total_size); + return total_size; +} + +void HttpClient::parse_ws_url(const std::string& url_part, std::string& host, std::string& port, + std::string& path, const char* default_port) { + size_t path_pos = url_part.find('/'); + std::string host_port; + if (path_pos != std::string::npos) { + host_port = url_part.substr(0, path_pos); + path = url_part.substr(path_pos); + } else { + host_port = url_part; + path = "/"; + } + size_t port_pos = host_port.find(':'); + if (port_pos != std::string::npos) { + host = host_port.substr(0, port_pos); + port = host_port.substr(port_pos + 1); + } else { + host = host_port; + port = default_port; + } +} + +} // namespace pmc::net