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