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