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