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