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