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 utils::optional<std::vector<std::string>> multi_find_illegal_names(
const Vals&... args)
236 std::vector<std::string>
names;
239 return !
names.empty() ? utils::optional(
names) : utils::nullopt;
247 template<
typename... Vals>
248 utils::optional<std::vector<std::string>> multi_find_case_conflicts(
const Vals&... args)
250 std::vector<std::string>
names;
253 return !
names.empty() ? utils::optional(
names) : utils::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";
348 constexpr std::chrono::seconds seconds_in_a_month{30 * 24 * 60 * 60};
355 web_url_ = svinfo_cfg[
"web_url"].str(default_web_url);
356 license_notice_ = svinfo_cfg[
"license_notice"].str(default_license_notice);
364 hooks_.emplace(std::string(
"hook_post_upload"),
cfg_[
"hook_post_upload"]);
365 hooks_.emplace(std::string(
"hook_post_erase"),
cfg_[
"hook_post_erase"]);
369 if(!
cfg_[
"control_socket"].empty()) {
370 const std::string&
path =
cfg_[
"control_socket"].str();
373 const int res = mkfifo(
path.c_str(),0660);
374 if(res != 0 && errno != EEXIST) {
375 ERR_CS <<
"could not make fifo at '" <<
path <<
"' (" << strerror(errno) <<
")";
378 int fifo = open(
path.c_str(), O_RDWR|O_NONBLOCK);
380 LOG_CS <<
"opened fifo at '" <<
path <<
"'. Server commands may be written to this file.";
404 std::vector<std::string> legacy_addons, dirs;
407 for(
const std::string& addon_dir : dirs) {
411 addons_.emplace(meta[
"name"].str(), meta);
420 WRN_CS <<
"Old format addons have been detected in the config! They will be converted to the new file format! "
421 << campaigns.
child_count(
"campaign") <<
" entries to be processed.";
423 const std::string& addon_id = campaign[
"name"].str();
424 const std::string& addon_file = campaign[
"filename"].str();
427 +
"' already exists in the new form! Possible code or filesystem interference!\n");
429 if(std::find(legacy_addons.begin(), legacy_addons.end(), addon_id) == legacy_addons.end()) {
431 +
"'. 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 <<
"] ";
495 #if BOOST_VERSION >= 108000
496 , [](std::exception_ptr
e) {
if (
e) std::rethrow_exception(
e); }
505 #if BOOST_VERSION >= 108000
506 , [](std::exception_ptr
e) {
if (
e) std::rethrow_exception(
e); }
511 template<
class Socket>
517 socket->lowest_layer().close();
526 if(
i !=
data.ordered_end()) {
530 request_handlers_table::const_iterator j
535 request req{
c.key,
c.cfg, socket, yield};
536 auto st = service_timer(req);
537 j->second(
this, req);
539 send_error(
"Unrecognized [" +
c.key +
"] request.",socket);
550 if(error == boost::asio::error::operation_aborted)
553 ERR_CS <<
"Error reading from fifo: " << error.message();
559 std::getline(is, cmd);
563 if(ctl ==
"shut_down") {
564 LOG_CS <<
"Shut down requested by admin, shutting down...";
566 }
else if(ctl ==
"readonly") {
572 }
else if(ctl ==
"flush") {
573 LOG_CS <<
"Flushing config to disk...";
575 }
else if(ctl ==
"reload") {
577 if(ctl[1] ==
"blacklist") {
578 LOG_CS <<
"Reloading blacklist...";
581 ERR_CS <<
"Unrecognized admin reload argument: " << ctl[1];
584 LOG_CS <<
"Reloading all configuration...";
586 LOG_CS <<
"Reloaded configuration";
588 }
else if(ctl ==
"delete") {
590 ERR_CS <<
"Incorrect number of arguments for 'delete'";
592 const std::string& addon_id = ctl[1];
594 LOG_CS <<
"deleting add-on '" << addon_id <<
"' requested from control FIFO";
597 }
else if(ctl ==
"hide" || ctl ==
"unhide") {
599 ERR_CS <<
"Incorrect number of arguments for '" << ctl.
cmd() <<
"'";
601 const std::string& addon_id = ctl[1];
605 ERR_CS <<
"Add-on '" << addon_id <<
"' not found, cannot " << ctl.
cmd();
607 addon[
"hidden"] = (ctl.
cmd() ==
"hide");
610 LOG_CS <<
"Add-on '" << addon_id <<
"' is now " << (ctl.
cmd() ==
"hide" ?
"hidden" :
"unhidden");
613 }
else if(ctl ==
"setpass") {
615 ERR_CS <<
"Incorrect number of arguments for 'setpass'";
617 const std::string& addon_id = ctl[1];
618 const std::string& newpass = ctl[2];
622 ERR_CS <<
"Add-on '" << addon_id <<
"' not found, cannot set passphrase";
623 }
else if(newpass.empty()) {
625 ERR_CS <<
"Add-on passphrases may not be empty!";
626 }
else if(addon[
"forum_auth"].to_bool()) {
627 ERR_CS <<
"Can't set passphrase for add-on using forum_auth.";
629 set_passphrase(*addon, newpass);
632 LOG_CS <<
"New passphrase set for '" << addon_id <<
"'";
635 }
else if(ctl ==
"setattr") {
637 ERR_CS <<
"Incorrect number of arguments for 'setattr'";
639 const std::string& addon_id = ctl[1];
640 const std::string& key = ctl[2];
652 ERR_CS <<
"Add-on '" << addon_id <<
"' not found, cannot set attribute";
653 }
else if(key ==
"name" || key ==
"version") {
654 ERR_CS <<
"setattr cannot be used to rename add-ons or change their version";
655 }
else if(key ==
"passhash"|| key ==
"passsalt") {
656 ERR_CS <<
"setattr cannot be used to set auth data -- use setpass instead";
657 }
else if(!addon->has_attribute(key)) {
664 ERR_CS <<
"Attribute '" << key <<
"' is not a recognized add-on attribute";
669 LOG_CS <<
"Set attribute on add-on '" << addon_id <<
"':\n"
670 << key <<
"=\"" << value <<
"\"";
673 }
else if(ctl ==
"log") {
674 static const std::map<std::string, lg::severity> log_levels = {
683 ERR_CS <<
"Incorrect number of arguments for 'log'";
684 }
else if(ctl[1] ==
"precise") {
687 LOG_CS <<
"Precise timestamps enabled";
688 }
else if(ctl[2] ==
"off") {
690 LOG_CS <<
"Precise timestamps disabled";
692 ERR_CS <<
"Invalid argument for 'log precise': " << ctl[2];
694 }
else if(log_levels.find(ctl[1]) == log_levels.end()) {
695 ERR_CS <<
"Invalid log level '" << ctl[1] <<
"'";
697 auto sev = log_levels.find(ctl[1])->second;
700 ERR_CS <<
"Unknown log domain '" << domain <<
"'";
702 LOG_CS <<
"Set log level for domain '" << domain <<
"' to " << ctl[1];
706 }
else if(ctl ==
"timings") {
708 ERR_CS <<
"Incorrect number of arguments for 'timings'";
709 }
else if(ctl[1] ==
"on") {
710 campaignd::timing_reports_enabled =
true;
711 LOG_CS <<
"Request servicing timing reports enabled";
712 }
else if(ctl[1] ==
"off") {
713 campaignd::timing_reports_enabled =
false;
714 LOG_CS <<
"Request servicing timing reports disabled";
716 ERR_CS <<
"Invalid argument for 'timings': " << ctl[1];
719 ERR_CS <<
"Unrecognized admin command: " << ctl.
full();
727 LOG_CS <<
"SIGHUP caught, reloading config.";
731 LOG_CS <<
"Reloaded configuration";
740 flush_timer_.expires_from_now(std::chrono::minutes(10));
747 ERR_CS <<
"Error from reload timer: " << error.message();
748 throw boost::system::system_error(error);
779 DBG_CS <<
"writing configuration and add-ons list to disk...";
786 if(addon && !addon[
"filename"].empty()) {
797 void server::fire(
const std::string& hook, [[maybe_unused]]
const std::string& addon)
799 const std::map<std::string, std::string>::const_iterator itor =
hooks_.find(hook);
800 if(itor ==
hooks_.end()) {
804 const std::string& script = itor->second;
810 ERR_CS <<
"Tried to execute a script on an unsupported platform";
815 if((childpid = fork()) == -1) {
816 ERR_CS <<
"fork failed while updating add-on " << addon;
824 execlp(script.c_str(), script.c_str(), addon.c_str(),
static_cast<char *
>(
nullptr));
827 PLAIN_LOG <<
"ERROR: exec failed with errno " << errno <<
" for addon " << addon;
850 const auto& escaped_msg = simple_wml_escape(
msg);
863 const auto& escaped_msg = simple_wml_escape(
msg);
871 const std::string& status_hex =
formatter()
872 <<
"0x" << std::setfill(
'0') << std::setw(2*
sizeof(
unsigned int)) << std::hex
873 << std::uppercase << status_code;
876 const auto& escaped_status_str = simple_wml_escape(std::to_string(status_code));
877 const auto& escaped_msg = simple_wml_escape(
msg);
878 const auto& escaped_extra_data = simple_wml_escape(extra_data);
884 err_cfg.
set_attr_dup(
"extra_data", escaped_extra_data.c_str());
885 err_cfg.
set_attr_dup(
"status_code", escaped_status_str.c_str());
894 return addon->second;
905 ERR_CS <<
"Cannot delete unrecognized add-on '" <<
id <<
"'";
909 if(cfg[
"forum_auth"].to_bool()) {
913 std::string fn = cfg[
"filename"].str();
916 ERR_CS <<
"Add-on '" <<
id <<
"' does not have an associated filename, cannot delete";
920 ERR_CS <<
"Could not delete the directory for addon '" <<
id
921 <<
"' (" << fn <<
"): " << strerror(errno);
927 fire(
"hook_post_erase",
id);
929 LOG_CS <<
"Deleted add-on '" <<
id <<
"'";
932 #define REGISTER_CAMPAIGND_HANDLER(req_id) \
933 handlers_[#req_id] = std::bind(&server::handle_##req_id, \
934 std::placeholders::_1, std::placeholders::_2)
950 DBG_CS << req <<
"Sending server identification";
952 std::ostringstream ostr;
961 const auto& wml = ostr.str();
970 LOG_CS << req <<
"Sending add-ons list";
972 auto now = std::chrono::system_clock::now();
973 const bool relative_to_now = req.
cfg[
"times_relative_to"] ==
"now";
978 bool before_flag = !req.
cfg[
"before"].
empty();
979 std::chrono::system_clock::time_point before;
981 if(relative_to_now) {
982 auto time_delta = chrono::parse_duration<std::chrono::seconds>(req.
cfg[
"before"]);
983 before = now + time_delta;
989 bool after_flag = !req.
cfg[
"after"].
empty();
990 std::chrono::system_clock::time_point after;
992 if(relative_to_now) {
993 auto time_delta = chrono::parse_duration<std::chrono::seconds>(req.
cfg[
"after"]);
994 after = now + time_delta;
1000 const std::string& name = req.
cfg[
"name"];
1001 const std::string& lang = req.
cfg[
"language"];
1003 for(
const auto& addon :
addons_)
1005 if(!name.empty() && name != addon.first) {
1011 if(
i[
"hidden"].to_bool()) {
1015 const auto& tm =
i[
"timestamp"];
1027 for(
const config& j :
i.child_range(
"translation"))
1029 if(j[
"language"] == lang && j[
"supported"].to_bool(
true)) {
1047 j.remove_attributes(
"passphrase",
"passhash",
"passsalt",
"upload_ip",
"email");
1057 j.clear_children(
"feedback");
1060 j.clear_children(
"update_pack");
1066 std::ostringstream ostr;
1067 write(ostr, response);
1068 std::string wml = ostr.str();
1079 if(!addon || addon[
"hidden"].to_bool()) {
1084 const auto& name = req.
cfg[
"name"].str();
1087 if(version_map.empty()) {
1088 send_error(
"No versions of the add-on '" + name +
"' are available on the server.", req.
sock);
1093 const auto& from = req.
cfg[
"from_version"].str();
1094 const auto& to = req.
cfg[
"version"].str(version_map.rbegin()->first);
1099 auto to_version_iter = version_map.find(to_parsed);
1100 if(to_version_iter == version_map.end()) {
1101 send_error(
"Could not find requested version " + to +
" of the addon '" + name +
1106 auto full_pack_path = addon[
"filename"].str() +
'/' + to_version_iter->second[
"filename"].str();
1115 if(!from.empty() && from_parsed < to_parsed && version_map.count(from_parsed) != 0) {
1124 int delivery_size = 0;
1125 bool force_use_full =
false;
1127 auto start_point = version_map.find(from_parsed);
1128 auto end_point = std::next(to_version_iter, 1);
1130 if(std::distance(start_point, end_point) <= 1) {
1132 ERR_CS <<
"Bad update sequence bounds in version " << from <<
" -> " << to <<
" update sequence for the add-on '" << name <<
"', sending a full pack instead";
1133 force_use_full =
true;
1136 for(
auto iter = start_point; !force_use_full && std::distance(iter, end_point) > 1;) {
1137 const auto& prev_version_cfg = iter->second;
1138 const auto& next_version_cfg = (++iter)->second;
1141 if(pack[
"from"].str() != prev_version_cfg[
"version"].str() ||
1142 pack[
"to"].str() != next_version_cfg[
"version"].str()) {
1147 const auto& update_pack_path = addon[
"filename"].str() +
'/' + pack[
"filename"].str();
1152 if(!step_delta.
empty()) {
1154 delta.
append(std::move(step_delta));
1157 ERR_CS <<
"Broken update sequence from version " << from <<
" to "
1158 << to <<
" for the add-on '" << name <<
"', sending a full pack instead";
1159 force_use_full =
true;
1167 if(delivery_size > full_pack_size && full_pack_size > 0) {
1168 force_use_full =
true;
1174 if(!force_use_full && !delta.
empty()) {
1175 std::ostringstream ostr;
1177 const auto& wml_text = ostr.str();
1182 LOG_CS << req <<
"Sending add-on '" << name <<
"' version: " << from <<
" -> " << to <<
" (delta)";
1184 utils::visit([
this, &req, &doc](
auto && sock) {
1188 full_pack_path.clear();
1195 if(!full_pack_path.empty()) {
1196 if(full_pack_size < 0) {
1197 send_error(
"Add-on '" + name +
"' could not be read by the server.", req.
sock);
1201 LOG_CS << req <<
"Sending add-on '" << name <<
"' version: " << to <<
" size: " << full_pack_size / 1024 <<
" KiB";
1202 utils::visit([
this, &req, &full_pack_path](
auto&& socket) {
1211 addon[
"downloads"] = 1 + addon[
"downloads"].to_int();
1223 if(!addon || addon[
"hidden"].to_bool()) {
1228 std::string
path = addon[
"filename"].str() +
'/';
1232 if(version_map.empty()) {
1233 send_error(
"No versions of the add-on '" + req.
cfg[
"name"].str() +
"' are available on the server.", req.
sock);
1236 const auto& version_str = addon[
"version"].str();
1238 auto version = version_map.find(version_parsed);
1239 if(version != version_map.end()) {
1240 path += version->second[
"filename"].str();
1243 if(version_str.empty()) {
1244 path += version_map.rbegin()->second[
"filename"].str();
1246 path += (--version_map.upper_bound(version_parsed))->second[
"filename"].str();
1250 path = index_from_full_pack_filename(
path);
1254 send_error(
"Missing index file for the add-on '" + req.
cfg[
"name"].str() +
"'.", req.
sock);
1258 LOG_CS << req <<
"Sending add-on hash index for '" << req.
cfg[
"name"] <<
"' size: " <<
file_size / 1024 <<
" KiB";
1259 utils::visit([
this, &
path, &req](
auto&& socket) {
1270 LOG_CS <<
"in read-only mode, request for upload terms denied";
1271 send_error(
"The server is currently in read-only mode, add-on uploads are disabled.", req.
sock);
1275 LOG_CS << req <<
"Sending license terms";
1282 LOG_CS <<
"Validation error: uploads not permitted in read-only mode.";
1292 const bool is_upload_pack = have_wml(removelist) || have_wml(addlist);
1294 const std::string& name = upload[
"name"].str();
1296 existing_addon =
nullptr;
1299 bool passed_name_utf8_check =
false;
1303 passed_name_utf8_check =
true;
1307 existing_addon = &
c.second;
1312 if(!passed_name_utf8_check) {
1313 LOG_CS <<
"Validation error: bad UTF-8 in add-on name";
1316 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";
1323 if(upload[
"passphrase"].empty()) {
1324 LOG_CS <<
"Validation error: no passphrase specified";
1328 if(existing_addon && upload[
"forum_auth"].to_bool() != (*existing_addon)[
"forum_auth"].to_bool()) {
1329 LOG_CS <<
"Validation error: forum_auth is " << upload[
"forum_auth"].to_bool() <<
" but was previously uploaded set to " << (*existing_addon)[
"forum_auth"].to_bool();
1331 }
else if(upload[
"forum_auth"].to_bool()) {
1333 LOG_CS <<
"Validation error: client requested forum authentication but server does not support it";
1337 LOG_CS <<
"Validation error: forum auth requested for an author who doesn't exist";
1341 for(
const std::string& secondary_author :
utils::split(upload[
"secondary_authors"].str(),
',')) {
1343 LOG_CS <<
"Validation error: forum auth requested for a secondary author who doesn't exist";
1349 LOG_CS <<
"Validation error: forum passphrase does not match";
1353 }
else if(existing_addon && !authenticate(*existing_addon, upload[
"passphrase"])) {
1354 LOG_CS <<
"Validation error: campaignd passphrase does not match";
1358 if(existing_addon && (*existing_addon)[
"hidden"].to_bool()) {
1359 LOG_CS <<
"Validation error: add-on is hidden";
1365 upload[
"title"].str(),
1366 upload[
"description"].str(),
1367 upload[
"author"].str(),
1369 upload[
"email"].str()))
1371 LOG_CS <<
"Validation error: blacklisted uploader or publish information";
1375 LOG_CS <<
"Validation error: invalid UTF-8 sequence in publish information while checking against the blacklist";
1381 if(!is_upload_pack && !have_wml(
data)) {
1382 LOG_CS <<
"Validation error: no add-on data.";
1386 if(is_upload_pack && !have_wml(removelist) && !have_wml(addlist)) {
1387 LOG_CS <<
"Validation error: no add-on data.";
1392 LOG_CS <<
"Validation error: invalid add-on name.";
1397 LOG_CS <<
"Validation error: add-on name starts with an illegal formatting character.";
1401 if(upload[
"title"].empty()) {
1402 LOG_CS <<
"Validation error: no add-on title specified";
1407 LOG_CS <<
"Validation error: add-on title starts with an illegal formatting character.";
1412 LOG_CS <<
"Validation error: unknown add-on type specified";
1416 if(upload[
"author"].empty()) {
1417 LOG_CS <<
"Validation error: no add-on author specified";
1421 if(upload[
"version"].empty()) {
1422 LOG_CS <<
"Validation error: no add-on version specified";
1426 if(existing_addon) {
1430 if(
strict_versions_ ? new_version <= old_version : new_version < old_version) {
1431 LOG_CS <<
"Validation error: add-on version not incremented";
1436 if(upload[
"description"].empty()) {
1437 LOG_CS <<
"Validation error: no add-on description specified";
1442 if(upload[
"email"].empty() && !upload[
"forum_auth"].to_bool()) {
1443 LOG_CS <<
"Validation error: no add-on email specified";
1447 if(
const auto badnames = multi_find_illegal_names(
data, addlist, removelist)) {
1449 LOG_CS <<
"Validation error: invalid filenames in add-on pack (" << badnames->size() <<
" entries)";
1453 if(
const auto badnames = multi_find_case_conflicts(
data, addlist, removelist)) {
1455 LOG_CS <<
"Validation error: case conflicts in add-on pack (" << badnames->size() <<
" entries)";
1459 if(is_upload_pack && !existing_addon) {
1460 LOG_CS <<
"Validation error: attempted to send an update pack for a non-existent add-on";
1466 int topic_id = std::stoi(url_params[
"topic_id"].str(
"0"));
1469 LOG_CS <<
"Validation error: feedback topic ID does not exist in forum database";
1474 LOG_CS <<
"Validation error: feedback topic ID is not a valid number";
1484 const auto upload_ts = std::chrono::system_clock::now();
1486 const auto& name = upload[
"name"].str();
1488 LOG_CS << req <<
"Validating add-on '" << name <<
"'...";
1490 config* addon_ptr =
nullptr;
1491 std::string val_error_data;
1492 const auto val_status =
validate_addon(req, addon_ptr, val_error_data);
1495 LOG_CS <<
"Upload of '" << name <<
"' aborted due to a failed validation check";
1497 send_error(
msg, val_error_data,
static_cast<unsigned int>(val_status), req.
sock);
1501 LOG_CS << req <<
"Processing add-on '" << name <<
"'...";
1507 const bool is_delta_upload = have_wml(delta_remove) || have_wml(delta_add);
1508 const bool is_existing_upload = addon_ptr !=
nullptr;
1510 if(!is_existing_upload) {
1513 addon_ptr = &(*entry.first).second;
1516 config& addon = *addon_ptr;
1518 LOG_CS << req <<
"Upload type: "
1519 << (is_delta_upload ?
"delta" :
"full") <<
", "
1520 << (is_existing_upload ?
"update" :
"new");
1525 "title",
"name",
"uploader",
"author",
"secondary_authors",
"description",
"version",
"icon",
1526 "translate",
"dependencies",
"core",
"type",
"tags",
"email",
"forum_auth"
1529 const std::string& pathstem =
"data/" + name;
1530 addon[
"filename"] = pathstem;
1531 addon[
"upload_ip"] = req.
addr;
1533 if(!is_existing_upload && !addon[
"forum_auth"].to_bool()) {
1534 set_passphrase(addon, upload[
"passphrase"]);
1537 if(addon[
"downloads"].empty()) {
1538 addon[
"downloads"] = 0;
1542 addon[
"uploads"] = 1 + addon[
"uploads"].to_int();
1547 addon.
add_child(
"feedback", *url_params);
1549 topic_id = url_params[
"topic_id"].to_int();
1553 if(addon[
"forum_auth"].to_bool()) {
1554 addon[
"email"] =
user_handler_->get_user_email(upload[
"uploader"].str());
1560 if(!do_authors_exist || is_primary) {
1570 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());
1581 if(!locale_params[
"language"].empty()) {
1583 locale[
"language"] = locale_params[
"language"].str();
1584 locale[
"supported"] =
false;
1586 if(!locale_params[
"title"].empty()) {
1587 locale[
"title"] = locale_params[
"title"].str();
1589 if(!locale_params[
"description"].empty()) {
1590 locale[
"description"] = locale_params[
"description"].str();
1601 if(have_wml(full_pack)) {
1603 rw_full_pack = std::move(
const_cast<config&
>(*full_pack));
1608 const auto& new_version = addon[
"version"].str();
1611 if(is_delta_upload) {
1616 if(version_map.empty()) {
1618 ERR_CS <<
"Add-on '" << name <<
"' has an empty version table, this should not happen";
1623 auto prev_version = upload[
"from"].str();
1625 if(prev_version.empty()) {
1626 prev_version = version_map.rbegin()->first;
1631 auto vm_entry = version_map.find(prev_version_parsed);
1632 if(vm_entry == version_map.end()) {
1633 prev_version = (--version_map.upper_bound(prev_version_parsed))->first;
1641 std::set<std::string> delete_packs;
1642 for(
const auto& pack : addon.
child_range(
"update_pack")) {
1643 if(pack[
"to"].str() == new_version) {
1644 const auto& pack_filename = pack[
"filename"].
str();
1646 delete_packs.insert(pack_filename);
1650 if(!delete_packs.empty()) {
1652 return delete_packs.find(
p[
"filename"].str()) != delete_packs.end();
1656 const auto& update_pack_fn = make_update_pack_filename(prev_version, new_version);
1660 pack_info[
"from"] = prev_version;
1661 pack_info[
"to"] = new_version;
1663 pack_info[
"filename"] = update_pack_fn;
1668 LOG_CS <<
"Saving provided update pack for " << prev_version <<
" -> " << new_version <<
"...";
1672 static const config empty_config;
1674 writer.open_child(
"removelist");
1675 writer.write(have_wml(delta_remove) ? *delta_remove : empty_config);
1676 writer.close_child(
"removelist");
1678 writer.open_child(
"addlist");
1679 writer.write(have_wml(delta_add) ? *delta_add : empty_config);
1680 writer.close_child(
"addlist");
1690 auto it = version_map.find(prev_version_parsed);
1691 if(it == version_map.end()) {
1693 ERR_CS <<
"Previous version dropped off the version map?";
1699 rw_full_pack.
clear();
1702 if(have_wml(delta_remove)) {
1706 if(have_wml(delta_add)) {
1722 config version_cfg{
"version", new_version};
1723 version_cfg[
"filename"] = make_full_pack_filename(new_version);
1725 version_map.erase(new_version_parsed);
1728 return old_cfg[
"version"].str() == new_version;
1732 version_map.emplace(new_version_parsed, version_cfg);
1733 addon.
add_child(
"version", version_cfg);
1737 rw_full_pack[
"name"] =
"";
1741 const auto& full_pack_path = pathstem +
'/' + version_cfg[
"filename"].str();
1742 const auto& index_path = pathstem +
'/' + make_index_filename(new_version);
1745 config pack_index{
"name",
""};
1750 addon_pack_file.commit();
1754 addon_index_file.commit();
1761 std::set<std::string> expire_packs;
1764 if(upload_ts >
chrono::parse_timestamp(pack[
"expire"]) || pack[
"from"].str() == new_version || (!is_delta_upload && pack[
"to"].str() == new_version)) {
1765 LOG_CS <<
"Expiring upate pack for " << pack[
"from"].str() <<
" -> " << pack[
"to"].str();
1766 const auto& pack_filename = pack[
"filename"].str();
1768 expire_packs.insert(pack_filename);
1772 if(!expire_packs.empty()) {
1774 return expire_packs.find(
p[
"filename"].str()) != expire_packs.end();
1781 for(
auto iter = version_map.begin(); std::distance(iter, version_map.end()) > 1;) {
1782 const config& prev_version = iter->second;
1783 const config& next_version = (++iter)->second;
1785 const auto& prev_version_name = prev_version[
"version"].str();
1786 const auto& next_version_name = next_version[
"version"].str();
1790 for(
const auto& pack : addon.
child_range(
"update_pack")) {
1791 if(pack[
"from"].str() == prev_version_name && pack[
"to"].str() == next_version_name) {
1802 LOG_CS <<
"Automatically generating update pack for " << prev_version_name <<
" -> " << next_version_name <<
"...";
1804 const auto& prev_path = pathstem +
'/' + prev_version[
"filename"].str();
1805 const auto& next_path = pathstem +
'/' + next_version[
"filename"].str();
1808 ERR_CS <<
"Unable to automatically generate an update pack for '" << name
1809 <<
"' for version " << prev_version_name <<
" to " << next_version_name
1814 const auto& update_pack_fn = make_update_pack_filename(prev_version_name, next_version_name);
1817 pack_info[
"from"] = prev_version_name;
1818 pack_info[
"to"] = next_version_name;
1820 pack_info[
"filename"] = update_pack_fn;
1843 LOG_CS << req <<
"Finished uploading add-on '" << upload[
"name"] <<
"'";
1847 fire(
"hook_post_upload", name);
1853 const std::string&
id =
erase[
"name"].str();
1856 LOG_CS << req <<
"in read-only mode, request to delete '" <<
id <<
"' denied";
1857 send_error(
"Cannot delete add-on: The server is currently in read-only mode.", req.
sock);
1861 LOG_CS << req <<
"Deleting add-on '" <<
id <<
"'";
1878 if(!addon[
"forum_auth"].to_bool()) {
1879 if(!authenticate(*addon, pass)) {
1890 if(addon[
"hidden"].to_bool()) {
1891 LOG_CS <<
"Add-on removal denied - hidden add-on.";
1892 send_error(
"Add-on deletion denied. Please contact the server administration for assistance.", req.
sock);
1906 LOG_CS <<
"in read-only mode, request to change passphrase denied";
1907 send_error(
"Cannot change passphrase: The server is currently in read-only mode.", req.
sock);
1915 }
else if(addon[
"forum_auth"].to_bool()) {
1916 send_error(
"Changing the password for add-ons using forum_auth is not supported.", req.
sock);
1917 }
else if(!authenticate(*addon, cpass[
"passphrase"])) {
1919 }
else if(addon[
"hidden"].to_bool()) {
1920 LOG_CS <<
"Passphrase change denied - hidden add-on.";
1921 send_error(
"Add-on passphrase change denied. Please contact the server administration for assistance.", req.
sock);
1922 }
else if(cpass[
"new_passphrase"].empty()) {
1925 set_passphrase(*addon, cpass[
"new_passphrase"]);
1937 std::string uploader = addon[
"uploader"].str();
1938 std::string
id = addon[
"name"].str();
1946 if((do_authors_exist && !is_primary && !is_secondary) || (is_secondary && is_delete)) {
1950 std::string author = addon[
"uploader"].str();
1952 std::string hashed_password =
hash_password(passphrase, salt, author);
1963 std::string config_file =
"server.cfg";
1964 unsigned short port = 0;
1970 for(
auto domain : {
"campaignd",
"campaignd/blacklist",
"server" }) {
1981 std::cout << cmdline.help_text();
1985 if(cmdline.version) {
1990 if(cmdline.config_file) {
1996 if(cmdline.server_dir) {
2001 port = *cmdline.port;
2006 PLAIN_LOG <<
"Invalid network port: " << port;
2011 if(cmdline.show_log_domains) {
2016 for(
const auto& ldl : cmdline.log_domain_levels) {
2018 PLAIN_LOG <<
"Unknown log domain: " << ldl.first;
2023 if(cmdline.log_precise_timestamps) {
2027 if(cmdline.report_timings) {
2028 campaignd::timing_reports_enabled =
true;
2034 PLAIN_LOG <<
"Server directory '" << *cmdline.server_dir <<
"' does not exist or is not a directory.";
2039 PLAIN_LOG <<
"Server configuration file '" << config_file <<
"' is not a file.";
2047 PLAIN_LOG <<
"Bad server directory '" << server_path <<
"'.";
2063 }
catch(
const boost::program_options::error&
e) {
2064 PLAIN_LOG <<
"Error in command line: " <<
e.what();
2067 PLAIN_LOG <<
"Could not parse config file: " <<
e.message;
2072 }
catch(
const std::bad_function_call& ) {
2073 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.
child_itors child_range(config_key_type key)
void remove_children(config_key_type key, std::function< bool(const config &)> p={})
Removes all children with tag key for which p returns true.
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...
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.
boost::asio::signal_set sighup_
boost::asio::streambuf admin_cmd_
std::unique_ptr< simple_wml::document > coro_receive_doc(SocketPtr socket, boost::asio::yield_context yield)
Receive WML document from a coroutine.
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, boost::asio::yield_context yield)
Send a WML document from within a coroutine.
boost::asio::io_service io_service_
boost::asio::posix::stream_descriptor input_
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(const std::string &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.
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)
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.