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