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