diff --git a/changelog b/changelog index f4d9b7f77e7..750c0b81d9a 100644 --- a/changelog +++ b/changelog @@ -1,4 +1,6 @@ Version 1.13.0-dev: + * Add-ons server: + * Add-on metadata pattern blacklisting implemented. * AI: * New Micro AI: Fast AI * Messenger Escort Micro AI: new optional parameters [filter], diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 351570a23d7..0a82bb57df1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1098,6 +1098,7 @@ if(ENABLE_CAMPAIGN_SERVER) set(campaignd_SRC network_worker.cpp # NEEDED when compiling with ANA support addon/validation.cpp + campaign_server/blacklist.cpp campaign_server/campaign_server.cpp server/input_stream.cpp ${network_implementation_files} diff --git a/src/SConscript b/src/SConscript index 83d6011d1a0..fde9ae233d3 100644 --- a/src/SConscript +++ b/src/SConscript @@ -581,6 +581,7 @@ if env["host"] in ["x86_64-nacl", "i686-nacl"]: client_env.WesnothProgram("wesnoth", wesnoth_objects, have_client_prereqs) campaignd_sources = Split(""" + campaign_server/blacklist.cpp server/input_stream.cpp """) diff --git a/src/campaign_server/blacklist.cpp b/src/campaign_server/blacklist.cpp new file mode 100644 index 00000000000..dbe2a212ad6 --- /dev/null +++ b/src/campaign_server/blacklist.cpp @@ -0,0 +1,134 @@ +/* + Copyright (C) 2014 by Ignacio Riquelme Morelle + Part of the Battle for Wesnoth Project http://www.wesnoth.org/ + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY. + + See the COPYING file for more details. +*/ + +#include "campaign_server/blacklist.hpp" + +#include "log.hpp" +#include "serialization/string_utils.hpp" +#include "serialization/unicode.hpp" + +#include + +static lg::log_domain log_campaignd_bl("campaignd/blacklist"); +#define LOG_BL LOG_STREAM(err, log_campaignd_bl) + +namespace campaignd +{ + +blacklist::blacklist() + : names_() + , titles_() + , descriptions_() + , authors_() + , ips_() + , emails_() +{ +} + +blacklist::blacklist(const config& cfg) + : names_() + , titles_() + , descriptions_() + , authors_() + , ips_() + , emails_() +{ + this->read(cfg); +} + +void blacklist::clear() +{ + names_.clear(); + titles_.clear(); + descriptions_.clear(); + + authors_.clear(); + ips_.clear(); + emails_.clear(); +} + +void blacklist::read(const config& cfg) +{ + parse_str_to_globlist(cfg["name"], names_); + parse_str_to_globlist(cfg["title"], titles_); + parse_str_to_globlist(cfg["description"], descriptions_); + + parse_str_to_globlist(cfg["author"], authors_); + parse_str_to_globlist(cfg["ip"], ips_); + parse_str_to_globlist(cfg["email"], emails_); +} + +void blacklist::parse_str_to_globlist(const std::string& str, blacklist::globlist& glist) +{ + glist = utils::split(str); +} + +bool blacklist::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 +{ + // Checks done in increasing order of performance impact and decreasing + // order of relevance. + return is_in_ip_masklist(ip, ips_) || + is_in_globlist(email, emails_) || + is_in_globlist(name, names_) || + is_in_globlist(title, titles_) || + is_in_globlist(author, authors_) || + is_in_globlist(description, descriptions_); +} + +bool blacklist::is_in_globlist(const std::string& str, const blacklist::globlist& glist) const +{ + if (!str.empty()) + { + const std::string& lc_str = utf8::lowercase(str); + BOOST_FOREACH(const std::string& glob, glist) + { + const std::string& lc_glob = utf8::lowercase(glob); + if (utils::wildcard_string_match(lc_str, lc_glob)) { + LOG_BL << "Blacklisted field found: " << str << " (" << glob << ")\n"; + return true; + } + } + } + + return false; +} + +bool blacklist::is_in_ip_masklist(const std::string& ip, const blacklist::globlist& mlist) const +{ + if (!ip.empty()) + { + BOOST_FOREACH(const std::string& ip_mask, mlist) + { + if (ip_matches(ip, ip_mask)) { + LOG_BL << "Blacklisted IP found: " << ip << " (" << ip_mask << ")\n"; + return true; + } + } + } + + return false; +} + +bool blacklist::ip_matches(const std::string& ip, const blacklist::glob& ip_mask) const +{ + // TODO: we want CIDR subnet mask matching here, not glob matching! + return utils::wildcard_string_match(ip, ip_mask); +} + +} diff --git a/src/campaign_server/blacklist.hpp b/src/campaign_server/blacklist.hpp new file mode 100644 index 00000000000..feb5527caa1 --- /dev/null +++ b/src/campaign_server/blacklist.hpp @@ -0,0 +1,82 @@ +/* + Copyright (C) 2014 by Ignacio Riquelme Morelle + Part of the Battle for Wesnoth Project http://www.wesnoth.org/ + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY. + + See the COPYING file for more details. +*/ + +#ifndef CAMPAIGN_SERVER_BLACKLIST_HPP_INCLUDED +#define CAMPAIGN_SERVER_BLACKLIST_HPP_INCLUDED + +#include "config.hpp" + +#include + +namespace campaignd +{ + +class blacklist : private boost::noncopyable +{ +public: + typedef std::string glob; + typedef std::vector globlist; + + blacklist(); + explicit blacklist(const config& cfg); + + void clear(); + + /** + * Initializes the blacklist from WML. + * + * @param cfg WML node object with the contents of the [blacklist] tag. + */ + void read(const config& cfg); + + /** + * Writes the blacklist to a WML node. + * + * @param cfg WML node object to write to. Any existing contents are + * erased by this method. + */ + void write(config& cfg) const; + + /** + * Whether an add-on described by these fields is blacklisted. + * + * Empty parameters are ignored. + */ + 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; + +private: + globlist names_; + globlist titles_; + globlist descriptions_; + + globlist authors_; + globlist ips_; + globlist emails_; + + void parse_str_to_globlist(const std::string& str, globlist& glist); + + bool is_in_globlist(const std::string& str, const globlist& glist) const; + + bool is_in_ip_masklist(const std::string& ip, const globlist& mlist) const; + bool ip_matches(const std::string& ip, const glob& ip_mask) const; +}; + +} + +#endif diff --git a/src/campaign_server/campaign_server.cpp b/src/campaign_server/campaign_server.cpp index 1d06a9cc73a..0f8c153e7e6 100644 --- a/src/campaign_server/campaign_server.cpp +++ b/src/campaign_server/campaign_server.cpp @@ -28,6 +28,7 @@ #include "serialization/unicode.hpp" #include "game_config.hpp" #include "addon/validation.hpp" +#include "campaign_server/blacklist.hpp" #include "version.hpp" #include "server/input_stream.hpp" #include "util.hpp" @@ -199,6 +200,8 @@ namespace { const config& server_info() const { return cfg_.child("server_info"); } config& server_info() { return cfg_.child("server_info"); } + void load_blacklist(); + config cfg_; const std::string file_; const network::manager net_manager_; @@ -210,6 +213,9 @@ namespace { /** Feedback URL format string used for add-ons. */ std::string feedback_url_format_; + campaignd::blacklist blacklist_; + std::string blacklist_file_; + const network::server_manager server_manager_; }; @@ -268,9 +274,35 @@ namespace { feedback_url_format_ = svinfo_cfg["feedback_url_format"].str(); } + blacklist_file_ = cfg_["blacklist_file"].str(); + load_blacklist(); + return cfg_["port"].to_int(default_campaignd_port); } + void campaign_server::load_blacklist() + { + // We *always* want to clear the blacklist first, especially if we are + // reloading the configuration and the blacklist is no longer enabled. + blacklist_.clear(); + + if(blacklist_file_.empty()) { + return; + } + + try { + scoped_istream stream = istream_file(blacklist_file_); + config cfg; + + read(cfg, *stream); + + blacklist_.read(cfg); + LOG_CS << "using blacklist from " << blacklist_file_ << '\n'; + } catch(const config::error&) { + LOG_CS << "ERROR: failed to read blacklist from " << blacklist_file_ << ", blacklist disabled\n"; + } + } + campaign_server::campaign_server(const std::string& cfgfile, size_t min_thread, size_t max_thread) : cfg_(), @@ -281,6 +313,8 @@ namespace { compress_level_(0), // Will be properly set by load_config() read_only_(false), feedback_url_format_(), // Will be properly set by load_config() + blacklist_(), + blacklist_file_(), server_manager_(load_config()) { #ifndef _MSC_VER @@ -542,6 +576,19 @@ namespace { 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()) {