xref: /openbmc/bmcweb/http/http_client.hpp (revision d78572018fc2022091ff8b8eb5a7fef2172ba3d6)
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     // Ascync 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                              endpoint.address().to_string(), endpoint.port(),
223                              connId, 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         state = ConnState::handshakeInProgress;
247         timer.expires_after(std::chrono::seconds(30));
248         timer.async_wait(std::bind_front(onTimeout, weak_from_this()));
249         sslConn->async_handshake(
250             boost::asio::ssl::stream_base::client,
251             std::bind_front(&ConnectionInfo::afterSslHandshake, this,
252                             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         // NOTE: The SSL_set_tlsext_host_name is defined in tlsv1.h header
587         // file but its having old style casting (name is cast to void*).
588         // Since bmcweb compiler treats all old-style-cast as error, its
589         // causing the build failure. So replaced the same macro inline and
590         // did corrected the code by doing static_cast to viod*. This has to
591         // be fixed in openssl library in long run. Set SNI Hostname (many
592         // hosts need this to handshake successfully)
593         if (SSL_ctrl(sslConn->native_handle(), SSL_CTRL_SET_TLSEXT_HOSTNAME,
594                      TLSEXT_NAMETYPE_host_name,
595                      static_cast<void*>(hostname.data())) == 0)
596 
597         {
598             boost::beast::error_code ec{static_cast<int>(::ERR_get_error()),
599                                         boost::asio::error::get_ssl_category()};
600 
601             BMCWEB_LOG_ERROR("SSL_set_tlsext_host_name {}, id: {} failed: {}",
602                              host, connId, ec.message());
603             // Set state as sslInit failed so that we close the connection
604             // and take appropriate action as per retry configuration.
605             state = ConnState::sslInitFailed;
606             waitAndRetry();
607             return;
608         }
609     }
610 
initializeConnection(bool ssl)611     void initializeConnection(bool ssl)
612     {
613         conn = boost::asio::ip::tcp::socket(ioc);
614         if (ssl)
615         {
616             std::optional<boost::asio::ssl::context> sslCtx =
617                 ensuressl::getSSLClientContext(verifyCert);
618 
619             if (!sslCtx)
620             {
621                 BMCWEB_LOG_ERROR("prepareSSLContext failed - {}, id: {}", host,
622                                  connId);
623                 // Don't retry if failure occurs while preparing SSL context
624                 // such as certificate is invalid or set cipher failure or
625                 // set host name failure etc... Setting conn state to
626                 // sslInitFailed and connection state will be transitioned
627                 // to next state depending on retry policy set by
628                 // subscription.
629                 state = ConnState::sslInitFailed;
630                 waitAndRetry();
631                 return;
632             }
633             sslConn.emplace(conn, *sslCtx);
634             setCipherSuiteTLSext();
635         }
636     }
637 
638   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)639     explicit ConnectionInfo(
640         boost::asio::io_context& iocIn, const std::string& idIn,
641         const std::shared_ptr<ConnectionPolicy>& connPolicyIn,
642         const boost::urls::url_view_base& hostIn,
643         ensuressl::VerifyCertificate verifyCertIn, unsigned int connIdIn) :
644         subId(idIn), connPolicy(connPolicyIn), host(hostIn),
645         verifyCert(verifyCertIn), connId(connIdIn), ioc(iocIn), resolver(iocIn),
646         conn(iocIn), timer(iocIn)
647     {
648         initializeConnection(host.scheme() == "https");
649     }
650 };
651 
652 class ConnectionPool : public std::enable_shared_from_this<ConnectionPool>
653 {
654   private:
655     boost::asio::io_context& ioc;
656     std::string id;
657     std::shared_ptr<ConnectionPolicy> connPolicy;
658     boost::urls::url destIP;
659     std::vector<std::shared_ptr<ConnectionInfo>> connections;
660     boost::container::devector<PendingRequest> requestQueue;
661     ensuressl::VerifyCertificate verifyCert;
662 
663     friend class HttpClient;
664 
665     // Configure a connections's request, callback, and retry info in
666     // preparation to begin sending the request
setConnProps(ConnectionInfo & conn)667     void setConnProps(ConnectionInfo& conn)
668     {
669         if (requestQueue.empty())
670         {
671             BMCWEB_LOG_ERROR(
672                 "setConnProps() should not have been called when requestQueue is empty");
673             return;
674         }
675 
676         PendingRequest& nextReq = requestQueue.front();
677         conn.req = std::move(nextReq.req);
678         conn.callback = std::move(nextReq.callback);
679 
680         BMCWEB_LOG_DEBUG("Setting properties for connection {}, id: {}",
681                          conn.host, conn.connId);
682 
683         // We can remove the request from the queue at this point
684         requestQueue.pop_front();
685     }
686 
687     // Gets called as part of callback after request is sent
688     // Reuses the connection if there are any requests waiting to be sent
689     // Otherwise closes the connection if it is not a keep-alive
sendNext(bool keepAlive,uint32_t connId)690     void sendNext(bool keepAlive, uint32_t connId)
691     {
692         auto conn = connections[connId];
693 
694         // Allow the connection's handler to be deleted
695         // This is needed because of Redfish Aggregation passing an
696         // AsyncResponse shared_ptr to this callback
697         conn->callback = nullptr;
698 
699         // Reuse the connection to send the next request in the queue
700         if (!requestQueue.empty())
701         {
702             BMCWEB_LOG_DEBUG(
703                 "{} requests remaining in queue for {}, reusing connection {}",
704                 requestQueue.size(), destIP, connId);
705 
706             setConnProps(*conn);
707 
708             if (keepAlive)
709             {
710                 conn->sendMessage();
711             }
712             else
713             {
714                 // Server is not keep-alive enabled so we need to close the
715                 // connection and then start over from resolve
716                 conn->doClose();
717                 conn->restartConnection();
718             }
719             return;
720         }
721 
722         // No more messages to send so close the connection if necessary
723         if (keepAlive)
724         {
725             conn->state = ConnState::idle;
726         }
727         else
728         {
729             // Abort the connection since server is not keep-alive enabled
730             conn->state = ConnState::abortConnection;
731             conn->doClose();
732         }
733     }
734 
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)735     void sendData(std::string&& data, const boost::urls::url_view_base& destUri,
736                   const boost::beast::http::fields& httpHeader,
737                   const boost::beast::http::verb verb,
738                   const std::function<void(Response&)>& resHandler)
739     {
740         // Construct the request to be sent
741         boost::beast::http::request<bmcweb::HttpBody> thisReq(
742             verb, destUri.encoded_target(), 11, "", httpHeader);
743         thisReq.set(boost::beast::http::field::host,
744                     destUri.encoded_host_address());
745         thisReq.keep_alive(true);
746         thisReq.body().str() = std::move(data);
747         thisReq.prepare_payload();
748         auto cb = std::bind_front(&ConnectionPool::afterSendData,
749                                   weak_from_this(), resHandler);
750         // Reuse an existing connection if one is available
751         for (unsigned int i = 0; i < connections.size(); i++)
752         {
753             auto conn = connections[i];
754             if ((conn->state == ConnState::idle) ||
755                 (conn->state == ConnState::initialized) ||
756                 (conn->state == ConnState::closed))
757             {
758                 conn->req = std::move(thisReq);
759                 conn->callback = std::move(cb);
760                 std::string commonMsg = std::format("{} from pool {}", i, id);
761 
762                 if (conn->state == ConnState::idle)
763                 {
764                     BMCWEB_LOG_DEBUG("Grabbing idle connection {}", commonMsg);
765                     conn->sendMessage();
766                 }
767                 else
768                 {
769                     BMCWEB_LOG_DEBUG("Reusing existing connection {}",
770                                      commonMsg);
771                     conn->restartConnection();
772                 }
773                 return;
774             }
775         }
776 
777         // All connections in use so create a new connection or add request
778         // to the queue
779         if (connections.size() < connPolicy->maxConnections)
780         {
781             BMCWEB_LOG_DEBUG("Adding new connection to pool {}", id);
782             auto conn = addConnection();
783             conn->req = std::move(thisReq);
784             conn->callback = std::move(cb);
785             conn->doResolve();
786         }
787         else if (requestQueue.size() < maxRequestQueueSize)
788         {
789             BMCWEB_LOG_DEBUG("Max pool size reached. Adding data to queue {}",
790                              id);
791             requestQueue.emplace_back(std::move(thisReq), std::move(cb));
792         }
793         else
794         {
795             // If we can't buffer the request then we should let the
796             // callback handle a 429 Too Many Requests dummy response
797             BMCWEB_LOG_ERROR("{} request queue full.  Dropping request.", id);
798             Response dummyRes;
799             dummyRes.result(boost::beast::http::status::too_many_requests);
800             resHandler(dummyRes);
801         }
802     }
803 
804     // 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)805     static void afterSendData(const std::weak_ptr<ConnectionPool>& weakSelf,
806                               const std::function<void(Response&)>& resHandler,
807                               bool keepAlive, uint32_t connId, Response& res)
808     {
809         // Allow provided callback to perform additional processing of the
810         // request
811         resHandler(res);
812 
813         // If requests remain in the queue then we want to reuse this
814         // connection to send the next request
815         std::shared_ptr<ConnectionPool> self = weakSelf.lock();
816         if (!self)
817         {
818             BMCWEB_LOG_CRITICAL("{} Failed to capture connection",
819                                 logPtr(self.get()));
820             return;
821         }
822 
823         self->sendNext(keepAlive, connId);
824     }
825 
addConnection()826     std::shared_ptr<ConnectionInfo>& addConnection()
827     {
828         unsigned int newId = static_cast<unsigned int>(connections.size());
829 
830         auto& ret = connections.emplace_back(std::make_shared<ConnectionInfo>(
831             ioc, id, connPolicy, destIP, verifyCert, newId));
832 
833         BMCWEB_LOG_DEBUG("Added connection {} to pool {}",
834                          connections.size() - 1, id);
835 
836         return ret;
837     }
838 
839   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)840     explicit ConnectionPool(
841         boost::asio::io_context& iocIn, const std::string& idIn,
842         const std::shared_ptr<ConnectionPolicy>& connPolicyIn,
843         const boost::urls::url_view_base& destIPIn,
844         ensuressl::VerifyCertificate verifyCertIn) :
845         ioc(iocIn), id(idIn), connPolicy(connPolicyIn), destIP(destIPIn),
846         verifyCert(verifyCertIn)
847     {
848         BMCWEB_LOG_DEBUG("Initializing connection pool for {}", id);
849 
850         // Initialize the pool with a single connection
851         addConnection();
852     }
853 
854     // Check whether all connections are terminated
areAllConnectionsTerminated()855     bool areAllConnectionsTerminated()
856     {
857         if (connections.empty())
858         {
859             BMCWEB_LOG_DEBUG("There are no connections for pool id:{}", id);
860             return false;
861         }
862         for (const auto& conn : connections)
863         {
864             if (conn != nullptr && conn->state != ConnState::terminated)
865             {
866                 BMCWEB_LOG_DEBUG(
867                     "Not all connections of pool id:{} are terminated", id);
868                 return false;
869             }
870         }
871         BMCWEB_LOG_INFO("All connections of pool id:{} are terminated", id);
872         return true;
873     }
874 };
875 
876 class HttpClient
877 {
878   private:
879     std::unordered_map<std::string, std::shared_ptr<ConnectionPool>>
880         connectionPools;
881 
882     // reference_wrapper here makes HttpClient movable
883     std::reference_wrapper<boost::asio::io_context> ioc;
884     std::shared_ptr<ConnectionPolicy> connPolicy;
885 
886     // Used as a dummy callback by sendData() in order to call
887     // sendDataWithCallback()
genericResHandler(const Response & res)888     static void genericResHandler(const Response& res)
889     {
890         BMCWEB_LOG_DEBUG("Response handled with return code: {}",
891                          res.resultInt());
892     }
893 
894   public:
895     HttpClient() = delete;
HttpClient(boost::asio::io_context & iocIn,const std::shared_ptr<ConnectionPolicy> & connPolicyIn)896     explicit HttpClient(boost::asio::io_context& iocIn,
897                         const std::shared_ptr<ConnectionPolicy>& connPolicyIn) :
898         ioc(iocIn), connPolicy(connPolicyIn)
899     {}
900 
901     HttpClient(const HttpClient&) = delete;
902     HttpClient& operator=(const HttpClient&) = delete;
903     HttpClient(HttpClient&& client) = default;
904     HttpClient& operator=(HttpClient&& client) = default;
905     ~HttpClient() = default;
906 
907     // Send a request to destIP where additional processing of the
908     // 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)909     void sendData(std::string&& data, const boost::urls::url_view_base& destUri,
910                   ensuressl::VerifyCertificate verifyCert,
911                   const boost::beast::http::fields& httpHeader,
912                   const boost::beast::http::verb verb)
913     {
914         const std::function<void(Response&)> cb = genericResHandler;
915         sendDataWithCallback(std::move(data), destUri, verifyCert, httpHeader,
916                              verb, cb);
917     }
918 
919     // Send request to destIP and use the provided callback to
920     // 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)921     void sendDataWithCallback(std::string&& data,
922                               const boost::urls::url_view_base& destUrl,
923                               ensuressl::VerifyCertificate verifyCert,
924                               const boost::beast::http::fields& httpHeader,
925                               const boost::beast::http::verb verb,
926                               const std::function<void(Response&)>& resHandler)
927     {
928         std::string_view verify = "ssl_verify";
929         if (verifyCert == ensuressl::VerifyCertificate::NoVerify)
930         {
931             verify = "ssl no verify";
932         }
933         std::string clientKey =
934             std::format("{}{}://{}", verify, destUrl.scheme(),
935                         destUrl.encoded_host_and_port());
936         auto pool = connectionPools.try_emplace(clientKey);
937         if (pool.first->second == nullptr)
938         {
939             pool.first->second = std::make_shared<ConnectionPool>(
940                 ioc, clientKey, connPolicy, destUrl, verifyCert);
941         }
942         // Send the data using either the existing connection pool or the
943         // newly created connection pool
944         pool.first->second->sendData(std::move(data), destUrl, httpHeader, verb,
945                                      resHandler);
946     }
947 
948     // Test whether all connections are terminated (after MaxRetryAttempts)
isTerminated()949     bool isTerminated()
950     {
951         for (const auto& pool : connectionPools)
952         {
953             if (pool.second != nullptr &&
954                 !pool.second->areAllConnectionsTerminated())
955             {
956                 BMCWEB_LOG_DEBUG(
957                     "Not all of client connections are terminated");
958                 return false;
959             }
960         }
961         BMCWEB_LOG_DEBUG("All client connections are terminated");
962         return true;
963     }
964 };
965 } // namespace crow
966