57 #if !(defined(_WIN32)) 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) 68 #define ERR_CONFIG LOG_STREAM(err, log_config) 69 #define WRN_CONFIG LOG_STREAM(warn, log_config) 72 #define ERR_SERVER LOG_STREAM(err, log_server) 84 const std::set<std::string> cap_defaults = {
94 const std::string default_web_url =
"https://add-ons.wesnoth.org/";
105 const std::string default_license_notice = R
"""(<span size='x-large'>General Rules</span> 107 The current version of the server rules can be found at: https://r.wesnoth.org/t51347 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> 111 <span size='x-large'>Licensing</span> 113 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: 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>”. 118 <b>By uploading content to this server, you certify that you have the right to:</b> 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.)"""; 123 bool timing_reports_enabled =
false;
127 if(timing_reports_enabled) {
129 LOG_CS << req <<
"Time elapsed: " << tim <<
" ms";
131 LOG_CS << req <<
"Time elapsed [" <<
label <<
"]: " << tim <<
" ms";
155 if(!addon[
"forum_auth"].to_bool()) {
168 inline void set_passphrase(
config& addon,
const std::string& passphrase)
171 if(!addon[
"forum_auth"].to_bool()) {
181 inline std::string make_update_pack_filename(
const std::string& old_version,
const std::string& new_version)
191 inline std::string make_full_pack_filename(
const std::string& version)
201 inline std::string make_index_filename(
const std::string& version)
211 inline std::string index_from_full_pack_filename(std::string pack_fn)
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");
225 return cfg && !cfg->
empty();
233 template<
typename... Vals>
234 std::optional<std::vector<std::string>> multi_find_illegal_names(
const Vals&... args)
236 std::vector<std::string>
names;
239 return !names.empty() ? std::optional(names) : std::nullopt;
247 template<
typename... Vals>
248 std::optional<std::vector<std::string>> multi_find_case_conflicts(
const Vals&... args)
250 std::vector<std::string>
names;
253 return !names.empty() ? std::optional(names) : std::nullopt;
261 std::string simple_wml_escape(
const std::string& text)
264 auto it = text.begin();
266 while(it != text.end()) {
267 res.append(*it ==
'"' ? 2 : 1, *it);
278 , user_handler_(nullptr)
279 , capabilities_(cap_defaults)
283 , cfg_file_(cfg_file)
286 , update_pack_lifespan_(0)
287 , strict_versions_(true)
291 , feedback_url_format_()
296 , stats_exempt_ips_()
297 , flush_timer_(io_service_)
302 std::memset( &sa, 0,
sizeof(sa) );
303 #pragma GCC diagnostic ignored "-Wold-style-cast" 304 sa.sa_handler = SIG_IGN;
305 int res = sigaction( SIGPIPE, &sa,
nullptr);
340 LOG_CS <<
"READ-ONLY MODE ACTIVE";
353 web_url_ = svinfo_cfg[
"web_url"].str(default_web_url);
354 license_notice_ = svinfo_cfg[
"license_notice"].str(default_license_notice);
363 hooks_.emplace(std::string(
"hook_post_upload"),
cfg_[
"hook_post_upload"]);
364 hooks_.emplace(std::string(
"hook_post_erase"),
cfg_[
"hook_post_erase"]);
368 if(!
cfg_[
"control_socket"].empty()) {
369 const std::string&
path =
cfg_[
"control_socket"].str();
372 const int res = mkfifo(path.c_str(),0660);
373 if(res != 0 && errno != EEXIST) {
374 ERR_CS <<
"could not make fifo at '" << path <<
"' (" << strerror(errno) <<
")";
377 int fifo = open(path.c_str(), O_RDWR|O_NONBLOCK);
379 LOG_CS <<
"opened fifo at '" << path <<
"'. Server commands may be written to this file.";
403 std::vector<std::string> legacy_addons, dirs;
406 for(
const std::string& addon_dir : dirs) {
410 addons_.emplace(meta[
"name"].str(), meta);
419 WRN_CS <<
"Old format addons have been detected in the config! They will be converted to the new file format! " 420 << campaigns.
child_count(
"campaign") <<
" entries to be processed.";
422 const std::string& addon_id = campaign[
"name"].str();
423 const std::string& addon_file = campaign[
"filename"].str();
426 +
"' already exists in the new form! Possible code or filesystem interference!\n");
428 if(std::find(legacy_addons.begin(), legacy_addons.end(), addon_id) == legacy_addons.end()) {
430 +
"'. Check the file structure!\n");
440 config version_cfg =
config(
"version", campaign[
"version"].str());
441 version_cfg[
"filename"] = make_full_pack_filename(campaign[
"version"]);
442 campaign.add_child(
"version", version_cfg);
444 data.
remove_attributes(
"title",
"campaign_name",
"author",
"description",
"version",
"timestamp",
"original_timestamp",
"icon",
"type",
"tags");
457 writer.write(data_hash);
458 campaign_hash_file.
commit();
461 addons_.emplace(addon_id, campaign);
465 LOG_CS <<
"Legacy addons processing finished.";
469 LOG_CS <<
"Loaded addons metadata. " <<
addons_.size() <<
" addons found.";
474 ERR_CS <<
"The server id must be set when database support is used.";
478 LOG_CS <<
"User handler initialized.";
487 o << '[' << (utils::holds_alternative<tls_socket_ptr>(r.
sock) ?
"+" :
"") << r.
addr <<
' ' << r.
cmd <<
"] ";
493 boost::asio::spawn(
io_service_, [
this, socket](boost::asio::yield_context yield) {
500 boost::asio::spawn(
io_service_, [
this, socket](boost::asio::yield_context yield) {
505 template<
class Socket>
511 socket->lowest_layer().close();
516 read(data, doc->output());
524 request_handlers_table::const_iterator j
530 auto st = service_timer(req);
531 j->second(
this, req);
544 if(error == boost::asio::error::operation_aborted)
547 ERR_CS <<
"Error reading from fifo: " << error.message();
553 std::getline(is, cmd);
557 if(ctl ==
"shut_down") {
558 LOG_CS <<
"Shut down requested by admin, shutting down...";
560 }
else if(ctl ==
"readonly") {
566 }
else if(ctl ==
"flush") {
567 LOG_CS <<
"Flushing config to disk...";
569 }
else if(ctl ==
"reload") {
571 if(ctl[1] ==
"blacklist") {
572 LOG_CS <<
"Reloading blacklist...";
575 ERR_CS <<
"Unrecognized admin reload argument: " << ctl[1];
578 LOG_CS <<
"Reloading all configuration...";
580 LOG_CS <<
"Reloaded configuration";
582 }
else if(ctl ==
"delete") {
584 ERR_CS <<
"Incorrect number of arguments for 'delete'";
586 const std::string& addon_id = ctl[1];
588 LOG_CS <<
"deleting add-on '" << addon_id <<
"' requested from control FIFO";
591 }
else if(ctl ==
"hide" || ctl ==
"unhide") {
593 ERR_CS <<
"Incorrect number of arguments for '" << ctl.
cmd() <<
"'";
595 const std::string& addon_id = ctl[1];
599 ERR_CS <<
"Add-on '" << addon_id <<
"' not found, cannot " << ctl.
cmd();
601 addon[
"hidden"] = ctl.
cmd() ==
"hide";
604 LOG_CS <<
"Add-on '" << addon_id <<
"' is now " << (ctl.
cmd() ==
"hide" ?
"hidden" :
"unhidden");
607 }
else if(ctl ==
"setpass") {
609 ERR_CS <<
"Incorrect number of arguments for 'setpass'";
611 const std::string& addon_id = ctl[1];
612 const std::string& newpass = ctl[2];
616 ERR_CS <<
"Add-on '" << addon_id <<
"' not found, cannot set passphrase";
617 }
else if(newpass.empty()) {
619 ERR_CS <<
"Add-on passphrases may not be empty!";
620 }
else if(addon[
"forum_auth"].to_bool()) {
621 ERR_CS <<
"Can't set passphrase for add-on using forum_auth.";
623 set_passphrase(addon, newpass);
626 LOG_CS <<
"New passphrase set for '" << addon_id <<
"'";
629 }
else if(ctl ==
"setattr") {
631 ERR_CS <<
"Incorrect number of arguments for 'setattr'";
633 const std::string& addon_id = ctl[1];
634 const std::string& key = ctl[2];
646 ERR_CS <<
"Add-on '" << addon_id <<
"' not found, cannot set attribute";
647 }
else if(key ==
"name" || key ==
"version") {
648 ERR_CS <<
"setattr cannot be used to rename add-ons or change their version";
649 }
else if(key ==
"passhash"|| key ==
"passsalt") {
650 ERR_CS <<
"setattr cannot be used to set auth data -- use setpass instead";
658 ERR_CS <<
"Attribute '" << key <<
"' is not a recognized add-on attribute";
663 LOG_CS <<
"Set attribute on add-on '" << addon_id <<
"':\n" 664 << key <<
"=\"" << value <<
"\"";
667 }
else if(ctl ==
"log") {
668 static const std::map<std::string, int> log_levels = {
677 ERR_CS <<
"Incorrect number of arguments for 'log'";
678 }
else if(ctl[1] ==
"precise") {
681 LOG_CS <<
"Precise timestamps enabled";
682 }
else if(ctl[2] ==
"off") {
684 LOG_CS <<
"Precise timestamps disabled";
686 ERR_CS <<
"Invalid argument for 'log precise': " << ctl[2];
688 }
else if(log_levels.find(ctl[1]) == log_levels.end()) {
689 ERR_CS <<
"Invalid log level '" << ctl[1] <<
"'";
691 auto sev = log_levels.find(ctl[1])->second;
694 ERR_CS <<
"Unknown log domain '" << domain <<
"'";
696 LOG_CS <<
"Set log level for domain '" << domain <<
"' to " << ctl[1];
700 }
else if(ctl ==
"timings") {
702 ERR_CS <<
"Incorrect number of arguments for 'timings'";
703 }
else if(ctl[1] ==
"on") {
704 campaignd::timing_reports_enabled =
true;
705 LOG_CS <<
"Request servicing timing reports enabled";
706 }
else if(ctl[1] ==
"off") {
707 campaignd::timing_reports_enabled =
false;
708 LOG_CS <<
"Request servicing timing reports disabled";
710 ERR_CS <<
"Invalid argument for 'timings': " << ctl[1];
713 ERR_CS <<
"Unrecognized admin command: " << ctl.
full();
721 LOG_CS <<
"SIGHUP caught, reloading config.";
725 LOG_CS <<
"Reloaded configuration";
734 flush_timer_.expires_from_now(std::chrono::minutes(10));
741 ERR_CS <<
"Error from reload timer: " << error.message();
742 throw boost::system::system_error(error);
773 DBG_CS <<
"writing configuration and add-ons list to disk...";
780 if(addon && !addon[
"filename"].empty()) {
787 dirty_addons_.clear();
791 void server::fire(
const std::string& hook, [[maybe_unused]]
const std::string& addon)
793 const std::map<std::string, std::string>::const_iterator itor =
hooks_.find(hook);
794 if(itor ==
hooks_.end()) {
798 const std::string& script = itor->second;
804 ERR_CS <<
"Tried to execute a script on an unsupported platform";
809 if((childpid = fork()) == -1) {
810 ERR_CS <<
"fork failed while updating add-on " << addon;
818 execlp(script.c_str(), script.c_str(), addon.c_str(),
static_cast<char *
>(
nullptr));
821 PLAIN_LOG <<
"ERROR: exec failed with errno " << errno <<
" for addon " << addon;
844 const auto& escaped_msg = simple_wml_escape(msg);
857 const auto& escaped_msg = simple_wml_escape(msg);
865 const std::string& status_hex =
formatter()
866 <<
"0x" << std::setfill(
'0') << std::setw(2*
sizeof(
unsigned int)) << std::hex
867 << std::uppercase << status_code;
870 const auto& escaped_status_str = simple_wml_escape(std::to_string(status_code));
871 const auto& escaped_msg = simple_wml_escape(msg);
872 const auto& escaped_extra_data = simple_wml_escape(extra_data);
878 err_cfg.
set_attr_dup(
"extra_data", escaped_extra_data.c_str());
879 err_cfg.
set_attr_dup(
"status_code", escaped_status_str.c_str());
888 return addon->second;
898 if(cfg[
"forum_auth"].to_bool()) {
903 ERR_CS <<
"Cannot delete unrecognized add-on '" <<
id <<
"'";
907 std::string fn = cfg[
"filename"].str();
910 ERR_CS <<
"Add-on '" <<
id <<
"' does not have an associated filename, cannot delete";
914 ERR_CS <<
"Could not delete the directory for addon '" <<
id 915 <<
"' (" << fn <<
"): " << strerror(errno);
921 fire(
"hook_post_erase",
id);
923 LOG_CS <<
"Deleted add-on '" <<
id <<
"'";
926 #define REGISTER_CAMPAIGND_HANDLER(req_id) \ 927 handlers_[#req_id] = std::bind(&server::handle_##req_id, \ 928 std::placeholders::_1, std::placeholders::_2) 944 DBG_CS << req <<
"Sending server identification";
946 std::ostringstream ostr;
955 const auto& wml = ostr.str();
964 LOG_CS << req <<
"Sending add-ons list";
966 std::time_t epoch = std::time(
nullptr);
969 addons_list[
"timestamp"] = epoch;
970 if(req.cfg[
"times_relative_to"] !=
"now") {
974 bool before_flag =
false;
975 std::time_t before = epoch;
976 if(!req.cfg[
"before"].empty()) {
977 before += req.cfg[
"before"].to_time_t();
981 bool after_flag =
false;
982 std::time_t after = epoch;
983 if(!req.cfg[
"after"].empty()) {
984 after += req.cfg[
"after"].to_time_t();
988 const std::string& name = req.cfg[
"name"];
989 const std::string& lang = req.cfg[
"language"];
991 for(
const auto& addon :
addons_)
993 if(!name.empty() && name != addon.first) {
999 if(i[
"hidden"].to_bool()) {
1003 const auto& tm = i[
"timestamp"];
1005 if(before_flag && (tm.empty() || tm.to_time_t(0) >= before)) {
1008 if(after_flag && (tm.empty() || tm.to_time_t(0) <= after)) {
1017 if(j[
"language"] == lang && j[
"supported"].to_bool(
true)) {
1035 j.
remove_attributes(
"passphrase",
"passhash",
"passsalt",
"upload_ip",
"email");
1045 j.clear_children(
"feedback");
1048 j.clear_children(
"update_pack");
1052 response.
add_child(
"campaigns", std::move(addons_list));
1054 std::ostringstream ostr;
1055 write(ostr, response);
1056 std::string wml = ostr.str();
1067 if(!addon || addon[
"hidden"].to_bool()) {
1072 const auto& name = req.
cfg[
"name"].str();
1075 if(version_map.empty()) {
1076 send_error(
"No versions of the add-on '" + name +
"' are available on the server.", req.
sock);
1081 const auto& from = req.
cfg[
"from_version"].str();
1082 const auto& to = req.
cfg[
"version"].str(version_map.rbegin()->first);
1087 auto to_version_iter = version_map.find(to_parsed);
1088 if(to_version_iter == version_map.end()) {
1089 send_error(
"Could not find requested version " + to +
" of the addon '" + name +
1094 auto full_pack_path = addon[
"filename"].str() +
'/' + to_version_iter->second[
"filename"].str();
1103 if(!from.empty() && from_parsed < to_parsed && version_map.count(from_parsed) != 0) {
1112 int delivery_size = 0;
1113 bool force_use_full =
false;
1115 auto start_point = version_map.find(from_parsed);
1116 auto end_point = std::next(to_version_iter, 1);
1118 if(std::distance(start_point, end_point) <= 1) {
1120 ERR_CS <<
"Bad update sequence bounds in version " << from <<
" -> " << to <<
" update sequence for the add-on '" << name <<
"', sending a full pack instead";
1121 force_use_full =
true;
1124 for(
auto iter = start_point; !force_use_full && std::distance(iter, end_point) > 1;) {
1125 const auto& prev_version_cfg = iter->second;
1126 const auto& next_version_cfg = (++iter)->second;
1129 if(pack[
"from"].str() != prev_version_cfg[
"version"].str() ||
1130 pack[
"to"].str() != next_version_cfg[
"version"].str()) {
1135 const auto& update_pack_path = addon[
"filename"].str() +
'/' + pack[
"filename"].str();
1140 if(!step_delta.
empty()) {
1142 delta.
append(std::move(step_delta));
1145 ERR_CS <<
"Broken update sequence from version " << from <<
" to " 1146 << to <<
" for the add-on '" << name <<
"', sending a full pack instead";
1147 force_use_full =
true;
1155 if(delivery_size > full_pack_size && full_pack_size > 0) {
1156 force_use_full =
true;
1162 if(!force_use_full && !delta.
empty()) {
1163 std::ostringstream ostr;
1165 const auto& wml_text = ostr.str();
1170 LOG_CS << req <<
"Sending add-on '" << name <<
"' version: " << from <<
" -> " << to <<
" (delta)";
1172 utils::visit([
this, &req, &doc](
auto && sock) {
1176 full_pack_path.clear();
1183 if(!full_pack_path.empty()) {
1184 if(full_pack_size < 0) {
1185 send_error(
"Add-on '" + name +
"' could not be read by the server.", req.
sock);
1189 LOG_CS << req <<
"Sending add-on '" << name <<
"' version: " << to <<
" size: " << full_pack_size / 1024 <<
" KiB";
1190 utils::visit([
this, &req, &full_pack_path](
auto&& socket) {
1199 addon[
"downloads"] = 1 + addon[
"downloads"].to_int();
1211 if(!addon || addon[
"hidden"].to_bool()) {
1216 std::string
path = addon[
"filename"].str() +
'/';
1220 if(version_map.empty()) {
1221 send_error(
"No versions of the add-on '" + req.
cfg[
"name"].str() +
"' are available on the server.", req.
sock);
1224 const auto& version_str = addon[
"version"].str();
1226 auto version = version_map.find(version_parsed);
1227 if(version != version_map.end()) {
1228 path += version->second[
"filename"].
str();
1231 if(version_str.empty()) {
1232 path += version_map.rbegin()->second[
"filename"].str();
1234 path += (--version_map.upper_bound(version_parsed))->second[
"filename"].str();
1238 path = index_from_full_pack_filename(path);
1242 send_error(
"Missing index file for the add-on '" + req.
cfg[
"name"].str() +
"'.", req.
sock);
1246 LOG_CS << req <<
"Sending add-on hash index for '" << req.
cfg[
"name"] <<
"' size: " << file_size / 1024 <<
" KiB";
1247 utils::visit([
this, &path, &req](
auto&& socket) {
1258 LOG_CS <<
"in read-only mode, request for upload terms denied";
1259 send_error(
"The server is currently in read-only mode, add-on uploads are disabled.", req.
sock);
1263 LOG_CS << req <<
"Sending license terms";
1270 LOG_CS <<
"Validation error: uploads not permitted in read-only mode.";
1280 const bool is_upload_pack = have_wml(removelist) || have_wml(addlist);
1282 const std::string& name = upload[
"name"].str();
1284 existing_addon =
nullptr;
1287 bool passed_name_utf8_check =
false;
1291 passed_name_utf8_check =
true;
1295 existing_addon = &
c.second;
1300 if(!passed_name_utf8_check) {
1301 LOG_CS <<
"Validation error: bad UTF-8 in add-on name";
1304 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";
1311 if(upload[
"passphrase"].empty()) {
1312 LOG_CS <<
"Validation error: no passphrase specified";
1316 if(existing_addon && upload[
"forum_auth"].to_bool() != (*existing_addon)[
"forum_auth"].to_bool()) {
1317 LOG_CS <<
"Validation error: forum_auth is " << upload[
"forum_auth"].to_bool() <<
" but was previously uploaded set to " << (*existing_addon)[
"forum_auth"].to_bool();
1319 }
else if(upload[
"forum_auth"].to_bool()) {
1321 LOG_CS <<
"Validation error: client requested forum authentication but server does not support it";
1325 LOG_CS <<
"Validation error: forum auth requested for an author who doesn't exist";
1329 for(
const std::string& secondary_author :
utils::split(upload[
"secondary_authors"].str(),
',')) {
1331 LOG_CS <<
"Validation error: forum auth requested for a secondary author who doesn't exist";
1337 LOG_CS <<
"Validation error: forum passphrase does not match";
1341 }
else if(existing_addon && !authenticate(*existing_addon, upload[
"passphrase"])) {
1342 LOG_CS <<
"Validation error: campaignd passphrase does not match";
1346 if(existing_addon && (*existing_addon)[
"hidden"].to_bool()) {
1347 LOG_CS <<
"Validation error: add-on is hidden";
1353 upload[
"title"].str(),
1354 upload[
"description"].str(),
1355 upload[
"author"].str(),
1357 upload[
"email"].str()))
1359 LOG_CS <<
"Validation error: blacklisted uploader or publish information";
1363 LOG_CS <<
"Validation error: invalid UTF-8 sequence in publish information while checking against the blacklist";
1369 if(!is_upload_pack && !have_wml(
data)) {
1370 LOG_CS <<
"Validation error: no add-on data.";
1374 if(is_upload_pack && !have_wml(removelist) && !have_wml(addlist)) {
1375 LOG_CS <<
"Validation error: no add-on data.";
1380 LOG_CS <<
"Validation error: invalid add-on name.";
1385 LOG_CS <<
"Validation error: add-on name starts with an illegal formatting character.";
1389 if(upload[
"title"].empty()) {
1390 LOG_CS <<
"Validation error: no add-on title specified";
1395 LOG_CS <<
"Validation error: add-on title starts with an illegal formatting character.";
1400 LOG_CS <<
"Validation error: unknown add-on type specified";
1404 if(upload[
"author"].empty()) {
1405 LOG_CS <<
"Validation error: no add-on author specified";
1409 if(upload[
"version"].empty()) {
1410 LOG_CS <<
"Validation error: no add-on version specified";
1414 if(existing_addon) {
1418 if(
strict_versions_ ? new_version <= old_version : new_version < old_version) {
1419 LOG_CS <<
"Validation error: add-on version not incremented";
1424 if(upload[
"description"].empty()) {
1425 LOG_CS <<
"Validation error: no add-on description specified";
1430 if(upload[
"email"].empty() && !upload[
"forum_auth"].to_bool()) {
1431 LOG_CS <<
"Validation error: no add-on email specified";
1435 if(
const auto badnames = multi_find_illegal_names(
data, addlist, removelist)) {
1437 LOG_CS <<
"Validation error: invalid filenames in add-on pack (" << badnames->size() <<
" entries)";
1441 if(
const auto badnames = multi_find_case_conflicts(
data, addlist, removelist)) {
1443 LOG_CS <<
"Validation error: case conflicts in add-on pack (" << badnames->size() <<
" entries)";
1447 if(is_upload_pack && !existing_addon) {
1448 LOG_CS <<
"Validation error: attempted to send an update pack for a non-existent add-on";
1452 if(
const config& url_params = upload.
child(
"feedback")) {
1454 int topic_id = std::stoi(url_params[
"topic_id"].str(
"0"));
1457 LOG_CS <<
"Validation error: feedback topic ID does not exist in forum database";
1462 LOG_CS <<
"Validation error: feedback topic ID is not a valid number";
1472 const std::time_t upload_ts = std::time(
nullptr);
1474 const auto& name = upload[
"name"].str();
1476 LOG_CS << req <<
"Validating add-on '" << name <<
"'...";
1478 config* addon_ptr =
nullptr;
1479 std::string val_error_data;
1480 const auto val_status =
validate_addon(req, addon_ptr, val_error_data);
1483 LOG_CS <<
"Upload of '" << name <<
"' aborted due to a failed validation check";
1485 send_error(
msg, val_error_data, static_cast<unsigned int>(val_status), req.
sock);
1489 LOG_CS << req <<
"Processing add-on '" << name <<
"'...";
1495 const bool is_delta_upload = have_wml(delta_remove) || have_wml(delta_add);
1496 const bool is_existing_upload = addon_ptr !=
nullptr;
1498 if(!is_existing_upload) {
1500 auto entry =
addons_.emplace(name,
config(
"original_timestamp", upload_ts));
1501 addon_ptr = &(*entry.first).second;
1504 config& addon = *addon_ptr;
1506 LOG_CS << req <<
"Upload type: " 1507 << (is_delta_upload ?
"delta" :
"full") <<
", " 1508 << (is_existing_upload ?
"update" :
"new");
1513 "title",
"name",
"uploader",
"author",
"secondary_authors",
"description",
"version",
"icon",
1514 "translate",
"dependencies",
"core",
"type",
"tags",
"email",
"forum_auth" 1517 const std::string& pathstem =
"data/" + name;
1518 addon[
"filename"] = pathstem;
1519 addon[
"upload_ip"] = req.addr;
1521 if(!is_existing_upload && !addon[
"forum_auth"].to_bool()) {
1522 set_passphrase(addon, upload[
"passphrase"]);
1525 if(addon[
"downloads"].empty()) {
1526 addon[
"downloads"] = 0;
1529 addon[
"timestamp"] = upload_ts;
1530 addon[
"uploads"] = 1 + addon[
"uploads"].to_int();
1534 if(
const config& url_params = upload.
child(
"feedback")) {
1535 addon.
add_child(
"feedback", url_params);
1537 topic_id = url_params[
"topic_id"].to_int();
1541 if(addon[
"forum_auth"].to_bool()) {
1542 addon[
"email"] =
user_handler_->get_user_email(upload[
"uploader"].str());
1548 if(!do_authors_exist || is_primary) {
1558 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());
1569 if(!locale_params[
"language"].empty()) {
1571 locale[
"language"] = locale_params[
"language"].str();
1572 locale[
"supported"] =
false;
1574 if(!locale_params[
"title"].empty()) {
1575 locale[
"title"] = locale_params[
"title"].str();
1577 if(!locale_params[
"description"].empty()) {
1578 locale[
"description"] = locale_params[
"description"].str();
1589 if(have_wml(full_pack)) {
1591 rw_full_pack = std::move(const_cast<config&>(*full_pack));
1596 const auto& new_version = addon[
"version"].str();
1599 if(is_delta_upload) {
1604 if(version_map.empty()) {
1606 ERR_CS <<
"Add-on '" << name <<
"' has an empty version table, this should not happen";
1611 auto prev_version = upload[
"from"].str();
1613 if(prev_version.empty()) {
1614 prev_version = version_map.rbegin()->first;
1619 auto vm_entry = version_map.find(prev_version_parsed);
1620 if(vm_entry == version_map.end()) {
1621 prev_version = (--version_map.upper_bound(prev_version_parsed))->first;
1629 std::set<std::string> delete_packs;
1630 for(
const auto& pack : addon.
child_range(
"update_pack")) {
1631 if(pack[
"to"].str() == new_version) {
1632 const auto& pack_filename = pack[
"filename"].
str();
1634 delete_packs.insert(pack_filename);
1638 if(!delete_packs.empty()) {
1640 return delete_packs.find(p[
"filename"].str()) != delete_packs.end();
1644 const auto& update_pack_fn = make_update_pack_filename(prev_version, new_version);
1648 pack_info[
"from"] = prev_version;
1649 pack_info[
"to"] = new_version;
1651 pack_info[
"filename"] = update_pack_fn;
1656 LOG_CS <<
"Saving provided update pack for " << prev_version <<
" -> " << new_version <<
"...";
1660 static const config empty_config;
1662 writer.open_child(
"removelist");
1663 writer.write(have_wml(delta_remove) ? *delta_remove : empty_config);
1664 writer.close_child(
"removelist");
1666 writer.open_child(
"addlist");
1667 writer.write(have_wml(delta_add) ? *delta_add : empty_config);
1668 writer.close_child(
"addlist");
1678 auto it = version_map.find(prev_version_parsed);
1679 if(it == version_map.end()) {
1681 ERR_CS <<
"Previous version dropped off the version map?";
1687 rw_full_pack.
clear();
1690 if(have_wml(delta_remove)) {
1694 if(have_wml(delta_add)) {
1710 config version_cfg{
"version", new_version};
1711 version_cfg[
"filename"] = make_full_pack_filename(new_version);
1713 version_map.erase(new_version_parsed);
1716 return old_cfg[
"version"].str() == new_version;
1720 version_map.emplace(new_version_parsed, version_cfg);
1721 addon.
add_child(
"version", version_cfg);
1725 rw_full_pack[
"name"] =
"";
1729 const auto& full_pack_path = pathstem +
'/' + version_cfg[
"filename"].str();
1730 const auto& index_path = pathstem +
'/' + make_index_filename(new_version);
1733 config pack_index{
"name",
""};
1738 addon_pack_file.commit();
1742 addon_index_file.commit();
1749 std::set<std::string> expire_packs;
1752 if(upload_ts > pack[
"expire"].to_time_t() || pack[
"from"].str() == new_version || (!is_delta_upload && pack[
"to"].str() == new_version)) {
1753 LOG_CS <<
"Expiring upate pack for " << pack[
"from"].str() <<
" -> " << pack[
"to"].str();
1754 const auto& pack_filename = pack[
"filename"].str();
1756 expire_packs.insert(pack_filename);
1760 if(!expire_packs.empty()) {
1762 return expire_packs.find(p[
"filename"].str()) != expire_packs.end();
1769 for(
auto iter = version_map.begin(); std::distance(iter, version_map.end()) > 1;) {
1770 const config& prev_version = iter->second;
1771 const config& next_version = (++iter)->second;
1773 const auto& prev_version_name = prev_version[
"version"].str();
1774 const auto& next_version_name = next_version[
"version"].str();
1778 for(
const auto& pack : addon.
child_range(
"update_pack")) {
1779 if(pack[
"from"].str() == prev_version_name && pack[
"to"].str() == next_version_name) {
1790 LOG_CS <<
"Automatically generating update pack for " << prev_version_name <<
" -> " << next_version_name <<
"...";
1792 const auto& prev_path = pathstem +
'/' + prev_version[
"filename"].str();
1793 const auto& next_path = pathstem +
'/' + next_version[
"filename"].str();
1796 ERR_CS <<
"Unable to automatically generate an update pack for '" << name
1797 <<
"' for version " << prev_version_name <<
" to " << next_version_name
1802 const auto& update_pack_fn = make_update_pack_filename(prev_version_name, next_version_name);
1805 pack_info[
"from"] = prev_version_name;
1806 pack_info[
"to"] = next_version_name;
1808 pack_info[
"filename"] = update_pack_fn;
1831 LOG_CS << req <<
"Finished uploading add-on '" << upload[
"name"] <<
"'";
1835 fire(
"hook_post_upload", name);
1841 const std::string&
id = erase[
"name"].str();
1844 LOG_CS << req <<
"in read-only mode, request to delete '" <<
id <<
"' denied";
1845 send_error(
"Cannot delete add-on: The server is currently in read-only mode.", req.
sock);
1849 LOG_CS << req <<
"Deleting add-on '" <<
id <<
"'";
1866 if(!addon[
"forum_auth"].to_bool()) {
1867 if(!authenticate(addon, pass)) {
1878 if(addon[
"hidden"].to_bool()) {
1879 LOG_CS <<
"Add-on removal denied - hidden add-on.";
1880 send_error(
"Add-on deletion denied. Please contact the server administration for assistance.", req.
sock);
1894 LOG_CS <<
"in read-only mode, request to change passphrase denied";
1895 send_error(
"Cannot change passphrase: The server is currently in read-only mode.", req.
sock);
1903 }
else if(addon[
"forum_auth"].to_bool()) {
1904 send_error(
"Changing the password for add-ons using forum_auth is not supported.", req.
sock);
1905 }
else if(!authenticate(addon, cpass[
"passphrase"])) {
1907 }
else if(addon[
"hidden"].to_bool()) {
1908 LOG_CS <<
"Passphrase change denied - hidden add-on.";
1909 send_error(
"Add-on passphrase change denied. Please contact the server administration for assistance.", req.
sock);
1910 }
else if(cpass[
"new_passphrase"].empty()) {
1913 set_passphrase(addon, cpass[
"new_passphrase"]);
1925 std::string uploader = addon[
"uploader"].str();
1926 std::string
id = addon[
"name"].str();
1934 if((do_authors_exist && !is_primary && !is_secondary) || (is_secondary && is_delete)) {
1938 std::string author = addon[
"uploader"].str();
1940 std::string hashed_password =
hash_password(passphrase, salt, author);
1951 std::string config_file =
"server.cfg";
1952 unsigned short port = 0;
1958 for(
auto domain : {
"campaignd",
"campaignd/blacklist",
"server" }) {
1969 std::cout << cmdline.help_text();
1973 if(cmdline.version) {
1978 if(cmdline.config_file) {
1984 if(cmdline.server_dir) {
1989 port = *cmdline.port;
1994 PLAIN_LOG <<
"Invalid network port: " << port;
1999 if(cmdline.show_log_domains) {
2004 for(
const auto& ldl : cmdline.log_domain_levels) {
2006 PLAIN_LOG <<
"Unknown log domain: " << ldl.first;
2011 if(cmdline.log_precise_timestamps) {
2015 if(cmdline.report_timings) {
2016 campaignd::timing_reports_enabled =
true;
2022 PLAIN_LOG <<
"Server directory '" << *cmdline.server_dir <<
"' does not exist or is not a directory.";
2027 PLAIN_LOG <<
"Server configuration file '" << config_file <<
"' is not a file.";
2035 PLAIN_LOG <<
"Bad server directory '" << server_path <<
"'.";
2051 }
catch(
const boost::program_options::error&
e) {
2052 PLAIN_LOG <<
"Error in command line: " << e.what();
2055 PLAIN_LOG <<
"Could not parse config file";
2060 }
catch(
const std::bad_function_call& ) {
2061 PLAIN_LOG <<
"Bad request handler function call";
time_t update_pack_lifespan_
node & add_child(const char *name)
void copy_or_remove_attributes(const config &from, T... keys)
Copies or deletes attributes to match the source config.
campaignd authentication API.
bool empty() const
Tests for an attribute that either was never set or was set to "".
bool check_names_legal(const config &dir, std::vector< std::string > *badlist)
Scans an add-on archive for illegal names.
bool delete_directory(const std::string &dirname, const bool keep_pbl)
std::string feedback_url_format_
bool ignore_address_stats(const std::string &addr) const
Checks if the specified address should never bump download counts.
std::unique_ptr< simple_wml::document > coro_receive_doc(SocketPtr socket, boost::asio::yield_context yield)
Receive WML document from a coroutine.
config & child(config_key_type key, int n=0)
Returns the nth child with the given key, or a reference to an invalid config if there is none...
bool check_case_insensitive_duplicates(const config &dir, std::vector< std::string > *badlist)
Scans an add-on archive for case-conflicts.
The remote add-ons server does not support forum authorization.
void write(const config &cfg)
void handle_request_terms(const request &)
void clear_children(T... keys)
Invalid UTF-8 sequence in add-on name.
static const std::size_t default_document_size_limit
Default upload size limit in bytes.
Interfaces for manipulating version numbers of engine, add-ons, etc.
void append(const config &cfg)
Append data from another config object to this one.
bool delete_file(const std::string &filename)
void coro_send_file(socket_ptr socket, const std::string &filename, boost::asio::yield_context yield)
Send contents of entire file directly to socket from within a coroutine.
std::string join(const T &v, const std::string &s=",")
Generates a new string joining container items in a list.
void handle_flush(const boost::system::error_code &error)
void send_message(const std::string &msg, const any_socket_ptr &sock)
Send a client an informational message.
Variant for storing WML attributes.
config & get_addon(const std::string &id)
Retrieves an addon by id if found, or a null config otherwise.
std::unordered_set< std::string > dirty_addons_
The set of unique addon names with pending metadata updates.
boost::asio::signal_set sighup_
New lexcical_cast header.
No versions to deltify against.
bool has_attribute(config_key_type key) const
void handle_delete(const request &)
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
unsigned child_count(config_key_type key) const
Client request information object.
std::map< version_info, config > get_version_map(config &addon)
child_itors child_range(config_key_type key)
void load_config()
Reads the server configuration from WML.
void remove_attributes(T... keys)
Reports time elapsed at the end of an object scope.
filesystem::scoped_istream istream_file(const std::string &fname, bool treat_failure_as_error)
bool data_apply_removelist(config &data, const config &removelist)
Delta for a non-existent add-on.
bool wildcard_string_match(const std::string &str, const std::string &match)
Match using '*' as any number of characters (including none), '+' as one or more characters, and '?' as any one character.
void fire(const std::string &hook, const std::string &addon)
Fires a hook script.
void handle_request_campaign_hash(const request &)
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
request_handlers_table handlers_
std::string normalize_path(const std::string &fpath, bool normalize_separators, bool resolve_dot_entries)
Returns the absolute path of a file.
void write_hashlist(config &hashlist, const config &data)
Base class for implementing servers that use gzipped-WML network protocol.
void load_tls_config(const config &cfg)
void read_gz(config &cfg, std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially a gzip_error.
std::string addon_check_status_desc(unsigned int code)
Wrapper class that guarantees that file commit atomicity.
The addon's forum_auth value does not match its previously set value.
std::map< std::string, std::string > hooks_
void load_blacklist()
Reads the add-ons upload blacklist from WML.
void data_apply_addlist(config &data, const config &addlist)
static lg::log_domain log_config("config")
void register_handlers()
Registers client request handlers.
No description specified.
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.
utils::variant< socket_ptr, tls_socket_ptr > any_socket_ptr
const std::string cfg_file_
Invalid UTF-8 sequence in add-on metadata.
unsigned in
If equal to search_counter, the node is off the list.
const_all_children_iterator ordered_end() const
void write(std::ostream &out, const configr_of &cfg, unsigned int level)
std::pair< std::string, std::string > generate_hash(const std::string &passphrase)
Generates a salted hash from the specified passphrase.
void handle_sighup(const boost::system::error_code &error, int signal_number)
boost::asio::basic_waitable_timer< std::chrono::steady_clock > flush_timer_
std::string client_address(const any_socket_ptr &sock)
A class to handle the non-SQL logic for connecting to the phpbb forum database.
boost::asio::streambuf admin_cmd_
int main(int argc, char **argv)
std::string label
What to show in the filter's drop-down list.
std::string blacklist_file_
void read(config &cfg, std::istream &in, abstract_validator *validator)
Class for writing a config out to a file in pieces.
void get_files_in_dir(const std::string &dir, std::vector< std::string > *files, std::vector< std::string > *dirs, name_mode mode, filter_mode filter, reorder_mode reorder, file_tree_checksum *checksum)
Get a list of all files and/or directories in a given directory.
void commit()
Commits the new file contents to disk atomically.
const child_map::key_type & key
boost::asio::io_service io_service_
void async_send_doc_queued(SocketPtr socket, simple_wml::document &doc)
High level wrapper for sending a WML document.
void erase(const std::string &key)
bool is_directory(const std::string &fname)
Returns true if the given file is a directory.
An interface class to handle nick registration To activate it put a [user_handler] section into the s...
std::unique_ptr< std::istream > scoped_istream
void handle_read_from_fifo(const boost::system::error_code &error, std::size_t bytes_transferred)
void delete_addon(const std::string &id)
Server read-only mode on.
scoped_ostream & ostream()
Returns the write stream associated with the file.
utils::optional_reference< config > optional_child(config_key_type key, int n=0)
Euivalent to child, but returns an empty optional if the nth child was not found. ...
void handle_change_passphrase(const request &)
const char * what() const noexcept
int run_campaignd(int argc, char **argv)
#define REGISTER_CAMPAIGND_HANDLER(req_id)
ADDON_TYPE get_addon_type(const std::string &str)
static std::size_t document_size_limit
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...
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.
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 ...
bool addon_name_legal(const std::string &name)
Checks whether an add-on id/name is legal or not.
boost::asio::yield_context yield
context of the coroutine the request is executed in async operations on sock can use it instead of a ...
std::size_t args_count() const
Returns the total number of arguments, not including the command itself.
The provided topic ID for the addon's feedback forum thread wasn't found in the forum database...
Unspecified server error.
static config & get_invalid()
void write_config()
Writes the server configuration WML back to disk.
void handle_new_client(socket_ptr socket)
Thrown by operations encountering invalid UTF-8 data.
virtual std::string hex_digest() const override
const config & server_info() const
Retrieves the contents of the [server_info] WML node.
const std::string revision
node & set_attr_dup(const char *key, const char *value)
Atomic filesystem commit functions.
Represents a server control line written to a communication socket.
static lg::log_domain log_campaignd("campaignd")
void handle_request_campaign(const request &)
An exception object used when an IO error occurs.
void coro_send_doc(SocketPtr socket, simple_wml::document &doc, boost::asio::yield_context yield)
Send a WML document from within a coroutine.
std::vector< std::string > names
void send_error(const std::string &msg, const any_socket_ptr &sock)
Send a client an error message.
void handle_server_id(const request &)
bool string_bool(const std::string &str, bool def)
Convert no, false, off, 0, 0.0 to false, empty to def, and others to true.
Declarations for File-IO.
std::string lowercase(const std::string &s)
Returns a lowercased version of the string.
boost::asio::posix::stream_descriptor input_
bool set_log_domain_severity(const std::string &name, int severity)
Represents version numbers.
config & add_child(config_key_type key)
std::vector< std::string > stats_exempt_ips_
Requested forum authentication for a user that doesn't exist on the forums.
friend std::ostream & operator<<(std::ostream &o, const request &r)
int file_size(const std::string &fname)
Returns the size of a file, or -1 if the file doesn't exist.
Corrupted server add-ons list.
const_all_children_iterator ordered_begin() const
bool set_cwd(const std::string &dir)
void handle_request_campaign_list(const request &)
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_CHECK_STATUS validate_addon(const server::request &req, config *&existing_addon, std::string &error_data)
Performs validation on an incoming add-on.
std::vector< std::string > split(const config_attribute_value &val)
const unsigned short default_campaignd_port
Default port number for the addon server.
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::string list_logdomains(const std::string &filter)
std::shared_ptr< boost::asio::ssl::stream< socket_ptr::element_type > > tls_socket_ptr
const std::string & cmd() const
Returns the control command.
void flush_cfg()
Starts timer to write config to disk every ten minutes.
campaignd command line options parsing.
Standard logging facilities (interface).
The provided topic ID for the addon's feedback forum thread is invalid.
std::string str() const
Serializes the version number into string form.
void read(const config &cfg)
Initializes the blacklist from WML.
std::shared_ptr< boost::asio::ip::tcp::socket > socket_ptr
std::string full() const
Return the full command line string.
bool is_text_markup_char(char c)
config cfg_
Server config.
void find_translations(const config &base_dir, config &addon)
Scans an add-on archive directory for translations.
void remove_children(config_key_type key, std::function< bool(const config &)> p=[](config){return true;})
Removes all children with tag key for which p returns true.
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.
std::unordered_map< std::string, config > addons_
The hash map of addons metadata.
std::set< std::string > capabilities_
A config object defines a single node in a WML file, with access to child nodes.
server(const std::string &cfg_file, unsigned short port=0)
std::string format_addon_feedback_url(const std::string &format, const config ¶ms)
Format a feedback URL for an add-on.
int get_severity() const
Returns following values depending on the logger: error: 0 warn: 1 info: 2 debug: 3 See also the lg::...
std::map< std::string, addon_info > addons_list
static lg::log_domain log_server("server")
void handle_upload(const request &)
void serve_requests(Socket socket, boost::asio::yield_context yield)
A simple wrapper class for optional reference types.
void mark_dirty(const std::string &addon)
std::string debug() const
void precise_timestamps(bool pt)
std::string license_notice_
Version number is not an increment.
std::unique_ptr< user_handler > user_handler_
int compress_level_
Used for add-on archives.