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 #include <boost/asio/ip/address.hpp> 18 #include <boost/asio/ip/basic_endpoint.hpp> 19 #include <boost/asio/steady_timer.hpp> 20 #include <boost/beast/core/flat_buffer.hpp> 21 #include <boost/beast/core/tcp_stream.hpp> 22 #include <boost/beast/http/message.hpp> 23 #include <boost/beast/version.hpp> 24 #include <include/async_resolve.hpp> 25 26 #include <cstdlib> 27 #include <functional> 28 #include <iostream> 29 #include <memory> 30 #include <queue> 31 #include <string> 32 33 namespace crow 34 { 35 36 static constexpr uint8_t maxRequestQueueSize = 50; 37 static constexpr unsigned int httpReadBodyLimit = 8192; 38 39 enum class ConnState 40 { 41 initialized, 42 resolveInProgress, 43 resolveFailed, 44 connectInProgress, 45 connectFailed, 46 connected, 47 sendInProgress, 48 sendFailed, 49 recvInProgress, 50 recvFailed, 51 idle, 52 closeInProgress, 53 closed, 54 suspended, 55 terminated, 56 abortConnection, 57 retry 58 }; 59 60 class HttpClient : public std::enable_shared_from_this<HttpClient> 61 { 62 private: 63 crow::async_resolve::Resolver resolver; 64 boost::beast::tcp_stream conn; 65 boost::asio::steady_timer timer; 66 boost::beast::flat_static_buffer<httpReadBodyLimit> buffer; 67 boost::beast::http::request<boost::beast::http::string_body> req; 68 std::optional< 69 boost::beast::http::response_parser<boost::beast::http::string_body>> 70 parser; 71 std::vector<std::pair<std::string, std::string>> headers; 72 boost::circular_buffer_space_optimized<std::string> requestDataQueue{}; 73 74 ConnState state; 75 std::string subId; 76 std::string host; 77 std::string port; 78 std::string uri; 79 uint32_t retryCount; 80 uint32_t maxRetryAttempts; 81 uint32_t retryIntervalSecs; 82 std::string retryPolicyAction; 83 bool runningTimer; 84 85 void doResolve() 86 { 87 state = ConnState::resolveInProgress; 88 BMCWEB_LOG_DEBUG << "Trying to resolve: " << host << ":" << port; 89 90 auto respHandler = 91 [self(shared_from_this())]( 92 const boost::beast::error_code ec, 93 const std::vector<boost::asio::ip::tcp::endpoint>& 94 endpointList) { 95 if (ec || (endpointList.size() == 0)) 96 { 97 BMCWEB_LOG_ERROR << "Resolve failed: " << ec.message(); 98 self->state = ConnState::resolveFailed; 99 self->handleConnState(); 100 return; 101 } 102 BMCWEB_LOG_DEBUG << "Resolved"; 103 self->doConnect(endpointList); 104 }; 105 resolver.asyncResolve(host, port, std::move(respHandler)); 106 } 107 108 void doConnect( 109 const std::vector<boost::asio::ip::tcp::endpoint>& endpointList) 110 { 111 state = ConnState::connectInProgress; 112 113 BMCWEB_LOG_DEBUG << "Trying to connect to: " << host << ":" << port; 114 115 conn.expires_after(std::chrono::seconds(30)); 116 conn.async_connect( 117 endpointList, [self(shared_from_this())]( 118 const boost::beast::error_code ec, 119 const boost::asio::ip::tcp::endpoint& endpoint) { 120 if (ec) 121 { 122 BMCWEB_LOG_ERROR << "Connect " << endpoint 123 << " failed: " << ec.message(); 124 self->state = ConnState::connectFailed; 125 self->handleConnState(); 126 return; 127 } 128 BMCWEB_LOG_DEBUG << "Connected to: " << endpoint; 129 self->state = ConnState::connected; 130 self->handleConnState(); 131 }); 132 } 133 134 void sendMessage(const std::string& data) 135 { 136 state = ConnState::sendInProgress; 137 138 BMCWEB_LOG_DEBUG << __FUNCTION__ << "(): " << host << ":" << port; 139 140 req.version(static_cast<int>(11)); // HTTP 1.1 141 req.target(uri); 142 req.method(boost::beast::http::verb::post); 143 144 // Set headers 145 for (const auto& [key, value] : headers) 146 { 147 req.set(key, value); 148 } 149 req.set(boost::beast::http::field::host, host); 150 req.keep_alive(true); 151 152 req.body() = data; 153 req.prepare_payload(); 154 155 // Set a timeout on the operation 156 conn.expires_after(std::chrono::seconds(30)); 157 158 // Send the HTTP request to the remote host 159 boost::beast::http::async_write( 160 conn, req, 161 [self(shared_from_this())](const boost::beast::error_code& ec, 162 const std::size_t& bytesTransferred) { 163 if (ec) 164 { 165 BMCWEB_LOG_ERROR << "sendMessage() failed: " 166 << ec.message(); 167 self->state = ConnState::sendFailed; 168 self->handleConnState(); 169 return; 170 } 171 BMCWEB_LOG_DEBUG << "sendMessage() bytes transferred: " 172 << bytesTransferred; 173 boost::ignore_unused(bytesTransferred); 174 175 self->recvMessage(); 176 }); 177 } 178 179 void recvMessage() 180 { 181 state = ConnState::recvInProgress; 182 183 parser.emplace(std::piecewise_construct, std::make_tuple()); 184 parser->body_limit(httpReadBodyLimit); 185 186 // Check only for the response header 187 parser->skip(true); 188 189 // Receive the HTTP response 190 boost::beast::http::async_read( 191 conn, buffer, *parser, 192 [self(shared_from_this())](const boost::beast::error_code& ec, 193 const std::size_t& bytesTransferred) { 194 if (ec) 195 { 196 BMCWEB_LOG_ERROR << "recvMessage() failed: " 197 << ec.message(); 198 self->state = ConnState::recvFailed; 199 self->handleConnState(); 200 return; 201 } 202 BMCWEB_LOG_DEBUG << "recvMessage() bytes transferred: " 203 << bytesTransferred; 204 BMCWEB_LOG_DEBUG << "recvMessage() data: " 205 << self->parser->get(); 206 207 unsigned int respCode = self->parser->get().result_int(); 208 BMCWEB_LOG_DEBUG << "recvMessage() Header Response Code: " 209 << respCode; 210 211 // 2XX response is considered to be successful 212 if ((respCode < 200) || (respCode >= 300)) 213 { 214 // The listener failed to receive the Sent-Event 215 BMCWEB_LOG_ERROR << "recvMessage() Listener Failed to " 216 "receive Sent-Event"; 217 self->state = ConnState::recvFailed; 218 self->handleConnState(); 219 return; 220 } 221 222 // Send is successful, Lets remove data from queue 223 // check for next request data in queue. 224 if (!self->requestDataQueue.empty()) 225 { 226 self->requestDataQueue.pop_front(); 227 } 228 self->state = ConnState::idle; 229 230 // Keep the connection alive if server supports it 231 // Else close the connection 232 BMCWEB_LOG_DEBUG << "recvMessage() keepalive : " 233 << self->parser->keep_alive(); 234 if (!self->parser->keep_alive()) 235 { 236 // Abort the connection since server is not keep-alive 237 // enabled 238 self->state = ConnState::abortConnection; 239 } 240 241 self->handleConnState(); 242 }); 243 } 244 245 void doClose() 246 { 247 state = ConnState::closeInProgress; 248 boost::beast::error_code ec; 249 conn.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); 250 conn.close(); 251 252 // not_connected happens sometimes so don't bother reporting it. 253 if (ec && ec != boost::beast::errc::not_connected) 254 { 255 BMCWEB_LOG_ERROR << "shutdown failed: " << ec.message(); 256 return; 257 } 258 BMCWEB_LOG_DEBUG << "Connection closed gracefully"; 259 if ((state != ConnState::suspended) && (state != ConnState::terminated)) 260 { 261 state = ConnState::closed; 262 handleConnState(); 263 } 264 } 265 266 void waitAndRetry() 267 { 268 if (retryCount >= maxRetryAttempts) 269 { 270 BMCWEB_LOG_ERROR << "Maximum number of retries reached."; 271 272 // Clear queue. 273 while (!requestDataQueue.empty()) 274 { 275 requestDataQueue.pop_front(); 276 } 277 278 BMCWEB_LOG_DEBUG << "Retry policy: " << retryPolicyAction; 279 if (retryPolicyAction == "TerminateAfterRetries") 280 { 281 // TODO: delete subscription 282 state = ConnState::terminated; 283 } 284 if (retryPolicyAction == "SuspendRetries") 285 { 286 state = ConnState::suspended; 287 } 288 // Reset the retrycount to zero so that client can try connecting 289 // again if needed 290 retryCount = 0; 291 handleConnState(); 292 return; 293 } 294 295 if (runningTimer) 296 { 297 BMCWEB_LOG_DEBUG << "Retry timer is already running."; 298 return; 299 } 300 runningTimer = true; 301 302 retryCount++; 303 304 BMCWEB_LOG_DEBUG << "Attempt retry after " << retryIntervalSecs 305 << " seconds. RetryCount = " << retryCount; 306 timer.expires_after(std::chrono::seconds(retryIntervalSecs)); 307 timer.async_wait( 308 [self = shared_from_this()](const boost::system::error_code ec) { 309 if (ec == boost::asio::error::operation_aborted) 310 { 311 BMCWEB_LOG_DEBUG 312 << "async_wait failed since the operation is aborted" 313 << ec.message(); 314 } 315 else if (ec) 316 { 317 BMCWEB_LOG_ERROR << "async_wait failed: " << ec.message(); 318 // Ignore the error and continue the retry loop to attempt 319 // sending the event as per the retry policy 320 } 321 self->runningTimer = false; 322 323 // Lets close connection and start from resolve. 324 self->doClose(); 325 }); 326 return; 327 } 328 329 void handleConnState() 330 { 331 switch (state) 332 { 333 case ConnState::resolveInProgress: 334 case ConnState::connectInProgress: 335 case ConnState::sendInProgress: 336 case ConnState::recvInProgress: 337 case ConnState::closeInProgress: 338 { 339 BMCWEB_LOG_DEBUG << "Async operation is already in progress"; 340 break; 341 } 342 case ConnState::initialized: 343 case ConnState::closed: 344 { 345 if (requestDataQueue.empty()) 346 { 347 BMCWEB_LOG_DEBUG << "requestDataQueue is empty"; 348 return; 349 } 350 doResolve(); 351 break; 352 } 353 case ConnState::suspended: 354 case ConnState::terminated: 355 { 356 doClose(); 357 break; 358 } 359 case ConnState::resolveFailed: 360 case ConnState::connectFailed: 361 case ConnState::sendFailed: 362 case ConnState::recvFailed: 363 case ConnState::retry: 364 { 365 // In case of failures during connect and handshake 366 // the retry policy will be applied 367 waitAndRetry(); 368 break; 369 } 370 case ConnState::connected: 371 case ConnState::idle: 372 { 373 // State idle means, previous attempt is successful 374 // State connected means, client connection is established 375 // successfully 376 if (requestDataQueue.empty()) 377 { 378 BMCWEB_LOG_DEBUG << "requestDataQueue is empty"; 379 return; 380 } 381 std::string data = requestDataQueue.front(); 382 sendMessage(data); 383 break; 384 } 385 case ConnState::abortConnection: 386 { 387 // Server did not want to keep alive the session 388 doClose(); 389 break; 390 } 391 default: 392 break; 393 } 394 } 395 396 public: 397 explicit HttpClient(boost::asio::io_context& ioc, const std::string& id, 398 const std::string& destIP, const std::string& destPort, 399 const std::string& destUri) : 400 conn(ioc), 401 timer(ioc), subId(id), host(destIP), port(destPort), uri(destUri), 402 retryCount(0), maxRetryAttempts(5), retryIntervalSecs(0), 403 retryPolicyAction("TerminateAfterRetries"), runningTimer(false) 404 { 405 state = ConnState::initialized; 406 } 407 408 void sendData(const std::string& data) 409 { 410 if ((state == ConnState::suspended) || (state == ConnState::terminated)) 411 { 412 return; 413 } 414 415 if (requestDataQueue.size() <= maxRequestQueueSize) 416 { 417 requestDataQueue.push_back(data); 418 handleConnState(); 419 } 420 else 421 { 422 BMCWEB_LOG_ERROR << "Request queue is full. So ignoring data."; 423 } 424 425 return; 426 } 427 428 void setHeaders( 429 const std::vector<std::pair<std::string, std::string>>& httpHeaders) 430 { 431 headers = httpHeaders; 432 } 433 434 void setRetryConfig(const uint32_t retryAttempts, 435 const uint32_t retryTimeoutInterval) 436 { 437 maxRetryAttempts = retryAttempts; 438 retryIntervalSecs = retryTimeoutInterval; 439 } 440 441 void setRetryPolicy(const std::string& retryPolicy) 442 { 443 retryPolicyAction = retryPolicy; 444 } 445 }; 446 447 } // namespace crow 448