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
|