https_client.cc
1 // Copyright 2019, Beeri 15. All rights reserved.
2 // Author: Roman Gershman (romange@gmail.com)
3 //
4 
5 #include "util/http/https_client.h"
6 
7 #include "base/logging.h"
8 #include "util/asio/io_context.h"
9 
10 #ifdef BOOST_ASIO_SEPARATE_COMPILATION
11 #include <boost/asio/ssl/impl/src.hpp>
12 #endif
13 
14 // libssl1.0.x has performance locking bugs that practically prevent scalable communication
15 // with ssl library. It seems that libssl1.1 fixes that. libssl-dev in 18.04 installs libssl1.1.
16 #if OPENSSL_VERSION_NUMBER < 0x10100000L
17 #warning Please update your libssl to libssl1.1 - install libssl-dev
18 #endif
19 
20 DEFINE_bool(https_client_disable_ssl, false, "");
21 
22 namespace util {
23 namespace http {
24 
25 using namespace boost;
26 using namespace std;
27 namespace h2 = beast::http;
28 
29 namespace {
30 constexpr const char kPort[] = "443";
31 
32 ::boost::system::error_code SslConnect(SslStream* stream, unsigned ms) {
33  system::error_code ec;
34  for (unsigned i = 0; i < 2; ++i) {
35  ec = stream->next_layer().ClientWaitToConnect(ms);
36  if (ec) {
37  VLOG(1) << "Error " << i << ": " << ec << "/" << ec.message() << " for socket "
38  << stream->next_layer().native_handle();
39 
40  continue;
41  }
42 
43  stream->handshake(asio::ssl::stream_base::client, ec);
44  if (!ec) {
45  auto* cipher = SSL_get_current_cipher(stream->native_handle());
46  VLOG(1) << "SSL handshake success " << i << ", chosen " << SSL_CIPHER_get_name(cipher) << "/"
47  << SSL_CIPHER_get_version(cipher);
48  }
49  return ec;
50  }
51 
52  return ec;
53 }
54 
55 } // namespace
56 
57 SslContextResult CreateClientSslContext(absl::string_view cert_string) {
58  system::error_code ec;
59  asio::ssl::context cntx{asio::ssl::context::tlsv12_client};
60  cntx.set_options(boost::asio::ssl::context::default_workarounds |
61  boost::asio::ssl::context::no_compression | boost::asio::ssl::context::no_sslv2 |
62  boost::asio::ssl::context::no_sslv3 | boost::asio::ssl::context::no_tlsv1 |
63  boost::asio::ssl::context::no_tlsv1_1);
64  cntx.set_verify_mode(asio::ssl::verify_peer, ec);
65  if (ec) {
66  return SslContextResult(ec);
67  }
68  cntx.add_certificate_authority(asio::buffer(cert_string.data(), cert_string.size()), ec);
69  if (ec) {
70  return SslContextResult(ec);
71  }
72  SSL_CTX* ssl_cntx = cntx.native_handle();
73 
74  long flags = SSL_CTX_get_options(ssl_cntx);
75  flags |= SSL_OP_CIPHER_SERVER_PREFERENCE;
76  SSL_CTX_set_options(ssl_cntx, flags);
77 
78  constexpr char kCiphers[] = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256";
79  CHECK_EQ(1, SSL_CTX_set_cipher_list(ssl_cntx, kCiphers));
80  CHECK_EQ(1, SSL_CTX_set_ecdh_auto(ssl_cntx, 1));
81 
82  return SslContextResult(std::move(cntx));
83 }
84 
85 HttpsClient::HttpsClient(absl::string_view host, IoContext* context,
86  ::boost::asio::ssl::context* ssl_ctx)
87  : io_context_(*context), ssl_cntx_(*ssl_ctx), host_name_(host) {
88 }
89 
90 auto HttpsClient::Connect(unsigned msec) -> error_code {
91  CHECK(io_context_.InContextThread());
92 
93  reconnect_msec_ = msec;
94 
95  return InitSslClient();
96 }
97 
98 auto HttpsClient::InitSslClient() -> error_code {
99  VLOG(2) << "Https::InitSslClient " << reconnect_needed_;
100 
101  error_code ec;
102  if (!reconnect_needed_)
103  return ec;
104  if (FLAGS_https_client_disable_ssl) {
105  socket_.reset(new FiberSyncSocket{host_name_, "80", &io_context_});
106  socket_->set_keep_alive(true);
107  ec = socket_->ClientWaitToConnect(reconnect_msec_);
108  if (ec) {
109  VLOG(1) << "Error " << ": " << ec << "/" << ec.message() << " for socket "
110  << socket_->native_handle();
111  }
112  } else {
113  client_.reset(new SslStream(FiberSyncSocket{host_name_, kPort, &io_context_}, ssl_cntx_));
114  client_->next_layer().set_keep_alive(true);
115 
116  if (SSL_set_tlsext_host_name(client_->native_handle(), host_name_.c_str()) != 1) {
117  char buf[128];
118  ERR_error_string_n(::ERR_peek_last_error(), buf, sizeof(buf));
119 
120  LOG(FATAL) << "Could not set hostname: " << buf;
121  }
122 
123  ec = SslConnect(client_.get(), reconnect_msec_);
124  if (!ec) {
125  reconnect_needed_ = false;
126  } else {
127  VLOG(1) << "Error connecting " << ec << ", socket " << client_->next_layer().native_handle();
128  }
129  }
130  return ec;
131 }
132 
133 auto HttpsClient::DrainResponse(h2::response_parser<h2::buffer_body>* parser) -> error_code {
134  if (parser->is_done())
135  return error_code{};
136 
137  constexpr size_t kBufSize = 1 << 16;
138  std::unique_ptr<uint8_t[]> buf(new uint8_t[kBufSize]);
139  auto& body = parser->get().body();
140  error_code ec;
141  size_t sz = 0;
142 
143  // Drain pending response completely to allow reusing the current connection.
144  VLOG(1) << "parser: " << parser->got_some();
145  while (!parser->is_done()) {
146  body.data = buf.get();
147  body.size = kBufSize;
148  size_t raw_bytes;
149 
150  if (client_)
151  raw_bytes = h2::read(*client_, tmp_buffer_, *parser, ec);
152  else
153  raw_bytes = h2::read(*socket_, tmp_buffer_, *parser, ec);
154 
155  if (ec && ec != h2::error::need_buffer) {
156  VLOG(1) << "Error " << ec << "/" << ec.message();
157  return ec;
158  }
159  sz += raw_bytes;
160 
161  VLOG(1) << "DrainResp: " << raw_bytes << "/" << body.size;
162  }
163  VLOG_IF(1, sz > 0) << "Drained " << sz << " bytes";
164 
165  return error_code{};
166 }
167 
168 bool HttpsClient::HandleWriteError(const error_code& ec) {
169  if (!ec)
170  return true;
171 
172  LOG(WARNING) << "Write returned error " << ec << "/" << ec.message();
173  reconnect_needed_ = true;
174 
175  return false;
176 }
177 
178 } // namespace http
179 } // 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