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