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