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