From 8f6d82bcb7e48b98417885f80894306d20e95e89 Mon Sep 17 00:00:00 2001 From: "Ignacio R. Morelle" Date: Sat, 7 Jun 2014 23:55:02 -0400 Subject: [PATCH] campaignd: Refactor request handling blocks into separate methods These methods are tried and run in sequence by looking them up from a request handler registry maintained as part of the server object. Since every campaignd request is a single WML node with an identifying name, I feel this approach makes things more readable than the previous massive deeply-nested if-else-if chain approach, but that might be just me. (Backported c164cb22b04601a1329274ab65934d02a83ec143 from master.) Conflicts: src/campaign_server/campaign_server.cpp --- src/campaign_server/campaign_server.cpp | 667 +++++++++++++----------- src/campaign_server/campaign_server.hpp | 76 +++ 2 files changed, 429 insertions(+), 314 deletions(-) diff --git a/src/campaign_server/campaign_server.cpp b/src/campaign_server/campaign_server.cpp index 58900c997473..9a49640d35e9 100644 --- a/src/campaign_server/campaign_server.cpp +++ b/src/campaign_server/campaign_server.cpp @@ -35,6 +35,7 @@ #include +#include #include #include #include @@ -177,6 +178,7 @@ server::server(const std::string& cfg_file, size_t min_threads, size_t max_threa , compress_level_(0) , input_() , hooks_() + , handlers_() , feedback_url_format_() , blacklist_() , blacklist_file_() @@ -190,6 +192,8 @@ server::server(const std::string& cfg_file, size_t min_threads, size_t max_threa signal(SIGTERM, exit_sigterm); cfg_.child_or_add("campaigns"); + + register_handlers(); } server::~server() @@ -393,322 +397,12 @@ void server::run() while((sock = network::receive_data(data, 0)) != network::null_connection) { - if (const config &req = data.child("request_campaign_list")) - { - LOG_CS << "sending campaign list to " << network::ip_address(sock) << " using gzip"; - time_t epoch = time(NULL); - config campaign_list; - campaign_list["timestamp"] = lexical_cast(epoch); - if (req["times_relative_to"] != "now") { - epoch = 0; - } - - bool before_flag = false; - time_t before = epoch; - try { - before = before + lexical_cast(req["before"]); - before_flag = true; - } catch(bad_lexical_cast) {} - - bool after_flag = false; - time_t after = epoch; - try { - after = after + lexical_cast(req["after"]); - after_flag = true; - } catch(bad_lexical_cast) {} - - std::string name = req["name"], lang = req["language"]; - BOOST_FOREACH(const config &i, campaigns().child_range("campaign")) - { - if (!name.empty() && name != i["name"]) continue; - std::string tm = i["timestamp"]; - if (before_flag && (tm.empty() || lexical_cast_default(tm, 0) >= before)) continue; - if (after_flag && (tm.empty() || lexical_cast_default(tm, 0) <= after)) continue; - if (!lang.empty()) { - bool found = false; - BOOST_FOREACH(const config &j, i.child_range("translation")) { - if (j["language"] == lang) { - found = true; - break; - } - } - if (!found) continue; - } - campaign_list.add_child("campaign", i); - } - - BOOST_FOREACH(config &j, campaign_list.child_range("campaign")) { - j["passphrase"] = t_string(); - j["upload_ip"] = t_string(); - j["email"] = t_string(); - j["feedback_url"] = t_string(); - - // Build a feedback_url string attribute from the - // internal [feedback] data. - config url_params = j.child_or_empty("feedback"); - j.clear_children("feedback"); - - if(!url_params.empty() && !feedback_url_format_.empty()) { - j["feedback_url"] = format_addon_feedback_url(feedback_url_format_, url_params); - } - } - - config response; - response.add_child("campaigns",campaign_list); - - std::cerr << " size: " << (network::send_data(response, sock)/1024) << "KiB\n"; - } - else if (const config &req = data.child("request_campaign")) - { - LOG_CS << "sending campaign '" << req["name"] << "' to " << network::ip_address(sock) << " using gzip"; - config &campaign = campaigns().find_child("campaign", "name", req["name"]); - if (!campaign) { - network::send_data(construct_error("Add-on '" + req["name"].str() + "' not found."), sock); - } else { - const int size = file_size(campaign["filename"]); - - if(size < 0) { - std::cerr << " size: KiB\n"; - LOG_CS << "File size unknown, aborting send.\n"; - network::send_data(construct_error("Add-on '" + req["name"].str() + "' could not be read by the server."), sock); - continue; - } - - std::cerr << " size: " << size/1024 << "KiB\n"; - network::send_file(campaign["filename"], sock); - // Clients doing upgrades or some other specific thing shouldn't bump - // the downloads count. Default to true for compatibility with old - // clients that won't tell us what they are trying to do. - if(req["increase_downloads"].to_bool(true)) { - int downloads = campaign["downloads"].to_int() + 1; - campaign["downloads"] = downloads; - } - } - - } - else if (data.child("request_terms")) - { - // This usually means the client wants to upload content, so tell it - // to give up when we're in read-only mode. - if(read_only_) { - LOG_CS << "in read-only mode, request for upload terms denied\n"; - network::send_data(construct_error("The server is currently in read-only mode, add-on uploads are disabled."), sock); - continue; - } - - LOG_CS << "sending terms " << network::ip_address(sock) << "\n"; - network::send_data(construct_message("All add-ons uploaded to this server must be licensed under the terms of the GNU General Public License (GPL). By uploading content to this server, you certify that you have the right to place the content under the conditions of the GPL, and choose to do so."), sock); - LOG_CS << " Done\n"; - } - else if (config &upload = data.child("upload")) - { - const std::string& addr = network::ip_address(sock); - - LOG_CS << "uploading campaign '" << upload["name"] << "' from " << addr << ".\n"; - config &data = upload.child("data"); - - const std::string& name = upload["name"]; - const std::string& lc_name = utils::lowercase(name); - - config *campaign = NULL; - BOOST_FOREACH(config &c, campaigns().child_range("campaign")) { - if (utils::lowercase(c["name"]) == lc_name) { - campaign = &c; - break; - } - } - - if (read_only_) { - LOG_CS << "Upload aborted - uploads not permitted in read-only mode.\n"; - network::send_data(construct_error("Add-on rejected: The server is currently in read-only mode."), sock); - } else if (!data) { - LOG_CS << "Upload aborted - no add-on data.\n"; - network::send_data(construct_error("Add-on rejected: No add-on data was supplied."), sock); - } else if (!addon_name_legal(upload["name"])) { - LOG_CS << "Upload aborted - invalid add-on name.\n"; - network::send_data(construct_error("Add-on rejected: The name of the add-on is invalid."), sock); - } else if (is_text_markup_char(upload["name"].str()[0])) { - LOG_CS << "Upload aborted - add-on name starts with an illegal formatting character.\n"; - network::send_data(construct_error("Add-on rejected: The name of the add-on starts with an illegal formatting character."), sock); - } else if (upload["title"].empty()) { - LOG_CS << "Upload aborted - no add-on title specified.\n"; - network::send_data(construct_error("Add-on rejected: You did not specify the title of the add-on in the pbl file!"), sock); - } else if (is_text_markup_char(upload["title"].str()[0])) { - LOG_CS << "Upload aborted - add-on title starts with an illegal formatting character.\n"; - network::send_data(construct_error("Add-on rejected: The title of the add-on starts with an illegal formatting character."), sock); - } else if (get_addon_type(upload["type"]) == ADDON_UNKNOWN) { - LOG_CS << "Upload aborted - unknown add-on type specified.\n"; - network::send_data(construct_error("Add-on rejected: You did not specify a known type for the add-on in the pbl file! (See PblWML: wiki.wesnoth.org/PblWML)"), sock); - } else if (upload["author"].empty()) { - LOG_CS << "Upload aborted - no add-on author specified.\n"; - network::send_data(construct_error("Add-on rejected: You did not specify the author(s) of the add-on in the pbl file!"), sock); - } else if (upload["version"].empty()) { - LOG_CS << "Upload aborted - no add-on version specified.\n"; - network::send_data(construct_error("Add-on rejected: You did not specify the version of the add-on in the pbl file!"), sock); - } else if (upload["description"].empty()) { - LOG_CS << "Upload aborted - no add-on description specified.\n"; - network::send_data(construct_error("Add-on rejected: You did not specify a description of the add-on in the pbl file!"), sock); - } else if (upload["email"].empty()) { - LOG_CS << "Upload aborted - no add-on email specified.\n"; - network::send_data(construct_error("Add-on rejected: You did not specify your email address in the pbl file!"), sock); - } else if (!check_names_legal(data)) { - LOG_CS << "Upload aborted - invalid file names in add-on data.\n"; - network::send_data(construct_error("Add-on rejected: The add-on contains an illegal file or directory name." - " File or directory names may not contain whitespace or any of the following characters: '/ \\ : ~'"), sock); - } else if (campaign && (*campaign)["passphrase"].str() != upload["passphrase"]) { - LOG_CS << "Upload aborted - incorrect passphrase.\n"; - network::send_data(construct_error("Add-on rejected: The add-on already exists, and your passphrase was incorrect."), sock); - } else { - const time_t upload_ts = time(NULL); - - LOG_CS << "Upload is owner upload.\n"; - - if(blacklist_.is_blacklisted(name, - upload["title"].str(), - upload["description"].str(), - upload["author"].str(), - addr, - upload["email"].str())) - { - LOG_CS << "Upload denied - blacklisted add-on information.\n"; - network::send_data(construct_error("Add-on upload denied. Please contact the server administration for assistance."), sock); - continue; - } - - std::string message = "Add-on accepted."; - - if (!version_info(upload["version"]).good()) { - message += "\n<255,255,0>Note: The version you specified is invalid. This add-on will be ignored for automatic update checks."; - } - - if(campaign == NULL) { - campaign = &campaigns().add_child("campaign"); - (*campaign)["original_timestamp"] = lexical_cast(upload_ts); - } - - (*campaign)["title"] = upload["title"]; - (*campaign)["name"] = upload["name"]; - (*campaign)["filename"] = "data/" + upload["name"].str(); - (*campaign)["passphrase"] = upload["passphrase"]; - (*campaign)["author"] = upload["author"]; - (*campaign)["description"] = upload["description"]; - (*campaign)["version"] = upload["version"]; - (*campaign)["icon"] = upload["icon"]; - (*campaign)["translate"] = upload["translate"]; - (*campaign)["dependencies"] = upload["dependencies"]; - (*campaign)["upload_ip"] = addr; - (*campaign)["type"] = upload["type"]; - (*campaign)["email"] = upload["email"]; - - if((*campaign)["downloads"].empty()) { - (*campaign)["downloads"] = 0; - } - (*campaign)["timestamp"] = lexical_cast(upload_ts); - - int uploads = (*campaign)["uploads"].to_int() + 1; - (*campaign)["uploads"] = uploads; - - (*campaign).clear_children("feedback"); - if(const config& url_params = upload.child("feedback")) { - (*campaign).add_child("feedback", url_params); - } - - std::string filename = (*campaign)["filename"]; - data["title"] = (*campaign)["title"]; - data["name"] = ""; - data["campaign_name"] = (*campaign)["name"]; - data["author"] = (*campaign)["author"]; - data["description"] = (*campaign)["description"]; - data["version"] = (*campaign)["version"]; - data["timestamp"] = (*campaign)["timestamp"]; - data["original_timestamp"] = (*campaign)["original_timestamp"]; - data["icon"] = (*campaign)["icon"]; - data["type"] = (*campaign)["type"]; - (*campaign).clear_children("translation"); - find_translations(data, *campaign); - - add_license(data); - - { - scoped_ostream campaign_file = ostream_file(filename); - config_writer writer(*campaign_file, true, compress_level_); - writer.write(data); - } - - (*campaign)["size"] = lexical_cast( - file_size(filename)); - - write_config(); - - network::send_data(construct_message(message), sock); - - fire("hook_post_upload", upload["name"]); - } - } - else if (const config &erase = data.child("delete")) - { - if(read_only_) { - LOG_CS << "in read-only mode, request to delete '" << erase["name"] << "' from " << network::ip_address(sock) << " denied\n"; - network::send_data(construct_error("Cannot delete add-on: The server is currently in read-only mode."), sock); - continue; - } - - LOG_CS << "deleting campaign '" << erase["name"] << "' requested from " << network::ip_address(sock) << "\n"; - const config &campaign = campaigns().find_child("campaign", "name", erase["name"]); - if (!campaign) { - network::send_data(construct_error("The add-on does not exist."), sock); - continue; - } - - if (campaign["passphrase"] != erase["passphrase"] - && (campaigns()["master_password"].empty() - || campaigns()["master_password"] != erase["passphrase"])) - { - network::send_data(construct_error("The passphrase is incorrect."), sock); - continue; - } - - //erase the campaign - write_file(campaign["filename"], std::string()); - remove(campaign["filename"].str().c_str()); - - config::child_itors itors = campaigns().child_range("campaign"); - for (size_t index = 0; itors.first != itors.second; - ++index, ++itors.first) - { - if (&campaign == &*itors.first) { - campaigns().remove_child("campaign", index); - break; - } - } - - write_config(); - - network::send_data(construct_message("Add-on deleted."), sock); - - fire("hook_post_erase", erase["name"]); - } - else if (const config &cpass = data.child("change_passphrase")) + BOOST_FOREACH(const request_handler_info& rh, handlers_) { - if(read_only_) { - LOG_CS << "in read-only mode, request to change passphrase denied\n"; - network::send_data(construct_error("Cannot change passphrase: The server is currently in read-only mode."), sock); - continue; - } - - config &campaign = campaigns().find_child("campaign", "name", cpass["name"]); - if (!campaign) { - network::send_data(construct_error("No add-on with that name exists."), sock); - } else if (campaign["passphrase"] != cpass["passphrase"]) { - network::send_data(construct_error("Your old passphrase was incorrect."), sock); - } else if (cpass["new_passphrase"].empty()) { - network::send_data(construct_error("No new passphrase was supplied."), sock); - } else { - campaign["passphrase"] = cpass["new_passphrase"]; - - write_config(); + const config& req_body = data.child(rh.first); - network::send_data(construct_message("Passphrase changed."), sock); + if(req_body) { + rh.second(request(rh.first, req_body, sock)); } } } @@ -744,6 +438,351 @@ void server::run() } } +void server::register_handler(const std::string& cmd, const request_handler& func) +{ + handlers_.push_back(std::make_pair(cmd, func)); +} + +void server::register_handlers() +{ + register_handler("request_campaign_list", boost::bind(&server::handle_request_campaign_list, this, _1)); + register_handler("request_campaign", boost::bind(&server::handle_request_campaign, this, _1)); + register_handler("request_terms", boost::bind(&server::handle_request_terms, this, _1)); + register_handler("upload", boost::bind(&server::handle_upload, this, _1)); + register_handler("delete", boost::bind(&server::handle_delete, this, _1)); + register_handler("change_passphrase", boost::bind(&server::handle_change_passphrase, this, _1)); +} + +void server::handle_request_campaign_list(const server::request& req) +{ + LOG_CS << "sending campaign list to " << req.addr << " using gzip"; + + time_t epoch = time(NULL); + config campaign_list; + + campaign_list["timestamp"] = lexical_cast(epoch); + if(req.cfg["times_relative_to"] != "now") { + epoch = 0; + } + + bool before_flag = false; + time_t before = epoch; + try { + before = before + lexical_cast(req.cfg["before"]); + before_flag = true; + } catch(bad_lexical_cast) {} + + bool after_flag = false; + time_t after = epoch; + try { + after = after + lexical_cast(req.cfg["after"]); + after_flag = true; + } catch(bad_lexical_cast) {} + + std::string name = req.cfg["name"], lang = req.cfg["language"]; + BOOST_FOREACH(const config &i, campaigns().child_range("campaign")) + { + if (!name.empty() && name != i["name"]) continue; + std::string tm = i["timestamp"]; + if (before_flag && (tm.empty() || lexical_cast_default(tm, 0) >= before)) continue; + if (after_flag && (tm.empty() || lexical_cast_default(tm, 0) <= after)) continue; + if (!lang.empty()) { + bool found = false; + BOOST_FOREACH(const config &j, i.child_range("translation")) { + if (j["language"] == lang) { + found = true; + break; + } + } + if (!found) continue; + } + campaign_list.add_child("campaign", i); + } + + BOOST_FOREACH(config &j, campaign_list.child_range("campaign")) { + j["passphrase"] = t_string(); + j["upload_ip"] = t_string(); + j["email"] = t_string(); + j["feedback_url"] = t_string(); + + // Build a feedback_url string attribute from the + // internal [feedback] data. + config url_params = j.child_or_empty("feedback"); + j.clear_children("feedback"); + + if(!url_params.empty() && !feedback_url_format_.empty()) { + j["feedback_url"] = format_addon_feedback_url(feedback_url_format_, url_params); + } + } + + config response; + response.add_child("campaigns",campaign_list); + + std::cerr << " size: " << (network::send_data(response, req.sock)/1024) << "KiB\n"; +} + +void server::handle_request_campaign(const server::request& req) +{ + LOG_CS << "sending campaign '" << req.cfg["name"] << "' to " << req.addr << " using gzip"; + config &campaign = campaigns().find_child("campaign", "name", req.cfg["name"]); + if (!campaign) { + network::send_data(construct_error("Add-on '" + req.cfg["name"].str() + "' not found."), req.sock); + } else { + const int size = file_size(campaign["filename"]); + + if(size < 0) { + std::cerr << " size: KiB\n"; + LOG_CS << "File size unknown, aborting send.\n"; + network::send_data(construct_error("Add-on '" + req.cfg["name"].str() + "' could not be read by the server."), req.sock); + return; + } + + std::cerr << " size: " << size/1024 << "KiB\n"; + network::send_file(campaign["filename"], req.sock); + // Clients doing upgrades or some other specific thing shouldn't bump + // the downloads count. Default to true for compatibility with old + // clients that won't tell us what they are trying to do. + if(req.cfg["increase_downloads"].to_bool(true)) { + int downloads = campaign["downloads"].to_int() + 1; + campaign["downloads"] = downloads; + } + } +} + +void server::handle_request_terms(const server::request& req) +{ + // This usually means the client wants to upload content, so tell it + // to give up when we're in read-only mode. + if(read_only_) { + LOG_CS << "in read-only mode, request for upload terms denied\n"; + network::send_data(construct_error("The server is currently in read-only mode, add-on uploads are disabled."), req.sock); + return; + } + + LOG_CS << "sending terms " << req.addr << "\n"; + network::send_data(construct_message("All add-ons uploaded to this server must be licensed under the terms of the GNU General Public License (GPL). By uploading content to this server, you certify that you have the right to place the content under the conditions of the GPL, and choose to do so."), req.sock); + LOG_CS << " Done\n"; +} + +void server::handle_upload(const server::request& req) +{ + const config& upload = req.cfg; + + LOG_CS << "uploading campaign '" << upload["name"] << "' from " << req.addr << ".\n"; + config data = upload.child("data"); + + const std::string& name = upload["name"]; + const std::string& lc_name = utils::lowercase(name); + + config *campaign = NULL; + BOOST_FOREACH(config &c, campaigns().child_range("campaign")) { + if (utils::lowercase(c["name"]) == lc_name) { + campaign = &c; + break; + } + } + + if (read_only_) { + LOG_CS << "Upload aborted - uploads not permitted in read-only mode.\n"; + network::send_data(construct_error("Add-on rejected: The server is currently in read-only mode."), req.sock); + } else if (!data) { + LOG_CS << "Upload aborted - no add-on data.\n"; + network::send_data(construct_error("Add-on rejected: No add-on data was supplied."), req.sock); + } else if (!addon_name_legal(upload["name"])) { + LOG_CS << "Upload aborted - invalid add-on name.\n"; + network::send_data(construct_error("Add-on rejected: The name of the add-on is invalid."), req.sock); + } else if (is_text_markup_char(upload["name"].str()[0])) { + LOG_CS << "Upload aborted - add-on name starts with an illegal formatting character.\n"; + network::send_data(construct_error("Add-on rejected: The name of the add-on starts with an illegal formatting character."), req.sock); + } else if (upload["title"].empty()) { + LOG_CS << "Upload aborted - no add-on title specified.\n"; + network::send_data(construct_error("Add-on rejected: You did not specify the title of the add-on in the pbl file!"), req.sock); + } else if (is_text_markup_char(upload["title"].str()[0])) { + LOG_CS << "Upload aborted - add-on title starts with an illegal formatting character.\n"; + network::send_data(construct_error("Add-on rejected: The title of the add-on starts with an illegal formatting character."), req.sock); + } else if (get_addon_type(upload["type"]) == ADDON_UNKNOWN) { + LOG_CS << "Upload aborted - unknown add-on type specified.\n"; + network::send_data(construct_error("Add-on rejected: You did not specify a known type for the add-on in the pbl file! (See PblWML: wiki.wesnoth.org/PblWML)"), req.sock); + } else if (upload["author"].empty()) { + LOG_CS << "Upload aborted - no add-on author specified.\n"; + network::send_data(construct_error("Add-on rejected: You did not specify the author(s) of the add-on in the pbl file!"), req.sock); + } else if (upload["version"].empty()) { + LOG_CS << "Upload aborted - no add-on version specified.\n"; + network::send_data(construct_error("Add-on rejected: You did not specify the version of the add-on in the pbl file!"), req.sock); + } else if (upload["description"].empty()) { + LOG_CS << "Upload aborted - no add-on description specified.\n"; + network::send_data(construct_error("Add-on rejected: You did not specify a description of the add-on in the pbl file!"), req.sock); + } else if (upload["email"].empty()) { + LOG_CS << "Upload aborted - no add-on email specified.\n"; + network::send_data(construct_error("Add-on rejected: You did not specify your email address in the pbl file!"), req.sock); + } else if (!check_names_legal(data)) { + LOG_CS << "Upload aborted - invalid file names in add-on data.\n"; + network::send_data(construct_error("Add-on rejected: The add-on contains an illegal file or directory name." + " File or directory names may not contain whitespace or any of the following characters: '/ \\ : ~'"), req.sock); + } else if (campaign && (*campaign)["passphrase"].str() != upload["passphrase"]) { + LOG_CS << "Upload aborted - incorrect passphrase.\n"; + network::send_data(construct_error("Add-on rejected: The add-on already exists, and your passphrase was incorrect."), req.sock); + } else { + const time_t upload_ts = time(NULL); + + LOG_CS << "Upload is owner upload.\n"; + + if(blacklist_.is_blacklisted(name, + upload["title"].str(), + upload["description"].str(), + upload["author"].str(), + req.addr, + upload["email"].str())) + { + LOG_CS << "Upload denied - blacklisted add-on information.\n"; + network::send_data(construct_error("Add-on upload denied. Please contact the server administration for assistance."), req.sock); + return; + } + + std::string message = "Add-on accepted."; + + if (!version_info(upload["version"]).good()) { + message += "\n<255,255,0>Note: The version you specified is invalid. This add-on will be ignored for automatic update checks."; + } + + if(campaign == NULL) { + campaign = &campaigns().add_child("campaign"); + (*campaign)["original_timestamp"] = lexical_cast(upload_ts); + } + + (*campaign)["title"] = upload["title"]; + (*campaign)["name"] = upload["name"]; + (*campaign)["filename"] = "data/" + upload["name"].str(); + (*campaign)["passphrase"] = upload["passphrase"]; + (*campaign)["author"] = upload["author"]; + (*campaign)["description"] = upload["description"]; + (*campaign)["version"] = upload["version"]; + (*campaign)["icon"] = upload["icon"]; + (*campaign)["translate"] = upload["translate"]; + (*campaign)["dependencies"] = upload["dependencies"]; + (*campaign)["upload_ip"] = req.addr; + (*campaign)["type"] = upload["type"]; + (*campaign)["email"] = upload["email"]; + + if((*campaign)["downloads"].empty()) { + (*campaign)["downloads"] = 0; + } + (*campaign)["timestamp"] = lexical_cast(upload_ts); + + int uploads = (*campaign)["uploads"].to_int() + 1; + (*campaign)["uploads"] = uploads; + + (*campaign).clear_children("feedback"); + if(const config& url_params = upload.child("feedback")) { + (*campaign).add_child("feedback", url_params); + } + + std::string filename = (*campaign)["filename"]; + data["title"] = (*campaign)["title"]; + data["name"] = ""; + data["campaign_name"] = (*campaign)["name"]; + data["author"] = (*campaign)["author"]; + data["description"] = (*campaign)["description"]; + data["version"] = (*campaign)["version"]; + data["timestamp"] = (*campaign)["timestamp"]; + data["original_timestamp"] = (*campaign)["original_timestamp"]; + data["icon"] = (*campaign)["icon"]; + data["type"] = (*campaign)["type"]; + (*campaign).clear_children("translation"); + find_translations(data, *campaign); + + add_license(data); + + { + scoped_ostream campaign_file = ostream_file(filename); + config_writer writer(*campaign_file, true, compress_level_); + writer.write(data); + } + + (*campaign)["size"] = lexical_cast( + file_size(filename)); + + write_config(); + + network::send_data(construct_message(message), req.sock); + + fire("hook_post_upload", upload["name"]); + } +} + +void server::handle_delete(const server::request& req) +{ + const config& erase = req.cfg; + + if(read_only_) { + LOG_CS << "in read-only mode, request to delete '" << erase["name"] << "' from " << req.addr << " denied\n"; + network::send_data(construct_error("Cannot delete add-on: The server is currently in read-only mode."), req.sock); + return; + } + + LOG_CS << "deleting campaign '" << erase["name"] << "' requested from " << req.addr << "\n"; + const config &campaign = campaigns().find_child("campaign", "name", erase["name"]); + if (!campaign) { + network::send_data(construct_error("The add-on does not exist."), req.sock); + return; + } + + if (campaign["passphrase"] != erase["passphrase"] + && (campaigns()["master_password"].empty() + || campaigns()["master_password"] != erase["passphrase"])) + { + network::send_data(construct_error("The passphrase is incorrect."), req.sock); + return; + } + + //erase the campaign + write_file(campaign["filename"], std::string()); + remove(campaign["filename"].str().c_str()); + + config::child_itors itors = campaigns().child_range("campaign"); + for (size_t index = 0; itors.first != itors.second; + ++index, ++itors.first) + { + if (&campaign == &*itors.first) { + campaigns().remove_child("campaign", index); + break; + } + } + + write_config(); + + network::send_data(construct_message("Add-on deleted."), req.sock); + + fire("hook_post_erase", erase["name"]); + +} + +void server::handle_change_passphrase(const server::request& req) +{ + const config& cpass = req.cfg; + + if(read_only_) { + LOG_CS << "in read-only mode, request to change passphrase denied\n"; + network::send_data(construct_error("Cannot change passphrase: The server is currently in read-only mode."), req.sock); + return; + } + + config &campaign = campaigns().find_child("campaign", "name", cpass["name"]); + if (!campaign) { + network::send_data(construct_error("No add-on with that name exists."), req.sock); + } else if (campaign["passphrase"] != cpass["passphrase"]) { + network::send_data(construct_error("Your old passphrase was incorrect."), req.sock); + } else if (cpass["new_passphrase"].empty()) { + network::send_data(construct_error("No new passphrase was supplied."), req.sock); + } else { + campaign["passphrase"] = cpass["new_passphrase"]; + + write_config(); + + network::send_data(construct_message("Passphrase changed."), req.sock); + } +} + } // end namespace campaignd int main(int argc, char**argv) diff --git a/src/campaign_server/campaign_server.hpp b/src/campaign_server/campaign_server.hpp index 2067530ea23f..40128b2512c2 100644 --- a/src/campaign_server/campaign_server.hpp +++ b/src/campaign_server/campaign_server.hpp @@ -19,7 +19,9 @@ #include "network.hpp" #include "server/input_stream.hpp" +#include #include +#include namespace campaignd { @@ -40,6 +42,45 @@ class server : private boost::noncopyable void run(); private: + /** + * Client request information object. + * + * Contains data and metadata associated with a single request from a + * remote add-ons client, in a light-weight format for passing to request + * handlers. + */ + struct request + { + const std::string& cmd; + const config& cfg; + + const network::connection sock; + const std::string addr; + + /** + * Constructor. + * + * @param reqcmd Request command. + * @param reqcfg Request WML body. + * @param reqsock Client socket that initiated the request. + * + * @note Neither @a reqcmd nor @a reqcfg are copied into instances, so + * they are required to exist for as long as every @a request + * instance that uses them. + */ + request(const std::string& reqcmd, + const config& reqcfg, + network::connection reqsock) + : cmd(reqcmd) + , cfg(reqcfg) + , sock(reqsock) + , addr(network::ip_address(sock)) + {} + }; + + typedef boost::function request_handler; + typedef std::pair request_handler_info; + config cfg_; const std::string cfg_file_; @@ -49,6 +90,7 @@ class server : private boost::noncopyable boost::scoped_ptr input_; /**< Server control socket. */ std::map hooks_; + std::vector handlers_; std::string feedback_url_format_; @@ -91,6 +133,40 @@ class server : private boost::noncopyable /** Retrieves the contents of the [server_info] WML node. */ config& server_info() { return cfg_.child("server_info"); } + + // + // Request handling. + // + + /** + * Registers client request handlers. + * + * This is called by the class constructor. Individual handlers must be + * methods of this class that take a single parameter of type @a request + * and they are registered using the @a register_handler method. + * + * When adding new handlers, make sure to update the implementation of + * this method accordingly so they are recognized and invoked at runtime. + */ + void register_handlers(); + + /** + * Registers a single request handler. + * + * @param cmd The request command, corresponding to the name of the [tag} + * with the request body (e.g. "handle_request_terms"). + * @param func The request function. This should be a class method passed + * as a @a boost::bind function object that takes a single + * parameter of type @a request. + */ + void register_handler(const std::string& cmd, const request_handler& func); + + void handle_request_campaign_list(const request&); + void handle_request_campaign(const request&); + void handle_request_terms(const request&); + void handle_upload(const request&); + void handle_delete(const request&); + void handle_change_passphrase(const request&); }; } // end namespace campaignd