xref: /openbmc/bmcweb/http/http_client.hpp (revision a778c026)
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