The Battle for Wesnoth  1.13.10+dev
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages
campaign_server.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2017 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"
28 #include "serialization/parser.hpp"
31 #include "game_config.hpp"
32 #include "addon/validation.hpp"
37 #include "version.hpp"
38 #include "hash.hpp"
39 
40 #include <csignal>
41 #include <ctime>
42 
43 #include <boost/iostreams/filter/gzip.hpp>
44 #include <boost/exception/get_error_info.hpp>
45 #include <boost/random.hpp>
46 #include <boost/generator_iterator.hpp>
47 
48 // the fork execute is unix specific only tested on Linux quite sure it won't
49 // work on Windows not sure which other platforms have a problem with it.
50 #if !(defined(_WIN32))
51 #include <errno.h>
52 #endif
53 
54 static lg::log_domain log_campaignd("campaignd");
55 #define DBG_CS LOG_STREAM(debug, log_campaignd)
56 #define LOG_CS LOG_STREAM(info, log_campaignd)
57 #define WRN_CS LOG_STREAM(warn, log_campaignd)
58 #define ERR_CS LOG_STREAM(err, log_campaignd)
59 
60 static lg::log_domain log_config("config");
61 #define ERR_CONFIG LOG_STREAM(err, log_config)
62 #define WRN_CONFIG LOG_STREAM(warn, log_config)
63 
64 static lg::log_domain log_server("server");
65 #define ERR_SERVER LOG_STREAM(err, log_server)
66 
68 
69 namespace {
70 
71 /* Secure password storage functions */
72 bool authenticate(config& campaign, const config::attribute_value& passphrase)
73 {
74  return utils::md5(passphrase, campaign["passsalt"]).base64_digest() == campaign["passhash"];
75 }
76 
77 std::string generate_salt(size_t len)
78 {
79  static const std::string itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
80  boost::mt19937 mt(time(0));
81  std::string salt = std::string(len, '0');
82  boost::uniform_int<> from_str(0, itoa64.length() - 1);
83  boost::variate_generator< boost::mt19937, boost::uniform_int<> > get_char(mt, from_str);
84 
85  for(size_t i = 0; i < len; i++) {
86  salt[i] = itoa64[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 == "setpass") {
301  if(ctl.args_count() != 2) {
302  ERR_CS << "Incorrect number of arguments for 'setpass'\n";
303  } else {
304  const std::string& addon_id = ctl[1];
305  const std::string& newpass = ctl[2];
306  config& campaign = get_campaign(addon_id);
307 
308  if(!campaign) {
309  ERR_CS << "Add-on '" << addon_id << "' not found, cannot set passphrase\n";
310  } else if(newpass.empty()) {
311  // Shouldn't happen!
312  ERR_CS << "Add-on passphrases may not be empty!\n";
313  } else {
314  set_passphrase(campaign, newpass);
315  write_config();
316  LOG_CS << "New passphrase set for '" << addon_id << "'\n";
317  }
318  }
319  } else {
320  ERR_CS << "Unrecognized admin command: " << ctl.full() << '\n';
321  }
322 
323  read_from_fifo();
324 }
325 
326 void server::handle_sighup(const boost::system::error_code&, int)
327 {
328  LOG_CS << "SIGHUP caught, reloading config.\n";
329 
330  load_config(); // TODO: handle port number config changes
331 
332  LOG_CS << "Reloaded configuration\n";
333 
334  sighup_.async_wait(std::bind(&server::handle_sighup, this, _1, _2));
335 }
336 
337 #endif
338 
340 {
341  flush_timer_.expires_from_now(std::chrono::minutes(10));
342  flush_timer_.async_wait(std::bind(&server::handle_flush, this, _1));
343 }
344 
345 void server::handle_flush(const boost::system::error_code& error)
346 {
347  if(error) {
348  ERR_CS << "Error from reload timer: " << error.message() << "\n";
349  throw boost::system::system_error(error);
350  }
351  write_config();
352  flush_cfg();
353 }
354 
356 {
357  // We *always* want to clear the blacklist first, especially if we are
358  // reloading the configuration and the blacklist is no longer enabled.
359  blacklist_.clear();
360 
361  if(blacklist_file_.empty()) {
362  return;
363  }
364 
365  try {
367  config blcfg;
368 
369  read(blcfg, *in);
370 
371  blacklist_.read(blcfg);
372  LOG_CS << "using blacklist from " << blacklist_file_ << '\n';
373  } catch(const config::error&) {
374  ERR_CS << "failed to read blacklist from " << blacklist_file_ << ", blacklist disabled\n";
375  }
376 }
377 
379 {
380  DBG_CS << "writing configuration and add-ons list to disk...\n";
382  write(*out.ostream(), cfg_);
383  out.commit();
384  DBG_CS << "... done\n";
385 }
386 
387 void server::fire(const std::string& hook, const std::string& addon)
388 {
389  const std::map<std::string, std::string>::const_iterator itor = hooks_.find(hook);
390  if(itor == hooks_.end()) {
391  return;
392  }
393 
394  const std::string& script = itor->second;
395  if(script.empty()) {
396  return;
397  }
398 
399 #if defined(_WIN32)
400  (void)addon;
401  ERR_CS << "Tried to execute a script on an unsupported platform\n";
402  return;
403 #else
404  pid_t childpid;
405 
406  if((childpid = fork()) == -1) {
407  ERR_CS << "fork failed while updating campaign " << addon << '\n';
408  return;
409  }
410 
411  if(childpid == 0) {
412  // We are the child process. Execute the script. We run as a
413  // separate thread sharing stdout/stderr, which will make the
414  // log look ugly.
415  execlp(script.c_str(), script.c_str(), addon.c_str(), static_cast<char *>(nullptr));
416 
417  // exec() and family never return; if they do, we have a problem
418  std::cerr << "ERROR: exec failed with errno " << errno << " for addon " << addon
419  << '\n';
420  exit(errno);
421 
422  } else {
423  return;
424  }
425 #endif
426 }
427 
429 {
430  for(const auto& mask : stats_exempt_ips_) {
431  // TODO: we want CIDR subnet mask matching here, not glob matching!
432  if(utils::wildcard_string_match(addr, mask)) {
433  return true;
434  }
435  }
436 
437  return false;
438 }
439 
441 {
443  doc.root().add_child("message").set_attr_dup("message", msg.c_str());
444  async_send_doc(sock, doc, std::bind(&server::handle_new_client, this, _1), null_handler);
445 }
446 
448 {
449  ERR_CS << "[" << client_address(sock) << "]: " << msg << '\n';
451  doc.root().add_child("error").set_attr_dup("message", msg.c_str());
452  async_send_doc(sock, doc, std::bind(&server::handle_new_client, this, _1), null_handler);
453 }
454 
455 void server::send_error(const std::string& msg, const std::string& extra_data, socket_ptr sock)
456 {
457  ERR_CS << "[" << client_address(sock) << "]: " << msg << '\n';
459  simple_wml::node& err_cfg = doc.root().add_child("error");
460  err_cfg.set_attr_dup("message", msg.c_str());
461  err_cfg.set_attr_dup("extra_data", extra_data.c_str());
462  async_send_doc(sock, doc, std::bind(&server::handle_new_client, this, _1), null_handler);
463 }
464 
465 #define REGISTER_CAMPAIGND_HANDLER(req_id) \
466  handlers_[#req_id] = std::bind(&server::handle_##req_id, \
467  std::placeholders::_1, std::placeholders::_2)
468 
470 {
471  REGISTER_CAMPAIGND_HANDLER(request_campaign_list);
472  REGISTER_CAMPAIGND_HANDLER(request_campaign);
473  REGISTER_CAMPAIGND_HANDLER(request_terms);
476  REGISTER_CAMPAIGND_HANDLER(change_passphrase);
477 }
478 
480 {
481  LOG_CS << "sending campaign list to " << req.addr << " using gzip\n";
482 
483  time_t epoch = time(nullptr);
484  config campaign_list;
485 
486  campaign_list["timestamp"] = epoch;
487  if(req.cfg["times_relative_to"] != "now") {
488  epoch = 0;
489  }
490 
491  bool before_flag = false;
492  time_t before = epoch;
493  try {
494  before = before + lexical_cast<time_t>(req.cfg["before"]);
495  before_flag = true;
496  } catch(bad_lexical_cast) {}
497 
498  bool after_flag = false;
499  time_t after = epoch;
500  try {
501  after = after + lexical_cast<time_t>(req.cfg["after"]);
502  after_flag = true;
503  } catch(bad_lexical_cast) {}
504 
505  const std::string& name = req.cfg["name"];
506  const std::string& lang = req.cfg["language"];
507 
508  for(const config& i : campaigns().child_range("campaign"))
509  {
510  if(!name.empty() && name != i["name"]) {
511  continue;
512  }
513 
514  const std::string& tm = i["timestamp"];
515 
516  if(before_flag && (tm.empty() || lexical_cast_default<time_t>(tm, 0) >= before)) {
517  continue;
518  }
519  if(after_flag && (tm.empty() || lexical_cast_default<time_t>(tm, 0) <= after)) {
520  continue;
521  }
522 
523  if(!lang.empty()) {
524  bool found = false;
525 
526  for(const config& j : i.child_range("translation"))
527  {
528  if(j["language"] == lang) {
529  found = true;
530  break;
531  }
532  }
533 
534  if(!found) {
535  continue;
536  }
537  }
538 
539  campaign_list.add_child("campaign", i);
540  }
541 
542  for(config& j : campaign_list.child_range("campaign"))
543  {
544  j["passphrase"] = "";
545  j["passhash"] = "";
546  j["passsalt"] = "";
547  j["upload_ip"] = "";
548  j["email"] = "";
549  j["feedback_url"] = "";
550 
551  // Build a feedback_url string attribute from the
552  // internal [feedback] data.
553  const config& url_params = j.child_or_empty("feedback");
554  if(!url_params.empty() && !feedback_url_format_.empty()) {
555  j["feedback_url"] = format_addon_feedback_url(feedback_url_format_, url_params);
556  }
557 
558  // Clients don't need to see the original data, so discard it.
559  j.clear_children("feedback");
560  }
561 
562  config response;
563  response.add_child("campaigns", std::move(campaign_list));
564 
565  std::ostringstream ostr;
566  write(ostr, response);
567  std::string wml = ostr.str();
569  doc.compress();
570 
571  async_send_doc(req.sock, doc, std::bind(&server::handle_new_client, this, _1));
572 }
573 
575 {
576  LOG_CS << "sending campaign '" << req.cfg["name"] << "' to " << req.addr << " using gzip\n";
577 
578  config& campaign = get_campaign(req.cfg["name"]);
579 
580  if(!campaign) {
581  send_error("Add-on '" + req.cfg["name"].str() + "' not found.", req.sock);
582  } else {
583  const int size = filesystem::file_size(campaign["filename"]);
584 
585  if(size < 0) {
586  std::cerr << " size: <unknown> KiB\n";
587  ERR_CS << "File size unknown, aborting send.\n";
588  send_error("Add-on '" + req.cfg["name"].str() + "' could not be read by the server.", req.sock);
589  return;
590  }
591 
592  std::cerr << " size: " << size/1024 << "KiB\n";
593  async_send_file(req.sock, campaign["filename"],
594  std::bind(&server::handle_new_client, this, _1), null_handler);
595  // Clients doing upgrades or some other specific thing shouldn't bump
596  // the downloads count. Default to true for compatibility with old
597  // clients that won't tell us what they are trying to do.
598  if(req.cfg["increase_downloads"].to_bool(true) && !ignore_address_stats(req.addr)) {
599  const int downloads = campaign["downloads"].to_int() + 1;
600  campaign["downloads"] = downloads;
601  }
602  }
603 }
604 
606 {
607  // This usually means the client wants to upload content, so tell it
608  // to give up when we're in read-only mode.
609  if(read_only_) {
610  LOG_CS << "in read-only mode, request for upload terms denied\n";
611  send_error("The server is currently in read-only mode, add-on uploads are disabled.", req.sock);
612  return;
613  }
614 
615  // TODO: possibly move to server.cfg
616  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:
617 
618  a) a combined toplevel file, e.g. “My_Addon/ART_LICENSE”; <b>or</b>
619  b) a file with the same path as the asset with “.license” appended, e.g. “My_Addon/images/units/axeman.png.license”.
620 
621 <b>By uploading content to this server, you certify that you have the right to:</b>
622 
623  a) release all included art and audio explicitly denoted with a Creative Commons license in the proscribed manner under that license; <b>and</b>
624  b) release all other included content under the terms of the GPL; and that you choose to do so.)""";
625 
626  LOG_CS << "sending terms " << req.addr << "\n";
627  send_message(terms, req.sock);
628  LOG_CS << " Done\n";
629 }
630 
632 {
633  const config& upload = req.cfg;
634 
635  LOG_CS << "uploading campaign '" << upload["name"] << "' from " << req.addr << ".\n";
636  config data = upload.child("data");
637 
638  const std::string& name = upload["name"];
639  config *campaign = nullptr;
640 
641  bool passed_name_utf8_check = false;
642 
643  try {
644  const std::string& lc_name = utf8::lowercase(name);
645  passed_name_utf8_check = true;
646 
647  for(config& c : campaigns().child_range("campaign"))
648  {
649  if(utf8::lowercase(c["name"]) == lc_name) {
650  campaign = &c;
651  break;
652  }
653  }
654  } catch(const utf8::invalid_utf8_exception&) {
655  if(!passed_name_utf8_check) {
656  LOG_CS << "Upload aborted - invalid_utf8_exception caught on handle_upload() check 1, "
657  << "the add-on pbl info contains invalid UTF-8\n";
658  send_error("Add-on rejected: The add-on name contains an invalid UTF-8 sequence.", req.sock);
659  } else {
660  LOG_CS << "Upload aborted - invalid_utf8_exception caught on handle_upload() check 2, "
661  << "the internal add-ons list contains invalid UTF-8\n";
662  send_error("Server error: The server add-ons list is damaged.", req.sock);
663  }
664 
665  return;
666  }
667 
668  std::vector<std::string> badnames;
669 
670  if(read_only_) {
671  LOG_CS << "Upload aborted - uploads not permitted in read-only mode.\n";
672  send_error("Add-on rejected: The server is currently in read-only mode.", req.sock);
673  } else if(!data) {
674  LOG_CS << "Upload aborted - no add-on data.\n";
675  send_error("Add-on rejected: No add-on data was supplied.", req.sock);
676  } else if(!addon_name_legal(upload["name"])) {
677  LOG_CS << "Upload aborted - invalid add-on name.\n";
678  send_error("Add-on rejected: The name of the add-on is invalid.", req.sock);
679  } else if(is_text_markup_char(upload["name"].str()[0])) {
680  LOG_CS << "Upload aborted - add-on name starts with an illegal formatting character.\n";
681  send_error("Add-on rejected: The name of the add-on starts with an illegal formatting character.", req.sock);
682  } else if(upload["title"].empty()) {
683  LOG_CS << "Upload aborted - no add-on title specified.\n";
684  send_error("Add-on rejected: You did not specify the title of the add-on in the pbl file!", req.sock);
685  } else if(is_text_markup_char(upload["title"].str()[0])) {
686  LOG_CS << "Upload aborted - add-on title starts with an illegal formatting character.\n";
687  send_error("Add-on rejected: The title of the add-on starts with an illegal formatting character.", req.sock);
688  } else if(get_addon_type(upload["type"]) == ADDON_UNKNOWN) {
689  LOG_CS << "Upload aborted - unknown add-on type specified.\n";
690  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);
691  } else if(upload["author"].empty()) {
692  LOG_CS << "Upload aborted - no add-on author specified.\n";
693  send_error("Add-on rejected: You did not specify the author(s) of the add-on in the pbl file!", req.sock);
694  } else if(upload["version"].empty()) {
695  LOG_CS << "Upload aborted - no add-on version specified.\n";
696  send_error("Add-on rejected: You did not specify the version of the add-on in the pbl file!", req.sock);
697  } else if(upload["description"].empty()) {
698  LOG_CS << "Upload aborted - no add-on description specified.\n";
699  send_error("Add-on rejected: You did not specify a description of the add-on in the pbl file!", req.sock);
700  } else if(upload["email"].empty()) {
701  LOG_CS << "Upload aborted - no add-on email specified.\n";
702  send_error("Add-on rejected: You did not specify your email address in the pbl file!", req.sock);
703  } else if(!check_names_legal(data, &badnames)) {
704  const std::string& filelist = utils::join(badnames, "\n");
705  LOG_CS << "Upload aborted - invalid file names in add-on data (" << badnames.size() << " entries).\n";
706  send_error(
707  "Add-on rejected: The add-on contains files or directories with illegal names. "
708  "File or directory names may not contain whitespace, control characters or any of the following characters: '\" * / : < > ? \\ | ~'. "
709  "It also may not contain '..' end with '.' or be longer than 255 characters.",
710  filelist, req.sock);
711  } else if(!check_case_insensitive_duplicates(data, &badnames)) {
712  const std::string& filelist = utils::join(badnames, "\n");
713  LOG_CS << "Upload aborted - case conflict in add-on data (" << badnames.size() << " entries).\n";
714  send_error(
715  "Add-on rejected: The add-on contains files or directories with case conflicts. "
716  "File or directory names may not be differently-cased versions of the same string.",
717  filelist, req.sock);
718  } else if(campaign && !authenticate(*campaign, upload["passphrase"])) {
719  LOG_CS << "Upload aborted - incorrect passphrase.\n";
720  send_error("Add-on rejected: The add-on already exists, and your passphrase was incorrect.", req.sock);
721  } else {
722  const time_t upload_ts = time(nullptr);
723 
724  LOG_CS << "Upload is owner upload.\n";
725 
726  try {
727  if(blacklist_.is_blacklisted(name,
728  upload["title"].str(),
729  upload["description"].str(),
730  upload["author"].str(),
731  req.addr,
732  upload["email"].str()))
733  {
734  LOG_CS << "Upload denied - blacklisted add-on information.\n";
735  send_error("Add-on upload denied. Please contact the server administration for assistance.", req.sock);
736  return;
737  }
738  } catch(const utf8::invalid_utf8_exception&) {
739  LOG_CS << "Upload aborted - the add-on pbl info contains invalid UTF-8 and cannot be "
740  << "checked against the blacklist\n";
741  send_error("Add-on rejected: The add-on publish information contains an invalid UTF-8 sequence.", req.sock);
742  return;
743  }
744 
745  const bool existing_upload = campaign != nullptr;
746 
747  std::string message = "Add-on accepted.";
748 
749  if(campaign == nullptr) {
750  campaign = &campaigns().add_child("campaign");
751  (*campaign)["original_timestamp"] = upload_ts;
752  }
753 
754  (*campaign)["title"] = upload["title"];
755  (*campaign)["name"] = upload["name"];
756  (*campaign)["filename"] = "data/" + upload["name"].str();
757  (*campaign)["author"] = upload["author"];
758  (*campaign)["description"] = upload["description"];
759  (*campaign)["version"] = upload["version"];
760  (*campaign)["icon"] = upload["icon"];
761  (*campaign)["translate"] = upload["translate"];
762  (*campaign)["dependencies"] = upload["dependencies"];
763  (*campaign)["upload_ip"] = req.addr;
764  (*campaign)["type"] = upload["type"];
765  (*campaign)["email"] = upload["email"];
766 
767  if(!existing_upload) {
768  set_passphrase(*campaign, upload["passphrase"]);
769  }
770 
771  if((*campaign)["downloads"].empty()) {
772  (*campaign)["downloads"] = 0;
773  }
774  (*campaign)["timestamp"] = upload_ts;
775 
776  int uploads = (*campaign)["uploads"].to_int() + 1;
777  (*campaign)["uploads"] = uploads;
778 
779  (*campaign).clear_children("feedback");
780  if(const config& url_params = upload.child("feedback")) {
781  (*campaign).add_child("feedback", url_params);
782  }
783 
784  const std::string& filename = (*campaign)["filename"].str();
785  data["title"] = (*campaign)["title"];
786  data["name"] = "";
787  data["campaign_name"] = (*campaign)["name"];
788  data["author"] = (*campaign)["author"];
789  data["description"] = (*campaign)["description"];
790  data["version"] = (*campaign)["version"];
791  data["timestamp"] = (*campaign)["timestamp"];
792  data["original_timestamp"] = (*campaign)["original_timestamp"];
793  data["icon"] = (*campaign)["icon"];
794  data["type"] = (*campaign)["type"];
795  (*campaign).clear_children("translation");
796  find_translations(data, *campaign);
797 
798  add_license(data);
799 
800  {
801  filesystem::atomic_commit campaign_file(filename);
802  config_writer writer(*campaign_file.ostream(), true, compress_level_);
803  writer.write(data);
804  campaign_file.commit();
805  }
806 
807  (*campaign)["size"] = filesystem::file_size(filename);
808 
809  write_config();
810 
811  send_message(message, req.sock);
812 
813  fire("hook_post_upload", upload["name"]);
814  }
815 }
816 
818 {
819  const config& erase = req.cfg;
820 
821  if(read_only_) {
822  LOG_CS << "in read-only mode, request to delete '" << erase["name"] << "' from " << req.addr << " denied\n";
823  send_error("Cannot delete add-on: The server is currently in read-only mode.", req.sock);
824  return;
825  }
826 
827  LOG_CS << "deleting campaign '" << erase["name"] << "' requested from " << req.addr << "\n";
828 
829  config& campaign = get_campaign(erase["name"]);
830 
831  if(!campaign) {
832  send_error("The add-on does not exist.", req.sock);
833  return;
834  }
835 
836  if(!authenticate(campaign, erase["passphrase"])
837  && (campaigns()["master_password"].empty()
838  || campaigns()["master_password"] != erase["passphrase"]))
839  {
840  send_error("The passphrase is incorrect.", req.sock);
841  return;
842  }
843 
844  // Erase the campaign.
845  filesystem::write_file(campaign["filename"], std::string());
846  if(remove(campaign["filename"].str().c_str()) != 0) {
847  ERR_CS << "failed to delete archive for campaign '" << erase["name"]
848  << "' (" << campaign["filename"] << "): " << strerror(errno)
849  << '\n';
850  }
851 
852  config::child_itors itors = campaigns().child_range("campaign");
853  for(size_t index = 0; !itors.empty(); ++index, itors.pop_front())
854  {
855  if(&campaign == &itors.front()) {
856  campaigns().remove_child("campaign", index);
857  break;
858  }
859  }
860 
861  write_config();
862 
863  send_message("Add-on deleted.", req.sock);
864 
865  fire("hook_post_erase", erase["name"]);
866 
867 }
868 
870 {
871  const config& cpass = req.cfg;
872 
873  if(read_only_) {
874  LOG_CS << "in read-only mode, request to change passphrase denied\n";
875  send_error("Cannot change passphrase: The server is currently in read-only mode.", req.sock);
876  return;
877  }
878 
879  config& campaign = get_campaign(cpass["name"]);
880 
881  if(!campaign) {
882  send_error("No add-on with that name exists.", req.sock);
883  } else if(!authenticate(campaign, cpass["passphrase"])) {
884  send_error("Your old passphrase was incorrect.", req.sock);
885  } else if(cpass["new_passphrase"].empty()) {
886  send_error("No new passphrase was supplied.", req.sock);
887  } else {
888  set_passphrase(campaign, cpass["new_passphrase"]);
889  write_config();
890  send_message("Passphrase changed.", req.sock);
891  }
892 }
893 
894 } // end namespace campaignd
895 
896 int main()
897 {
899 
900  lg::set_log_domain_severity("campaignd", lg::info());
902  lg::timestamps(true);
903 
904  try {
905  std::cerr << "Wesnoth campaignd v" << game_config::revision << " starting...\n";
906 
907  const std::string cfg_path = filesystem::normalize_path("server.cfg");
908 
909  campaignd::server(cfg_path).run();
910  } catch(config::error& /*e*/) {
911  std::cerr << "Could not parse config file\n";
912  return 1;
913  } catch(filesystem::io_exception& e) {
914  std::cerr << "File I/O error: " << e.what() << "\n";
915  return 2;
916  } catch(std::bad_function_call& /*e*/) {
917  std::cerr << "Bad request handler function call\n";
918  return 4;
919  }
920 
921  return 0;
922 }
static size_t document_size_limit
Definition: simple_wml.hpp:285
node & add_child(const char *name)
Definition: simple_wml.cpp:472
virtual std::string base64_digest() const override
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")
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:400
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
bool ignore_address_stats(const std::string &addr) const
Checks if the specified address should never bump download counts.
const char * what() const NOEXCEPT
Definition: exceptions.hpp:37
std::vector< char_t > string
size_t index(const utf8::string &str, const size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:71
Legacy add-ons server.
void handle_request_terms(const request &)
std::string join(const T &v, const std::string &s=",")
Generates a new string joining container items in a list.
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:70
static l_noret error(LoadState *S, const char *why)
Definition: lundump.cpp:39
New lexcical_cast header.
logger & info()
Definition: log.cpp:91
void handle_delete(const request &)
Client request information object.
#define LOG_CS
child_itors child_range(config_key_type key)
Definition: config.cpp:343
void load_config()
Reads the server configuration from WML.
void timestamps(bool t)
Definition: log.cpp:76
const config & server_info() const
Retrieves the contents of the [server_info] WML node.
bool wildcard_string_match(const std::string &str, const std::string &match)
Match using '*' as any number of characters (including none), and '?' as any one character.
utf8::string lowercase(const utf8::string &s)
Returns a lowercased version of the string.
Definition: unicode.cpp:51
static lg::log_domain log_config("config")
#define ERR_CS
void fire(const std::string &hook, const std::string &addon)
Fires a hook script.
size_t args_count() const
Returns the total number of arguments, not including the command itself.
Definition: control.hpp:78
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
Definition: debugger.cpp:110
config & child_or_add(config_key_type key)
Definition: config.cpp:446
request_handlers_table handlers_
static void null_handler(socket_ptr)
std::string normalize_path(const std::string &path, bool normalize_separators=false, bool resolve_dot_entries=false)
Returns the absolute path of a file.
bool empty() const
Definition: config.cpp:811
To lexical_cast(From value)
Lexical cast converts one type to another.
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::string full() const
Return the full command line string.
Definition: control.hpp:110
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 client_address(socket_ptr socket)
std::string get_cwd()
void start_server()
Definition: server_base.cpp:46
unsigned in
If equal to search_counter, the node is off the list.
void handle_sighup(const boost::system::error_code &error, int signal_number)
boost::asio::basic_waitable_timer< std::chrono::steady_clock > flush_timer_
const config & campaigns() const
Retrieves the contents of the [campaigns] WML node.
filesystem::scoped_istream istream_file(const std::string &fname, bool treat_failure_as_error=true)
boost::asio::streambuf admin_cmd_
Definition: server_base.hpp:68
std::string blacklist_file_
void clear_children(T...keys)
Definition: config.hpp:507
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.
void commit()
Commits the new file contents to disk atomically.
Definition: fs_commit.cpp:90
const child_map::key_type & key
Definition: config.hpp:549
void erase(const std::string &key)
Definition: general.cpp:222
std::unique_ptr< std::istream > scoped_istream
Definition: filesystem.hpp:37
void handle_read_from_fifo(const boost::system::error_code &error, std::size_t bytes_transferred)
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
std::string path
Definition: game_config.cpp:56
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 &)
#define DBG_CS
ADDON_TYPE get_addon_type(const std::string &str)
Definition: validation.cpp:235
size_t size(const utf8::string &str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:86
bool addon_name_legal(const std::string &name)
Checks whether an add-on id/name is legal or not.
Definition: validation.cpp:90
const_all_children_iterator ordered_end() const
Definition: config.cpp:838
void write_config()
Writes the server configuration WML back to disk.
void handle_new_client(socket_ptr socket)
Thrown by operations encountering invalid UTF-8 data.
const std::string revision
Definition: game_config.cpp:50
node & set_attr_dup(const char *key, const char *value)
Definition: simple_wml.cpp:435
Atomic filesystem commit functions.
Represents a server control line written to a communication socket.
Definition: control.hpp:32
void handle_request_campaign(const request &)
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:433
An exception object used when an IO error occurs.
Definition: filesystem.hpp:46
int main()
size_t i
Definition: function.cpp:933
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()
boost::asio::posix::stream_descriptor input_
Definition: server_base.hpp:64
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:456
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't exist.
config & cfg
Definition: config.hpp:550
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
void write(std::ostream &out, configr_of const &cfg, unsigned int level)
Definition: parser.cpp:749
std::string fifo_path_
Definition: server_base.hpp:65
boost::iterator_range< child_iterator > child_itors
Definition: config.hpp:235
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
static const size_t default_document_size_limit
Default upload size limit in bytes.
static const char * name(const std::vector< SDL_Joystick * > &joysticks, const size_t index)
Definition: joystick.cpp:48
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
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:93
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:624
void handle_upload(const request &)
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.
const_all_children_iterator ordered_begin() const
Definition: config.cpp:828