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