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