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