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 resolver.asyncResolve(host, port, 161 std::bind_front(&ConnectionInfo::afterResolve, 162 this, shared_from_this())); 163 } 164 165 void afterResolve( 166 const std::shared_ptr<ConnectionInfo>& /*self*/, 167 const boost::beast::error_code ec, 168 const std::vector<boost::asio::ip::tcp::endpoint>& endpointList) 169 { 170 if (ec || (endpointList.empty())) 171 { 172 BMCWEB_LOG_ERROR << "Resolve failed: " << ec.message(); 173 state = ConnState::resolveFailed; 174 waitAndRetry(); 175 return; 176 } 177 BMCWEB_LOG_DEBUG << "Resolved " << host << ":" << std::to_string(port) 178 << ", id: " << std::to_string(connId); 179 state = ConnState::connectInProgress; 180 181 BMCWEB_LOG_DEBUG << "Trying to connect to: " << host << ":" 182 << std::to_string(port) 183 << ", id: " << std::to_string(connId); 184 185 timer.expires_after(std::chrono::seconds(30)); 186 timer.async_wait(std::bind_front(onTimeout, weak_from_this())); 187 188 boost::asio::async_connect( 189 conn, endpointList, 190 std::bind_front(&ConnectionInfo::afterConnect, this, 191 shared_from_this())); 192 } 193 194 void afterConnect(const std::shared_ptr<ConnectionInfo>& /*self*/, 195 boost::beast::error_code ec, 196 const boost::asio::ip::tcp::endpoint& endpoint) 197 { 198 timer.cancel(); 199 if (ec) 200 { 201 BMCWEB_LOG_ERROR << "Connect " << endpoint.address().to_string() 202 << ":" << std::to_string(endpoint.port()) 203 << ", id: " << std::to_string(connId) 204 << " failed: " << ec.message(); 205 state = ConnState::connectFailed; 206 waitAndRetry(); 207 return; 208 } 209 BMCWEB_LOG_DEBUG << "Connected to: " << endpoint.address().to_string() 210 << ":" << std::to_string(endpoint.port()) 211 << ", id: " << std::to_string(connId); 212 if (sslConn) 213 { 214 doSslHandshake(); 215 return; 216 } 217 state = ConnState::connected; 218 sendMessage(); 219 } 220 221 void doSslHandshake() 222 { 223 if (!sslConn) 224 { 225 return; 226 } 227 state = ConnState::handshakeInProgress; 228 timer.expires_after(std::chrono::seconds(30)); 229 timer.async_wait(std::bind_front(onTimeout, weak_from_this())); 230 sslConn->async_handshake( 231 boost::asio::ssl::stream_base::client, 232 std::bind_front(&ConnectionInfo::afterSslHandshake, this, 233 shared_from_this())); 234 } 235 236 void afterSslHandshake(const std::shared_ptr<ConnectionInfo>& /*self*/, 237 boost::beast::error_code ec) 238 { 239 timer.cancel(); 240 if (ec) 241 { 242 BMCWEB_LOG_ERROR << "SSL Handshake failed -" 243 << " id: " << std::to_string(connId) 244 << " error: " << ec.message(); 245 state = ConnState::handshakeFailed; 246 waitAndRetry(); 247 return; 248 } 249 BMCWEB_LOG_DEBUG << "SSL Handshake successful -" 250 << " id: " << std::to_string(connId); 251 state = ConnState::connected; 252 sendMessage(); 253 } 254 255 void sendMessage() 256 { 257 state = ConnState::sendInProgress; 258 259 // Set a timeout on the operation 260 timer.expires_after(std::chrono::seconds(30)); 261 timer.async_wait(std::bind_front(onTimeout, weak_from_this())); 262 263 // Send the HTTP request to the remote host 264 if (sslConn) 265 { 266 boost::beast::http::async_write( 267 *sslConn, req, 268 std::bind_front(&ConnectionInfo::afterWrite, this, 269 shared_from_this())); 270 } 271 else 272 { 273 boost::beast::http::async_write( 274 conn, req, 275 std::bind_front(&ConnectionInfo::afterWrite, this, 276 shared_from_this())); 277 } 278 } 279 280 void afterWrite(const std::shared_ptr<ConnectionInfo>& /*self*/, 281 const boost::beast::error_code& ec, size_t bytesTransferred) 282 { 283 timer.cancel(); 284 if (ec) 285 { 286 BMCWEB_LOG_ERROR << "sendMessage() failed: " << ec.message(); 287 state = ConnState::sendFailed; 288 waitAndRetry(); 289 return; 290 } 291 BMCWEB_LOG_DEBUG << "sendMessage() bytes transferred: " 292 << bytesTransferred; 293 294 recvMessage(); 295 } 296 297 void recvMessage() 298 { 299 state = ConnState::recvInProgress; 300 301 parser.emplace(std::piecewise_construct, std::make_tuple()); 302 parser->body_limit(httpReadBodyLimit); 303 304 timer.expires_after(std::chrono::seconds(30)); 305 timer.async_wait(std::bind_front(onTimeout, weak_from_this())); 306 307 // Receive the HTTP response 308 if (sslConn) 309 { 310 boost::beast::http::async_read( 311 *sslConn, buffer, *parser, 312 std::bind_front(&ConnectionInfo::afterRead, this, 313 shared_from_this())); 314 } 315 else 316 { 317 boost::beast::http::async_read( 318 conn, buffer, *parser, 319 std::bind_front(&ConnectionInfo::afterRead, this, 320 shared_from_this())); 321 } 322 } 323 324 void afterRead(const std::shared_ptr<ConnectionInfo>& /*self*/, 325 const boost::beast::error_code& ec, 326 const std::size_t& bytesTransferred) 327 { 328 timer.cancel(); 329 if (ec && ec != boost::asio::ssl::error::stream_truncated) 330 { 331 BMCWEB_LOG_ERROR << "recvMessage() failed: " << ec.message(); 332 state = ConnState::recvFailed; 333 waitAndRetry(); 334 return; 335 } 336 BMCWEB_LOG_DEBUG << "recvMessage() bytes transferred: " 337 << bytesTransferred; 338 BMCWEB_LOG_DEBUG << "recvMessage() data: " << parser->get().body(); 339 340 unsigned int respCode = parser->get().result_int(); 341 BMCWEB_LOG_DEBUG << "recvMessage() Header Response Code: " << respCode; 342 343 // Make sure the received response code is valid as defined by 344 // the associated retry policy 345 if (retryPolicy.invalidResp(respCode)) 346 { 347 // The listener failed to receive the Sent-Event 348 BMCWEB_LOG_ERROR << "recvMessage() Listener Failed to " 349 "receive Sent-Event. Header Response Code: " 350 << respCode; 351 state = ConnState::recvFailed; 352 waitAndRetry(); 353 return; 354 } 355 356 // Send is successful 357 // Reset the counter just in case this was after retrying 358 retryCount = 0; 359 360 // Keep the connection alive if server supports it 361 // Else close the connection 362 BMCWEB_LOG_DEBUG << "recvMessage() keepalive : " 363 << parser->keep_alive(); 364 365 // Copy the response into a Response object so that it can be 366 // processed by the callback function. 367 res.clear(); 368 res.stringResponse = parser->release(); 369 callback(parser->keep_alive(), connId, res); 370 } 371 372 static void onTimeout(const std::weak_ptr<ConnectionInfo>& weakSelf, 373 const boost::system::error_code ec) 374 { 375 if (ec == boost::asio::error::operation_aborted) 376 { 377 BMCWEB_LOG_DEBUG 378 << "async_wait failed since the operation is aborted" 379 << ec.message(); 380 return; 381 } 382 if (ec) 383 { 384 BMCWEB_LOG_ERROR << "async_wait failed: " << ec.message(); 385 // If the timer fails, we need to close the socket anyway, same as 386 // if it expired. 387 } 388 std::shared_ptr<ConnectionInfo> self = weakSelf.lock(); 389 if (self == nullptr) 390 { 391 return; 392 } 393 self->waitAndRetry(); 394 } 395 396 void waitAndRetry() 397 { 398 if ((retryCount >= retryPolicy.maxRetryAttempts) || 399 (state == ConnState::sslInitFailed)) 400 { 401 BMCWEB_LOG_ERROR << "Maximum number of retries reached."; 402 BMCWEB_LOG_DEBUG << "Retry policy: " 403 << retryPolicy.retryPolicyAction; 404 405 // We want to return a 502 to indicate there was an error with the 406 // external server 407 res.clear(); 408 res.result(boost::beast::http::status::bad_gateway); 409 410 if (retryPolicy.retryPolicyAction == "TerminateAfterRetries") 411 { 412 // TODO: delete subscription 413 state = ConnState::terminated; 414 callback(false, connId, res); 415 } 416 if (retryPolicy.retryPolicyAction == "SuspendRetries") 417 { 418 state = ConnState::suspended; 419 callback(false, connId, res); 420 } 421 // Reset the retrycount to zero so that client can try connecting 422 // again if needed 423 retryCount = 0; 424 return; 425 } 426 427 retryCount++; 428 429 BMCWEB_LOG_DEBUG << "Attempt retry after " 430 << std::to_string( 431 retryPolicy.retryIntervalSecs.count()) 432 << " seconds. RetryCount = " << retryCount; 433 timer.expires_after(retryPolicy.retryIntervalSecs); 434 timer.async_wait(std::bind_front(&ConnectionInfo::onTimerDone, this, 435 shared_from_this())); 436 } 437 438 void onTimerDone(const std::shared_ptr<ConnectionInfo>& /*self*/, 439 const boost::system::error_code& ec) 440 { 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 doClose(true); 456 } 457 458 void shutdownConn(bool retry) 459 { 460 boost::beast::error_code ec; 461 conn.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); 462 conn.close(); 463 464 // not_connected happens sometimes so don't bother reporting it. 465 if (ec && ec != boost::beast::errc::not_connected) 466 { 467 BMCWEB_LOG_ERROR << host << ":" << std::to_string(port) 468 << ", id: " << std::to_string(connId) 469 << "shutdown failed: " << ec.message(); 470 } 471 else 472 { 473 BMCWEB_LOG_DEBUG << host << ":" << std::to_string(port) 474 << ", id: " << std::to_string(connId) 475 << " closed gracefully"; 476 } 477 478 if ((state != ConnState::suspended) && (state != ConnState::terminated)) 479 { 480 if (retry) 481 { 482 // Now let's try to resend the data 483 state = ConnState::retry; 484 doResolve(); 485 } 486 else 487 { 488 state = ConnState::closed; 489 } 490 } 491 } 492 493 void doClose(bool retry = false) 494 { 495 if (!sslConn) 496 { 497 shutdownConn(retry); 498 return; 499 } 500 501 sslConn->async_shutdown( 502 std::bind_front(&ConnectionInfo::afterSslShutdown, this, 503 shared_from_this(), retry)); 504 } 505 506 void afterSslShutdown(const std::shared_ptr<ConnectionInfo>& /*self*/, 507 bool retry, const boost::system::error_code& ec) 508 { 509 510 if (ec) 511 { 512 BMCWEB_LOG_ERROR << host << ":" << std::to_string(port) 513 << ", id: " << std::to_string(connId) 514 << " shutdown failed: " << ec.message(); 515 } 516 else 517 { 518 BMCWEB_LOG_DEBUG << host << ":" << std::to_string(port) 519 << ", id: " << std::to_string(connId) 520 << " closed gracefully"; 521 } 522 shutdownConn(retry); 523 } 524 525 void setCipherSuiteTLSext() 526 { 527 if (!sslConn) 528 { 529 return; 530 } 531 // NOTE: The SSL_set_tlsext_host_name is defined in tlsv1.h header 532 // file but its having old style casting (name is cast to void*). 533 // Since bmcweb compiler treats all old-style-cast as error, its 534 // causing the build failure. So replaced the same macro inline and 535 // did corrected the code by doing static_cast to viod*. This has to 536 // be fixed in openssl library in long run. Set SNI Hostname (many 537 // hosts need this to handshake successfully) 538 if (SSL_ctrl(sslConn->native_handle(), SSL_CTRL_SET_TLSEXT_HOSTNAME, 539 TLSEXT_NAMETYPE_host_name, 540 static_cast<void*>(&host.front())) == 0) 541 542 { 543 boost::beast::error_code ec{static_cast<int>(::ERR_get_error()), 544 boost::asio::error::get_ssl_category()}; 545 546 BMCWEB_LOG_ERROR << "SSL_set_tlsext_host_name " << host << ":" 547 << port << ", id: " << std::to_string(connId) 548 << " failed: " << ec.message(); 549 // Set state as sslInit failed so that we close the connection 550 // and take appropriate action as per retry configuration. 551 state = ConnState::sslInitFailed; 552 waitAndRetry(); 553 return; 554 } 555 } 556 557 public: 558 explicit ConnectionInfo(boost::asio::io_context& iocIn, 559 const std::string& idIn, 560 const std::string& destIPIn, uint16_t destPortIn, 561 bool useSSL, unsigned int connIdIn) : 562 subId(idIn), 563 host(destIPIn), port(destPortIn), connId(connIdIn), conn(iocIn), 564 timer(iocIn) 565 { 566 if (useSSL) 567 { 568 std::optional<boost::asio::ssl::context> sslCtx = 569 ensuressl::getSSLClientContext(); 570 571 if (!sslCtx) 572 { 573 BMCWEB_LOG_ERROR << "prepareSSLContext failed - " << host << ":" 574 << port << ", id: " << std::to_string(connId); 575 // Don't retry if failure occurs while preparing SSL context 576 // such as certificate is invalid or set cipher failure or set 577 // host name failure etc... Setting conn state to sslInitFailed 578 // and connection state will be transitioned to next state 579 // depending on retry policy set by subscription. 580 state = ConnState::sslInitFailed; 581 waitAndRetry(); 582 return; 583 } 584 sslConn.emplace(conn, *sslCtx); 585 setCipherSuiteTLSext(); 586 } 587 } 588 }; 589 590 class ConnectionPool : public std::enable_shared_from_this<ConnectionPool> 591 { 592 private: 593 boost::asio::io_context& ioc; 594 std::string id; 595 std::string destIP; 596 uint16_t destPort; 597 bool useSSL; 598 std::vector<std::shared_ptr<ConnectionInfo>> connections; 599 boost::container::devector<PendingRequest> requestQueue; 600 601 friend class HttpClient; 602 603 // Configure a connections's request, callback, and retry info in 604 // preparation to begin sending the request 605 void setConnProps(ConnectionInfo& conn) 606 { 607 if (requestQueue.empty()) 608 { 609 BMCWEB_LOG_ERROR 610 << "setConnProps() should not have been called when requestQueue is empty"; 611 return; 612 } 613 614 auto nextReq = requestQueue.front(); 615 conn.retryPolicy = std::move(nextReq.retryPolicy); 616 conn.req = std::move(nextReq.req); 617 conn.callback = std::move(nextReq.callback); 618 619 BMCWEB_LOG_DEBUG << "Setting properties for connection " << conn.host 620 << ":" << std::to_string(conn.port) 621 << ", id: " << std::to_string(conn.connId); 622 623 // We can remove the request from the queue at this point 624 requestQueue.pop_front(); 625 } 626 627 // Configures a connection to use the specific retry policy. 628 inline void setConnRetryPolicy(ConnectionInfo& conn, 629 const RetryPolicyData& retryPolicy) 630 { 631 BMCWEB_LOG_DEBUG << destIP << ":" << std::to_string(destPort) 632 << ", id: " << std::to_string(conn.connId); 633 634 conn.retryPolicy = retryPolicy; 635 } 636 637 // Gets called as part of callback after request is sent 638 // Reuses the connection if there are any requests waiting to be sent 639 // Otherwise closes the connection if it is not a keep-alive 640 void sendNext(bool keepAlive, uint32_t connId) 641 { 642 auto conn = connections[connId]; 643 644 // Allow the connection's handler to be deleted 645 // This is needed because of Redfish Aggregation passing an 646 // AsyncResponse shared_ptr to this callback 647 conn->callback = nullptr; 648 649 // Reuse the connection to send the next request in the queue 650 if (!requestQueue.empty()) 651 { 652 BMCWEB_LOG_DEBUG << std::to_string(requestQueue.size()) 653 << " requests remaining in queue for " << destIP 654 << ":" << std::to_string(destPort) 655 << ", reusing connnection " 656 << std::to_string(connId); 657 658 setConnProps(*conn); 659 660 if (keepAlive) 661 { 662 conn->sendMessage(); 663 } 664 else 665 { 666 // Server is not keep-alive enabled so we need to close the 667 // connection and then start over from resolve 668 conn->doClose(); 669 conn->doResolve(); 670 } 671 return; 672 } 673 674 // No more messages to send so close the connection if necessary 675 if (keepAlive) 676 { 677 conn->state = ConnState::idle; 678 } 679 else 680 { 681 // Abort the connection since server is not keep-alive enabled 682 conn->state = ConnState::abortConnection; 683 conn->doClose(); 684 } 685 } 686 687 void sendData(std::string& data, const std::string& destUri, 688 const boost::beast::http::fields& httpHeader, 689 const boost::beast::http::verb verb, 690 const RetryPolicyData& retryPolicy, 691 const std::function<void(Response&)>& resHandler) 692 { 693 // Construct the request to be sent 694 boost::beast::http::request<boost::beast::http::string_body> thisReq( 695 verb, destUri, 11, "", httpHeader); 696 thisReq.set(boost::beast::http::field::host, destIP); 697 thisReq.keep_alive(true); 698 thisReq.body() = std::move(data); 699 thisReq.prepare_payload(); 700 auto cb = std::bind_front(&ConnectionPool::afterSendData, 701 weak_from_this(), resHandler); 702 // Reuse an existing connection if one is available 703 for (unsigned int i = 0; i < connections.size(); i++) 704 { 705 auto conn = connections[i]; 706 if ((conn->state == ConnState::idle) || 707 (conn->state == ConnState::initialized) || 708 (conn->state == ConnState::closed)) 709 { 710 conn->req = std::move(thisReq); 711 conn->callback = std::move(cb); 712 setConnRetryPolicy(*conn, retryPolicy); 713 std::string commonMsg = std::to_string(i) + " from pool " + 714 destIP + ":" + std::to_string(destPort); 715 716 if (conn->state == ConnState::idle) 717 { 718 BMCWEB_LOG_DEBUG << "Grabbing idle connection " 719 << commonMsg; 720 conn->sendMessage(); 721 } 722 else 723 { 724 BMCWEB_LOG_DEBUG << "Reusing existing connection " 725 << commonMsg; 726 conn->doResolve(); 727 } 728 return; 729 } 730 } 731 732 // All connections in use so create a new connection or add request to 733 // the queue 734 if (connections.size() < maxPoolSize) 735 { 736 BMCWEB_LOG_DEBUG << "Adding new connection to pool " << destIP 737 << ":" << std::to_string(destPort); 738 auto conn = addConnection(); 739 conn->req = std::move(thisReq); 740 conn->callback = std::move(cb); 741 setConnRetryPolicy(*conn, retryPolicy); 742 conn->doResolve(); 743 } 744 else if (requestQueue.size() < maxRequestQueueSize) 745 { 746 BMCWEB_LOG_ERROR << "Max pool size reached. Adding data to queue."; 747 requestQueue.emplace_back(std::move(thisReq), std::move(cb), 748 retryPolicy); 749 } 750 else 751 { 752 BMCWEB_LOG_ERROR << destIP << ":" << std::to_string(destPort) 753 << " request queue full. Dropping request."; 754 } 755 } 756 757 // Callback to be called once the request has been sent 758 static void afterSendData(const std::weak_ptr<ConnectionPool>& weakSelf, 759 const std::function<void(Response&)>& resHandler, 760 bool keepAlive, uint32_t connId, Response& res) 761 { 762 // Allow provided callback to perform additional processing of the 763 // request 764 resHandler(res); 765 766 // If requests remain in the queue then we want to reuse this 767 // connection to send the next request 768 std::shared_ptr<ConnectionPool> self = weakSelf.lock(); 769 if (!self) 770 { 771 BMCWEB_LOG_CRITICAL << self << " Failed to capture connection"; 772 return; 773 } 774 775 self->sendNext(keepAlive, connId); 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