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