Network System 0.1.1
High-performance modular networking library for scalable client-server applications
Loading...
Searching...
No Matches
http_client.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2024, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
6#include <condition_variable>
7#include <mutex>
8#include <regex>
9#include <sstream>
10#include <thread>
11
12namespace kcenon::network::core {
13namespace {
14// Default timeout: 30 seconds
15constexpr auto DEFAULT_TIMEOUT_MS = std::chrono::milliseconds(30000);
16} // namespace
17
18// URL parsing implementation
19
20auto http_url::parse(const std::string &url) -> Result<http_url> {
21 http_url result;
22
23 // Regular expression for URL parsing
24 // Format: scheme://[user:pass@]host[:port][/path][?query]
25 // Static to avoid recompilation and ensure thread-safety (C++11 magic
26 // statics)
27 static const std::regex url_regex(
28 R"(^(https?):\/\/([^:\/\s]+)(?::(\d+))?(\/[^\?]*)?(?:\?(.*))?$)",
29 std::regex::icase);
30
31 std::smatch matches;
32 if (!std::regex_match(url, matches, url_regex)) {
33 return error<http_url>(-1, "Invalid URL format: " + url);
34 }
35
36 // Extract components
37 result.scheme = matches[1].str();
38 result.host = matches[2].str();
39
40 // Parse port
41 if (matches[3].matched) {
42 try {
43 result.port = static_cast<unsigned short>(std::stoi(matches[3].str()));
44 } catch (const std::exception &) {
45 return error<http_url>(-1, "Invalid port number in URL");
46 }
47 } else {
48 result.port = result.get_default_port();
49 }
50
51 // Parse path
52 result.path = matches[4].matched ? matches[4].str() : "/";
53
54 // Parse query string
55 if (matches[5].matched) {
56 result.query = internal::http_parser::parse_query_string(matches[5].str());
57 }
58
59 return ok(std::move(result));
60}
61
62auto http_url::get_default_port() const -> unsigned short {
63 if (scheme == "https") {
64 return 443;
65 }
66 return 80; // default for http
67}
68
69// HTTP client implementation
70
71http_client::http_client() : timeout_(DEFAULT_TIMEOUT_MS) {}
72
73http_client::http_client(std::chrono::milliseconds timeout_ms)
74 : timeout_(timeout_ms) {}
75
76auto http_client::get(const std::string &url,
77 const std::map<std::string, std::string> &query,
78 const std::map<std::string, std::string> &headers)
80 return request(internal::http_method::HTTP_GET, url, {}, headers, query);
81}
82
83auto http_client::post(const std::string &url, const std::string &body,
84 const std::map<std::string, std::string> &headers)
86 std::vector<uint8_t> body_bytes(body.begin(), body.end());
87 return request(internal::http_method::HTTP_POST, url, body_bytes, headers,
88 {});
89}
90
91auto http_client::post(const std::string &url, const std::vector<uint8_t> &body,
92 const std::map<std::string, std::string> &headers)
94 return request(internal::http_method::HTTP_POST, url, body, headers, {});
95}
96
97auto http_client::put(const std::string &url, const std::string &body,
98 const std::map<std::string, std::string> &headers)
100 std::vector<uint8_t> body_bytes(body.begin(), body.end());
101 return request(internal::http_method::HTTP_PUT, url, body_bytes, headers, {});
102}
103
104auto http_client::del(const std::string &url,
105 const std::map<std::string, std::string> &headers)
107 return request(internal::http_method::HTTP_DELETE, url, {}, headers, {});
108}
109
110auto http_client::head(const std::string &url,
111 const std::map<std::string, std::string> &headers)
113 return request(internal::http_method::HTTP_HEAD, url, {}, headers, {});
114}
115
116auto http_client::patch(const std::string &url, const std::string &body,
117 const std::map<std::string, std::string> &headers)
119 std::vector<uint8_t> body_bytes(body.begin(), body.end());
120 return request(internal::http_method::HTTP_PATCH, url, body_bytes, headers,
121 {});
122}
123
124auto http_client::set_timeout(std::chrono::milliseconds timeout_ms) -> void {
125 timeout_ = timeout_ms;
126}
127
128auto http_client::get_timeout() const -> std::chrono::milliseconds {
129 return timeout_;
130}
131
133 internal::http_method method, const http_url &url_info,
134 const std::vector<uint8_t> &body,
135 const std::map<std::string, std::string> &headers)
138 request.method = method;
139 request.uri = url_info.path;
141 request.query_params = url_info.query;
142 request.body = body;
143
144 // Add custom headers
145 for (const auto &[name, value] : headers) {
146 request.set_header(name, value);
147 }
148
149 // Add required headers
150 request.set_header("Host", url_info.host);
151 request.set_header("Connection", "close");
152 request.set_header("Accept", "*/*");
153
154 // Add Content-Length if body is present
155 if (!body.empty()) {
156 request.set_header("Content-Length", std::to_string(body.size()));
157 }
158
159 // Add User-Agent if not present
160 if (!request.get_header("User-Agent")) {
161 request.set_header("User-Agent", "NetworkSystem-HTTP-Client/1.0");
162 }
163
164 return request;
165}
166
167auto http_client::request(internal::http_method method, const std::string &url,
168 const std::vector<uint8_t> &body,
169 const std::map<std::string, std::string> &headers,
170 const std::map<std::string, std::string> &query)
172 // Parse URL
173 auto url_result = http_url::parse(url);
174 if (url_result.is_err()) {
175 return error<internal::http_response>(-1, "Failed to parse URL: " +
176 url_result.error().message);
177 }
178
179 auto url_info = std::move(url_result.value());
180
181 // Merge query parameters
182 for (const auto &[key, value] : query) {
183 url_info.query[key] = value;
184 }
185
186 // Check for HTTPS (not supported yet)
187 if (url_info.scheme == "https") {
189 -1, "HTTPS not supported yet. Use HTTP for now.");
190 }
191
192 // Build HTTP request
193 auto http_request = build_request(method, url_info, body, headers);
194
195 // Serialize request
196 auto request_bytes = internal::http_parser::serialize_request(http_request);
197
198 // Create messaging client for this request
199 auto client = std::make_shared<messaging_client>(
200 "http_client_" +
201 std::to_string(
202 std::chrono::system_clock::now().time_since_epoch().count()));
203
204 // Response data collection
205 std::vector<uint8_t> response_data;
206 std::mutex response_mutex;
207 std::condition_variable response_cv;
208 bool response_complete = false;
209 bool has_error = false;
210 std::string error_message;
211
212 // Set up receive callback
213 client->set_receive_callback([&](const std::vector<uint8_t> &data) {
214 std::lock_guard<std::mutex> lock(response_mutex);
215 response_data.insert(response_data.end(), data.begin(), data.end());
216
217 // Check if we have received the complete response
218 // For simplicity, we check for end of headers + Content-Length
219 // or connection close
220 std::string_view response_str(
221 reinterpret_cast<const char *>(response_data.data()),
222 response_data.size());
223
224 auto headers_end = response_str.find("\r\n\r\n");
225 if (headers_end != std::string_view::npos) {
226 // We have headers, check Content-Length
227 auto headers_section = response_str.substr(0, headers_end);
228
229 // Simple Content-Length extraction
230 auto cl_pos = headers_section.find("Content-Length:");
231 if (cl_pos == std::string_view::npos) {
232 cl_pos = headers_section.find("content-length:");
233 }
234
235 if (cl_pos != std::string_view::npos) {
236 auto cl_start = headers_section.find(':', cl_pos) + 1;
237 auto cl_end = headers_section.find('\r', cl_start);
238 auto cl_str = headers_section.substr(cl_start, cl_end - cl_start);
239
240 // Trim whitespace
241 while (!cl_str.empty() && std::isspace(cl_str.front()))
242 cl_str.remove_prefix(1);
243 while (!cl_str.empty() && std::isspace(cl_str.back()))
244 cl_str.remove_suffix(1);
245
246 try {
247 std::size_t content_length = std::stoull(std::string(cl_str));
248 std::size_t body_start = headers_end + 4;
249
250 if (response_data.size() >= body_start + content_length) {
251 response_complete = true;
252 response_cv.notify_one();
253 }
254 } catch (...) {
255 // Invalid Content-Length, treat as complete
256 response_complete = true;
257 response_cv.notify_one();
258 }
259 } else {
260 // No Content-Length, will rely on connection close
261 }
262 }
263 });
264
265 // Set up error callback
266 client->set_error_callback([&](std::error_code ec) {
267 std::lock_guard<std::mutex> lock(response_mutex);
268 if (!response_complete) {
269 // Connection closed, mark as complete if we have any data
270 if (!response_data.empty()) {
271 response_complete = true;
272 } else {
273 has_error = true;
274 error_message = ec.message();
275 }
276 response_cv.notify_one();
277 }
278 });
279
280 // Connect to server
281 auto connect_result = client->start_client(url_info.host, url_info.port);
282 if (connect_result.is_err()) {
284 -1, "Failed to connect to " + url_info.host + ":" +
285 std::to_string(url_info.port) + ": " +
286 connect_result.error().message);
287 }
288
289 // Wait for connection (give it a moment to establish)
290 std::this_thread::sleep_for(std::chrono::milliseconds(100));
291
292 // Send request
293 auto send_result = client->send_packet(std::move(request_bytes));
294 if (send_result.is_err()) {
295 (void)client->stop_client();
296 return error<internal::http_response>(-1, "Failed to send request: " +
297 send_result.error().message);
298 }
299
300 // Wait for response with timeout
301 {
302 std::unique_lock<std::mutex> lock(response_mutex);
303 if (!response_cv.wait_for(lock, timeout_,
304 [&] { return response_complete || has_error; })) {
305 (void)client->stop_client();
306 return error<internal::http_response>(-1, "Request timeout");
307 }
308 }
309
310 // Stop client
311 (void)client->stop_client();
312
313 // Check for errors
314 if (has_error) {
316 "Request failed: " + error_message);
317 }
318
319 // Parse response
320 auto response_result = internal::http_parser::parse_response(response_data);
321 if (response_result.is_err()) {
323 -1, "Failed to parse response: " + response_result.error().message);
324 }
325
326 return response_result;
327}
328
329} // namespace kcenon::network::core
auto del(const std::string &url, const std::map< std::string, std::string > &headers={}) -> Result< internal::http_response >
Perform HTTP DELETE request.
auto patch(const std::string &url, const std::string &body, const std::map< std::string, std::string > &headers={}) -> Result< internal::http_response >
Perform HTTP PATCH request.
auto put(const std::string &url, const std::string &body, const std::map< std::string, std::string > &headers={}) -> Result< internal::http_response >
Perform HTTP PUT request.
std::chrono::milliseconds timeout_
auto get_timeout() const -> std::chrono::milliseconds
Get current timeout setting.
auto request(internal::http_method method, const std::string &url, const std::vector< uint8_t > &body, const std::map< std::string, std::string > &headers, const std::map< std::string, std::string > &query) -> Result< internal::http_response >
Perform generic HTTP request.
http_client()
Construct HTTP client with default timeout.
auto build_request(internal::http_method method, const http_url &url_info, const std::vector< uint8_t > &body, const std::map< std::string, std::string > &headers) -> internal::http_request
Build HTTP request from components.
auto post(const std::string &url, const std::string &body, const std::map< std::string, std::string > &headers={}) -> Result< internal::http_response >
Perform HTTP POST request.
auto set_timeout(std::chrono::milliseconds timeout_ms) -> void
Set request timeout.
auto head(const std::string &url, const std::map< std::string, std::string > &headers={}) -> Result< internal::http_response >
Perform HTTP HEAD request.
auto get(const std::string &url, const std::map< std::string, std::string > &query={}, const std::map< std::string, std::string > &headers={}) -> Result< internal::http_response >
Perform HTTP GET request.
static auto serialize_request(const http_request &request) -> std::vector< uint8_t >
Serialize HTTP request to raw bytes.
static auto parse_query_string(const std::string &query_string) -> std::map< std::string, std::string >
Parse query string into key-value pairs.
static auto parse_response(const std::vector< uint8_t > &data) -> Result< http_response >
Parse HTTP response from raw bytes.
http_method
HTTP request methods (verbs)
Definition http_types.h:24
VoidResult ok()
Parsed URL components.
Definition http_client.h:25
std::map< std::string, std::string > query
Definition http_client.h:30
static auto parse(const std::string &url) -> Result< http_url >
Parse URL string into components.
auto get_default_port() const -> unsigned short
Get default port for scheme.
Represents an HTTP request message.
Definition http_types.h:112
auto set_header(const std::string &name, const std::string &value) -> void
Set a header value.
std::map< std::string, std::string > query_params
Definition http_types.h:118
auto get_header(const std::string &name) const -> std::optional< std::string >
Get the value of a header (case-insensitive)