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