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