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