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