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