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