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