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