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