xref: /openbmc/bmcweb/http/http_client.hpp (revision 9d424669)
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     std::vector<std::pair<std::string, std::string>> headers;
72     boost::circular_buffer_space_optimized<std::string> requestDataQueue{};
73 
74     ConnState state;
75     std::string subId;
76     std::string host;
77     std::string port;
78     std::string uri;
79     uint32_t retryCount;
80     uint32_t maxRetryAttempts;
81     uint32_t retryIntervalSecs;
82     std::string retryPolicyAction;
83     bool runningTimer;
84 
85     void doResolve()
86     {
87         state = ConnState::resolveInProgress;
88         BMCWEB_LOG_DEBUG << "Trying to resolve: " << host << ":" << port;
89 
90         auto respHandler =
91             [self(shared_from_this())](
92                 const boost::beast::error_code ec,
93                 const std::vector<boost::asio::ip::tcp::endpoint>&
94                     endpointList) {
95                 if (ec || (endpointList.size() == 0))
96                 {
97                     BMCWEB_LOG_ERROR << "Resolve failed: " << ec.message();
98                     self->state = ConnState::resolveFailed;
99                     self->handleConnState();
100                     return;
101                 }
102                 BMCWEB_LOG_DEBUG << "Resolved";
103                 self->doConnect(endpointList);
104             };
105         resolver.asyncResolve(host, port, std::move(respHandler));
106     }
107 
108     void doConnect(
109         const std::vector<boost::asio::ip::tcp::endpoint>& endpointList)
110     {
111         state = ConnState::connectInProgress;
112 
113         BMCWEB_LOG_DEBUG << "Trying to connect to: " << host << ":" << port;
114 
115         conn.expires_after(std::chrono::seconds(30));
116         conn.async_connect(
117             endpointList, [self(shared_from_this())](
118                               const boost::beast::error_code ec,
119                               const boost::asio::ip::tcp::endpoint& endpoint) {
120                 if (ec)
121                 {
122                     BMCWEB_LOG_ERROR << "Connect " << endpoint
123                                      << " failed: " << ec.message();
124                     self->state = ConnState::connectFailed;
125                     self->handleConnState();
126                     return;
127                 }
128                 BMCWEB_LOG_DEBUG << "Connected to: " << endpoint;
129                 self->state = ConnState::connected;
130                 self->handleConnState();
131             });
132     }
133 
134     void sendMessage(const std::string& data)
135     {
136         state = ConnState::sendInProgress;
137 
138         BMCWEB_LOG_DEBUG << __FUNCTION__ << "(): " << host << ":" << port;
139 
140         req.version(static_cast<int>(11)); // HTTP 1.1
141         req.target(uri);
142         req.method(boost::beast::http::verb::post);
143 
144         // Set headers
145         for (const auto& [key, value] : headers)
146         {
147             req.set(key, value);
148         }
149         req.set(boost::beast::http::field::host, host);
150         req.keep_alive(true);
151 
152         req.body() = data;
153         req.prepare_payload();
154 
155         // Set a timeout on the operation
156         conn.expires_after(std::chrono::seconds(30));
157 
158         // Send the HTTP request to the remote host
159         boost::beast::http::async_write(
160             conn, req,
161             [self(shared_from_this())](const boost::beast::error_code& ec,
162                                        const std::size_t& bytesTransferred) {
163                 if (ec)
164                 {
165                     BMCWEB_LOG_ERROR << "sendMessage() failed: "
166                                      << ec.message();
167                     self->state = ConnState::sendFailed;
168                     self->handleConnState();
169                     return;
170                 }
171                 BMCWEB_LOG_DEBUG << "sendMessage() bytes transferred: "
172                                  << bytesTransferred;
173                 boost::ignore_unused(bytesTransferred);
174 
175                 self->recvMessage();
176             });
177     }
178 
179     void recvMessage()
180     {
181         state = ConnState::recvInProgress;
182 
183         parser.emplace(std::piecewise_construct, std::make_tuple());
184         parser->body_limit(httpReadBodyLimit);
185 
186         // Check only for the response header
187         parser->skip(true);
188 
189         // Receive the HTTP response
190         boost::beast::http::async_read(
191             conn, buffer, *parser,
192             [self(shared_from_this())](const boost::beast::error_code& ec,
193                                        const std::size_t& bytesTransferred) {
194                 if (ec)
195                 {
196                     BMCWEB_LOG_ERROR << "recvMessage() failed: "
197                                      << ec.message();
198                     self->state = ConnState::recvFailed;
199                     self->handleConnState();
200                     return;
201                 }
202                 BMCWEB_LOG_DEBUG << "recvMessage() bytes transferred: "
203                                  << bytesTransferred;
204                 BMCWEB_LOG_DEBUG << "recvMessage() data: "
205                                  << self->parser->get();
206 
207                 unsigned int respCode = self->parser->get().result_int();
208                 BMCWEB_LOG_DEBUG << "recvMessage() Header Response Code: "
209                                  << respCode;
210 
211                 // 2XX response is considered to be successful
212                 if ((respCode < 200) || (respCode >= 300))
213                 {
214                     // The listener failed to receive the Sent-Event
215                     BMCWEB_LOG_ERROR << "recvMessage() Listener Failed to "
216                                         "receive Sent-Event";
217                     self->state = ConnState::recvFailed;
218                     self->handleConnState();
219                     return;
220                 }
221 
222                 // Send is successful, Lets remove data from queue
223                 // check for next request data in queue.
224                 if (!self->requestDataQueue.empty())
225                 {
226                     self->requestDataQueue.pop_front();
227                 }
228                 self->state = ConnState::idle;
229 
230                 // Keep the connection alive if server supports it
231                 // Else close the connection
232                 BMCWEB_LOG_DEBUG << "recvMessage() keepalive : "
233                                  << self->parser->keep_alive();
234                 if (!self->parser->keep_alive())
235                 {
236                     // Abort the connection since server is not keep-alive
237                     // enabled
238                     self->state = ConnState::abortConnection;
239                 }
240 
241                 self->handleConnState();
242             });
243     }
244 
245     void doClose()
246     {
247         state = ConnState::closeInProgress;
248         boost::beast::error_code ec;
249         conn.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
250         conn.close();
251 
252         // not_connected happens sometimes so don't bother reporting it.
253         if (ec && ec != boost::beast::errc::not_connected)
254         {
255             BMCWEB_LOG_ERROR << "shutdown failed: " << ec.message();
256             return;
257         }
258         BMCWEB_LOG_DEBUG << "Connection closed gracefully";
259         if ((state != ConnState::suspended) && (state != ConnState::terminated))
260         {
261             state = ConnState::closed;
262             handleConnState();
263         }
264     }
265 
266     void waitAndRetry()
267     {
268         if (retryCount >= maxRetryAttempts)
269         {
270             BMCWEB_LOG_ERROR << "Maximum number of retries reached.";
271 
272             // Clear queue.
273             while (!requestDataQueue.empty())
274             {
275                 requestDataQueue.pop_front();
276             }
277 
278             BMCWEB_LOG_DEBUG << "Retry policy: " << retryPolicyAction;
279             if (retryPolicyAction == "TerminateAfterRetries")
280             {
281                 // TODO: delete subscription
282                 state = ConnState::terminated;
283             }
284             if (retryPolicyAction == "SuspendRetries")
285             {
286                 state = ConnState::suspended;
287             }
288             // Reset the retrycount to zero so that client can try connecting
289             // again if needed
290             retryCount = 0;
291             handleConnState();
292             return;
293         }
294 
295         if (runningTimer)
296         {
297             BMCWEB_LOG_DEBUG << "Retry timer is already running.";
298             return;
299         }
300         runningTimer = true;
301 
302         retryCount++;
303 
304         BMCWEB_LOG_DEBUG << "Attempt retry after " << retryIntervalSecs
305                          << " seconds. RetryCount = " << retryCount;
306         timer.expires_after(std::chrono::seconds(retryIntervalSecs));
307         timer.async_wait(
308             [self = shared_from_this()](const boost::system::error_code ec) {
309                 if (ec == boost::asio::error::operation_aborted)
310                 {
311                     BMCWEB_LOG_DEBUG
312                         << "async_wait failed since the operation is aborted"
313                         << ec.message();
314                 }
315                 else if (ec)
316                 {
317                     BMCWEB_LOG_ERROR << "async_wait failed: " << ec.message();
318                     // Ignore the error and continue the retry loop to attempt
319                     // sending the event as per the retry policy
320                 }
321                 self->runningTimer = false;
322 
323                 // Lets close connection and start from resolve.
324                 self->doClose();
325             });
326         return;
327     }
328 
329     void handleConnState()
330     {
331         switch (state)
332         {
333             case ConnState::resolveInProgress:
334             case ConnState::connectInProgress:
335             case ConnState::sendInProgress:
336             case ConnState::recvInProgress:
337             case ConnState::closeInProgress:
338             {
339                 BMCWEB_LOG_DEBUG << "Async operation is already in progress";
340                 break;
341             }
342             case ConnState::initialized:
343             case ConnState::closed:
344             {
345                 if (requestDataQueue.empty())
346                 {
347                     BMCWEB_LOG_DEBUG << "requestDataQueue is empty";
348                     return;
349                 }
350                 doResolve();
351                 break;
352             }
353             case ConnState::suspended:
354             case ConnState::terminated:
355             {
356                 doClose();
357                 break;
358             }
359             case ConnState::resolveFailed:
360             case ConnState::connectFailed:
361             case ConnState::sendFailed:
362             case ConnState::recvFailed:
363             case ConnState::retry:
364             {
365                 // In case of failures during connect and handshake
366                 // the retry policy will be applied
367                 waitAndRetry();
368                 break;
369             }
370             case ConnState::connected:
371             case ConnState::idle:
372             {
373                 // State idle means, previous attempt is successful
374                 // State connected means, client connection is established
375                 // successfully
376                 if (requestDataQueue.empty())
377                 {
378                     BMCWEB_LOG_DEBUG << "requestDataQueue is empty";
379                     return;
380                 }
381                 std::string data = requestDataQueue.front();
382                 sendMessage(data);
383                 break;
384             }
385             case ConnState::abortConnection:
386             {
387                 // Server did not want to keep alive the session
388                 doClose();
389                 break;
390             }
391             default:
392                 break;
393         }
394     }
395 
396   public:
397     explicit HttpClient(boost::asio::io_context& ioc, const std::string& id,
398                         const std::string& destIP, const std::string& destPort,
399                         const std::string& destUri) :
400         conn(ioc),
401         timer(ioc), subId(id), host(destIP), port(destPort), uri(destUri),
402         retryCount(0), maxRetryAttempts(5), retryIntervalSecs(0),
403         retryPolicyAction("TerminateAfterRetries"), runningTimer(false)
404     {
405         state = ConnState::initialized;
406     }
407 
408     void sendData(const std::string& data)
409     {
410         if ((state == ConnState::suspended) || (state == ConnState::terminated))
411         {
412             return;
413         }
414 
415         if (requestDataQueue.size() <= maxRequestQueueSize)
416         {
417             requestDataQueue.push_back(data);
418             handleConnState();
419         }
420         else
421         {
422             BMCWEB_LOG_ERROR << "Request queue is full. So ignoring data.";
423         }
424 
425         return;
426     }
427 
428     void setHeaders(
429         const std::vector<std::pair<std::string, std::string>>& httpHeaders)
430     {
431         headers = httpHeaders;
432     }
433 
434     void setRetryConfig(const uint32_t retryAttempts,
435                         const uint32_t retryTimeoutInterval)
436     {
437         maxRetryAttempts = retryAttempts;
438         retryIntervalSecs = retryTimeoutInterval;
439     }
440 
441     void setRetryPolicy(const std::string& retryPolicy)
442     {
443         retryPolicyAction = retryPolicy;
444     }
445 };
446 
447 } // namespace crow
448