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