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