The Battle for Wesnoth  1.17.12+dev
network_asio.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2011 - 2022
3  by Sergey Popov <loonycyborg@gmail.com>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #define BOOST_ASIO_NO_DEPRECATED
17 
18 #include "network_asio.hpp"
19 
20 #include "log.hpp"
21 #include "serialization/parser.hpp"
22 #include "tls_root_store.hpp"
23 
24 #include <boost/asio/connect.hpp>
25 #include <boost/asio/read.hpp>
26 #include <boost/asio/write.hpp>
27 
28 #include <algorithm>
29 #include <cstdint>
30 #include <deque>
31 #include <functional>
32 
33 static lg::log_domain log_network("network");
34 #define DBG_NW LOG_STREAM(debug, log_network)
35 #define LOG_NW LOG_STREAM(info, log_network)
36 #define WRN_NW LOG_STREAM(warn, log_network)
37 #define ERR_NW LOG_STREAM(err, log_network)
38 
39 namespace
40 {
41 std::deque<boost::asio::const_buffer> split_buffer(boost::asio::streambuf::const_buffers_type source_buffer)
42 {
43  const unsigned int chunk_size = 4096;
44 
45  std::deque<boost::asio::const_buffer> buffers;
46  unsigned int remaining_size = boost::asio::buffer_size(source_buffer);
47 
48  const uint8_t* data = static_cast<const uint8_t*>(source_buffer.data());
49 
50  while(remaining_size > 0u) {
51  unsigned int size = std::min(remaining_size, chunk_size);
52  buffers.emplace_back(data, size);
53  data += size;
54  remaining_size -= size;
55  }
56 
57  return buffers;
58 }
59 }
60 
61 namespace network_asio
62 {
63 using boost::system::system_error;
64 
65 connection::connection(const std::string& host, const std::string& service)
66  : io_context_()
67  , host_(host)
68  , service_(service)
69  , resolver_(io_context_)
70  , use_tls_(true)
71  , socket_(raw_socket(new raw_socket::element_type{io_context_}))
72  , done_(false)
73  , write_buf_()
74  , read_buf_()
76  , payload_size_(0)
77  , bytes_to_write_(0)
78  , bytes_written_(0)
79  , bytes_to_read_(0)
80  , bytes_read_(0)
81 {
82  boost::system::error_code ec;
83  auto result = resolver_.resolve(host, service, boost::asio::ip::resolver_query_base::numeric_host, ec);
84  if(!ec) { // if numeric resolve succeeds then we got raw ip address so TLS host name validation would never pass
85  use_tls_ = false;
86  boost::asio::post(io_context_, [this, ec, result](){ handle_resolve(ec, { result } ); } );
87  } else {
88  resolver_.async_resolve(host, service,
89  std::bind(&connection::handle_resolve, this, std::placeholders::_1, std::placeholders::_2));
90  }
91 
92  LOG_NW << "Resolving hostname: " << host;
93 }
94 
96 {
97  if(auto socket = utils::get_if<tls_socket>(&socket_)) {
98  boost::system::error_code ec;
99  // this sends close_notify for secure connection shutdown
100  (*socket)->async_shutdown([](const boost::system::error_code&) {} );
101  const char buffer[] = "";
102  // this write is needed to trigger immediate close instead of waiting for other side's close_notify
103  boost::asio::write(**socket, boost::asio::buffer(buffer, 0), ec);
104  }
105 }
106 
107 void connection::handle_resolve(const boost::system::error_code& ec, results_type results)
108 {
109  if(ec) {
110  throw system_error(ec);
111  }
112 
113  boost::asio::async_connect(*utils::get<raw_socket>(socket_), results,
114  std::bind(&connection::handle_connect, this, std::placeholders::_1, std::placeholders::_2));
115 }
116 
117 void connection::handle_connect(const boost::system::error_code& ec, endpoint endpoint)
118 {
119  if(ec) {
120  ERR_NW << "Tried all IPs. Giving up";
121  throw system_error(ec);
122  } else {
123  LOG_NW << "Connected to " << endpoint.address();
124 
125  if(endpoint.address().is_loopback()) {
126  use_tls_ = false;
127  }
128  handshake();
129  }
130 }
131 
133 {
134  static const uint32_t handshake = 0;
135  static const uint32_t tls_handshake = htonl(uint32_t(1));
136 
137  boost::asio::async_write(
138  *utils::get<raw_socket>(socket_),
139  boost::asio::buffer(use_tls_ ? reinterpret_cast<const char*>(&tls_handshake) : reinterpret_cast<const char*>(&handshake), 4),
140  std::bind(&connection::handle_write, this, std::placeholders::_1, std::placeholders::_2)
141  );
142 
143  boost::asio::async_read(*utils::get<raw_socket>(socket_), boost::asio::buffer(reinterpret_cast<std::byte*>(&handshake_response_), 4),
144  std::bind(&connection::handle_handshake, this, std::placeholders::_1));
145 }
146 
147 template<typename Verifier> auto verbose_verify(Verifier&& verifier)
148 {
149  return [verifier](bool preverified, boost::asio::ssl::verify_context& ctx) {
150  char subject_name[256];
151  X509* cert = X509_STORE_CTX_get_current_cert(ctx.native_handle());
152  X509_NAME_oneline(X509_get_subject_name(cert), subject_name, 256);
153  bool verified = verifier(preverified, ctx);
154  DBG_NW << "Verifying TLS certificate: " << subject_name << ": " <<
155  (verified ? "verified" : "failed");
156  BIO* bio = BIO_new(BIO_s_mem());
157  char buffer[1024];
158  X509_print(bio, cert);
159  while(BIO_read(bio, buffer, 1024) > 0)
160  {
161  DBG_NW << buffer;
162  }
163  BIO_free(bio);
164  return verified;
165  };
166 }
167 
168 void connection::handle_handshake(const boost::system::error_code& ec)
169 {
170  if(ec) {
171  if(ec == boost::asio::error::eof && use_tls_) {
172  // immediate disconnect likely means old server not supporting TLS handshake code
174  return;
175  }
176 
177  throw system_error(ec);
178  }
179 
180  if(use_tls_) {
181  if(handshake_response_ == 0xFFFFFFFFU) {
182  use_tls_ = false;
183  handle_handshake(ec);
184  return;
185  }
186 
187  if(handshake_response_ == 0x00000000) {
189  raw_socket s { std::move(utils::get<raw_socket>(socket_)) };
190  tls_socket ts { new tls_socket::element_type { std::move(*s), tls_context_ } };
191  socket_ = std::move(ts);
192 
193  auto& socket { *utils::get<tls_socket>(socket_) };
194 
195  socket.set_verify_mode(
196  boost::asio::ssl::verify_peer |
197  boost::asio::ssl::verify_fail_if_no_peer_cert
198  );
199 
200 #if BOOST_VERSION >= 107300
201  socket.set_verify_callback(verbose_verify(boost::asio::ssl::host_name_verification(host_)));
202 #else
203  socket.set_verify_callback(verbose_verify(boost::asio::ssl::rfc2818_verification(host_)));
204 #endif
205 
206  socket.async_handshake(boost::asio::ssl::stream_base::client, [this](const boost::system::error_code& ec) {
207  if(ec) {
208  throw system_error(ec);
209  }
210 
211  done_ = true;
212  });
213  return;
214  }
215 
217  } else {
218  done_ = true;
219  }
220 }
221 
223 {
224  assert(use_tls_ == true);
225  use_tls_ = false;
226 
227  boost::asio::ip::tcp::endpoint endpoint { utils::get<raw_socket>(socket_)->remote_endpoint() };
228  utils::get<raw_socket>(socket_)->close();
229 
230  utils::get<raw_socket>(socket_)->async_connect(endpoint,
231  std::bind(&connection::handle_connect, this, std::placeholders::_1, endpoint));
232 }
233 
234 void connection::transfer(const config& request, config& response)
235 {
236  io_context_.restart();
237  done_ = false;
238 
239  write_buf_.reset(new boost::asio::streambuf);
240  read_buf_.reset(new boost::asio::streambuf);
241  std::ostream os(write_buf_.get());
242  write_gz(os, request);
243 
244  bytes_to_write_ = write_buf_->size() + 4;
245  bytes_written_ = 0;
246  payload_size_ = htonl(bytes_to_write_ - 4);
247 
248  auto bufs = split_buffer(write_buf_->data());
249  bufs.push_front(boost::asio::buffer(reinterpret_cast<const char*>(&payload_size_), 4));
250 
251  utils::visit([this, &bufs, &response](auto&& socket) {
252  boost::asio::async_write(*socket, bufs,
253  std::bind(&connection::is_write_complete, this, std::placeholders::_1, std::placeholders::_2),
254  std::bind(&connection::handle_write, this, std::placeholders::_1, std::placeholders::_2));
255 
256  boost::asio::async_read(*socket, *read_buf_,
257  std::bind(&connection::is_read_complete, this, std::placeholders::_1, std::placeholders::_2),
258  std::bind(&connection::handle_read, this, std::placeholders::_1, std::placeholders::_2, std::ref(response)));
259  }, socket_);
260 }
261 
263 {
264  utils::visit([](auto&& socket) {
265  if(socket->lowest_layer().is_open()) {
266  boost::system::error_code ec;
267 
268 #ifdef _MSC_VER
269 // Silence warning about boost::asio::basic_socket<Protocol>::cancel always
270 // returning an error on XP, which we don't support anymore.
271 #pragma warning(push)
272 #pragma warning(disable:4996)
273 #endif
274  socket->lowest_layer().cancel(ec);
275 #ifdef _MSC_VER
276 #pragma warning(pop)
277 #endif
278 
279  if(ec) {
280  WRN_NW << "Failed to cancel network operations: " << ec.message();
281  }
282  }
283  }, socket_);
284  bytes_to_write_ = 0;
285  bytes_written_ = 0;
286  bytes_to_read_ = 0;
287  bytes_read_ = 0;
288 }
289 
290 std::size_t connection::is_write_complete(const boost::system::error_code& ec, std::size_t bytes_transferred)
291 {
292  if(ec) {
293  throw system_error(ec);
294  }
295 
296  bytes_written_ = bytes_transferred;
297  return bytes_to_write_ - bytes_transferred;
298 }
299 
300 void connection::handle_write(const boost::system::error_code& ec, std::size_t bytes_transferred)
301 {
302  DBG_NW << "Written " << bytes_transferred << " bytes.";
303  if(write_buf_)
304  write_buf_->consume(bytes_transferred);
305 
306  if(ec) {
307  throw system_error(ec);
308  }
309 }
310 
311 std::size_t connection::is_read_complete(const boost::system::error_code& ec, std::size_t bytes_transferred)
312 {
313  if(ec) {
314  throw system_error(ec);
315  }
316 
317  bytes_read_ = bytes_transferred;
318  if(bytes_transferred < 4) {
319  return 4;
320  }
321 
322  if(!bytes_to_read_) {
323  std::istream is(read_buf_.get());
324  uint32_t data_size;
325 
326  is.read(reinterpret_cast<char*>(&data_size), 4);
327  bytes_to_read_ = ntohl(data_size) + 4;
328 
329  // Close immediately if we receive an invalid length
330  if(bytes_to_read_ < 4) {
331  bytes_to_read_ = bytes_transferred;
332  }
333  }
334 
335  return bytes_to_read_ - bytes_transferred;
336 }
337 
338 void connection::handle_read(const boost::system::error_code& ec, std::size_t bytes_transferred, config& response)
339 {
340  DBG_NW << "Read " << bytes_transferred << " bytes.";
341 
342  bytes_to_read_ = 0;
343  bytes_to_write_ = 0;
344  done_ = true;
345 
346  if(ec && ec != boost::asio::error::eof) {
347  throw system_error(ec);
348  }
349 
350  std::istream is(read_buf_.get());
351  read_gz(response, is);
352 }
353 }
void handle_read(const boost::system::error_code &ec, std::size_t bytes_transferred, config &response)
connection(const std::string &host, const std::string &service)
Constructor.
std::string_view data
Definition: picture.cpp:206
void load_tls_root_certs(boost::asio::ssl::context &ctx)
void read_gz(config &cfg, std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially a gzip_error.
Definition: parser.cpp:683
auto verbose_verify(Verifier &&verifier)
#define ERR_NW
std::size_t is_write_complete(const boost::system::error_code &error, std::size_t bytes_transferred)
void write(std::ostream &out, const configr_of &cfg, unsigned int level)
Definition: parser.cpp:764
boost::asio::io_context io_context_
std::size_t size(const std::string &str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:87
void handle_resolve(const boost::system::error_code &ec, results_type results)
void write_gz(std::ostream &out, const configr_of &cfg)
Definition: parser.cpp:783
#define WRN_NW
void handle_write(const boost::system::error_code &ec, std::size_t bytes_transferred)
std::size_t is_read_complete(const boost::system::error_code &error, std::size_t bytes_transferred)
const boost::asio::ip::tcp::endpoint & endpoint
static lg::log_domain log_network("network")
void transfer(const config &request, config &response)
boost::asio::ssl::context tls_context_
static map_location::DIRECTION s
#define LOG_NW
resolver::results_type results_type
std::unique_ptr< boost::asio::streambuf > read_buf_
std::unique_ptr< boost::asio::streambuf > write_buf_
Standard logging facilities (interface).
std::unique_ptr< boost::asio::ssl::stream< raw_socket::element_type > > tls_socket
std::unique_ptr< boost::asio::ip::tcp::socket > raw_socket
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:60
void handle_handshake(const boost::system::error_code &ec)
#define DBG_NW
void handle_connect(const boost::system::error_code &ec, endpoint endpoint)