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