The Battle for Wesnoth  1.13.4+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 - 2016 by David White <[email protected]>
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 "log.hpp"
26 #include "network_worker.hpp"
28 #include "serialization/parser.hpp"
31 #include "game_config.hpp"
32 #include "addon/validation.hpp"
36 #include "version.hpp"
37 #include "util.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 //compatibility code for MS compilers
61 #ifndef SIGHUP
62 #define SIGHUP 20
63 #endif
64 /** @todo FIXME: should define SIGINT here too, but to what? */
65 
66 namespace {
67 
68 /**
69  * Whether to reload the server configuration as soon as possible
70  * (e.g. after SIGHUP).
71  */
72 sig_atomic_t need_reload = 0;
73 
74 void flag_sighup(int signal)
75 {
76  assert(signal == SIGHUP);
77  LOG_CS << "SIGHUP caught, scheduling config reload.\n";
78  need_reload = 1;
79 }
80 
81 void exit_sigint(int signal)
82 {
83  assert(signal == SIGINT);
84  LOG_CS << "SIGINT caught, exiting without cleanup immediately.\n";
85  exit(0);
86 }
87 
88 void exit_sigterm(int signal)
89 {
90  assert(signal == SIGTERM);
91  LOG_CS << "SIGTERM caught, exiting without cleanup immediately.\n";
92  exit(128 + SIGTERM);
93 }
94 
95 time_t monotonic_clock()
96 {
97 #if defined(_POSIX_MONOTONIC_CLOCK) && !defined(_WIN32)
98  timespec ts;
99  clock_gettime(CLOCK_MONOTONIC, &ts);
100  return ts.tv_sec;
101 #else
102  #warning monotonic_clock() is not truly monotonic!
103  return time(nullptr);
104 #endif
105 }
106 
107 /* Secure password storage functions */
108 bool authenticate(config& campaign, const config::attribute_value& passphrase)
109 {
110  return util::create_hash(passphrase, campaign["passsalt"]) == campaign["passhash"];
111 }
112 
113 std::string generate_salt(size_t len)
114 {
115  static const std::string itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
116  boost::mt19937 mt(time(0));
117  std::string salt = std::string(len, '0');
118  boost::uniform_int<> from_str(0, itoa64.length() - 1);
119  boost::variate_generator< boost::mt19937, boost::uniform_int<> > get_char(mt, from_str);
120 
121  for(size_t i = 0; i < len; i++) {
122  salt[i] = itoa64[get_char()];
123  }
124 
125  return salt;
126 }
127 
128 void set_passphrase(config& campaign, std::string passphrase)
129 {
130  std::string salt = generate_salt(16);
131  campaign["passsalt"] = salt;
132  campaign["passhash"] = util::create_hash(passphrase, salt);
133 }
134 
135 } // end anonymous namespace
136 
137 namespace campaignd {
138 
139 server::server(const std::string& cfg_file, size_t min_threads, size_t max_threads)
140  : cfg_()
141  , cfg_file_(cfg_file)
142  , read_only_(false)
143  , compress_level_(0)
144  , input_()
145  , hooks_()
146  , handlers_()
147  , feedback_url_format_()
148  , blacklist_()
149  , blacklist_file_()
150  , port_(load_config())
151  , net_manager_(min_threads, max_threads)
152  , server_manager_(port_)
153 {
154 #ifndef _MSC_VER
155  signal(SIGHUP, flag_sighup);
156 #endif
157  signal(SIGINT, exit_sigint);
158  signal(SIGTERM, exit_sigterm);
159 
160  LOG_CS << "Port: " << port_ << " Worker threads min/max: " << min_threads
161  << '/' << max_threads << '\n';
162 
163  // Ensure all campaigns to use secure hash passphrase storage
164  if(!read_only_) {
165  for(config& campaign : campaigns().child_range("campaign")) {
166  // Campaign already has a hashed password
167  if (campaign["passphrase"].empty()) {
168  continue;
169  }
170 
171  LOG_CS << "Campaign '" << campaign["title"] << "' uses unhashed passphrase. Fixing.\n";
172  set_passphrase(campaign, campaign["passphrase"]);
173  campaign["passphrase"] = "";
174  }
175  write_config();
176  }
177 
179 }
180 
182 {
183  write_config();
184 }
185 
187 {
188  LOG_CS << "Reading configuration from " << cfg_file_ << "...\n";
189 
191  read(cfg_, *in);
192 
193  read_only_ = cfg_["read_only"].to_bool(false);
194 
195  if(read_only_) {
196  LOG_CS << "READ-ONLY MODE ACTIVE\n";
197  }
198 
199  const bool use_system_sendfile = cfg_["network_use_system_sendfile"].to_bool();
201 
202  // Seems like compression level above 6 is a waste of CPU cycles.
203  compress_level_ = cfg_["compress_level"].to_int(6);
204 
205  const config& svinfo_cfg = server_info();
206  if(svinfo_cfg) {
207  feedback_url_format_ = svinfo_cfg["feedback_url_format"].str();
208  }
209 
210  blacklist_file_ = cfg_["blacklist_file"].str();
211  load_blacklist();
212 
213  // Load any configured hooks.
214  hooks_.insert(std::make_pair(std::string("hook_post_upload"), cfg_["hook_post_upload"]));
215  hooks_.insert(std::make_pair(std::string("hook_post_erase"), cfg_["hook_post_erase"]));
216 
217  // Open the control socket if enabled.
218  if(!cfg_["control_socket"].empty()) {
219  const std::string& path = cfg_["control_socket"].str();
220 
221  if(!input_.get() || input_->path() != path) {
222  input_.reset(new input_stream(cfg_["control_socket"]));
223  }
224  }
225 
226  // Ensure the campaigns list WML exists even if empty, other functions
227  // depend on its existence.
228  cfg_.child_or_add("campaigns");
229 
230  // Certain config values are saved to WML again so that a given server
231  // instance's parameters remain constant even if the code defaults change
232  // at some later point.
233  cfg_["network_use_system_sendfile"] = use_system_sendfile;
234  cfg_["compress_level"] = compress_level_;
235 
236  // But not the listening port number.
237  return cfg_["port"].to_int(default_campaignd_port);
238 }
239 
241 {
242  // We *always* want to clear the blacklist first, especially if we are
243  // reloading the configuration and the blacklist is no longer enabled.
244  blacklist_.clear();
245 
246  if(blacklist_file_.empty()) {
247  return;
248  }
249 
250  try {
252  config blcfg;
253 
254  read(blcfg, *in);
255 
256  blacklist_.read(blcfg);
257  LOG_CS << "using blacklist from " << blacklist_file_ << '\n';
258  } catch(const config::error&) {
259  ERR_CS << "failed to read blacklist from " << blacklist_file_ << ", blacklist disabled\n";
260  }
261 }
262 
264 {
265  DBG_CS << "writing configuration and add-ons list to disk...\n";
267  write(*out, cfg_);
268  DBG_CS << "... done\n";
269 }
270 
271 void server::fire(const std::string& hook, const std::string& addon)
272 {
273  const std::map<std::string, std::string>::const_iterator itor = hooks_.find(hook);
274  if(itor == hooks_.end()) {
275  return;
276  }
277 
278  const std::string& script = itor->second;
279  if(script.empty()) {
280  return;
281  }
282 
283 #if defined(_WIN32)
284  (void)addon;
285  ERR_CS << "Tried to execute a script on an unsupported platform\n";
286  return;
287 #else
288  pid_t childpid;
289 
290  if((childpid = fork()) == -1) {
291  ERR_CS << "fork failed while updating campaign " << addon << '\n';
292  return;
293  }
294 
295  if(childpid == 0) {
296  // We are the child process. Execute the script. We run as a
297  // separate thread sharing stdout/stderr, which will make the
298  // log look ugly.
299  execlp(script.c_str(), script.c_str(), addon.c_str(), static_cast<char *>(nullptr));
300 
301  // exec() and family never return; if they do, we have a problem
302  std::cerr << "ERROR: exec failed with errno " << errno << " for addon " << addon
303  << '\n';
304  exit(errno);
305 
306  } else {
307  return;
308  }
309 #endif
310 }
311 
313 {
314  config cfg;
315  cfg.add_child("message")["message"] = msg;
316  network::send_data(cfg, sock);
317 }
318 
320 {
321  config cfg;
322  cfg.add_child("error")["message"] = msg;
323  ERR_CS << "[" << network::ip_address(sock) << "]: " << msg << '\n';
324  network::send_data(cfg, sock);
325 }
326 
328 {
329  network::connection sock = 0;
330 
331  time_t last_ts = monotonic_clock();
332 
333  for(;;)
334  {
335  if(need_reload) {
336  load_config(); // TODO: handle port number config changes
337 
338  need_reload = 0;
339  last_ts = 0;
340 
341  LOG_CS << "Reloaded configuration\n";
342  }
343 
344  try {
345  bool force_flush = false;
346  std::string admin_cmd;
347 
348  if(input_ && input_->read_line(admin_cmd)) {
349  control_line ctl = admin_cmd;
350 
351  if(ctl == "shut_down") {
352  LOG_CS << "Shut down requested by admin, shutting down...\n";
353  break;
354  } else if(ctl == "readonly") {
355  if(ctl.args_count()) {
356  cfg_["read_only"] = read_only_ = utils::string_bool(ctl[1], true);
357  }
358 
359  LOG_CS << "Read only mode: " << (read_only_ ? "enabled" : "disabled") << '\n';
360  } else if(ctl == "flush") {
361  force_flush = true;
362  LOG_CS << "Flushing config to disk...\n";
363  } else if(ctl == "reload") {
364  if(ctl.args_count()) {
365  if(ctl[1] == "blacklist") {
366  LOG_CS << "Reloading blacklist...\n";
367  load_blacklist();
368  } else {
369  ERR_CS << "Unrecognized admin reload argument: " << ctl[1] << '\n';
370  }
371  } else {
372  LOG_CS << "Reloading all configuration...\n";
373  need_reload = 1;
374  // Avoid flush timer ellapsing
375  continue;
376  }
377  } else if(ctl == "setpass") {
378  if(ctl.args_count() != 2) {
379  ERR_CS << "Incorrect number of arguments for 'setpass'\n";
380  } else {
381  const std::string& addon_id = ctl[1];
382  const std::string& newpass = ctl[2];
383  config& campaign = get_campaign(addon_id);
384 
385  if(!campaign) {
386  ERR_CS << "Add-on '" << addon_id << "' not found, cannot set passphrase\n";
387  } else if(newpass.empty()) {
388  // Shouldn't happen!
389  ERR_CS << "Add-on passphrases may not be empty!\n";
390  } else {
391  set_passphrase(campaign, newpass);
392  write_config();
393  LOG_CS << "New passphrase set for '" << addon_id << "'\n";
394  }
395  }
396  } else {
397  ERR_CS << "Unrecognized admin command: " << ctl.full() << '\n';
398  }
399  }
400 
401  const time_t cur_ts = monotonic_clock();
402  // Write config to disk every ten minutes.
403  if(force_flush || labs(cur_ts - last_ts) >= 10*60) {
404  write_config();
405  last_ts = cur_ts;
406  }
407 
409 
411  if(sock) {
412  LOG_CS << "received connection from " << network::ip_address(sock) << "\n";
413  }
414 
415  config data;
416 
417  while((sock = network::receive_data(data, 0)) != network::null_connection)
418  {
420 
421  if(i != data.ordered_end()) {
422  // We only handle the first child.
423  const config::any_child& c = *i;
424 
425  request_handlers_table::const_iterator j
426  = handlers_.find(c.key);
427 
428  if(j != handlers_.end()) {
429  // Call the handler.
430  j->second(this, request(c.key, c.cfg, sock));
431  } else {
432  send_error("Unrecognized [" + c.key + "] request.",
433  sock);
434  }
435  }
436  }
437  } catch(network::error& e) {
438  if(!e.socket) {
439  ERR_CS << "fatal network error: " << e.message << "\n";
440  throw;
441  } else {
442  LOG_CS << "client disconnect: " << e.message << " " << network::ip_address(e.socket) << "\n";
443  e.disconnect();
444  }
445  } catch(const config::error& e) {
446  network::connection err_sock = 0;
447  network::connection const * err_connection = boost::get_error_info<network::connection_info>(e);
448 
449  if(err_connection != nullptr) {
450  err_sock = *err_connection;
451  }
452 
453  if(err_sock == 0 && sock > 0) {
454  err_sock = sock;
455  }
456 
457  if(err_sock) {
458  ERR_CS << "client disconnect due to exception: " << e.what() << " " << network::ip_address(err_sock) << "\n";
459  network::disconnect(err_sock);
460  } else {
461  throw;
462  }
463  }
464 
465  SDL_Delay(20);
466  }
467 }
468 
470 {
471  handlers_[cmd] = func;
472 }
473 
474 #define REGISTER_CAMPAIGND_HANDLER(req_id) \
475  register_handler(#req_id, &server::handle_##req_id)
476 
478 {
479  REGISTER_CAMPAIGND_HANDLER(request_campaign_list);
480  REGISTER_CAMPAIGND_HANDLER(request_campaign);
481  REGISTER_CAMPAIGND_HANDLER(request_terms);
484  REGISTER_CAMPAIGND_HANDLER(change_passphrase);
485 }
486 
488 {
489  LOG_CS << "sending campaign list to " << req.addr << " using gzip";
490 
491  time_t epoch = time(nullptr);
492  config campaign_list;
493 
494  campaign_list["timestamp"] = epoch;
495  if(req.cfg["times_relative_to"] != "now") {
496  epoch = 0;
497  }
498 
499  bool before_flag = false;
500  time_t before = epoch;
501  try {
502  before = before + lexical_cast<time_t>(req.cfg["before"]);
503  before_flag = true;
504  } catch(bad_lexical_cast) {}
505 
506  bool after_flag = false;
507  time_t after = epoch;
508  try {
509  after = after + lexical_cast<time_t>(req.cfg["after"]);
510  after_flag = true;
511  } catch(bad_lexical_cast) {}
512 
513  const std::string& name = req.cfg["name"];
514  const std::string& lang = req.cfg["language"];
515 
516  for(const config& i : campaigns().child_range("campaign"))
517  {
518  if(!name.empty() && name != i["name"]) {
519  continue;
520  }
521 
522  const std::string& tm = i["timestamp"];
523 
524  if(before_flag && (tm.empty() || lexical_cast_default<time_t>(tm, 0) >= before)) {
525  continue;
526  }
527  if(after_flag && (tm.empty() || lexical_cast_default<time_t>(tm, 0) <= after)) {
528  continue;
529  }
530 
531  if(!lang.empty()) {
532  bool found = false;
533 
534  for(const config& j : i.child_range("translation"))
535  {
536  if(j["language"] == lang) {
537  found = true;
538  break;
539  }
540  }
541 
542  if(!found) {
543  continue;
544  }
545  }
546 
547  campaign_list.add_child("campaign", i);
548  }
549 
550  for(config& j : campaign_list.child_range("campaign"))
551  {
552  j["passphrase"] = "";
553  j["passhash"] = "";
554  j["passsalt"] = "";
555  j["upload_ip"] = "";
556  j["email"] = "";
557  j["feedback_url"] = "";
558 
559  // Build a feedback_url string attribute from the
560  // internal [feedback] data.
561  const config& url_params = j.child_or_empty("feedback");
562  if(!url_params.empty() && !feedback_url_format_.empty()) {
563  j["feedback_url"] = format_addon_feedback_url(feedback_url_format_, url_params);
564  }
565 
566  // Clients don't need to see the original data, so discard it.
567  j.clear_children("feedback");
568  }
569 
570  config response;
571  response.add_child("campaigns", campaign_list);
572 
573  std::cerr << " size: " << (network::send_data(response, req.sock)/1024) << "KiB\n";
574 }
575 
577 {
578  LOG_CS << "sending campaign '" << req.cfg["name"] << "' to " << req.addr << " using gzip";
579 
580  config& campaign = get_campaign(req.cfg["name"]);
581 
582  if(!campaign) {
583  send_error("Add-on '" + req.cfg["name"].str() + "' not found.", req.sock);
584  } else {
585  const int size = filesystem::file_size(campaign["filename"]);
586 
587  if(size < 0) {
588  std::cerr << " size: <unknown> KiB\n";
589  ERR_CS << "File size unknown, aborting send.\n";
590  send_error("Add-on '" + req.cfg["name"].str() + "' could not be read by the server.", req.sock);
591  return;
592  }
593 
594  std::cerr << " size: " << size/1024 << "KiB\n";
595  network::send_file(campaign["filename"], req.sock);
596  // Clients doing upgrades or some other specific thing shouldn't bump
597  // the downloads count. Default to true for compatibility with old
598  // clients that won't tell us what they are trying to do.
599  if(req.cfg["increase_downloads"].to_bool(true)) {
600  const int downloads = campaign["downloads"].to_int() + 1;
601  campaign["downloads"] = downloads;
602  }
603  }
604 }
605 
607 {
608  // This usually means the client wants to upload content, so tell it
609  // to give up when we're in read-only mode.
610  if(read_only_) {
611  LOG_CS << "in read-only mode, request for upload terms denied\n";
612  send_error("The server is currently in read-only mode, add-on uploads are disabled.", req.sock);
613  return;
614  }
615 
616  LOG_CS << "sending terms " << req.addr << "\n";
617  send_message("All add-ons uploaded to this server must be licensed under the terms of the GNU General Public License (GPL). By uploading content to this server, you certify that you have the right to place the content under the conditions of the GPL, and choose to do so.", req.sock);
618  LOG_CS << " Done\n";
619 }
620 
622 {
623  const config& upload = req.cfg;
624 
625  LOG_CS << "uploading campaign '" << upload["name"] << "' from " << req.addr << ".\n";
626  config data = upload.child("data");
627 
628  const std::string& name = upload["name"];
629  config *campaign = nullptr;
630 
631  bool passed_name_utf8_check = false;
632 
633  try {
634  const std::string& lc_name = utf8::lowercase(name);
635  passed_name_utf8_check = true;
636 
637  for(config& c : campaigns().child_range("campaign"))
638  {
639  if(utf8::lowercase(c["name"]) == lc_name) {
640  campaign = &c;
641  break;
642  }
643  }
644  } catch(const utf8::invalid_utf8_exception&) {
645  if(!passed_name_utf8_check) {
646  LOG_CS << "Upload aborted - invalid_utf8_exception caught on handle_upload() check 1, "
647  << "the add-on pbl info contains invalid UTF-8\n";
648  send_error("Add-on rejected: The add-on name contains an invalid UTF-8 sequence.", req.sock);
649  } else {
650  LOG_CS << "Upload aborted - invalid_utf8_exception caught on handle_upload() check 2, "
651  << "the internal add-ons list contains invalid UTF-8\n";
652  send_error("Server error: The server add-ons list is damaged.", req.sock);
653  }
654 
655  return;
656  }
657 
658  if(read_only_) {
659  LOG_CS << "Upload aborted - uploads not permitted in read-only mode.\n";
660  send_error("Add-on rejected: The server is currently in read-only mode.", req.sock);
661  } else if(!data) {
662  LOG_CS << "Upload aborted - no add-on data.\n";
663  send_error("Add-on rejected: No add-on data was supplied.", req.sock);
664  } else if(!addon_name_legal(upload["name"])) {
665  LOG_CS << "Upload aborted - invalid add-on name.\n";
666  send_error("Add-on rejected: The name of the add-on is invalid.", req.sock);
667  } else if(is_text_markup_char(upload["name"].str()[0])) {
668  LOG_CS << "Upload aborted - add-on name starts with an illegal formatting character.\n";
669  send_error("Add-on rejected: The name of the add-on starts with an illegal formatting character.", req.sock);
670  } else if(upload["title"].empty()) {
671  LOG_CS << "Upload aborted - no add-on title specified.\n";
672  send_error("Add-on rejected: You did not specify the title of the add-on in the pbl file!", req.sock);
673  } else if(is_text_markup_char(upload["title"].str()[0])) {
674  LOG_CS << "Upload aborted - add-on title starts with an illegal formatting character.\n";
675  send_error("Add-on rejected: The title of the add-on starts with an illegal formatting character.", req.sock);
676  } else if(get_addon_type(upload["type"]) == ADDON_UNKNOWN) {
677  LOG_CS << "Upload aborted - unknown add-on type specified.\n";
678  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);
679  } else if(upload["author"].empty()) {
680  LOG_CS << "Upload aborted - no add-on author specified.\n";
681  send_error("Add-on rejected: You did not specify the author(s) of the add-on in the pbl file!", req.sock);
682  } else if(upload["version"].empty()) {
683  LOG_CS << "Upload aborted - no add-on version specified.\n";
684  send_error("Add-on rejected: You did not specify the version of the add-on in the pbl file!", req.sock);
685  } else if(upload["description"].empty()) {
686  LOG_CS << "Upload aborted - no add-on description specified.\n";
687  send_error("Add-on rejected: You did not specify a description of the add-on in the pbl file!", req.sock);
688  } else if(upload["email"].empty()) {
689  LOG_CS << "Upload aborted - no add-on email specified.\n";
690  send_error("Add-on rejected: You did not specify your email address in the pbl file!", req.sock);
691  } else if(!check_names_legal(data)) {
692  LOG_CS << "Upload aborted - invalid file names in add-on data.\n";
693  send_error("Add-on rejected: The add-on contains an illegal file or directory name."
694  " File or directory names may not contain whitespace or any of the following characters: '/ \\ : ~'",
695  req.sock);
696  } else if(campaign && !authenticate(*campaign, upload["passphrase"])) {
697  LOG_CS << "Upload aborted - incorrect passphrase.\n";
698  send_error("Add-on rejected: The add-on already exists, and your passphrase was incorrect.", req.sock);
699  } else {
700  const time_t upload_ts = time(nullptr);
701 
702  LOG_CS << "Upload is owner upload.\n";
703 
704  try {
705  if(blacklist_.is_blacklisted(name,
706  upload["title"].str(),
707  upload["description"].str(),
708  upload["author"].str(),
709  req.addr,
710  upload["email"].str()))
711  {
712  LOG_CS << "Upload denied - blacklisted add-on information.\n";
713  send_error("Add-on upload denied. Please contact the server administration for assistance.", req.sock);
714  return;
715  }
716  } catch(const utf8::invalid_utf8_exception&) {
717  LOG_CS << "Upload aborted - the add-on pbl info contains invalid UTF-8 and cannot be "
718  << "checked against the blacklist\n";
719  send_error("Add-on rejected: The add-on publish information contains an invalid UTF-8 sequence.", req.sock);
720  return;
721  }
722 
723  const bool existing_upload = campaign != nullptr;
724 
725  std::string message = "Add-on accepted.";
726 
727  if(campaign == nullptr) {
728  campaign = &campaigns().add_child("campaign");
729  (*campaign)["original_timestamp"] = upload_ts;
730  }
731 
732  (*campaign)["title"] = upload["title"];
733  (*campaign)["name"] = upload["name"];
734  (*campaign)["filename"] = "data/" + upload["name"].str();
735  (*campaign)["author"] = upload["author"];
736  (*campaign)["description"] = upload["description"];
737  (*campaign)["version"] = upload["version"];
738  (*campaign)["icon"] = upload["icon"];
739  (*campaign)["translate"] = upload["translate"];
740  (*campaign)["dependencies"] = upload["dependencies"];
741  (*campaign)["upload_ip"] = req.addr;
742  (*campaign)["type"] = upload["type"];
743  (*campaign)["email"] = upload["email"];
744 
745  if(!existing_upload) {
746  set_passphrase(*campaign, upload["passphrase"]);
747  }
748 
749  if((*campaign)["downloads"].empty()) {
750  (*campaign)["downloads"] = 0;
751  }
752  (*campaign)["timestamp"] = upload_ts;
753 
754  int uploads = (*campaign)["uploads"].to_int() + 1;
755  (*campaign)["uploads"] = uploads;
756 
757  (*campaign).clear_children("feedback");
758  if(const config& url_params = upload.child("feedback")) {
759  (*campaign).add_child("feedback", url_params);
760  }
761 
762  const std::string& filename = (*campaign)["filename"].str();
763  data["title"] = (*campaign)["title"];
764  data["name"] = "";
765  data["campaign_name"] = (*campaign)["name"];
766  data["author"] = (*campaign)["author"];
767  data["description"] = (*campaign)["description"];
768  data["version"] = (*campaign)["version"];
769  data["timestamp"] = (*campaign)["timestamp"];
770  data["original_timestamp"] = (*campaign)["original_timestamp"];
771  data["icon"] = (*campaign)["icon"];
772  data["type"] = (*campaign)["type"];
773  (*campaign).clear_children("translation");
774  find_translations(data, *campaign);
775 
776  add_license(data);
777 
778  {
779  filesystem::scoped_ostream campaign_file = filesystem::ostream_file(filename);
780  config_writer writer(*campaign_file, true, compress_level_);
781  writer.write(data);
782  }
783 
784  (*campaign)["size"] = filesystem::file_size(filename);
785 
786  write_config();
787 
788  send_message(message, req.sock);
789 
790  fire("hook_post_upload", upload["name"]);
791  }
792 }
793 
795 {
796  const config& erase = req.cfg;
797 
798  if(read_only_) {
799  LOG_CS << "in read-only mode, request to delete '" << erase["name"] << "' from " << req.addr << " denied\n";
800  send_error("Cannot delete add-on: The server is currently in read-only mode.", req.sock);
801  return;
802  }
803 
804  LOG_CS << "deleting campaign '" << erase["name"] << "' requested from " << req.addr << "\n";
805 
806  config& campaign = get_campaign(erase["name"]);
807 
808  if(!campaign) {
809  send_error("The add-on does not exist.", req.sock);
810  return;
811  }
812 
813  if(!authenticate(campaign, erase["passphrase"])
814  && (campaigns()["master_password"].empty()
815  || campaigns()["master_password"] != erase["passphrase"]))
816  {
817  send_error("The passphrase is incorrect.", req.sock);
818  return;
819  }
820 
821  // Erase the campaign.
822  filesystem::write_file(campaign["filename"], std::string());
823  if(remove(campaign["filename"].str().c_str()) != 0) {
824  ERR_CS << "failed to delete archive for campaign '" << erase["name"]
825  << "' (" << campaign["filename"] << "): " << strerror(errno)
826  << '\n';
827  }
828 
829  config::child_itors itors = campaigns().child_range("campaign");
830  for(size_t index = 0; itors.first != itors.second; ++index, ++itors.first)
831  {
832  if(&campaign == &*itors.first) {
833  campaigns().remove_child("campaign", index);
834  break;
835  }
836  }
837 
838  write_config();
839 
840  send_message("Add-on deleted.", req.sock);
841 
842  fire("hook_post_erase", erase["name"]);
843 
844 }
845 
847 {
848  const config& cpass = req.cfg;
849 
850  if(read_only_) {
851  LOG_CS << "in read-only mode, request to change passphrase denied\n";
852  send_error("Cannot change passphrase: The server is currently in read-only mode.", req.sock);
853  return;
854  }
855 
856  config& campaign = get_campaign(cpass["name"]);
857 
858  if(!campaign) {
859  send_error("No add-on with that name exists.", req.sock);
860  } else if(!authenticate(campaign, cpass["passphrase"])) {
861  send_error("Your old passphrase was incorrect.", req.sock);
862  } else if(cpass["new_passphrase"].empty()) {
863  send_error("No new passphrase was supplied.", req.sock);
864  } else {
865  set_passphrase(campaign, cpass["new_passphrase"]);
866  write_config();
867  send_message("Passphrase changed.", req.sock);
868  }
869 }
870 
871 } // end namespace campaignd
872 
873 int main(int argc, char**argv)
874 {
876 
877  lg::set_log_domain_severity("campaignd", lg::info());
878  lg::timestamps(true);
879 
880  try {
881  std::cerr << "Wesnoth campaignd v" << game_config::revision << " starting...\n";
882 
883  const std::string& cfg_path = filesystem::normalize_path("server.cfg");
884 
885  if(argc >= 2 && atoi(argv[1])){
886  campaignd::server(cfg_path, atoi(argv[1])).run();
887  } else {
888  campaignd::server(cfg_path).run();
889  }
890  } catch(config::error& /*e*/) {
891  std::cerr << "Could not parse config file\n";
892  return 1;
893  } catch(filesystem::io_exception& /*e*/) {
894  std::cerr << "File I/O error\n";
895  return 2;
896  } catch(network::error& e) {
897  std::cerr << "Aborted with network error: " << e.message << '\n';
898  return 3;
899  } catch(std::bad_function_call& /*e*/) {
900  std::cerr << "Bad request handler function call\n";
901  return 4;
902  }
903 
904  return 0;
905 }
child_itors child_range(const std::string &key)
Definition: config.cpp:613
std::string feedback_url_format_
static lg::log_domain log_campaignd("campaignd")
Legacy add-ons server.
void write(const config &cfg)
GLvoid **typedef void(GLAPIENTRY *PFNGLGETVERTEXATTRIBDVPROC)(GLuint
Definition: glew.h:1806
void handle_request_terms(const request &)
all_children_iterator ordered_end() const
Definition: config.cpp:1122
const char * what() const
Definition: exceptions.hpp:35
const GLfloat * c
Definition: glew.h:12741
logger & info()
Definition: log.cpp:91
void handle_delete(const request &)
config & child_or_add(const std::string &key)
Returns a reference to the first child with the given key.
Definition: config.cpp:734
Client request information object.
#define LOG_CS
void timestamps(bool t)
Definition: log.cpp:76
connection receive_data(config &cfg, connection connection_num, unsigned int timeout, bandwidth_in_ptr *bandwidth_in)
Definition: network.cpp:702
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:53
void process_send_queue(connection, size_t)
Function to send any data that is in a connection's send_queue, up to a maximum of 'max_size' bytes �...
Definition: network.cpp:1163
#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:79
request_handlers_table handlers_
connection socket
Definition: network.hpp:265
const config & child_or_empty(const std::string &key) const
Returns the first child with the given key, or an empty config if there is none.
Definition: config.cpp:722
GLint GLenum GLsizei GLint GLsizei const GLvoid * data
Definition: glew.h:1347
void clear_children(const std::string &key)
Definition: config.cpp:820
bool empty() const
Definition: config.cpp:1105
To lexical_cast(From value)
Lexical cast converts one type to another.
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:111
void load_config(const config &v)
void register_handlers()
Registers client request handlers.
const config & cfg
Definition: config.hpp:629
Variant for storing WML attributes.
Definition: config.hpp:223
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_
GLuint in
Definition: glew.h:9261
std::string get_cwd()
bool set_log_domain_severity(std::string const &name, int severity)
Definition: log.cpp:117
void send_file(const std::string &filename, connection connection_num, const std::string &packet_type)
Definition: network.cpp:1071
const config & campaigns() const
Retrieves the contents of the [campaigns] WML node.
GLsizei const char ** path
Definition: glew.h:4654
std::string blacklist_file_
std::string create_hash(const std::string &password, const std::string &salt, int iteration_count)
Definition: hash.cpp:74
Class for writing a config out to a file in pieces.
std::string normalize_path(const std::string &path)
Returns the absolute path of a file.
void write_file(const std::string &fname, const std::string &data)
Throws io_exception if an error occurs.
const child_map::key_type & key
Definition: config.hpp:628
GLenum GLsizei len
Definition: glew.h:5662
std::istream * istream_file(const std::string &fname, bool treat_failure_as_error=true)
void remove_child(const std::string &key, unsigned index)
Definition: config.cpp:899
std::ostream * ostream_file(std::string const &fname, bool create_directory=true)
void erase(const std::string &key)
config & add_child(const std::string &key)
Definition: config.cpp:743
int port_
Definition: network.cpp:375
const network::connection sock
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
#define REGISTER_CAMPAIGND_HANDLER(req_id)
void handle_change_passphrase(const request &)
#define DBG_CS
Templates and utility-routines for strings and numbers.
ADDON_TYPE get_addon_type(const std::string &str)
Definition: validation.cpp:82
size_t send_data(const config &cfg, connection connection_num, const std::string &packet_type)
Function to send data down a given connection, or broadcast to all peers if connection_num is 0...
Definition: network.cpp:1098
bool addon_name_legal(const std::string &name)
Checks whether an add-on id/name is legal or not.
Definition: validation.cpp:48
void send_error(const std::string &msg, network::connection sock)
Send a client an error message.
void write_config()
Writes the server configuration WML back to disk.
boost::scoped_ptr< input_stream > input_
Server control socket.
std::map< std::string, tfilter >::iterator itor
Definition: filter.cpp:199
Thrown by operations encountering invalid UTF-8 data.
const std::string revision
Definition: game_config.cpp:57
Represents a server control line written to a communication socket.
Definition: control.hpp:33
void handle_request_campaign(const request &)
An exception object used when an IO error occurs.
Definition: filesystem.hpp:40
GLuint index
Definition: glew.h:1782
void send_message(const std::string &msg, network::connection sock)
Send a client an informational message.
std::pair< child_iterator, child_iterator > child_itors
Definition: config.hpp:213
int load_config()
Reads the server configuration from WML.
const std::string itoa64
Definition: hash.cpp:23
size_t i
Definition: function.cpp:1057
connection accept_connection()
Function to accept a connection from a remote host.
Definition: network.cpp:582
void register_handler(const std::string &cmd, const request_handler &func)
Registers a single request handler.
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
Definition: debugger.cpp:112
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.
void read(config &cfg, std::istream &in, abstract_validator *validator)
Definition: parser.cpp:400
static int writer(lua_State *L, const void *b, size_t size, void *B)
Definition: lstrlib.cpp:166
bool disconnect(connection s)
Function to disconnect from a certain host, or close all connections if connection_num is 0...
Definition: network.cpp:646
int main(int argc, char **argv)
int file_size(const std::string &fname)
Returns the size of a file, or -1 if the file doesn't exist.
GLuint const GLchar * name
Definition: glew.h:1782
GLsizeiptr size
Definition: glew.h:1649
void handle_request_campaign_list(const request &)
std::function< void(server *, const request &req)> request_handler
std::string ip_address(connection connection_num)
Function to get the remote ip address of a socket.
Definition: network.cpp:1179
const unsigned short default_campaignd_port
Default port number for the addon server.
Definition: validation.cpp:20
config & child(const std::string &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:658
Standard logging facilities (interface).
void read(const config &cfg)
Initializes the blacklist from WML.
Definition: blacklist.cpp:59
std::string message
Definition: exceptions.hpp:29
server(const std::string &cfg_file, size_t min_threads=10, size_t max_threads=0)
GLsizei GLenum GLuint GLuint GLsizei char * message
Definition: glew.h:2499
#define c
Definition: glew.h:12743
bool is_text_markup_char(char c)
Definition: addon_utils.hpp:32
int connection
Definition: network.hpp:74
void set_use_system_sendfile(bool use)
void find_translations(const config &base_dir, config &addon)
Scans an add-on archive directory for translations.
Definition: addon_utils.cpp:93
#define e
#define SIGHUP
bool check_names_legal(const config &dir)
Probes an add-on archive for illegal names.
Definition: validation.cpp:70
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:83
void run()
Runs the server request processing loop.
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:61
Interfaces for manipulating version numbers of engine, add-ons, etc.
void write(std::ostream &out, configr_of const &cfg, unsigned int level)
Definition: parser.cpp:621
Thrown when a lexical_cast fails.
void handle_upload(const request &)
GLsizei const GLcharARB ** string
Definition: glew.h:4503
connection const null_connection
Definition: network.hpp:75
void disconnect()
Definition: network.cpp:238
config & get_campaign(const std::string &id)
Retrieves a campaign by id if found, or a null config otherwise.
all_children_iterator ordered_begin() const
Definition: config.cpp:1117
int compress_level_
Used for add-on archives.