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