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