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
|