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