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