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