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