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  , flush_timer_(io_service_)
115 {
116 
117 #ifndef _WIN32
118  struct sigaction sa;
119  std::memset( &sa, 0, sizeof(sa) );
120  #pragma GCC diagnostic ignored "-Wold-style-cast"
121  sa.sa_handler = SIG_IGN;
122  int res = sigaction( SIGPIPE, &sa, nullptr);
123  assert( res == 0 );
124 #endif
125 
126  load_config();
127 
128  LOG_CS << "Port: " << port_ << "\n";
129 
130  // Ensure all campaigns to use secure hash passphrase storage
131  if(!read_only_) {
132  for(config& campaign : campaigns().child_range("campaign")) {
133  // Campaign already has a hashed password
134  if(campaign["passphrase"].empty()) {
135  continue;
136  }
137 
138  LOG_CS << "Campaign '" << campaign["title"] << "' uses unhashed passphrase. Fixing.\n";
139  set_passphrase(campaign, campaign["passphrase"]);
140  campaign["passphrase"] = "";
141  }
142  write_config();
143  }
144 
146 
147  start_server();
148  flush_cfg();
149 }
150 
152 {
153  write_config();
154 }
155 
157 {
158  LOG_CS << "Reading configuration from " << cfg_file_ << "...\n";
159 
161  read(cfg_, *in);
162 
163  read_only_ = cfg_["read_only"].to_bool(false);
164 
165  if(read_only_) {
166  LOG_CS << "READ-ONLY MODE ACTIVE\n";
167  }
168 
169  // Seems like compression level above 6 is a waste of CPU cycles.
170  compress_level_ = cfg_["compress_level"].to_int(6);
171 
172  const config& svinfo_cfg = server_info();
173  if(svinfo_cfg) {
174  feedback_url_format_ = svinfo_cfg["feedback_url_format"].str();
175  }
176 
177  blacklist_file_ = cfg_["blacklist_file"].str();
178  load_blacklist();
179 
180  // Load any configured hooks.
181  hooks_.emplace(std::string("hook_post_upload"), cfg_["hook_post_upload"]);
182  hooks_.emplace(std::string("hook_post_erase"), cfg_["hook_post_erase"]);
183 
184 #ifndef _WIN32
185  // Open the control socket if enabled.
186  if(!cfg_["control_socket"].empty()) {
187  const std::string& path = cfg_["control_socket"].str();
188 
189  if(path != fifo_path_) {
190  const int res = mkfifo(path.c_str(),0660);
191  if(res != 0 && errno != EEXIST) {
192  ERR_CS << "could not make fifo at '" << path << "' (" << strerror(errno) << ")\n";
193  } else {
194  input_.close();
195  int fifo = open(path.c_str(), O_RDWR|O_NONBLOCK);
196  input_.assign(fifo);
197  LOG_CS << "opened fifo at '" << path << "'. Server commands may be written to this file.\n";
198  read_from_fifo();
199  fifo_path_ = path;
200  }
201  }
202  }
203 #endif
204 
205  // Ensure the campaigns list WML exists even if empty, other functions
206  // depend on its existence.
207  cfg_.child_or_add("campaigns");
208 
209  // Certain config values are saved to WML again so that a given server
210  // instance's parameters remain constant even if the code defaults change
211  // at some later point.
212  cfg_["compress_level"] = compress_level_;
213 
214  // But not the listening port number.
215  port_ = cfg_["port"].to_int(default_campaignd_port);
216 
217  // Limit the max size of WML documents received from the net to prevent the
218  // possible excessive use of resources due to malformed packets received.
219  // Since an addon is sent in a single WML document this essentially limits
220  // the maximum size of an addon that can be uploaded.
222 }
223 
225 {
226  async_receive_doc(socket,
227  std::bind(&server::handle_request, this, _1, _2)
228  );
229 }
230 
231 void server::handle_request(socket_ptr socket, std::shared_ptr<simple_wml::document> doc)
232 {
233  config data;
234  read(data, doc->output());
235 
237 
238  if(i != data.ordered_end()) {
239  // We only handle the first child.
240  const config::any_child& c = *i;
241 
242  request_handlers_table::const_iterator j
243  = handlers_.find(c.key);
244 
245  if(j != handlers_.end()) {
246  // Call the handler.
247  j->second(this, request(c.key, c.cfg, socket));
248  } else {
249  send_error("Unrecognized [" + c.key + "] request.",socket);
250  }
251  }
252 }
253 
254 #ifndef _WIN32
255 
256 void server::handle_read_from_fifo(const boost::system::error_code& error, std::size_t)
257 {
258  if(error) {
259  if(error == boost::asio::error::operation_aborted)
260  // This means fifo was closed by load_config() to open another fifo
261  return;
262  ERR_CS << "Error reading from fifo: " << error.message() << '\n';
263  return;
264  }
265 
266  std::istream is(&admin_cmd_);
267  std::string cmd;
268  std::getline(is, cmd);
269 
270  const control_line ctl = cmd;
271 
272  if(ctl == "shut_down") {
273  LOG_CS << "Shut down requested by admin, shutting down...\n";
274  throw server_shutdown("Shut down via fifo command");
275  } else if(ctl == "readonly") {
276  if(ctl.args_count()) {
277  cfg_["read_only"] = read_only_ = utils::string_bool(ctl[1], true);
278  }
279 
280  LOG_CS << "Read only mode: " << (read_only_ ? "enabled" : "disabled") << '\n';
281  } else if(ctl == "flush") {
282  LOG_CS << "Flushing config to disk...\n";
283  write_config();
284  } else if(ctl == "reload") {
285  if(ctl.args_count()) {
286  if(ctl[1] == "blacklist") {
287  LOG_CS << "Reloading blacklist...\n";
288  load_blacklist();
289  } else {
290  ERR_CS << "Unrecognized admin reload argument: " << ctl[1] << '\n';
291  }
292  } else {
293  LOG_CS << "Reloading all configuration...\n";
294  load_config();
295  LOG_CS << "Reloaded configuration\n";
296  }
297  } else if(ctl == "setpass") {
298  if(ctl.args_count() != 2) {
299  ERR_CS << "Incorrect number of arguments for 'setpass'\n";
300  } else {
301  const std::string& addon_id = ctl[1];
302  const std::string& newpass = ctl[2];
303  config& campaign = get_campaign(addon_id);
304 
305  if(!campaign) {
306  ERR_CS << "Add-on '" << addon_id << "' not found, cannot set passphrase\n";
307  } else if(newpass.empty()) {
308  // Shouldn't happen!
309  ERR_CS << "Add-on passphrases may not be empty!\n";
310  } else {
311  set_passphrase(campaign, newpass);
312  write_config();
313  LOG_CS << "New passphrase set for '" << addon_id << "'\n";
314  }
315  }
316  } else {
317  ERR_CS << "Unrecognized admin command: " << ctl.full() << '\n';
318  }
319 
320  read_from_fifo();
321 }
322 
323 void server::handle_sighup(const boost::system::error_code&, int)
324 {
325  LOG_CS << "SIGHUP caught, reloading config.\n";
326 
327  load_config(); // TODO: handle port number config changes
328 
329  LOG_CS << "Reloaded configuration\n";
330 
331  sighup_.async_wait(std::bind(&server::handle_sighup, this, _1, _2));
332 }
333 
334 #endif
335 
337 {
338  flush_timer_.expires_from_now(std::chrono::minutes(10));
339  flush_timer_.async_wait(std::bind(&server::handle_flush, this, _1));
340 }
341 
342 void server::handle_flush(const boost::system::error_code& error)
343 {
344  if(error) {
345  ERR_CS << "Error from reload timer: " << error.message() << "\n";
346  throw boost::system::system_error(error);
347  }
348  write_config();
349  flush_cfg();
350 }
351 
353 {
354  // We *always* want to clear the blacklist first, especially if we are
355  // reloading the configuration and the blacklist is no longer enabled.
356  blacklist_.clear();
357 
358  if(blacklist_file_.empty()) {
359  return;
360  }
361 
362  try {
364  config blcfg;
365 
366  read(blcfg, *in);
367 
368  blacklist_.read(blcfg);
369  LOG_CS << "using blacklist from " << blacklist_file_ << '\n';
370  } catch(const config::error&) {
371  ERR_CS << "failed to read blacklist from " << blacklist_file_ << ", blacklist disabled\n";
372  }
373 }
374 
376 {
377  DBG_CS << "writing configuration and add-ons list to disk...\n";
379  write(*out.ostream(), cfg_);
380  out.commit();
381  DBG_CS << "... done\n";
382 }
383 
384 void server::fire(const std::string& hook, const std::string& addon)
385 {
386  const std::map<std::string, std::string>::const_iterator itor = hooks_.find(hook);
387  if(itor == hooks_.end()) {
388  return;
389  }
390 
391  const std::string& script = itor->second;
392  if(script.empty()) {
393  return;
394  }
395 
396 #if defined(_WIN32)
397  (void)addon;
398  ERR_CS << "Tried to execute a script on an unsupported platform\n";
399  return;
400 #else
401  pid_t childpid;
402 
403  if((childpid = fork()) == -1) {
404  ERR_CS << "fork failed while updating campaign " << addon << '\n';
405  return;
406  }
407 
408  if(childpid == 0) {
409  // We are the child process. Execute the script. We run as a
410  // separate thread sharing stdout/stderr, which will make the
411  // log look ugly.
412  execlp(script.c_str(), script.c_str(), addon.c_str(), static_cast<char *>(nullptr));
413 
414  // exec() and family never return; if they do, we have a problem
415  std::cerr << "ERROR: exec failed with errno " << errno << " for addon " << addon
416  << '\n';
417  exit(errno);
418 
419  } else {
420  return;
421  }
422 #endif
423 }
424 
426 {
428  doc.root().add_child("message").set_attr_dup("message", msg.c_str());
429  async_send_doc(sock, doc, std::bind(&server::handle_new_client, this, _1), null_handler);
430 }
431 
433 {
434  ERR_CS << "[" << client_address(sock) << "]: " << msg << '\n';
436  doc.root().add_child("error").set_attr_dup("message", msg.c_str());
437  async_send_doc(sock, doc, std::bind(&server::handle_new_client, this, _1), null_handler);
438 }
439 
440 void server::send_error(const std::string& msg, const std::string& extra_data, socket_ptr sock)
441 {
442  ERR_CS << "[" << client_address(sock) << "]: " << msg << '\n';
444  simple_wml::node& err_cfg = doc.root().add_child("error");
445  err_cfg.set_attr_dup("message", msg.c_str());
446  err_cfg.set_attr_dup("extra_data", extra_data.c_str());
447  async_send_doc(sock, doc, std::bind(&server::handle_new_client, this, _1), null_handler);
448 }
449 
450 #define REGISTER_CAMPAIGND_HANDLER(req_id) \
451  handlers_[#req_id] = std::bind(&server::handle_##req_id, \
452  std::placeholders::_1, std::placeholders::_2)
453 
455 {
456  REGISTER_CAMPAIGND_HANDLER(request_campaign_list);
457  REGISTER_CAMPAIGND_HANDLER(request_campaign);
458  REGISTER_CAMPAIGND_HANDLER(request_terms);
461  REGISTER_CAMPAIGND_HANDLER(change_passphrase);
462 }
463 
465 {
466  LOG_CS << "sending campaign list to " << req.addr << " using gzip\n";
467 
468  time_t epoch = time(nullptr);
469  config campaign_list;
470 
471  campaign_list["timestamp"] = epoch;
472  if(req.cfg["times_relative_to"] != "now") {
473  epoch = 0;
474  }
475 
476  bool before_flag = false;
477  time_t before = epoch;
478  try {
479  before = before + lexical_cast<time_t>(req.cfg["before"]);
480  before_flag = true;
481  } catch(bad_lexical_cast) {}
482 
483  bool after_flag = false;
484  time_t after = epoch;
485  try {
486  after = after + lexical_cast<time_t>(req.cfg["after"]);
487  after_flag = true;
488  } catch(bad_lexical_cast) {}
489 
490  const std::string& name = req.cfg["name"];
491  const std::string& lang = req.cfg["language"];
492 
493  for(const config& i : campaigns().child_range("campaign"))
494  {
495  if(!name.empty() && name != i["name"]) {
496  continue;
497  }
498 
499  const std::string& tm = i["timestamp"];
500 
501  if(before_flag && (tm.empty() || lexical_cast_default<time_t>(tm, 0) >= before)) {
502  continue;
503  }
504  if(after_flag && (tm.empty() || lexical_cast_default<time_t>(tm, 0) <= after)) {
505  continue;
506  }
507 
508  if(!lang.empty()) {
509  bool found = false;
510 
511  for(const config& j : i.child_range("translation"))
512  {
513  if(j["language"] == lang) {
514  found = true;
515  break;
516  }
517  }
518 
519  if(!found) {
520  continue;
521  }
522  }
523 
524  campaign_list.add_child("campaign", i);
525  }
526 
527  for(config& j : campaign_list.child_range("campaign"))
528  {
529  j["passphrase"] = "";
530  j["passhash"] = "";
531  j["passsalt"] = "";
532  j["upload_ip"] = "";
533  j["email"] = "";
534  j["feedback_url"] = "";
535 
536  // Build a feedback_url string attribute from the
537  // internal [feedback] data.
538  const config& url_params = j.child_or_empty("feedback");
539  if(!url_params.empty() && !feedback_url_format_.empty()) {
540  j["feedback_url"] = format_addon_feedback_url(feedback_url_format_, url_params);
541  }
542 
543  // Clients don't need to see the original data, so discard it.
544  j.clear_children("feedback");
545  }
546 
547  config response;
548  response.add_child("campaigns", campaign_list);
549 
550  std::ostringstream ostr;
551  write(ostr, response);
552  std::string wml = ostr.str();
554  doc.compress();
555 
556  async_send_doc(req.sock, doc, std::bind(&server::handle_new_client, this, _1));
557 }
558 
560 {
561  LOG_CS << "sending campaign '" << req.cfg["name"] << "' to " << req.addr << " using gzip\n";
562 
563  config& campaign = get_campaign(req.cfg["name"]);
564 
565  if(!campaign) {
566  send_error("Add-on '" + req.cfg["name"].str() + "' not found.", req.sock);
567  } else {
568  const int size = filesystem::file_size(campaign["filename"]);
569 
570  if(size < 0) {
571  std::cerr << " size: <unknown> KiB\n";
572  ERR_CS << "File size unknown, aborting send.\n";
573  send_error("Add-on '" + req.cfg["name"].str() + "' could not be read by the server.", req.sock);
574  return;
575  }
576 
577  std::cerr << " size: " << size/1024 << "KiB\n";
578  async_send_file(req.sock, campaign["filename"],
579  std::bind(&server::handle_new_client, this, _1), null_handler);
580  // Clients doing upgrades or some other specific thing shouldn't bump
581  // the downloads count. Default to true for compatibility with old
582  // clients that won't tell us what they are trying to do.
583  if(req.cfg["increase_downloads"].to_bool(true)) {
584  const int downloads = campaign["downloads"].to_int() + 1;
585  campaign["downloads"] = downloads;
586  }
587  }
588 }
589 
591 {
592  // This usually means the client wants to upload content, so tell it
593  // to give up when we're in read-only mode.
594  if(read_only_) {
595  LOG_CS << "in read-only mode, request for upload terms denied\n";
596  send_error("The server is currently in read-only mode, add-on uploads are disabled.", req.sock);
597  return;
598  }
599 
600  // TODO: possibly move to server.cfg
601  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:
602 
603  a) a combined toplevel file, e.g. “My_Addon/ART_LICENSE”; <b>or</b>
604  b) a file with the same path as the asset with “.license” appended, e.g. “My_Addon/images/units/axeman.png.license”.
605 
606 <b>By uploading content to this server, you certify that you have the right to:</b>
607 
608  a) release all included art and audio explicitly denoted with a Creative Commons license in the proscribed manner under that license; <b>and</b>
609  b) release all other included content under the terms of the GPL; and that you choose to do so.)""";
610 
611  LOG_CS << "sending terms " << req.addr << "\n";
612  send_message(terms, req.sock);
613  LOG_CS << " Done\n";
614 }
615 
617 {
618  const config& upload = req.cfg;
619 
620  LOG_CS << "uploading campaign '" << upload["name"] << "' from " << req.addr << ".\n";
621  config data = upload.child("data");
622 
623  const std::string& name = upload["name"];
624  config *campaign = nullptr;
625 
626  bool passed_name_utf8_check = false;
627 
628  try {
629  const std::string& lc_name = utf8::lowercase(name);
630  passed_name_utf8_check = true;
631 
632  for(config& c : campaigns().child_range("campaign"))
633  {
634  if(utf8::lowercase(c["name"]) == lc_name) {
635  campaign = &c;
636  break;
637  }
638  }
639  } catch(const utf8::invalid_utf8_exception&) {
640  if(!passed_name_utf8_check) {
641  LOG_CS << "Upload aborted - invalid_utf8_exception caught on handle_upload() check 1, "
642  << "the add-on pbl info contains invalid UTF-8\n";
643  send_error("Add-on rejected: The add-on name contains an invalid UTF-8 sequence.", req.sock);
644  } else {
645  LOG_CS << "Upload aborted - invalid_utf8_exception caught on handle_upload() check 2, "
646  << "the internal add-ons list contains invalid UTF-8\n";
647  send_error("Server error: The server add-ons list is damaged.", req.sock);
648  }
649 
650  return;
651  }
652 
653  std::vector<std::string> badnames;
654 
655  if(read_only_) {
656  LOG_CS << "Upload aborted - uploads not permitted in read-only mode.\n";
657  send_error("Add-on rejected: The server is currently in read-only mode.", req.sock);
658  } else if(!data) {
659  LOG_CS << "Upload aborted - no add-on data.\n";
660  send_error("Add-on rejected: No add-on data was supplied.", req.sock);
661  } else if(!addon_name_legal(upload["name"])) {
662  LOG_CS << "Upload aborted - invalid add-on name.\n";
663  send_error("Add-on rejected: The name of the add-on is invalid.", req.sock);
664  } else if(is_text_markup_char(upload["name"].str()[0])) {
665  LOG_CS << "Upload aborted - add-on name starts with an illegal formatting character.\n";
666  send_error("Add-on rejected: The name of the add-on starts with an illegal formatting character.", req.sock);
667  } else if(upload["title"].empty()) {
668  LOG_CS << "Upload aborted - no add-on title specified.\n";
669  send_error("Add-on rejected: You did not specify the title of the add-on in the pbl file!", req.sock);
670  } else if(is_text_markup_char(upload["title"].str()[0])) {
671  LOG_CS << "Upload aborted - add-on title starts with an illegal formatting character.\n";
672  send_error("Add-on rejected: The title of the add-on starts with an illegal formatting character.", req.sock);
673  } else if(get_addon_type(upload["type"]) == ADDON_UNKNOWN) {
674  LOG_CS << "Upload aborted - unknown add-on type specified.\n";
675  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);
676  } else if(upload["author"].empty()) {
677  LOG_CS << "Upload aborted - no add-on author specified.\n";
678  send_error("Add-on rejected: You did not specify the author(s) of the add-on in the pbl file!", req.sock);
679  } else if(upload["version"].empty()) {
680  LOG_CS << "Upload aborted - no add-on version specified.\n";
681  send_error("Add-on rejected: You did not specify the version of the add-on in the pbl file!", req.sock);
682  } else if(upload["description"].empty()) {
683  LOG_CS << "Upload aborted - no add-on description specified.\n";
684  send_error("Add-on rejected: You did not specify a description of the add-on in the pbl file!", req.sock);
685  } else if(upload["email"].empty()) {
686  LOG_CS << "Upload aborted - no add-on email specified.\n";
687  send_error("Add-on rejected: You did not specify your email address in the pbl file!", req.sock);
688  } else if(!check_names_legal(data, &badnames)) {
689  const std::string& filelist = utils::join(badnames, "\n");
690  LOG_CS << "Upload aborted - invalid file names in add-on data (" << badnames.size() << " entries).\n";
691  send_error(
692  "Add-on rejected: The add-on contains files or directories with illegal names. "
693  "File or directory names may not contain whitespace, control characters or any of the following characters: '\" * / : < > ? \\ | ~'. "
694  "It also may not contain '..' end with '.' or be longer than 255 characters.",
695  filelist, req.sock);
696  } else if(!check_case_insensitive_duplicates(data, &badnames)) {
697  const std::string& filelist = utils::join(badnames, "\n");
698  LOG_CS << "Upload aborted - case conflict in add-on data (" << badnames.size() << " entries).\n";
699  send_error(
700  "Add-on rejected: The add-on contains files or directories with case conflicts. "
701  "File or directory names may not be differently-cased versions of the same string.",
702  filelist, req.sock);
703  } else if(campaign && !authenticate(*campaign, upload["passphrase"])) {
704  LOG_CS << "Upload aborted - incorrect passphrase.\n";
705  send_error("Add-on rejected: The add-on already exists, and your passphrase was incorrect.", req.sock);
706  } else {
707  const time_t upload_ts = time(nullptr);
708 
709  LOG_CS << "Upload is owner upload.\n";
710 
711  try {
712  if(blacklist_.is_blacklisted(name,
713  upload["title"].str(),
714  upload["description"].str(),
715  upload["author"].str(),
716  req.addr,
717  upload["email"].str()))
718  {
719  LOG_CS << "Upload denied - blacklisted add-on information.\n";
720  send_error("Add-on upload denied. Please contact the server administration for assistance.", req.sock);
721  return;
722  }
723  } catch(const utf8::invalid_utf8_exception&) {
724  LOG_CS << "Upload aborted - the add-on pbl info contains invalid UTF-8 and cannot be "
725  << "checked against the blacklist\n";
726  send_error("Add-on rejected: The add-on publish information contains an invalid UTF-8 sequence.", req.sock);
727  return;
728  }
729 
730  const bool existing_upload = campaign != nullptr;
731 
732  std::string message = "Add-on accepted.";
733 
734  if(campaign == nullptr) {
735  campaign = &campaigns().add_child("campaign");
736  (*campaign)["original_timestamp"] = upload_ts;
737  }
738 
739  (*campaign)["title"] = upload["title"];
740  (*campaign)["name"] = upload["name"];
741  (*campaign)["filename"] = "data/" + upload["name"].str();
742  (*campaign)["author"] = upload["author"];
743  (*campaign)["description"] = upload["description"];
744  (*campaign)["version"] = upload["version"];
745  (*campaign)["icon"] = upload["icon"];
746  (*campaign)["translate"] = upload["translate"];
747  (*campaign)["dependencies"] = upload["dependencies"];
748  (*campaign)["upload_ip"] = req.addr;
749  (*campaign)["type"] = upload["type"];
750  (*campaign)["email"] = upload["email"];
751 
752  if(!existing_upload) {
753  set_passphrase(*campaign, upload["passphrase"]);
754  }
755 
756  if((*campaign)["downloads"].empty()) {
757  (*campaign)["downloads"] = 0;
758  }
759  (*campaign)["timestamp"] = upload_ts;
760 
761  int uploads = (*campaign)["uploads"].to_int() + 1;
762  (*campaign)["uploads"] = uploads;
763 
764  (*campaign).clear_children("feedback");
765  if(const config& url_params = upload.child("feedback")) {
766  (*campaign).add_child("feedback", url_params);
767  }
768 
769  const std::string& filename = (*campaign)["filename"].str();
770  data["title"] = (*campaign)["title"];
771  data["name"] = "";
772  data["campaign_name"] = (*campaign)["name"];
773  data["author"] = (*campaign)["author"];
774  data["description"] = (*campaign)["description"];
775  data["version"] = (*campaign)["version"];
776  data["timestamp"] = (*campaign)["timestamp"];
777  data["original_timestamp"] = (*campaign)["original_timestamp"];
778  data["icon"] = (*campaign)["icon"];
779  data["type"] = (*campaign)["type"];
780  (*campaign).clear_children("translation");
781  find_translations(data, *campaign);
782 
783  add_license(data);
784 
785  {
786  filesystem::atomic_commit campaign_file(filename);
787  config_writer writer(*campaign_file.ostream(), true, compress_level_);
788  writer.write(data);
789  campaign_file.commit();
790  }
791 
792  (*campaign)["size"] = filesystem::file_size(filename);
793 
794  write_config();
795 
796  send_message(message, req.sock);
797 
798  fire("hook_post_upload", upload["name"]);
799  }
800 }
801 
803 {
804  const config& erase = req.cfg;
805 
806  if(read_only_) {
807  LOG_CS << "in read-only mode, request to delete '" << erase["name"] << "' from " << req.addr << " denied\n";
808  send_error("Cannot delete add-on: The server is currently in read-only mode.", req.sock);
809  return;
810  }
811 
812  LOG_CS << "deleting campaign '" << erase["name"] << "' requested from " << req.addr << "\n";
813 
814  config& campaign = get_campaign(erase["name"]);
815 
816  if(!campaign) {
817  send_error("The add-on does not exist.", req.sock);
818  return;
819  }
820 
821  if(!authenticate(campaign, erase["passphrase"])
822  && (campaigns()["master_password"].empty()
823  || campaigns()["master_password"] != erase["passphrase"]))
824  {
825  send_error("The passphrase is incorrect.", req.sock);
826  return;
827  }
828 
829  // Erase the campaign.
830  filesystem::write_file(campaign["filename"], std::string());
831  if(remove(campaign["filename"].str().c_str()) != 0) {
832  ERR_CS << "failed to delete archive for campaign '" << erase["name"]
833  << "' (" << campaign["filename"] << "): " << strerror(errno)
834  << '\n';
835  }
836 
837  config::child_itors itors = campaigns().child_range("campaign");
838  for(size_t index = 0; !itors.empty(); ++index, itors.pop_front())
839  {
840  if(&campaign == &itors.front()) {
841  campaigns().remove_child("campaign", index);
842  break;
843  }
844  }
845 
846  write_config();
847 
848  send_message("Add-on deleted.", req.sock);
849 
850  fire("hook_post_erase", erase["name"]);
851 
852 }
853 
855 {
856  const config& cpass = req.cfg;
857 
858  if(read_only_) {
859  LOG_CS << "in read-only mode, request to change passphrase denied\n";
860  send_error("Cannot change passphrase: The server is currently in read-only mode.", req.sock);
861  return;
862  }
863 
864  config& campaign = get_campaign(cpass["name"]);
865 
866  if(!campaign) {
867  send_error("No add-on with that name exists.", req.sock);
868  } else if(!authenticate(campaign, cpass["passphrase"])) {
869  send_error("Your old passphrase was incorrect.", req.sock);
870  } else if(cpass["new_passphrase"].empty()) {
871  send_error("No new passphrase was supplied.", req.sock);
872  } else {
873  set_passphrase(campaign, cpass["new_passphrase"]);
874  write_config();
875  send_message("Passphrase changed.", req.sock);
876  }
877 }
878 
879 } // end namespace campaignd
880 
881 int main()
882 {
884 
885  lg::set_log_domain_severity("campaignd", lg::info());
887  lg::timestamps(true);
888 
889  try {
890  std::cerr << "Wesnoth campaignd v" << game_config::revision << " starting...\n";
891 
892  const std::string cfg_path = filesystem::normalize_path("server.cfg");
893 
894  campaignd::server(cfg_path).run();
895  } catch(config::error& /*e*/) {
896  std::cerr << "Could not parse config file\n";
897  return 1;
898  } catch(filesystem::io_exception& e) {
899  std::cerr << "File I/O error: " << e.what() << "\n";
900  return 2;
901  } catch(std::bad_function_call& /*e*/) {
902  std::cerr << "Bad request handler function call\n";
903  return 4;
904  }
905 
906  return 0;
907 }
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:198
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:352
bool check_case_insensitive_duplicates(const config &dir, std::vector< std::string > *badlist)
Scans an add-on archive for case-conflicts.
Definition: validation.cpp:207
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:75
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:295
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.
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:398
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:750
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
void register_handlers()
Registers client request handlers.
unsigned short port_
Definition: server_base.hpp:48
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:73
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:547
void erase(const std::string &key)
Definition: general.cpp:220
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:211
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:80
const_all_children_iterator ordered_end() const
Definition: config.cpp:777
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:385
An exception object used when an IO error occurs.
Definition: filesystem.hpp:46
int main()
#define i
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: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:408
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:548
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:70
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:33
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:576
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:767