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