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