xref: /openbmc/bmcweb/http/http_client.hpp (revision 141d9431)
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 <boost/circular_buffer.hpp>
25 #include <include/async_resolve.hpp>
26 
27 #include <cstdlib>
28 #include <functional>
29 #include <iostream>
30 #include <memory>
31 #include <queue>
32 #include <string>
33 
34 namespace crow
35 {
36 
37 static constexpr uint8_t maxRequestQueueSize = 50;
38 static constexpr unsigned int httpReadBodyLimit = 8192;
39 
40 enum class ConnState
41 {
42     initialized,
43     resolveInProgress,
44     resolveFailed,
45     connectInProgress,
46     connectFailed,
47     connected,
48     sendInProgress,
49     sendFailed,
50     recvInProgress,
51     recvFailed,
52     idle,
53     closeInProgress,
54     closed,
55     suspended,
56     terminated,
57     abortConnection,
58     retry
59 };
60 
61 class HttpClient : public std::enable_shared_from_this<HttpClient>
62 {
63   private:
64     crow::async_resolve::Resolver resolver;
65     boost::beast::tcp_stream conn;
66     boost::asio::steady_timer timer;
67     boost::beast::flat_static_buffer<httpReadBodyLimit> buffer;
68     boost::beast::http::request<boost::beast::http::string_body> req;
69     std::optional<
70         boost::beast::http::response_parser<boost::beast::http::string_body>>
71         parser;
72     boost::circular_buffer_space_optimized<std::string> requestDataQueue{
73         maxRequestQueueSize};
74 
75     ConnState state = ConnState::initialized;
76 
77     std::string subId;
78     std::string host;
79     std::string port;
80     uint32_t retryCount = 0;
81     uint32_t maxRetryAttempts = 5;
82     uint32_t retryIntervalSecs = 0;
83     std::string retryPolicyAction = "TerminateAfterRetries";
84     bool runningTimer = false;
85 
86     void doResolve()
87     {
88         state = ConnState::resolveInProgress;
89         BMCWEB_LOG_DEBUG << "Trying to resolve: " << host << ":" << port;
90 
91         auto respHandler =
92             [self(shared_from_this())](
93                 const boost::beast::error_code ec,
94                 const std::vector<boost::asio::ip::tcp::endpoint>&
95                     endpointList) {
96                 if (ec || (endpointList.empty()))
97                 {
98                     BMCWEB_LOG_ERROR << "Resolve failed: " << ec.message();
99                     self->state = ConnState::resolveFailed;
100                     self->handleConnState();
101                     return;
102                 }
103                 BMCWEB_LOG_DEBUG << "Resolved";
104                 self->doConnect(endpointList);
105             };
106         resolver.asyncResolve(host, port, std::move(respHandler));
107     }
108 
109     void doConnect(
110         const std::vector<boost::asio::ip::tcp::endpoint>& endpointList)
111     {
112         state = ConnState::connectInProgress;
113 
114         BMCWEB_LOG_DEBUG << "Trying to connect to: " << host << ":" << port;
115 
116         conn.expires_after(std::chrono::seconds(30));
117         conn.async_connect(
118             endpointList, [self(shared_from_this())](
119                               const boost::beast::error_code ec,
120                               const boost::asio::ip::tcp::endpoint& endpoint) {
121                 if (ec)
122                 {
123                     BMCWEB_LOG_ERROR << "Connect " << endpoint
124                                      << " failed: " << ec.message();
125                     self->state = ConnState::connectFailed;
126                     self->handleConnState();
127                     return;
128                 }
129                 BMCWEB_LOG_DEBUG << "Connected to: " << endpoint;
130                 self->state = ConnState::connected;
131                 self->handleConnState();
132             });
133     }
134 
135     void sendMessage(const std::string& data)
136     {
137         state = ConnState::sendInProgress;
138 
139         req.body() = data;
140         req.prepare_payload();
141 
142         // Set a timeout on the operation
143         conn.expires_after(std::chrono::seconds(30));
144 
145         // Send the HTTP request to the remote host
146         boost::beast::http::async_write(
147             conn, req,
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 << "sendMessage() failed: "
153                                      << ec.message();
154                     self->state = ConnState::sendFailed;
155                     self->handleConnState();
156                     return;
157                 }
158                 BMCWEB_LOG_DEBUG << "sendMessage() bytes transferred: "
159                                  << bytesTransferred;
160                 boost::ignore_unused(bytesTransferred);
161 
162                 self->recvMessage();
163             });
164     }
165 
166     void recvMessage()
167     {
168         state = ConnState::recvInProgress;
169 
170         parser.emplace(std::piecewise_construct, std::make_tuple());
171         parser->body_limit(httpReadBodyLimit);
172 
173         // Receive the HTTP response
174         boost::beast::http::async_read(
175             conn, buffer, *parser,
176             [self(shared_from_this())](const boost::beast::error_code& ec,
177                                        const std::size_t& bytesTransferred) {
178                 if (ec)
179                 {
180                     BMCWEB_LOG_ERROR << "recvMessage() failed: "
181                                      << ec.message();
182                     self->state = ConnState::recvFailed;
183                     self->handleConnState();
184                     return;
185                 }
186                 BMCWEB_LOG_DEBUG << "recvMessage() bytes transferred: "
187                                  << bytesTransferred;
188                 BMCWEB_LOG_DEBUG << "recvMessage() data: "
189                                  << self->parser->get();
190 
191                 unsigned int respCode = self->parser->get().result_int();
192                 BMCWEB_LOG_DEBUG << "recvMessage() Header Response Code: "
193                                  << respCode;
194 
195                 // 2XX response is considered to be successful
196                 if ((respCode < 200) || (respCode >= 300))
197                 {
198                     // The listener failed to receive the Sent-Event
199                     BMCWEB_LOG_ERROR
200                         << "recvMessage() Listener Failed to "
201                            "receive Sent-Event. Header Response Code: "
202                         << respCode;
203                     self->state = ConnState::recvFailed;
204                     self->handleConnState();
205                     return;
206                 }
207 
208                 // Send is successful, Lets remove data from queue
209                 // check for next request data in queue.
210                 if (!self->requestDataQueue.empty())
211                 {
212                     self->requestDataQueue.pop_front();
213                 }
214                 self->state = ConnState::idle;
215 
216                 // Keep the connection alive if server supports it
217                 // Else close the connection
218                 BMCWEB_LOG_DEBUG << "recvMessage() keepalive : "
219                                  << self->parser->keep_alive();
220                 if (!self->parser->keep_alive())
221                 {
222                     // Abort the connection since server is not keep-alive
223                     // enabled
224                     self->state = ConnState::abortConnection;
225                 }
226 
227                 self->handleConnState();
228             });
229     }
230 
231     void doClose()
232     {
233         state = ConnState::closeInProgress;
234         boost::beast::error_code ec;
235         conn.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
236         conn.close();
237 
238         // not_connected happens sometimes so don't bother reporting it.
239         if (ec && ec != boost::beast::errc::not_connected)
240         {
241             BMCWEB_LOG_ERROR << "shutdown failed: " << ec.message();
242             return;
243         }
244         BMCWEB_LOG_DEBUG << "Connection closed gracefully";
245         if ((state != ConnState::suspended) && (state != ConnState::terminated))
246         {
247             state = ConnState::closed;
248             handleConnState();
249         }
250     }
251 
252     void waitAndRetry()
253     {
254         if (retryCount >= maxRetryAttempts)
255         {
256             BMCWEB_LOG_ERROR << "Maximum number of retries reached.";
257 
258             // Clear queue.
259             while (!requestDataQueue.empty())
260             {
261                 requestDataQueue.pop_front();
262             }
263 
264             BMCWEB_LOG_DEBUG << "Retry policy: " << retryPolicyAction;
265             if (retryPolicyAction == "TerminateAfterRetries")
266             {
267                 // TODO: delete subscription
268                 state = ConnState::terminated;
269             }
270             if (retryPolicyAction == "SuspendRetries")
271             {
272                 state = ConnState::suspended;
273             }
274             // Reset the retrycount to zero so that client can try connecting
275             // again if needed
276             retryCount = 0;
277             handleConnState();
278             return;
279         }
280 
281         if (runningTimer)
282         {
283             BMCWEB_LOG_DEBUG << "Retry timer is already running.";
284             return;
285         }
286         runningTimer = true;
287 
288         retryCount++;
289 
290         BMCWEB_LOG_DEBUG << "Attempt retry after " << retryIntervalSecs
291                          << " seconds. RetryCount = " << retryCount;
292         timer.expires_after(std::chrono::seconds(retryIntervalSecs));
293         timer.async_wait(
294             [self = shared_from_this()](const boost::system::error_code ec) {
295                 if (ec == boost::asio::error::operation_aborted)
296                 {
297                     BMCWEB_LOG_DEBUG
298                         << "async_wait failed since the operation is aborted"
299                         << ec.message();
300                 }
301                 else if (ec)
302                 {
303                     BMCWEB_LOG_ERROR << "async_wait failed: " << ec.message();
304                     // Ignore the error and continue the retry loop to attempt
305                     // sending the event as per the retry policy
306                 }
307                 self->runningTimer = false;
308 
309                 // Lets close connection and start from resolve.
310                 self->doClose();
311             });
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         }
377     }
378 
379   public:
380     explicit HttpClient(boost::asio::io_context& ioc, const std::string& id,
381                         const std::string& destIP, const std::string& destPort,
382                         const std::string& destUri,
383                         const boost::beast::http::fields& httpHeader) :
384         conn(ioc),
385         timer(ioc),
386         req(boost::beast::http::verb::post, destUri, 11, "", httpHeader),
387         subId(id), host(destIP), port(destPort)
388     {
389         req.set(boost::beast::http::field::host, host);
390         req.keep_alive(true);
391     }
392 
393     void sendData(const std::string& data)
394     {
395         if ((state == ConnState::suspended) || (state == ConnState::terminated))
396         {
397             return;
398         }
399 
400         if (requestDataQueue.size() <= maxRequestQueueSize)
401         {
402             requestDataQueue.push_back(data);
403             handleConnState();
404         }
405         else
406         {
407             BMCWEB_LOG_ERROR << "Request queue is full. So ignoring data.";
408         }
409     }
410 
411     void setRetryConfig(const uint32_t retryAttempts,
412                         const uint32_t retryTimeoutInterval)
413     {
414         maxRetryAttempts = retryAttempts;
415         retryIntervalSecs = retryTimeoutInterval;
416     }
417 
418     void setRetryPolicy(const std::string& retryPolicy)
419     {
420         retryPolicyAction = retryPolicy;
421     }
422 };
423 
424 } // namespace crow
425