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