1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 // SPDX-FileCopyrightText: Copyright 2020 Intel Corporation
4 #pragma once
5
6 #include "bmcweb_config.h"
7
8 #include "async_resolve.hpp"
9 #include "boost_formatters.hpp"
10 #include "http_body.hpp"
11 #include "http_response.hpp"
12 #include "logging.hpp"
13 #include "ssl_key_handler.hpp"
14
15 #include <openssl/err.h>
16 #include <openssl/ssl.h>
17 #include <openssl/tls1.h>
18
19 #include <boost/asio/connect.hpp>
20 #include <boost/asio/error.hpp>
21 #include <boost/asio/io_context.hpp>
22 #include <boost/asio/ip/address.hpp>
23 #include <boost/asio/ip/tcp.hpp>
24 #include <boost/asio/ssl/context.hpp>
25 #include <boost/asio/ssl/error.hpp>
26 #include <boost/asio/ssl/stream.hpp>
27 #include <boost/asio/ssl/stream_base.hpp>
28 #include <boost/asio/steady_timer.hpp>
29 #include <boost/beast/core/error.hpp>
30 #include <boost/beast/core/flat_static_buffer.hpp>
31 #include <boost/beast/http/field.hpp>
32 #include <boost/beast/http/fields.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/status.hpp>
37 #include <boost/beast/http/verb.hpp>
38 #include <boost/beast/http/write.hpp>
39 #include <boost/container/devector.hpp>
40 #include <boost/optional/optional.hpp>
41 #include <boost/system/errc.hpp>
42 #include <boost/system/error_code.hpp>
43 #include <boost/url/host_type.hpp>
44 #include <boost/url/url.hpp>
45 #include <boost/url/url_view_base.hpp>
46
47 #include <chrono>
48 #include <cstdint>
49 #include <cstdlib>
50 #include <format>
51 #include <functional>
52 #include <memory>
53 #include <optional>
54 #include <string>
55 #include <string_view>
56 #include <type_traits>
57 #include <unordered_map>
58 #include <utility>
59 #include <vector>
60
61 namespace crow
62 {
63 // With Redfish Aggregation it is assumed we will connect to another
64 // instance of BMCWeb which can handle 100 simultaneous connections.
65 constexpr size_t maxPoolSize = 20;
66 constexpr size_t maxRequestQueueSize = 500;
67 constexpr unsigned int httpReadBodyLimit = 131072;
68 constexpr unsigned int httpReadBufferSize = 4096;
69
70 enum class ConnState
71 {
72 initialized,
73 resolveInProgress,
74 resolveFailed,
75 connectInProgress,
76 connectFailed,
77 connected,
78 handshakeInProgress,
79 handshakeFailed,
80 sendInProgress,
81 sendFailed,
82 recvInProgress,
83 recvFailed,
84 idle,
85 closed,
86 suspended,
87 terminated,
88 abortConnection,
89 sslInitFailed,
90 retry
91 };
92
defaultRetryHandler(unsigned int respCode)93 inline boost::system::error_code defaultRetryHandler(unsigned int respCode)
94 {
95 // As a default, assume 200X is alright
96 BMCWEB_LOG_DEBUG("Using default check for response code validity");
97 if ((respCode < 200) || (respCode >= 300))
98 {
99 return boost::system::errc::make_error_code(
100 boost::system::errc::result_out_of_range);
101 }
102
103 // Return 0 if the response code is valid
104 return boost::system::errc::make_error_code(boost::system::errc::success);
105 };
106
107 // We need to allow retry information to be set before a message has been
108 // sent and a connection pool has been created
109 struct ConnectionPolicy
110 {
111 uint32_t maxRetryAttempts = 5;
112
113 // the max size of requests in bytes. 0 for unlimited
114 boost::optional<uint64_t> requestByteLimit = httpReadBodyLimit;
115
116 size_t maxConnections = 1;
117
118 std::string retryPolicyAction = "TerminateAfterRetries";
119
120 std::chrono::seconds retryIntervalSecs = std::chrono::seconds(0);
121 std::function<boost::system::error_code(unsigned int respCode)>
122 invalidResp = defaultRetryHandler;
123 };
124
125 struct PendingRequest
126 {
127 boost::beast::http::request<bmcweb::HttpBody> req;
128 std::function<void(bool, uint32_t, Response&)> callback;
PendingRequestcrow::PendingRequest129 PendingRequest(
130 boost::beast::http::request<bmcweb::HttpBody>&& reqIn,
131 const std::function<void(bool, uint32_t, Response&)>& callbackIn) :
132 req(std::move(reqIn)), callback(callbackIn)
133 {}
134 };
135
136 namespace http = boost::beast::http;
137 class ConnectionInfo : public std::enable_shared_from_this<ConnectionInfo>
138 {
139 private:
140 ConnState state = ConnState::initialized;
141 uint32_t retryCount = 0;
142 std::string subId;
143 std::shared_ptr<ConnectionPolicy> connPolicy;
144 boost::urls::url host;
145 ensuressl::VerifyCertificate verifyCert;
146 uint32_t connId;
147 // Data buffers
148 http::request<bmcweb::HttpBody> req;
149 using parser_type = http::response_parser<bmcweb::HttpBody>;
150 std::optional<parser_type> parser;
151 boost::beast::flat_static_buffer<httpReadBufferSize> buffer;
152 Response res;
153
154 // Async callables
155 std::function<void(bool, uint32_t, Response&)> callback;
156
157 boost::asio::io_context& ioc;
158
159 using Resolver = std::conditional_t<BMCWEB_DNS_RESOLVER == "systemd-dbus",
160 async_resolve::Resolver,
161 boost::asio::ip::tcp::resolver>;
162 Resolver resolver;
163
164 boost::asio::ip::tcp::socket conn;
165 std::optional<boost::asio::ssl::stream<boost::asio::ip::tcp::socket&>>
166 sslConn;
167
168 boost::asio::steady_timer timer;
169
170 friend class ConnectionPool;
171
doResolve()172 void doResolve()
173 {
174 state = ConnState::resolveInProgress;
175 BMCWEB_LOG_DEBUG("Trying to resolve: {}, id: {}", host, connId);
176
177 resolver.async_resolve(host.encoded_host_address(), host.port(),
178 std::bind_front(&ConnectionInfo::afterResolve,
179 this, shared_from_this()));
180 }
181
afterResolve(const std::shared_ptr<ConnectionInfo> &,const boost::system::error_code & ec,const Resolver::results_type & endpointList)182 void afterResolve(const std::shared_ptr<ConnectionInfo>& /*self*/,
183 const boost::system::error_code& ec,
184 const Resolver::results_type& endpointList)
185 {
186 if (ec || (endpointList.empty()))
187 {
188 BMCWEB_LOG_ERROR("Resolve failed: {} {}", ec.message(), host);
189 state = ConnState::resolveFailed;
190 waitAndRetry();
191 return;
192 }
193 BMCWEB_LOG_DEBUG("Resolved {}, id: {}", host, connId);
194 state = ConnState::connectInProgress;
195
196 BMCWEB_LOG_DEBUG("Trying to connect to: {}, id: {}", host, connId);
197
198 timer.expires_after(std::chrono::seconds(30));
199 timer.async_wait(std::bind_front(onTimeout, weak_from_this()));
200
201 boost::asio::async_connect(
202 conn, endpointList,
203 std::bind_front(&ConnectionInfo::afterConnect, this,
204 shared_from_this()));
205 }
206
afterConnect(const std::shared_ptr<ConnectionInfo> &,const boost::beast::error_code & ec,const boost::asio::ip::tcp::endpoint & endpoint)207 void afterConnect(const std::shared_ptr<ConnectionInfo>& /*self*/,
208 const boost::beast::error_code& ec,
209 const boost::asio::ip::tcp::endpoint& endpoint)
210 {
211 // The operation already timed out. We don't want do continue down
212 // this branch
213 if (ec && ec == boost::asio::error::operation_aborted)
214 {
215 return;
216 }
217
218 timer.cancel();
219 if (ec)
220 {
221 BMCWEB_LOG_ERROR("Connect {}:{}, id: {} failed: {}",
222 host.encoded_host_address(), host.port(), connId,
223 ec.message());
224 state = ConnState::connectFailed;
225 waitAndRetry();
226 return;
227 }
228 BMCWEB_LOG_DEBUG("Connected to: {}:{}, id: {}",
229 endpoint.address().to_string(), endpoint.port(),
230 connId);
231 if (sslConn)
232 {
233 doSslHandshake();
234 return;
235 }
236 state = ConnState::connected;
237 sendMessage();
238 }
239
doSslHandshake()240 void doSslHandshake()
241 {
242 if (!sslConn)
243 {
244 return;
245 }
246 auto& ssl = *sslConn;
247 state = ConnState::handshakeInProgress;
248 timer.expires_after(std::chrono::seconds(30));
249 timer.async_wait(std::bind_front(onTimeout, weak_from_this()));
250 ssl.async_handshake(boost::asio::ssl::stream_base::client,
251 std::bind_front(&ConnectionInfo::afterSslHandshake,
252 this, shared_from_this()));
253 }
254
afterSslHandshake(const std::shared_ptr<ConnectionInfo> &,const boost::beast::error_code & ec)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 - id: {} error: {}", connId,
269 ec.message());
270 state = ConnState::handshakeFailed;
271 waitAndRetry();
272 return;
273 }
274 BMCWEB_LOG_DEBUG("SSL Handshake successful - id: {}", connId);
275 state = ConnState::connected;
276 sendMessage();
277 }
278
sendMessage()279 void sendMessage()
280 {
281 state = ConnState::sendInProgress;
282
283 // Set a timeout on the operation
284 timer.expires_after(std::chrono::seconds(30));
285 timer.async_wait(std::bind_front(onTimeout, weak_from_this()));
286 // Send the HTTP request to the remote host
287 if (sslConn)
288 {
289 boost::beast::http::async_write(
290 *sslConn, req,
291 std::bind_front(&ConnectionInfo::afterWrite, this,
292 shared_from_this()));
293 }
294 else
295 {
296 boost::beast::http::async_write(
297 conn, req,
298 std::bind_front(&ConnectionInfo::afterWrite, this,
299 shared_from_this()));
300 }
301 }
302
afterWrite(const std::shared_ptr<ConnectionInfo> &,const boost::beast::error_code & ec,size_t bytesTransferred)303 void afterWrite(const std::shared_ptr<ConnectionInfo>& /*self*/,
304 const boost::beast::error_code& ec, size_t bytesTransferred)
305 {
306 // The operation already timed out. We don't want do continue down
307 // this branch
308 if (ec && ec == boost::asio::error::operation_aborted)
309 {
310 return;
311 }
312
313 timer.cancel();
314 if (ec)
315 {
316 BMCWEB_LOG_ERROR("sendMessage() failed: {} {}", ec.message(), host);
317 state = ConnState::sendFailed;
318 waitAndRetry();
319 return;
320 }
321 BMCWEB_LOG_DEBUG("sendMessage() bytes transferred: {}",
322 bytesTransferred);
323
324 recvMessage();
325 }
326
recvMessage()327 void recvMessage()
328 {
329 state = ConnState::recvInProgress;
330
331 parser_type& thisParser = parser.emplace();
332
333 thisParser.body_limit(connPolicy->requestByteLimit);
334
335 timer.expires_after(std::chrono::seconds(30));
336 timer.async_wait(std::bind_front(onTimeout, weak_from_this()));
337
338 // Receive the HTTP response
339 if (sslConn)
340 {
341 boost::beast::http::async_read(
342 *sslConn, buffer, thisParser,
343 std::bind_front(&ConnectionInfo::afterRead, this,
344 shared_from_this()));
345 }
346 else
347 {
348 boost::beast::http::async_read(
349 conn, buffer, thisParser,
350 std::bind_front(&ConnectionInfo::afterRead, this,
351 shared_from_this()));
352 }
353 }
354
afterRead(const std::shared_ptr<ConnectionInfo> &,const boost::beast::error_code & ec,const std::size_t bytesTransferred)355 void afterRead(const std::shared_ptr<ConnectionInfo>& /*self*/,
356 const boost::beast::error_code& ec,
357 const std::size_t bytesTransferred)
358 {
359 // The operation already timed out. We don't want do continue down
360 // this branch
361 if (ec && ec == boost::asio::error::operation_aborted)
362 {
363 return;
364 }
365
366 timer.cancel();
367 if (ec && ec != boost::asio::ssl::error::stream_truncated)
368 {
369 BMCWEB_LOG_ERROR("recvMessage() failed: {} from {}", ec.message(),
370 host);
371 state = ConnState::recvFailed;
372 waitAndRetry();
373 return;
374 }
375 BMCWEB_LOG_DEBUG("recvMessage() bytes transferred: {}",
376 bytesTransferred);
377 if (!parser)
378 {
379 return;
380 }
381 BMCWEB_LOG_DEBUG("recvMessage() data: {}", parser->get().body().str());
382
383 unsigned int respCode = parser->get().result_int();
384 BMCWEB_LOG_DEBUG("recvMessage() Header Response Code: {}", respCode);
385
386 // Handle the case of stream_truncated. Some servers close the ssl
387 // connection uncleanly, so check to see if we got a full response
388 // before we handle this as an error.
389 if (!parser->is_done())
390 {
391 state = ConnState::recvFailed;
392 waitAndRetry();
393 return;
394 }
395
396 // Make sure the received response code is valid as defined by
397 // the associated retry policy
398 if (connPolicy->invalidResp(respCode))
399 {
400 // The listener failed to receive the Sent-Event
401 BMCWEB_LOG_ERROR(
402 "recvMessage() Listener Failed to "
403 "receive Sent-Event. Header Response Code: {} from {}",
404 respCode, host);
405 state = ConnState::recvFailed;
406 waitAndRetry();
407 return;
408 }
409
410 // Send is successful
411 // Reset the counter just in case this was after retrying
412 retryCount = 0;
413
414 // Keep the connection alive if server supports it
415 // Else close the connection
416 BMCWEB_LOG_DEBUG("recvMessage() keepalive : {}", parser->keep_alive());
417
418 // Copy the response into a Response object so that it can be
419 // processed by the callback function.
420 res.response = parser->release();
421 callback(parser->keep_alive(), connId, res);
422 res.clear();
423 }
424
onTimeout(const std::weak_ptr<ConnectionInfo> & weakSelf,const boost::system::error_code & ec)425 static void onTimeout(const std::weak_ptr<ConnectionInfo>& weakSelf,
426 const boost::system::error_code& ec)
427 {
428 if (ec == boost::asio::error::operation_aborted)
429 {
430 BMCWEB_LOG_DEBUG(
431 "async_wait failed since the operation is aborted");
432 return;
433 }
434 if (ec)
435 {
436 BMCWEB_LOG_ERROR("async_wait failed: {}", ec.message());
437 // If the timer fails, we need to close the socket anyway, same
438 // as if it expired.
439 }
440 std::shared_ptr<ConnectionInfo> self = weakSelf.lock();
441 if (self == nullptr)
442 {
443 return;
444 }
445 self->waitAndRetry();
446 }
447
waitAndRetry()448 void waitAndRetry()
449 {
450 if ((retryCount >= connPolicy->maxRetryAttempts) ||
451 (state == ConnState::sslInitFailed))
452 {
453 BMCWEB_LOG_ERROR("Maximum number of retries reached. {}", host);
454 BMCWEB_LOG_DEBUG("Retry policy: {}", connPolicy->retryPolicyAction);
455
456 if (connPolicy->retryPolicyAction == "TerminateAfterRetries")
457 {
458 // TODO: delete subscription
459 state = ConnState::terminated;
460 }
461 if (connPolicy->retryPolicyAction == "SuspendRetries")
462 {
463 state = ConnState::suspended;
464 }
465
466 // We want to return a 502 to indicate there was an error with
467 // the external server
468 res.result(boost::beast::http::status::bad_gateway);
469 callback(false, connId, res);
470 res.clear();
471
472 // Reset the retrycount to zero so that client can try
473 // connecting again if needed
474 retryCount = 0;
475 return;
476 }
477
478 retryCount++;
479
480 BMCWEB_LOG_DEBUG("Attempt retry after {} seconds. RetryCount = {}",
481 connPolicy->retryIntervalSecs.count(), retryCount);
482 timer.expires_after(connPolicy->retryIntervalSecs);
483 timer.async_wait(std::bind_front(&ConnectionInfo::onTimerDone, this,
484 shared_from_this()));
485 }
486
onTimerDone(const std::shared_ptr<ConnectionInfo> &,const boost::system::error_code & ec)487 void onTimerDone(const std::shared_ptr<ConnectionInfo>& /*self*/,
488 const boost::system::error_code& ec)
489 {
490 if (ec == boost::asio::error::operation_aborted)
491 {
492 BMCWEB_LOG_DEBUG(
493 "async_wait failed since the operation is aborted{}",
494 ec.message());
495 }
496 else if (ec)
497 {
498 BMCWEB_LOG_ERROR("async_wait failed: {}", ec.message());
499 // Ignore the error and continue the retry loop to attempt
500 // sending the event as per the retry policy
501 }
502
503 // Let's close the connection and restart from resolve.
504 shutdownConn(true);
505 }
506
restartConnection()507 void restartConnection()
508 {
509 BMCWEB_LOG_DEBUG("{}, id: {} restartConnection", host,
510 std::to_string(connId));
511 initializeConnection(host.scheme() == "https");
512 doResolve();
513 }
514
shutdownConn(bool retry)515 void shutdownConn(bool retry)
516 {
517 boost::beast::error_code ec;
518 conn.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
519 conn.close();
520
521 // not_connected happens sometimes so don't bother reporting it.
522 if (ec && ec != boost::beast::errc::not_connected)
523 {
524 BMCWEB_LOG_ERROR("{}, id: {} shutdown failed: {}", host, connId,
525 ec.message());
526 }
527 else
528 {
529 BMCWEB_LOG_DEBUG("{}, id: {} closed gracefully", host, connId);
530 }
531
532 if (retry)
533 {
534 // Now let's try to resend the data
535 state = ConnState::retry;
536 restartConnection();
537 }
538 else
539 {
540 state = ConnState::closed;
541 }
542 }
543
doClose(bool retry=false)544 void doClose(bool retry = false)
545 {
546 if (!sslConn)
547 {
548 shutdownConn(retry);
549 return;
550 }
551
552 sslConn->async_shutdown(
553 std::bind_front(&ConnectionInfo::afterSslShutdown, this,
554 shared_from_this(), retry));
555 }
556
afterSslShutdown(const std::shared_ptr<ConnectionInfo> &,bool retry,const boost::system::error_code & ec)557 void afterSslShutdown(const std::shared_ptr<ConnectionInfo>& /*self*/,
558 bool retry, const boost::system::error_code& ec)
559 {
560 if (ec)
561 {
562 BMCWEB_LOG_ERROR("{}, id: {} shutdown failed: {}", host, connId,
563 ec.message());
564 }
565 else
566 {
567 BMCWEB_LOG_DEBUG("{}, id: {} closed gracefully", host, connId);
568 }
569 shutdownConn(retry);
570 }
571
setCipherSuiteTLSext()572 void setCipherSuiteTLSext()
573 {
574 if (!sslConn)
575 {
576 return;
577 }
578
579 if (host.host_type() != boost::urls::host_type::name)
580 {
581 // Avoid setting SNI hostname if its IP address
582 return;
583 }
584 // Create a null terminated string for SSL
585 std::string hostname(host.encoded_host_address());
586 if (SSL_set_tlsext_host_name(sslConn->native_handle(),
587 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 // Check whether all connections are terminated
areAllConnectionsTerminated()847 bool areAllConnectionsTerminated()
848 {
849 if (connections.empty())
850 {
851 BMCWEB_LOG_DEBUG("There are no connections for pool id:{}", id);
852 return false;
853 }
854 for (const auto& conn : connections)
855 {
856 if (conn != nullptr && conn->state != ConnState::terminated)
857 {
858 BMCWEB_LOG_DEBUG(
859 "Not all connections of pool id:{} are terminated", id);
860 return false;
861 }
862 }
863 BMCWEB_LOG_INFO("All connections of pool id:{} are terminated", id);
864 return true;
865 }
866 };
867
868 class HttpClient
869 {
870 private:
871 std::unordered_map<std::string, std::shared_ptr<ConnectionPool>>
872 connectionPools;
873
874 // reference_wrapper here makes HttpClient movable
875 std::reference_wrapper<boost::asio::io_context> ioc;
876 std::shared_ptr<ConnectionPolicy> connPolicy;
877
878 // Used as a dummy callback by sendData() in order to call
879 // sendDataWithCallback()
genericResHandler(const Response & res)880 static void genericResHandler(const Response& res)
881 {
882 BMCWEB_LOG_DEBUG("Response handled with return code: {}",
883 res.resultInt());
884 }
885
886 public:
887 HttpClient() = delete;
HttpClient(boost::asio::io_context & iocIn,const std::shared_ptr<ConnectionPolicy> & connPolicyIn)888 explicit HttpClient(boost::asio::io_context& iocIn,
889 const std::shared_ptr<ConnectionPolicy>& connPolicyIn) :
890 ioc(iocIn), connPolicy(connPolicyIn)
891 {}
892
893 HttpClient(const HttpClient&) = delete;
894 HttpClient& operator=(const HttpClient&) = delete;
895 HttpClient(HttpClient&& client) = default;
896 HttpClient& operator=(HttpClient&& client) = default;
897 ~HttpClient() = default;
898
899 // Send a request to destIP where additional processing of the
900 // 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)901 void sendData(std::string&& data, const boost::urls::url_view_base& destUri,
902 ensuressl::VerifyCertificate verifyCert,
903 const boost::beast::http::fields& httpHeader,
904 const boost::beast::http::verb verb)
905 {
906 const std::function<void(Response&)> cb = genericResHandler;
907 sendDataWithCallback(std::move(data), destUri, verifyCert, httpHeader,
908 verb, cb);
909 }
910
911 // Send request to destIP and use the provided callback to
912 // 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)913 void sendDataWithCallback(std::string&& data,
914 const boost::urls::url_view_base& destUrl,
915 ensuressl::VerifyCertificate verifyCert,
916 const boost::beast::http::fields& httpHeader,
917 const boost::beast::http::verb verb,
918 const std::function<void(Response&)>& resHandler)
919 {
920 std::string_view verify = "ssl_verify";
921 if (verifyCert == ensuressl::VerifyCertificate::NoVerify)
922 {
923 verify = "ssl no verify";
924 }
925 std::string clientKey =
926 std::format("{}{}://{}", verify, destUrl.scheme(),
927 destUrl.encoded_host_and_port());
928 auto pool = connectionPools.try_emplace(clientKey);
929 if (pool.first->second == nullptr)
930 {
931 pool.first->second = std::make_shared<ConnectionPool>(
932 ioc, clientKey, connPolicy, destUrl, verifyCert);
933 }
934 // Send the data using either the existing connection pool or the
935 // newly created connection pool
936 pool.first->second->sendData(std::move(data), destUrl, httpHeader, verb,
937 resHandler);
938 }
939
940 // Test whether all connections are terminated (after MaxRetryAttempts)
isTerminated()941 bool isTerminated()
942 {
943 for (const auto& pool : connectionPools)
944 {
945 if (pool.second != nullptr &&
946 !pool.second->areAllConnectionsTerminated())
947 {
948 BMCWEB_LOG_DEBUG(
949 "Not all of client connections are terminated");
950 return false;
951 }
952 }
953 BMCWEB_LOG_DEBUG("All client connections are terminated");
954 return true;
955 }
956 };
957 } // namespace crow
958