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