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