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