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