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 state = ConnState::resolveFailed; 185 waitAndRetry(); 186 return; 187 } 188 BMCWEB_LOG_DEBUG << "Resolved " << host << ":" << std::to_string(port) 189 << ", id: " << std::to_string(connId); 190 state = ConnState::connectInProgress; 191 192 BMCWEB_LOG_DEBUG << "Trying to connect to: " << host << ":" 193 << std::to_string(port) 194 << ", id: " << std::to_string(connId); 195 196 timer.expires_after(std::chrono::seconds(30)); 197 timer.async_wait(std::bind_front(onTimeout, weak_from_this())); 198 199 boost::asio::async_connect( 200 conn, endpointList, 201 std::bind_front(&ConnectionInfo::afterConnect, this, 202 shared_from_this())); 203 } 204 205 void afterConnect(const std::shared_ptr<ConnectionInfo>& /*self*/, 206 const boost::beast::error_code& ec, 207 const boost::asio::ip::tcp::endpoint& endpoint) 208 { 209 // The operation already timed out. We don't want do continue down 210 // this branch 211 if (ec && ec == boost::asio::error::operation_aborted) 212 { 213 return; 214 } 215 216 timer.cancel(); 217 if (ec) 218 { 219 BMCWEB_LOG_ERROR << "Connect " << endpoint.address().to_string() 220 << ":" << std::to_string(endpoint.port()) 221 << ", id: " << std::to_string(connId) 222 << " failed: " << ec.message(); 223 state = ConnState::connectFailed; 224 waitAndRetry(); 225 return; 226 } 227 BMCWEB_LOG_DEBUG << "Connected to: " << endpoint.address().to_string() 228 << ":" << std::to_string(endpoint.port()) 229 << ", id: " << std::to_string(connId); 230 if (sslConn) 231 { 232 doSslHandshake(); 233 return; 234 } 235 state = ConnState::connected; 236 sendMessage(); 237 } 238 239 void doSslHandshake() 240 { 241 if (!sslConn) 242 { 243 return; 244 } 245 state = ConnState::handshakeInProgress; 246 timer.expires_after(std::chrono::seconds(30)); 247 timer.async_wait(std::bind_front(onTimeout, weak_from_this())); 248 sslConn->async_handshake( 249 boost::asio::ssl::stream_base::client, 250 std::bind_front(&ConnectionInfo::afterSslHandshake, this, 251 shared_from_this())); 252 } 253 254 void afterSslHandshake(const std::shared_ptr<ConnectionInfo>& /*self*/, 255 const boost::beast::error_code& ec) 256 { 257 // The operation already timed out. We don't want do continue down 258 // this branch 259 if (ec && ec == boost::asio::error::operation_aborted) 260 { 261 return; 262 } 263 264 timer.cancel(); 265 if (ec) 266 { 267 BMCWEB_LOG_ERROR << "SSL Handshake failed -" 268 << " id: " << std::to_string(connId) 269 << " error: " << ec.message(); 270 state = ConnState::handshakeFailed; 271 waitAndRetry(); 272 return; 273 } 274 BMCWEB_LOG_DEBUG << "SSL Handshake successful -" 275 << " id: " << std::to_string(connId); 276 state = ConnState::connected; 277 sendMessage(); 278 } 279 280 void sendMessage() 281 { 282 state = ConnState::sendInProgress; 283 284 // Set a timeout on the operation 285 timer.expires_after(std::chrono::seconds(30)); 286 timer.async_wait(std::bind_front(onTimeout, weak_from_this())); 287 288 // Send the HTTP request to the remote host 289 if (sslConn) 290 { 291 boost::beast::http::async_write( 292 *sslConn, req, 293 std::bind_front(&ConnectionInfo::afterWrite, this, 294 shared_from_this())); 295 } 296 else 297 { 298 boost::beast::http::async_write( 299 conn, req, 300 std::bind_front(&ConnectionInfo::afterWrite, this, 301 shared_from_this())); 302 } 303 } 304 305 void afterWrite(const std::shared_ptr<ConnectionInfo>& /*self*/, 306 const boost::beast::error_code& ec, size_t bytesTransferred) 307 { 308 // The operation already timed out. We don't want do continue down 309 // this branch 310 if (ec && ec == boost::asio::error::operation_aborted) 311 { 312 return; 313 } 314 315 timer.cancel(); 316 if (ec) 317 { 318 BMCWEB_LOG_ERROR << "sendMessage() failed: " << ec.message(); 319 state = ConnState::sendFailed; 320 waitAndRetry(); 321 return; 322 } 323 BMCWEB_LOG_DEBUG << "sendMessage() bytes transferred: " 324 << bytesTransferred; 325 326 recvMessage(); 327 } 328 329 void recvMessage() 330 { 331 state = ConnState::recvInProgress; 332 333 parser.emplace(std::piecewise_construct, std::make_tuple()); 334 335 parser->body_limit(connPolicy->requestByteLimit); 336 337 timer.expires_after(std::chrono::seconds(30)); 338 timer.async_wait(std::bind_front(onTimeout, weak_from_this())); 339 340 // Receive the HTTP response 341 if (sslConn) 342 { 343 boost::beast::http::async_read( 344 *sslConn, buffer, *parser, 345 std::bind_front(&ConnectionInfo::afterRead, this, 346 shared_from_this())); 347 } 348 else 349 { 350 boost::beast::http::async_read( 351 conn, buffer, *parser, 352 std::bind_front(&ConnectionInfo::afterRead, this, 353 shared_from_this())); 354 } 355 } 356 357 void afterRead(const std::shared_ptr<ConnectionInfo>& /*self*/, 358 const boost::beast::error_code& ec, 359 const std::size_t& bytesTransferred) 360 { 361 // The operation already timed out. We don't want do continue down 362 // this branch 363 if (ec && ec == boost::asio::error::operation_aborted) 364 { 365 return; 366 } 367 368 timer.cancel(); 369 if (ec && ec != boost::asio::ssl::error::stream_truncated) 370 { 371 BMCWEB_LOG_ERROR << "recvMessage() failed: " << ec.message(); 372 state = ConnState::recvFailed; 373 waitAndRetry(); 374 return; 375 } 376 BMCWEB_LOG_DEBUG << "recvMessage() bytes transferred: " 377 << bytesTransferred; 378 BMCWEB_LOG_DEBUG << "recvMessage() data: " << parser->get().body(); 379 380 unsigned int respCode = parser->get().result_int(); 381 BMCWEB_LOG_DEBUG << "recvMessage() Header Response Code: " << respCode; 382 383 // Make sure the received response code is valid as defined by 384 // the associated retry policy 385 if (connPolicy->invalidResp(respCode)) 386 { 387 // The listener failed to receive the Sent-Event 388 BMCWEB_LOG_ERROR << "recvMessage() Listener Failed to " 389 "receive Sent-Event. Header Response Code: " 390 << respCode; 391 state = ConnState::recvFailed; 392 waitAndRetry(); 393 return; 394 } 395 396 // Send is successful 397 // Reset the counter just in case this was after retrying 398 retryCount = 0; 399 400 // Keep the connection alive if server supports it 401 // Else close the connection 402 BMCWEB_LOG_DEBUG << "recvMessage() keepalive : " 403 << parser->keep_alive(); 404 405 // Copy the response into a Response object so that it can be 406 // processed by the callback function. 407 res.stringResponse = parser->release(); 408 callback(parser->keep_alive(), connId, res); 409 res.clear(); 410 } 411 412 static void onTimeout(const std::weak_ptr<ConnectionInfo>& weakSelf, 413 const boost::system::error_code& ec) 414 { 415 if (ec == boost::asio::error::operation_aborted) 416 { 417 BMCWEB_LOG_DEBUG 418 << "async_wait failed since the operation is aborted"; 419 return; 420 } 421 if (ec) 422 { 423 BMCWEB_LOG_ERROR << "async_wait failed: " << ec.message(); 424 // If the timer fails, we need to close the socket anyway, same as 425 // if it expired. 426 } 427 std::shared_ptr<ConnectionInfo> self = weakSelf.lock(); 428 if (self == nullptr) 429 { 430 return; 431 } 432 self->waitAndRetry(); 433 } 434 435 void waitAndRetry() 436 { 437 if ((retryCount >= connPolicy->maxRetryAttempts) || 438 (state == ConnState::sslInitFailed)) 439 { 440 BMCWEB_LOG_ERROR << "Maximum number of retries reached."; 441 BMCWEB_LOG_DEBUG << "Retry policy: " 442 << connPolicy->retryPolicyAction; 443 444 if (connPolicy->retryPolicyAction == "TerminateAfterRetries") 445 { 446 // TODO: delete subscription 447 state = ConnState::terminated; 448 } 449 if (connPolicy->retryPolicyAction == "SuspendRetries") 450 { 451 state = ConnState::suspended; 452 } 453 454 // We want to return a 502 to indicate there was an error with 455 // the external server 456 res.result(boost::beast::http::status::bad_gateway); 457 callback(false, connId, res); 458 res.clear(); 459 460 // Reset the retrycount to zero so that client can try connecting 461 // again if needed 462 retryCount = 0; 463 return; 464 } 465 466 retryCount++; 467 468 BMCWEB_LOG_DEBUG << "Attempt retry after " 469 << std::to_string( 470 connPolicy->retryIntervalSecs.count()) 471 << " seconds. RetryCount = " << retryCount; 472 timer.expires_after(connPolicy->retryIntervalSecs); 473 timer.async_wait(std::bind_front(&ConnectionInfo::onTimerDone, this, 474 shared_from_this())); 475 } 476 477 void onTimerDone(const std::shared_ptr<ConnectionInfo>& /*self*/, 478 const boost::system::error_code& ec) 479 { 480 if (ec == boost::asio::error::operation_aborted) 481 { 482 BMCWEB_LOG_DEBUG 483 << "async_wait failed since the operation is aborted" 484 << ec.message(); 485 } 486 else if (ec) 487 { 488 BMCWEB_LOG_ERROR << "async_wait failed: " << ec.message(); 489 // Ignore the error and continue the retry loop to attempt 490 // sending the event as per the retry policy 491 } 492 493 // Let's close the connection and restart from resolve. 494 doClose(true); 495 } 496 497 void shutdownConn(bool retry) 498 { 499 boost::beast::error_code ec; 500 conn.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); 501 conn.close(); 502 503 // not_connected happens sometimes so don't bother reporting it. 504 if (ec && ec != boost::beast::errc::not_connected) 505 { 506 BMCWEB_LOG_ERROR << host << ":" << std::to_string(port) 507 << ", id: " << std::to_string(connId) 508 << " shutdown failed: " << ec.message(); 509 } 510 else 511 { 512 BMCWEB_LOG_DEBUG << host << ":" << std::to_string(port) 513 << ", id: " << std::to_string(connId) 514 << " closed gracefully"; 515 } 516 517 if (retry) 518 { 519 // Now let's try to resend the data 520 state = ConnState::retry; 521 doResolve(); 522 } 523 else 524 { 525 state = ConnState::closed; 526 } 527 } 528 529 void doClose(bool retry = false) 530 { 531 if (!sslConn) 532 { 533 shutdownConn(retry); 534 return; 535 } 536 537 sslConn->async_shutdown( 538 std::bind_front(&ConnectionInfo::afterSslShutdown, this, 539 shared_from_this(), retry)); 540 } 541 542 void afterSslShutdown(const std::shared_ptr<ConnectionInfo>& /*self*/, 543 bool retry, const boost::system::error_code& ec) 544 { 545 if (ec) 546 { 547 BMCWEB_LOG_ERROR << host << ":" << std::to_string(port) 548 << ", id: " << std::to_string(connId) 549 << " shutdown failed: " << ec.message(); 550 } 551 else 552 { 553 BMCWEB_LOG_DEBUG << host << ":" << std::to_string(port) 554 << ", id: " << std::to_string(connId) 555 << " closed gracefully"; 556 } 557 shutdownConn(retry); 558 } 559 560 void setCipherSuiteTLSext() 561 { 562 if (!sslConn) 563 { 564 return; 565 } 566 // NOTE: The SSL_set_tlsext_host_name is defined in tlsv1.h header 567 // file but its having old style casting (name is cast to void*). 568 // Since bmcweb compiler treats all old-style-cast as error, its 569 // causing the build failure. So replaced the same macro inline and 570 // did corrected the code by doing static_cast to viod*. This has to 571 // be fixed in openssl library in long run. Set SNI Hostname (many 572 // hosts need this to handshake successfully) 573 if (SSL_ctrl(sslConn->native_handle(), SSL_CTRL_SET_TLSEXT_HOSTNAME, 574 TLSEXT_NAMETYPE_host_name, 575 static_cast<void*>(&host.front())) == 0) 576 577 { 578 boost::beast::error_code ec{static_cast<int>(::ERR_get_error()), 579 boost::asio::error::get_ssl_category()}; 580 581 BMCWEB_LOG_ERROR << "SSL_set_tlsext_host_name " << host << ":" 582 << port << ", id: " << std::to_string(connId) 583 << " failed: " << ec.message(); 584 // Set state as sslInit failed so that we close the connection 585 // and take appropriate action as per retry configuration. 586 state = ConnState::sslInitFailed; 587 waitAndRetry(); 588 return; 589 } 590 } 591 592 public: 593 explicit ConnectionInfo( 594 boost::asio::io_context& iocIn, const std::string& idIn, 595 const std::shared_ptr<ConnectionPolicy>& connPolicyIn, 596 const std::string& destIPIn, uint16_t destPortIn, bool useSSL, 597 unsigned int connIdIn) : 598 subId(idIn), 599 connPolicy(connPolicyIn), host(destIPIn), port(destPortIn), 600 connId(connIdIn), resolver(iocIn), conn(iocIn), timer(iocIn) 601 { 602 if (useSSL) 603 { 604 std::optional<boost::asio::ssl::context> sslCtx = 605 ensuressl::getSSLClientContext(); 606 607 if (!sslCtx) 608 { 609 BMCWEB_LOG_ERROR << "prepareSSLContext failed - " << host << ":" 610 << port << ", id: " << std::to_string(connId); 611 // Don't retry if failure occurs while preparing SSL context 612 // such as certificate is invalid or set cipher failure or set 613 // host name failure etc... Setting conn state to sslInitFailed 614 // and connection state will be transitioned to next state 615 // depending on retry policy set by subscription. 616 state = ConnState::sslInitFailed; 617 waitAndRetry(); 618 return; 619 } 620 sslConn.emplace(conn, *sslCtx); 621 setCipherSuiteTLSext(); 622 } 623 } 624 }; 625 626 class ConnectionPool : public std::enable_shared_from_this<ConnectionPool> 627 { 628 private: 629 boost::asio::io_context& ioc; 630 std::string id; 631 std::shared_ptr<ConnectionPolicy> connPolicy; 632 std::string destIP; 633 uint16_t destPort; 634 bool useSSL; 635 std::vector<std::shared_ptr<ConnectionInfo>> connections; 636 boost::container::devector<PendingRequest> requestQueue; 637 638 friend class HttpClient; 639 640 // Configure a connections's request, callback, and retry info in 641 // preparation to begin sending the request 642 void setConnProps(ConnectionInfo& conn) 643 { 644 if (requestQueue.empty()) 645 { 646 BMCWEB_LOG_ERROR 647 << "setConnProps() should not have been called when requestQueue is empty"; 648 return; 649 } 650 651 auto nextReq = requestQueue.front(); 652 conn.req = std::move(nextReq.req); 653 conn.callback = std::move(nextReq.callback); 654 655 BMCWEB_LOG_DEBUG << "Setting properties for connection " << conn.host 656 << ":" << std::to_string(conn.port) 657 << ", id: " << std::to_string(conn.connId); 658 659 // We can remove the request from the queue at this point 660 requestQueue.pop_front(); 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 std::function<void(Response&)>& resHandler) 717 { 718 // Construct the request to be sent 719 boost::beast::http::request<boost::beast::http::string_body> thisReq( 720 verb, destUri, 11, "", httpHeader); 721 thisReq.set(boost::beast::http::field::host, destIP); 722 thisReq.keep_alive(true); 723 thisReq.body() = std::move(data); 724 thisReq.prepare_payload(); 725 auto cb = std::bind_front(&ConnectionPool::afterSendData, 726 weak_from_this(), resHandler); 727 // Reuse an existing connection if one is available 728 for (unsigned int i = 0; i < connections.size(); i++) 729 { 730 auto conn = connections[i]; 731 if ((conn->state == ConnState::idle) || 732 (conn->state == ConnState::initialized) || 733 (conn->state == ConnState::closed)) 734 { 735 conn->req = std::move(thisReq); 736 conn->callback = std::move(cb); 737 std::string commonMsg = std::to_string(i) + " from pool " + 738 destIP + ":" + std::to_string(destPort); 739 740 if (conn->state == ConnState::idle) 741 { 742 BMCWEB_LOG_DEBUG << "Grabbing idle connection " 743 << commonMsg; 744 conn->sendMessage(); 745 } 746 else 747 { 748 BMCWEB_LOG_DEBUG << "Reusing existing connection " 749 << commonMsg; 750 conn->doResolve(); 751 } 752 return; 753 } 754 } 755 756 // All connections in use so create a new connection or add request to 757 // the queue 758 if (connections.size() < connPolicy->maxConnections) 759 { 760 BMCWEB_LOG_DEBUG << "Adding new connection to pool " << destIP 761 << ":" << std::to_string(destPort); 762 auto conn = addConnection(); 763 conn->req = std::move(thisReq); 764 conn->callback = std::move(cb); 765 conn->doResolve(); 766 } 767 else if (requestQueue.size() < maxRequestQueueSize) 768 { 769 BMCWEB_LOG_ERROR << "Max pool size reached. Adding data to queue."; 770 requestQueue.emplace_back(std::move(thisReq), std::move(cb)); 771 } 772 else 773 { 774 // If we can't buffer the request then we should let the callback 775 // handle a 429 Too Many Requests dummy response 776 BMCWEB_LOG_ERROR << destIP << ":" << std::to_string(destPort) 777 << " request queue full. Dropping request."; 778 Response dummyRes; 779 dummyRes.result(boost::beast::http::status::too_many_requests); 780 resHandler(dummyRes); 781 } 782 } 783 784 // Callback to be called once the request has been sent 785 static void afterSendData(const std::weak_ptr<ConnectionPool>& weakSelf, 786 const std::function<void(Response&)>& resHandler, 787 bool keepAlive, uint32_t connId, Response& res) 788 { 789 // Allow provided callback to perform additional processing of the 790 // request 791 resHandler(res); 792 793 // If requests remain in the queue then we want to reuse this 794 // connection to send the next request 795 std::shared_ptr<ConnectionPool> self = weakSelf.lock(); 796 if (!self) 797 { 798 BMCWEB_LOG_CRITICAL << self << " Failed to capture connection"; 799 return; 800 } 801 802 self->sendNext(keepAlive, connId); 803 } 804 805 std::shared_ptr<ConnectionInfo>& addConnection() 806 { 807 unsigned int newId = static_cast<unsigned int>(connections.size()); 808 809 auto& ret = connections.emplace_back(std::make_shared<ConnectionInfo>( 810 ioc, id, connPolicy, destIP, destPort, useSSL, newId)); 811 812 BMCWEB_LOG_DEBUG << "Added connection " 813 << std::to_string(connections.size() - 1) 814 << " to pool " << destIP << ":" 815 << std::to_string(destPort); 816 817 return ret; 818 } 819 820 public: 821 explicit ConnectionPool( 822 boost::asio::io_context& iocIn, const std::string& idIn, 823 const std::shared_ptr<ConnectionPolicy>& connPolicyIn, 824 const std::string& destIPIn, uint16_t destPortIn, bool useSSLIn) : 825 ioc(iocIn), 826 id(idIn), connPolicy(connPolicyIn), destIP(destIPIn), 827 destPort(destPortIn), useSSL(useSSLIn) 828 { 829 BMCWEB_LOG_DEBUG << "Initializing connection pool for " << destIP << ":" 830 << std::to_string(destPort); 831 832 // Initialize the pool with a single connection 833 addConnection(); 834 } 835 }; 836 837 class HttpClient 838 { 839 private: 840 std::unordered_map<std::string, std::shared_ptr<ConnectionPool>> 841 connectionPools; 842 boost::asio::io_context& ioc; 843 std::shared_ptr<ConnectionPolicy> connPolicy; 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() = delete; 855 explicit HttpClient(boost::asio::io_context& iocIn, 856 const std::shared_ptr<ConnectionPolicy>& connPolicyIn) : 857 ioc(iocIn), 858 connPolicy(connPolicyIn) 859 {} 860 861 HttpClient(const HttpClient&) = delete; 862 HttpClient& operator=(const HttpClient&) = delete; 863 HttpClient(HttpClient&&) = delete; 864 HttpClient& operator=(HttpClient&&) = delete; 865 ~HttpClient() = default; 866 867 // Send a request to destIP:destPort where additional processing of the 868 // result is not required 869 void sendData(std::string&& data, const std::string& destIP, 870 uint16_t destPort, const std::string& destUri, bool useSSL, 871 const boost::beast::http::fields& httpHeader, 872 const boost::beast::http::verb verb) 873 { 874 const std::function<void(Response&)> cb = genericResHandler; 875 sendDataWithCallback(std::move(data), destIP, destPort, destUri, useSSL, 876 httpHeader, verb, cb); 877 } 878 879 // Send request to destIP:destPort and use the provided callback to 880 // handle the response 881 void sendDataWithCallback(std::string&& data, const std::string& destIP, 882 uint16_t destPort, const std::string& destUri, 883 bool useSSL, 884 const boost::beast::http::fields& httpHeader, 885 const boost::beast::http::verb verb, 886 const std::function<void(Response&)>& resHandler) 887 { 888 std::string clientKey = useSSL ? "https" : "http"; 889 clientKey += destIP; 890 clientKey += ":"; 891 clientKey += std::to_string(destPort); 892 auto pool = connectionPools.try_emplace(clientKey); 893 if (pool.first->second == nullptr) 894 { 895 pool.first->second = std::make_shared<ConnectionPool>( 896 ioc, clientKey, connPolicy, destIP, destPort, useSSL); 897 } 898 // Send the data using either the existing connection pool or the newly 899 // created connection pool 900 pool.first->second->sendData(std::move(data), destUri, httpHeader, verb, 901 resHandler); 902 } 903 }; 904 } // namespace crow 905