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