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