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 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; 128 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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: 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 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 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 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 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 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: 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 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() 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; 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 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 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) 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