58 #if !(defined(_WIN32))
63 #define DBG_CS LOG_STREAM(debug, log_campaignd)
64 #define LOG_CS LOG_STREAM(info, log_campaignd)
65 #define WRN_CS LOG_STREAM(warn, log_campaignd)
66 #define ERR_CS LOG_STREAM(err, log_campaignd)
69 #define ERR_CONFIG LOG_STREAM(err, log_config)
70 #define WRN_CONFIG LOG_STREAM(warn, log_config)
73 #define ERR_SERVER LOG_STREAM(err, log_server)
85 const std::set<std::string> cap_defaults = {
95 const std::string default_web_url =
"https://add-ons.wesnoth.org/";
106 const std::string default_license_notice = R
"(<span size='x-large'>General Rules</span>
108 The current version of the server rules can be found at: https://r.wesnoth.org/t51347
110 <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>
112 <span size='x-large'>Licensing</span>
114 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:
116 a) a combined toplevel file, e.g. “<span font_family='monospace'>My_Addon/ART_LICENSE</span>”; <b>or</b>
117 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>”.
119 <b>By uploading content to this server, you certify that you have the right to:</b>
121 a) release all included art and audio explicitly denoted with a Creative Commons license in the prescribed manner under that license; <b>and</b>
122 b) release all other included content under the terms of the chosen versions of the GNU GPL.)";
124 bool timing_reports_enabled =
false;
128 if(timing_reports_enabled) {
130 LOG_CS << req <<
"Time elapsed: " << tim <<
" ms";
132 LOG_CS << req <<
"Time elapsed [" <<
label <<
"]: " << tim <<
" ms";
156 if(!addon[
"forum_auth"].to_bool()) {
169 inline void set_passphrase(
config& addon,
const std::string& passphrase)
172 if(!addon[
"forum_auth"].to_bool()) {
182 inline std::string make_update_pack_filename(
const std::string& old_version,
const std::string& new_version)
192 inline std::string make_full_pack_filename(
const std::string& version)
202 inline std::string make_index_filename(
const std::string& version)
212 inline std::string index_from_full_pack_filename(std::string pack_fn)
214 auto dot_pos = pack_fn.find_last_of(
'.');
215 if(dot_pos != std::string::npos) {
216 pack_fn.replace(dot_pos, std::string::npos,
".hash.gz");
226 return cfg && !cfg->empty();
234 template<
typename... Vals>
235 utils::optional<std::vector<std::string>> multi_find_illegal_names(
const Vals&... args)
237 std::vector<std::string>
names;
240 return !
names.empty() ? utils::optional(
names) : utils::nullopt;
248 template<
typename... Vals>
249 utils::optional<std::vector<std::string>> multi_find_case_conflicts(
const Vals&... args)
251 std::vector<std::string>
names;
254 return !
names.empty() ? utils::optional(
names) : utils::nullopt;
262 std::string simple_wml_escape(
const std::string& text)
265 auto it = text.begin();
267 while(it != text.end()) {
268 res.append(*it ==
'"' ? 2 : 1, *it);
279 , user_handler_(nullptr)
280 , capabilities_(cap_defaults)
284 , cfg_file_(cfg_file)
287 , update_pack_lifespan_(0)
288 , strict_versions_(true)
292 , feedback_url_format_()
297 , stats_exempt_ips_()
298 , flush_timer_(io_service_)
303 std::memset( &sa, 0,
sizeof(sa) );
304 #pragma GCC diagnostic ignored "-Wold-style-cast"
305 sa.sa_handler = SIG_IGN;
306 int res = sigaction( SIGPIPE, &sa,
nullptr);
341 LOG_CS <<
"READ-ONLY MODE ACTIVE";
349 constexpr std::chrono::seconds seconds_in_a_month{30 * 24 * 60 * 60};
356 web_url_ = svinfo_cfg[
"web_url"].str(default_web_url);
357 license_notice_ = svinfo_cfg[
"license_notice"].str(default_license_notice);
365 hooks_.emplace(std::string(
"hook_post_upload"),
cfg_[
"hook_post_upload"]);
366 hooks_.emplace(std::string(
"hook_post_erase"),
cfg_[
"hook_post_erase"]);
370 if(!
cfg_[
"control_socket"].empty()) {
371 const std::string&
path =
cfg_[
"control_socket"].str();
374 const int res = mkfifo(
path.c_str(),0660);
375 if(res != 0 && errno != EEXIST) {
376 ERR_CS <<
"could not make fifo at '" <<
path <<
"' (" << strerror(errno) <<
")";
379 int fifo = open(
path.c_str(), O_RDWR|O_NONBLOCK);
381 LOG_CS <<
"opened fifo at '" <<
path <<
"'. Server commands may be written to this file.";
405 std::vector<std::string> legacy_addons, dirs;
408 for(
const std::string& addon_dir : dirs) {
412 addons_.emplace(meta[
"name"].str(), meta);
421 WRN_CS <<
"Old format addons have been detected in the config! They will be converted to the new file format! "
422 << campaigns.
child_count(
"campaign") <<
" entries to be processed.";
424 const std::string& addon_id = campaign[
"name"].str();
425 const std::string& addon_file = campaign[
"filename"].str();
428 +
"' already exists in the new form! Possible code or filesystem interference!\n");
430 if(
std::find(legacy_addons.begin(), legacy_addons.end(), addon_id) == legacy_addons.end()) {
432 +
"'. Check the file structure!\n");
441 config version_cfg =
config(
"version", campaign[
"version"].str());
442 version_cfg[
"filename"] = make_full_pack_filename(campaign[
"version"]);
443 campaign.add_child(
"version", version_cfg);
445 data.remove_attributes(
"title",
"campaign_name",
"author",
"description",
"version",
"timestamp",
"original_timestamp",
"icon",
"type",
"tags");
458 writer.
write(data_hash);
459 campaign_hash_file.
commit();
462 addons_.emplace(addon_id, campaign);
466 LOG_CS <<
"Legacy addons processing finished.";
470 LOG_CS <<
"Loaded addons metadata. " <<
addons_.size() <<
" addons found.";
475 ERR_CS <<
"The server id must be set when database support is used.";
479 LOG_CS <<
"User handler initialized.";
488 o << '[' << (utils::holds_alternative<tls_socket_ptr>(r.
sock) ?
"+" :
"") << r.
addr <<
' ' << r.
cmd <<
"] ";
496 #if BOOST_VERSION >= 108000
497 , [](
const std::exception_ptr&
e) {
if (
e) std::rethrow_exception(
e); }
506 #if BOOST_VERSION >= 108000
507 , [](
const std::exception_ptr&
e) {
if (
e) std::rethrow_exception(
e); }
512 template<
class Socket>
518 socket->lowest_layer().close();
527 if(
i !=
data.ordered_end()) {
531 request_handlers_table::const_iterator j
536 request req{
c.key,
c.cfg, socket, yield};
537 auto st = service_timer(req);
538 j->second(
this, req);
540 send_error(
"Unrecognized [" +
c.key +
"] request.",socket);
551 if(error == boost::asio::error::operation_aborted)
554 ERR_CS <<
"Error reading from fifo: " << error.message();
560 std::getline(is, cmd);
564 if(ctl ==
"shut_down") {
565 LOG_CS <<
"Shut down requested by admin, shutting down...";
567 }
else if(ctl ==
"readonly") {
573 }
else if(ctl ==
"flush") {
574 LOG_CS <<
"Flushing config to disk...";
576 }
else if(ctl ==
"reload") {
578 if(ctl[1] ==
"blacklist") {
579 LOG_CS <<
"Reloading blacklist...";
582 ERR_CS <<
"Unrecognized admin reload argument: " << ctl[1];
585 LOG_CS <<
"Reloading all configuration...";
587 LOG_CS <<
"Reloaded configuration";
589 }
else if(ctl ==
"delete") {
591 ERR_CS <<
"Incorrect number of arguments for 'delete'";
593 const std::string& addon_id = ctl[1];
595 LOG_CS <<
"deleting add-on '" << addon_id <<
"' requested from control FIFO";
598 }
else if(ctl ==
"hide" || ctl ==
"unhide") {
600 ERR_CS <<
"Incorrect number of arguments for '" << ctl.
cmd() <<
"'";
602 const std::string& addon_id = ctl[1];
606 ERR_CS <<
"Add-on '" << addon_id <<
"' not found, cannot " << ctl.
cmd();
608 addon[
"hidden"] = (ctl.
cmd() ==
"hide");
611 LOG_CS <<
"Add-on '" << addon_id <<
"' is now " << (ctl.
cmd() ==
"hide" ?
"hidden" :
"unhidden");
614 }
else if(ctl ==
"setpass") {
616 ERR_CS <<
"Incorrect number of arguments for 'setpass'";
618 const std::string& addon_id = ctl[1];
619 const std::string& newpass = ctl[2];
623 ERR_CS <<
"Add-on '" << addon_id <<
"' not found, cannot set passphrase";
624 }
else if(newpass.empty()) {
626 ERR_CS <<
"Add-on passphrases may not be empty!";
627 }
else if(addon[
"forum_auth"].to_bool()) {
628 ERR_CS <<
"Can't set passphrase for add-on using forum_auth.";
630 set_passphrase(*addon, newpass);
633 LOG_CS <<
"New passphrase set for '" << addon_id <<
"'";
636 }
else if(ctl ==
"setattr") {
638 ERR_CS <<
"Incorrect number of arguments for 'setattr'";
640 const std::string& addon_id = ctl[1];
641 const std::string& key = ctl[2];
653 ERR_CS <<
"Add-on '" << addon_id <<
"' not found, cannot set attribute";
654 }
else if(key ==
"name" || key ==
"version") {
655 ERR_CS <<
"setattr cannot be used to rename add-ons or change their version";
656 }
else if(key ==
"passhash"|| key ==
"passsalt") {
657 ERR_CS <<
"setattr cannot be used to set auth data -- use setpass instead";
658 }
else if(!addon->has_attribute(key)) {
665 ERR_CS <<
"Attribute '" << key <<
"' is not a recognized add-on attribute";
670 LOG_CS <<
"Set attribute on add-on '" << addon_id <<
"':\n"
671 << key <<
"=\"" << value <<
"\"";
674 }
else if(ctl ==
"log") {
675 static const std::map<std::string, lg::severity> log_levels = {
684 ERR_CS <<
"Incorrect number of arguments for 'log'";
685 }
else if(ctl[1] ==
"precise") {
688 LOG_CS <<
"Precise timestamps enabled";
689 }
else if(ctl[2] ==
"off") {
691 LOG_CS <<
"Precise timestamps disabled";
693 ERR_CS <<
"Invalid argument for 'log precise': " << ctl[2];
695 }
else if(log_levels.find(ctl[1]) == log_levels.end()) {
696 ERR_CS <<
"Invalid log level '" << ctl[1] <<
"'";
698 auto sev = log_levels.find(ctl[1])->second;
701 ERR_CS <<
"Unknown log domain '" << domain <<
"'";
703 LOG_CS <<
"Set log level for domain '" << domain <<
"' to " << ctl[1];
707 }
else if(ctl ==
"timings") {
709 ERR_CS <<
"Incorrect number of arguments for 'timings'";
710 }
else if(ctl[1] ==
"on") {
711 campaignd::timing_reports_enabled =
true;
712 LOG_CS <<
"Request servicing timing reports enabled";
713 }
else if(ctl[1] ==
"off") {
714 campaignd::timing_reports_enabled =
false;
715 LOG_CS <<
"Request servicing timing reports disabled";
717 ERR_CS <<
"Invalid argument for 'timings': " << ctl[1];
720 ERR_CS <<
"Unrecognized admin command: " << ctl.
full();
728 LOG_CS <<
"SIGHUP caught, reloading config.";
732 LOG_CS <<
"Reloaded configuration";
748 ERR_CS <<
"Error from reload timer: " << error.message();
749 throw boost::system::system_error(error);
780 DBG_CS <<
"writing configuration and add-ons list to disk...";
787 if(addon && !addon[
"filename"].empty()) {
798 void server::fire(
const std::string& hook, [[maybe_unused]]
const std::string& addon)
800 const std::map<std::string, std::string>::const_iterator itor =
hooks_.find(hook);
801 if(itor ==
hooks_.end()) {
805 const std::string& script = itor->second;
811 ERR_CS <<
"Tried to execute a script on an unsupported platform";
816 if((childpid = fork()) == -1) {
817 ERR_CS <<
"fork failed while updating add-on " << addon;
825 execlp(script.c_str(), script.c_str(), addon.c_str(),
static_cast<char *
>(
nullptr));
828 PLAIN_LOG <<
"ERROR: exec failed with errno " << errno <<
" for addon " << addon;
851 const auto& escaped_msg = simple_wml_escape(
msg);
864 const auto& escaped_msg = simple_wml_escape(
msg);
872 const std::string& status_hex =
formatter()
873 <<
"0x" << std::setfill(
'0') << std::setw(2*
sizeof(
unsigned int)) << std::hex
874 << std::uppercase << status_code;
877 const auto& escaped_status_str = simple_wml_escape(std::to_string(status_code));
878 const auto& escaped_msg = simple_wml_escape(
msg);
879 const auto& escaped_extra_data = simple_wml_escape(extra_data);
885 err_cfg.
set_attr_dup(
"extra_data", escaped_extra_data.c_str());
886 err_cfg.
set_attr_dup(
"status_code", escaped_status_str.c_str());
895 return addon->second;
906 ERR_CS <<
"Cannot delete unrecognized add-on '" <<
id <<
"'";
910 if(cfg[
"forum_auth"].to_bool()) {
914 std::string fn = cfg[
"filename"].str();
917 ERR_CS <<
"Add-on '" <<
id <<
"' does not have an associated filename, cannot delete";
921 ERR_CS <<
"Could not delete the directory for addon '" <<
id
922 <<
"' (" << fn <<
"): " << strerror(errno);
928 fire(
"hook_post_erase",
id);
930 LOG_CS <<
"Deleted add-on '" <<
id <<
"'";
933 #define REGISTER_CAMPAIGND_HANDLER(req_id) \
934 handlers_[#req_id] = std::bind(&server::handle_##req_id, \
935 std::placeholders::_1, std::placeholders::_2)
951 DBG_CS << req <<
"Sending server identification";
953 std::ostringstream ostr;
962 const auto& wml = ostr.str();
971 LOG_CS << req <<
"Sending add-ons list";
973 auto now = std::chrono::system_clock::now();
974 const bool relative_to_now = req.
cfg[
"times_relative_to"] ==
"now";
979 bool before_flag = !req.
cfg[
"before"].
empty();
980 std::chrono::system_clock::time_point before;
982 if(relative_to_now) {
983 auto time_delta = chrono::parse_duration<std::chrono::seconds>(req.
cfg[
"before"]);
984 before = now + time_delta;
990 bool after_flag = !req.
cfg[
"after"].
empty();
991 std::chrono::system_clock::time_point after;
993 if(relative_to_now) {
994 auto time_delta = chrono::parse_duration<std::chrono::seconds>(req.
cfg[
"after"]);
995 after = now + time_delta;
1001 const std::string& name = req.
cfg[
"name"];
1002 const std::string& lang = req.
cfg[
"language"];
1004 for(
const auto& addon :
addons_)
1006 if(!name.empty() && name != addon.first) {
1012 if(
i[
"hidden"].to_bool()) {
1016 const auto& tm =
i[
"timestamp"];
1028 for(
const config& j :
i.child_range(
"translation"))
1030 if(j[
"language"] == lang && j[
"supported"].to_bool(
true)) {
1048 j.remove_attributes(
"passphrase",
"passhash",
"passsalt",
"upload_ip",
"email");
1051 if(!req.
cfg[
"send_icons"].to_bool(
true)) {
1052 j.remove_attribute(
"icon");
1063 j.clear_children(
"feedback");
1066 j.clear_children(
"update_pack");
1072 std::ostringstream ostr;
1073 write(ostr, response);
1074 std::string wml = ostr.str();
1085 if(!addon || addon[
"hidden"].to_bool()) {
1090 const auto& name = req.
cfg[
"name"].str();
1093 if(version_map.empty()) {
1094 send_error(
"No versions of the add-on '" + name +
"' are available on the server.", req.
sock);
1099 const auto& from = req.
cfg[
"from_version"].str();
1100 const auto& to = req.
cfg[
"version"].str(version_map.rbegin()->first);
1105 auto to_version_iter = version_map.find(to_parsed);
1106 if(to_version_iter == version_map.end()) {
1107 send_error(
"Could not find requested version " + to +
" of the addon '" + name +
1112 auto full_pack_path = addon[
"filename"].str() +
'/' + to_version_iter->second[
"filename"].str();
1121 if(!from.empty() && from_parsed < to_parsed && version_map.count(from_parsed) != 0) {
1130 int delivery_size = 0;
1131 bool force_use_full =
false;
1133 auto start_point = version_map.find(from_parsed);
1134 auto end_point = std::next(to_version_iter, 1);
1136 if(std::distance(start_point, end_point) <= 1) {
1138 ERR_CS <<
"Bad update sequence bounds in version " << from <<
" -> " << to <<
" update sequence for the add-on '" << name <<
"', sending a full pack instead";
1139 force_use_full =
true;
1142 for(
auto iter = start_point; !force_use_full && std::distance(iter, end_point) > 1;) {
1143 const auto& prev_version_cfg = iter->second;
1144 const auto& next_version_cfg = (++iter)->second;
1147 if(pack[
"from"].str() != prev_version_cfg[
"version"].str() ||
1148 pack[
"to"].str() != next_version_cfg[
"version"].str()) {
1153 const auto& update_pack_path = addon[
"filename"].str() +
'/' + pack[
"filename"].str();
1158 if(!step_delta.
empty()) {
1160 delta.
append(std::move(step_delta));
1163 ERR_CS <<
"Broken update sequence from version " << from <<
" to "
1164 << to <<
" for the add-on '" << name <<
"', sending a full pack instead";
1165 force_use_full =
true;
1173 if(delivery_size > full_pack_size && full_pack_size > 0) {
1174 force_use_full =
true;
1180 if(!force_use_full && !delta.
empty()) {
1181 std::ostringstream ostr;
1183 const auto& wml_text = ostr.str();
1188 LOG_CS << req <<
"Sending add-on '" << name <<
"' version: " << from <<
" -> " << to <<
" (delta)";
1190 utils::visit([
this, &req, &doc](
auto && sock) {
1194 full_pack_path.clear();
1201 if(!full_pack_path.empty()) {
1202 if(full_pack_size < 0) {
1203 send_error(
"Add-on '" + name +
"' could not be read by the server.", req.
sock);
1207 LOG_CS << req <<
"Sending add-on '" << name <<
"' version: " << to <<
" size: " << full_pack_size / 1024 <<
" KiB";
1208 utils::visit([
this, &req, &full_pack_path](
auto&& socket) {
1217 addon[
"downloads"] = 1 + addon[
"downloads"].to_int();
1229 if(!addon || addon[
"hidden"].to_bool()) {
1234 std::string
path = addon[
"filename"].str() +
'/';
1238 if(version_map.empty()) {
1239 send_error(
"No versions of the add-on '" + req.
cfg[
"name"].str() +
"' are available on the server.", req.
sock);
1242 const auto& version_str = addon[
"version"].str();
1244 auto version = version_map.find(version_parsed);
1245 if(version != version_map.end()) {
1246 path += version->second[
"filename"].str();
1249 if(version_str.empty()) {
1250 path += version_map.rbegin()->second[
"filename"].str();
1252 path += (--version_map.upper_bound(version_parsed))->second[
"filename"].str();
1256 path = index_from_full_pack_filename(
path);
1260 send_error(
"Missing index file for the add-on '" + req.
cfg[
"name"].str() +
"'.", req.
sock);
1264 LOG_CS << req <<
"Sending add-on hash index for '" << req.
cfg[
"name"] <<
"' size: " <<
file_size / 1024 <<
" KiB";
1265 utils::visit([
this, &
path, &req](
auto&& socket) {
1276 LOG_CS <<
"in read-only mode, request for upload terms denied";
1277 send_error(
"The server is currently in read-only mode, add-on uploads are disabled.", req.
sock);
1281 LOG_CS << req <<
"Sending license terms";
1288 LOG_CS <<
"Validation error: uploads not permitted in read-only mode.";
1298 const bool is_upload_pack = have_wml(removelist) || have_wml(addlist);
1300 const std::string& name = upload[
"name"].str();
1302 existing_addon =
nullptr;
1305 bool passed_name_utf8_check =
false;
1309 passed_name_utf8_check =
true;
1313 existing_addon = &
c.second;
1318 if(!passed_name_utf8_check) {
1319 LOG_CS <<
"Validation error: bad UTF-8 in add-on name";
1322 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";
1329 if(upload[
"passphrase"].empty()) {
1330 LOG_CS <<
"Validation error: no passphrase specified";
1334 if(existing_addon && upload[
"forum_auth"].to_bool() != (*existing_addon)[
"forum_auth"].to_bool()) {
1335 LOG_CS <<
"Validation error: forum_auth is " << upload[
"forum_auth"].to_bool() <<
" but was previously uploaded set to " << (*existing_addon)[
"forum_auth"].to_bool();
1337 }
else if(upload[
"forum_auth"].to_bool()) {
1339 LOG_CS <<
"Validation error: client requested forum authentication but server does not support it";
1343 LOG_CS <<
"Validation error: forum auth requested for an author who doesn't exist";
1347 for(
const std::string& primary_author :
utils::split(upload[
"primary_authors"].str(),
',')) {
1349 LOG_CS <<
"Validation error: forum auth requested for a primary author who doesn't exist";
1354 for(
const std::string& secondary_author :
utils::split(upload[
"secondary_authors"].str(),
',')) {
1356 LOG_CS <<
"Validation error: forum auth requested for a secondary author who doesn't exist";
1362 LOG_CS <<
"Validation error: forum passphrase does not match";
1366 }
else if(existing_addon && !authenticate(*existing_addon, upload[
"passphrase"])) {
1367 LOG_CS <<
"Validation error: campaignd passphrase does not match";
1371 if(existing_addon && (*existing_addon)[
"hidden"].to_bool()) {
1372 LOG_CS <<
"Validation error: add-on is hidden";
1378 upload[
"title"].str(),
1379 upload[
"description"].str(),
1380 upload[
"author"].str(),
1382 upload[
"email"].str()))
1384 LOG_CS <<
"Validation error: blacklisted uploader or publish information";
1388 LOG_CS <<
"Validation error: invalid UTF-8 sequence in publish information while checking against the blacklist";
1394 if(!is_upload_pack && !have_wml(
data)) {
1395 LOG_CS <<
"Validation error: no add-on data.";
1399 if(is_upload_pack && !have_wml(removelist) && !have_wml(addlist)) {
1400 LOG_CS <<
"Validation error: no add-on data.";
1405 LOG_CS <<
"Validation error: invalid add-on name.";
1410 LOG_CS <<
"Validation error: add-on name starts with an illegal formatting character.";
1414 if(upload[
"title"].empty()) {
1415 LOG_CS <<
"Validation error: no add-on title specified";
1420 LOG_CS <<
"Validation error: add-on title starts with an illegal formatting character.";
1425 LOG_CS <<
"Validation error: unknown add-on type specified";
1429 if(upload[
"author"].empty()) {
1430 LOG_CS <<
"Validation error: no add-on author specified";
1434 if(upload[
"version"].empty()) {
1435 LOG_CS <<
"Validation error: no add-on version specified";
1439 if(existing_addon) {
1443 if(
strict_versions_ ? new_version <= old_version : new_version < old_version) {
1444 LOG_CS <<
"Validation error: add-on version not incremented";
1449 if(upload[
"description"].empty()) {
1450 LOG_CS <<
"Validation error: no add-on description specified";
1455 if(upload[
"email"].empty() && !upload[
"forum_auth"].to_bool()) {
1456 LOG_CS <<
"Validation error: no add-on email specified";
1460 if(
const auto badnames = multi_find_illegal_names(
data, addlist, removelist)) {
1462 LOG_CS <<
"Validation error: invalid filenames in add-on pack (" << badnames->size() <<
" entries)";
1466 if(
const auto badnames = multi_find_case_conflicts(
data, addlist, removelist)) {
1468 LOG_CS <<
"Validation error: case conflicts in add-on pack (" << badnames->size() <<
" entries)";
1472 if(is_upload_pack && !existing_addon) {
1473 LOG_CS <<
"Validation error: attempted to send an update pack for a non-existent add-on";
1479 int topic_id =
std::stoi(url_params[
"topic_id"].str(
"0"));
1482 LOG_CS <<
"Validation error: feedback topic ID does not exist in forum database";
1487 LOG_CS <<
"Validation error: feedback topic ID is not a valid number";
1497 const auto upload_ts = std::chrono::system_clock::now();
1499 const auto& name = upload[
"name"].str();
1501 LOG_CS << req <<
"Validating add-on '" << name <<
"'...";
1503 config* addon_ptr =
nullptr;
1504 std::string val_error_data;
1505 const auto val_status =
validate_addon(req, addon_ptr, val_error_data);
1508 LOG_CS <<
"Upload of '" << name <<
"' aborted due to a failed validation check";
1510 send_error(
msg, val_error_data,
static_cast<unsigned int>(val_status), req.
sock);
1514 LOG_CS << req <<
"Processing add-on '" << name <<
"'...";
1520 const bool is_delta_upload = have_wml(delta_remove) || have_wml(delta_add);
1521 const bool is_existing_upload = addon_ptr !=
nullptr;
1523 if(!is_existing_upload) {
1526 addon_ptr = &(*entry.first).second;
1529 config& addon = *addon_ptr;
1531 LOG_CS << req <<
"Upload type: "
1532 << (is_delta_upload ?
"delta" :
"full") <<
", "
1533 << (is_existing_upload ?
"update" :
"new");
1538 "title",
"name",
"uploader",
"author",
"primary_authors",
"secondary_authors",
"description",
"version",
"icon",
1539 "translate",
"dependencies",
"core",
"type",
"tags",
"email",
"forum_auth"
1542 const std::string& pathstem =
"data/" + name;
1543 addon[
"filename"] = pathstem;
1544 addon[
"upload_ip"] = req.
addr;
1546 if(!is_existing_upload && !addon[
"forum_auth"].to_bool()) {
1547 set_passphrase(addon, upload[
"passphrase"]);
1550 if(addon[
"downloads"].empty()) {
1551 addon[
"downloads"] = 0;
1555 addon[
"uploads"] = 1 + addon[
"uploads"].to_int();
1560 addon.
add_child(
"feedback", *url_params);
1562 topic_id = url_params[
"topic_id"].to_int();
1566 if(addon[
"forum_auth"].to_bool()) {
1567 addon[
"email"] =
user_handler_->get_user_email(upload[
"uploader"].str());
1573 if(!do_authors_exist || is_primary) {
1583 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());
1594 if(!locale_params[
"language"].empty()) {
1596 locale[
"language"] = locale_params[
"language"].str();
1597 locale[
"supported"] =
false;
1599 if(!locale_params[
"title"].empty()) {
1600 locale[
"title"] = locale_params[
"title"].str();
1602 if(!locale_params[
"description"].empty()) {
1603 locale[
"description"] = locale_params[
"description"].str();
1614 if(have_wml(full_pack)) {
1616 rw_full_pack = std::move(
const_cast<config&
>(*full_pack));
1621 const auto& new_version = addon[
"version"].str();
1624 if(is_delta_upload) {
1629 if(version_map.empty()) {
1631 ERR_CS <<
"Add-on '" << name <<
"' has an empty version table, this should not happen";
1636 auto prev_version = upload[
"from"].str();
1638 if(prev_version.empty()) {
1639 prev_version = version_map.rbegin()->first;
1644 auto vm_entry = version_map.find(prev_version_parsed);
1645 if(vm_entry == version_map.end()) {
1646 prev_version = (--version_map.upper_bound(prev_version_parsed))->first;
1654 std::set<std::string> delete_packs;
1655 for(
const auto& pack : addon.
child_range(
"update_pack")) {
1656 if(pack[
"to"].str() == new_version) {
1657 const auto& pack_filename = pack[
"filename"].
str();
1659 delete_packs.insert(pack_filename);
1663 if(!delete_packs.empty()) {
1665 return delete_packs.find(
p[
"filename"].str()) != delete_packs.end();
1669 const auto& update_pack_fn = make_update_pack_filename(prev_version, new_version);
1673 pack_info[
"from"] = prev_version;
1674 pack_info[
"to"] = new_version;
1676 pack_info[
"filename"] = update_pack_fn;
1681 LOG_CS <<
"Saving provided update pack for " << prev_version <<
" -> " << new_version <<
"...";
1685 static const config empty_config;
1687 writer.open_child(
"removelist");
1688 writer.write(have_wml(delta_remove) ? *delta_remove : empty_config);
1689 writer.close_child(
"removelist");
1691 writer.open_child(
"addlist");
1692 writer.write(have_wml(delta_add) ? *delta_add : empty_config);
1693 writer.close_child(
"addlist");
1703 auto it = version_map.find(prev_version_parsed);
1704 if(it == version_map.end()) {
1706 ERR_CS <<
"Previous version dropped off the version map?";
1712 rw_full_pack.
clear();
1715 if(have_wml(delta_remove)) {
1719 if(have_wml(delta_add)) {
1735 config version_cfg{
"version", new_version};
1736 version_cfg[
"filename"] = make_full_pack_filename(new_version);
1738 version_map.erase(new_version_parsed);
1741 return old_cfg[
"version"].str() == new_version;
1745 version_map.emplace(new_version_parsed, version_cfg);
1746 addon.
add_child(
"version", version_cfg);
1750 rw_full_pack[
"name"] =
"";
1754 const auto& full_pack_path = pathstem +
'/' + version_cfg[
"filename"].str();
1755 const auto& index_path = pathstem +
'/' + make_index_filename(new_version);
1758 config pack_index{
"name",
""};
1763 addon_pack_file.commit();
1767 addon_index_file.commit();
1774 std::set<std::string> expire_packs;
1777 if(upload_ts >
chrono::parse_timestamp(pack[
"expire"]) || pack[
"from"].str() == new_version || (!is_delta_upload && pack[
"to"].str() == new_version)) {
1778 LOG_CS <<
"Expiring upate pack for " << pack[
"from"].str() <<
" -> " << pack[
"to"].str();
1779 const auto& pack_filename = pack[
"filename"].str();
1781 expire_packs.insert(pack_filename);
1785 if(!expire_packs.empty()) {
1787 return expire_packs.find(
p[
"filename"].str()) != expire_packs.end();
1794 for(
auto iter = version_map.begin(); std::distance(iter, version_map.end()) > 1;) {
1795 const config& prev_version = iter->second;
1796 const config& next_version = (++iter)->second;
1798 const auto& prev_version_name = prev_version[
"version"].str();
1799 const auto& next_version_name = next_version[
"version"].str();
1803 for(
const auto& pack : addon.
child_range(
"update_pack")) {
1804 if(pack[
"from"].str() == prev_version_name && pack[
"to"].str() == next_version_name) {
1815 LOG_CS <<
"Automatically generating update pack for " << prev_version_name <<
" -> " << next_version_name <<
"...";
1817 const auto& prev_path = pathstem +
'/' + prev_version[
"filename"].str();
1818 const auto& next_path = pathstem +
'/' + next_version[
"filename"].str();
1821 ERR_CS <<
"Unable to automatically generate an update pack for '" << name
1822 <<
"' for version " << prev_version_name <<
" to " << next_version_name
1827 const auto& update_pack_fn = make_update_pack_filename(prev_version_name, next_version_name);
1830 pack_info[
"from"] = prev_version_name;
1831 pack_info[
"to"] = next_version_name;
1833 pack_info[
"filename"] = update_pack_fn;
1856 LOG_CS << req <<
"Finished uploading add-on '" << upload[
"name"] <<
"'";
1860 fire(
"hook_post_upload", name);
1866 const std::string&
id =
erase[
"name"].str();
1869 LOG_CS << req <<
"in read-only mode, request to delete '" <<
id <<
"' denied";
1870 send_error(
"Cannot delete add-on: The server is currently in read-only mode.", req.
sock);
1874 LOG_CS << req <<
"Deleting add-on '" <<
id <<
"'";
1891 if(!addon[
"forum_auth"].to_bool()) {
1892 if(!authenticate(*addon, pass)) {
1903 if(addon[
"hidden"].to_bool()) {
1904 LOG_CS <<
"Add-on removal denied - hidden add-on.";
1905 send_error(
"Add-on deletion denied. Please contact the server administration for assistance.", req.
sock);
1919 LOG_CS <<
"in read-only mode, request to change passphrase denied";
1920 send_error(
"Cannot change passphrase: The server is currently in read-only mode.", req.
sock);
1928 }
else if(addon[
"forum_auth"].to_bool()) {
1929 send_error(
"Changing the password for add-ons using forum_auth is not supported.", req.
sock);
1930 }
else if(!authenticate(*addon, cpass[
"passphrase"])) {
1932 }
else if(addon[
"hidden"].to_bool()) {
1933 LOG_CS <<
"Passphrase change denied - hidden add-on.";
1934 send_error(
"Add-on passphrase change denied. Please contact the server administration for assistance.", req.
sock);
1935 }
else if(cpass[
"new_passphrase"].empty()) {
1938 set_passphrase(*addon, cpass[
"new_passphrase"]);
1950 std::string uploader = addon[
"uploader"].str();
1951 std::string
id = addon[
"name"].str();
1959 if((do_authors_exist && !is_primary && !is_secondary) || (is_secondary && is_delete)) {
1963 std::string author = addon[
"uploader"].str();
1965 std::string hashed_password =
hash_password(passphrase, salt, author);
1976 std::string config_file =
"server.cfg";
1977 unsigned short port = 0;
1983 for(
auto domain : {
"campaignd",
"campaignd/blacklist",
"server" }) {
1994 std::cout << cmdline.help_text();
1998 if(cmdline.version) {
2003 if(cmdline.config_file) {
2009 if(cmdline.server_dir) {
2014 port = *cmdline.port;
2019 PLAIN_LOG <<
"Invalid network port: " << port;
2024 if(cmdline.show_log_domains) {
2029 for(
const auto& ldl : cmdline.log_domain_levels) {
2031 PLAIN_LOG <<
"Unknown log domain: " << ldl.first;
2036 if(cmdline.log_precise_timestamps) {
2040 if(cmdline.report_timings) {
2041 campaignd::timing_reports_enabled =
true;
2047 PLAIN_LOG <<
"Server directory '" << *cmdline.server_dir <<
"' does not exist or is not a directory.";
2052 PLAIN_LOG <<
"Server configuration file '" << config_file <<
"' is not a file.";
2060 PLAIN_LOG <<
"Bad server directory '" << server_path <<
"'.";
2076 }
catch(
const boost::program_options::error&
e) {
2077 PLAIN_LOG <<
"Error in command line: " <<
e.what();
2080 PLAIN_LOG <<
"Could not parse config file: " <<
e.message;
2085 }
catch(
const std::bad_function_call& ) {
2086 PLAIN_LOG <<
"Bad request handler function call";
campaignd authentication API.
std::vector< std::string > names
#define REGISTER_CAMPAIGND_HANDLER(req_id)
int main(int argc, char **argv)
static lg::log_domain log_campaignd("campaignd")
static int run_campaignd(int argc, char **argv)
static lg::log_domain log_server("server")
static lg::log_domain log_config("config")
void read(const config &cfg)
Initializes the blacklist from WML.
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.
Represents a server control line written to a communication socket.
const std::string & cmd() const
Returns the control command.
std::string full() const
Return the full command line string.
std::size_t args_count() const
Returns the total number of arguments, not including the command itself.
request_handlers_table handlers_
ADDON_CHECK_STATUS validate_addon(const server::request &req, config *&existing_addon, std::string &error_data)
Performs validation on an incoming add-on.
void handle_upload(const request &)
void delete_addon(const std::string &id)
std::set< std::string > capabilities_
config cfg_
Server config.
void handle_request_terms(const request &)
void send_error(const std::string &msg, const any_socket_ptr &sock)
Send a client an error message.
std::string license_notice_
void load_blacklist()
Reads the add-ons upload blacklist from WML.
std::map< std::string, std::string > hooks_
void handle_new_client(socket_ptr socket)
const config & server_info() const
Retrieves the contents of the [server_info] WML node.
std::string blacklist_file_
std::unique_ptr< user_handler > user_handler_
void handle_server_id(const request &)
void handle_delete(const request &)
void write_config()
Writes the server configuration WML back to disk.
void mark_dirty(const std::string &addon)
void send_message(const std::string &msg, const any_socket_ptr &sock)
Send a client an informational message.
optional_config get_addon(const std::string &id)
Retrieves an addon by id if found, or a null config otherwise.
void handle_request_campaign_hash(const request &)
void handle_flush(const boost::system::error_code &error)
std::string feedback_url_format_
void flush_cfg()
Starts timer to write config to disk every ten minutes.
std::unordered_map< std::string, config > addons_
The hash map of addons metadata.
std::unordered_set< std::string > dirty_addons_
The set of unique addon names with pending metadata updates.
bool ignore_address_stats(const std::string &addr) const
Checks if the specified address should never bump download counts.
static const std::size_t default_document_size_limit
Default upload size limit in bytes.
int compress_level_
Used for add-on archives.
boost::asio::basic_waitable_timer< std::chrono::steady_clock > flush_timer_
void handle_read_from_fifo(const boost::system::error_code &error, std::size_t bytes_transferred)
const std::string cfg_file_
void serve_requests(Socket socket, boost::asio::yield_context yield)
void fire(const std::string &hook, const std::string &addon)
Fires a hook script.
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 ...
void handle_change_passphrase(const request &)
std::vector< std::string > stats_exempt_ips_
void handle_request_campaign(const request &)
void load_config()
Reads the server configuration from WML.
server(const std::string &cfg_file, unsigned short port=0)
std::chrono::seconds update_pack_lifespan_
void handle_sighup(const boost::system::error_code &error, int signal_number)
void register_handlers()
Registers client request handlers.
void handle_request_campaign_list(const request &)
Variant for storing WML attributes.
bool empty() const
Tests for an attribute that either was never set or was set to "".
Class for writing a config out to a file in pieces.
void write(const config &cfg)
A config object defines a single node in a WML file, with access to child nodes.
void copy_or_remove_attributes(const config &from, T... keys)
Copies or deletes attributes to match the source config.
void append(const config &cfg)
Append data from another config object to this one.
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.
config & mandatory_child(config_key_type key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
std::size_t child_count(config_key_type key) const
void clear_children(T... keys)
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
void remove_children(config_key_type key, const std::function< bool(const config &)> &p={})
Removes all children with tag key for which p returns true.
child_itors child_range(config_key_type key)
optional_config_impl< config > optional_child(config_key_type key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
config & add_child(config_key_type key)
Wrapper class that guarantees that file commit atomicity.
void commit()
Commits the new file contents to disk atomically.
scoped_ostream & ostream()
Returns the write stream associated with the file.
A class to handle the non-SQL logic for connecting to the phpbb forum database.
severity get_severity() const
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_
void async_send_doc_queued(SocketPtr socket, simple_wml::document &doc)
High level wrapper for sending a WML document.
void coro_send_doc(SocketPtr socket, simple_wml::document &doc, const boost::asio::yield_context &yield)
Send a WML document from within a coroutine.
boost::asio::io_context io_service_
std::unique_ptr< simple_wml::document > coro_receive_doc(SocketPtr socket, const boost::asio::yield_context &yield)
Receive WML document from a coroutine.
boost::asio::posix::stream_descriptor input_
void coro_send_file(const socket_ptr &socket, const std::string &filename, const boost::asio::yield_context &yield)
Send contents of entire file directly to socket from within a coroutine.
void load_tls_config(const config &cfg)
static std::size_t document_size_limit
node & add_child(const char *name)
node & set_attr_dup(const char *key, const char *value)
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
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
Declarations for File-IO.
Atomic filesystem commit functions.
unsigned in
If equal to search_counter, the node is off the list.
Interfaces for manipulating version numbers of engine, add-ons, etc.
std::string label
What to show in the filter's drop-down list.
std::map< std::string, addon_info > addons_list
New lexcical_cast header.
Standard logging facilities (interface).
std::pair< std::string, std::string > generate_hash(const std::string &passphrase)
Generates a salted hash from the specified passphrase.
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.
std::ostream & operator<<(std::ostream &o, const server::request &r)
std::string format_addon_feedback_url(const std::string &format, const config ¶ms)
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)
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)
auto parse_timestamp(long long val)
auto parse_duration(const config_attribute_value &val, const Duration &def=Duration{0})
filesystem::scoped_istream istream_file(const std::string &fname, bool treat_failure_as_error)
void get_files_in_dir(const std::string &dir, std::vector< std::string > *files, std::vector< std::string > *dirs, name_mode mode, filter_mode filter, reorder_mode reorder, file_tree_checksum *checksum)
Get a list of all files and/or directories in a given directory.
bool delete_file(const std::string &filename)
bool is_directory(const std::string &fname)
Returns true if the given file is a directory.
bool delete_directory(const std::string &dirname, const bool keep_pbl)
int file_size(const std::string &fname)
Returns the size of a file, or -1 if the file doesn't exist.
std::unique_ptr< std::istream > scoped_istream
bool set_cwd(const std::string &dir)
std::string normalize_path(const std::string &fpath, bool normalize_separators, bool resolve_dot_entries)
Returns the absolute path of a file.
const std::string revision
std::string list_log_domains(const std::string &filter)
void precise_timestamps(bool pt)
bool set_log_domain_severity(const std::string &name, severity severity)
std::string lowercase(std::string_view s)
Returns a lowercased version of the string.
std::string & erase(std::string &str, const std::size_t start, const std::size_t len)
Erases a portion of a UTF-8 string.
int stoi(std::string_view str)
Same interface as std::stoi and meant as a drop in replacement, except:
bool wildcard_string_match(const std::string &str, const std::string &match)
Match using '*' as any number of characters (including none), '+' as one or more characters,...
bool string_bool(const std::string &str, bool def)
Convert no, false, off, 0, 0.0 to false, empty to def, and others to true.
std::string join(const T &v, const std::string &s=",")
Generates a new string joining container items in a list.
std::vector< std::string > split(const config_attribute_value &val)
auto * find(Container &container, const Value &value)
Convenience wrapper for using find on a container without needing to comare to end()
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
campaignd command line options parsing.
void read(config &cfg, std::istream &in, abstract_validator *validator)
void write(std::ostream &out, const configr_of &cfg, unsigned int level)
void read_gz(config &cfg, std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially a gzip_error.
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
Client request information object.
const any_socket_ptr sock
boost::asio::yield_context yield
context of the coroutine the request is executed in async operations on sock can use it instead of a ...
An exception object used when an IO error occurs.
Reports time elapsed at the end of an object scope.
bool addon_name_legal(const std::string &name)
Checks whether an add-on id/name is legal or not.
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.
@ SERVER_ADDONS_LIST
Corrupted server add-ons list.
@ UNAUTHORIZED
Authentication failed.
@ BAD_FEEDBACK_TOPIC_ID
The provided topic ID for the addon's feedback forum thread is invalid.
@ SERVER_DELTA_NO_VERSIONS
No versions to deltify against.
@ NO_TITLE
No title specified.
@ AUTH_TYPE_MISMATCH
The addon's forum_auth value does not match its previously set value.
@ ILLEGAL_FILENAME
Bad filename.
@ NO_PASSPHRASE
No passphrase specified.
@ UNEXPECTED_DELTA
Delta for a non-existent add-on.
@ VERSION_NOT_INCREMENTED
Version number is not an increment.
@ FILENAME_CASE_CONFLICT
Filename case conflict.
@ TITLE_HAS_MARKUP
Markup in add-on title.
@ SERVER_UNSPECIFIED
Unspecified server error.
@ BAD_TYPE
Bad add-on type.
@ NO_AUTHOR
No author specified.
@ 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.
@ 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.