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