xref: /openbmc/bmcweb/http/http_client.hpp (revision b00dcc27)
1bd030d0aSAppaRao Puli /*
2bd030d0aSAppaRao Puli // Copyright (c) 2020 Intel Corporation
3bd030d0aSAppaRao Puli //
4bd030d0aSAppaRao Puli // Licensed under the Apache License, Version 2.0 (the "License");
5bd030d0aSAppaRao Puli // you may not use this file except in compliance with the License.
6bd030d0aSAppaRao Puli // You may obtain a copy of the License at
7bd030d0aSAppaRao Puli //
8bd030d0aSAppaRao Puli //      http://www.apache.org/licenses/LICENSE-2.0
9bd030d0aSAppaRao Puli //
10bd030d0aSAppaRao Puli // Unless required by applicable law or agreed to in writing, software
11bd030d0aSAppaRao Puli // distributed under the License is distributed on an "AS IS" BASIS,
12bd030d0aSAppaRao Puli // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13bd030d0aSAppaRao Puli // See the License for the specific language governing permissions and
14bd030d0aSAppaRao Puli // limitations under the License.
15bd030d0aSAppaRao Puli */
16bd030d0aSAppaRao Puli #pragma once
17d43cd0caSEd Tanous #include <boost/asio/steady_timer.hpp>
18d43cd0caSEd Tanous #include <boost/beast/core/flat_buffer.hpp>
19d43cd0caSEd Tanous #include <boost/beast/core/tcp_stream.hpp>
20d43cd0caSEd Tanous #include <boost/beast/http/message.hpp>
21bd030d0aSAppaRao Puli #include <boost/beast/version.hpp>
221214b7e7SGunnar Mills 
23bd030d0aSAppaRao Puli #include <cstdlib>
24bd030d0aSAppaRao Puli #include <functional>
25bd030d0aSAppaRao Puli #include <iostream>
26bd030d0aSAppaRao Puli #include <memory>
272a5689a7SAppaRao Puli #include <queue>
28bd030d0aSAppaRao Puli #include <string>
29bd030d0aSAppaRao Puli 
30bd030d0aSAppaRao Puli namespace crow
31bd030d0aSAppaRao Puli {
32bd030d0aSAppaRao Puli 
332a5689a7SAppaRao Puli static constexpr uint8_t maxRequestQueueSize = 50;
342a5689a7SAppaRao Puli 
35bd030d0aSAppaRao Puli enum class ConnState
36bd030d0aSAppaRao Puli {
372a5689a7SAppaRao Puli     initialized,
382a5689a7SAppaRao Puli     connectInProgress,
392a5689a7SAppaRao Puli     connectFailed,
40bd030d0aSAppaRao Puli     connected,
412a5689a7SAppaRao Puli     sendInProgress,
422a5689a7SAppaRao Puli     sendFailed,
432a5689a7SAppaRao Puli     recvFailed,
442a5689a7SAppaRao Puli     idle,
452a5689a7SAppaRao Puli     suspended,
46fe44eb0bSAyushi Smriti     closed,
47fe44eb0bSAyushi Smriti     terminated
48bd030d0aSAppaRao Puli };
49bd030d0aSAppaRao Puli 
50bd030d0aSAppaRao Puli class HttpClient : public std::enable_shared_from_this<HttpClient>
51bd030d0aSAppaRao Puli {
52bd030d0aSAppaRao Puli   private:
53bd030d0aSAppaRao Puli     boost::beast::tcp_stream conn;
54fe44eb0bSAyushi Smriti     boost::asio::steady_timer timer;
55bd030d0aSAppaRao Puli     boost::beast::flat_buffer buffer;
56bd030d0aSAppaRao Puli     boost::beast::http::request<boost::beast::http::string_body> req;
57bd030d0aSAppaRao Puli     boost::beast::http::response<boost::beast::http::string_body> res;
58bd030d0aSAppaRao Puli     boost::asio::ip::tcp::resolver::results_type endpoint;
59bd030d0aSAppaRao Puli     std::vector<std::pair<std::string, std::string>> headers;
602a5689a7SAppaRao Puli     std::queue<std::string> requestDataQueue;
61bd030d0aSAppaRao Puli     ConnState state;
62fe44eb0bSAyushi Smriti     std::string subId;
63bd030d0aSAppaRao Puli     std::string host;
64bd030d0aSAppaRao Puli     std::string port;
652a5689a7SAppaRao Puli     std::string uri;
66fe44eb0bSAyushi Smriti     uint32_t retryCount;
67fe44eb0bSAyushi Smriti     uint32_t maxRetryAttempts;
68fe44eb0bSAyushi Smriti     uint32_t retryIntervalSecs;
69fe44eb0bSAyushi Smriti     std::string retryPolicyAction;
70fe44eb0bSAyushi Smriti     bool runningTimer;
71bd030d0aSAppaRao Puli 
722a5689a7SAppaRao Puli     void doConnect()
73bd030d0aSAppaRao Puli     {
742a5689a7SAppaRao Puli         if (state == ConnState::connectInProgress)
75bd030d0aSAppaRao Puli         {
76bd030d0aSAppaRao Puli             return;
77bd030d0aSAppaRao Puli         }
782a5689a7SAppaRao Puli         state = ConnState::connectInProgress;
792a5689a7SAppaRao Puli 
802a5689a7SAppaRao Puli         BMCWEB_LOG_DEBUG << "Trying to connect to: " << host << ":" << port;
812a5689a7SAppaRao Puli         // Set a timeout on the operation
822a5689a7SAppaRao Puli         conn.expires_after(std::chrono::seconds(30));
83*b00dcc27SEd Tanous 
842a5689a7SAppaRao Puli         conn.async_connect(endpoint, [self(shared_from_this())](
852a5689a7SAppaRao Puli                                          const boost::beast::error_code& ec,
862a5689a7SAppaRao Puli                                          const boost::asio::ip::tcp::resolver::
872a5689a7SAppaRao Puli                                              results_type::endpoint_type& ep) {
882a5689a7SAppaRao Puli             if (ec)
892a5689a7SAppaRao Puli             {
902a5689a7SAppaRao Puli                 BMCWEB_LOG_ERROR << "Connect " << ep
912a5689a7SAppaRao Puli                                  << " failed: " << ec.message();
922a5689a7SAppaRao Puli                 self->state = ConnState::connectFailed;
932a5689a7SAppaRao Puli                 self->checkQueue();
942a5689a7SAppaRao Puli                 return;
952a5689a7SAppaRao Puli             }
962a5689a7SAppaRao Puli             self->state = ConnState::connected;
972a5689a7SAppaRao Puli             BMCWEB_LOG_DEBUG << "Connected to: " << ep;
982a5689a7SAppaRao Puli 
992a5689a7SAppaRao Puli             self->checkQueue();
1002a5689a7SAppaRao Puli         });
1012a5689a7SAppaRao Puli     }
1022a5689a7SAppaRao Puli 
1032a5689a7SAppaRao Puli     void sendMessage(const std::string& data)
1042a5689a7SAppaRao Puli     {
1052a5689a7SAppaRao Puli         if (state == ConnState::sendInProgress)
1062a5689a7SAppaRao Puli         {
1072a5689a7SAppaRao Puli             return;
1082a5689a7SAppaRao Puli         }
1092a5689a7SAppaRao Puli         state = ConnState::sendInProgress;
1102a5689a7SAppaRao Puli 
1112a5689a7SAppaRao Puli         BMCWEB_LOG_DEBUG << __FUNCTION__ << "(): " << host << ":" << port;
1122a5689a7SAppaRao Puli 
1132a5689a7SAppaRao Puli         req.version(static_cast<int>(11)); // HTTP 1.1
1142a5689a7SAppaRao Puli         req.target(uri);
1152a5689a7SAppaRao Puli         req.method(boost::beast::http::verb::post);
1162a5689a7SAppaRao Puli 
1172a5689a7SAppaRao Puli         // Set headers
1182a5689a7SAppaRao Puli         for (const auto& [key, value] : headers)
1192a5689a7SAppaRao Puli         {
1202a5689a7SAppaRao Puli             req.set(key, value);
1212a5689a7SAppaRao Puli         }
1222a5689a7SAppaRao Puli         req.set(boost::beast::http::field::host, host);
1232a5689a7SAppaRao Puli         req.keep_alive(true);
1242a5689a7SAppaRao Puli 
1252a5689a7SAppaRao Puli         req.body() = data;
1262a5689a7SAppaRao Puli         req.prepare_payload();
127bd030d0aSAppaRao Puli 
128bd030d0aSAppaRao Puli         // Set a timeout on the operation
129bd030d0aSAppaRao Puli         conn.expires_after(std::chrono::seconds(30));
130bd030d0aSAppaRao Puli 
131bd030d0aSAppaRao Puli         // Send the HTTP request to the remote host
132bd030d0aSAppaRao Puli         boost::beast::http::async_write(
133bd030d0aSAppaRao Puli             conn, req,
1342a5689a7SAppaRao Puli             [self(shared_from_this())](const boost::beast::error_code& ec,
135bd030d0aSAppaRao Puli                                        const std::size_t& bytesTransferred) {
136bd030d0aSAppaRao Puli                 if (ec)
137bd030d0aSAppaRao Puli                 {
138bd030d0aSAppaRao Puli                     BMCWEB_LOG_ERROR << "sendMessage() failed: "
139bd030d0aSAppaRao Puli                                      << ec.message();
1402a5689a7SAppaRao Puli                     self->state = ConnState::sendFailed;
1412a5689a7SAppaRao Puli                     self->checkQueue();
142bd030d0aSAppaRao Puli                     return;
143bd030d0aSAppaRao Puli                 }
144bd030d0aSAppaRao Puli                 BMCWEB_LOG_DEBUG << "sendMessage() bytes transferred: "
145bd030d0aSAppaRao Puli                                  << bytesTransferred;
146bd030d0aSAppaRao Puli                 boost::ignore_unused(bytesTransferred);
147bd030d0aSAppaRao Puli 
1482a5689a7SAppaRao Puli                 self->recvMessage();
149bd030d0aSAppaRao Puli             });
150bd030d0aSAppaRao Puli     }
151bd030d0aSAppaRao Puli 
152bd030d0aSAppaRao Puli     void recvMessage()
153bd030d0aSAppaRao Puli     {
154bd030d0aSAppaRao Puli         // Receive the HTTP response
155bd030d0aSAppaRao Puli         boost::beast::http::async_read(
156bd030d0aSAppaRao Puli             conn, buffer, res,
1572a5689a7SAppaRao Puli             [self(shared_from_this())](const boost::beast::error_code& ec,
158bd030d0aSAppaRao Puli                                        const std::size_t& bytesTransferred) {
159bd030d0aSAppaRao Puli                 if (ec)
160bd030d0aSAppaRao Puli                 {
161bd030d0aSAppaRao Puli                     BMCWEB_LOG_ERROR << "recvMessage() failed: "
162bd030d0aSAppaRao Puli                                      << ec.message();
1632a5689a7SAppaRao Puli                     self->state = ConnState::recvFailed;
1642a5689a7SAppaRao Puli                     self->checkQueue();
165bd030d0aSAppaRao Puli                     return;
166bd030d0aSAppaRao Puli                 }
167bd030d0aSAppaRao Puli                 BMCWEB_LOG_DEBUG << "recvMessage() bytes transferred: "
168bd030d0aSAppaRao Puli                                  << bytesTransferred;
169bd030d0aSAppaRao Puli                 boost::ignore_unused(bytesTransferred);
170bd030d0aSAppaRao Puli 
171bd030d0aSAppaRao Puli                 // Discard received data. We are not interested.
1722a5689a7SAppaRao Puli                 BMCWEB_LOG_DEBUG << "recvMessage() data: " << self->res;
173bd030d0aSAppaRao Puli 
1742a5689a7SAppaRao Puli                 // Send is successful, Lets remove data from queue
1752a5689a7SAppaRao Puli                 // check for next request data in queue.
1762a5689a7SAppaRao Puli                 self->requestDataQueue.pop();
1772a5689a7SAppaRao Puli                 self->state = ConnState::idle;
1782a5689a7SAppaRao Puli                 self->checkQueue();
179bd030d0aSAppaRao Puli             });
180bd030d0aSAppaRao Puli     }
181bd030d0aSAppaRao Puli 
182bd030d0aSAppaRao Puli     void doClose()
183bd030d0aSAppaRao Puli     {
184bd030d0aSAppaRao Puli         boost::beast::error_code ec;
185bd030d0aSAppaRao Puli         conn.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
186bd030d0aSAppaRao Puli 
187bd030d0aSAppaRao Puli         state = ConnState::closed;
188bd030d0aSAppaRao Puli         // not_connected happens sometimes so don't bother reporting it.
189bd030d0aSAppaRao Puli         if (ec && ec != boost::beast::errc::not_connected)
190bd030d0aSAppaRao Puli         {
191bd030d0aSAppaRao Puli             BMCWEB_LOG_ERROR << "shutdown failed: " << ec.message();
192bd030d0aSAppaRao Puli             return;
193bd030d0aSAppaRao Puli         }
194bd030d0aSAppaRao Puli         BMCWEB_LOG_DEBUG << "Connection closed gracefully";
195bd030d0aSAppaRao Puli     }
196bd030d0aSAppaRao Puli 
1972a5689a7SAppaRao Puli     void checkQueue(const bool newRecord = false)
198bd030d0aSAppaRao Puli     {
1992a5689a7SAppaRao Puli         if (requestDataQueue.empty())
2002a5689a7SAppaRao Puli         {
2012a5689a7SAppaRao Puli             // TODO: Having issue in keeping connection alive. So lets close if
202caa3ce3cSGunnar Mills             // nothing to be transferred.
2032a5689a7SAppaRao Puli             doClose();
2042a5689a7SAppaRao Puli 
2052a5689a7SAppaRao Puli             BMCWEB_LOG_DEBUG << "requestDataQueue is empty\n";
2062a5689a7SAppaRao Puli             return;
2072a5689a7SAppaRao Puli         }
2082a5689a7SAppaRao Puli 
2092a5689a7SAppaRao Puli         if (retryCount >= maxRetryAttempts)
2102a5689a7SAppaRao Puli         {
2112a5689a7SAppaRao Puli             BMCWEB_LOG_ERROR << "Maximum number of retries is reached.";
2122a5689a7SAppaRao Puli 
2132a5689a7SAppaRao Puli             // Clear queue.
2142a5689a7SAppaRao Puli             while (!requestDataQueue.empty())
2152a5689a7SAppaRao Puli             {
2162a5689a7SAppaRao Puli                 requestDataQueue.pop();
2172a5689a7SAppaRao Puli             }
2182a5689a7SAppaRao Puli 
219fe44eb0bSAyushi Smriti             BMCWEB_LOG_DEBUG << "Retry policy is set to " << retryPolicyAction;
220fe44eb0bSAyushi Smriti             if (retryPolicyAction == "TerminateAfterRetries")
221fe44eb0bSAyushi Smriti             {
222fe44eb0bSAyushi Smriti                 // TODO: delete subscription
223fe44eb0bSAyushi Smriti                 state = ConnState::terminated;
224fe44eb0bSAyushi Smriti                 return;
225fe44eb0bSAyushi Smriti             }
2263174e4dfSEd Tanous             if (retryPolicyAction == "SuspendRetries")
227fe44eb0bSAyushi Smriti             {
2282a5689a7SAppaRao Puli                 state = ConnState::suspended;
2292a5689a7SAppaRao Puli                 return;
2302a5689a7SAppaRao Puli             }
231fe44eb0bSAyushi Smriti             // keep retrying, reset count and continue.
232fe44eb0bSAyushi Smriti             retryCount = 0;
233fe44eb0bSAyushi Smriti         }
2342a5689a7SAppaRao Puli 
2352a5689a7SAppaRao Puli         if ((state == ConnState::connectFailed) ||
2362a5689a7SAppaRao Puli             (state == ConnState::sendFailed) ||
2372a5689a7SAppaRao Puli             (state == ConnState::recvFailed))
2382a5689a7SAppaRao Puli         {
2392a5689a7SAppaRao Puli             if (newRecord)
2402a5689a7SAppaRao Puli             {
2412a5689a7SAppaRao Puli                 // We are already running async wait and retry.
2422a5689a7SAppaRao Puli                 // Since record is added to queue, it gets the
2432a5689a7SAppaRao Puli                 // turn in FIFO.
2442a5689a7SAppaRao Puli                 return;
2452a5689a7SAppaRao Puli             }
2462a5689a7SAppaRao Puli 
247fe44eb0bSAyushi Smriti             if (runningTimer)
248fe44eb0bSAyushi Smriti             {
249fe44eb0bSAyushi Smriti                 BMCWEB_LOG_DEBUG << "Retry timer is already running.";
250fe44eb0bSAyushi Smriti                 return;
251fe44eb0bSAyushi Smriti             }
252fe44eb0bSAyushi Smriti             runningTimer = true;
253fe44eb0bSAyushi Smriti 
2542a5689a7SAppaRao Puli             retryCount++;
255fe44eb0bSAyushi Smriti 
256fe44eb0bSAyushi Smriti             BMCWEB_LOG_DEBUG << "Attempt retry after " << retryIntervalSecs
257fe44eb0bSAyushi Smriti                              << " seconds. RetryCount = " << retryCount;
258fe44eb0bSAyushi Smriti             timer.expires_after(std::chrono::seconds(retryIntervalSecs));
259cb13a392SEd Tanous             timer.async_wait(
260cb13a392SEd Tanous                 [self = shared_from_this()](const boost::system::error_code&) {
261fe44eb0bSAyushi Smriti                     self->runningTimer = false;
262fe44eb0bSAyushi Smriti                     self->connStateCheck();
263fe44eb0bSAyushi Smriti                 });
264fe44eb0bSAyushi Smriti             return;
2652a5689a7SAppaRao Puli         }
2662a5689a7SAppaRao Puli         // reset retry count.
2672a5689a7SAppaRao Puli         retryCount = 0;
268fe44eb0bSAyushi Smriti         connStateCheck();
2692a5689a7SAppaRao Puli 
270fe44eb0bSAyushi Smriti         return;
271fe44eb0bSAyushi Smriti     }
272fe44eb0bSAyushi Smriti 
273fe44eb0bSAyushi Smriti     void connStateCheck()
274fe44eb0bSAyushi Smriti     {
2752a5689a7SAppaRao Puli         switch (state)
2762a5689a7SAppaRao Puli         {
2772a5689a7SAppaRao Puli             case ConnState::connectInProgress:
2782a5689a7SAppaRao Puli             case ConnState::sendInProgress:
2792a5689a7SAppaRao Puli             case ConnState::suspended:
280fe44eb0bSAyushi Smriti             case ConnState::terminated:
2812a5689a7SAppaRao Puli                 // do nothing
2822a5689a7SAppaRao Puli                 break;
2832a5689a7SAppaRao Puli             case ConnState::initialized:
2842a5689a7SAppaRao Puli             case ConnState::closed:
2852a5689a7SAppaRao Puli             case ConnState::connectFailed:
2862a5689a7SAppaRao Puli             case ConnState::sendFailed:
28792a74e56SAppaRao Puli             case ConnState::recvFailed:
28892a74e56SAppaRao Puli             {
2892a5689a7SAppaRao Puli                 // After establishing the connection, checkQueue() will
2902a5689a7SAppaRao Puli                 // get called and it will attempt to send data.
2912a5689a7SAppaRao Puli                 doConnect();
2922a5689a7SAppaRao Puli                 break;
2932a5689a7SAppaRao Puli             }
2942a5689a7SAppaRao Puli             case ConnState::connected:
29592a74e56SAppaRao Puli             case ConnState::idle:
29692a74e56SAppaRao Puli             {
2972a5689a7SAppaRao Puli                 std::string data = requestDataQueue.front();
2982a5689a7SAppaRao Puli                 sendMessage(data);
2992a5689a7SAppaRao Puli                 break;
3002a5689a7SAppaRao Puli             }
3012a5689a7SAppaRao Puli         }
302bd030d0aSAppaRao Puli     }
303bd030d0aSAppaRao Puli 
304bd030d0aSAppaRao Puli   public:
305fe44eb0bSAyushi Smriti     explicit HttpClient(boost::asio::io_context& ioc, const std::string& id,
306fe44eb0bSAyushi Smriti                         const std::string& destIP, const std::string& destPort,
3072a5689a7SAppaRao Puli                         const std::string& destUri) :
308bd030d0aSAppaRao Puli         conn(ioc),
309fe44eb0bSAyushi Smriti         timer(ioc), subId(id), host(destIP), port(destPort), uri(destUri),
310f23b7296SEd Tanous         retryCount(0), maxRetryAttempts(5), retryIntervalSecs(0),
311fe44eb0bSAyushi Smriti         retryPolicyAction("TerminateAfterRetries"), runningTimer(false)
312bd030d0aSAppaRao Puli     {
313bd030d0aSAppaRao Puli         boost::asio::ip::tcp::resolver resolver(ioc);
314bd030d0aSAppaRao Puli         endpoint = resolver.resolve(host, port);
3152a5689a7SAppaRao Puli         state = ConnState::initialized;
316bd030d0aSAppaRao Puli     }
317bd030d0aSAppaRao Puli 
3182a5689a7SAppaRao Puli     void sendData(const std::string& data)
319bd030d0aSAppaRao Puli     {
3202a5689a7SAppaRao Puli         if (state == ConnState::suspended)
321bd030d0aSAppaRao Puli         {
322bd030d0aSAppaRao Puli             return;
323bd030d0aSAppaRao Puli         }
324bd030d0aSAppaRao Puli 
3252a5689a7SAppaRao Puli         if (requestDataQueue.size() <= maxRequestQueueSize)
3262a5689a7SAppaRao Puli         {
3272a5689a7SAppaRao Puli             requestDataQueue.push(data);
3282a5689a7SAppaRao Puli             checkQueue(true);
3292a5689a7SAppaRao Puli         }
3302a5689a7SAppaRao Puli         else
3312a5689a7SAppaRao Puli         {
3322a5689a7SAppaRao Puli             BMCWEB_LOG_ERROR << "Request queue is full. So ignoring data.";
3332a5689a7SAppaRao Puli         }
3342a5689a7SAppaRao Puli 
3352a5689a7SAppaRao Puli         return;
336bd030d0aSAppaRao Puli     }
337bd030d0aSAppaRao Puli 
338bd030d0aSAppaRao Puli     void setHeaders(
339bd030d0aSAppaRao Puli         const std::vector<std::pair<std::string, std::string>>& httpHeaders)
340bd030d0aSAppaRao Puli     {
341bd030d0aSAppaRao Puli         headers = httpHeaders;
342bd030d0aSAppaRao Puli     }
343fe44eb0bSAyushi Smriti 
344fe44eb0bSAyushi Smriti     void setRetryConfig(const uint32_t retryAttempts,
345fe44eb0bSAyushi Smriti                         const uint32_t retryTimeoutInterval)
346fe44eb0bSAyushi Smriti     {
347fe44eb0bSAyushi Smriti         maxRetryAttempts = retryAttempts;
348fe44eb0bSAyushi Smriti         retryIntervalSecs = retryTimeoutInterval;
349fe44eb0bSAyushi Smriti     }
350fe44eb0bSAyushi Smriti 
351fe44eb0bSAyushi Smriti     void setRetryPolicy(const std::string& retryPolicy)
352fe44eb0bSAyushi Smriti     {
353fe44eb0bSAyushi Smriti         retryPolicyAction = retryPolicy;
354fe44eb0bSAyushi Smriti     }
355bd030d0aSAppaRao Puli };
356bd030d0aSAppaRao Puli 
357bd030d0aSAppaRao Puli } // namespace crow
358