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