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 << "recvMessage() Listener Failed to " 201 "receive Sent-Event"; 202 self->state = ConnState::recvFailed; 203 self->handleConnState(); 204 return; 205 } 206 207 // Send is successful, Lets remove data from queue 208 // check for next request data in queue. 209 if (!self->requestDataQueue.empty()) 210 { 211 self->requestDataQueue.pop_front(); 212 } 213 self->state = ConnState::idle; 214 215 // Keep the connection alive if server supports it 216 // Else close the connection 217 BMCWEB_LOG_DEBUG << "recvMessage() keepalive : " 218 << self->parser->keep_alive(); 219 if (!self->parser->keep_alive()) 220 { 221 // Abort the connection since server is not keep-alive 222 // enabled 223 self->state = ConnState::abortConnection; 224 } 225 226 self->handleConnState(); 227 }); 228 } 229 230 void doClose() 231 { 232 state = ConnState::closeInProgress; 233 boost::beast::error_code ec; 234 conn.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); 235 conn.close(); 236 237 // not_connected happens sometimes so don't bother reporting it. 238 if (ec && ec != boost::beast::errc::not_connected) 239 { 240 BMCWEB_LOG_ERROR << "shutdown failed: " << ec.message(); 241 return; 242 } 243 BMCWEB_LOG_DEBUG << "Connection closed gracefully"; 244 if ((state != ConnState::suspended) && (state != ConnState::terminated)) 245 { 246 state = ConnState::closed; 247 handleConnState(); 248 } 249 } 250 251 void waitAndRetry() 252 { 253 if (retryCount >= maxRetryAttempts) 254 { 255 BMCWEB_LOG_ERROR << "Maximum number of retries reached."; 256 257 // Clear queue. 258 while (!requestDataQueue.empty()) 259 { 260 requestDataQueue.pop_front(); 261 } 262 263 BMCWEB_LOG_DEBUG << "Retry policy: " << retryPolicyAction; 264 if (retryPolicyAction == "TerminateAfterRetries") 265 { 266 // TODO: delete subscription 267 state = ConnState::terminated; 268 } 269 if (retryPolicyAction == "SuspendRetries") 270 { 271 state = ConnState::suspended; 272 } 273 // Reset the retrycount to zero so that client can try connecting 274 // again if needed 275 retryCount = 0; 276 handleConnState(); 277 return; 278 } 279 280 if (runningTimer) 281 { 282 BMCWEB_LOG_DEBUG << "Retry timer is already running."; 283 return; 284 } 285 runningTimer = true; 286 287 retryCount++; 288 289 BMCWEB_LOG_DEBUG << "Attempt retry after " << retryIntervalSecs 290 << " seconds. RetryCount = " << retryCount; 291 timer.expires_after(std::chrono::seconds(retryIntervalSecs)); 292 timer.async_wait( 293 [self = shared_from_this()](const boost::system::error_code ec) { 294 if (ec == boost::asio::error::operation_aborted) 295 { 296 BMCWEB_LOG_DEBUG 297 << "async_wait failed since the operation is aborted" 298 << ec.message(); 299 } 300 else if (ec) 301 { 302 BMCWEB_LOG_ERROR << "async_wait failed: " << ec.message(); 303 // Ignore the error and continue the retry loop to attempt 304 // sending the event as per the retry policy 305 } 306 self->runningTimer = false; 307 308 // Lets close connection and start from resolve. 309 self->doClose(); 310 }); 311 return; 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 default: 377 break; 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