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