The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
network_asio.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2011 - 2025
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"
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
32static 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
38namespace
39{
40std::deque<boost::asio::const_buffer> split_buffer(const 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
60namespace network_asio
61{
62using boost::system::system_error;
63
64connection::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
106void connection::handle_resolve(const boost::system::error_code& ec, const 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
116void 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
143template<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
164void 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;
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
230void 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 io::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
286std::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
296void 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
307std::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
334void 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 response = io::read_gz(is);
348}
349}
A config object defines a single node in a WML file, with access to child nodes.
Definition config.hpp:157
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_
void handle_resolve(const boost::system::error_code &ec, const results_type &results)
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_
const boost::asio::ip::tcp::endpoint & endpoint
std::unique_ptr< boost::asio::streambuf > read_buf_
#define LOG_NW
Standard logging facilities (interface).
#define DBG_NW
#define ERR_NW
config read_gz(std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially a gzip_error.
Definition parser.cpp:666
void write_gz(std::ostream &out, const configr_of &cfg)
Definition parser.cpp:767
void load_tls_root_certs(boost::asio::ssl::context &ctx)
auto verbose_verify(Verifier &&verifier)
static lg::log_domain log_network("network")
#define DBG_NW
std::string_view data
Definition picture.cpp:188
static map_location::direction s