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