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