1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 // SPDX-FileCopyrightText: Copyright 2020 Intel Corporation
4 #pragma once
5
6 #include "bmcweb_config.h"
7
8 #include "async_resolve.hpp"
9 #include "boost_formatters.hpp"
10 #include "http_body.hpp"
11 #include "http_response.hpp"
12 #include "logging.hpp"
13 #include "ssl_key_handler.hpp"
14
15 #include <openssl/err.h>
16 #include <openssl/ssl.h>
17 #include <openssl/tls1.h>
18
19 #include <boost/asio/connect.hpp>
20 #include <boost/asio/error.hpp>
21 #include <boost/asio/io_context.hpp>
22 #include <boost/asio/ip/address.hpp>
23 #include <boost/asio/ip/tcp.hpp>
24 #include <boost/asio/ssl/context.hpp>
25 #include <boost/asio/ssl/error.hpp>
26 #include <boost/asio/ssl/stream.hpp>
27 #include <boost/asio/ssl/stream_base.hpp>
28 #include <boost/asio/steady_timer.hpp>
29 #include <boost/beast/core/error.hpp>
30 #include <boost/beast/core/flat_static_buffer.hpp>
31 #include <boost/beast/http/field.hpp>
32 #include <boost/beast/http/fields.hpp>
33 #include <boost/beast/http/message.hpp>
34 #include <boost/beast/http/parser.hpp>
35 #include <boost/beast/http/read.hpp>
36 #include <boost/beast/http/status.hpp>
37 #include <boost/beast/http/verb.hpp>
38 #include <boost/beast/http/write.hpp>
39 #include <boost/container/devector.hpp>
40 #include <boost/optional/optional.hpp>
41 #include <boost/system/errc.hpp>
42 #include <boost/system/error_code.hpp>
43 #include <boost/url/host_type.hpp>
44 #include <boost/url/url.hpp>
45 #include <boost/url/url_view_base.hpp>
46
47 #include <chrono>
48 #include <cstdint>
49 #include <cstdlib>
50 #include <format>
51 #include <functional>
52 #include <memory>
53 #include <optional>
54 #include <string>
55 #include <string_view>
56 #include <type_traits>
57 #include <unordered_map>
58 #include <utility>
59 #include <vector>
60
61 namespace crow
62 {
63 // With Redfish Aggregation it is assumed we will connect to another
64 // instance of BMCWeb which can handle 100 simultaneous connections.
65 constexpr size_t maxPoolSize = 20;
66 constexpr size_t maxRequestQueueSize = 500;
67 constexpr unsigned int httpReadBodyLimit = 131072;
68 constexpr unsigned int httpReadBufferSize = 4096;
69
70 enum class ConnState
71 {
72 initialized,
73 resolveInProgress,
74 resolveFailed,
75 connectInProgress,
76 connectFailed,
77 connected,
78 handshakeInProgress,
79 handshakeFailed,
80 sendInProgress,
81 sendFailed,
82 recvInProgress,
83 recvFailed,
84 idle,
85 closed,
86 suspended,
87 terminated,
88 abortConnection,
89 sslInitFailed,
90 retry
91 };
92
defaultRetryHandler(unsigned int respCode)93 inline boost::system::error_code defaultRetryHandler(unsigned int respCode)
94 {
95 // As a default, assume 200X is alright
96 BMCWEB_LOG_DEBUG("Using default check for response code validity");
97 if ((respCode < 200) || (respCode >= 300))
98 {
99 return boost::system::errc::make_error_code(
100 boost::system::errc::result_out_of_range);
101 }
102
103 // Return 0 if the response code is valid
104 return boost::system::errc::make_error_code(boost::system::errc::success);
105 };
106
107 // We need to allow retry information to be set before a message has been
108 // sent and a connection pool has been created
109 struct ConnectionPolicy
110 {
111 uint32_t maxRetryAttempts = 5;
112
113 // the max size of requests in bytes. 0 for unlimited
114 boost::optional<uint64_t> requestByteLimit = httpReadBodyLimit;
115
116 size_t maxConnections = 1;
117
118 std::string retryPolicyAction = "TerminateAfterRetries";
119
120 std::chrono::seconds retryIntervalSecs = std::chrono::seconds(0);
121 std::function<boost::system::error_code(unsigned int respCode)>
122 invalidResp = defaultRetryHandler;
123 };
124
125 struct PendingRequest
126 {
127 boost::beast::http::request<bmcweb::HttpBody> req;
128 std::function<void(bool, uint32_t, Response&)> callback;
PendingRequestcrow::PendingRequest129 PendingRequest(
130 boost::beast::http::request<bmcweb::HttpBody>&& reqIn,
131 const std::function<void(bool, uint32_t, Response&)>& callbackIn) :
132 req(std::move(reqIn)), callback(callbackIn)
133 {}
134 };
135
136 namespace http = boost::beast::http;
137 class ConnectionInfo : public std::enable_shared_from_this<ConnectionInfo>
138 {
139 private:
140 ConnState state = ConnState::initialized;
141 uint32_t retryCount = 0;
142 std::string subId;
143 std::shared_ptr<ConnectionPolicy> connPolicy;
144 boost::urls::url host;
145 ensuressl::VerifyCertificate verifyCert;
146 uint32_t connId;
147 // Data buffers
148 http::request<bmcweb::HttpBody> req;
149 using parser_type = http::response_parser<bmcweb::HttpBody>;
150 std::optional<parser_type> parser;
151 boost::beast::flat_static_buffer<httpReadBufferSize> buffer;
152 Response res;
153
154 // Ascync callables
155 std::function<void(bool, uint32_t, Response&)> callback;
156
157 boost::asio::io_context& ioc;
158
159 using Resolver = std::conditional_t<BMCWEB_DNS_RESOLVER == "systemd-dbus",
160 async_resolve::Resolver,
161 boost::asio::ip::tcp::resolver>;
162 Resolver resolver;
163
164 boost::asio::ip::tcp::socket conn;
165 std::optional<boost::asio::ssl::stream<boost::asio::ip::tcp::socket&>>
166 sslConn;
167
168 boost::asio::steady_timer timer;
169
170 friend class ConnectionPool;
171
doResolve()172 void doResolve()
173 {
174 state = ConnState::resolveInProgress;
175 BMCWEB_LOG_DEBUG("Trying to resolve: {}, id: {}", host, connId);
176
177 resolver.async_resolve(host.encoded_host_address(), host.port(),
178 std::bind_front(&ConnectionInfo::afterResolve,
179 this, shared_from_this()));
180 }
181
afterResolve(const std::shared_ptr<ConnectionInfo> &,const boost::system::error_code & ec,const Resolver::results_type & endpointList)182 void afterResolve(const std::shared_ptr<ConnectionInfo>& /*self*/,
183 const boost::system::error_code& ec,
184 const Resolver::results_type& endpointList)
185 {
186 if (ec || (endpointList.empty()))
187 {
188 BMCWEB_LOG_ERROR("Resolve failed: {} {}", ec.message(), host);
189 state = ConnState::resolveFailed;
190 waitAndRetry();
191 return;
192 }
193 BMCWEB_LOG_DEBUG("Resolved {}, id: {}", host, connId);
194 state = ConnState::connectInProgress;
195
196 BMCWEB_LOG_DEBUG("Trying to connect to: {}, id: {}", host, connId);
197
198 timer.expires_after(std::chrono::seconds(30));
199 timer.async_wait(std::bind_front(onTimeout, weak_from_this()));
200
201 boost::asio::async_connect(
202 conn, endpointList,
203 std::bind_front(&ConnectionInfo::afterConnect, this,
204 shared_from_this()));
205 }
206
afterConnect(const std::shared_ptr<ConnectionInfo> &,const boost::beast::error_code & ec,const boost::asio::ip::tcp::endpoint & endpoint)207 void afterConnect(const std::shared_ptr<ConnectionInfo>& /*self*/,
208 const boost::beast::error_code& ec,
209 const boost::asio::ip::tcp::endpoint& endpoint)
210 {
211 // The operation already timed out. We don't want do continue down
212 // this branch
213 if (ec && ec == boost::asio::error::operation_aborted)
214 {
215 return;
216 }
217
218 timer.cancel();
219 if (ec)
220 {
221 BMCWEB_LOG_ERROR("Connect {}:{}, id: {} failed: {}",
222 endpoint.address().to_string(), endpoint.port(),
223 connId, ec.message());
224 state = ConnState::connectFailed;
225 waitAndRetry();
226 return;
227 }
228 BMCWEB_LOG_DEBUG("Connected to: {}:{}, id: {}",
229 endpoint.address().to_string(), endpoint.port(),
230 connId);
231 if (sslConn)
232 {
233 doSslHandshake();
234 return;
235 }
236 state = ConnState::connected;
237 sendMessage();
238 }
239
doSslHandshake()240 void doSslHandshake()
241 {
242 if (!sslConn)
243 {
244 return;
245 }
246 state = ConnState::handshakeInProgress;
247 timer.expires_after(std::chrono::seconds(30));
248 timer.async_wait(std::bind_front(onTimeout, weak_from_this()));
249 sslConn->async_handshake(
250 boost::asio::ssl::stream_base::client,
251 std::bind_front(&ConnectionInfo::afterSslHandshake, this,
252 shared_from_this()));
253 }
254
afterSslHandshake(const std::shared_ptr<ConnectionInfo> &,const boost::beast::error_code & ec)255 void afterSslHandshake(const std::shared_ptr<ConnectionInfo>& /*self*/,
256 const boost::beast::error_code& ec)
257 {
258 // The operation already timed out. We don't want do continue down
259 // this branch
260 if (ec && ec == boost::asio::error::operation_aborted)
261 {
262 return;
263 }
264
265 timer.cancel();
266 if (ec)
267 {
268 BMCWEB_LOG_ERROR("SSL Handshake failed - id: {} error: {}", connId,
269 ec.message());
270 state = ConnState::handshakeFailed;
271 waitAndRetry();
272 return;
273 }
274 BMCWEB_LOG_DEBUG("SSL Handshake successful - id: {}", connId);
275 state = ConnState::connected;
276 sendMessage();
277 }
278
sendMessage()279 void sendMessage()
280 {
281 state = ConnState::sendInProgress;
282
283 // Set a timeout on the operation
284 timer.expires_after(std::chrono::seconds(30));
285 timer.async_wait(std::bind_front(onTimeout, weak_from_this()));
286 // Send the HTTP request to the remote host
287 if (sslConn)
288 {
289 boost::beast::http::async_write(
290 *sslConn, req,
291 std::bind_front(&ConnectionInfo::afterWrite, this,
292 shared_from_this()));
293 }
294 else
295 {
296 boost::beast::http::async_write(
297 conn, req,
298 std::bind_front(&ConnectionInfo::afterWrite, this,
299 shared_from_this()));
300 }
301 }
302
afterWrite(const std::shared_ptr<ConnectionInfo> &,const boost::beast::error_code & ec,size_t bytesTransferred)303 void afterWrite(const std::shared_ptr<ConnectionInfo>& /*self*/,
304 const boost::beast::error_code& ec, size_t bytesTransferred)
305 {
306 // The operation already timed out. We don't want do continue down
307 // this branch
308 if (ec && ec == boost::asio::error::operation_aborted)
309 {
310 return;
311 }
312
313 timer.cancel();
314 if (ec)
315 {
316 BMCWEB_LOG_ERROR("sendMessage() failed: {} {}", ec.message(), host);
317 state = ConnState::sendFailed;
318 waitAndRetry();
319 return;
320 }
321 BMCWEB_LOG_DEBUG("sendMessage() bytes transferred: {}",
322 bytesTransferred);
323
324 recvMessage();
325 }
326
recvMessage()327 void recvMessage()
328 {
329 state = ConnState::recvInProgress;
330
331 parser_type& thisParser = parser.emplace();
332
333 thisParser.body_limit(connPolicy->requestByteLimit);
334
335 timer.expires_after(std::chrono::seconds(30));
336 timer.async_wait(std::bind_front(onTimeout, weak_from_this()));
337
338 // Receive the HTTP response
339 if (sslConn)
340 {
341 boost::beast::http::async_read(
342 *sslConn, buffer, thisParser,
343 std::bind_front(&ConnectionInfo::afterRead, this,
344 shared_from_this()));
345 }
346 else
347 {
348 boost::beast::http::async_read(
349 conn, buffer, thisParser,
350 std::bind_front(&ConnectionInfo::afterRead, this,
351 shared_from_this()));
352 }
353 }
354
afterRead(const std::shared_ptr<ConnectionInfo> &,const boost::beast::error_code & ec,const std::size_t & bytesTransferred)355 void afterRead(const std::shared_ptr<ConnectionInfo>& /*self*/,
356 const boost::beast::error_code& ec,
357 const std::size_t& bytesTransferred)
358 {
359 // The operation already timed out. We don't want do continue down
360 // this branch
361 if (ec && ec == boost::asio::error::operation_aborted)
362 {
363 return;
364 }
365
366 timer.cancel();
367 if (ec && ec != boost::asio::ssl::error::stream_truncated)
368 {
369 BMCWEB_LOG_ERROR("recvMessage() failed: {} from {}", ec.message(),
370 host);
371 state = ConnState::recvFailed;
372 waitAndRetry();
373 return;
374 }
375 BMCWEB_LOG_DEBUG("recvMessage() bytes transferred: {}",
376 bytesTransferred);
377 if (!parser)
378 {
379 return;
380 }
381 BMCWEB_LOG_DEBUG("recvMessage() data: {}", parser->get().body().str());
382
383 unsigned int respCode = parser->get().result_int();
384 BMCWEB_LOG_DEBUG("recvMessage() Header Response Code: {}", respCode);
385
386 // Handle the case of stream_truncated. Some servers close the ssl
387 // connection uncleanly, so check to see if we got a full response
388 // before we handle this as an error.
389 if (!parser->is_done())
390 {
391 state = ConnState::recvFailed;
392 waitAndRetry();
393 return;
394 }
395
396 // Make sure the received response code is valid as defined by
397 // the associated retry policy
398 if (connPolicy->invalidResp(respCode))
399 {
400 // The listener failed to receive the Sent-Event
401 BMCWEB_LOG_ERROR(
402 "recvMessage() Listener Failed to "
403 "receive Sent-Event. Header Response Code: {} from {}",
404 respCode, host);
405 state = ConnState::recvFailed;
406 waitAndRetry();
407 return;
408 }
409
410 // Send is successful
411 // Reset the counter just in case this was after retrying
412 retryCount = 0;
413
414 // Keep the connection alive if server supports it
415 // Else close the connection
416 BMCWEB_LOG_DEBUG("recvMessage() keepalive : {}", parser->keep_alive());
417
418 // Copy the response into a Response object so that it can be
419 // processed by the callback function.
420 res.response = parser->release();
421 callback(parser->keep_alive(), connId, res);
422 res.clear();
423 }
424
onTimeout(const std::weak_ptr<ConnectionInfo> & weakSelf,const boost::system::error_code & ec)425 static void onTimeout(const std::weak_ptr<ConnectionInfo>& weakSelf,
426 const boost::system::error_code& ec)
427 {
428 if (ec == boost::asio::error::operation_aborted)
429 {
430 BMCWEB_LOG_DEBUG(
431 "async_wait failed since the operation is aborted");
432 return;
433 }
434 if (ec)
435 {
436 BMCWEB_LOG_ERROR("async_wait failed: {}", ec.message());
437 // If the timer fails, we need to close the socket anyway, same
438 // as if it expired.
439 }
440 std::shared_ptr<ConnectionInfo> self = weakSelf.lock();
441 if (self == nullptr)
442 {
443 return;
444 }
445 self->waitAndRetry();
446 }
447
waitAndRetry()448 void waitAndRetry()
449 {
450 if ((retryCount >= connPolicy->maxRetryAttempts) ||
451 (state == ConnState::sslInitFailed))
452 {
453 BMCWEB_LOG_ERROR("Maximum number of retries reached. {}", host);
454 BMCWEB_LOG_DEBUG("Retry policy: {}", connPolicy->retryPolicyAction);
455
456 if (connPolicy->retryPolicyAction == "TerminateAfterRetries")
457 {
458 // TODO: delete subscription
459 state = ConnState::terminated;
460 }
461 if (connPolicy->retryPolicyAction == "SuspendRetries")
462 {
463 state = ConnState::suspended;
464 }
465
466 // We want to return a 502 to indicate there was an error with
467 // the external server
468 res.result(boost::beast::http::status::bad_gateway);
469 callback(false, connId, res);
470 res.clear();
471
472 // Reset the retrycount to zero so that client can try
473 // connecting again if needed
474 retryCount = 0;
475 return;
476 }
477
478 retryCount++;
479
480 BMCWEB_LOG_DEBUG("Attempt retry after {} seconds. RetryCount = {}",
481 connPolicy->retryIntervalSecs.count(), retryCount);
482 timer.expires_after(connPolicy->retryIntervalSecs);
483 timer.async_wait(std::bind_front(&ConnectionInfo::onTimerDone, this,
484 shared_from_this()));
485 }
486
onTimerDone(const std::shared_ptr<ConnectionInfo> &,const boost::system::error_code & ec)487 void onTimerDone(const std::shared_ptr<ConnectionInfo>& /*self*/,
488 const boost::system::error_code& ec)
489 {
490 if (ec == boost::asio::error::operation_aborted)
491 {
492 BMCWEB_LOG_DEBUG(
493 "async_wait failed since the operation is aborted{}",
494 ec.message());
495 }
496 else if (ec)
497 {
498 BMCWEB_LOG_ERROR("async_wait failed: {}", ec.message());
499 // Ignore the error and continue the retry loop to attempt
500 // sending the event as per the retry policy
501 }
502
503 // Let's close the connection and restart from resolve.
504 shutdownConn(true);
505 }
506
restartConnection()507 void restartConnection()
508 {
509 BMCWEB_LOG_DEBUG("{}, id: {} restartConnection", host,
510 std::to_string(connId));
511 initializeConnection(host.scheme() == "https");
512 doResolve();
513 }
514
shutdownConn(bool retry)515 void shutdownConn(bool retry)
516 {
517 boost::beast::error_code ec;
518 conn.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
519 conn.close();
520
521 // not_connected happens sometimes so don't bother reporting it.
522 if (ec && ec != boost::beast::errc::not_connected)
523 {
524 BMCWEB_LOG_ERROR("{}, id: {} shutdown failed: {}", host, connId,
525 ec.message());
526 }
527 else
528 {
529 BMCWEB_LOG_DEBUG("{}, id: {} closed gracefully", host, connId);
530 }
531
532 if (retry)
533 {
534 // Now let's try to resend the data
535 state = ConnState::retry;
536 restartConnection();
537 }
538 else
539 {
540 state = ConnState::closed;
541 }
542 }
543
doClose(bool retry=false)544 void doClose(bool retry = false)
545 {
546 if (!sslConn)
547 {
548 shutdownConn(retry);
549 return;
550 }
551
552 sslConn->async_shutdown(
553 std::bind_front(&ConnectionInfo::afterSslShutdown, this,
554 shared_from_this(), retry));
555 }
556
afterSslShutdown(const std::shared_ptr<ConnectionInfo> &,bool retry,const boost::system::error_code & ec)557 void afterSslShutdown(const std::shared_ptr<ConnectionInfo>& /*self*/,
558 bool retry, const boost::system::error_code& ec)
559 {
560 if (ec)
561 {
562 BMCWEB_LOG_ERROR("{}, id: {} shutdown failed: {}", host, connId,
563 ec.message());
564 }
565 else
566 {
567 BMCWEB_LOG_DEBUG("{}, id: {} closed gracefully", host, connId);
568 }
569 shutdownConn(retry);
570 }
571
setCipherSuiteTLSext()572 void setCipherSuiteTLSext()
573 {
574 if (!sslConn)
575 {
576 return;
577 }
578
579 if (host.host_type() != boost::urls::host_type::name)
580 {
581 // Avoid setting SNI hostname if its IP address
582 return;
583 }
584 // Create a null terminated string for SSL
585 std::string hostname(host.encoded_host_address());
586 // NOTE: The SSL_set_tlsext_host_name is defined in tlsv1.h header
587 // file but its having old style casting (name is cast to void*).
588 // Since bmcweb compiler treats all old-style-cast as error, its
589 // causing the build failure. So replaced the same macro inline and
590 // did corrected the code by doing static_cast to viod*. This has to
591 // be fixed in openssl library in long run. Set SNI Hostname (many
592 // hosts need this to handshake successfully)
593 if (SSL_ctrl(sslConn->native_handle(), SSL_CTRL_SET_TLSEXT_HOSTNAME,
594 TLSEXT_NAMETYPE_host_name,
595 static_cast<void*>(hostname.data())) == 0)
596
597 {
598 boost::beast::error_code ec{static_cast<int>(::ERR_get_error()),
599 boost::asio::error::get_ssl_category()};
600
601 BMCWEB_LOG_ERROR("SSL_set_tlsext_host_name {}, id: {} failed: {}",
602 host, connId, ec.message());
603 // Set state as sslInit failed so that we close the connection
604 // and take appropriate action as per retry configuration.
605 state = ConnState::sslInitFailed;
606 waitAndRetry();
607 return;
608 }
609 }
610
initializeConnection(bool ssl)611 void initializeConnection(bool ssl)
612 {
613 conn = boost::asio::ip::tcp::socket(ioc);
614 if (ssl)
615 {
616 std::optional<boost::asio::ssl::context> sslCtx =
617 ensuressl::getSSLClientContext(verifyCert);
618
619 if (!sslCtx)
620 {
621 BMCWEB_LOG_ERROR("prepareSSLContext failed - {}, id: {}", host,
622 connId);
623 // Don't retry if failure occurs while preparing SSL context
624 // such as certificate is invalid or set cipher failure or
625 // set host name failure etc... Setting conn state to
626 // sslInitFailed and connection state will be transitioned
627 // to next state depending on retry policy set by
628 // subscription.
629 state = ConnState::sslInitFailed;
630 waitAndRetry();
631 return;
632 }
633 sslConn.emplace(conn, *sslCtx);
634 setCipherSuiteTLSext();
635 }
636 }
637
638 public:
ConnectionInfo(boost::asio::io_context & iocIn,const std::string & idIn,const std::shared_ptr<ConnectionPolicy> & connPolicyIn,const boost::urls::url_view_base & hostIn,ensuressl::VerifyCertificate verifyCertIn,unsigned int connIdIn)639 explicit ConnectionInfo(
640 boost::asio::io_context& iocIn, const std::string& idIn,
641 const std::shared_ptr<ConnectionPolicy>& connPolicyIn,
642 const boost::urls::url_view_base& hostIn,
643 ensuressl::VerifyCertificate verifyCertIn, unsigned int connIdIn) :
644 subId(idIn), connPolicy(connPolicyIn), host(hostIn),
645 verifyCert(verifyCertIn), connId(connIdIn), ioc(iocIn), resolver(iocIn),
646 conn(iocIn), timer(iocIn)
647 {
648 initializeConnection(host.scheme() == "https");
649 }
650 };
651
652 class ConnectionPool : public std::enable_shared_from_this<ConnectionPool>
653 {
654 private:
655 boost::asio::io_context& ioc;
656 std::string id;
657 std::shared_ptr<ConnectionPolicy> connPolicy;
658 boost::urls::url destIP;
659 std::vector<std::shared_ptr<ConnectionInfo>> connections;
660 boost::container::devector<PendingRequest> requestQueue;
661 ensuressl::VerifyCertificate verifyCert;
662
663 friend class HttpClient;
664
665 // Configure a connections's request, callback, and retry info in
666 // preparation to begin sending the request
setConnProps(ConnectionInfo & conn)667 void setConnProps(ConnectionInfo& conn)
668 {
669 if (requestQueue.empty())
670 {
671 BMCWEB_LOG_ERROR(
672 "setConnProps() should not have been called when requestQueue is empty");
673 return;
674 }
675
676 PendingRequest& nextReq = requestQueue.front();
677 conn.req = std::move(nextReq.req);
678 conn.callback = std::move(nextReq.callback);
679
680 BMCWEB_LOG_DEBUG("Setting properties for connection {}, id: {}",
681 conn.host, conn.connId);
682
683 // We can remove the request from the queue at this point
684 requestQueue.pop_front();
685 }
686
687 // Gets called as part of callback after request is sent
688 // Reuses the connection if there are any requests waiting to be sent
689 // Otherwise closes the connection if it is not a keep-alive
sendNext(bool keepAlive,uint32_t connId)690 void sendNext(bool keepAlive, uint32_t connId)
691 {
692 auto conn = connections[connId];
693
694 // Allow the connection's handler to be deleted
695 // This is needed because of Redfish Aggregation passing an
696 // AsyncResponse shared_ptr to this callback
697 conn->callback = nullptr;
698
699 // Reuse the connection to send the next request in the queue
700 if (!requestQueue.empty())
701 {
702 BMCWEB_LOG_DEBUG(
703 "{} requests remaining in queue for {}, reusing connection {}",
704 requestQueue.size(), destIP, connId);
705
706 setConnProps(*conn);
707
708 if (keepAlive)
709 {
710 conn->sendMessage();
711 }
712 else
713 {
714 // Server is not keep-alive enabled so we need to close the
715 // connection and then start over from resolve
716 conn->doClose();
717 conn->restartConnection();
718 }
719 return;
720 }
721
722 // No more messages to send so close the connection if necessary
723 if (keepAlive)
724 {
725 conn->state = ConnState::idle;
726 }
727 else
728 {
729 // Abort the connection since server is not keep-alive enabled
730 conn->state = ConnState::abortConnection;
731 conn->doClose();
732 }
733 }
734
sendData(std::string && data,const boost::urls::url_view_base & destUri,const boost::beast::http::fields & httpHeader,const boost::beast::http::verb verb,const std::function<void (Response &)> & resHandler)735 void sendData(std::string&& data, const boost::urls::url_view_base& destUri,
736 const boost::beast::http::fields& httpHeader,
737 const boost::beast::http::verb verb,
738 const std::function<void(Response&)>& resHandler)
739 {
740 // Construct the request to be sent
741 boost::beast::http::request<bmcweb::HttpBody> thisReq(
742 verb, destUri.encoded_target(), 11, "", httpHeader);
743 thisReq.set(boost::beast::http::field::host,
744 destUri.encoded_host_address());
745 thisReq.keep_alive(true);
746 thisReq.body().str() = std::move(data);
747 thisReq.prepare_payload();
748 auto cb = std::bind_front(&ConnectionPool::afterSendData,
749 weak_from_this(), resHandler);
750 // Reuse an existing connection if one is available
751 for (unsigned int i = 0; i < connections.size(); i++)
752 {
753 auto conn = connections[i];
754 if ((conn->state == ConnState::idle) ||
755 (conn->state == ConnState::initialized) ||
756 (conn->state == ConnState::closed))
757 {
758 conn->req = std::move(thisReq);
759 conn->callback = std::move(cb);
760 std::string commonMsg = std::format("{} from pool {}", i, id);
761
762 if (conn->state == ConnState::idle)
763 {
764 BMCWEB_LOG_DEBUG("Grabbing idle connection {}", commonMsg);
765 conn->sendMessage();
766 }
767 else
768 {
769 BMCWEB_LOG_DEBUG("Reusing existing connection {}",
770 commonMsg);
771 conn->restartConnection();
772 }
773 return;
774 }
775 }
776
777 // All connections in use so create a new connection or add request
778 // to the queue
779 if (connections.size() < connPolicy->maxConnections)
780 {
781 BMCWEB_LOG_DEBUG("Adding new connection to pool {}", id);
782 auto conn = addConnection();
783 conn->req = std::move(thisReq);
784 conn->callback = std::move(cb);
785 conn->doResolve();
786 }
787 else if (requestQueue.size() < maxRequestQueueSize)
788 {
789 BMCWEB_LOG_DEBUG("Max pool size reached. Adding data to queue {}",
790 id);
791 requestQueue.emplace_back(std::move(thisReq), std::move(cb));
792 }
793 else
794 {
795 // If we can't buffer the request then we should let the
796 // callback handle a 429 Too Many Requests dummy response
797 BMCWEB_LOG_ERROR("{} request queue full. Dropping request.", id);
798 Response dummyRes;
799 dummyRes.result(boost::beast::http::status::too_many_requests);
800 resHandler(dummyRes);
801 }
802 }
803
804 // Callback to be called once the request has been sent
afterSendData(const std::weak_ptr<ConnectionPool> & weakSelf,const std::function<void (Response &)> & resHandler,bool keepAlive,uint32_t connId,Response & res)805 static void afterSendData(const std::weak_ptr<ConnectionPool>& weakSelf,
806 const std::function<void(Response&)>& resHandler,
807 bool keepAlive, uint32_t connId, Response& res)
808 {
809 // Allow provided callback to perform additional processing of the
810 // request
811 resHandler(res);
812
813 // If requests remain in the queue then we want to reuse this
814 // connection to send the next request
815 std::shared_ptr<ConnectionPool> self = weakSelf.lock();
816 if (!self)
817 {
818 BMCWEB_LOG_CRITICAL("{} Failed to capture connection",
819 logPtr(self.get()));
820 return;
821 }
822
823 self->sendNext(keepAlive, connId);
824 }
825
addConnection()826 std::shared_ptr<ConnectionInfo>& addConnection()
827 {
828 unsigned int newId = static_cast<unsigned int>(connections.size());
829
830 auto& ret = connections.emplace_back(std::make_shared<ConnectionInfo>(
831 ioc, id, connPolicy, destIP, verifyCert, newId));
832
833 BMCWEB_LOG_DEBUG("Added connection {} to pool {}",
834 connections.size() - 1, id);
835
836 return ret;
837 }
838
839 public:
ConnectionPool(boost::asio::io_context & iocIn,const std::string & idIn,const std::shared_ptr<ConnectionPolicy> & connPolicyIn,const boost::urls::url_view_base & destIPIn,ensuressl::VerifyCertificate verifyCertIn)840 explicit ConnectionPool(
841 boost::asio::io_context& iocIn, const std::string& idIn,
842 const std::shared_ptr<ConnectionPolicy>& connPolicyIn,
843 const boost::urls::url_view_base& destIPIn,
844 ensuressl::VerifyCertificate verifyCertIn) :
845 ioc(iocIn), id(idIn), connPolicy(connPolicyIn), destIP(destIPIn),
846 verifyCert(verifyCertIn)
847 {
848 BMCWEB_LOG_DEBUG("Initializing connection pool for {}", id);
849
850 // Initialize the pool with a single connection
851 addConnection();
852 }
853
854 // Check whether all connections are terminated
areAllConnectionsTerminated()855 bool areAllConnectionsTerminated()
856 {
857 if (connections.empty())
858 {
859 BMCWEB_LOG_DEBUG("There are no connections for pool id:{}", id);
860 return false;
861 }
862 for (const auto& conn : connections)
863 {
864 if (conn != nullptr && conn->state != ConnState::terminated)
865 {
866 BMCWEB_LOG_DEBUG(
867 "Not all connections of pool id:{} are terminated", id);
868 return false;
869 }
870 }
871 BMCWEB_LOG_INFO("All connections of pool id:{} are terminated", id);
872 return true;
873 }
874 };
875
876 class HttpClient
877 {
878 private:
879 std::unordered_map<std::string, std::shared_ptr<ConnectionPool>>
880 connectionPools;
881
882 // reference_wrapper here makes HttpClient movable
883 std::reference_wrapper<boost::asio::io_context> ioc;
884 std::shared_ptr<ConnectionPolicy> connPolicy;
885
886 // Used as a dummy callback by sendData() in order to call
887 // sendDataWithCallback()
genericResHandler(const Response & res)888 static void genericResHandler(const Response& res)
889 {
890 BMCWEB_LOG_DEBUG("Response handled with return code: {}",
891 res.resultInt());
892 }
893
894 public:
895 HttpClient() = delete;
HttpClient(boost::asio::io_context & iocIn,const std::shared_ptr<ConnectionPolicy> & connPolicyIn)896 explicit HttpClient(boost::asio::io_context& iocIn,
897 const std::shared_ptr<ConnectionPolicy>& connPolicyIn) :
898 ioc(iocIn), connPolicy(connPolicyIn)
899 {}
900
901 HttpClient(const HttpClient&) = delete;
902 HttpClient& operator=(const HttpClient&) = delete;
903 HttpClient(HttpClient&& client) = default;
904 HttpClient& operator=(HttpClient&& client) = default;
905 ~HttpClient() = default;
906
907 // Send a request to destIP where additional processing of the
908 // result is not required
sendData(std::string && data,const boost::urls::url_view_base & destUri,ensuressl::VerifyCertificate verifyCert,const boost::beast::http::fields & httpHeader,const boost::beast::http::verb verb)909 void sendData(std::string&& data, const boost::urls::url_view_base& destUri,
910 ensuressl::VerifyCertificate verifyCert,
911 const boost::beast::http::fields& httpHeader,
912 const boost::beast::http::verb verb)
913 {
914 const std::function<void(Response&)> cb = genericResHandler;
915 sendDataWithCallback(std::move(data), destUri, verifyCert, httpHeader,
916 verb, cb);
917 }
918
919 // Send request to destIP and use the provided callback to
920 // handle the response
sendDataWithCallback(std::string && data,const boost::urls::url_view_base & destUrl,ensuressl::VerifyCertificate verifyCert,const boost::beast::http::fields & httpHeader,const boost::beast::http::verb verb,const std::function<void (Response &)> & resHandler)921 void sendDataWithCallback(std::string&& data,
922 const boost::urls::url_view_base& destUrl,
923 ensuressl::VerifyCertificate verifyCert,
924 const boost::beast::http::fields& httpHeader,
925 const boost::beast::http::verb verb,
926 const std::function<void(Response&)>& resHandler)
927 {
928 std::string_view verify = "ssl_verify";
929 if (verifyCert == ensuressl::VerifyCertificate::NoVerify)
930 {
931 verify = "ssl no verify";
932 }
933 std::string clientKey =
934 std::format("{}{}://{}", verify, destUrl.scheme(),
935 destUrl.encoded_host_and_port());
936 auto pool = connectionPools.try_emplace(clientKey);
937 if (pool.first->second == nullptr)
938 {
939 pool.first->second = std::make_shared<ConnectionPool>(
940 ioc, clientKey, connPolicy, destUrl, verifyCert);
941 }
942 // Send the data using either the existing connection pool or the
943 // newly created connection pool
944 pool.first->second->sendData(std::move(data), destUrl, httpHeader, verb,
945 resHandler);
946 }
947
948 // Test whether all connections are terminated (after MaxRetryAttempts)
isTerminated()949 bool isTerminated()
950 {
951 for (const auto& pool : connectionPools)
952 {
953 if (pool.second != nullptr &&
954 !pool.second->areAllConnectionsTerminated())
955 {
956 BMCWEB_LOG_DEBUG(
957 "Not all of client connections are terminated");
958 return false;
959 }
960 }
961 BMCWEB_LOG_DEBUG("All client connections are terminated");
962 return true;
963 }
964 };
965 } // namespace crow
966