00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023 #include "filesystem.hpp"
00024 #include "foreach.hpp"
00025 #include "log.hpp"
00026 #include "network_worker.hpp"
00027 #include "serialization/binary_or_text.hpp"
00028 #include "serialization/parser.hpp"
00029 #include "serialization/string_utils.hpp"
00030 #include "game_config.hpp"
00031 #include "addon/validation.hpp"
00032 #include "version.hpp"
00033 #include "server/input_stream.hpp"
00034 #include "util.hpp"
00035
00036 #include <csignal>
00037
00038 #include <boost/iostreams/filter/gzip.hpp>
00039
00040
00041
00042 #if !(defined(_WIN32))
00043 #include <errno.h>
00044 #endif
00045
00046 static lg::log_domain log_network("network");
00047 #define LOG_CS if (lg::err.dont_log(log_network)) ; else lg::err(log_network, false)
00048
00049
00050 #ifndef SIGHUP
00051 #define SIGHUP 20
00052 #endif
00053
00054
00055 static void exit_sighup(int signal) {
00056 assert(signal == SIGHUP);
00057 LOG_CS << "SIGHUP caught, exiting without cleanup immediately.\n";
00058 exit(128 + SIGHUP);
00059 }
00060
00061 static void exit_sigint(int signal) {
00062 assert(signal == SIGINT);
00063 LOG_CS << "SIGINT caught, exiting without cleanup immediately.\n";
00064 exit(0);
00065 }
00066
00067 static void exit_sigterm(int signal) {
00068 assert(signal == SIGTERM);
00069 LOG_CS << "SIGTERM caught, exiting without cleanup immediately.\n";
00070 exit(128 + SIGTERM);
00071 }
00072
00073 namespace {
00074
00075
00076 const std::string illegal_markup_chars = "*`~{^}|@#<&";
00077
00078 inline bool is_text_markup_char(char c)
00079 {
00080 return illegal_markup_chars.find(c) != std::string::npos;
00081 }
00082
00083 config construct_message(const std::string& msg)
00084 {
00085 config cfg;
00086 cfg.add_child("message")["message"] = msg;
00087 return cfg;
00088 }
00089
00090 config construct_error(const std::string& msg)
00091 {
00092 config cfg;
00093 cfg.add_child("error")["message"] = "#Error: " + msg;
00094 LOG_CS << "ERROR: "<<msg<<"\n";
00095 return cfg;
00096 }
00097
00098 class campaign_server
00099 {
00100 public:
00101 explicit campaign_server(const std::string& cfgfile,size_t min_thread = 10,size_t max_thread = 0);
00102 void run();
00103 ~campaign_server()
00104 {
00105 delete input_;
00106 scoped_ostream cfgfile = ostream_file(file_);
00107 write(*cfgfile, cfg_);
00108 }
00109 private:
00110
00111
00112
00113
00114 void fire(const std::string& hook, const std::string& addon);
00115 void convert_binary_to_gzip();
00116 int load_config();
00117 const config &campaigns() const { return cfg_.child("campaigns"); }
00118 config &campaigns() { return cfg_.child("campaigns"); }
00119 config cfg_;
00120 const std::string file_;
00121 const network::manager net_manager_;
00122 std::map<std::string, std::string> hooks_;
00123 input_stream* input_;
00124 int compress_level_;
00125 const network::server_manager server_manager_;
00126
00127 };
00128
00129 void campaign_server::fire(const std::string& hook, const std::string& addon)
00130 {
00131 const std::map<std::string, std::string>::const_iterator itor = hooks_.find(hook);
00132 if(itor == hooks_.end()) return;
00133
00134 const std::string& script = itor->second;
00135 if(script == "") return;
00136
00137 #if (defined(_WIN32))
00138 LOG_CS << "ERROR: Tried to execute a script on an unsupported platform\n";
00139 return;
00140 #else
00141 pid_t childpid;
00142
00143 if((childpid = fork()) == -1) {
00144 LOG_CS << "ERROR: fork failed while updating campaign " << addon << "\n";
00145 return;
00146 }
00147
00148 if(childpid == 0) {
00149
00150
00151
00152
00153 execlp(script.c_str(), script.c_str(), addon.c_str(), static_cast<char *>(NULL));
00154
00155
00156 std::cerr << "ERROR: exec failed with errno " << errno << " for addon " << addon
00157 << '\n';
00158 exit(errno);
00159
00160 } else {
00161 return;
00162 }
00163
00164 #endif
00165 }
00166
00167 int campaign_server::load_config()
00168 {
00169 scoped_istream stream = istream_file(file_);
00170 read(cfg_, *stream);
00171 bool network_use_system_sendfile = cfg_["network_use_system_sendfile"].to_bool();
00172 network_worker_pool::set_use_system_sendfile(network_use_system_sendfile);
00173 cfg_["network_use_system_sendfile"] = network_use_system_sendfile;
00174
00175 compress_level_ = cfg_["compress_level"].to_int(6);
00176 cfg_["compress_level"] = compress_level_;
00177 return cfg_["port"].to_int(default_campaignd_port);
00178 }
00179
00180 campaign_server::campaign_server(const std::string& cfgfile,
00181 size_t min_thread, size_t max_thread) :
00182 cfg_(),
00183 file_(cfgfile),
00184 net_manager_(min_thread,max_thread),
00185 hooks_(),
00186 input_(0),
00187 compress_level_(0),
00188 server_manager_(load_config())
00189 {
00190 #ifndef _MSC_VER
00191 signal(SIGHUP, exit_sighup);
00192 #endif
00193 signal(SIGINT, exit_sigint);
00194 signal(SIGTERM, exit_sigterm);
00195
00196 cfg_.child_or_add("campaigns");
00197
00198
00199 hooks_.insert(std::make_pair(std::string("hook_post_upload"), cfg_["hook_post_upload"]));
00200 hooks_.insert(std::make_pair(std::string("hook_post_erase"), cfg_["hook_post_erase"]));
00201 }
00202
00203 void find_translations(const config& cfg, config& campaign)
00204 {
00205 foreach (const config &dir, cfg.child_range("dir"))
00206 {
00207 if (dir["name"] == "LC_MESSAGES") {
00208 config &language = campaign.add_child("translation");
00209 language["language"] = cfg["name"];
00210 } else {
00211 find_translations(dir, campaign);
00212 }
00213 }
00214 }
00215
00216
00217 void add_license(config &data)
00218 {
00219 config &dir = data.find_child("dir", "name", data["campaign_name"]);
00220
00221 if (!dir) return;
00222
00223
00224 if (dir.find_child("file", "name", "COPYING.txt")) return;
00225 if (dir.find_child("file", "name", "COPYING")) return;
00226 if (dir.find_child("file", "name", "copying.txt")) return;
00227 if (dir.find_child("file", "name", "Copying.txt")) return;
00228 if (dir.find_child("file", "name", "COPYING.TXT")) return;
00229
00230
00231 std::string contents = read_file("COPYING.txt");
00232 if (contents.empty()) {
00233 LOG_CS << "Could not find COPYING.txt, path is \""
00234 << game_config::path << "\"\n";
00235 return;
00236 }
00237 config ©ing = dir.add_child("file");
00238 copying["name"] = "COPYING.txt";
00239 copying["contents"] = contents;
00240
00241 }
00242
00243 void campaign_server::convert_binary_to_gzip()
00244 {
00245 if (!cfg_["encoded"].to_bool())
00246 {
00247
00248 config::const_child_itors camps = campaigns().child_range("campaign");
00249 LOG_CS << "Encoding all stored addons. Number of addons: "
00250 << std::distance(camps.first, camps.second) << '\n';
00251
00252 foreach (const config &cm, camps)
00253 {
00254 LOG_CS << "Encoding " << cm["name"] << '\n';
00255 std::string filename = cm["filename"], newfilename = filename + ".new";
00256
00257 {
00258 scoped_istream in_file = istream_file(filename);
00259 boost::iostreams::filtering_stream<boost::iostreams::input> in_filter;
00260 in_filter.push(boost::iostreams::gzip_decompressor());
00261 in_filter.push(*in_file);
00262
00263 scoped_ostream out_file = ostream_file(newfilename);
00264 boost::iostreams::filtering_stream<boost::iostreams::output> out_filter;
00265 out_filter.push(boost::iostreams::gzip_compressor(boost::iostreams::gzip_params(compress_level_)));
00266 out_filter.push(*out_file);
00267
00268 unsigned char c = in_filter.get();
00269 while( in_filter.good())
00270 {
00271 if (needs_escaping(c) && c != '\x01')
00272 {
00273 out_filter.put('\x01');
00274 out_filter.put(c+1);
00275 } else {
00276 out_filter.put(c);
00277 }
00278 c = in_filter.get();
00279 }
00280 }
00281
00282 std::remove(filename.c_str());
00283 std::rename(newfilename.c_str(), filename.c_str());
00284 }
00285
00286 cfg_["encoded"] = true;
00287 }
00288 }
00289
00290 void campaign_server::run()
00291 {
00292 convert_binary_to_gzip();
00293
00294 if (!cfg_["control_socket"].empty())
00295 input_ = new input_stream(cfg_["control_socket"]);
00296 network::connection sock = 0;
00297 for(int increment = 0; ; ++increment) {
00298 try {
00299 std::string admin_cmd;
00300 if (input_ && input_->read_line(admin_cmd))
00301 {
00302
00303 if (admin_cmd == "shut_down")
00304 {
00305 break;
00306 }
00307 }
00308
00309 if((increment%(60*10*50)) == 0) {
00310 scoped_ostream cfgfile = ostream_file(file_);
00311 write(*cfgfile, cfg_);
00312 }
00313
00314 network::process_send_queue();
00315
00316 sock = network::accept_connection();
00317 if(sock) {
00318 LOG_CS << "received connection from " << network::ip_address(sock) << "\n";
00319 }
00320
00321 config data;
00322 while((sock = network::receive_data(data, 0)) != network::null_connection) {
00323 if (const config &req = data.child("request_campaign_list"))
00324 {
00325 LOG_CS << "sending campaign list to " << network::ip_address(sock) << " using gzip";
00326 time_t epoch = time(NULL);
00327 config campaign_list;
00328 campaign_list["timestamp"] = lexical_cast<std::string>(epoch);
00329 if (req["times_relative_to"] != "now") {
00330 epoch = 0;
00331 }
00332
00333 bool before_flag = false;
00334 time_t before = epoch;
00335 try {
00336 before = before + lexical_cast<time_t>(req["before"]);
00337 before_flag = true;
00338 } catch(bad_lexical_cast) {}
00339
00340 bool after_flag = false;
00341 time_t after = epoch;
00342 try {
00343 after = after + lexical_cast<time_t>(req["after"]);
00344 after_flag = true;
00345 } catch(bad_lexical_cast) {}
00346
00347 std::string name = req["name"], lang = req["language"];
00348 foreach (const config &i, campaigns().child_range("campaign"))
00349 {
00350 if (!name.empty() && name != i["name"]) continue;
00351 std::string tm = i["timestamp"];
00352 if (before_flag && (tm.empty() || lexical_cast_default<time_t>(tm, 0) >= before)) continue;
00353 if (after_flag && (tm.empty() || lexical_cast_default<time_t>(tm, 0) <= after)) continue;
00354 if (!lang.empty()) {
00355 bool found = false;
00356 foreach (const config &j, i.child_range("translation")) {
00357 if (j["language"] == lang) {
00358 found = true;
00359 break;
00360 }
00361 }
00362 if (!found) continue;
00363 }
00364 campaign_list.add_child("campaign", i);
00365 }
00366
00367 foreach (config &j, campaign_list.child_range("campaign")) {
00368 j["passphrase"] = t_string();
00369 j["upload_ip"] = t_string();
00370 j["email"] = t_string();
00371 }
00372
00373 config response;
00374 response.add_child("campaigns",campaign_list);
00375 std::cerr << " size: " << (network::send_data(response, sock)/1024) << "KiB\n";
00376 }
00377 else if (const config &req = data.child("request_campaign"))
00378 {
00379 LOG_CS << "sending campaign '" << req["name"] << "' to " << network::ip_address(sock) << " using gzip";
00380 config &campaign = campaigns().find_child("campaign", "name", req["name"]);
00381 if (!campaign) {
00382 network::send_data(construct_error("Add-on '" + req["name"].str() + "'not found."), sock);
00383 } else {
00384 std::cerr << " size: " << (file_size(campaign["filename"])/1024) << "KiB\n";
00385 network::send_file(campaign["filename"], sock);
00386 int downloads = campaign["downloads"].to_int() + 1;
00387 campaign["downloads"] = downloads;
00388 }
00389
00390 }
00391 else if (data.child("request_terms"))
00392 {
00393 LOG_CS << "sending terms " << network::ip_address(sock) << "\n";
00394 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);
00395 LOG_CS << " Done\n";
00396 }
00397 else if (config &upload = data.child("upload"))
00398 {
00399 LOG_CS << "uploading campaign '" << upload["name"] << "' from " << network::ip_address(sock) << ".\n";
00400 config &data = upload.child("data");
00401 const std::string& name = upload["name"];
00402 std::string lc_name(name.size(), ' ');
00403 std::transform(name.begin(), name.end(), lc_name.begin(), tolower);
00404 config *campaign = NULL;
00405 foreach (config &c, campaigns().child_range("campaign")) {
00406 if (utils::lowercase(c["name"]) == lc_name) {
00407 campaign = &c;
00408 break;
00409 }
00410 }
00411 if (!data) {
00412 LOG_CS << "Upload aborted - no add-on data.\n";
00413 network::send_data(construct_error("Add-on rejected: No add-on data was supplied."), sock);
00414 } else if (!addon_name_legal(upload["name"])) {
00415 LOG_CS << "Upload aborted - invalid add-on name.\n";
00416 network::send_data(construct_error("Add-on rejected: The name of the add-on is invalid."), sock);
00417 } else if (is_text_markup_char(upload["name"].str()[0])) {
00418 LOG_CS << "Upload aborted - add-on name starts with an illegal formatting character.\n";
00419 network::send_data(construct_error("Add-on rejected: The name of the add-on starts with an illegal formatting character."), sock);
00420 } else if (upload["title"].empty()) {
00421 LOG_CS << "Upload aborted - no add-on title specified.\n";
00422 network::send_data(construct_error("Add-on rejected: You did not specify the title of the add-on in the pbl file!"), sock);
00423 } else if (is_text_markup_char(upload["title"].str()[0])) {
00424 LOG_CS << "Upload aborted - add-on title starts with an illegal formatting character.\n";
00425 network::send_data(construct_error("Add-on rejected: The title of the add-on starts with an illegal formatting character."), sock);
00426 } else if (get_addon_type(upload["type"]) == ADDON_UNKNOWN) {
00427 LOG_CS << "Upload aborted - unknown add-on type specified.\n";
00428 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);
00429 } else if (upload["author"].empty()) {
00430 LOG_CS << "Upload aborted - no add-on author specified.\n";
00431 network::send_data(construct_error("Add-on rejected: You did not specify the author(s) of the add-on in the pbl file!"), sock);
00432 } else if (upload["version"].empty()) {
00433 LOG_CS << "Upload aborted - no add-on version specified.\n";
00434 network::send_data(construct_error("Add-on rejected: You did not specify the version of the add-on in the pbl file!"), sock);
00435 } else if (upload["description"].empty()) {
00436 LOG_CS << "Upload aborted - no add-on description specified.\n";
00437 network::send_data(construct_error("Add-on rejected: You did not specify a description of the add-on in the pbl file!"), sock);
00438 } else if (upload["email"].empty()) {
00439 LOG_CS << "Upload aborted - no add-on email specified.\n";
00440 network::send_data(construct_error("Add-on rejected: You did not specify your email address in the pbl file!"), sock);
00441 } else if (!check_names_legal(data)) {
00442 LOG_CS << "Upload aborted - invalid file names in add-on data.\n";
00443 network::send_data(construct_error("Add-on rejected: The add-on contains an illegal file or directory name."
00444 " File or directory names may not contain any of the following characters: '/ \\ : ~'"), sock);
00445 } else if (campaign && (*campaign)["passphrase"].str() != upload["passphrase"]) {
00446 LOG_CS << "Upload aborted - incorrect passphrase.\n";
00447 network::send_data(construct_error("Add-on rejected: The add-on already exists, and your passphrase was incorrect."), sock);
00448 } else {
00449 LOG_CS << "Upload is owner upload.\n";
00450 std::string message = "Add-on accepted.";
00451
00452 if (!version_info(upload["version"]).good()) {
00453 message += "\n<255,255,0>Note: The version you specified is invalid. This add-on will be ignored for automatic update checks.";
00454 }
00455
00456 if(campaign == NULL) {
00457 campaign = &campaigns().add_child("campaign");
00458 }
00459
00460 (*campaign)["title"] = upload["title"];
00461 (*campaign)["name"] = upload["name"];
00462 (*campaign)["filename"] = "data/" + upload["name"].str();
00463 (*campaign)["passphrase"] = upload["passphrase"];
00464 (*campaign)["author"] = upload["author"];
00465 (*campaign)["description"] = upload["description"];
00466 (*campaign)["version"] = upload["version"];
00467 (*campaign)["icon"] = upload["icon"];
00468 (*campaign)["translate"] = upload["translate"];
00469 (*campaign)["dependencies"] = upload["dependencies"];
00470 (*campaign)["upload_ip"] = network::ip_address(sock);
00471 (*campaign)["type"] = upload["type"];
00472 (*campaign)["email"] = upload["email"];
00473
00474 if((*campaign)["downloads"].empty()) {
00475 (*campaign)["downloads"] = 0;
00476 }
00477 (*campaign)["timestamp"] = lexical_cast<std::string>(time(NULL));
00478
00479 int uploads = (*campaign)["uploads"].to_int() + 1;
00480 (*campaign)["uploads"] = uploads;
00481
00482 std::string filename = (*campaign)["filename"];
00483 data["title"] = (*campaign)["title"];
00484 data["name"] = "";
00485 data["campaign_name"] = (*campaign)["name"];
00486 data["author"] = (*campaign)["author"];
00487 data["description"] = (*campaign)["description"];
00488 data["version"] = (*campaign)["version"];
00489 data["timestamp"] = (*campaign)["timestamp"];
00490 data["icon"] = (*campaign)["icon"];
00491 data["type"] = (*campaign)["type"];
00492 (*campaign).clear_children("translation");
00493 find_translations(data, *campaign);
00494
00495 add_license(data);
00496
00497 {
00498 scoped_ostream campaign_file = ostream_file(filename);
00499 config_writer writer(*campaign_file, true, compress_level_);
00500 writer.write(data);
00501 }
00502
00503
00504 (*campaign)["size"] = lexical_cast<std::string>(
00505 file_size(filename));
00506 scoped_ostream cfgfile = ostream_file(file_);
00507 write(*cfgfile, cfg_);
00508 network::send_data(construct_message(message), sock);
00509
00510 fire("hook_post_upload", upload["name"]);
00511 }
00512 }
00513 else if (const config &erase = data.child("delete"))
00514 {
00515 LOG_CS << "deleting campaign '" << erase["name"] << "' requested from " << network::ip_address(sock) << "\n";
00516 const config &campaign = campaigns().find_child("campaign", "name", erase["name"]);
00517 if (!campaign) {
00518 network::send_data(construct_error("The add-on does not exist."), sock);
00519 continue;
00520 }
00521
00522 if (campaign["passphrase"] != erase["passphrase"]
00523 && (campaigns()["master_password"].empty()
00524 || campaigns()["master_password"] != erase["passphrase"]))
00525 {
00526 network::send_data(construct_error("The passphrase is incorrect."), sock);
00527 continue;
00528 }
00529
00530
00531 write_file(campaign["filename"], std::string());
00532 remove(campaign["filename"].str().c_str());
00533
00534 config::child_itors itors = campaigns().child_range("campaign");
00535 for (size_t index = 0; itors.first != itors.second;
00536 ++index, ++itors.first)
00537 {
00538 if (&campaign == &*itors.first) {
00539 campaigns().remove_child("campaign", index);
00540 break;
00541 }
00542 }
00543 scoped_ostream cfgfile = ostream_file(file_);
00544 write(*cfgfile, cfg_);
00545 network::send_data(construct_message("Add-on deleted."), sock);
00546
00547 fire("hook_post_erase", erase["name"]);
00548 }
00549 else if (const config &cpass = data.child("change_passphrase"))
00550 {
00551 config &campaign = campaigns().find_child("campaign", "name", cpass["name"]);
00552 if (!campaign) {
00553 network::send_data(construct_error("No add-on with that name exists."), sock);
00554 } else if (campaign["passphrase"] != cpass["passphrase"]) {
00555 network::send_data(construct_error("Your old passphrase was incorrect."), sock);
00556 } else if (cpass["new_passphrase"].empty()) {
00557 network::send_data(construct_error("No new passphrase was supplied."), sock);
00558 } else {
00559 campaign["passphrase"] = cpass["new_passphrase"];
00560 scoped_ostream cfgfile = ostream_file(file_);
00561 write(*cfgfile, cfg_);
00562 network::send_data(construct_message("Passphrase changed."), sock);
00563 }
00564 }
00565 }
00566 } catch(network::error& e) {
00567 if(!e.socket) {
00568 LOG_CS << "fatal network error: " << e.message << "\n";
00569 throw;
00570 } else {
00571 LOG_CS << "client disconnect: " << e.message << " " << network::ip_address(e.socket) << "\n";
00572 e.disconnect();
00573 }
00574 } catch(config::error& ) {
00575 LOG_CS << "error in receiving data...\n";
00576 network::disconnect(sock);
00577 }
00578
00579 SDL_Delay(20);
00580 }
00581 }
00582
00583 }
00584
00585 int main(int argc, char**argv)
00586 {
00587 game_config::path = get_cwd();
00588 lg::timestamps(true);
00589 try {
00590 printf("argc %d argv[0] %s 1 %s\n",argc,argv[0],argv[1]);
00591 std::string cfg_path = normalize_path("server.cfg");
00592 if(argc >= 2 && atoi(argv[1])){
00593 campaign_server(cfg_path, atoi(argv[1])).run();
00594 }else {
00595 campaign_server(cfg_path).run();
00596 }
00597 } catch(config::error& ) {
00598 std::cerr << "Could not parse config file\n";
00599 return 1;
00600 } catch(io_exception& ) {
00601 std::cerr << "File I/O error\n";
00602 return 2;
00603 } catch(network::error& e) {
00604 std::cerr << "Aborted with network error: " << e.message << '\n';
00605 return 3;
00606 }
00607 return 0;
00608 }