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