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