xref: /openbmc/bmcweb/http/http_client.hpp (revision a0969c70)
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 
defaultRetryHandler(unsigned int respCode)83 inline boost::system::error_code defaultRetryHandler(unsigned int respCode)
84 {
85     // As a default, assume 200X is alright
86     BMCWEB_LOG_DEBUG("Using default check for response code validity");
87     if ((respCode < 200) || (respCode >= 300))
88     {
89         return boost::system::errc::make_error_code(
90             boost::system::errc::result_out_of_range);
91     }
92 
93     // Return 0 if the response code is valid
94     return boost::system::errc::make_error_code(boost::system::errc::success);
95 };
96 
97 // We need to allow retry information to be set before a message has been
98 // sent and a connection pool has been created
99 struct ConnectionPolicy
100 {
101     uint32_t maxRetryAttempts = 5;
102 
103     // the max size of requests in bytes.  0 for unlimited
104     boost::optional<uint64_t> requestByteLimit = httpReadBodyLimit;
105 
106     size_t maxConnections = 1;
107 
108     std::string retryPolicyAction = "TerminateAfterRetries";
109 
110     std::chrono::seconds retryIntervalSecs = std::chrono::seconds(0);
111     std::function<boost::system::error_code(unsigned int respCode)>
112         invalidResp = defaultRetryHandler;
113 };
114 
115 struct PendingRequest
116 {
117     boost::beast::http::request<bmcweb::HttpBody> req;
118     std::function<void(bool, uint32_t, Response&)> callback;
PendingRequestcrow::PendingRequest119     PendingRequest(
120         boost::beast::http::request<bmcweb::HttpBody>&& reqIn,
121         const std::function<void(bool, uint32_t, Response&)>& callbackIn) :
122         req(std::move(reqIn)), callback(callbackIn)
123     {}
124 };
125 
126 namespace http = boost::beast::http;
127 class ConnectionInfo : public std::enable_shared_from_this<ConnectionInfo>
128 {
129   private:
130     ConnState state = ConnState::initialized;
131     uint32_t retryCount = 0;
132     std::string subId;
133     std::shared_ptr<ConnectionPolicy> connPolicy;
134     boost::urls::url host;
135     ensuressl::VerifyCertificate verifyCert;
136     uint32_t connId;
137     // Data buffers
138     http::request<bmcweb::HttpBody> req;
139     using parser_type = http::response_parser<bmcweb::HttpBody>;
140     std::optional<parser_type> parser;
141     boost::beast::flat_static_buffer<httpReadBufferSize> buffer;
142     Response res;
143 
144     // Ascync callables
145     std::function<void(bool, uint32_t, Response&)> callback;
146 
147     boost::asio::io_context& ioc;
148 
149     using Resolver = std::conditional_t<BMCWEB_DNS_RESOLVER == "systemd-dbus",
150                                         async_resolve::Resolver,
151                                         boost::asio::ip::tcp::resolver>;
152     Resolver resolver;
153 
154     boost::asio::ip::tcp::socket conn;
155     std::optional<boost::asio::ssl::stream<boost::asio::ip::tcp::socket&>>
156         sslConn;
157 
158     boost::asio::steady_timer timer;
159 
160     friend class ConnectionPool;
161 
doResolve()162     void doResolve()
163     {
164         state = ConnState::resolveInProgress;
165         BMCWEB_LOG_DEBUG("Trying to resolve: {}, id: {}", host, connId);
166 
167         resolver.async_resolve(host.encoded_host_address(), host.port(),
168                                std::bind_front(&ConnectionInfo::afterResolve,
169                                                this, shared_from_this()));
170     }
171 
afterResolve(const std::shared_ptr<ConnectionInfo> &,const boost::system::error_code & ec,const Resolver::results_type & endpointList)172     void afterResolve(const std::shared_ptr<ConnectionInfo>& /*self*/,
173                       const boost::system::error_code& ec,
174                       const Resolver::results_type& endpointList)
175     {
176         if (ec || (endpointList.empty()))
177         {
178             BMCWEB_LOG_ERROR("Resolve failed: {} {}", ec.message(), host);
179             state = ConnState::resolveFailed;
180             waitAndRetry();
181             return;
182         }
183         BMCWEB_LOG_DEBUG("Resolved {}, id: {}", host, connId);
184         state = ConnState::connectInProgress;
185 
186         BMCWEB_LOG_DEBUG("Trying to connect to: {}, id: {}", host, connId);
187 
188         timer.expires_after(std::chrono::seconds(30));
189         timer.async_wait(std::bind_front(onTimeout, weak_from_this()));
190 
191         boost::asio::async_connect(
192             conn, endpointList,
193             std::bind_front(&ConnectionInfo::afterConnect, this,
194                             shared_from_this()));
195     }
196 
afterConnect(const std::shared_ptr<ConnectionInfo> &,const boost::beast::error_code & ec,const boost::asio::ip::tcp::endpoint & endpoint)197     void afterConnect(const std::shared_ptr<ConnectionInfo>& /*self*/,
198                       const boost::beast::error_code& ec,
199                       const boost::asio::ip::tcp::endpoint& endpoint)
200     {
201         // The operation already timed out.  We don't want do continue down
202         // this branch
203         if (ec && ec == boost::asio::error::operation_aborted)
204         {
205             return;
206         }
207 
208         timer.cancel();
209         if (ec)
210         {
211             BMCWEB_LOG_ERROR("Connect {}:{}, id: {} failed: {}",
212                              endpoint.address().to_string(), endpoint.port(),
213                              connId, ec.message());
214             state = ConnState::connectFailed;
215             waitAndRetry();
216             return;
217         }
218         BMCWEB_LOG_DEBUG("Connected to: {}:{}, id: {}",
219                          endpoint.address().to_string(), endpoint.port(),
220                          connId);
221         if (sslConn)
222         {
223             doSslHandshake();
224             return;
225         }
226         state = ConnState::connected;
227         sendMessage();
228     }
229 
doSslHandshake()230     void doSslHandshake()
231     {
232         if (!sslConn)
233         {
234             return;
235         }
236         state = ConnState::handshakeInProgress;
237         timer.expires_after(std::chrono::seconds(30));
238         timer.async_wait(std::bind_front(onTimeout, weak_from_this()));
239         sslConn->async_handshake(
240             boost::asio::ssl::stream_base::client,
241             std::bind_front(&ConnectionInfo::afterSslHandshake, this,
242                             shared_from_this()));
243     }
244 
afterSslHandshake(const std::shared_ptr<ConnectionInfo> &,const boost::beast::error_code & ec)245     void afterSslHandshake(const std::shared_ptr<ConnectionInfo>& /*self*/,
246                            const boost::beast::error_code& ec)
247     {
248         // The operation already timed out.  We don't want do continue down
249         // this branch
250         if (ec && ec == boost::asio::error::operation_aborted)
251         {
252             return;
253         }
254 
255         timer.cancel();
256         if (ec)
257         {
258             BMCWEB_LOG_ERROR("SSL Handshake failed - id: {} error: {}", connId,
259                              ec.message());
260             state = ConnState::handshakeFailed;
261             waitAndRetry();
262             return;
263         }
264         BMCWEB_LOG_DEBUG("SSL Handshake successful - id: {}", connId);
265         state = ConnState::connected;
266         sendMessage();
267     }
268 
sendMessage()269     void sendMessage()
270     {
271         state = ConnState::sendInProgress;
272 
273         // Set a timeout on the operation
274         timer.expires_after(std::chrono::seconds(30));
275         timer.async_wait(std::bind_front(onTimeout, weak_from_this()));
276         // Send the HTTP request to the remote host
277         if (sslConn)
278         {
279             boost::beast::http::async_write(
280                 *sslConn, req,
281                 std::bind_front(&ConnectionInfo::afterWrite, this,
282                                 shared_from_this()));
283         }
284         else
285         {
286             boost::beast::http::async_write(
287                 conn, req,
288                 std::bind_front(&ConnectionInfo::afterWrite, this,
289                                 shared_from_this()));
290         }
291     }
292 
afterWrite(const std::shared_ptr<ConnectionInfo> &,const boost::beast::error_code & ec,size_t bytesTransferred)293     void afterWrite(const std::shared_ptr<ConnectionInfo>& /*self*/,
294                     const boost::beast::error_code& ec, size_t bytesTransferred)
295     {
296         // The operation already timed out.  We don't want do continue down
297         // this branch
298         if (ec && ec == boost::asio::error::operation_aborted)
299         {
300             return;
301         }
302 
303         timer.cancel();
304         if (ec)
305         {
306             BMCWEB_LOG_ERROR("sendMessage() failed: {} {}", ec.message(), host);
307             state = ConnState::sendFailed;
308             waitAndRetry();
309             return;
310         }
311         BMCWEB_LOG_DEBUG("sendMessage() bytes transferred: {}",
312                          bytesTransferred);
313 
314         recvMessage();
315     }
316 
recvMessage()317     void recvMessage()
318     {
319         state = ConnState::recvInProgress;
320 
321         parser_type& thisParser =
322             parser.emplace(std::piecewise_construct, std::make_tuple());
323 
324         thisParser.body_limit(connPolicy->requestByteLimit);
325 
326         timer.expires_after(std::chrono::seconds(30));
327         timer.async_wait(std::bind_front(onTimeout, weak_from_this()));
328 
329         // Receive the HTTP response
330         if (sslConn)
331         {
332             boost::beast::http::async_read(
333                 *sslConn, buffer, thisParser,
334                 std::bind_front(&ConnectionInfo::afterRead, this,
335                                 shared_from_this()));
336         }
337         else
338         {
339             boost::beast::http::async_read(
340                 conn, buffer, thisParser,
341                 std::bind_front(&ConnectionInfo::afterRead, this,
342                                 shared_from_this()));
343         }
344     }
345 
afterRead(const std::shared_ptr<ConnectionInfo> &,const boost::beast::error_code & ec,const std::size_t & bytesTransferred)346     void afterRead(const std::shared_ptr<ConnectionInfo>& /*self*/,
347                    const boost::beast::error_code& ec,
348                    const std::size_t& bytesTransferred)
349     {
350         // The operation already timed out.  We don't want do continue down
351         // this branch
352         if (ec && ec == boost::asio::error::operation_aborted)
353         {
354             return;
355         }
356 
357         timer.cancel();
358         if (ec && ec != boost::asio::ssl::error::stream_truncated)
359         {
360             BMCWEB_LOG_ERROR("recvMessage() failed: {} from {}", ec.message(),
361                              host);
362             state = ConnState::recvFailed;
363             waitAndRetry();
364             return;
365         }
366         BMCWEB_LOG_DEBUG("recvMessage() bytes transferred: {}",
367                          bytesTransferred);
368         if (!parser)
369         {
370             return;
371         }
372         BMCWEB_LOG_DEBUG("recvMessage() data: {}", parser->get().body().str());
373 
374         unsigned int respCode = parser->get().result_int();
375         BMCWEB_LOG_DEBUG("recvMessage() Header Response Code: {}", respCode);
376 
377         // Handle the case of stream_truncated.  Some servers close the ssl
378         // connection uncleanly, so check to see if we got a full response
379         // before we handle this as an error.
380         if (!parser->is_done())
381         {
382             state = ConnState::recvFailed;
383             waitAndRetry();
384             return;
385         }
386 
387         // Make sure the received response code is valid as defined by
388         // the associated retry policy
389         if (connPolicy->invalidResp(respCode))
390         {
391             // The listener failed to receive the Sent-Event
392             BMCWEB_LOG_ERROR(
393                 "recvMessage() Listener Failed to "
394                 "receive Sent-Event. Header Response Code: {} from {}",
395                 respCode, host);
396             state = ConnState::recvFailed;
397             waitAndRetry();
398             return;
399         }
400 
401         // Send is successful
402         // Reset the counter just in case this was after retrying
403         retryCount = 0;
404 
405         // Keep the connection alive if server supports it
406         // Else close the connection
407         BMCWEB_LOG_DEBUG("recvMessage() keepalive : {}", parser->keep_alive());
408 
409         // Copy the response into a Response object so that it can be
410         // processed by the callback function.
411         res.response = parser->release();
412         callback(parser->keep_alive(), connId, res);
413         res.clear();
414     }
415 
onTimeout(const std::weak_ptr<ConnectionInfo> & weakSelf,const boost::system::error_code & ec)416     static void onTimeout(const std::weak_ptr<ConnectionInfo>& weakSelf,
417                           const boost::system::error_code& ec)
418     {
419         if (ec == boost::asio::error::operation_aborted)
420         {
421             BMCWEB_LOG_DEBUG(
422                 "async_wait failed since the operation is aborted");
423             return;
424         }
425         if (ec)
426         {
427             BMCWEB_LOG_ERROR("async_wait failed: {}", ec.message());
428             // If the timer fails, we need to close the socket anyway, same
429             // as if it expired.
430         }
431         std::shared_ptr<ConnectionInfo> self = weakSelf.lock();
432         if (self == nullptr)
433         {
434             return;
435         }
436         self->waitAndRetry();
437     }
438 
waitAndRetry()439     void waitAndRetry()
440     {
441         if ((retryCount >= connPolicy->maxRetryAttempts) ||
442             (state == ConnState::sslInitFailed))
443         {
444             BMCWEB_LOG_ERROR("Maximum number of retries reached. {}", host);
445             BMCWEB_LOG_DEBUG("Retry policy: {}", connPolicy->retryPolicyAction);
446 
447             if (connPolicy->retryPolicyAction == "TerminateAfterRetries")
448             {
449                 // TODO: delete subscription
450                 state = ConnState::terminated;
451             }
452             if (connPolicy->retryPolicyAction == "SuspendRetries")
453             {
454                 state = ConnState::suspended;
455             }
456 
457             // We want to return a 502 to indicate there was an error with
458             // the external server
459             res.result(boost::beast::http::status::bad_gateway);
460             callback(false, connId, res);
461             res.clear();
462 
463             // Reset the retrycount to zero so that client can try
464             // connecting again if needed
465             retryCount = 0;
466             return;
467         }
468 
469         retryCount++;
470 
471         BMCWEB_LOG_DEBUG("Attempt retry after {} seconds. RetryCount = {}",
472                          connPolicy->retryIntervalSecs.count(), retryCount);
473         timer.expires_after(connPolicy->retryIntervalSecs);
474         timer.async_wait(std::bind_front(&ConnectionInfo::onTimerDone, this,
475                                          shared_from_this()));
476     }
477 
onTimerDone(const std::shared_ptr<ConnectionInfo> &,const boost::system::error_code & ec)478     void onTimerDone(const std::shared_ptr<ConnectionInfo>& /*self*/,
479                      const boost::system::error_code& ec)
480     {
481         if (ec == boost::asio::error::operation_aborted)
482         {
483             BMCWEB_LOG_DEBUG(
484                 "async_wait failed since the operation is aborted{}",
485                 ec.message());
486         }
487         else if (ec)
488         {
489             BMCWEB_LOG_ERROR("async_wait failed: {}", ec.message());
490             // Ignore the error and continue the retry loop to attempt
491             // sending the event as per the retry policy
492         }
493 
494         // Let's close the connection and restart from resolve.
495         shutdownConn(true);
496     }
497 
restartConnection()498     void restartConnection()
499     {
500         BMCWEB_LOG_DEBUG("{}, id: {}  restartConnection", host,
501                          std::to_string(connId));
502         initializeConnection(host.scheme() == "https");
503         doResolve();
504     }
505 
shutdownConn(bool retry)506     void shutdownConn(bool retry)
507     {
508         boost::beast::error_code ec;
509         conn.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
510         conn.close();
511 
512         // not_connected happens sometimes so don't bother reporting it.
513         if (ec && ec != boost::beast::errc::not_connected)
514         {
515             BMCWEB_LOG_ERROR("{}, id: {} shutdown failed: {}", host, connId,
516                              ec.message());
517         }
518         else
519         {
520             BMCWEB_LOG_DEBUG("{}, id: {} closed gracefully", host, connId);
521         }
522 
523         if (retry)
524         {
525             // Now let's try to resend the data
526             state = ConnState::retry;
527             restartConnection();
528         }
529         else
530         {
531             state = ConnState::closed;
532         }
533     }
534 
doClose(bool retry=false)535     void doClose(bool retry = false)
536     {
537         if (!sslConn)
538         {
539             shutdownConn(retry);
540             return;
541         }
542 
543         sslConn->async_shutdown(
544             std::bind_front(&ConnectionInfo::afterSslShutdown, this,
545                             shared_from_this(), retry));
546     }
547 
afterSslShutdown(const std::shared_ptr<ConnectionInfo> &,bool retry,const boost::system::error_code & ec)548     void afterSslShutdown(const std::shared_ptr<ConnectionInfo>& /*self*/,
549                           bool retry, const boost::system::error_code& ec)
550     {
551         if (ec)
552         {
553             BMCWEB_LOG_ERROR("{}, id: {} shutdown failed: {}", host, connId,
554                              ec.message());
555         }
556         else
557         {
558             BMCWEB_LOG_DEBUG("{}, id: {} closed gracefully", host, connId);
559         }
560         shutdownConn(retry);
561     }
562 
setCipherSuiteTLSext()563     void setCipherSuiteTLSext()
564     {
565         if (!sslConn)
566         {
567             return;
568         }
569 
570         if (host.host_type() != boost::urls::host_type::name)
571         {
572             // Avoid setting SNI hostname if its IP address
573             return;
574         }
575         // Create a null terminated string for SSL
576         std::string hostname(host.encoded_host_address());
577         // NOTE: The SSL_set_tlsext_host_name is defined in tlsv1.h header
578         // file but its having old style casting (name is cast to void*).
579         // Since bmcweb compiler treats all old-style-cast as error, its
580         // causing the build failure. So replaced the same macro inline and
581         // did corrected the code by doing static_cast to viod*. This has to
582         // be fixed in openssl library in long run. Set SNI Hostname (many
583         // hosts need this to handshake successfully)
584         if (SSL_ctrl(sslConn->native_handle(), SSL_CTRL_SET_TLSEXT_HOSTNAME,
585                      TLSEXT_NAMETYPE_host_name,
586                      static_cast<void*>(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