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 = 322 parser.emplace(std::piecewise_construct, std::make_tuple()); 323 324 thisParser.body_limit(connPolicy->requestByteLimit); 325 326 timer.expires_after(std::chrono::seconds(30)); 327 timer.async_wait(std::bind_front(onTimeout, weak_from_this())); 328 329 // Receive the HTTP response 330 if (sslConn) 331 { 332 boost::beast::http::async_read( 333 *sslConn, buffer, thisParser, 334 std::bind_front(&ConnectionInfo::afterRead, this, 335 shared_from_this())); 336 } 337 else 338 { 339 boost::beast::http::async_read( 340 conn, buffer, thisParser, 341 std::bind_front(&ConnectionInfo::afterRead, this, 342 shared_from_this())); 343 } 344 } 345 346 void afterRead(const std::shared_ptr<ConnectionInfo>& /*self*/, 347 const boost::beast::error_code& ec, 348 const std::size_t& bytesTransferred) 349 { 350 // The operation already timed out. We don't want do continue down 351 // this branch 352 if (ec && ec == boost::asio::error::operation_aborted) 353 { 354 return; 355 } 356 357 timer.cancel(); 358 if (ec && ec != boost::asio::ssl::error::stream_truncated) 359 { 360 BMCWEB_LOG_ERROR("recvMessage() failed: {} from {}", ec.message(), 361 host); 362 state = ConnState::recvFailed; 363 waitAndRetry(); 364 return; 365 } 366 BMCWEB_LOG_DEBUG("recvMessage() bytes transferred: {}", 367 bytesTransferred); 368 if (!parser) 369 { 370 return; 371 } 372 BMCWEB_LOG_DEBUG("recvMessage() data: {}", parser->get().body().str()); 373 374 unsigned int respCode = parser->get().result_int(); 375 BMCWEB_LOG_DEBUG("recvMessage() Header Response Code: {}", respCode); 376 377 // Handle the case of stream_truncated. Some servers close the ssl 378 // connection uncleanly, so check to see if we got a full response 379 // before we handle this as an error. 380 if (!parser->is_done()) 381 { 382 state = ConnState::recvFailed; 383 waitAndRetry(); 384 return; 385 } 386 387 // Make sure the received response code is valid as defined by 388 // the associated retry policy 389 if (connPolicy->invalidResp(respCode)) 390 { 391 // The listener failed to receive the Sent-Event 392 BMCWEB_LOG_ERROR( 393 "recvMessage() Listener Failed to " 394 "receive Sent-Event. Header Response Code: {} from {}", 395 respCode, host); 396 state = ConnState::recvFailed; 397 waitAndRetry(); 398 return; 399 } 400 401 // Send is successful 402 // Reset the counter just in case this was after retrying 403 retryCount = 0; 404 405 // Keep the connection alive if server supports it 406 // Else close the connection 407 BMCWEB_LOG_DEBUG("recvMessage() keepalive : {}", parser->keep_alive()); 408 409 // Copy the response into a Response object so that it can be 410 // processed by the callback function. 411 res.response = parser->release(); 412 callback(parser->keep_alive(), connId, res); 413 res.clear(); 414 } 415 416 static void onTimeout(const std::weak_ptr<ConnectionInfo>& weakSelf, 417 const boost::system::error_code& ec) 418 { 419 if (ec == boost::asio::error::operation_aborted) 420 { 421 BMCWEB_LOG_DEBUG( 422 "async_wait failed since the operation is aborted"); 423 return; 424 } 425 if (ec) 426 { 427 BMCWEB_LOG_ERROR("async_wait failed: {}", ec.message()); 428 // If the timer fails, we need to close the socket anyway, same 429 // as if it expired. 430 } 431 std::shared_ptr<ConnectionInfo> self = weakSelf.lock(); 432 if (self == nullptr) 433 { 434 return; 435 } 436 self->waitAndRetry(); 437 } 438 439 void waitAndRetry() 440 { 441 if ((retryCount >= connPolicy->maxRetryAttempts) || 442 (state == ConnState::sslInitFailed)) 443 { 444 BMCWEB_LOG_ERROR("Maximum number of retries reached. {}", host); 445 BMCWEB_LOG_DEBUG("Retry policy: {}", connPolicy->retryPolicyAction); 446 447 if (connPolicy->retryPolicyAction == "TerminateAfterRetries") 448 { 449 // TODO: delete subscription 450 state = ConnState::terminated; 451 } 452 if (connPolicy->retryPolicyAction == "SuspendRetries") 453 { 454 state = ConnState::suspended; 455 } 456 457 // We want to return a 502 to indicate there was an error with 458 // the external server 459 res.result(boost::beast::http::status::bad_gateway); 460 callback(false, connId, res); 461 res.clear(); 462 463 // Reset the retrycount to zero so that client can try 464 // connecting again if needed 465 retryCount = 0; 466 return; 467 } 468 469 retryCount++; 470 471 BMCWEB_LOG_DEBUG("Attempt retry after {} seconds. RetryCount = {}", 472 connPolicy->retryIntervalSecs.count(), retryCount); 473 timer.expires_after(connPolicy->retryIntervalSecs); 474 timer.async_wait(std::bind_front(&ConnectionInfo::onTimerDone, this, 475 shared_from_this())); 476 } 477 478 void onTimerDone(const std::shared_ptr<ConnectionInfo>& /*self*/, 479 const boost::system::error_code& ec) 480 { 481 if (ec == boost::asio::error::operation_aborted) 482 { 483 BMCWEB_LOG_DEBUG( 484 "async_wait failed since the operation is aborted{}", 485 ec.message()); 486 } 487 else if (ec) 488 { 489 BMCWEB_LOG_ERROR("async_wait failed: {}", ec.message()); 490 // Ignore the error and continue the retry loop to attempt 491 // sending the event as per the retry policy 492 } 493 494 // Let's close the connection and restart from resolve. 495 shutdownConn(true); 496 } 497 498 void restartConnection() 499 { 500 BMCWEB_LOG_DEBUG("{}, id: {} restartConnection", host, 501 std::to_string(connId)); 502 initializeConnection(host.scheme() == "https"); 503 doResolve(); 504 } 505 506 void shutdownConn(bool retry) 507 { 508 boost::beast::error_code ec; 509 conn.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); 510 conn.close(); 511 512 // not_connected happens sometimes so don't bother reporting it. 513 if (ec && ec != boost::beast::errc::not_connected) 514 { 515 BMCWEB_LOG_ERROR("{}, id: {} shutdown failed: {}", host, connId, 516 ec.message()); 517 } 518 else 519 { 520 BMCWEB_LOG_DEBUG("{}, id: {} closed gracefully", host, connId); 521 } 522 523 if (retry) 524 { 525 // Now let's try to resend the data 526 state = ConnState::retry; 527 restartConnection(); 528 } 529 else 530 { 531 state = ConnState::closed; 532 } 533 } 534 535 void doClose(bool retry = false) 536 { 537 if (!sslConn) 538 { 539 shutdownConn(retry); 540 return; 541 } 542 543 sslConn->async_shutdown( 544 std::bind_front(&ConnectionInfo::afterSslShutdown, this, 545 shared_from_this(), retry)); 546 } 547 548 void afterSslShutdown(const std::shared_ptr<ConnectionInfo>& /*self*/, 549 bool retry, const boost::system::error_code& ec) 550 { 551 if (ec) 552 { 553 BMCWEB_LOG_ERROR("{}, id: {} shutdown failed: {}", host, connId, 554 ec.message()); 555 } 556 else 557 { 558 BMCWEB_LOG_DEBUG("{}, id: {} closed gracefully", host, connId); 559 } 560 shutdownConn(retry); 561 } 562 563 void setCipherSuiteTLSext() 564 { 565 if (!sslConn) 566 { 567 return; 568 } 569 570 if (host.host_type() != boost::urls::host_type::name) 571 { 572 // Avoid setting SNI hostname if its IP address 573 return; 574 } 575 // Create a null terminated string for SSL 576 std::string hostname(host.encoded_host_address()); 577 // NOTE: The SSL_set_tlsext_host_name is defined in tlsv1.h header 578 // file but its having old style casting (name is cast to void*). 579 // Since bmcweb compiler treats all old-style-cast as error, its 580 // causing the build failure. So replaced the same macro inline and 581 // did corrected the code by doing static_cast to viod*. This has to 582 // be fixed in openssl library in long run. Set SNI Hostname (many 583 // hosts need this to handshake successfully) 584 if (SSL_ctrl(sslConn->native_handle(), SSL_CTRL_SET_TLSEXT_HOSTNAME, 585 TLSEXT_NAMETYPE_host_name, 586 static_cast<void*>(hostname.data())) == 0) 587 588 { 589 boost::beast::error_code ec{static_cast<int>(::ERR_get_error()), 590 boost::asio::error::get_ssl_category()}; 591 592 BMCWEB_LOG_ERROR("SSL_set_tlsext_host_name {}, id: {} failed: {}", 593 host, connId, ec.message()); 594 // Set state as sslInit failed so that we close the connection 595 // and take appropriate action as per retry configuration. 596 state = ConnState::sslInitFailed; 597 waitAndRetry(); 598 return; 599 } 600 } 601 602 void initializeConnection(bool ssl) 603 { 604 conn = boost::asio::ip::tcp::socket(ioc); 605 if (ssl) 606 { 607 std::optional<boost::asio::ssl::context> sslCtx = 608 ensuressl::getSSLClientContext(verifyCert); 609 610 if (!sslCtx) 611 { 612 BMCWEB_LOG_ERROR("prepareSSLContext failed - {}, id: {}", host, 613 connId); 614 // Don't retry if failure occurs while preparing SSL context 615 // such as certificate is invalid or set cipher failure or 616 // set host name failure etc... Setting conn state to 617 // sslInitFailed and connection state will be transitioned 618 // to next state depending on retry policy set by 619 // subscription. 620 state = ConnState::sslInitFailed; 621 waitAndRetry(); 622 return; 623 } 624 sslConn.emplace(conn, *sslCtx); 625 setCipherSuiteTLSext(); 626 } 627 } 628 629 public: 630 explicit ConnectionInfo( 631 boost::asio::io_context& iocIn, const std::string& idIn, 632 const std::shared_ptr<ConnectionPolicy>& connPolicyIn, 633 const boost::urls::url_view_base& hostIn, 634 ensuressl::VerifyCertificate verifyCertIn, unsigned int connIdIn) : 635 subId(idIn), connPolicy(connPolicyIn), host(hostIn), 636 verifyCert(verifyCertIn), connId(connIdIn), ioc(iocIn), resolver(iocIn), 637 conn(iocIn), timer(iocIn) 638 { 639 initializeConnection(host.scheme() == "https"); 640 } 641 }; 642 643 class ConnectionPool : public std::enable_shared_from_this<ConnectionPool> 644 { 645 private: 646 boost::asio::io_context& ioc; 647 std::string id; 648 std::shared_ptr<ConnectionPolicy> connPolicy; 649 boost::urls::url destIP; 650 std::vector<std::shared_ptr<ConnectionInfo>> connections; 651 boost::container::devector<PendingRequest> requestQueue; 652 ensuressl::VerifyCertificate verifyCert; 653 654 friend class HttpClient; 655 656 // Configure a connections's request, callback, and retry info in 657 // preparation to begin sending the request 658 void setConnProps(ConnectionInfo& conn) 659 { 660 if (requestQueue.empty()) 661 { 662 BMCWEB_LOG_ERROR( 663 "setConnProps() should not have been called when requestQueue is empty"); 664 return; 665 } 666 667 PendingRequest& nextReq = requestQueue.front(); 668 conn.req = std::move(nextReq.req); 669 conn.callback = std::move(nextReq.callback); 670 671 BMCWEB_LOG_DEBUG("Setting properties for connection {}, id: {}", 672 conn.host, conn.connId); 673 674 // We can remove the request from the queue at this point 675 requestQueue.pop_front(); 676 } 677 678 // Gets called as part of callback after request is sent 679 // Reuses the connection if there are any requests waiting to be sent 680 // Otherwise closes the connection if it is not a keep-alive 681 void sendNext(bool keepAlive, uint32_t connId) 682 { 683 auto conn = connections[connId]; 684 685 // Allow the connection's handler to be deleted 686 // This is needed because of Redfish Aggregation passing an 687 // AsyncResponse shared_ptr to this callback 688 conn->callback = nullptr; 689 690 // Reuse the connection to send the next request in the queue 691 if (!requestQueue.empty()) 692 { 693 BMCWEB_LOG_DEBUG( 694 "{} requests remaining in queue for {}, reusing connection {}", 695 requestQueue.size(), destIP, connId); 696 697 setConnProps(*conn); 698 699 if (keepAlive) 700 { 701 conn->sendMessage(); 702 } 703 else 704 { 705 // Server is not keep-alive enabled so we need to close the 706 // connection and then start over from resolve 707 conn->doClose(); 708 conn->restartConnection(); 709 } 710 return; 711 } 712 713 // No more messages to send so close the connection if necessary 714 if (keepAlive) 715 { 716 conn->state = ConnState::idle; 717 } 718 else 719 { 720 // Abort the connection since server is not keep-alive enabled 721 conn->state = ConnState::abortConnection; 722 conn->doClose(); 723 } 724 } 725 726 void sendData(std::string&& data, const boost::urls::url_view_base& destUri, 727 const boost::beast::http::fields& httpHeader, 728 const boost::beast::http::verb verb, 729 const std::function<void(Response&)>& resHandler) 730 { 731 // Construct the request to be sent 732 boost::beast::http::request<bmcweb::HttpBody> thisReq( 733 verb, destUri.encoded_target(), 11, "", httpHeader); 734 thisReq.set(boost::beast::http::field::host, 735 destUri.encoded_host_address()); 736 thisReq.keep_alive(true); 737 thisReq.body().str() = std::move(data); 738 thisReq.prepare_payload(); 739 auto cb = std::bind_front(&ConnectionPool::afterSendData, 740 weak_from_this(), resHandler); 741 // Reuse an existing connection if one is available 742 for (unsigned int i = 0; i < connections.size(); i++) 743 { 744 auto conn = connections[i]; 745 if ((conn->state == ConnState::idle) || 746 (conn->state == ConnState::initialized) || 747 (conn->state == ConnState::closed)) 748 { 749 conn->req = std::move(thisReq); 750 conn->callback = std::move(cb); 751 std::string commonMsg = std::format("{} from pool {}", i, id); 752 753 if (conn->state == ConnState::idle) 754 { 755 BMCWEB_LOG_DEBUG("Grabbing idle connection {}", commonMsg); 756 conn->sendMessage(); 757 } 758 else 759 { 760 BMCWEB_LOG_DEBUG("Reusing existing connection {}", 761 commonMsg); 762 conn->restartConnection(); 763 } 764 return; 765 } 766 } 767 768 // All connections in use so create a new connection or add request 769 // to the queue 770 if (connections.size() < connPolicy->maxConnections) 771 { 772 BMCWEB_LOG_DEBUG("Adding new connection to pool {}", id); 773 auto conn = addConnection(); 774 conn->req = std::move(thisReq); 775 conn->callback = std::move(cb); 776 conn->doResolve(); 777 } 778 else if (requestQueue.size() < maxRequestQueueSize) 779 { 780 BMCWEB_LOG_DEBUG("Max pool size reached. Adding data to queue {}", 781 id); 782 requestQueue.emplace_back(std::move(thisReq), std::move(cb)); 783 } 784 else 785 { 786 // If we can't buffer the request then we should let the 787 // callback handle a 429 Too Many Requests dummy response 788 BMCWEB_LOG_ERROR("{} request queue full. Dropping request.", id); 789 Response dummyRes; 790 dummyRes.result(boost::beast::http::status::too_many_requests); 791 resHandler(dummyRes); 792 } 793 } 794 795 // Callback to be called once the request has been sent 796 static void afterSendData(const std::weak_ptr<ConnectionPool>& weakSelf, 797 const std::function<void(Response&)>& resHandler, 798 bool keepAlive, uint32_t connId, Response& res) 799 { 800 // Allow provided callback to perform additional processing of the 801 // request 802 resHandler(res); 803 804 // If requests remain in the queue then we want to reuse this 805 // connection to send the next request 806 std::shared_ptr<ConnectionPool> self = weakSelf.lock(); 807 if (!self) 808 { 809 BMCWEB_LOG_CRITICAL("{} Failed to capture connection", 810 logPtr(self.get())); 811 return; 812 } 813 814 self->sendNext(keepAlive, connId); 815 } 816 817 std::shared_ptr<ConnectionInfo>& addConnection() 818 { 819 unsigned int newId = static_cast<unsigned int>(connections.size()); 820 821 auto& ret = connections.emplace_back(std::make_shared<ConnectionInfo>( 822 ioc, id, connPolicy, destIP, verifyCert, newId)); 823 824 BMCWEB_LOG_DEBUG("Added connection {} to pool {}", 825 connections.size() - 1, id); 826 827 return ret; 828 } 829 830 public: 831 explicit ConnectionPool( 832 boost::asio::io_context& iocIn, const std::string& idIn, 833 const std::shared_ptr<ConnectionPolicy>& connPolicyIn, 834 const boost::urls::url_view_base& destIPIn, 835 ensuressl::VerifyCertificate verifyCertIn) : 836 ioc(iocIn), id(idIn), connPolicy(connPolicyIn), destIP(destIPIn), 837 verifyCert(verifyCertIn) 838 { 839 BMCWEB_LOG_DEBUG("Initializing connection pool for {}", id); 840 841 // Initialize the pool with a single connection 842 addConnection(); 843 } 844 }; 845 846 class HttpClient 847 { 848 private: 849 std::unordered_map<std::string, std::shared_ptr<ConnectionPool>> 850 connectionPools; 851 boost::asio::io_context& ioc; 852 std::shared_ptr<ConnectionPolicy> connPolicy; 853 854 // Used as a dummy callback by sendData() in order to call 855 // sendDataWithCallback() 856 static void genericResHandler(const Response& res) 857 { 858 BMCWEB_LOG_DEBUG("Response handled with return code: {}", 859 res.resultInt()); 860 } 861 862 public: 863 HttpClient() = delete; 864 explicit HttpClient(boost::asio::io_context& iocIn, 865 const std::shared_ptr<ConnectionPolicy>& connPolicyIn) : 866 ioc(iocIn), connPolicy(connPolicyIn) 867 {} 868 869 HttpClient(const HttpClient&) = delete; 870 HttpClient& operator=(const HttpClient&) = delete; 871 HttpClient(HttpClient&&) = delete; 872 HttpClient& operator=(HttpClient&&) = delete; 873 ~HttpClient() = default; 874 875 // Send a request to destIP where additional processing of the 876 // result is not required 877 void sendData(std::string&& data, const boost::urls::url_view_base& destUri, 878 ensuressl::VerifyCertificate verifyCert, 879 const boost::beast::http::fields& httpHeader, 880 const boost::beast::http::verb verb) 881 { 882 const std::function<void(Response&)> cb = genericResHandler; 883 sendDataWithCallback(std::move(data), destUri, verifyCert, httpHeader, 884 verb, cb); 885 } 886 887 // Send request to destIP and use the provided callback to 888 // handle the response 889 void sendDataWithCallback(std::string&& data, 890 const boost::urls::url_view_base& destUrl, 891 ensuressl::VerifyCertificate verifyCert, 892 const boost::beast::http::fields& httpHeader, 893 const boost::beast::http::verb verb, 894 const std::function<void(Response&)>& resHandler) 895 { 896 std::string_view verify = "ssl_verify"; 897 if (verifyCert == ensuressl::VerifyCertificate::NoVerify) 898 { 899 verify = "ssl no verify"; 900 } 901 std::string clientKey = 902 std::format("{}{}://{}", verify, destUrl.scheme(), 903 destUrl.encoded_host_and_port()); 904 auto pool = connectionPools.try_emplace(clientKey); 905 if (pool.first->second == nullptr) 906 { 907 pool.first->second = std::make_shared<ConnectionPool>( 908 ioc, clientKey, connPolicy, destUrl, verifyCert); 909 } 910 // Send the data using either the existing connection pool or the 911 // newly created connection pool 912 pool.first->second->sendData(std::move(data), destUrl, httpHeader, verb, 913 resHandler); 914 } 915 }; 916 } // namespace crow 917