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