The Battle for Wesnoth  1.15.12+dev
server_base.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2016 - 2018 by Sergey Popov <dave@whitevine.net>
3  Part of the Battle for Wesnoth Project https://www.wesnoth.org
4 
5  This program is free software; you can redistribute it and/or modify
6  it under the terms of the GNU General Public License 2
7  the Free Software Foundation; either version 2 of the License, or
8  (at your option) any later version.
9  This program is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY.
11 
12  See the COPYING file for more details.
13 */
14 
16 
17 #include "log.hpp"
18 #include "filesystem.hpp"
19 
20 #ifdef HAVE_CONFIG_H
21 #include "config.h"
22 #endif
23 
24 #ifdef HAVE_SENDFILE
25 #include <sys/sendfile.h>
26 #endif
27 
28 #ifdef _WIN32
29 #include <windows.h>
30 #include <boost/scope_exit.hpp>
31 #endif
32 
33 #include <boost/asio/ip/v6_only.hpp>
34 #include <boost/asio/read.hpp>
35 #ifndef _WIN32
36 #include <boost/asio/read_until.hpp>
37 #endif
38 #include <boost/asio/write.hpp>
39 
40 #include <functional>
41 #include <queue>
42 
43 static lg::log_domain log_server("server");
44 #define ERR_SERVER LOG_STREAM(err, log_server)
45 #define WRN_SERVER LOG_STREAM(warn, log_server)
46 #define LOG_SERVER LOG_STREAM(info, log_server)
47 #define DBG_SERVER LOG_STREAM(debug, log_server)
48 
49 static lg::log_domain log_config("config");
50 #define ERR_CONFIG LOG_STREAM(err, log_config)
51 #define WRN_CONFIG LOG_STREAM(warn, log_config)
52 
53 bool dump_wml = false;
54 
55 server_base::server_base(unsigned short port, bool keep_alive)
56  : port_(port)
57  , keep_alive_(keep_alive)
58  , io_service_()
59  , acceptor_v6_(io_service_)
60  , acceptor_v4_(io_service_)
61  , handshake_response_()
62 #ifndef _WIN32
63  , input_(io_service_)
64  , sighup_(io_service_, SIGHUP)
65 #endif
66  , sigs_(io_service_, SIGINT, SIGTERM)
67 {
68 }
69 
71 {
72  boost::asio::ip::tcp::endpoint endpoint_v6(boost::asio::ip::tcp::v6(), port_);
73  boost::asio::spawn(io_service_, [this, endpoint_v6](boost::asio::yield_context yield) { serve(yield, acceptor_v6_, endpoint_v6); });
74 
75  boost::asio::ip::tcp::endpoint endpoint_v4(boost::asio::ip::tcp::v4(), port_);
76  boost::asio::spawn(io_service_, [this, endpoint_v4](boost::asio::yield_context yield) { serve(yield, acceptor_v4_, endpoint_v4); });
77 
78  handshake_response_.connection_num = htonl(42);
79 
80 #ifndef _WIN32
81  sighup_.async_wait(
82  [=](const boost::system::error_code& error, int sig)
83  { this->handle_sighup(error, sig); });
84 #endif
85  sigs_.async_wait(std::bind(&server_base::handle_termination, this, std::placeholders::_1, std::placeholders::_2));
86 }
87 
88 void server_base::serve(boost::asio::yield_context yield, boost::asio::ip::tcp::acceptor& acceptor, boost::asio::ip::tcp::endpoint endpoint)
89 {
90  if(!acceptor.is_open()) {
91  acceptor.open(endpoint.protocol());
92  acceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
93  acceptor.set_option(boost::asio::ip::tcp::acceptor::keep_alive(keep_alive_));
94  if(endpoint.protocol() == boost::asio::ip::tcp::v6())
95  acceptor.set_option(boost::asio::ip::v6_only(true));
96  acceptor.bind(endpoint);
97  acceptor.listen();
98  }
99 
100  socket_ptr socket = std::make_shared<socket_ptr::element_type>(io_service_);
101 
102  boost::system::error_code error;
103  acceptor.async_accept(socket->lowest_layer(), yield[error]);
104  if(error) {
105  ERR_SERVER << "Accept failed: " << error.message() << "\n";
106  return;
107  }
108 
109  if(accepting_connections()) {
110  boost::asio::spawn(io_service_, [this, &acceptor, endpoint](boost::asio::yield_context yield) { serve(yield, acceptor, endpoint); });
111  }
112 
113 #ifndef _WIN32
114  if(keep_alive_) {
115  int timeout = 30;
116 #ifdef __linux__
117  int cnt = 10;
118  int interval = 30;
119  setsockopt(socket->native_handle(), SOL_TCP, TCP_KEEPIDLE, &timeout, sizeof(timeout));
120  setsockopt(socket->native_handle(), SOL_TCP, TCP_KEEPCNT, &cnt, sizeof(cnt));
121  setsockopt(socket->native_handle(), SOL_TCP, TCP_KEEPINTVL, &interval, sizeof(interval));
122 #endif
123 #if defined(__APPLE__) && defined(__MACH__)
124  setsockopt(socket->native_handle(), IPPROTO_TCP, TCP_KEEPALIVE, &timeout, sizeof(timeout));
125 #endif
126  }
127 #endif
128 
129  DBG_SERVER << client_address(socket) << "\tnew connection tentatively accepted\n";
130 
131  boost::shared_array<char> handshake(new char[4]);
132  async_read(*socket, boost::asio::buffer(handshake.get(), 4), yield[error]);
133  if(check_error(error, socket))
134  return;
135 
136  if(memcmp(handshake.get(), "\0\0\0\0", 4) != 0) {
137  ERR_SERVER << client_address(socket) << "\tincorrect handshake\n";
138  return;
139  }
140 
141  async_write(*socket, boost::asio::buffer(handshake_response_.buf, 4), yield[error]);
142 
143  if(!check_error(error, socket)) {
144  const std::string ip = client_address(socket);
145 
146  const std::string reason = is_ip_banned(ip);
147  if (!reason.empty()) {
148  LOG_SERVER << ip << "\trejected banned user. Reason: " << reason << "\n";
149  async_send_error(socket, "You are banned. Reason: " + reason);
150  return;
151  } else if (ip_exceeds_connection_limit(ip)) {
152  LOG_SERVER << ip << "\trejected ip due to excessive connections\n";
153  async_send_error(socket, "Too many connections from your IP.");
154  return;
155  } else {
156  DBG_SERVER << ip << "\tnew connection fully accepted\n";
157  this->handle_new_client(socket);
158  }
159  }
160 }
161 
162 #ifndef _WIN32
164  async_read_until(input_,
165  admin_cmd_, '\n',
166  [=](const boost::system::error_code& error, std::size_t bytes_transferred)
167  { this->handle_read_from_fifo(error, bytes_transferred); }
168  );
169 }
170 #endif
171 
172 void server_base::handle_termination(const boost::system::error_code& error, int signal_number)
173 {
174  assert(!error);
175 
176  std::string signame;
177  if(signal_number == SIGINT) signame = "SIGINT";
178  else if(signal_number == SIGTERM) signame = "SIGTERM";
179  else signame = std::to_string(signal_number);
180  LOG_SERVER << signame << " caught, exiting without cleanup immediately.\n";
181  exit(128 + signal_number);
182 }
183 
185  try {
186  io_service_.run();
187  LOG_SERVER << "Server has shut down because event loop is out of work\n";
188  } catch(const server_shutdown& e) {
189  LOG_SERVER << "Server has been shut down: " << e.what() << "\n";
190  }
191 }
192 
193 std::string client_address(const socket_ptr socket)
194 {
195  boost::system::error_code error;
196  std::string result = socket->remote_endpoint(error).address().to_string();
197  if(error)
198  return "<unknown address>";
199  else
200  return result;
201 }
202 
203 bool check_error(const boost::system::error_code& error, socket_ptr socket)
204 {
205  if(error) {
206  if(error == boost::asio::error::eof)
207  LOG_SERVER << client_address(socket) << "\tconnection closed\n";
208  else
209  ERR_SERVER << client_address(socket) << "\t" << error.message() << "\n";
210  return true;
211  }
212  return false;
213 }
214 
215 namespace {
216 
217 void info_table_into_simple_wml(simple_wml::document& doc, const std::string& parent_name, const server_base::info_table& info)
218 {
219  if(info.empty()) {
220  return;
221  }
222 
223  auto& node = doc.child(parent_name.c_str())->add_child("data");
224  for(const auto& kv : info) {
225  node.set_attr_dup(kv.first.c_str(), kv.second.c_str());
226  }
227 }
228 
229 }
230 
231 /**
232  * Send a WML document from within a coroutine
233  * @param socket
234  * @param doc
235  * @param yield The function will suspend on write operation using this yield context
236  */
237 void server_base::coro_send_doc(socket_ptr socket, simple_wml::document& doc, boost::asio::yield_context yield)
238 {
239  if(dump_wml) {
240  std::cout << "Sending WML to " << client_address(socket) << ": \n" << doc.output() << std::endl;
241  }
242 
243  try {
245 
246  union DataSize
247  {
248  uint32_t size;
249  char buf[4];
250  } data_size {};
251  data_size.size = htonl(s.size());
252 
253  std::vector<boost::asio::const_buffer> buffers {
254  { data_size.buf, 4 },
255  { s.begin(), std::size_t(s.size()) }
256  };
257 
258  async_write(*socket, buffers, yield);
259  } catch (simple_wml::error& e) {
260  WRN_CONFIG << __func__ << ": simple_wml error: " << e.message << std::endl;
261  throw;
262  }
263 }
264 
265 #ifdef HAVE_SENDFILE
266 
267 void server_base::coro_send_file(socket_ptr socket, const std::string& filename, boost::asio::yield_context yield)
268 {
269  std::size_t filesize { std::size_t(filesystem::file_size(filename)) };
270  int in_file { open(filename.c_str(), O_RDONLY) };
271  off_t offset { 0 };
272  std::size_t total_bytes_transferred { 0 };
273 
274  union DataSize
275  {
276  uint32_t size;
277  char buf[4];
278  } data_size {};
279  data_size.size = htonl(filesize);
280 
281  async_write(*socket, boost::asio::buffer(data_size.buf), yield);
282  if(*(yield.ec_)) return;
283 
284  // Put the underlying socket into non-blocking mode.
285  if(!socket->native_non_blocking())
286  socket->native_non_blocking(true, *yield.ec_);
287  if(*(yield.ec_)) return;
288 
289  for (;;)
290  {
291  // Try the system call.
292  errno = 0;
293  int n = ::sendfile(socket->native_handle(), in_file, &offset, 65536);
294  *(yield.ec_) = boost::system::error_code(n < 0 ? errno : 0,
295  boost::asio::error::get_system_category());
296  total_bytes_transferred += *(yield.ec_) ? 0 : n;
297 
298  // Retry operation immediately if interrupted by signal.
299  if (*(yield.ec_) == boost::asio::error::interrupted)
300  continue;
301 
302  // Check if we need to run the operation again.
303  if (*(yield.ec_) == boost::asio::error::would_block
304  || *(yield.ec_) == boost::asio::error::try_again)
305  {
306  // We have to wait for the socket to become ready again.
307  socket->async_write_some(boost::asio::null_buffers(), yield);
308  continue;
309  }
310 
311  if (*(yield.ec_) || n == 0)
312  {
313  // An error occurred, or we have reached the end of the file.
314  // Either way we must exit the loop.
315  break;
316  }
317 
318  // Loop around to try calling sendfile again.
319  }
320 }
321 
322 #elif defined(_WIN32)
323 
324 void server_base::coro_send_file(socket_ptr socket, const std::string& filename, boost::asio::yield_context yield)
325 {
326 
327  OVERLAPPED overlap;
328  std::vector<boost::asio::const_buffer> buffers;
329 
330  SetLastError(ERROR_SUCCESS);
331 
332  std::size_t filesize = filesystem::file_size(filename);
333  std::wstring filename_ucs2 = unicode_cast<std::wstring>(filename);
334  HANDLE in_file = CreateFileW(filename_ucs2.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING,
335  FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
336  if (GetLastError() != ERROR_SUCCESS)
337  {
338  throw std::runtime_error("Failed to open the file");
339  }
340  BOOST_SCOPE_EXIT_ALL(in_file) {
341  CloseHandle(&in_file);
342  };
343 
344  HANDLE event = CreateEvent(nullptr, TRUE, TRUE, nullptr);
345  if (GetLastError() != ERROR_SUCCESS)
346  {
347  throw std::runtime_error("Failed to create an event");
348  }
349  BOOST_SCOPE_EXIT_ALL(&overlap) {
350  CloseHandle(overlap.hEvent);
351  };
352 
353  overlap.hEvent = event;
354 
355  union DataSize
356  {
357  uint32_t size;
358  char buf[4];
359  } data_size {};
360  data_size.size = htonl(filesize);
361 
362  async_write(*socket, boost::asio::buffer(data_size.buf, 4), yield);
363 
364  BOOL success = TransmitFile(socket->native_handle(), in_file, 0, 0, &overlap, nullptr, 0);
365  if(!success) {
366  if(WSAGetLastError() == WSA_IO_PENDING) {
367  while(true) {
368  // The request is pending. Wait until it completes.
369  socket->async_write_some(boost::asio::null_buffers(), yield);
370 
371  DWORD win_ec = GetLastError();
372  if (win_ec != ERROR_IO_PENDING && win_ec != ERROR_SUCCESS)
373  throw std::runtime_error("TransmitFile failed");
374 
375  if(HasOverlappedIoCompleted(&overlap)) break;
376  }
377  } else {
378  throw std::runtime_error("TransmitFile failed");
379  }
380  }
381 }
382 
383 #else
384 
385 void server_base::coro_send_file(socket_ptr socket, const std::string& filename, boost::asio::yield_context yield)
386 {
387 // TODO: Implement this for systems without sendfile()
388  assert(false && "Not implemented yet");
389 }
390 
391 #endif
392 
393 std::unique_ptr<simple_wml::document> server_base::coro_receive_doc(socket_ptr socket, boost::asio::yield_context yield)
394 {
395  union DataSize
396  {
397  uint32_t size;
398  char buf[4];
399  } data_size {};
400  async_read(*socket, boost::asio::buffer(data_size.buf, 4), yield);
401  if(*yield.ec_) return {};
402  uint32_t size = ntohl(data_size.size);
403 
404  if(size == 0) {
405  ERR_SERVER <<
406  client_address(socket) <<
407  "\treceived invalid packet with payload size 0" << std::endl;
408  return {};
409  }
411  ERR_SERVER <<
412  client_address(socket) <<
413  "\treceived packet with payload size over size limit" << std::endl;
414  return {};
415  }
416 
417  boost::shared_array<char> buffer{ new char[size] };
418  async_read(*socket, boost::asio::buffer(buffer.get(), size), yield);
419 
420  try {
421  simple_wml::string_span compressed_buf(buffer.get(), size);
422  return std::make_unique<simple_wml::document>(compressed_buf);
423  } catch (simple_wml::error& e) {
424  ERR_SERVER <<
425  client_address(socket) <<
426  "\tsimple_wml error in received data: " << e.message << std::endl;
427  async_send_error(socket, "Invalid WML received: " + e.message);
428  return {};
429  }
430 }
431 
433 {
434  boost::asio::spawn(
435  io_service_, [this, doc_ptr = doc.clone(), socket](boost::asio::yield_context yield) mutable {
436  static std::map<socket_ptr, std::queue<std::unique_ptr<simple_wml::document>>> queues;
437 
438  queues[socket].push(std::move(doc_ptr));
439  if(queues[socket].size() > 1) {
440  return;
441  }
442 
443  while(queues[socket].size() > 0) {
444  coro_send_doc(socket, *(queues[socket].front()), yield);
445  queues[socket].pop();
446  }
447  queues.erase(socket);
448  }
449  );
450 }
451 
452 void server_base::async_send_error(socket_ptr socket, const std::string& msg, const char* error_code, const info_table& info)
453 {
455  doc.root().add_child("error").set_attr_dup("message", msg.c_str());
456  if(*error_code != '\0') {
457  doc.child("error")->set_attr("error_code", error_code);
458  }
459  info_table_into_simple_wml(doc, "error", info);
460 
461  async_send_doc_queued(socket, doc);
462 }
463 
464 void server_base::async_send_warning(socket_ptr socket, const std::string& msg, const char* warning_code, const info_table& info)
465 {
467  doc.root().add_child("warning").set_attr_dup("message", msg.c_str());
468  if(*warning_code != '\0') {
469  doc.child("warning")->set_attr("warning_code", warning_code);
470  }
471  info_table_into_simple_wml(doc, "warning", info);
472 
473  async_send_doc_queued(socket, doc);
474 }
475 
476 // This is just here to get it to build without the deprecation_message function
477 #include "game_version.hpp"
478 #include "deprecation.hpp"
479 
480 std::string deprecated_message(const std::string&, DEP_LEVEL, const version_info&, const std::string&) {return "";}
boost::asio::ip::tcp::acceptor acceptor_v4_
Definition: server_base.hpp:97
node & add_child(const char *name)
Definition: simple_wml.cpp:464
virtual void handle_sighup(const boost::system::error_code &error, int signal_number)=0
string_span output_compressed(bool bzip2=false)
#define LOG_SERVER
Definition: server_base.cpp:46
std::unique_ptr< simple_wml::document > coro_receive_doc(socket_ptr socket, boost::asio::yield_context yield)
Receive WML document from a coroutine.
bool check_error(const boost::system::error_code &error, socket_ptr socket)
Interfaces for manipulating version numbers of engine, add-ons, etc.
void async_send_error(socket_ptr socket, const std::string &msg, const char *error_code="", const info_table &info={})
void coro_send_file(socket_ptr socket, const std::string &filename, boost::asio::yield_context yield)
Send contents of entire file directly to socket from within a coroutine.
virtual void handle_new_client(socket_ptr socket)=0
boost::asio::signal_set sighup_
static l_noret error(LoadState *S, const char *why)
Definition: lundump.cpp:40
logger & info()
Definition: log.cpp:88
ucs4_convert_impl::enableif< TD, typename TS::value_type >::type unicode_cast(const TS &source)
node & set_attr(const char *key, const char *value)
Definition: simple_wml.cpp:411
void handle_termination(const boost::system::error_code &error, int signal_number)
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
Definition: debugger.cpp:109
bool dump_wml
Definition: server_base.cpp:53
unsigned short port_
Definition: server_base.hpp:93
#define ERR_SERVER
Definition: server_base.cpp:44
void start_server()
Definition: server_base.cpp:70
std::unique_ptr< document > clone()
node * child(const char *name)
Definition: simple_wml.hpp:261
union server_base::@27 handshake_response_
const char * output()
bool keep_alive_
Definition: server_base.hpp:94
const char * begin() const
Definition: simple_wml.hpp:90
std::size_t size(const std::string &str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:86
boost::asio::streambuf admin_cmd_
boost::asio::io_service io_service_
Definition: server_base.hpp:95
#define DBG_SERVER
Definition: server_base.cpp:47
boost::asio::ip::tcp::acceptor acceptor_v6_
Definition: server_base.hpp:96
virtual bool accepting_connections() const
const char * what() const noexcept
Definition: exceptions.hpp:35
static std::size_t document_size_limit
Definition: simple_wml.hpp:290
virtual bool ip_exceeds_connection_limit(const std::string &) const
virtual void handle_read_from_fifo(const boost::system::error_code &error, std::size_t bytes_transferred)=0
static lg::log_domain log_server("server")
#define WRN_CONFIG
Definition: server_base.cpp:51
node & set_attr_dup(const char *key, const char *value)
Definition: simple_wml.cpp:427
server_base(unsigned short port, bool keep_alive)
Definition: server_base.cpp:55
std::map< std::string, std::string > info_table
Definition: server_base.hpp:88
static map_location::DIRECTION s
static lg::log_domain log_config("config")
void async_send_doc_queued(socket_ptr socket, simple_wml::document &doc)
High level wrapper for sending a WML document.
std::string client_address(const socket_ptr socket)
Declarations for File-IO.
void read_from_fifo()
virtual std::string is_ip_banned(const std::string &)
boost::asio::posix::stream_descriptor input_
Represents version numbers.
std::string deprecated_message(const std::string &, DEP_LEVEL, const version_info &, const std::string &)
int file_size(const std::string &fname)
Returns the size of a file, or -1 if the file doesn&#39;t exist.
void coro_send_doc(socket_ptr socket, simple_wml::document &doc, boost::asio::yield_context yield)
Send a WML document from within a coroutine.
DEP_LEVEL
See https://wiki.wesnoth.org/CompatibilityStandards for more info.
Definition: deprecation.hpp:19
Standard logging facilities (interface).
std::string message
Definition: exceptions.hpp:29
std::shared_ptr< boost::asio::ip::tcp::socket > socket_ptr
Definition: server_base.hpp:43
#define e
void async_send_warning(socket_ptr socket, const std::string &msg, const char *warning_code="", const info_table &info={})
static map_location::DIRECTION n
void serve(boost::asio::yield_context yield, boost::asio::ip::tcp::acceptor &acceptor, boost::asio::ip::tcp::endpoint endpoint)
Definition: server_base.cpp:88
Base class for servers using Wesnoth&#39;s WML over TCP protocol.
boost::asio::signal_set sigs_