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/strand.hpp> 18 #include <boost/beast/core.hpp> 19 #include <boost/beast/http.hpp> 20 #include <boost/beast/version.hpp> 21 #include <cstdlib> 22 #include <functional> 23 #include <iostream> 24 #include <memory> 25 #include <queue> 26 #include <string> 27 28 namespace crow 29 { 30 31 static constexpr uint8_t maxRequestQueueSize = 50; 32 33 enum class ConnState 34 { 35 initialized, 36 connectInProgress, 37 connectFailed, 38 connected, 39 sendInProgress, 40 sendFailed, 41 recvFailed, 42 idle, 43 suspended, 44 closed 45 }; 46 47 class HttpClient : public std::enable_shared_from_this<HttpClient> 48 { 49 private: 50 boost::beast::tcp_stream conn; 51 boost::beast::flat_buffer buffer; 52 boost::beast::http::request<boost::beast::http::string_body> req; 53 boost::beast::http::response<boost::beast::http::string_body> res; 54 boost::asio::ip::tcp::resolver::results_type endpoint; 55 std::vector<std::pair<std::string, std::string>> headers; 56 std::queue<std::string> requestDataQueue; 57 ConnState state; 58 std::string host; 59 std::string port; 60 std::string uri; 61 int retryCount; 62 int maxRetryAttempts; 63 64 void doConnect() 65 { 66 if (state == ConnState::connectInProgress) 67 { 68 return; 69 } 70 state = ConnState::connectInProgress; 71 72 BMCWEB_LOG_DEBUG << "Trying to connect to: " << host << ":" << port; 73 // Set a timeout on the operation 74 conn.expires_after(std::chrono::seconds(30)); 75 conn.async_connect(endpoint, [self(shared_from_this())]( 76 const boost::beast::error_code& ec, 77 const boost::asio::ip::tcp::resolver:: 78 results_type::endpoint_type& ep) { 79 if (ec) 80 { 81 BMCWEB_LOG_ERROR << "Connect " << ep 82 << " failed: " << ec.message(); 83 self->state = ConnState::connectFailed; 84 self->checkQueue(); 85 return; 86 } 87 self->state = ConnState::connected; 88 BMCWEB_LOG_DEBUG << "Connected to: " << ep; 89 90 self->checkQueue(); 91 }); 92 } 93 94 void sendMessage(const std::string& data) 95 { 96 if (state == ConnState::sendInProgress) 97 { 98 return; 99 } 100 state = ConnState::sendInProgress; 101 102 BMCWEB_LOG_DEBUG << __FUNCTION__ << "(): " << host << ":" << port; 103 104 req.version(static_cast<int>(11)); // HTTP 1.1 105 req.target(uri); 106 req.method(boost::beast::http::verb::post); 107 108 // Set headers 109 for (const auto& [key, value] : headers) 110 { 111 req.set(key, value); 112 } 113 req.set(boost::beast::http::field::host, host); 114 req.keep_alive(true); 115 116 req.body() = data; 117 req.prepare_payload(); 118 119 // Set a timeout on the operation 120 conn.expires_after(std::chrono::seconds(30)); 121 122 // Send the HTTP request to the remote host 123 boost::beast::http::async_write( 124 conn, req, 125 [self(shared_from_this())](const boost::beast::error_code& ec, 126 const std::size_t& bytesTransferred) { 127 if (ec) 128 { 129 BMCWEB_LOG_ERROR << "sendMessage() failed: " 130 << ec.message(); 131 self->state = ConnState::sendFailed; 132 self->checkQueue(); 133 return; 134 } 135 BMCWEB_LOG_DEBUG << "sendMessage() bytes transferred: " 136 << bytesTransferred; 137 boost::ignore_unused(bytesTransferred); 138 139 self->recvMessage(); 140 }); 141 } 142 143 void recvMessage() 144 { 145 // Receive the HTTP response 146 boost::beast::http::async_read( 147 conn, buffer, res, 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 << "recvMessage() failed: " 153 << ec.message(); 154 self->state = ConnState::recvFailed; 155 self->checkQueue(); 156 return; 157 } 158 BMCWEB_LOG_DEBUG << "recvMessage() bytes transferred: " 159 << bytesTransferred; 160 boost::ignore_unused(bytesTransferred); 161 162 // Discard received data. We are not interested. 163 BMCWEB_LOG_DEBUG << "recvMessage() data: " << self->res; 164 165 // Send is successful, Lets remove data from queue 166 // check for next request data in queue. 167 self->requestDataQueue.pop(); 168 self->state = ConnState::idle; 169 self->checkQueue(); 170 }); 171 } 172 173 void doClose() 174 { 175 boost::beast::error_code ec; 176 conn.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); 177 178 state = ConnState::closed; 179 // not_connected happens sometimes so don't bother reporting it. 180 if (ec && ec != boost::beast::errc::not_connected) 181 { 182 BMCWEB_LOG_ERROR << "shutdown failed: " << ec.message(); 183 return; 184 } 185 BMCWEB_LOG_DEBUG << "Connection closed gracefully"; 186 } 187 188 void checkQueue(const bool newRecord = false) 189 { 190 if (requestDataQueue.empty()) 191 { 192 // TODO: Having issue in keeping connection alive. So lets close if 193 // nothing to be trasferred. 194 doClose(); 195 196 BMCWEB_LOG_DEBUG << "requestDataQueue is empty\n"; 197 return; 198 } 199 200 if (retryCount >= maxRetryAttempts) 201 { 202 BMCWEB_LOG_ERROR << "Maximum number of retries is reached."; 203 204 // Clear queue. 205 while (!requestDataQueue.empty()) 206 { 207 requestDataQueue.pop(); 208 } 209 210 // TODO: Take 'DeliveryRetryPolicy' action. 211 // For now, doing 'SuspendRetries' action. 212 state = ConnState::suspended; 213 return; 214 } 215 216 if ((state == ConnState::connectFailed) || 217 (state == ConnState::sendFailed) || 218 (state == ConnState::recvFailed)) 219 { 220 if (newRecord) 221 { 222 // We are already running async wait and retry. 223 // Since record is added to queue, it gets the 224 // turn in FIFO. 225 return; 226 } 227 228 retryCount++; 229 // TODO: Perform async wait for retryTimeoutInterval before proceed. 230 } 231 else 232 { 233 // reset retry count. 234 retryCount = 0; 235 } 236 237 switch (state) 238 { 239 case ConnState::connectInProgress: 240 case ConnState::sendInProgress: 241 case ConnState::suspended: 242 // do nothing 243 break; 244 case ConnState::initialized: 245 case ConnState::closed: 246 case ConnState::connectFailed: 247 case ConnState::sendFailed: 248 case ConnState::recvFailed: 249 { 250 // After establishing the connection, checkQueue() will 251 // get called and it will attempt to send data. 252 doConnect(); 253 break; 254 } 255 case ConnState::connected: 256 case ConnState::idle: 257 { 258 std::string data = requestDataQueue.front(); 259 sendMessage(data); 260 break; 261 } 262 default: 263 break; 264 } 265 266 return; 267 } 268 269 public: 270 explicit HttpClient(boost::asio::io_context& ioc, const std::string& destIP, 271 const std::string& destPort, 272 const std::string& destUri) : 273 conn(ioc), 274 host(destIP), port(destPort), uri(destUri) 275 { 276 boost::asio::ip::tcp::resolver resolver(ioc); 277 endpoint = resolver.resolve(host, port); 278 state = ConnState::initialized; 279 retryCount = 0; 280 maxRetryAttempts = 5; 281 } 282 283 void sendData(const std::string& data) 284 { 285 if (state == ConnState::suspended) 286 { 287 return; 288 } 289 290 if (requestDataQueue.size() <= maxRequestQueueSize) 291 { 292 requestDataQueue.push(data); 293 checkQueue(true); 294 } 295 else 296 { 297 BMCWEB_LOG_ERROR << "Request queue is full. So ignoring data."; 298 } 299 300 return; 301 } 302 303 void setHeaders( 304 const std::vector<std::pair<std::string, std::string>>& httpHeaders) 305 { 306 headers = httpHeaders; 307 } 308 }; 309 310 } // namespace crow 311