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