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 RetryPolicyData 101 { 102 uint32_t maxRetryAttempts = 5; 103 std::chrono::seconds retryIntervalSecs = std::chrono::seconds(0); 104 std::string retryPolicyAction = "TerminateAfterRetries"; 105 std::function<boost::system::error_code(unsigned int respCode)> 106 invalidResp = defaultRetryHandler; 107 }; 108 109 struct PendingRequest 110 { 111 boost::beast::http::request<boost::beast::http::string_body> req; 112 std::function<void(bool, uint32_t, Response&)> callback; 113 RetryPolicyData retryPolicy; 114 PendingRequest( 115 boost::beast::http::request<boost::beast::http::string_body>&& reqIn, 116 const std::function<void(bool, uint32_t, Response&)>& callbackIn, 117 const RetryPolicyData& retryPolicyIn) : 118 req(std::move(reqIn)), 119 callback(callbackIn), retryPolicy(retryPolicyIn) 120 {} 121 }; 122 123 class ConnectionInfo : public std::enable_shared_from_this<ConnectionInfo> 124 { 125 private: 126 ConnState state = ConnState::initialized; 127 uint32_t retryCount = 0; 128 std::string subId; 129 std::string host; 130 uint16_t port; 131 uint32_t connId; 132 133 // Retry policy information 134 // This should be updated before each message is sent 135 RetryPolicyData retryPolicy; 136 137 // Data buffers 138 boost::beast::http::request<boost::beast::http::string_body> req; 139 std::optional< 140 boost::beast::http::response_parser<boost::beast::http::string_body>> 141 parser; 142 boost::beast::flat_static_buffer<httpReadBufferSize> buffer; 143 Response res; 144 145 // Ascync callables 146 std::function<void(bool, uint32_t, Response&)> callback; 147 crow::async_resolve::Resolver resolver; 148 boost::asio::ip::tcp::socket conn; 149 std::optional<boost::beast::ssl_stream<boost::asio::ip::tcp::socket&>> 150 sslConn; 151 152 boost::asio::steady_timer timer; 153 154 friend class ConnectionPool; 155 156 void doResolve() 157 { 158 state = ConnState::resolveInProgress; 159 BMCWEB_LOG_DEBUG << "Trying to resolve: " << host << ":" 160 << std::to_string(port) 161 << ", id: " << std::to_string(connId); 162 163 resolver.asyncResolve(host, port, 164 std::bind_front(&ConnectionInfo::afterResolve, 165 this, shared_from_this())); 166 } 167 168 void afterResolve( 169 const std::shared_ptr<ConnectionInfo>& /*self*/, 170 const boost::beast::error_code ec, 171 const std::vector<boost::asio::ip::tcp::endpoint>& endpointList) 172 { 173 if (ec || (endpointList.empty())) 174 { 175 BMCWEB_LOG_ERROR << "Resolve failed: " << ec.message(); 176 state = ConnState::resolveFailed; 177 waitAndRetry(); 178 return; 179 } 180 BMCWEB_LOG_DEBUG << "Resolved " << host << ":" << std::to_string(port) 181 << ", id: " << std::to_string(connId); 182 state = ConnState::connectInProgress; 183 184 BMCWEB_LOG_DEBUG << "Trying to connect to: " << host << ":" 185 << std::to_string(port) 186 << ", id: " << std::to_string(connId); 187 188 timer.expires_after(std::chrono::seconds(30)); 189 timer.async_wait(std::bind_front(onTimeout, weak_from_this())); 190 191 boost::asio::async_connect( 192 conn, endpointList, 193 std::bind_front(&ConnectionInfo::afterConnect, this, 194 shared_from_this())); 195 } 196 197 void afterConnect(const std::shared_ptr<ConnectionInfo>& /*self*/, 198 boost::beast::error_code ec, 199 const boost::asio::ip::tcp::endpoint& endpoint) 200 { 201 // The operation already timed out. We don't want do continue down 202 // this branch 203 if (ec && ec == boost::asio::error::operation_aborted) 204 { 205 return; 206 } 207 208 timer.cancel(); 209 if (ec) 210 { 211 BMCWEB_LOG_ERROR << "Connect " << endpoint.address().to_string() 212 << ":" << std::to_string(endpoint.port()) 213 << ", id: " << std::to_string(connId) 214 << " failed: " << ec.message(); 215 state = ConnState::connectFailed; 216 waitAndRetry(); 217 return; 218 } 219 BMCWEB_LOG_DEBUG << "Connected to: " << endpoint.address().to_string() 220 << ":" << std::to_string(endpoint.port()) 221 << ", id: " << std::to_string(connId); 222 if (sslConn) 223 { 224 doSslHandshake(); 225 return; 226 } 227 state = ConnState::connected; 228 sendMessage(); 229 } 230 231 void doSslHandshake() 232 { 233 if (!sslConn) 234 { 235 return; 236 } 237 state = ConnState::handshakeInProgress; 238 timer.expires_after(std::chrono::seconds(30)); 239 timer.async_wait(std::bind_front(onTimeout, weak_from_this())); 240 sslConn->async_handshake( 241 boost::asio::ssl::stream_base::client, 242 std::bind_front(&ConnectionInfo::afterSslHandshake, this, 243 shared_from_this())); 244 } 245 246 void afterSslHandshake(const std::shared_ptr<ConnectionInfo>& /*self*/, 247 boost::beast::error_code ec) 248 { 249 // The operation already timed out. We don't want do continue down 250 // this branch 251 if (ec && ec == boost::asio::error::operation_aborted) 252 { 253 return; 254 } 255 256 timer.cancel(); 257 if (ec) 258 { 259 BMCWEB_LOG_ERROR << "SSL Handshake failed -" 260 << " id: " << std::to_string(connId) 261 << " error: " << ec.message(); 262 state = ConnState::handshakeFailed; 263 waitAndRetry(); 264 return; 265 } 266 BMCWEB_LOG_DEBUG << "SSL Handshake successful -" 267 << " id: " << std::to_string(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(); 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.emplace(std::piecewise_construct, std::make_tuple()); 326 parser->body_limit(httpReadBodyLimit); 327 328 timer.expires_after(std::chrono::seconds(30)); 329 timer.async_wait(std::bind_front(onTimeout, weak_from_this())); 330 331 // Receive the HTTP response 332 if (sslConn) 333 { 334 boost::beast::http::async_read( 335 *sslConn, buffer, *parser, 336 std::bind_front(&ConnectionInfo::afterRead, this, 337 shared_from_this())); 338 } 339 else 340 { 341 boost::beast::http::async_read( 342 conn, buffer, *parser, 343 std::bind_front(&ConnectionInfo::afterRead, this, 344 shared_from_this())); 345 } 346 } 347 348 void afterRead(const std::shared_ptr<ConnectionInfo>& /*self*/, 349 const boost::beast::error_code& ec, 350 const std::size_t& bytesTransferred) 351 { 352 // The operation already timed out. We don't want do continue down 353 // this branch 354 if (ec && ec == boost::asio::error::operation_aborted) 355 { 356 return; 357 } 358 359 timer.cancel(); 360 if (ec && ec != boost::asio::ssl::error::stream_truncated) 361 { 362 BMCWEB_LOG_ERROR << "recvMessage() failed: " << ec.message(); 363 state = ConnState::recvFailed; 364 waitAndRetry(); 365 return; 366 } 367 BMCWEB_LOG_DEBUG << "recvMessage() bytes transferred: " 368 << bytesTransferred; 369 BMCWEB_LOG_DEBUG << "recvMessage() data: " << parser->get().body(); 370 371 unsigned int respCode = parser->get().result_int(); 372 BMCWEB_LOG_DEBUG << "recvMessage() Header Response Code: " << respCode; 373 374 // Make sure the received response code is valid as defined by 375 // the associated retry policy 376 if (retryPolicy.invalidResp(respCode)) 377 { 378 // The listener failed to receive the Sent-Event 379 BMCWEB_LOG_ERROR << "recvMessage() Listener Failed to " 380 "receive Sent-Event. Header Response Code: " 381 << respCode; 382 state = ConnState::recvFailed; 383 waitAndRetry(); 384 return; 385 } 386 387 // Send is successful 388 // Reset the counter just in case this was after retrying 389 retryCount = 0; 390 391 // Keep the connection alive if server supports it 392 // Else close the connection 393 BMCWEB_LOG_DEBUG << "recvMessage() keepalive : " 394 << parser->keep_alive(); 395 396 // Copy the response into a Response object so that it can be 397 // processed by the callback function. 398 res.stringResponse = parser->release(); 399 callback(parser->keep_alive(), connId, res); 400 res.clear(); 401 } 402 403 static void onTimeout(const std::weak_ptr<ConnectionInfo>& weakSelf, 404 const boost::system::error_code& ec) 405 { 406 if (ec == boost::asio::error::operation_aborted) 407 { 408 BMCWEB_LOG_DEBUG 409 << "async_wait failed since the operation is aborted"; 410 return; 411 } 412 if (ec) 413 { 414 BMCWEB_LOG_ERROR << "async_wait failed: " << ec.message(); 415 // If the timer fails, we need to close the socket anyway, same as 416 // if it expired. 417 } 418 std::shared_ptr<ConnectionInfo> self = weakSelf.lock(); 419 if (self == nullptr) 420 { 421 return; 422 } 423 self->waitAndRetry(); 424 } 425 426 void waitAndRetry() 427 { 428 if ((retryCount >= retryPolicy.maxRetryAttempts) || 429 (state == ConnState::sslInitFailed)) 430 { 431 BMCWEB_LOG_ERROR << "Maximum number of retries reached."; 432 BMCWEB_LOG_DEBUG << "Retry policy: " 433 << retryPolicy.retryPolicyAction; 434 435 if (retryPolicy.retryPolicyAction == "TerminateAfterRetries") 436 { 437 // TODO: delete subscription 438 state = ConnState::terminated; 439 } 440 if (retryPolicy.retryPolicyAction == "SuspendRetries") 441 { 442 state = ConnState::suspended; 443 } 444 445 // We want to return a 502 to indicate there was an error with 446 // the external server 447 res.result(boost::beast::http::status::bad_gateway); 448 callback(false, connId, res); 449 res.clear(); 450 451 // Reset the retrycount to zero so that client can try connecting 452 // again if needed 453 retryCount = 0; 454 return; 455 } 456 457 retryCount++; 458 459 BMCWEB_LOG_DEBUG << "Attempt retry after " 460 << std::to_string( 461 retryPolicy.retryIntervalSecs.count()) 462 << " seconds. RetryCount = " << retryCount; 463 timer.expires_after(retryPolicy.retryIntervalSecs); 464 timer.async_wait(std::bind_front(&ConnectionInfo::onTimerDone, this, 465 shared_from_this())); 466 } 467 468 void onTimerDone(const std::shared_ptr<ConnectionInfo>& /*self*/, 469 const boost::system::error_code& ec) 470 { 471 if (ec == boost::asio::error::operation_aborted) 472 { 473 BMCWEB_LOG_DEBUG 474 << "async_wait failed since the operation is aborted" 475 << ec.message(); 476 } 477 else if (ec) 478 { 479 BMCWEB_LOG_ERROR << "async_wait failed: " << ec.message(); 480 // Ignore the error and continue the retry loop to attempt 481 // sending the event as per the retry policy 482 } 483 484 // Let's close the connection and restart from resolve. 485 doClose(true); 486 } 487 488 void shutdownConn(bool retry) 489 { 490 boost::beast::error_code ec; 491 conn.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); 492 conn.close(); 493 494 // not_connected happens sometimes so don't bother reporting it. 495 if (ec && ec != boost::beast::errc::not_connected) 496 { 497 BMCWEB_LOG_ERROR << host << ":" << std::to_string(port) 498 << ", id: " << std::to_string(connId) 499 << " shutdown failed: " << ec.message(); 500 } 501 else 502 { 503 BMCWEB_LOG_DEBUG << host << ":" << std::to_string(port) 504 << ", id: " << std::to_string(connId) 505 << " closed gracefully"; 506 } 507 508 if (retry) 509 { 510 // Now let's try to resend the data 511 state = ConnState::retry; 512 doResolve(); 513 } 514 else 515 { 516 state = ConnState::closed; 517 } 518 } 519 520 void doClose(bool retry = false) 521 { 522 if (!sslConn) 523 { 524 shutdownConn(retry); 525 return; 526 } 527 528 sslConn->async_shutdown( 529 std::bind_front(&ConnectionInfo::afterSslShutdown, this, 530 shared_from_this(), retry)); 531 } 532 533 void afterSslShutdown(const std::shared_ptr<ConnectionInfo>& /*self*/, 534 bool retry, const boost::system::error_code& ec) 535 { 536 537 if (ec) 538 { 539 BMCWEB_LOG_ERROR << host << ":" << std::to_string(port) 540 << ", id: " << std::to_string(connId) 541 << " shutdown failed: " << ec.message(); 542 } 543 else 544 { 545 BMCWEB_LOG_DEBUG << host << ":" << std::to_string(port) 546 << ", id: " << std::to_string(connId) 547 << " closed gracefully"; 548 } 549 shutdownConn(retry); 550 } 551 552 void setCipherSuiteTLSext() 553 { 554 if (!sslConn) 555 { 556 return; 557 } 558 // NOTE: The SSL_set_tlsext_host_name is defined in tlsv1.h header 559 // file but its having old style casting (name is cast to void*). 560 // Since bmcweb compiler treats all old-style-cast as error, its 561 // causing the build failure. So replaced the same macro inline and 562 // did corrected the code by doing static_cast to viod*. This has to 563 // be fixed in openssl library in long run. Set SNI Hostname (many 564 // hosts need this to handshake successfully) 565 if (SSL_ctrl(sslConn->native_handle(), SSL_CTRL_SET_TLSEXT_HOSTNAME, 566 TLSEXT_NAMETYPE_host_name, 567 static_cast<void*>(&host.front())) == 0) 568 569 { 570 boost::beast::error_code ec{static_cast<int>(::ERR_get_error()), 571 boost::asio::error::get_ssl_category()}; 572 573 BMCWEB_LOG_ERROR << "SSL_set_tlsext_host_name " << host << ":" 574 << port << ", id: " << std::to_string(connId) 575 << " failed: " << ec.message(); 576 // Set state as sslInit failed so that we close the connection 577 // and take appropriate action as per retry configuration. 578 state = ConnState::sslInitFailed; 579 waitAndRetry(); 580 return; 581 } 582 } 583 584 public: 585 explicit ConnectionInfo(boost::asio::io_context& iocIn, 586 const std::string& idIn, 587 const std::string& destIPIn, uint16_t destPortIn, 588 bool useSSL, unsigned int connIdIn) : 589 subId(idIn), 590 host(destIPIn), port(destPortIn), connId(connIdIn), conn(iocIn), 591 timer(iocIn) 592 { 593 if (useSSL) 594 { 595 std::optional<boost::asio::ssl::context> sslCtx = 596 ensuressl::getSSLClientContext(); 597 598 if (!sslCtx) 599 { 600 BMCWEB_LOG_ERROR << "prepareSSLContext failed - " << host << ":" 601 << port << ", id: " << std::to_string(connId); 602 // Don't retry if failure occurs while preparing SSL context 603 // such as certificate is invalid or set cipher failure or set 604 // host name failure etc... Setting conn state to sslInitFailed 605 // and connection state will be transitioned to next state 606 // depending on retry policy set by subscription. 607 state = ConnState::sslInitFailed; 608 waitAndRetry(); 609 return; 610 } 611 sslConn.emplace(conn, *sslCtx); 612 setCipherSuiteTLSext(); 613 } 614 } 615 }; 616 617 class ConnectionPool : public std::enable_shared_from_this<ConnectionPool> 618 { 619 private: 620 boost::asio::io_context& ioc; 621 std::string id; 622 std::string destIP; 623 uint16_t destPort; 624 bool useSSL; 625 std::vector<std::shared_ptr<ConnectionInfo>> connections; 626 boost::container::devector<PendingRequest> requestQueue; 627 628 friend class HttpClient; 629 630 // Configure a connections's request, callback, and retry info in 631 // preparation to begin sending the request 632 void setConnProps(ConnectionInfo& conn) 633 { 634 if (requestQueue.empty()) 635 { 636 BMCWEB_LOG_ERROR 637 << "setConnProps() should not have been called when requestQueue is empty"; 638 return; 639 } 640 641 auto nextReq = requestQueue.front(); 642 conn.retryPolicy = std::move(nextReq.retryPolicy); 643 conn.req = std::move(nextReq.req); 644 conn.callback = std::move(nextReq.callback); 645 646 BMCWEB_LOG_DEBUG << "Setting properties for connection " << conn.host 647 << ":" << std::to_string(conn.port) 648 << ", id: " << std::to_string(conn.connId); 649 650 // We can remove the request from the queue at this point 651 requestQueue.pop_front(); 652 } 653 654 // Configures a connection to use the specific retry policy. 655 inline void setConnRetryPolicy(ConnectionInfo& conn, 656 const RetryPolicyData& retryPolicy) 657 { 658 BMCWEB_LOG_DEBUG << destIP << ":" << std::to_string(destPort) 659 << ", id: " << std::to_string(conn.connId); 660 661 conn.retryPolicy = retryPolicy; 662 } 663 664 // Gets called as part of callback after request is sent 665 // Reuses the connection if there are any requests waiting to be sent 666 // Otherwise closes the connection if it is not a keep-alive 667 void sendNext(bool keepAlive, uint32_t connId) 668 { 669 auto conn = connections[connId]; 670 671 // Allow the connection's handler to be deleted 672 // This is needed because of Redfish Aggregation passing an 673 // AsyncResponse shared_ptr to this callback 674 conn->callback = nullptr; 675 676 // Reuse the connection to send the next request in the queue 677 if (!requestQueue.empty()) 678 { 679 BMCWEB_LOG_DEBUG << std::to_string(requestQueue.size()) 680 << " requests remaining in queue for " << destIP 681 << ":" << std::to_string(destPort) 682 << ", reusing connnection " 683 << 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 RetryPolicyData& retryPolicy, 718 const std::function<void(Response&)>& resHandler) 719 { 720 // Construct the request to be sent 721 boost::beast::http::request<boost::beast::http::string_body> thisReq( 722 verb, destUri, 11, "", httpHeader); 723 thisReq.set(boost::beast::http::field::host, destIP); 724 thisReq.keep_alive(true); 725 thisReq.body() = std::move(data); 726 thisReq.prepare_payload(); 727 auto cb = std::bind_front(&ConnectionPool::afterSendData, 728 weak_from_this(), resHandler); 729 // Reuse an existing connection if one is available 730 for (unsigned int i = 0; i < connections.size(); i++) 731 { 732 auto conn = connections[i]; 733 if ((conn->state == ConnState::idle) || 734 (conn->state == ConnState::initialized) || 735 (conn->state == ConnState::closed)) 736 { 737 conn->req = std::move(thisReq); 738 conn->callback = std::move(cb); 739 setConnRetryPolicy(*conn, retryPolicy); 740 std::string commonMsg = std::to_string(i) + " from pool " + 741 destIP + ":" + std::to_string(destPort); 742 743 if (conn->state == ConnState::idle) 744 { 745 BMCWEB_LOG_DEBUG << "Grabbing idle connection " 746 << commonMsg; 747 conn->sendMessage(); 748 } 749 else 750 { 751 BMCWEB_LOG_DEBUG << "Reusing existing connection " 752 << commonMsg; 753 conn->doResolve(); 754 } 755 return; 756 } 757 } 758 759 // All connections in use so create a new connection or add request to 760 // the queue 761 if (connections.size() < maxPoolSize) 762 { 763 BMCWEB_LOG_DEBUG << "Adding new connection to pool " << destIP 764 << ":" << std::to_string(destPort); 765 auto conn = addConnection(); 766 conn->req = std::move(thisReq); 767 conn->callback = std::move(cb); 768 setConnRetryPolicy(*conn, retryPolicy); 769 conn->doResolve(); 770 } 771 else if (requestQueue.size() < maxRequestQueueSize) 772 { 773 BMCWEB_LOG_ERROR << "Max pool size reached. Adding data to queue."; 774 requestQueue.emplace_back(std::move(thisReq), std::move(cb), 775 retryPolicy); 776 } 777 else 778 { 779 // If we can't buffer the request then we should let the callback 780 // handle a 429 Too Many Requests dummy response 781 BMCWEB_LOG_ERROR << destIP << ":" << std::to_string(destPort) 782 << " request queue full. Dropping request."; 783 Response dummyRes; 784 dummyRes.result(boost::beast::http::status::too_many_requests); 785 resHandler(dummyRes); 786 } 787 } 788 789 // Callback to be called once the request has been sent 790 static void afterSendData(const std::weak_ptr<ConnectionPool>& weakSelf, 791 const std::function<void(Response&)>& resHandler, 792 bool keepAlive, uint32_t connId, Response& res) 793 { 794 // Allow provided callback to perform additional processing of the 795 // request 796 resHandler(res); 797 798 // If requests remain in the queue then we want to reuse this 799 // connection to send the next request 800 std::shared_ptr<ConnectionPool> self = weakSelf.lock(); 801 if (!self) 802 { 803 BMCWEB_LOG_CRITICAL << self << " Failed to capture connection"; 804 return; 805 } 806 807 self->sendNext(keepAlive, connId); 808 } 809 810 std::shared_ptr<ConnectionInfo>& addConnection() 811 { 812 unsigned int newId = static_cast<unsigned int>(connections.size()); 813 814 auto& ret = connections.emplace_back(std::make_shared<ConnectionInfo>( 815 ioc, id, destIP, destPort, useSSL, newId)); 816 817 BMCWEB_LOG_DEBUG << "Added connection " 818 << std::to_string(connections.size() - 1) 819 << " to pool " << destIP << ":" 820 << std::to_string(destPort); 821 822 return ret; 823 } 824 825 public: 826 explicit ConnectionPool(boost::asio::io_context& iocIn, 827 const std::string& idIn, 828 const std::string& destIPIn, uint16_t destPortIn, 829 bool useSSLIn) : 830 ioc(iocIn), 831 id(idIn), destIP(destIPIn), destPort(destPortIn), useSSL(useSSLIn) 832 { 833 BMCWEB_LOG_DEBUG << "Initializing connection pool for " << destIP << ":" 834 << std::to_string(destPort); 835 836 // Initialize the pool with a single connection 837 addConnection(); 838 } 839 }; 840 841 class HttpClient 842 { 843 private: 844 std::unordered_map<std::string, std::shared_ptr<ConnectionPool>> 845 connectionPools; 846 boost::asio::io_context& ioc = 847 crow::connections::systemBus->get_io_context(); 848 std::unordered_map<std::string, RetryPolicyData> retryInfo; 849 HttpClient() = default; 850 851 // Used as a dummy callback by sendData() in order to call 852 // sendDataWithCallback() 853 static void genericResHandler(const Response& res) 854 { 855 BMCWEB_LOG_DEBUG << "Response handled with return code: " 856 << std::to_string(res.resultInt()); 857 } 858 859 public: 860 HttpClient(const HttpClient&) = delete; 861 HttpClient& operator=(const HttpClient&) = delete; 862 HttpClient(HttpClient&&) = delete; 863 HttpClient& operator=(HttpClient&&) = delete; 864 ~HttpClient() = default; 865 866 static HttpClient& getInstance() 867 { 868 static HttpClient handler; 869 return handler; 870 } 871 872 // Send a request to destIP:destPort where additional processing of the 873 // result is not required 874 void sendData(std::string& data, const std::string& id, 875 const std::string& destIP, uint16_t destPort, 876 const std::string& destUri, bool useSSL, 877 const boost::beast::http::fields& httpHeader, 878 const boost::beast::http::verb verb, 879 const std::string& retryPolicyName) 880 { 881 const std::function<void(Response&)> cb = genericResHandler; 882 sendDataWithCallback(data, id, destIP, destPort, destUri, useSSL, 883 httpHeader, verb, retryPolicyName, cb); 884 } 885 886 // Send request to destIP:destPort and use the provided callback to 887 // handle the response 888 void sendDataWithCallback(std::string& data, const std::string& id, 889 const std::string& destIP, uint16_t destPort, 890 const std::string& destUri, bool useSSL, 891 const boost::beast::http::fields& httpHeader, 892 const boost::beast::http::verb verb, 893 const std::string& retryPolicyName, 894 const std::function<void(Response&)>& resHandler) 895 { 896 std::string clientKey = useSSL ? "https" : "http"; 897 clientKey += destIP; 898 clientKey += ":"; 899 clientKey += std::to_string(destPort); 900 // Use nullptr to avoid creating a ConnectionPool each time 901 std::shared_ptr<ConnectionPool>& conn = connectionPools[clientKey]; 902 if (conn == nullptr) 903 { 904 // Now actually create the ConnectionPool shared_ptr since it does 905 // not already exist 906 conn = std::make_shared<ConnectionPool>(ioc, id, destIP, destPort, 907 useSSL); 908 BMCWEB_LOG_DEBUG << "Created connection pool for " << clientKey; 909 } 910 else 911 { 912 BMCWEB_LOG_DEBUG << "Using existing connection pool for " 913 << clientKey; 914 } 915 916 // Get the associated retry policy 917 auto policy = retryInfo.try_emplace(retryPolicyName); 918 if (policy.second) 919 { 920 BMCWEB_LOG_DEBUG << "Creating retry policy \"" << retryPolicyName 921 << "\" with default values"; 922 } 923 924 // Send the data using either the existing connection pool or the newly 925 // created connection pool 926 conn->sendData(data, destUri, httpHeader, verb, policy.first->second, 927 resHandler); 928 } 929 930 void setRetryConfig( 931 const uint32_t retryAttempts, const uint32_t retryTimeoutInterval, 932 const std::function<boost::system::error_code(unsigned int respCode)>& 933 invalidResp, 934 const std::string& retryPolicyName) 935 { 936 // We need to create the retry policy if one does not already exist for 937 // the given retryPolicyName 938 auto result = retryInfo.try_emplace(retryPolicyName); 939 if (result.second) 940 { 941 BMCWEB_LOG_DEBUG << "setRetryConfig(): Creating new retry policy \"" 942 << retryPolicyName << "\""; 943 } 944 else 945 { 946 BMCWEB_LOG_DEBUG << "setRetryConfig(): Updating retry info for \"" 947 << retryPolicyName << "\""; 948 } 949 950 result.first->second.maxRetryAttempts = retryAttempts; 951 result.first->second.retryIntervalSecs = 952 std::chrono::seconds(retryTimeoutInterval); 953 result.first->second.invalidResp = invalidResp; 954 } 955 956 void setRetryPolicy(const std::string& retryPolicy, 957 const std::string& retryPolicyName) 958 { 959 // We need to create the retry policy if one does not already exist for 960 // the given retryPolicyName 961 auto result = retryInfo.try_emplace(retryPolicyName); 962 if (result.second) 963 { 964 BMCWEB_LOG_DEBUG << "setRetryPolicy(): Creating new retry policy \"" 965 << retryPolicyName << "\""; 966 } 967 else 968 { 969 BMCWEB_LOG_DEBUG << "setRetryPolicy(): Updating retry policy for \"" 970 << retryPolicyName << "\""; 971 } 972 973 result.first->second.retryPolicyAction = retryPolicy; 974 } 975 }; 976 } // namespace crow 977