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