The Battle for Wesnoth  1.15.9+dev
server.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
3  Copyright (C) 2015 - 2020 by Iris Morelle <shadowm2006@gmail.com>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14  */
15 
16 /**
17  * @file
18  * Wesnoth addon server.
19  * Expects a "server.cfg" config file in the current directory
20  * and saves addons under data/.
21  */
22 
24 
25 #include "filesystem.hpp"
26 #include "lexical_cast.hpp"
27 #include "log.hpp"
28 #include "serialization/base64.hpp"
30 #include "serialization/parser.hpp"
33 #include "game_config.hpp"
34 #include "addon/validation.hpp"
41 #include "game_version.hpp"
42 #include "hash.hpp"
43 #include "utils/optimer.hpp"
44 
45 #include <csignal>
46 #include <ctime>
47 #include <iomanip>
48 
49 // the fork execute is unix specific only tested on Linux quite sure it won't
50 // work on Windows not sure which other platforms have a problem with it.
51 #if !(defined(_WIN32))
52 #include <errno.h>
53 #endif
54 
55 static lg::log_domain log_campaignd("campaignd");
56 #define DBG_CS LOG_STREAM(debug, log_campaignd)
57 #define LOG_CS LOG_STREAM(info, log_campaignd)
58 #define WRN_CS LOG_STREAM(warn, log_campaignd)
59 #define ERR_CS LOG_STREAM(err, log_campaignd)
60 
61 static lg::log_domain log_config("config");
62 #define ERR_CONFIG LOG_STREAM(err, log_config)
63 #define WRN_CONFIG LOG_STREAM(warn, log_config)
64 
65 static lg::log_domain log_server("server");
66 #define ERR_SERVER LOG_STREAM(err, log_server)
67 
68 namespace campaignd {
69 
70 namespace {
71 
72 /**
73  * campaignd capabilities supported by this version of the server.
74  *
75  * These are advertised to clients using the @a [server_id] command. They may
76  * be disabled or re-enabled at runtime.
77  */
78 const std::set<std::string> cap_defaults = {
79  // Legacy item and passphrase-based authentication
80  "auth:legacy",
81  // Delta WML packs
82  "delta",
83 };
84 
85 /**
86  * Default URL to the add-ons server web index.
87  */
88 const std::string default_web_url = "https://add-ons.wesnoth.org/";
89 
90 /**
91  * Default license terms for content uploaded to the server.
92  *
93  * This used by both the @a [server_id] command and @a [request_terms] in
94  * their responses.
95  *
96  * The text is intended for display on the client with Pango markup enabled and
97  * sent by the server as-is, so it ought to be formatted accordingly.
98  */
99 const std::string default_license_notice = R"""(<span size='x-large'>General Rules</span>
100 
101 The current version of the server rules can be found at: https://r.wesnoth.org/t51347
102 
103 <span color='#f88'>Any content that does not conform to the rules listed at the link above, as well as the licensing terms below, may be removed at any time without prior notice.</span>
104 
105 <span size='x-large'>Licensing</span>
106 
107 All content within add-ons uploaded to this server must be licensed under the terms of the GNU General Public License (GPL), version 2 or later, with the sole exception of graphics and audio explicitly denoted as released under a Creative Commons license either in:
108 
109  a) a combined toplevel file, e.g. “<span font_family='monospace'>My_Addon/ART_LICENSE</span>”; <b>or</b>
110  b) a file with the same path as the asset with “<span font_family='monospace'>.license</span>” appended, e.g. “<span font_family='monospace'>My_Addon/images/units/axeman.png.license</span>”.
111 
112 <b>By uploading content to this server, you certify that you have the right to:</b>
113 
114  a) release all included art and audio explicitly denoted with a Creative Commons license in the prescribed manner under that license; <b>and</b>
115  b) release all other included content under the terms of the chosen versions of the GNU GPL.)""";
116 
117 bool timing_reports_enabled = false;
118 
119 void timing_report_function(const util::ms_optimer& tim, const campaignd::server::request& req, const std::string& label = {})
120 {
121  if(timing_reports_enabled) {
122  if(label.empty()) {
123  LOG_CS << req << "Time elapsed: " << tim << " ms\n";
124  } else {
125  LOG_CS << req << "Time elapsed [" << label << "]: " << tim << " ms\n";
126  }
127  }
128 }
129 
130 inline util::ms_optimer service_timer(const campaignd::server::request& req, const std::string& label = {})
131 {
132  return util::ms_optimer{std::bind(timing_report_function, std::placeholders::_1, req, label)};
133 }
134 
135 //
136 // Auxiliary shortcut functions
137 //
138 
139 /**
140  * WML version of campaignd::auth::verify_passphrase().
141  *
142  * The salt and hash are retrieved from the @a passsalt and @a passhash
143  * attributes, respectively.
144  */
145 inline bool authenticate(config& addon, const config::attribute_value& passphrase)
146 {
147  return auth::verify_passphrase(passphrase, addon["passsalt"], addon["passhash"]);
148 }
149 
150 /**
151  * WML version of campaignd::auth::generate_hash().
152  *
153  * The salt and hash are written into the @a passsalt and @a passhash
154  * attributes, respectively.
155  */
156 inline void set_passphrase(config& addon, const std::string& passphrase)
157 {
158  std::tie(addon["passsalt"], addon["passhash"]) = auth::generate_hash(passphrase);
159 }
160 
161 /**
162  * Returns the update pack filename for the specified old/new version pair.
163  *
164  * The filename is in the form @p "update_pack_<VERSION_MD5>.gz".
165  */
166 inline std::string make_update_pack_filename(const std::string& old_version, const std::string& new_version)
167 {
168  return "update_pack_" + utils::md5(old_version + new_version).hex_digest() + ".gz";
169 }
170 
171 /**
172  * Returns the full pack filename for the specified version.
173  *
174  * The filename is in the form @p "full_pack_<VERSION_MD5>.gz".
175  */
176 inline std::string make_full_pack_filename(const std::string& version)
177 {
178  return "full_pack_" + utils::md5(version).hex_digest() + ".gz";
179 }
180 
181 /**
182  * Returns the index filename for the specified version.
183  *
184  * The filename is in the form @p "full_pack_<VERSION_MD5>.hash.gz".
185  */
186 inline std::string make_index_filename(const std::string& version)
187 {
188  return "full_pack_" + utils::md5(version).hex_digest() + ".hash.gz";
189 }
190 
191 /**
192  * Returns the index counterpart for the specified full pack file.
193  *
194  * The result is in the same form as make_index_filename().
195  */
196 inline std::string index_from_full_pack_filename(std::string pack_fn)
197 {
198  auto dot_pos = pack_fn.find_last_of('.');
199  if(dot_pos != std::string::npos) {
200  pack_fn.replace(dot_pos, std::string::npos, ".hash.gz");
201  }
202  return pack_fn;
203 }
204 
205 /**
206  * Returns a pointer to a WML child if it exists or nullptr otherwise.
207  */
208 const config* optional_wml_child(const config& cfg, const std::string& child_name)
209 {
210  return cfg.has_child(child_name) ? &cfg.child(child_name) : nullptr;
211 }
212 
213 /**
214  * Returns @a false if @a cfg is null or empty.
215  */
216 bool have_wml(const config* cfg)
217 {
218  return cfg && !cfg->empty();
219 }
220 
221 /**
222  * Scans multiple WML pack-like trees for illegal names.
223  *
224  * Null WML objects are skipped.
225  */
226 bool multi_find_illegal_names(std::vector<std::string>& names, const std::vector<const config*>& indices)
227 {
228  names.clear();
229 
230  for(auto* index : indices) {
231  if(index) {
232  check_names_legal(*index, &names);
233  }
234  }
235 
236  return !names.empty();
237 }
238 
239 /**
240  * Scans multiple WML pack-like trees for case conflicts.
241  *
242  * Null WML objects are skipped.
243  */
244 bool multi_find_case_conflicts(std::vector<std::string>& names, const std::vector<const config*>& indices)
245 {
246  names.clear();
247 
248  for(auto* index : indices) {
249  if(index) {
251  }
252  }
253 
254  return !names.empty();
255 }
256 
257 /**
258  * Escapes double quotes intended to be passed into simple_wml.
259  *
260  * Just why does simple_wml have to be so broken to force us to use this, though?
261  */
262 std::string simple_wml_escape(const std::string& text)
263 {
264  std::string res;
265  auto it = text.begin();
266 
267  while(it != text.end()) {
268  res.append(*it == '"' ? 2 : 1, *it);
269  ++it;
270  }
271 
272  return res;
273 }
274 
275 } // end anonymous namespace
276 
277 server::server(const std::string& cfg_file, unsigned short port)
279  , capabilities_(cap_defaults)
280  , addons_()
281  , dirty_addons_()
282  , cfg_()
283  , cfg_file_(cfg_file)
284  , read_only_(false)
285  , compress_level_(0)
286  , update_pack_lifespan_(0)
287  , strict_versions_(true)
288  , hooks_()
289  , handlers_()
290  , feedback_url_format_()
291  , web_url_()
292  , license_notice_()
293  , blacklist_()
294  , blacklist_file_()
295  , stats_exempt_ips_()
296  , flush_timer_(io_service_)
297 {
298 
299 #ifndef _WIN32
300  struct sigaction sa;
301  std::memset( &sa, 0, sizeof(sa) );
302  #pragma GCC diagnostic ignored "-Wold-style-cast"
303  sa.sa_handler = SIG_IGN;
304  int res = sigaction( SIGPIPE, &sa, nullptr);
305  assert( res == 0 );
306 #endif
307  load_config();
308 
309  // Command line config override. This won't get saved back to disk since we
310  // leave the WML intentionally untouched.
311  if(port != 0) {
312  port_ = port;
313  }
314 
315  LOG_CS << "Port: " << port_ << '\n';
316  LOG_CS << "Server directory: " << game_config::path << " (" << addons_.size() << " add-ons)\n";
317 
319 
320  start_server();
321  flush_cfg();
322 }
323 
325 {
326  write_config();
327 }
328 
330 {
331  LOG_CS << "Reading configuration from " << cfg_file_ << "...\n";
332 
334  read(cfg_, *in);
335 
336  read_only_ = cfg_["read_only"].to_bool(false);
337 
338  if(read_only_) {
339  LOG_CS << "READ-ONLY MODE ACTIVE\n";
340  }
341 
342  strict_versions_ = cfg_["strict_versions"].to_bool(true);
343 
344  // Seems like compression level above 6 is a waste of CPU cycles.
345  compress_level_ = cfg_["compress_level"].to_int(6);
346  // One month probably will be fine (#TODO: testing needed)
347  update_pack_lifespan_ = cfg_["update_pack_lifespan"].to_time_t(30 * 24 * 60 * 60);
348 
349  if(const auto& svinfo_cfg = server_info()) {
350  feedback_url_format_ = svinfo_cfg["feedback_url_format"].str();
351  web_url_ = svinfo_cfg["web_url"].str(default_web_url);
352  license_notice_ = svinfo_cfg["license_notice"].str(default_license_notice);
353  }
354 
355  blacklist_file_ = cfg_["blacklist_file"].str();
356  load_blacklist();
357 
358  stats_exempt_ips_ = utils::split(cfg_["stats_exempt_ips"].str());
359 
360  // Load any configured hooks.
361  hooks_.emplace(std::string("hook_post_upload"), cfg_["hook_post_upload"]);
362  hooks_.emplace(std::string("hook_post_erase"), cfg_["hook_post_erase"]);
363 
364 #ifndef _WIN32
365  // Open the control socket if enabled.
366  if(!cfg_["control_socket"].empty()) {
367  const std::string& path = cfg_["control_socket"].str();
368 
369  if(path != fifo_path_) {
370  const int res = mkfifo(path.c_str(),0660);
371  if(res != 0 && errno != EEXIST) {
372  ERR_CS << "could not make fifo at '" << path << "' (" << strerror(errno) << ")\n";
373  } else {
374  input_.close();
375  int fifo = open(path.c_str(), O_RDWR|O_NONBLOCK);
376  input_.assign(fifo);
377  LOG_CS << "opened fifo at '" << path << "'. Server commands may be written to this file.\n";
378  read_from_fifo();
379  fifo_path_ = path;
380  }
381  }
382  }
383 #endif
384 
385  // Certain config values are saved to WML again so that a given server
386  // instance's parameters remain constant even if the code defaults change
387  // at some later point.
388  cfg_["compress_level"] = compress_level_;
389 
390  // But not the listening port number.
391  port_ = cfg_["port"].to_int(default_campaignd_port);
392 
393  // Limit the max size of WML documents received from the net to prevent the
394  // possible excessive use of resources due to malformed packets received.
395  // Since an addon is sent in a single WML document this essentially limits
396  // the maximum size of an addon that can be uploaded.
398 
399  //Loading addons
400  addons_.clear();
401  std::vector<std::string> legacy_addons, dirs;
402  filesystem::get_files_in_dir("data", &legacy_addons, &dirs);
403  config meta;
404  for(const std::string& addon_dir : dirs) {
405  in = filesystem::istream_file(filesystem::normalize_path("data/" + addon_dir + "/addon.cfg"));
406  read(meta, *in);
407  if(!meta.empty()) {
408  addons_.emplace(meta["name"].str(), meta);
409  } else {
410  throw filesystem::io_exception("Failed to load addon from dir '" + addon_dir + "'\n");
411  }
412  }
413 
414  // Convert all legacy addons to the new format on load
415  if(cfg_.has_child("campaigns")) {
416  config& campaigns = cfg_.child("campaigns");
417  WRN_CS << "Old format addons have been detected in the config! They will be converted to the new file format! "
418  << campaigns.child_count("campaign") << " entries to be processed.\n";
419  for(config& campaign : campaigns.child_range("campaign")) {
420  const std::string& addon_id = campaign["name"].str();
421  const std::string& addon_file = campaign["filename"].str();
422  if(get_addon(addon_id)) {
423  throw filesystem::io_exception("The addon '" + addon_id
424  + "' already exists in the new form! Possible code or filesystem interference!\n");
425  }
426  if(std::find(legacy_addons.begin(), legacy_addons.end(), addon_id) == legacy_addons.end()) {
427  throw filesystem::io_exception("No file has been found for the legacy addon '" + addon_id
428  + "'. Check the file structure!\n");
429  }
430 
431  config data;
433  read_gz(data, *in);
434  if(!data) {
435  throw filesystem::io_exception("Couldn't read the content file for the legacy addon '" + addon_id + "'!\n");
436  }
437 
438  config version_cfg = config("version", campaign["version"].str());
439  version_cfg["filename"] = make_full_pack_filename(campaign["version"]);
440  campaign.add_child("version", version_cfg);
441 
442  data.remove_attributes("title", "campaign_name", "author", "description", "version", "timestamp", "original_timestamp", "icon", "type", "tags");
444  {
445  filesystem::atomic_commit campaign_file(addon_file + "/" + version_cfg["filename"].str());
446  config_writer writer(*campaign_file.ostream(), true, compress_level_);
447  writer.write(data);
448  campaign_file.commit();
449  }
450  {
451  filesystem::atomic_commit campaign_hash_file(addon_file + "/" + make_index_filename(campaign["version"]));
452  config_writer writer(*campaign_hash_file.ostream(), true, compress_level_);
453  config data_hash = config("name", "");
454  write_hashlist(data_hash, data);
455  writer.write(data_hash);
456  campaign_hash_file.commit();
457  }
458 
459  addons_.emplace(addon_id, campaign);
460  mark_dirty(addon_id);
461  }
462  cfg_.clear_children("campaigns");
463  LOG_CS << "Legacy addons processing finished.\n";
464  write_config();
465  }
466 
467  LOG_CS << "Loaded addons metadata. " << addons_.size() << " addons found.\n";
468 }
469 
470 std::ostream& operator<<(std::ostream& o, const server::request& r)
471 {
472  o << '[' << r.addr << ' ' << r.cmd << "] ";
473  return o;
474 }
475 
477 {
478  boost::asio::spawn(io_service_, [this, socket](boost::asio::yield_context yield) {
479  while(true) {
480  boost::system::error_code ec;
481  auto doc { coro_receive_doc(socket, yield[ec]) };
482  if(check_error(ec, socket) || !doc) return;
483 
484  config data;
485  read(data, doc->output());
486 
488 
489  if(i != data.ordered_end()) {
490  // We only handle the first child.
491  const config::any_child& c = *i;
492 
493  request_handlers_table::const_iterator j
494  = handlers_.find(c.key);
495 
496  if(j != handlers_.end()) {
497  // Call the handler.
498  request req{c.key, c.cfg, socket, yield};
499  auto st = service_timer(req);
500  j->second(this, req);
501  } else {
502  send_error("Unrecognized [" + c.key + "] request.",socket);
503  }
504  }
505  }
506  });
507 }
508 
509 #ifndef _WIN32
510 
511 void server::handle_read_from_fifo(const boost::system::error_code& error, std::size_t)
512 {
513  if(error) {
514  if(error == boost::asio::error::operation_aborted)
515  // This means fifo was closed by load_config() to open another fifo
516  return;
517  ERR_CS << "Error reading from fifo: " << error.message() << '\n';
518  return;
519  }
520 
521  std::istream is(&admin_cmd_);
522  std::string cmd;
523  std::getline(is, cmd);
524 
525  const control_line ctl = cmd;
526 
527  if(ctl == "shut_down") {
528  LOG_CS << "Shut down requested by admin, shutting down...\n";
529  throw server_shutdown("Shut down via fifo command");
530  } else if(ctl == "readonly") {
531  if(ctl.args_count()) {
532  cfg_["read_only"] = read_only_ = utils::string_bool(ctl[1], true);
533  }
534 
535  LOG_CS << "Read only mode: " << (read_only_ ? "enabled" : "disabled") << '\n';
536  } else if(ctl == "flush") {
537  LOG_CS << "Flushing config to disk...\n";
538  write_config();
539  } else if(ctl == "reload") {
540  if(ctl.args_count()) {
541  if(ctl[1] == "blacklist") {
542  LOG_CS << "Reloading blacklist...\n";
543  load_blacklist();
544  } else {
545  ERR_CS << "Unrecognized admin reload argument: " << ctl[1] << '\n';
546  }
547  } else {
548  LOG_CS << "Reloading all configuration...\n";
549  load_config();
550  LOG_CS << "Reloaded configuration\n";
551  }
552  } else if(ctl == "delete") {
553  if(ctl.args_count() != 1) {
554  ERR_CS << "Incorrect number of arguments for 'delete'\n";
555  } else {
556  const std::string& addon_id = ctl[1];
557 
558  LOG_CS << "deleting add-on '" << addon_id << "' requested from control FIFO\n";
559  delete_addon(addon_id);
560  }
561  } else if(ctl == "hide" || ctl == "unhide") {
562  if(ctl.args_count() != 1) {
563  ERR_CS << "Incorrect number of arguments for '" << ctl.cmd() << "'\n";
564  } else {
565  const std::string& addon_id = ctl[1];
566  config& addon = get_addon(addon_id);
567 
568  if(!addon) {
569  ERR_CS << "Add-on '" << addon_id << "' not found, cannot " << ctl.cmd() << "\n";
570  } else {
571  addon["hidden"] = ctl.cmd() == "hide";
572  mark_dirty(addon_id);
573  write_config();
574  LOG_CS << "Add-on '" << addon_id << "' is now " << (ctl.cmd() == "hide" ? "hidden" : "unhidden") << '\n';
575  }
576  }
577  } else if(ctl == "setpass") {
578  if(ctl.args_count() != 2) {
579  ERR_CS << "Incorrect number of arguments for 'setpass'\n";
580  } else {
581  const std::string& addon_id = ctl[1];
582  const std::string& newpass = ctl[2];
583  config& addon = get_addon(addon_id);
584 
585  if(!addon) {
586  ERR_CS << "Add-on '" << addon_id << "' not found, cannot set passphrase\n";
587  } else if(newpass.empty()) {
588  // Shouldn't happen!
589  ERR_CS << "Add-on passphrases may not be empty!\n";
590  } else {
591  set_passphrase(addon, newpass);
592  mark_dirty(addon_id);
593  write_config();
594  LOG_CS << "New passphrase set for '" << addon_id << "'\n";
595  }
596  }
597  } else if(ctl == "setattr") {
598  if(ctl.args_count() != 3) {
599  ERR_CS << "Incorrect number of arguments for 'setattr'\n";
600  } else {
601  const std::string& addon_id = ctl[1];
602  const std::string& key = ctl[2];
603  const std::string& value = ctl[3];
604 
605  config& addon = get_addon(addon_id);
606 
607  if(!addon) {
608  ERR_CS << "Add-on '" << addon_id << "' not found, cannot set attribute\n";
609  } else if(key == "name" || key == "version") {
610  ERR_CS << "setattr cannot be used to rename add-ons or change their version\n";
611  } else if(key == "passhash"|| key == "passsalt") {
612  ERR_CS << "setattr cannot be used to set auth data -- use setpass instead\n";
613  } else if(!addon.has_attribute(key)) {
614  // NOTE: This is a very naive approach for validating setattr's
615  // input, but it should generally work since add-on
616  // uploads explicitly set all recognized attributes to
617  // the values provided by the .pbl data or the empty
618  // string if absent, and this is normally preserved by
619  // the config serialization.
620  ERR_CS << "Attribute '" << value << "' is not a recognized add-on attribute\n";
621  } else {
622  addon[key] = value;
623  mark_dirty(addon_id);
624  write_config();
625  LOG_CS << "Set attribute on add-on '" << addon_id << "':\n"
626  << key << "=\"" << value << "\"\n";
627  }
628  }
629  } else {
630  ERR_CS << "Unrecognized admin command: " << ctl.full() << '\n';
631  }
632 
633  read_from_fifo();
634 }
635 
636 void server::handle_sighup(const boost::system::error_code&, int)
637 {
638  LOG_CS << "SIGHUP caught, reloading config.\n";
639 
640  load_config(); // TODO: handle port number config changes
641 
642  LOG_CS << "Reloaded configuration\n";
643 
644  sighup_.async_wait(std::bind(&server::handle_sighup, this, std::placeholders::_1, std::placeholders::_2));
645 }
646 
647 #endif
648 
650 {
651  flush_timer_.expires_from_now(std::chrono::minutes(10));
652  flush_timer_.async_wait(std::bind(&server::handle_flush, this, std::placeholders::_1));
653 }
654 
655 void server::handle_flush(const boost::system::error_code& error)
656 {
657  if(error) {
658  ERR_CS << "Error from reload timer: " << error.message() << "\n";
659  throw boost::system::system_error(error);
660  }
661  write_config();
662  flush_cfg();
663 }
664 
666 {
667  // We *always* want to clear the blacklist first, especially if we are
668  // reloading the configuration and the blacklist is no longer enabled.
669  blacklist_.clear();
670 
671  if(blacklist_file_.empty()) {
672  return;
673  }
674 
675  try {
677  config blcfg;
678 
679  read(blcfg, *in);
680 
681  blacklist_.read(blcfg);
682  LOG_CS << "using blacklist from " << blacklist_file_ << '\n';
683  } catch(const config::error&) {
684  ERR_CS << "failed to read blacklist from " << blacklist_file_ << ", blacklist disabled\n";
685  }
686 }
687 
689 {
690  DBG_CS << "writing configuration and add-ons list to disk...\n";
692  write(*out.ostream(), cfg_);
693  out.commit();
694 
695  for(const std::string& name : dirty_addons_) {
696  const config& addon = get_addon(name);
697  if(addon && !addon["filename"].empty()) {
698  filesystem::atomic_commit addon_out(filesystem::normalize_path(addon["filename"].str() + "/addon.cfg"));
699  write(*addon_out.ostream(), addon);
700  addon_out.commit();
701  }
702  }
703 
704  dirty_addons_.clear();
705  DBG_CS << "... done\n";
706 }
707 
708 void server::fire(const std::string& hook, const std::string& addon)
709 {
710  const std::map<std::string, std::string>::const_iterator itor = hooks_.find(hook);
711  if(itor == hooks_.end()) {
712  return;
713  }
714 
715  const std::string& script = itor->second;
716  if(script.empty()) {
717  return;
718  }
719 
720 #if defined(_WIN32)
721  UNUSED(addon);
722  ERR_CS << "Tried to execute a script on an unsupported platform\n";
723  return;
724 #else
725  pid_t childpid;
726 
727  if((childpid = fork()) == -1) {
728  ERR_CS << "fork failed while updating add-on " << addon << '\n';
729  return;
730  }
731 
732  if(childpid == 0) {
733  // We are the child process. Execute the script. We run as a
734  // separate thread sharing stdout/stderr, which will make the
735  // log look ugly.
736  execlp(script.c_str(), script.c_str(), addon.c_str(), static_cast<char *>(nullptr));
737 
738  // exec() and family never return; if they do, we have a problem
739  std::cerr << "ERROR: exec failed with errno " << errno << " for addon " << addon
740  << '\n';
741  exit(errno);
742 
743  } else {
744  return;
745  }
746 #endif
747 }
748 
749 bool server::ignore_address_stats(const std::string& addr) const
750 {
751  for(const auto& mask : stats_exempt_ips_) {
752  // TODO: we want CIDR subnet mask matching here, not glob matching!
753  if(utils::wildcard_string_match(addr, mask)) {
754  return true;
755  }
756  }
757 
758  return false;
759 }
760 
761 void server::send_message(const std::string& msg, socket_ptr sock)
762 {
763  const auto& escaped_msg = simple_wml_escape(msg);
765  doc.root().add_child("message").set_attr_dup("message", escaped_msg.c_str());
766  async_send_doc_queued(sock, doc);
767 }
768 
769 void server::send_error(const std::string& msg, socket_ptr sock)
770 {
771  ERR_CS << "[" << client_address(sock) << "] " << msg << '\n';
772  const auto& escaped_msg = simple_wml_escape(msg);
774  doc.root().add_child("error").set_attr_dup("message", escaped_msg.c_str());
775  async_send_doc_queued(sock, doc);
776 }
777 
778 void server::send_error(const std::string& msg, const std::string& extra_data, unsigned int status_code, socket_ptr sock)
779 {
780  const std::string& status_hex = formatter()
781  << "0x" << std::setfill('0') << std::setw(2*sizeof(unsigned int)) << std::hex
782  << std::uppercase << status_code;
783  ERR_CS << "[" << client_address(sock) << "]: (" << status_hex << ") " << msg << '\n';
784 
785  const auto& escaped_status_str = simple_wml_escape(std::to_string(status_code));
786  const auto& escaped_msg = simple_wml_escape(msg);
787  const auto& escaped_extra_data = simple_wml_escape(extra_data);
788 
790  simple_wml::node& err_cfg = doc.root().add_child("error");
791 
792  err_cfg.set_attr_dup("message", escaped_msg.c_str());
793  err_cfg.set_attr_dup("extra_data", escaped_extra_data.c_str());
794  err_cfg.set_attr_dup("status_code", escaped_status_str.c_str());
795 
796  async_send_doc_queued(sock, doc);
797 }
798 
799 config& server::get_addon(const std::string& id)
800 {
801  auto addon = addons_.find(id);
802  if(addon != addons_.end()) {
803  return addon->second;
804  } else {
805  return config::get_invalid();
806  }
807 }
808 
809 void server::delete_addon(const std::string& id)
810 {
811  config& cfg = get_addon(id);
812 
813  if(!cfg) {
814  ERR_CS << "Cannot delete unrecognized add-on '" << id << "'\n";
815  return;
816  }
817 
818  std::string fn = cfg["filename"].str();
819 
820  if(fn.empty()) {
821  ERR_CS << "Add-on '" << id << "' does not have an associated filename, cannot delete\n";
822  }
823 
825  ERR_CS << "Could not delete the directory for addon '" << id
826  << "' (" << fn << "): " << strerror(errno) << '\n';
827  }
828 
829  addons_.erase(id);
830  write_config();
831 
832  fire("hook_post_erase", id);
833 
834  LOG_CS << "Deleted add-on '" << id << "'\n";
835 }
836 
837 #define REGISTER_CAMPAIGND_HANDLER(req_id) \
838  handlers_[#req_id] = std::bind(&server::handle_##req_id, \
839  std::placeholders::_1, std::placeholders::_2)
840 
842 {
843  REGISTER_CAMPAIGND_HANDLER(server_id);
844  REGISTER_CAMPAIGND_HANDLER(request_campaign_list);
845  REGISTER_CAMPAIGND_HANDLER(request_campaign);
846  REGISTER_CAMPAIGND_HANDLER(request_campaign_hash);
847  REGISTER_CAMPAIGND_HANDLER(request_terms);
850  REGISTER_CAMPAIGND_HANDLER(change_passphrase);
851 }
852 
854 {
855  DBG_CS << req << "Sending server identification\n";
856 
857  std::ostringstream ostr;
858  write(ostr, config{"server_id", config{
859  "cap", utils::join(capabilities_),
860  "version", game_config::revision,
861  "url", web_url_,
862  "license_notice", license_notice_,
863  }});
864 
865  const auto& wml = ostr.str();
867  doc.compress();
868 
869  async_send_doc_queued(req.sock, doc);
870 }
871 
873 {
874  LOG_CS << req << "Sending add-ons list\n";
875 
876  std::time_t epoch = std::time(nullptr);
878 
879  addons_list["timestamp"] = epoch;
880  if(req.cfg["times_relative_to"] != "now") {
881  epoch = 0;
882  }
883 
884  bool before_flag = false;
885  std::time_t before = epoch;
886  if(!req.cfg["before"].empty()) {
887  before += req.cfg["before"].to_time_t();
888  before_flag = true;
889  }
890 
891  bool after_flag = false;
892  std::time_t after = epoch;
893  if(!req.cfg["after"].empty()) {
894  after += req.cfg["after"].to_time_t();
895  after_flag = true;
896  }
897 
898  const std::string& name = req.cfg["name"];
899  const std::string& lang = req.cfg["language"];
900 
901  for(const auto& addon : addons_)
902  {
903  if(!name.empty() && name != addon.first) {
904  continue;
905  }
906 
907  config i = addon.second;
908 
909  if(i["hidden"].to_bool()) {
910  continue;
911  }
912 
913  const auto& tm = i["timestamp"];
914 
915  if(before_flag && (tm.empty() || tm.to_time_t(0) >= before)) {
916  continue;
917  }
918  if(after_flag && (tm.empty() || tm.to_time_t(0) <= after)) {
919  continue;
920  }
921 
922  if(!lang.empty()) {
923  bool found = false;
924 
925  for(const config& j : i.child_range("translation"))
926  {
927  if(j["language"] == lang && j["supported"].to_bool(true)) {//for old addons
928  found = true;
929  break;
930  }
931  }
932 
933  if(!found) {
934  continue;
935  }
936  }
937 
938  addons_list.add_child("campaign", i);
939  }
940 
941  for(config& j : addons_list.child_range("campaign"))
942  {
943  // Remove attributes containing information that's considered sensitive
944  // or irrelevant to clients
945  j.remove_attributes("passphrase", "passhash", "passsalt", "upload_ip", "email");
946 
947  // Build a feedback_url string attribute from the internal [feedback]
948  // data or deliver an empty value, in case clients decide to assume its
949  // presence.
950  const config& url_params = j.child_or_empty("feedback");
951  j["feedback_url"] = !url_params.empty() && !feedback_url_format_.empty()
953 
954  // Clients don't need to see the original data, so discard it.
955  j.clear_children("feedback");
956 
957  // Update packs info is internal stuff
958  j.clear_children("update_pack");
959  }
960 
961  config response;
962  response.add_child("campaigns", std::move(addons_list));
963 
964  std::ostringstream ostr;
965  write(ostr, response);
966  std::string wml = ostr.str();
968  doc.compress();
969 
970  async_send_doc_queued(req.sock, doc);
971 }
972 
974 {
975  config& addon = get_addon(req.cfg["name"]);
976 
977  if(!addon || addon["hidden"].to_bool()) {
978  send_error("Add-on '" + req.cfg["name"].str() + "' not found.", req.sock);
979  return;
980  }
981 
982  const auto& name = req.cfg["name"].str();
983  auto version_map = get_version_map(addon);
984 
985  if(version_map.empty()) {
986  send_error("No versions of the add-on '" + name + "' are available on the server.", req.sock);
987  return;
988  }
989 
990  // Base the payload against the latest version if no particular version is being requested
991  const auto& from = req.cfg["from_version"].str();
992  const auto& to = req.cfg["version"].str(version_map.rbegin()->first);
993 
994  auto to_version_iter = version_map.find(version_info{to});
995  if(to_version_iter == version_map.end()) {
996  send_error("Could not find requested version " + to + " of the addon '" + name +
997  "'.", req.sock);
998  return;
999  }
1000 
1001  auto full_pack_path = addon["filename"].str() + '/' + to_version_iter->second["filename"].str();
1002  const int full_pack_size = filesystem::file_size(full_pack_path);
1003 
1004  if(!from.empty() && version_map.count(version_info{from}) != 0) {
1005  // Build a sequence of updates beginning from the client's old version to the
1006  // requested version. Every pair of incrementing versions on the server should
1007  // have an update pack written to disk during the original upload(s).
1008  //
1009  // TODO: consider merging update packs instead of building a linear
1010  // and possibly redundant sequence out of them.
1011 
1012  config delta;
1013  int delivery_size = 0;
1014  bool force_use_full = false;
1015 
1016  auto start_point = version_map.find(version_info{from}); // Already known to exist
1017  auto end_point = std::next(to_version_iter, 1); // May be end()
1018 
1019  if(std::distance(start_point, end_point) <= 1) {
1020  // This should not happen, skip the sequence build entirely
1021  ERR_CS << "Bad update sequence bounds in version " << from << " -> " << to << " update sequence for the add-on '" << name << "', sending a full pack instead\n";
1022  force_use_full = true;
1023  }
1024 
1025  for(auto iter = start_point; !force_use_full && std::distance(iter, end_point) > 1;) {
1026  const auto& prev_version_cfg = iter->second;
1027  const auto& next_version_cfg = (++iter)->second;
1028 
1029  for(const config& pack : addon.child_range("update_pack")) {
1030  if(pack["from"].str() != prev_version_cfg["version"].str() ||
1031  pack["to"].str() != next_version_cfg["version"].str()) {
1032  continue;
1033  }
1034 
1035  config step_delta;
1036  const auto& update_pack_path = addon["filename"].str() + '/' + pack["filename"].str();
1037  auto in = filesystem::istream_file(update_pack_path);
1038 
1039  read_gz(step_delta, *in);
1040 
1041  if(!step_delta.empty()) {
1042  // Don't copy arbitrarily large data around
1043  delta.append(std::move(step_delta));
1044  delivery_size += filesystem::file_size(update_pack_path);
1045  } else {
1046  ERR_CS << "Broken update sequence from version " << from << " to "
1047  << to << " for the add-on '" << name << "', sending a full pack instead\n";
1048  force_use_full = true;
1049  break;
1050  }
1051 
1052  // No point in sending an overlarge delta update.
1053  // FIXME: This doesn't take into account over-the-wire compression
1054  // from async_send_doc() though, maybe some heuristics based on
1055  // individual update pack size would be useful?
1056  if(delivery_size > full_pack_size && full_pack_size > 0) {
1057  force_use_full = true;
1058  break;
1059  }
1060  }
1061  }
1062 
1063  if(!force_use_full && !delta.empty()) {
1064  std::ostringstream ostr;
1065  write(ostr, delta);
1066  const auto& wml_text = ostr.str();
1067 
1068  simple_wml::document doc(wml_text.c_str(), simple_wml::INIT_STATIC);
1069  doc.compress();
1070 
1071  LOG_CS << req << "Sending add-on '" << name << "' version: " << from << " -> " << to << " (delta))\n";
1072 
1073  boost::system::error_code ec;
1074  coro_send_doc(req.sock, doc, req.yield[ec]);
1075  if(check_error(ec, req.sock)) return;
1076 
1077  full_pack_path.clear();
1078  }
1079  }
1080 
1081  // Send a full pack if the client's previous version was not specified, is
1082  // not known by the server, or if any other condition above caused us to
1083  // give up on the update pack option.
1084  if(!full_pack_path.empty()) {
1085  if(full_pack_size < 0) {
1086  send_error("Add-on '" + name + "' could not be read by the server.", req.sock);
1087  return;
1088  }
1089 
1090  LOG_CS << req << "Sending add-on '" << name << "' version: " << to << " size: " << full_pack_size / 1024 << " KiB\n";
1091  boost::system::error_code ec;
1092  coro_send_file(req.sock, full_pack_path, req.yield[ec]);
1093  if(check_error(ec, req.sock)) return;
1094  }
1095 
1096  // Clients doing upgrades or some other specific thing shouldn't bump
1097  // the downloads count. Default to true for compatibility with old
1098  // clients that won't tell us what they are trying to do.
1099  if(req.cfg["increase_downloads"].to_bool(true) && !ignore_address_stats(req.addr)) {
1100  addon["downloads"] = 1 + addon["downloads"].to_int();
1101  mark_dirty(name);
1102  }
1103 }
1104 
1106 {
1107  config& addon = get_addon(req.cfg["name"]);
1108 
1109  if(!addon || addon["hidden"].to_bool()) {
1110  send_error("Add-on '" + req.cfg["name"].str() + "' not found.", req.sock);
1111  return;
1112  }
1113 
1114  std::string path = addon["filename"].str() + '/';
1115 
1116  auto version_map = get_version_map(addon);
1117 
1118  if(version_map.empty()) {
1119  send_error("No versions of the add-on '" + req.cfg["name"].str() + "' are available on the server.", req.sock);
1120  return;
1121  } else {
1122  const auto& version_str = addon["version"].str();
1123  version_info version_parsed{version_str};
1124  auto version = version_map.find(version_parsed);
1125  if(version != version_map.end()) {
1126  path += version->second["filename"].str();
1127  } else {
1128  // Selecting the latest version before the selected version or the overall latest version if unspecified
1129  if(version_str.empty()) {
1130  path += version_map.rbegin()->second["filename"].str();
1131  } else {
1132  path += (--version_map.upper_bound(version_parsed))->second["filename"].str();
1133  }
1134  }
1135 
1136  path = index_from_full_pack_filename(path);
1137  const int file_size = filesystem::file_size(path);
1138 
1139  if(file_size < 0) {
1140  send_error("Missing index file for the add-on '" + req.cfg["name"].str() + "'.", req.sock);
1141  return;
1142  }
1143 
1144  LOG_CS << req << "Sending add-on hash index for '" << req.cfg["name"] << "' size: " << file_size / 1024 << " KiB\n";
1145  boost::system::error_code ec;
1146  coro_send_file(req.sock, path, req.yield[ec]);
1147  if(check_error(ec, req.sock)) return;
1148  }
1149 }
1150 
1152 {
1153  // This usually means the client wants to upload content, so tell it
1154  // to give up when we're in read-only mode.
1155  if(read_only_) {
1156  LOG_CS << "in read-only mode, request for upload terms denied\n";
1157  send_error("The server is currently in read-only mode, add-on uploads are disabled.", req.sock);
1158  return;
1159  }
1160 
1161  LOG_CS << req << "Sending license terms\n";
1162  send_message(license_notice_, req.sock);
1163 }
1164 
1165 ADDON_CHECK_STATUS server::validate_addon(const server::request& req, config*& existing_addon, std::string& error_data)
1166 {
1167  if(read_only_) {
1168  LOG_CS << "Validation error: uploads not permitted in read-only mode.\n";
1170  }
1171 
1172  const config& upload = req.cfg;
1173 
1174  const config* data = optional_wml_child(upload, "data");
1175  const config* removelist = optional_wml_child(upload, "removelist");
1176  const config* addlist = optional_wml_child(upload, "addlist");
1177 
1178  const bool is_upload_pack = have_wml(removelist) || have_wml(addlist);
1179 
1180  const std::string& name = upload["name"].str();
1181 
1182  existing_addon = nullptr;
1183  error_data.clear();
1184 
1185  bool passed_name_utf8_check = false;
1186 
1187  try {
1188  const std::string& lc_name = utf8::lowercase(name);
1189  passed_name_utf8_check = true;
1190 
1191  for(auto& c : addons_) {
1192  if(utf8::lowercase(c.first) == lc_name) {
1193  existing_addon = &c.second;
1194  break;
1195  }
1196  }
1197  } catch(const utf8::invalid_utf8_exception&) {
1198  if(!passed_name_utf8_check) {
1199  LOG_CS << "Validation error: bad UTF-8 in add-on name\n";
1201  } else {
1202  ERR_CS << "Validation error: add-ons list has bad UTF-8 somehow, this is a server side issue, it's bad, and you should probably fix it ASAP\n";
1204  }
1205  }
1206 
1207  // Auth and block-list based checks go first
1208 
1209  if(upload["passphrase"].empty()) {
1210  LOG_CS << "Validation error: no passphrase specified\n";
1212  }
1213 
1214  if(existing_addon && !authenticate(*existing_addon, upload["passphrase"])) {
1215  LOG_CS << "Validation error: passphrase does not match\n";
1217  }
1218 
1219  if(existing_addon && (*existing_addon)["hidden"].to_bool()) {
1220  LOG_CS << "Validation error: add-on is hidden\n";
1222  }
1223 
1224  try {
1225  if(blacklist_.is_blacklisted(name,
1226  upload["title"].str(),
1227  upload["description"].str(),
1228  upload["author"].str(),
1229  req.addr,
1230  upload["email"].str()))
1231  {
1232  LOG_CS << "Validation error: blacklisted uploader or publish information\n";
1234  }
1235  } catch(const utf8::invalid_utf8_exception&) {
1236  LOG_CS << "Validation error: invalid UTF-8 sequence in publish information while checking against the blacklist\n";
1238  }
1239 
1240  // Structure and syntax checks follow
1241 
1242  if(!is_upload_pack && !have_wml(data)) {
1243  LOG_CS << "Validation error: no add-on data.\n";
1245  }
1246 
1247  if(is_upload_pack && !have_wml(removelist) && !have_wml(addlist)) {
1248  LOG_CS << "Validation error: no add-on data.\n";
1250  }
1251 
1252  if(!addon_name_legal(name)) {
1253  LOG_CS << "Validation error: invalid add-on name.\n";
1255  }
1256 
1257  if(is_text_markup_char(name[0])) {
1258  LOG_CS << "Validation error: add-on name starts with an illegal formatting character.\n";
1260  }
1261 
1262  if(upload["title"].empty()) {
1263  LOG_CS << "Validation error: no add-on title specified\n";
1265  }
1266 
1267  if(is_text_markup_char(upload["title"].str()[0])) {
1268  LOG_CS << "Validation error: add-on title starts with an illegal formatting character.\n";
1270  }
1271 
1272  if(get_addon_type(upload["type"]) == ADDON_UNKNOWN) {
1273  LOG_CS << "Validation error: unknown add-on type specified\n";
1275  }
1276 
1277  if(upload["author"].empty()) {
1278  LOG_CS << "Validation error: no add-on author specified\n";
1280  }
1281 
1282  if(upload["version"].empty()) {
1283  LOG_CS << "Validation error: no add-on version specified\n";
1285  }
1286 
1287  if(existing_addon) {
1288  version_info new_version{upload["version"].str()};
1289  version_info old_version{(*existing_addon)["version"].str()};
1290 
1291  if(strict_versions_ ? new_version <= old_version : new_version < old_version) {
1292  LOG_CS << "Validation error: add-on version not incremented\n";
1294  }
1295  }
1296 
1297  if(upload["description"].empty()) {
1298  LOG_CS << "Validation error: no add-on description specified\n";
1300  }
1301 
1302  if(upload["email"].empty()) {
1303  LOG_CS << "Validation error: no add-on email specified\n";
1305  }
1306 
1307  std::vector<std::string> badnames;
1308 
1309  if(multi_find_illegal_names(badnames, {data, addlist, removelist})) {
1310  error_data = utils::join(badnames, "\n");
1311  LOG_CS << "Validation error: invalid filenames in add-on pack (" << badnames.size() << " entries)\n";
1313  }
1314 
1315  if(multi_find_case_conflicts(badnames, {data, addlist, removelist})) {
1316  error_data = utils::join(badnames, "\n");
1317  LOG_CS << "Validation error: case conflicts in add-on pack (" << badnames.size() << " entries)\n";
1319  }
1320 
1321  if(is_upload_pack && !existing_addon) {
1322  LOG_CS << "Validation error: attempted to send an update pack for a non-existent add-on\n";
1324  }
1325 
1327 }
1328 
1330 {
1331  const std::time_t upload_ts = std::time(nullptr);
1332  const config& upload = req.cfg;
1333  const auto& name = upload["name"].str();
1334 
1335  LOG_CS << req << "Validating add-on '" << name << "'...\n";
1336 
1337  config* addon_ptr = nullptr;
1338  std::string val_error_data;
1339  const auto val_status = validate_addon(req, addon_ptr, val_error_data);
1340 
1341  if(val_status != ADDON_CHECK_STATUS::SUCCESS) {
1342  LOG_CS << "Upload of '" << name << "' aborted due to a failed validation check\n";
1343  const auto msg = std::string("Add-on rejected: ") + addon_check_status_desc(val_status);
1344  send_error(msg, val_error_data, static_cast<unsigned int>(val_status), req.sock);
1345  return;
1346  }
1347 
1348  LOG_CS << req << "Processing add-on '" << name << "'...\n";
1349 
1350  const config* const full_pack = optional_wml_child(upload, "data");
1351  const config* const delta_remove = optional_wml_child(upload, "removelist");
1352  const config* const delta_add = optional_wml_child(upload, "addlist");
1353 
1354  const bool is_delta_upload = have_wml(delta_remove) || have_wml(delta_add);
1355  const bool is_existing_upload = addon_ptr != nullptr;
1356 
1357  if(!is_existing_upload) {
1358  // Create a new add-ons list entry and work with that from now on
1359  auto entry = addons_.emplace(name, config("original_timestamp", upload_ts));
1360  addon_ptr = &(*entry.first).second;
1361  }
1362 
1363  config& addon = *addon_ptr;
1364 
1365  LOG_CS << req << "Upload type: "
1366  << (is_delta_upload ? "delta" : "full") << ", "
1367  << (is_existing_upload ? "update" : "new") << '\n';
1368 
1369  // Write general metadata attributes
1370 
1371  addon.copy_attributes(upload,
1372  "title", "name", "author", "description", "version", "icon",
1373  "translate", "dependencies", "type", "tags", "email");
1374 
1375  const std::string& pathstem = "data/" + name;
1376  addon["filename"] = pathstem;
1377  addon["upload_ip"] = req.addr;
1378 
1379  if(!is_existing_upload) {
1380  set_passphrase(addon, upload["passphrase"]);
1381  }
1382 
1383  if(addon["downloads"].empty()) {
1384  addon["downloads"] = 0;
1385  }
1386 
1387  addon["timestamp"] = upload_ts;
1388  addon["uploads"] = 1 + addon["uploads"].to_int();
1389 
1390  addon.clear_children("feedback");
1391  if(const config& url_params = upload.child("feedback")) {
1392  addon.add_child("feedback", url_params);
1393  }
1394 
1395  // Copy in any metadata translations provided directly in the .pbl.
1396  // Catalogue detection is done later -- in the meantime we just mark
1397  // translations with valid metadata as not supported until we find out
1398  // whether the add-on ships translation catalogues for them or not.
1399 
1400  addon.clear_children("translation");
1401 
1402  for(const config& locale_params : upload.child_range("translation")) {
1403  if(!locale_params["language"].empty()) {
1404  config& locale = addon.add_child("translation");
1405  locale["language"] = locale_params["language"].str();
1406  locale["supported"] = false;
1407 
1408  if(!locale_params["title"].empty()) {
1409  locale["title"] = locale_params["title"].str();
1410  }
1411  if(!locale_params["description"].empty()) {
1412  locale["description"] = locale_params["description"].str();
1413  }
1414  }
1415  }
1416 
1417  // We need to alter the WML pack slightly, but we don't want to do a deep
1418  // copy of data that's larger than 5 MB in the average case (and as large
1419  // as 100 MB in the worst case). On the other hand, if the upload is a
1420  // delta then need to leave this empty and fill it in later instead.
1421 
1422  config rw_full_pack;
1423  if(have_wml(full_pack)) {
1424  // Void the warranty
1425  rw_full_pack = std::move(const_cast<config&>(*full_pack));
1426  }
1427 
1428  // Versioning support
1429 
1430  const auto& new_version = addon["version"].str();
1431  auto version_map = get_version_map(addon);
1432 
1433  if(is_delta_upload) {
1434  // Create the full pack by grabbing the one for the requested 'from'
1435  // version (or latest available) and applying the delta on it. We
1436  // proceed from there by fill in rw_full_pack with the result.
1437 
1438  if(version_map.empty()) {
1439  // This should NEVER happen
1440  ERR_CS << "Add-on '" << name << "' has an empty version table, this should not happen\n";
1441  send_error("Server error: Cannot process update pack with an empty version table.", "", static_cast<unsigned int>(ADDON_CHECK_STATUS::SERVER_DELTA_NO_VERSIONS), req.sock);
1442  return;
1443  }
1444 
1445  auto prev_version = upload["from"].str();
1446 
1447  if(prev_version.empty()) {
1448  prev_version = version_map.rbegin()->first;
1449  } else {
1450  // If the requested 'from' version doesn't exist, select the newest
1451  // older version available.
1452  version_info prev_version_parsed{prev_version};
1453  auto vm_entry = version_map.find(prev_version_parsed);
1454  if(vm_entry == version_map.end()) {
1455  prev_version = (--version_map.upper_bound(prev_version_parsed))->first;
1456  }
1457  }
1458 
1459  // Remove any existing update packs targeting the new version. This is
1460  // really only needed if the server allows multiple uploads of an
1461  // add-on with the same version number.
1462 
1463  std::set<std::string> delete_packs;
1464  for(const auto& pack : addon.child_range("update_pack")) {
1465  if(pack["to"].str() == new_version) {
1466  const auto& pack_filename = pack["filename"].str();
1467  filesystem::delete_file(pathstem + '/' + pack_filename);
1468  delete_packs.insert(pack_filename);
1469  }
1470  }
1471 
1472  if(!delete_packs.empty()) {
1473  addon.remove_children("update_pack", [&delete_packs](const config& p) {
1474  return delete_packs.find(p["filename"].str()) != delete_packs.end();
1475  });
1476  }
1477 
1478  const auto& update_pack_fn = make_update_pack_filename(prev_version, new_version);
1479 
1480  config& pack_info = addon.add_child("update_pack");
1481 
1482  pack_info["from"] = prev_version;
1483  pack_info["to"] = new_version;
1484  pack_info["expire"] = upload_ts + update_pack_lifespan_;
1485  pack_info["filename"] = update_pack_fn;
1486 
1487  // Write the update pack to disk
1488 
1489  {
1490  LOG_CS << "Saving provided update pack for " << prev_version << " -> " << new_version << "...\n";
1491 
1492  filesystem::atomic_commit pack_file{pathstem + '/' + update_pack_fn};
1493  config_writer writer{*pack_file.ostream(), true, compress_level_};
1494  static const config empty_config;
1495 
1496  writer.open_child("removelist");
1497  writer.write(have_wml(delta_remove) ? *delta_remove : empty_config);
1498  writer.close_child("removelist");
1499 
1500  writer.open_child("addlist");
1501  writer.write(have_wml(delta_add) ? *delta_add : empty_config);
1502  writer.close_child("addlist");
1503 
1504  pack_file.commit();
1505  }
1506 
1507  // Apply it to the addon data from the previous version to generate a
1508  // new full pack, which will be written later near the end of this
1509  // request servicing routine.
1510 
1511  version_info prev_version_parsed{prev_version};
1512  auto it = version_map.find(prev_version_parsed);
1513  if(it == version_map.end()) {
1514  // This REALLY should never happen
1515  ERR_CS << "Previous version dropped off the version map?\n";
1516  send_error("Server error: Previous version disappeared.", "", static_cast<unsigned int>(ADDON_CHECK_STATUS::SERVER_UNSPECIFIED), req.sock);
1517  return;
1518  }
1519 
1520  auto in = filesystem::istream_file(pathstem + '/' + it->second["filename"].str());
1521  rw_full_pack.clear();
1522  read_gz(rw_full_pack, *in);
1523 
1524  if(have_wml(delta_remove)) {
1525  data_apply_removelist(rw_full_pack, *delta_remove);
1526  }
1527 
1528  if(have_wml(delta_add)) {
1529  data_apply_addlist(rw_full_pack, *delta_add);
1530  }
1531  }
1532 
1533  // Detect translation catalogues and toggle their supported status accordingly
1534 
1535  find_translations(rw_full_pack, addon);
1536 
1537  // Add default license information if needed
1538 
1539  add_license(rw_full_pack);
1540 
1541  // Update version map, first removing any identical existing versions
1542 
1543  version_info new_version_parsed{new_version};
1544  config version_cfg{"version", new_version};
1545  version_cfg["filename"] = make_full_pack_filename(new_version);
1546 
1547  version_map.erase(new_version_parsed);
1548  addon.remove_children("version", [&new_version](const config& old_cfg)
1549  {
1550  return old_cfg["version"].str() == new_version;
1551  }
1552  );
1553 
1554  version_map.emplace(new_version_parsed, version_cfg);
1555  addon.add_child("version", version_cfg);
1556 
1557  // Clean-up
1558 
1559  rw_full_pack["name"] = ""; // [dir] syntax expects this to be present and empty
1560 
1561  // Write the full pack and its index file
1562 
1563  const auto& full_pack_path = pathstem + '/' + version_cfg["filename"].str();
1564  const auto& index_path = pathstem + '/' + make_index_filename(new_version);
1565 
1566  {
1567  config pack_index{"name", ""}; // [dir] syntax expects this to be present and empty
1568  write_hashlist(pack_index, rw_full_pack);
1569 
1570  filesystem::atomic_commit addon_pack_file{full_pack_path};
1571  config_writer{*addon_pack_file.ostream(), true, compress_level_}.write(rw_full_pack);
1572  addon_pack_file.commit();
1573 
1574  filesystem::atomic_commit addon_index_file{index_path};
1575  config_writer{*addon_index_file.ostream(), true, compress_level_}.write(pack_index);
1576  addon_index_file.commit();
1577  }
1578 
1579  addon["size"] = filesystem::file_size(full_pack_path);
1580 
1581  // Expire old update packs and delete them
1582 
1583  std::set<std::string> expire_packs;
1584 
1585  for(const config& pack : addon.child_range("update_pack")) {
1586  if(upload_ts > pack["expire"].to_time_t() || pack["from"].str() == new_version || (!is_delta_upload && pack["to"].str() == new_version)) {
1587  LOG_CS << "Expiring upate pack for " << pack["from"].str() << " -> " << pack["to"].str() << "\n";
1588  const auto& pack_filename = pack["filename"].str();
1589  filesystem::delete_file(pathstem + '/' + pack_filename);
1590  expire_packs.insert(pack_filename);
1591  }
1592  }
1593 
1594  if(!expire_packs.empty()) {
1595  addon.remove_children("update_pack", [&expire_packs](const config& p) {
1596  return expire_packs.find(p["filename"].str()) != expire_packs.end();
1597  });
1598  }
1599 
1600  // Create any missing update packs between consecutive versions. This covers
1601  // cases where clients were not able to upload those update packs themselves.
1602 
1603  for(auto iter = version_map.begin(); std::distance(iter, version_map.end()) > 1;) {
1604  const config& prev_version = iter->second;
1605  const config& next_version = (++iter)->second;
1606 
1607  const auto& prev_version_name = prev_version["version"].str();
1608  const auto& next_version_name = next_version["version"].str();
1609 
1610  bool found = false;
1611 
1612  for(const auto& pack : addon.child_range("update_pack")) {
1613  if(pack["from"].str() == prev_version_name && pack["to"].str() == next_version_name) {
1614  found = true;
1615  break;
1616  }
1617  }
1618 
1619  if(found) {
1620  // Nothing to do
1621  continue;
1622  }
1623 
1624  LOG_CS << "Automatically generating update pack for " << prev_version_name << " -> " << next_version_name << "...\n";
1625 
1626  const auto& prev_path = pathstem + '/' + prev_version["filename"].str();
1627  const auto& next_path = pathstem + '/' + next_version["filename"].str();
1628 
1629  if(filesystem::file_size(prev_path) <= 0 || filesystem::file_size(next_path) <= 0) {
1630  ERR_CS << "Unable to automatically generate an update pack for '" << name
1631  << "' for version " << prev_version_name << " to " << next_version_name
1632  << "!\n";
1633  continue;
1634  }
1635 
1636  const auto& update_pack_fn = make_update_pack_filename(prev_version_name, next_version_name);
1637 
1638  config& pack_info = addon.add_child("update_pack");
1639  pack_info["from"] = prev_version_name;
1640  pack_info["to"] = next_version_name;
1641  pack_info["expire"] = upload_ts + update_pack_lifespan_;
1642  pack_info["filename"] = update_pack_fn;
1643 
1644  // Generate the update pack from both full packs
1645 
1646  config pack, from, to;
1647 
1649  read_gz(from, *in);
1650  in = filesystem::istream_file(next_path);
1651  read_gz(to, *in);
1652 
1653  make_updatepack(pack, from, to);
1654 
1655  {
1656  filesystem::atomic_commit pack_file{pathstem + '/' + update_pack_fn};
1657  config_writer{*pack_file.ostream(), true, compress_level_}.write(pack);
1658  pack_file.commit();
1659  }
1660  }
1661 
1662  mark_dirty(name);
1663  write_config();
1664 
1665  LOG_CS << req << "Finished uploading add-on '" << upload["name"] << "'\n";
1666 
1667  send_message("Add-on accepted.", req.sock);
1668 
1669  fire("hook_post_upload", name);
1670 }
1671 
1673 {
1674  const config& erase = req.cfg;
1675  const std::string& id = erase["name"].str();
1676 
1677  if(read_only_) {
1678  LOG_CS << req << "in read-only mode, request to delete '" << id << "' denied\n";
1679  send_error("Cannot delete add-on: The server is currently in read-only mode.", req.sock);
1680  return;
1681  }
1682 
1683  LOG_CS << req << "Deleting add-on '" << id << "'\n";
1684 
1685  config& addon = get_addon(id);
1686 
1687  if(!addon) {
1688  send_error("The add-on does not exist.", req.sock);
1689  return;
1690  }
1691 
1692  const config::attribute_value& pass = erase["passphrase"];
1693 
1694  if(pass.empty()) {
1695  send_error("No passphrase was specified.", req.sock);
1696  return;
1697  }
1698 
1699  if(!authenticate(addon, pass)) {
1700  send_error("The passphrase is incorrect.", req.sock);
1701  return;
1702  }
1703 
1704  if(addon["hidden"].to_bool()) {
1705  LOG_CS << "Add-on removal denied - hidden add-on.\n";
1706  send_error("Add-on deletion denied. Please contact the server administration for assistance.", req.sock);
1707  return;
1708  }
1709 
1710  delete_addon(id);
1711 
1712  send_message("Add-on deleted.", req.sock);
1713 }
1714 
1716 {
1717  const config& cpass = req.cfg;
1718 
1719  if(read_only_) {
1720  LOG_CS << "in read-only mode, request to change passphrase denied\n";
1721  send_error("Cannot change passphrase: The server is currently in read-only mode.", req.sock);
1722  return;
1723  }
1724 
1725  config& addon = get_addon(cpass["name"]);
1726 
1727  if(!addon) {
1728  send_error("No add-on with that name exists.", req.sock);
1729  } else if(!authenticate(addon, cpass["passphrase"])) {
1730  send_error("Your old passphrase was incorrect.", req.sock);
1731  } else if(addon["hidden"].to_bool()) {
1732  LOG_CS << "Passphrase change denied - hidden add-on.\n";
1733  send_error("Add-on passphrase change denied. Please contact the server administration for assistance.", req.sock);
1734  } else if(cpass["new_passphrase"].empty()) {
1735  send_error("No new passphrase was supplied.", req.sock);
1736  } else {
1737  set_passphrase(addon, cpass["new_passphrase"]);
1738  dirty_addons_.emplace(addon["name"]);
1739  write_config();
1740  send_message("Passphrase changed.", req.sock);
1741  }
1742 }
1743 
1744 } // end namespace campaignd
1745 
1746 int run_campaignd(int argc, char** argv)
1747 {
1748  campaignd::command_line cmdline{argc, argv};
1749  std::string server_path = filesystem::get_cwd();
1750  std::string config_file = "server.cfg";
1751  unsigned short port = 0;
1752 
1753  //
1754  // Log defaults
1755  //
1756 
1757  for(auto domain : { "campaignd", "campaignd/blacklist", "server" }) {
1759  }
1760 
1761  lg::timestamps(true);
1762 
1763  //
1764  // Process command line
1765  //
1766 
1767  if(cmdline.help) {
1768  std::cout << cmdline.help_text();
1769  return 0;
1770  }
1771 
1772  if(cmdline.version) {
1773  std::cout << "Wesnoth campaignd v" << game_config::revision << '\n';
1774  return 0;
1775  }
1776 
1777  if(cmdline.config_file) {
1778  // Don't fully resolve the path, so that filesystem::ostream_file() can
1779  // create path components as needed (dumb legacy behavior).
1780  config_file = filesystem::normalize_path(*cmdline.config_file, true, false);
1781  }
1782 
1783  if(cmdline.server_dir) {
1784  server_path = filesystem::normalize_path(*cmdline.server_dir, true, true);
1785  }
1786 
1787  if(cmdline.port) {
1788  port = *cmdline.port;
1789  // We use 0 as a placeholder for the default port for this version
1790  // otherwise, hence this check must only exists in this code path. It's
1791  // only meant to protect against user mistakes.
1792  if(!port) {
1793  std::cerr << "Invalid network port: " << port << '\n';
1794  return 2;
1795  }
1796  }
1797 
1798  if(cmdline.show_log_domains) {
1799  std::cout << lg::list_logdomains("");
1800  return 0;
1801  }
1802 
1803  for(const auto& ldl : cmdline.log_domain_levels) {
1804  if(!lg::set_log_domain_severity(ldl.first, ldl.second)) {
1805  std::cerr << "Unknown log domain: " << ldl.first << '\n';
1806  return 2;
1807  }
1808  }
1809 
1810  if(cmdline.log_precise_timestamps) {
1811  lg::precise_timestamps(true);
1812  }
1813 
1814  if(cmdline.report_timings) {
1815  campaignd::timing_reports_enabled = true;
1816  }
1817 
1818  std::cerr << "Wesnoth campaignd v" << game_config::revision << " starting...\n";
1819 
1820  if(server_path.empty() || !filesystem::is_directory(server_path)) {
1821  std::cerr << "Server directory '" << *cmdline.server_dir << "' does not exist or is not a directory.\n";
1822  return 1;
1823  }
1824 
1825  if(filesystem::is_directory(config_file)) {
1826  std::cerr << "Server configuration file '" << config_file << "' is not a file.\n";
1827  return 1;
1828  }
1829 
1830  // Everything does file I/O with pwd as the implicit starting point, so we
1831  // need to change it accordingly. We don't do this before because paths in
1832  // the command line need to remain relative to the original pwd.
1833  if(cmdline.server_dir && !filesystem::set_cwd(server_path)) {
1834  std::cerr << "Bad server directory '" << server_path << "'.\n";
1835  return 1;
1836  }
1837 
1838  game_config::path = server_path;
1839 
1840  //
1841  // Run the server
1842  //
1843  campaignd::server(config_file, port).run();
1844 
1845  return 0;
1846 }
1847 
1848 int main(int argc, char** argv)
1849 {
1850  try {
1851  run_campaignd(argc, argv);
1852  } catch(const boost::program_options::error& e) {
1853  std::cerr << "Error in command line: " << e.what() << '\n';
1854  return 10;
1855  } catch(const config::error& /*e*/) {
1856  std::cerr << "Could not parse config file\n";
1857  return 1;
1858  } catch(const filesystem::io_exception& e) {
1859  std::cerr << "File I/O error: " << e.what() << "\n";
1860  return 2;
1861  } catch(const std::bad_function_call& /*e*/) {
1862  std::cerr << "Bad request handler function call\n";
1863  return 4;
1864  }
1865 
1866  return 0;
1867 }
time_t update_pack_lifespan_
Definition: server.hpp:114
node & add_child(const char *name)
Definition: simple_wml.cpp:464
campaignd authentication API.
bool empty() const
Tests for an attribute that either was never set or was set to "".
bool check_names_legal(const config &dir, std::vector< std::string > *badlist)
Scans an add-on archive for illegal names.
Definition: validation.cpp:227
bool delete_directory(const std::string &dirname, const bool keep_pbl)
Definition: filesystem.cpp:947
std::string feedback_url_format_
Definition: server.hpp:124
bool strict_versions_
Definition: server.hpp:116
bool ignore_address_stats(const std::string &addr) const
Checks if the specified address should never bump download counts.
Definition: server.cpp:749
config & child(config_key_type key, int n=0)
Returns the nth child with the given key, or a reference to an invalid config if there is none...
Definition: config.cpp:420
bool check_case_insensitive_duplicates(const config &dir, std::vector< std::string > *badlist)
Scans an add-on archive for case-conflicts.
Definition: validation.cpp:236
std::unique_ptr< simple_wml::document > coro_receive_doc(socket_ptr socket, boost::asio::yield_context yield)
Receive WML document from a coroutine.
Legacy add-ons server.
Definition: server.hpp:37
bool check_error(const boost::system::error_code &error, socket_ptr socket)
No version specified.
void handle_request_terms(const request &)
Definition: server.cpp:1151
void clear_children(T... keys)
Definition: config.hpp:490
Invalid UTF-8 sequence in add-on name.
static const std::size_t default_document_size_limit
Default upload size limit in bytes.
Definition: server.hpp:119
Interfaces for manipulating version numbers of engine, add-ons, etc.
void append(const config &cfg)
Append data from another config object to this one.
Definition: config.cpp:287
bool delete_file(const std::string &filename)
Definition: filesystem.cpp:986
void coro_send_file(socket_ptr socket, const std::string &filename, boost::asio::yield_context yield)
Send contents of entire file directly to socket from within a coroutine.
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)
Definition: server.cpp:655
#define ERR_CS
Definition: server.cpp:59
Variant for storing WML attributes.
config & get_addon(const std::string &id)
Retrieves an addon by id if found, or a null config otherwise.
Definition: server.cpp:799
std::unordered_set< std::string > dirty_addons_
The set of unique addon names with pending metadata updates.
Definition: server.hpp:106
boost::asio::signal_set sighup_
static l_noret error(LoadState *S, const char *why)
Definition: lundump.cpp:39
New lexcical_cast header.
No versions to deltify against.
bool has_attribute(config_key_type key) const
Definition: config.cpp:213
logger & info()
Definition: log.cpp:91
void handle_delete(const request &)
Definition: server.cpp:1672
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:412
unsigned child_count(config_key_type key) const
Definition: config.cpp:390
Client request information object.
Definition: server.hpp:54
std::map< version_info, config > get_version_map(config &addon)
child_itors child_range(config_key_type key)
Definition: config.cpp:362
void load_config()
Reads the server configuration from WML.
Definition: server.cpp:329
void remove_attributes(T... keys)
Definition: config.hpp:455
void timestamps(bool t)
Definition: log.cpp:76
Reports time elapsed at the end of an object scope.
Definition: optimer.hpp:34
filesystem::scoped_istream istream_file(const std::string &fname, bool treat_failure_as_error)
bool data_apply_removelist(config &data, const config &removelist)
Delta for a non-existent add-on.
bool wildcard_string_match(const std::string &str, const std::string &match)
Match using &#39;*&#39; as any number of characters (including none), &#39;+&#39; as one or more characters, and &#39;?&#39; as any one character.
void fire(const std::string &hook, const std::string &addon)
Fires a hook script.
Definition: server.cpp:708
void handle_request_campaign_hash(const request &)
Definition: server.cpp:1105
#define LOG_CS
Definition: server.cpp:57
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
Definition: debugger.cpp:109
request_handlers_table handlers_
Definition: server.hpp:122
void clear()
Definition: config.cpp:863
std::string normalize_path(const std::string &fpath, bool normalize_separators, bool resolve_dot_entries)
Returns the absolute path of a file.
void write_hashlist(config &hashlist, const config &data)
Definition: validation.cpp:332
void read_gz(config &cfg, std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially a gzip_error.
Definition: parser.cpp:681
std::string addon_check_status_desc(unsigned int code)
Definition: validation.cpp:438
Wrapper class that guarantees that file commit atomicity.
Definition: fs_commit.hpp:51
std::map< std::string, std::string > hooks_
Definition: server.hpp:121
void load_blacklist()
Reads the add-ons upload blacklist from WML.
Definition: server.cpp:665
void data_apply_addlist(config &data, const config &addlist)
static lg::log_domain log_config("config")
static int pack(lua_State *L)
Definition: ltablib.cpp:194
void register_handlers()
Registers client request handlers.
Definition: server.cpp:841
unsigned short port_
Definition: server_base.hpp:93
No description specified.
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_
Definition: server.hpp:110
Invalid UTF-8 sequence in add-on metadata.
std::string get_cwd()
Definition: filesystem.cpp:878
void start_server()
Definition: server_base.cpp:70
unsigned in
If equal to search_counter, the node is off the list.
const_all_children_iterator ordered_end() const
Definition: config.cpp:911
void write(std::ostream &out, const configr_of &cfg, unsigned int level)
Definition: parser.cpp:762
No email specified.
std::pair< std::string, std::string > generate_hash(const std::string &passphrase)
Generates a salted hash from the specified passphrase.
Definition: auth.cpp:54
void handle_sighup(const boost::system::error_code &error, int signal_number)
Definition: server.cpp:636
boost::asio::basic_waitable_timer< std::chrono::steady_clock > flush_timer_
Definition: server.hpp:134
boost::asio::streambuf admin_cmd_
int main(int argc, char **argv)
Definition: server.cpp:1848
Authentication failed.
std::string blacklist_file_
Definition: server.hpp:130
std::ostringstream wrapper.
Definition: formatter.hpp:38
void read(config &cfg, std::istream &in, abstract_validator *validator)
Definition: parser.cpp:625
Class for writing a config out to a file in pieces.
void get_files_in_dir(const std::string &dir, std::vector< std::string > *files, std::vector< std::string > *dirs, name_mode mode, filter_mode filter, reorder_mode reorder, file_tree_checksum *checksum)
Populates &#39;files&#39; with all the files and &#39;dirs&#39; with all the directories in dir.
Definition: filesystem.cpp:349
void commit()
Commits the new file contents to disk atomically.
Definition: fs_commit.cpp:205
const child_map::key_type & key
Definition: config.hpp:532
#define DBG_CS
Definition: server.cpp:56
boost::asio::io_service io_service_
Definition: server_base.hpp:95
void erase(const std::string &key)
Definition: general.cpp:219
bool is_directory(const std::string &fname)
Returns true if the given file is a directory.
std::unique_ptr< std::istream > scoped_istream
Definition: filesystem.hpp:36
void handle_read_from_fifo(const boost::system::error_code &error, std::size_t bytes_transferred)
Definition: server.cpp:511
const std::string & cmd
Definition: server.hpp:56
void delete_addon(const std::string &id)
Definition: server.cpp:809
Server read-only mode on.
std::string path
Definition: game_config.cpp:39
scoped_ostream & ostream()
Returns the write stream associated with the file.
Definition: fs_commit.hpp:72
blacklist blacklist_
Definition: server.hpp:129
void handle_change_passphrase(const request &)
Definition: server.cpp:1715
const char * what() const noexcept
Definition: exceptions.hpp:37
int run_campaignd(int argc, char **argv)
Definition: server.cpp:1746
#define REGISTER_CAMPAIGND_HANDLER(req_id)
Definition: server.cpp:837
ADDON_TYPE get_addon_type(const std::string &str)
Definition: validation.cpp:240
static std::size_t document_size_limit
Definition: simple_wml.hpp:290
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
const std::string addr
Definition: server.hpp:60
#define UNUSED(x)
Definition: global.hpp:40
bool addon_name_legal(const std::string &name)
Checks whether an add-on id/name is legal or not.
Definition: validation.cpp:95
boost::asio::yield_context yield
context of the coroutine the request is executed in async operations on sock can use it instead of a ...
Definition: server.hpp:66
std::size_t args_count() const
Returns the total number of arguments, not including the command itself.
Definition: control.hpp:74
Unspecified server error.
static config & get_invalid()
Definition: config.hpp:111
void write_config()
Writes the server configuration WML back to disk.
Definition: server.cpp:688
std::string web_url_
Definition: server.hpp:126
#define WRN_CS
Definition: server.cpp:58
std::size_t i
Definition: function.cpp:933
void handle_new_client(socket_ptr socket)
Definition: server.cpp:476
Thrown by operations encountering invalid UTF-8 data.
virtual std::string hex_digest() const override
Definition: hash.cpp:116
const config & server_info() const
Retrieves the contents of the [server_info] WML node.
Definition: server.hpp:200
const std::string revision
node & set_attr_dup(const char *key, const char *value)
Definition: simple_wml.cpp:427
Atomic filesystem commit functions.
Represents a server control line written to a communication socket.
Definition: control.hpp:32
mock_party p
static lg::log_domain log_campaignd("campaignd")
void handle_request_campaign(const request &)
Definition: server.cpp:973
An exception object used when an IO error occurs.
Definition: filesystem.hpp:45
const socket_ptr sock
Definition: server.hpp:59
std::vector< std::string > names
Definition: build_info.cpp:63
Markup in add-on title.
void copy_attributes(const config &from, T... keys)
Definition: config.hpp:462
std::string name
Definition: sdl_ttf.cpp:70
void async_send_doc_queued(socket_ptr socket, simple_wml::document &doc)
High level wrapper for sending a WML document.
std::string client_address(const socket_ptr socket)
void handle_server_id(const request &)
Definition: server.cpp:853
bool string_bool(const std::string &str, bool def)
Convert no, false, off, 0, 0.0 to false, empty to def, and others to true.
Declarations for File-IO.
static int writer(lua_State *L, const void *b, size_t size, void *B)
Definition: lstrlib.cpp:182
void read_from_fifo()
std::size_t index(const std::string &str, const std::size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:71
std::string lowercase(const std::string &s)
Returns a lowercased version of the string.
Definition: unicode.cpp:51
boost::asio::posix::stream_descriptor input_
No title specified.
bool set_log_domain_severity(const std::string &name, int severity)
Definition: log.cpp:119
Represents version numbers.
config & add_child(config_key_type key)
Definition: config.cpp:476
std::vector< std::string > stats_exempt_ips_
Definition: server.hpp:132
friend std::ostream & operator<<(std::ostream &o, const request &r)
Definition: server.cpp:470
int file_size(const std::string &fname)
Returns the size of a file, or -1 if the file doesn&#39;t exist.
Corrupted server add-ons list.
const_all_children_iterator ordered_begin() const
Definition: config.cpp:901
bool set_cwd(const std::string &dir)
Definition: filesystem.cpp:891
#define next(ls)
Definition: llex.cpp:32
No passphrase specified.
config & cfg
Definition: config.hpp:533
void handle_request_campaign_list(const request &)
Definition: server.cpp:872
void make_updatepack(config &pack, const config &from, const config &to)
&from, &to are the top directories of their structures; addlist/removelist tag is treated as [dir] ...
Definition: validation.cpp:430
void coro_send_doc(socket_ptr socket, simple_wml::document &doc, boost::asio::yield_context yield)
Send a WML document from within a coroutine.
ADDON_CHECK_STATUS validate_addon(const server::request &req, config *&existing_addon, std::string &error_data)
Performs validation on an incoming add-on.
Definition: server.cpp:1165
std::vector< std::string > split(const config_attribute_value &val)
const unsigned short default_campaignd_port
Default port number for the addon server.
Definition: validation.cpp:27
std::string fifo_path_
bool verify_passphrase(const std::string &passphrase, const std::string &salt, const std::string &hash)
Verifies the specified plain text passphrase against a salted hash.
Definition: auth.cpp:49
const config & cfg
Definition: server.hpp:57
std::string list_logdomains(const std::string &filter)
Definition: log.cpp:152
const std::string & cmd() const
Returns the control command.
Definition: control.hpp:66
void flush_cfg()
Starts timer to write config to disk every ten minutes.
Definition: server.cpp:649
campaignd command line options parsing.
Standard logging facilities (interface).
std::string str() const
Serializes the version number into string form.
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:43
std::string full() const
Return the full command line string.
Definition: control.hpp:92
bool is_text_markup_char(char c)
Definition: addon_utils.hpp:33
config cfg_
Server config.
Definition: server.hpp:109
void find_translations(const config &base_dir, config &addon)
Scans an add-on archive directory for translations.
#define e
const config & child_or_empty(config_key_type key) const
Returns the first child with the given key, or an empty config if there is none.
Definition: config.cpp:453
std::unordered_map< std::string, config > addons_
The hash map of addons metadata.
Definition: server.hpp:104
No author specified.
std::set< std::string > capabilities_
Definition: server.hpp:101
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:60
void send_message(const std::string &msg, socket_ptr sock)
Send a client an informational message.
Definition: server.cpp:761
server(const std::string &cfg_file, unsigned short port=0)
Definition: server.cpp:277
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:64
void send_error(const std::string &msg, socket_ptr sock)
Send a client an error message.
Definition: server.cpp:769
std::map< std::string, addon_info > addons_list
Definition: info.hpp:27
static lg::log_domain log_server("server")
void handle_upload(const request &)
Definition: server.cpp:1329
bool empty() const
Definition: config.cpp:884
void mark_dirty(const std::string &addon)
Definition: server.hpp:175
void precise_timestamps(bool pt)
Definition: log.cpp:77
void remove_children(config_key_type key, std::function< bool(const config &)> p)
Removes all children with tag key for which p returns true.
Definition: config.cpp:707
ADDON_CHECK_STATUS
Definition: validation.hpp:31
std::string license_notice_
Definition: server.hpp:127
Version number is not an increment.
int compress_level_
Used for add-on archives.
Definition: server.hpp:113