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