https_client.h
1 // Copyright 2019, Beeri 15. All rights reserved.
2 // Author: Roman Gershman (romange@gmail.com)
3 //
4 
5 #pragma once
6 
7 #include <boost/beast/core/flat_buffer.hpp>
8 #include <boost/beast/http/buffer_body.hpp>
9 #include <boost/beast/http/message.hpp>
10 #include <boost/beast/http/parser.hpp>
11 #include <boost/beast/http/read.hpp>
12 #include <boost/beast/http/write.hpp>
13 
14 #include "absl/strings/string_view.h"
15 #include "absl/types/variant.h"
16 #include "util/http/ssl_stream.h"
17 
18 namespace util {
19 class IoContext;
20 class FiberSyncSocket;
21 
22 namespace http {
23 
24 // Waiting for std::expected to arrive. Meanwhile we use this interface.
25 using SslContextResult = absl::variant<::boost::system::error_code, ::boost::asio::ssl::context>;
26 SslContextResult CreateClientSslContext(absl::string_view cert_string);
27 
28 class HttpsClient {
29  public:
30  using error_code = ::boost::system::error_code;
31 
40  HttpsClient(absl::string_view host, IoContext* io_context, ::boost::asio::ssl::context* ssl_ctx);
41  HttpsClient(const HttpsClient&) = delete;
42  HttpsClient(HttpsClient&&) = delete;
43 
44  error_code Connect(unsigned msec);
45 
51  template <typename Req> error_code Send(const Req& req);
52 
58  template <typename Req, typename Resp> error_code Send(const Req& req, Resp* resp);
59 
60  error_code ReadHeader(::boost::beast::http::basic_parser<false>* parser);
61  template <typename Parser> error_code Read(Parser* parser);
62 
63  error_code DrainResponse(
64  ::boost::beast::http::response_parser<::boost::beast::http::buffer_body>* parser);
65 
66  SslStream* client() { return client_.get(); }
67 
68  void schedule_reconnect() { reconnect_needed_ = true; }
69 
70  int32_t native_handle() { return client_->next_layer().native_handle(); }
71 
72  uint32_t retry_count() const { return retry_cnt_; }
73 
75  void set_retry_count(uint32_t cnt) { retry_cnt_ = cnt; }
76 
77  ::boost::asio::ssl::context& ssl_context() { return ssl_cntx_; }
78 
79  error_code status() const {
80  namespace err = ::boost::asio::error;
81 
82  return reconnect_needed_ ? err::not_connected : client_->next_layer().status();
83  }
84 
85  private:
86  error_code HandleError(const error_code& ec);
87  bool HandleWriteError(const error_code& ec);
88 
89  bool IsError(const error_code& ec) const {
90  using err = ::boost::beast::http::error;
91  return ec && ec != err::need_buffer;
92  }
93 
94  error_code ReconnectIfNeeded() {
95  if (reconnect_needed_)
96  return InitSslClient();
97  return error_code{};
98  }
99 
100  error_code InitSslClient();
101 
102  IoContext& io_context_;
103  ::boost::asio::ssl::context& ssl_cntx_;
104 
105  ::boost::beast::flat_buffer tmp_buffer_;
106 
107  std::string host_name_;
108 
109  std::unique_ptr<FiberSyncSocket> socket_;
110  std::unique_ptr<SslStream> client_;
111 
112  uint32_t reconnect_msec_ = 1000;
113  bool reconnect_needed_ = true;
114  uint32_t retry_cnt_ = 1;
115 };
116 
117 // ::boost::system::error_code SslConnect(SslStream* stream, unsigned msec);
118 
119 template <typename Req, typename Resp>
120 auto HttpsClient::Send(const Req& req, Resp* resp) -> error_code {
121  namespace h2 = ::boost::beast::http;
122  error_code ec;
123 
124  for (uint32_t i = 0; i < retry_cnt_; ++i) {
125  ec = Send(req);
126  if (IsError(ec)) // Send already retries.
127  break;
128 
129  if (client_)
130  h2::read(*client_, tmp_buffer_, *resp, ec);
131  else
132  h2::read(*socket_, tmp_buffer_, *resp, ec);
133 
134  if (!IsError(ec)) {
135  return ec;
136  }
137  *resp = Resp{};
138  }
139  return HandleError(ec);
140 }
141 
142 template <typename Req> auto HttpsClient::Send(const Req& req) -> error_code {
143  error_code ec;
144  for (uint32_t i = 0; i < retry_cnt_; ++i) {
145  ec = ReconnectIfNeeded();
146  if (IsError(ec))
147  continue;
148  if (client_)
149  ::boost::beast::http::write(*client_, req, ec);
150  else
151  ::boost::beast::http::write(*socket_, req, ec);
152 
153  if (HandleWriteError(ec)) {
154  break;
155  }
156  }
157  return HandleError(ec);
158 }
159 
160 // Read methods should not reconnect since they assume some state (i.e. reading http request).
161 inline auto HttpsClient::ReadHeader(::boost::beast::http::basic_parser<false>* parser)
162  -> error_code {
163  error_code ec;
164  if (client_)
165  ::boost::beast::http::read_header(*client_, tmp_buffer_, *parser, ec);
166  else
167  ::boost::beast::http::read_header(*socket_, tmp_buffer_, *parser, ec);
168  return HandleError(ec);
169 }
170 
171 template <typename Parser> auto HttpsClient::Read(Parser* parser) -> error_code {
172  error_code ec;
173 
174  // Note that read returns number of raw bytes read from stream before parsing which
175  // does not correlate to the final data stored inside the parser.
176  if (client_)
177  ::boost::beast::http::read(*client_, tmp_buffer_, *parser, ec);
178  else
179  ::boost::beast::http::read(*socket_, tmp_buffer_, *parser, ec);
180  return HandleError(ec);
181 }
182 
183 inline auto HttpsClient::HandleError(const error_code& ec) -> error_code {
184  if (IsError(ec))
185  reconnect_needed_ = true;
186  return ec;
187 }
188 
189 } // namespace http
190 } // namespace util
HttpsClient(absl::string_view host, IoContext *io_context, ::boost::asio::ssl::context *ssl_ctx)
Construct a new Https Client object.
Definition: https_client.cc:85
error_code Send(const Req &req)
Sends http request but does not read response back.
void set_retry_count(uint32_t cnt)
Sets number of retries for Send(...) methods.
Definition: https_client.h:75