The Battle for Wesnoth  1.15.3+dev
campaign_server.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2018 by David White <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 as published by
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 
15 /**
16  * @file
17  * Wesnoth addon server.
18  * Expects a "server.cfg" config file in the current directory
19  * and saves addons under data/.
20  */
21 
23 
24 #include "filesystem.hpp"
25 #include "lexical_cast.hpp"
26 #include "log.hpp"
27 #include "serialization/base64.hpp"
29 #include "serialization/parser.hpp"
32 #include "game_config.hpp"
33 #include "addon/validation.hpp"
38 #include "game_version.hpp"
39 #include "hash.hpp"
40 
41 #include <csignal>
42 #include <ctime>
43 
44 #include <boost/random.hpp>
45 #include <boost/generator_iterator.hpp>
46 
47 // the fork execute is unix specific only tested on Linux quite sure it won't
48 // work on Windows not sure which other platforms have a problem with it.
49 #if !(defined(_WIN32))
50 #include <errno.h>
51 #endif
52 
53 static lg::log_domain log_campaignd("campaignd");
54 #define DBG_CS LOG_STREAM(debug, log_campaignd)
55 #define LOG_CS LOG_STREAM(info, log_campaignd)
56 #define WRN_CS LOG_STREAM(warn, log_campaignd)
57 #define ERR_CS LOG_STREAM(err, log_campaignd)
58 
59 static lg::log_domain log_config("config");
60 #define ERR_CONFIG LOG_STREAM(err, log_config)
61 #define WRN_CONFIG LOG_STREAM(warn, log_config)
62 
63 static lg::log_domain log_server("server");
64 #define ERR_SERVER LOG_STREAM(err, log_server)
65 
67 
68 namespace {
69 
70 /* Secure password storage functions */
71 bool authenticate(config& campaign, const config::attribute_value& passphrase)
72 {
73  return utils::md5(passphrase, campaign["passsalt"]).base64_digest() == campaign["passhash"];
74 }
75 
76 std::string generate_salt(std::size_t len)
77 {
78  boost::mt19937 mt(std::time(0));
79  std::string salt = std::string(len, '0');
80  boost::uniform_int<> from_str(0, 63); // 64 possible values for base64
81  boost::variate_generator< boost::mt19937, boost::uniform_int<>> get_char(mt, from_str);
82 
83  for(std::size_t i = 0; i < len; i++) {
84  salt[i] = crypt64::encode(get_char());
85  }
86 
87  return salt;
88 }
89 
90 void set_passphrase(config& campaign, std::string passphrase)
91 {
92  std::string salt = generate_salt(16);
93  campaign["passsalt"] = salt;
94  campaign["passhash"] = utils::md5(passphrase, salt).base64_digest();
95 }
96 
97 } // end anonymous namespace
98 
99 namespace campaignd {
100 
101 server::server(const std::string& cfg_file)
103  , cfg_()
104  , cfg_file_(cfg_file)
105  , read_only_(false)
106  , compress_level_(0)
107  , hooks_()
108  , handlers_()
109  , feedback_url_format_()
110  , blacklist_()
111  , blacklist_file_()
112  , stats_exempt_ips_()
113  , flush_timer_(io_service_)
114 {
115 
116 #ifndef _WIN32
117  struct sigaction sa;
118  std::memset( &sa, 0, sizeof(sa) );
119  #pragma GCC diagnostic ignored "-Wold-style-cast"
120  sa.sa_handler = SIG_IGN;
121  int res = sigaction( SIGPIPE, &sa, nullptr);
122  assert( res == 0 );
123 #endif
124 
125  load_config();
126 
127  LOG_CS << "Port: " << port_ << "\n";
128 
129  // Ensure all campaigns to use secure hash passphrase storage
130  if(!read_only_) {
131  for(config& campaign : campaigns().child_range("campaign")) {
132  // Campaign already has a hashed password
133  if(campaign["passphrase"].empty()) {
134  continue;
135  }
136 
137  LOG_CS << "Campaign '" << campaign["title"] << "' uses unhashed passphrase. Fixing.\n";
138  set_passphrase(campaign, campaign["passphrase"]);
139  campaign["passphrase"] = "";
140  }
141  write_config();
142  }
143 
145 
146  start_server();
147  flush_cfg();
148 }
149 
151 {
152  write_config();
153 }
154 
156 {
157  LOG_CS << "Reading configuration from " << cfg_file_ << "...\n";
158 
160  read(cfg_, *in);
161 
162  read_only_ = cfg_["read_only"].to_bool(false);
163 
164  if(read_only_) {
165  LOG_CS << "READ-ONLY MODE ACTIVE\n";
166  }
167 
168  // Seems like compression level above 6 is a waste of CPU cycles.
169  compress_level_ = cfg_["compress_level"].to_int(6);
170 
171  const config& svinfo_cfg = server_info();
172  if(svinfo_cfg) {
173  feedback_url_format_ = svinfo_cfg["feedback_url_format"].str();
174  }
175 
176  blacklist_file_ = cfg_["blacklist_file"].str();
177  load_blacklist();
178 
179  stats_exempt_ips_ = utils::split(cfg_["stats_exempt_ips"].str());
180 
181  // Load any configured hooks.
182  hooks_.emplace(std::string("hook_post_upload"), cfg_["hook_post_upload"]);
183  hooks_.emplace(std::string("hook_post_erase"), cfg_["hook_post_erase"]);
184 
185 #ifndef _WIN32
186  // Open the control socket if enabled.
187  if(!cfg_["control_socket"].empty()) {
188  const std::string& path = cfg_["control_socket"].str();
189 
190  if(path != fifo_path_) {
191  const int res = mkfifo(path.c_str(),0660);
192  if(res != 0 && errno != EEXIST) {
193  ERR_CS << "could not make fifo at '" << path << "' (" << strerror(errno) << ")\n";
194  } else {
195  input_.close();
196  int fifo = open(path.c_str(), O_RDWR|O_NONBLOCK);
197  input_.assign(fifo);
198  LOG_CS << "opened fifo at '" << path << "'. Server commands may be written to this file.\n";
199  read_from_fifo();
200  fifo_path_ = path;
201  }
202  }
203  }
204 #endif
205 
206  // Ensure the campaigns list WML exists even if empty, other functions
207  // depend on its existence.
208  cfg_.child_or_add("campaigns");
209 
210  // Certain config values are saved to WML again so that a given server
211  // instance's parameters remain constant even if the code defaults change
212  // at some later point.
213  cfg_["compress_level"] = compress_level_;
214 
215  // But not the listening port number.
216  port_ = cfg_["port"].to_int(default_campaignd_port);
217 
218  // Limit the max size of WML documents received from the net to prevent the
219  // possible excessive use of resources due to malformed packets received.
220  // Since an addon is sent in a single WML document this essentially limits
221  // the maximum size of an addon that can be uploaded.
223 }
224 
226 {
227  async_receive_doc(socket,
228  std::bind(&server::handle_request, this, _1, _2)
229  );
230 }
231 
232 void server::handle_request(socket_ptr socket, std::shared_ptr<simple_wml::document> doc)
233 {
234  config data;
235  read(data, doc->output());
236 
238 
239  if(i != data.ordered_end()) {
240  // We only handle the first child.
241  const config::any_child& c = *i;
242 
243  request_handlers_table::const_iterator j
244  = handlers_.find(c.key);
245 
246  if(j != handlers_.end()) {
247  // Call the handler.
248  j->second(this, request(c.key, c.cfg, socket));
249  } else {
250  send_error("Unrecognized [" + c.key + "] request.",socket);
251  }
252  }
253 }
254 
255 #ifndef _WIN32
256 
257 void server::handle_read_from_fifo(const boost::system::error_code& error, std::size_t)
258 {
259  if(error) {
260  if(error == boost::asio::error::operation_aborted)
261  // This means fifo was closed by load_config() to open another fifo
262  return;
263  ERR_CS << "Error reading from fifo: " << error.message() << '\n';
264  return;
265  }
266 
267  std::istream is(&admin_cmd_);
268  std::string cmd;
269  std::getline(is, cmd);
270 
271  const control_line ctl = cmd;
272 
273  if(ctl == "shut_down") {
274  LOG_CS << "Shut down requested by admin, shutting down...\n";
275  throw server_shutdown("Shut down via fifo command");
276  } else if(ctl == "readonly") {
277  if(ctl.args_count()) {
278  cfg_["read_only"] = read_only_ = utils::string_bool(ctl[1], true);
279  }
280 
281  LOG_CS << "Read only mode: " << (read_only_ ? "enabled" : "disabled") << '\n';
282  } else if(ctl == "flush") {
283  LOG_CS << "Flushing config to disk...\n";
284  write_config();
285  } else if(ctl == "reload") {
286  if(ctl.args_count()) {
287  if(ctl[1] == "blacklist") {
288  LOG_CS << "Reloading blacklist...\n";
289  load_blacklist();
290  } else {
291  ERR_CS << "Unrecognized admin reload argument: " << ctl[1] << '\n';
292  }
293  } else {
294  LOG_CS << "Reloading all configuration...\n";
295  load_config();
296  LOG_CS << "Reloaded configuration\n";
297  }
298  } else if(ctl == "delete") {
299  if(ctl.args_count() != 1) {
300  ERR_CS << "Incorrect number of arguments for 'delete'\n";
301  } else {
302  const std::string& addon_id = ctl[1];
303 
304  LOG_CS << "deleting add-on '" << addon_id << "' requested from control FIFO\n";
305  delete_campaign(addon_id);
306  }
307  } else if(ctl == "hide" || ctl == "unhide") {
308  if(ctl.args_count() != 1) {
309  ERR_CS << "Incorrect number of arguments for '" << ctl.cmd() << "'\n";
310  } else {
311  const std::string& addon_id = ctl[1];
312  config& campaign = get_campaign(addon_id);
313 
314  if(!campaign) {
315  ERR_CS << "Add-on '" << addon_id << "' not found, cannot " << ctl.cmd() << "\n";
316  } else {
317  campaign["hidden"] = ctl.cmd() == "hide";
318  write_config();
319  LOG_CS << "Add-on '" << addon_id << "' is now " << (ctl.cmd() == "hide" ? "hidden" : "unhidden") << '\n';
320  }
321  }
322  } else if(ctl == "setpass") {
323  if(ctl.args_count() != 2) {
324  ERR_CS << "Incorrect number of arguments for 'setpass'\n";
325  } else {
326  const std::string& addon_id = ctl[1];
327  const std::string& newpass = ctl[2];
328  config& campaign = get_campaign(addon_id);
329 
330  if(!campaign) {
331  ERR_CS << "Add-on '" << addon_id << "' not found, cannot set passphrase\n";
332  } else if(newpass.empty()) {
333  // Shouldn't happen!
334  ERR_CS << "Add-on passphrases may not be empty!\n";
335  } else {
336  set_passphrase(campaign, newpass);
337  write_config();
338  LOG_CS << "New passphrase set for '" << addon_id << "'\n";
339  }
340  }
341  } else if(ctl == "setattr") {
342  if(ctl.args_count() != 3) {
343  ERR_CS << "Incorrect number of arguments for 'setattr'\n";
344  } else {
345  const std::string& addon_id = ctl[1];
346  const std::string& key = ctl[2];
347  const std::string& value = ctl[3];
348 
349  config& campaign = get_campaign(addon_id);
350 
351  if(!campaign) {
352  ERR_CS << "Add-on '" << addon_id << "' not found, cannot set attribute\n";
353  } else if(key == "name") {
354  ERR_CS << "setattr cannot be used to rename add-ons\n";
355  } else if(key == "passphrase" || key == "passhash"|| key == "passsalt") {
356  ERR_CS << "setattr cannot be used to set auth data -- use setpass instead\n";
357  } else if(!campaign.has_attribute(key)) {
358  // NOTE: This is a very naive approach for validating setattr's
359  // input, but it should generally work since add-on
360  // uploads explicitly set all recognized attributes to
361  // the values provided by the .pbl data or the empty
362  // string if absent, and this is normally preserved by
363  // the config serialization.
364  ERR_CS << "Attribute '" << value << "' is not a recognized add-on attribute\n";
365  } else {
366  campaign[key] = value;
367  write_config();
368  LOG_CS << "Set attribute on add-on '" << addon_id << "':\n"
369  << key << "=\"" << value << "\"\n";
370  }
371  }
372  } else {
373  ERR_CS << "Unrecognized admin command: " << ctl.full() << '\n';
374  }
375 
376  read_from_fifo();
377 }
378 
379 void server::handle_sighup(const boost::system::error_code&, int)
380 {
381  LOG_CS << "SIGHUP caught, reloading config.\n";
382 
383  load_config(); // TODO: handle port number config changes
384 
385  LOG_CS << "Reloaded configuration\n";
386 
387  sighup_.async_wait(std::bind(&server::handle_sighup, this, _1, _2));
388 }
389 
390 #endif
391 
393 {
394  flush_timer_.expires_from_now(std::chrono::minutes(10));
395  flush_timer_.async_wait(std::bind(&server::handle_flush, this, _1));
396 }
397 
398 void server::handle_flush(const boost::system::error_code& error)
399 {
400  if(error) {
401  ERR_CS << "Error from reload timer: " << error.message() << "\n";
402  throw boost::system::system_error(error);
403  }
404  write_config();
405  flush_cfg();
406 }
407 
409 {
410  // We *always* want to clear the blacklist first, especially if we are
411  // reloading the configuration and the blacklist is no longer enabled.
412  blacklist_.clear();
413 
414  if(blacklist_file_.empty()) {
415  return;
416  }
417 
418  try {
420  config blcfg;
421 
422  read(blcfg, *in);
423 
424  blacklist_.read(blcfg);
425  LOG_CS << "using blacklist from " << blacklist_file_ << '\n';
426  } catch(const config::error&) {
427  ERR_CS << "failed to read blacklist from " << blacklist_file_ << ", blacklist disabled\n";
428  }
429 }
430 
432 {
433  DBG_CS << "writing configuration and add-ons list to disk...\n";
435  write(*out.ostream(), cfg_);
436  out.commit();
437  DBG_CS << "... done\n";
438 }
439 
440 void server::fire(const std::string& hook, const std::string& addon)
441 {
442  const std::map<std::string, std::string>::const_iterator itor = hooks_.find(hook);
443  if(itor == hooks_.end()) {
444  return;
445  }
446 
447  const std::string& script = itor->second;
448  if(script.empty()) {
449  return;
450  }
451 
452 #if defined(_WIN32)
453  UNUSED(addon);
454  ERR_CS << "Tried to execute a script on an unsupported platform\n";
455  return;
456 #else
457  pid_t childpid;
458 
459  if((childpid = fork()) == -1) {
460  ERR_CS << "fork failed while updating campaign " << addon << '\n';
461  return;
462  }
463 
464  if(childpid == 0) {
465  // We are the child process. Execute the script. We run as a
466  // separate thread sharing stdout/stderr, which will make the
467  // log look ugly.
468  execlp(script.c_str(), script.c_str(), addon.c_str(), static_cast<char *>(nullptr));
469 
470  // exec() and family never return; if they do, we have a problem
471  std::cerr << "ERROR: exec failed with errno " << errno << " for addon " << addon
472  << '\n';
473  exit(errno);
474 
475  } else {
476  return;
477  }
478 #endif
479 }
480 
481 bool server::ignore_address_stats(const std::string& addr) const
482 {
483  for(const auto& mask : stats_exempt_ips_) {
484  // TODO: we want CIDR subnet mask matching here, not glob matching!
485  if(utils::wildcard_string_match(addr, mask)) {
486  return true;
487  }
488  }
489 
490  return false;
491 }
492 
493 void server::send_message(const std::string& msg, socket_ptr sock)
494 {
496  doc.root().add_child("message").set_attr_dup("message", msg.c_str());
497  async_send_doc(sock, doc, std::bind(&server::handle_new_client, this, _1), null_handler);
498 }
499 
500 void server::send_error(const std::string& msg, socket_ptr sock)
501 {
502  ERR_CS << "[" << client_address(sock) << "]: " << msg << '\n';
504  doc.root().add_child("error").set_attr_dup("message", msg.c_str());
505  async_send_doc(sock, doc, std::bind(&server::handle_new_client, this, _1), null_handler);
506 }
507 
508 void server::send_error(const std::string& msg, const std::string& extra_data, socket_ptr sock)
509 {
510  ERR_CS << "[" << client_address(sock) << "]: " << msg << '\n';
512  simple_wml::node& err_cfg = doc.root().add_child("error");
513  err_cfg.set_attr_dup("message", msg.c_str());
514  err_cfg.set_attr_dup("extra_data", extra_data.c_str());
515  async_send_doc(sock, doc, std::bind(&server::handle_new_client, this, _1), null_handler);
516 }
517 
518 void server::delete_campaign(const std::string& id)
519 {
520  config::child_itors itors = campaigns().child_range("campaign");
521 
522  std::size_t pos = 0;
523  bool found = false;
524  std::string fn;
525 
526  for(config& cfg : itors) {
527  if(cfg["name"] == id) {
528  fn = cfg["filename"].str();
529  found = true;
530  break;
531  }
532 
533  ++pos;
534  }
535 
536  if(!found) {
537  ERR_CS << "Cannot delete unrecognized add-on '" << id << "'\n";
538  return;
539  }
540 
541  if(fn.empty()) {
542  ERR_CS << "Add-on '" << id << "' does not have an associated filename, cannot delete\n";
543  }
544 
545  filesystem::write_file(fn, {});
546  if(std::remove(fn.c_str()) != 0) {
547  ERR_CS << "Could not delete archive for campaign '" << id
548  << "' (" << fn << "): " << strerror(errno) << '\n';
549  }
550 
551  campaigns().remove_child("campaign", pos);
552  write_config();
553 
554  fire("hook_post_erase", id);
555 
556  LOG_CS << "Deleted add-on '" << id << "'\n";
557 }
558 
559 #define REGISTER_CAMPAIGND_HANDLER(req_id) \
560  handlers_[#req_id] = std::bind(&server::handle_##req_id, \
561  std::placeholders::_1, std::placeholders::_2)
562 
564 {
565  REGISTER_CAMPAIGND_HANDLER(request_campaign_list);
566  REGISTER_CAMPAIGND_HANDLER(request_campaign);
567  REGISTER_CAMPAIGND_HANDLER(request_terms);
570  REGISTER_CAMPAIGND_HANDLER(change_passphrase);
571 }
572 
574 {
575  LOG_CS << "sending campaign list to " << req.addr << " using gzip\n";
576 
577  std::time_t epoch = std::time(nullptr);
578  config campaign_list;
579 
580  campaign_list["timestamp"] = epoch;
581  if(req.cfg["times_relative_to"] != "now") {
582  epoch = 0;
583  }
584 
585  bool before_flag = false;
586  std::time_t before = epoch;
587  if(!req.cfg["before"].empty()) {
588  before += req.cfg["before"].to_time_t();
589  before_flag = true;
590  }
591 
592  bool after_flag = false;
593  std::time_t after = epoch;
594  if(!req.cfg["after"].empty()) {
595  after += req.cfg["after"].to_time_t();
596  after_flag = true;
597  }
598 
599  const std::string& name = req.cfg["name"];
600  const std::string& lang = req.cfg["language"];
601 
602  for(const config& i : campaigns().child_range("campaign"))
603  {
604  if(!name.empty() && name != i["name"]) {
605  continue;
606  }
607 
608  if(i["hidden"].to_bool()) {
609  continue;
610  }
611 
612  const auto& tm = i["timestamp"];
613 
614  if(before_flag && (tm.empty() || tm.to_time_t(0) >= before)) {
615  continue;
616  }
617  if(after_flag && (tm.empty() || tm.to_time_t(0) <= after)) {
618  continue;
619  }
620 
621  if(!lang.empty()) {
622  bool found = false;
623 
624  for(const config& j : i.child_range("translation"))
625  {
626  if(j["language"] == lang) {
627  found = true;
628  break;
629  }
630  }
631 
632  if(!found) {
633  continue;
634  }
635  }
636 
637  campaign_list.add_child("campaign", i);
638  }
639 
640  for(config& j : campaign_list.child_range("campaign"))
641  {
642  // Remove attributes containing information that's considered sensitive
643  // or irrelevant to clients
644  j.remove_attributes("passphrase", "passhash", "passsalt", "upload_ip", "email");
645 
646  // Build a feedback_url string attribute from the internal [feedback]
647  // data or deliver an empty value, in case clients decide to assume its
648  // presence.
649  const config& url_params = j.child_or_empty("feedback");
650  j["feedback_url"] = !url_params.empty() && !feedback_url_format_.empty()
652 
653  // Clients don't need to see the original data, so discard it.
654  j.clear_children("feedback");
655  }
656 
657  config response;
658  response.add_child("campaigns", std::move(campaign_list));
659 
660  std::ostringstream ostr;
661  write(ostr, response);
662  std::string wml = ostr.str();
664  doc.compress();
665 
666  async_send_doc(req.sock, doc, std::bind(&server::handle_new_client, this, _1));
667 }
668 
670 {
671  config& campaign = get_campaign(req.cfg["name"]);
672 
673  if(!campaign || campaign["hidden"].to_bool()) {
674  send_error("Add-on '" + req.cfg["name"].str() + "' not found.", req.sock);
675  return;
676  }
677 
678  const int size = filesystem::file_size(campaign["filename"]);
679 
680  if(size < 0) {
681  send_error("Add-on '" + req.cfg["name"].str() + "' could not be read by the server.", req.sock);
682  return;
683  }
684 
685  LOG_CS << "sending campaign '" << req.cfg["name"] << "' to " << req.addr << " size: " << size/1024 << "KiB\n";
686  async_send_file(req.sock, campaign["filename"], std::bind(&server::handle_new_client, this, _1), null_handler);
687 
688  // Clients doing upgrades or some other specific thing shouldn't bump
689  // the downloads count. Default to true for compatibility with old
690  // clients that won't tell us what they are trying to do.
691  if(req.cfg["increase_downloads"].to_bool(true) && !ignore_address_stats(req.addr)) {
692  const int downloads = campaign["downloads"].to_int() + 1;
693  campaign["downloads"] = downloads;
694  }
695 }
696 
698 {
699  // This usually means the client wants to upload content, so tell it
700  // to give up when we're in read-only mode.
701  if(read_only_) {
702  LOG_CS << "in read-only mode, request for upload terms denied\n";
703  send_error("The server is currently in read-only mode, add-on uploads are disabled.", req.sock);
704  return;
705  }
706 
707  // TODO: possibly move to server.cfg
708  static const std::string terms = R"""(All content within add-ons uploaded to this server must be licensed under the terms of the GNU General Public License (GPL), with the sole exception of graphics and audio explicitly denoted as released under a Creative Commons license either in:
709 
710  a) a combined toplevel file, e.g. “My_Addon/ART_LICENSE”; <b>or</b>
711  b) a file with the same path as the asset with “.license” appended, e.g. “My_Addon/images/units/axeman.png.license”.
712 
713 <b>By uploading content to this server, you certify that you have the right to:</b>
714 
715  a) release all included art and audio explicitly denoted with a Creative Commons license in the proscribed manner under that license; <b>and</b>
716  b) release all other included content under the terms of the GPL; and that you choose to do so.)""";
717 
718  LOG_CS << "sending terms " << req.addr << "\n";
719  send_message(terms, req.sock);
720  LOG_CS << " Done\n";
721 }
722 
724 {
725  const config& upload = req.cfg;
726 
727  LOG_CS << "uploading campaign '" << upload["name"] << "' from " << req.addr << ".\n";
728  config data = upload.child("data");
729 
730  const std::string& name = upload["name"];
731  config *campaign = nullptr;
732 
733  bool passed_name_utf8_check = false;
734 
735  try {
736  const std::string& lc_name = utf8::lowercase(name);
737  passed_name_utf8_check = true;
738 
739  for(config& c : campaigns().child_range("campaign"))
740  {
741  if(utf8::lowercase(c["name"]) == lc_name) {
742  campaign = &c;
743  break;
744  }
745  }
746  } catch(const utf8::invalid_utf8_exception&) {
747  if(!passed_name_utf8_check) {
748  LOG_CS << "Upload aborted - invalid_utf8_exception caught on handle_upload() check 1, "
749  << "the add-on pbl info contains invalid UTF-8\n";
750  send_error("Add-on rejected: The add-on name contains an invalid UTF-8 sequence.", req.sock);
751  } else {
752  LOG_CS << "Upload aborted - invalid_utf8_exception caught on handle_upload() check 2, "
753  << "the internal add-ons list contains invalid UTF-8\n";
754  send_error("Server error: The server add-ons list is damaged.", req.sock);
755  }
756 
757  return;
758  }
759 
760  std::vector<std::string> badnames;
761 
762  if(read_only_) {
763  LOG_CS << "Upload aborted - uploads not permitted in read-only mode.\n";
764  send_error("Add-on rejected: The server is currently in read-only mode.", req.sock);
765  } else if(!data) {
766  LOG_CS << "Upload aborted - no add-on data.\n";
767  send_error("Add-on rejected: No add-on data was supplied.", req.sock);
768  } else if(!addon_name_legal(upload["name"])) {
769  LOG_CS << "Upload aborted - invalid add-on name.\n";
770  send_error("Add-on rejected: The name of the add-on is invalid.", req.sock);
771  } else if(is_text_markup_char(upload["name"].str()[0])) {
772  LOG_CS << "Upload aborted - add-on name starts with an illegal formatting character.\n";
773  send_error("Add-on rejected: The name of the add-on starts with an illegal formatting character.", req.sock);
774  } else if(upload["title"].empty()) {
775  LOG_CS << "Upload aborted - no add-on title specified.\n";
776  send_error("Add-on rejected: You did not specify the title of the add-on in the pbl file!", req.sock);
777  } else if(is_text_markup_char(upload["title"].str()[0])) {
778  LOG_CS << "Upload aborted - add-on title starts with an illegal formatting character.\n";
779  send_error("Add-on rejected: The title of the add-on starts with an illegal formatting character.", req.sock);
780  } else if(get_addon_type(upload["type"]) == ADDON_UNKNOWN) {
781  LOG_CS << "Upload aborted - unknown add-on type specified.\n";
782  send_error("Add-on rejected: You did not specify a known type for the add-on in the pbl file! (See PblWML: wiki.wesnoth.org/PblWML)", req.sock);
783  } else if(upload["author"].empty()) {
784  LOG_CS << "Upload aborted - no add-on author specified.\n";
785  send_error("Add-on rejected: You did not specify the author(s) of the add-on in the pbl file!", req.sock);
786  } else if(upload["version"].empty()) {
787  LOG_CS << "Upload aborted - no add-on version specified.\n";
788  send_error("Add-on rejected: You did not specify the version of the add-on in the pbl file!", req.sock);
789  } else if(upload["description"].empty()) {
790  LOG_CS << "Upload aborted - no add-on description specified.\n";
791  send_error("Add-on rejected: You did not specify a description of the add-on in the pbl file!", req.sock);
792  } else if(upload["email"].empty()) {
793  LOG_CS << "Upload aborted - no add-on email specified.\n";
794  send_error("Add-on rejected: You did not specify your email address in the pbl file!", req.sock);
795  } else if(!check_names_legal(data, &badnames)) {
796  const std::string& filelist = utils::join(badnames, "\n");
797  LOG_CS << "Upload aborted - invalid file names in add-on data (" << badnames.size() << " entries).\n";
798  send_error(
799  "Add-on rejected: The add-on contains files or directories with illegal names. "
800  // Note: the double double quote will be flattened to a single double quote.
801  "File or directory names may not contain whitespace, control characters or any of the following characters: '\"\" * / : < > ? \\ | ~'. "
802  "It also may not contain '..' end with '.' or be longer than 255 characters.",
803  filelist, req.sock);
804  } else if(!check_case_insensitive_duplicates(data, &badnames)) {
805  const std::string& filelist = utils::join(badnames, "\n");
806  LOG_CS << "Upload aborted - case conflict in add-on data (" << badnames.size() << " entries).\n";
807  send_error(
808  "Add-on rejected: The add-on contains files or directories with case conflicts. "
809  "File or directory names may not be differently-cased versions of the same string.",
810  filelist, req.sock);
811  } else if(upload["passphrase"].empty()) {
812  LOG_CS << "Upload aborted - missing passphrase.\n";
813  send_error("No passphrase was specified.", req.sock);
814  } else if(campaign && !authenticate(*campaign, upload["passphrase"])) {
815  LOG_CS << "Upload aborted - incorrect passphrase.\n";
816  send_error("Add-on rejected: The add-on already exists, and your passphrase was incorrect.", req.sock);
817  } else if(campaign && (*campaign)["hidden"].to_bool()) {
818  LOG_CS << "Upload denied - hidden add-on.\n";
819  send_error("Add-on upload denied. Please contact the server administration for assistance.", req.sock);
820  } else {
821  const std::time_t upload_ts = std::time(nullptr);
822 
823  LOG_CS << "Upload is owner upload.\n";
824 
825  try {
826  if(blacklist_.is_blacklisted(name,
827  upload["title"].str(),
828  upload["description"].str(),
829  upload["author"].str(),
830  req.addr,
831  upload["email"].str()))
832  {
833  LOG_CS << "Upload denied - blacklisted add-on information.\n";
834  send_error("Add-on upload denied. Please contact the server administration for assistance.", req.sock);
835  return;
836  }
837  } catch(const utf8::invalid_utf8_exception&) {
838  LOG_CS << "Upload aborted - the add-on pbl info contains invalid UTF-8 and cannot be "
839  << "checked against the blacklist\n";
840  send_error("Add-on rejected: The add-on publish information contains an invalid UTF-8 sequence.", req.sock);
841  return;
842  }
843 
844  const bool existing_upload = campaign != nullptr;
845 
846  std::string message = "Add-on accepted.";
847 
848  if(campaign == nullptr) {
849  campaign = &campaigns().add_child("campaign");
850  (*campaign)["original_timestamp"] = upload_ts;
851  }
852 
853  (*campaign)["title"] = upload["title"];
854  (*campaign)["name"] = upload["name"];
855  (*campaign)["filename"] = "data/" + upload["name"].str();
856  (*campaign)["author"] = upload["author"];
857  (*campaign)["description"] = upload["description"];
858  (*campaign)["version"] = upload["version"];
859  (*campaign)["icon"] = upload["icon"];
860  (*campaign)["translate"] = upload["translate"];
861  (*campaign)["dependencies"] = upload["dependencies"];
862  (*campaign)["upload_ip"] = req.addr;
863  (*campaign)["type"] = upload["type"];
864  (*campaign)["tags"] = upload["tags"];
865  (*campaign)["email"] = upload["email"];
866 
867  if(!existing_upload) {
868  set_passphrase(*campaign, upload["passphrase"]);
869  }
870 
871  if((*campaign)["downloads"].empty()) {
872  (*campaign)["downloads"] = 0;
873  }
874  (*campaign)["timestamp"] = upload_ts;
875 
876  int uploads = (*campaign)["uploads"].to_int() + 1;
877  (*campaign)["uploads"] = uploads;
878 
879  (*campaign).clear_children("feedback");
880  if(const config& url_params = upload.child("feedback")) {
881  (*campaign).add_child("feedback", url_params);
882  }
883 
884  const std::string& filename = (*campaign)["filename"].str();
885  data["title"] = (*campaign)["title"];
886  data["name"] = "";
887  data["campaign_name"] = (*campaign)["name"];
888  data["author"] = (*campaign)["author"];
889  data["description"] = (*campaign)["description"];
890  data["version"] = (*campaign)["version"];
891  data["timestamp"] = (*campaign)["timestamp"];
892  data["original_timestamp"] = (*campaign)["original_timestamp"];
893  data["icon"] = (*campaign)["icon"];
894  data["type"] = (*campaign)["type"];
895  data["tags"] = (*campaign)["tags"];
896  (*campaign).clear_children("translation");
897  find_translations(data, *campaign);
898 
899  add_license(data);
900 
901  {
902  filesystem::atomic_commit campaign_file(filename);
903  config_writer writer(*campaign_file.ostream(), true, compress_level_);
904  writer.write(data);
905  campaign_file.commit();
906  }
907 
908  (*campaign)["size"] = filesystem::file_size(filename);
909 
910  write_config();
911 
912  send_message(message, req.sock);
913 
914  fire("hook_post_upload", upload["name"]);
915  }
916 }
917 
919 {
920  const config& erase = req.cfg;
921  const std::string& id = erase["name"].str();
922 
923  if(read_only_) {
924  LOG_CS << "in read-only mode, request to delete '" << id << "' from " << req.addr << " denied\n";
925  send_error("Cannot delete add-on: The server is currently in read-only mode.", req.sock);
926  return;
927  }
928 
929  LOG_CS << "deleting campaign '" << id << "' requested from " << req.addr << "\n";
930 
931  config& campaign = get_campaign(id);
932 
933  if(!campaign) {
934  send_error("The add-on does not exist.", req.sock);
935  return;
936  }
937 
938  const config::attribute_value& pass = erase["passphrase"];
939 
940  if(pass.empty()) {
941  send_error("No passphrase was specified.", req.sock);
942  return;
943  }
944 
945  if(!authenticate(campaign, pass)) {
946  send_error("The passphrase is incorrect.", req.sock);
947  return;
948  }
949 
950  if(campaign["hidden"].to_bool()) {
951  LOG_CS << "Add-on removal denied - hidden add-on.\n";
952  send_error("Add-on deletion denied. Please contact the server administration for assistance.", req.sock);
953  return;
954  }
955 
956  delete_campaign(id);
957 
958  send_message("Add-on deleted.", req.sock);
959 }
960 
962 {
963  const config& cpass = req.cfg;
964 
965  if(read_only_) {
966  LOG_CS << "in read-only mode, request to change passphrase denied\n";
967  send_error("Cannot change passphrase: The server is currently in read-only mode.", req.sock);
968  return;
969  }
970 
971  config& campaign = get_campaign(cpass["name"]);
972 
973  if(!campaign) {
974  send_error("No add-on with that name exists.", req.sock);
975  } else if(!authenticate(campaign, cpass["passphrase"])) {
976  send_error("Your old passphrase was incorrect.", req.sock);
977  } else if(campaign["hidden"].to_bool()) {
978  LOG_CS << "Passphrase change denied - hidden add-on.\n";
979  send_error("Add-on passphrase change denied. Please contact the server administration for assistance.", req.sock);
980  } else if(cpass["new_passphrase"].empty()) {
981  send_error("No new passphrase was supplied.", req.sock);
982  } else {
983  set_passphrase(campaign, cpass["new_passphrase"]);
984  write_config();
985  send_message("Passphrase changed.", req.sock);
986  }
987 }
988 
989 } // end namespace campaignd
990 
991 int main()
992 {
994 
995  lg::set_log_domain_severity("campaignd", lg::info());
997  lg::timestamps(true);
998 
999  try {
1000  std::cerr << "Wesnoth campaignd v" << game_config::revision << " starting...\n";
1001 
1002  const std::string cfg_path = filesystem::normalize_path("server.cfg");
1003 
1004  campaignd::server(cfg_path).run();
1005  } catch(const config::error& /*e*/) {
1006  std::cerr << "Could not parse config file\n";
1007  return 1;
1008  } catch(const filesystem::io_exception& e) {
1009  std::cerr << "File I/O error: " << e.what() << "\n";
1010  return 2;
1011  } catch(const std::bad_function_call& /*e*/) {
1012  std::cerr << "Bad request handler function call\n";
1013  return 4;
1014  }
1015 
1016  return 0;
1017 }
node & add_child(const char *name)
Definition: simple_wml.cpp:464
bool empty() const
Tests for an attribute that either was never set or was set to "".
void remove()
Removes a tip.
Definition: tooltip.cpp:189
bool check_names_legal(const config &dir, std::vector< std::string > *badlist)
Scans an add-on archive for illegal names.
Definition: validation.cpp:223
std::string feedback_url_format_
static lg::log_domain log_campaignd("campaignd")
bool ignore_address_stats(const std::string &addr) const
Checks if the specified address should never bump download counts.
config & child(config_key_type key, int n=0)
Returns the nth child with the given key, or a reference to an invalid config if there is none...
Definition: config.cpp:420
bool check_case_insensitive_duplicates(const config &dir, std::vector< std::string > *badlist)
Scans an add-on archive for case-conflicts.
Definition: validation.cpp:232
Legacy add-ons server.
void handle_request_terms(const request &)
void clear_children(T... keys)
Definition: config.hpp:479
static const std::size_t default_document_size_limit
Default upload size limit in bytes.
Interfaces for manipulating version numbers of engine, add-ons, etc.
std::string join(const T &v, const std::string &s=",")
Generates a new string joining container items in a list.
virtual std::string base64_digest() const override
Definition: hash.cpp:121
void handle_flush(const boost::system::error_code &error)
void handle_request(socket_ptr socket, std::shared_ptr< simple_wml::document > doc)
Variant for storing WML attributes.
boost::asio::signal_set sighup_
Definition: server_base.hpp:75
static l_noret error(LoadState *S, const char *why)
Definition: lundump.cpp:39
New lexcical_cast header.
bool has_attribute(config_key_type key) const
Definition: config.cpp:213
logger & info()
Definition: log.cpp:90
void handle_delete(const request &)
std::string encode(utils::byte_string_view bytes)
Definition: base64.cpp:229
Client request information object.
#define LOG_CS
child_itors child_range(config_key_type key)
Definition: config.cpp:362
void load_config()
Reads the server configuration from WML.
void remove_attributes(T... keys)
Definition: config.hpp:455
void timestamps(bool t)
Definition: log.cpp:75
filesystem::scoped_istream istream_file(const std::string &fname, bool treat_failure_as_error)
Definition: filesystem.cpp:993
void delete_campaign(const std::string &id)
bool wildcard_string_match(const std::string &str, const std::string &match)
Match using &#39;*&#39; as any number of characters (including none), &#39;+&#39; as one or more characters, and &#39;?&#39; as any one character.
static lg::log_domain log_config("config")
#define ERR_CS
void fire(const std::string &hook, const std::string &addon)
Fires a hook script.
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
Definition: debugger.cpp:109
config & child_or_add(config_key_type key)
Definition: config.cpp:466
request_handlers_table handlers_
static void null_handler(socket_ptr)
std::string normalize_path(const std::string &fpath, bool normalize_separators, bool resolve_dot_entries)
Returns the absolute path of a file.
Wrapper class that guarantees that file commit atomicity.
Definition: fs_commit.hpp:51
void async_send_file(socket_ptr, const std::string &, Handler, ErrorHandler)
std::map< std::string, std::string > hooks_
void load_blacklist()
Reads the add-ons upload blacklist from WML.
std::vector< std::string > split(const std::string &val, const char c, const int flags)
Splits a (comma-)separated string into a vector of pieces.
void register_handlers()
Registers client request handlers.
unsigned short port_
Definition: server_base.hpp:45
void add_license(config &cfg)
Adds a COPYING.txt file with the full text of the GNU GPL to an add-on.
const std::string cfg_file_
std::string get_cwd()
Definition: filesystem.cpp:882
void start_server()
Definition: server_base.cpp:58
unsigned in
If equal to search_counter, the node is off the list.
const_all_children_iterator ordered_end() const
Definition: config.cpp:911
void write(std::ostream &out, const configr_of &cfg, unsigned int level)
Definition: parser.cpp:762
void handle_sighup(const boost::system::error_code &error, int signal_number)
boost::asio::basic_waitable_timer< std::chrono::steady_clock > flush_timer_
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_
Definition: server_base.hpp:73
std::string blacklist_file_
void read(config &cfg, std::istream &in, abstract_validator *validator)
Definition: parser.cpp:625
Class for writing a config out to a file in pieces.
void write_file(const std::string &fname, const std::string &data)
Throws io_exception if an error occurs.
void commit()
Commits the new file contents to disk atomically.
Definition: fs_commit.cpp:90
const child_map::key_type & key
Definition: config.hpp:521
void erase(const std::string &key)
Definition: general.cpp:221
std::unique_ptr< std::istream > scoped_istream
Definition: filesystem.hpp:39
void handle_read_from_fifo(const boost::system::error_code &error, std::size_t bytes_transferred)
std::string path
Definition: game_config.cpp:39
scoped_ostream & ostream()
Returns the write stream associated with the file.
Definition: fs_commit.hpp:72
#define REGISTER_CAMPAIGND_HANDLER(req_id)
void handle_change_passphrase(const request &)
const char * what() const noexcept
Definition: exceptions.hpp:37
#define DBG_CS
ADDON_TYPE get_addon_type(const std::string &str)
Definition: validation.cpp:236
static std::size_t document_size_limit
Definition: simple_wml.hpp:289
bool is_blacklisted(const std::string &name, const std::string &title, const std::string &description, const std::string &author, const std::string &ip, const std::string &email) const
Whether an add-on described by these fields is blacklisted.
Definition: blacklist.cpp:75
#define UNUSED(x)
Definition: global.hpp:34
bool addon_name_legal(const std::string &name)
Checks whether an add-on id/name is legal or not.
Definition: validation.cpp:91
std::size_t args_count() const
Returns the total number of arguments, not including the command itself.
Definition: control.hpp:74
void write_config()
Writes the server configuration WML back to disk.
std::size_t i
Definition: function.cpp:933
void handle_new_client(socket_ptr socket)
Thrown by operations encountering invalid UTF-8 data.
const config & server_info() const
Retrieves the contents of the [server_info] WML node.
const std::string revision
node & set_attr_dup(const char *key, const char *value)
Definition: simple_wml.cpp:427
Atomic filesystem commit functions.
Represents a server control line written to a communication socket.
Definition: control.hpp:32
void handle_request_campaign(const request &)
An exception object used when an IO error occurs.
Definition: filesystem.hpp:48
int main()
std::string client_address(const socket_ptr socket)
bool string_bool(const std::string &str, bool def)
Convert no, false, off, 0, 0.0 to false, empty to def, and others to true.
Declarations for File-IO.
static int writer(lua_State *L, const void *b, size_t size, void *B)
Definition: lstrlib.cpp:182
void read_from_fifo()
std::string lowercase(const std::string &s)
Returns a lowercased version of the string.
Definition: unicode.cpp:51
boost::asio::posix::stream_descriptor input_
Definition: server_base.hpp:69
bool set_log_domain_severity(const std::string &name, int severity)
Definition: log.cpp:118
config & add_child(config_key_type key)
Definition: config.cpp:476
std::vector< std::string > stats_exempt_ips_
int file_size(const std::string &fname)
Returns the size of a file, or -1 if the file doesn&#39;t exist.
const_all_children_iterator ordered_begin() const
Definition: config.cpp:901
config & cfg
Definition: config.hpp:522
const config & campaigns() const
Retrieves the contents of the [campaigns] WML node.
void handle_request_campaign_list(const request &)
void async_send_doc(socket_ptr socket, simple_wml::document &doc, Handler handler, ErrorHandler error_handler)
void async_receive_doc(socket_ptr socket, Handler handler, ErrorHandler error_handler)
const unsigned short default_campaignd_port
Default port number for the addon server.
Definition: validation.cpp:25
std::string fifo_path_
Definition: server_base.hpp:70
const std::string & cmd() const
Returns the control command.
Definition: control.hpp:66
boost::iterator_range< child_iterator > child_itors
Definition: config.hpp:209
void flush_cfg()
Starts timer to write config to disk every ten minutes.
Standard logging facilities (interface).
static lg::log_domain log_server("server")
void read(const config &cfg)
Initializes the blacklist from WML.
Definition: blacklist.cpp:59
std::shared_ptr< boost::asio::ip::tcp::socket > socket_ptr
Definition: server_base.hpp:30
std::string full() const
Return the full command line string.
Definition: control.hpp:92
bool is_text_markup_char(char c)
Definition: addon_utils.hpp:31
void find_translations(const config &base_dir, config &addon)
Scans an add-on archive directory for translations.
Definition: addon_utils.cpp:95
#define e
const config & child_or_empty(config_key_type key) const
Returns the first child with the given key, or an empty config if there is none.
Definition: config.cpp:453
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:68
void send_message(const std::string &msg, socket_ptr sock)
Send a client an informational message.
mock_char c
std::string format_addon_feedback_url(const std::string &format, const config &params)
Format a feedback URL for an add-on.
Definition: addon_utils.cpp:63
void send_error(const std::string &msg, socket_ptr sock)
Send a client an error message.
void remove_child(config_key_type key, unsigned index)
Definition: config.cpp:694
void handle_upload(const request &)
bool empty() const
Definition: config.cpp:884
server(const std::string &cfg_file)
config & get_campaign(const std::string &id)
Retrieves a campaign by id if found, or a null config otherwise.
int compress_level_
Used for add-on archives.