Optimized server ban manager using integer ip for wildcard matching

Added who_banned and group fields to ban

Made reason required for banning
This commit is contained in:
Pauli Nieminen 2008-08-28 15:36:13 +00:00
parent 189a1821d7
commit 2beec3a977
3 changed files with 364 additions and 95 deletions

View file

@ -17,70 +17,228 @@
#include "filesystem.hpp"
#include "serialization/parser.hpp"
#include "serialization/binary_or_text.hpp"
#include "serialization/string_utils.hpp"
#include "ban.hpp"
#include <sstream>
#include <algorithm>
#include <boost/bind.hpp>
namespace wesnothd {
#define ERR_SERVER LOG_STREAM(err, mp_server)
#define LOG_SERVER LOG_STREAM(info, mp_server)
#define DBG_SERVER LOG_STREAM(debug, mp_server)
bool banned_compare::operator()(const banned_ptr a, const banned_ptr b) const
bool banned_compare::operator()(const banned_ptr& a, const banned_ptr& b) const
{
//! We want to move the lowest value to the top
return (*a) > (*b);
}
banned::banned(const std::string& ip, const time_t end_time, const std::string& reason) : ip_(ip), end_time_(end_time), reason_(reason), deleted_(false)
banned_compare_subnet::compare_fn banned_compare_subnet::active_ = &banned_compare_subnet::less;
void banned_compare_subnet::set_use_subnet_mask(bool use)
{
if (use)
{
assert(active_ == &banned_compare_subnet::less);
active_ = &banned_compare_subnet::less_with_subnet;
}
else
{
assert(active_ != &banned_compare_subnet::less);
active_ = &banned_compare_subnet::less;
}
}
bool banned_compare_subnet::operator()(const banned_ptr& a, const banned_ptr& b) const
{
return (this->*(active_))(a,b);
}
bool banned_compare_subnet::less(const banned_ptr& a, const banned_ptr& b) const
{
return a->get_int_ip() < b->get_int_ip();
}
bool banned_compare_subnet::less_with_subnet(const banned_ptr& a, const banned_ptr& b) const
{
return a->get_mask_ip(b->mask()) < b->get_mask_ip(a->mask());
}
subnet_compare_setter::subnet_compare_setter()
{
banned_compare_subnet::set_use_subnet_mask(true);
}
subnet_compare_setter::~subnet_compare_setter()
{
banned_compare_subnet::set_use_subnet_mask(false);
}
const std::string banned::who_banned_default_ = "system";
banned_ptr banned::create_dummy(const std::string& ip)
{
banned_ptr dummy(new banned(ip));
return dummy;
}
banned::banned(const std::string& ip) :
ip_(0),
mask_(0),
ip_text_(),
end_time_(0),
reason_(),
who_banned_(who_banned_default_)
{
ip_mask pair = parse_ip(ip);
ip_ = pair.first;
mask_ = 0xFFFFFFFF;
}
banned::banned(const std::string& ip,
const time_t end_time,
const std::string& reason,
const std::string& who_banned,
const std::string& group) :
ip_text_(ip),
end_time_(end_time),
reason_(reason),
who_banned_(who_banned),
group_(group)
{
ip_mask pair = parse_ip(ip_text_);
ip_ = pair.first;
mask_ = pair.second;
}
banned::banned(const config& cfg) :
ip_(),
ip_(0),
mask_(0),
ip_text_(),
end_time_(0),
reason_(),
deleted_(false)
who_banned_(who_banned_default_)
{
read(cfg);
}
banned::ip_mask banned::parse_ip(const std::string& ip) const
{
// We use bit operations to construct the integer
// ip_mask is a pair: first is ip and second is mask
ip_mask ret;
ret.first = 0;
ret.second = 0;
std::vector<std::string> split_ip = utils::split(ip, '.');
unsigned int shift = 4*8; // start shifting from the highest byte
unsigned int mask = 0xFF000000;
const unsigned int complite_part_mask = 0xFF;
std::vector<std::string>::const_iterator part = split_ip.begin();
bool wildcard = false;
do {
shift -= 8;
mask >>= 8;
if (part == split_ip.end())
{
if (!wildcard)
throw banned::error("Malformed ip address given for ban: " + ip);
// Adding 0 to ip and mask is nop
// we can then break out of loop
break;
} else {
if (*part == "*")
{
wildcard = true;
// Adding 0 to ip and mask is nop
} else {
wildcard = false;
unsigned int part_ip = lexical_cast_default<unsigned int>(*part, complite_part_mask + 1);
if (part_ip > complite_part_mask)
throw banned::error("Malformed ip address given for ban: " + ip);
ret.first |= (part_ip << shift);
ret.second |= (complite_part_mask << shift);
}
}
++part;
} while (shift);
return ret;
}
void banned::read(const config& cfg)
{
ip_ = cfg["ip"];
end_time_ = lexical_cast<time_t>(cfg["end_time"]);
reason_ = cfg["reason"];
deleted_ = utils::string_bool(cfg["deleted"]);
{
// parse ip and mask
ip_text_ = cfg["ip"];
ip_mask pair = parse_ip(ip_text_);
ip_ = pair.first;
mask_ = pair.second;
}
if (cfg.has_attribute("end_time"))
end_time_ = lexical_cast_default<time_t>(cfg["end_time"], 0);
reason_ = cfg["reason"];
// only overwrite defaults if exists
if (cfg.has_attribute("who_banned"))
who_banned_ = cfg["who_banned"];
if (cfg.has_attribute("group"))
group_ = cfg["group"];
}
void banned::write(config& cfg) const
{
std::stringstream ss;
cfg["ip"] = ip_;
ss << end_time_;
cfg["end_time"] = ss.str();
cfg["ip"] = get_ip();
if (end_time_ > 0)
{
std::stringstream ss;
ss << end_time_;
cfg["end_time"] = ss.str();
}
cfg["reason"] = reason_;
cfg["deleted"] = deleted_ ? "yes":"no";
if (who_banned_ != who_banned_default_)
{
cfg["who_banned"] = who_banned_;
}
if (!group_.empty())
{
cfg["group"] = group_;
}
}
std::string banned::get_human_end_time() const
std::string banned::get_human_end_time(const time_t& time)
{
if (end_time_ == 0)
if (time == 0)
{
return "permanent";
}
char buf[30];
struct tm* local;
local = localtime(&end_time_);
local = localtime(&time);
strftime(buf,30,"%H:%M:%S %d.%m.%Y", local );
return std::string(buf);
}
std::string banned::get_human_end_time() const
{
return banned::get_human_end_time(end_time_);
}
bool banned::operator>(const banned& b) const
{
return end_time_ > b.get_end_time();
}
unsigned int banned::get_mask_ip(unsigned int mask) const
{
return ip_ & mask & mask_;
}
void ban_manager::read()
{
if (filename_.empty() || !file_exists(filename_))
@ -93,13 +251,15 @@ namespace wesnothd {
for (config::child_list::const_iterator itor = bans.begin();
itor != bans.end(); ++itor)
{
banned_ptr new_ban(new banned(**itor));
if (!new_ban->is_deleted())
{
bans_[new_ban->get_ip()] = new_ban;
try {
banned_ptr new_ban(new banned(**itor));
assert(bans_.insert(new_ban).second);
if (new_ban->get_end_time() != 0)
time_queue_.push(new_ban);
} catch (banned::error& e) {
ERR_SERVER << e.message << " while reading bans\n";
}
if (new_ban->get_end_time() != 0)
time_queue_.push(new_ban);
}
}
@ -110,11 +270,11 @@ namespace wesnothd {
LOG_SERVER << "Writing bans to " << filename_ << "\n";
dirty_ = false;
config cfg;
for (ban_map::const_iterator itor = bans_.begin();
for (ban_set::const_iterator itor = bans_.begin();
itor != bans_.end(); ++itor)
{
config& child = cfg.add_child("ban");
itor->second->write(child);
(*itor)->write(child);
}
scoped_ostream ban_file = ostream_file(filename_);
config_writer writer(*ban_file, true, "");
@ -222,37 +382,70 @@ namespace wesnothd {
return ret;
}
void ban_manager::ban(const std::string& ip, const time_t& end_time, const std::string& reason)
std::string ban_manager::ban(const std::string& ip,
const time_t& end_time,
const std::string& reason,
const std::string& who_banned,
const std::string& group)
{
dirty_ = true;
ban_map::iterator ban;
if ((ban = bans_.find(ip)) != bans_.end())
{
// Already exsiting ban for ip. We have to first remove it
ban->second->remove_ban();
bans_.erase(ban);
ban_set::iterator ban;
try {
if ((ban = bans_.find(banned::create_dummy(ip))) != bans_.end())
{
// Already exsiting ban for ip. We have to first remove it
bans_.erase(ban);
LOG_SERVER << "Overwriting ban: " << (*ban)->get_ip() << " reason was: " << (*ban)->get_reason() << "\n";
}
} catch (banned::error& e) {
ERR_SERVER << e.message << " while creating dummy ban for finding existing ban\n";
return e.message;
}
banned_ptr new_ban(new banned(ip, end_time, reason));
bans_.insert(ban_map::value_type(ip,new_ban));
if (end_time != 0)
time_queue_.push(new_ban);
try {
banned_ptr new_ban(new banned(ip, end_time, reason,who_banned, group));
bans_.insert(new_ban);
if (end_time != 0)
time_queue_.push(new_ban);
} catch (banned::error& e) {
ERR_SERVER << e.message << " while banning\n";
return e.message;
}
dirty_ = true;
return std::string();
}
void ban_manager::unban(std::ostringstream& os, const std::string& ip)
{
dirty_ = true;
ban_map::iterator ban = bans_.find(ip);
ban_set::iterator ban;
try {
ban = bans_.find(banned::create_dummy(ip));
} catch (banned::error& e) {
ERR_SERVER << e.message << "\n";
os << e.message << "\n";
return;
}
if (ban == bans_.end())
{
os << "There is no ban on '" << ip << "'.";
return;
}
ban->second->remove_ban();
bans_.erase(ban);
dirty_ = true;
os << "Ban on '" << ip << "' removed.";
}
void ban_manager::unban_group(std::ostringstream& os, const std::string& group)
{
ban_set temp;
std::insert_iterator<ban_set> temp_inserter(temp, temp.begin());
std::remove_copy_if(bans_.begin(), bans_.end(), temp_inserter, boost::bind(&banned::match_group,boost::bind(&banned_ptr::get,_1),group));
os << "Removed " << (bans_.size() - temp.size()) << " bans";
bans_.swap(temp);
dirty_ = true;
}
void ban_manager::check_ban_times(time_t time_now)
{
while (!time_queue_.empty())
@ -266,20 +459,10 @@ namespace wesnothd {
break;
}
if (ban->is_deleted())
{
// This was allready deleted have to free memory;
time_queue_.pop();
continue;
}
// No need to make dirty because
// these bans will be handled correctly in next load.
// This ban is going to expire so delete it.
LOG_SERVER << "Remove a ban " << ban->get_ip() << ". time: " << time_now << " end_time " << ban->get_end_time() << "\n";
bans_.erase(bans_.find(ban->get_ip()));
std::ostringstream os;
unban(os, ban->get_ip());
time_queue_.pop();
}
@ -295,13 +478,30 @@ namespace wesnothd {
return;
}
std::set<std::string> groups;
out << "BAN LIST\n";
for (ban_map::const_iterator i = bans_.begin();
for (ban_set::const_iterator i = bans_.begin();
i != bans_.end(); ++i)
{
out << "IP: '" << i->second->get_ip() <<
"' end_time: '" << i->second->get_human_end_time() <<
"' reason: '" << i->second->get_reason() << "'\n";
if ((*i)->get_group().empty())
{
out << "IP: '" << (*i)->get_ip() <<
"' end_time: '" << (*i)->get_human_end_time() <<
"' reason: '" << (*i)->get_reason() <<
"' issuer: "<< (*i)->get_who_banned() << "\n";
} else {
groups.insert((*i)->get_group());
}
}
if (!groups.empty())
{
out << "ban groups:\n";
out << *groups.begin();
std::ostream& (*fn)(std::ostream&,const std::string&) = &std::operator<<;
std::for_each( ++groups.begin(), groups.end(), boost::bind(fn,boost::bind(fn,boost::ref(out),std::string(", ")),_1));
}
}
@ -309,19 +509,20 @@ namespace wesnothd {
bool ban_manager::is_ip_banned(std::string ip) const
{
for (ban_map::const_iterator i = bans_.begin(); i != bans_.end(); ++i) {
if (utils::wildcard_string_match(ip, i->first)) {
DBG_SERVER << "Comparing ban '" << i->first << "' vs '..." << ip << "'\t" << "banned.\n";
return true;
}
DBG_SERVER << "Comparing ban '" << i->first << "' vs '..." << ip << "'\t" << "not banned.\n";
subnet_compare_setter setter;
ban_set::const_iterator ban;
try {
ban = bans_.find(banned::create_dummy(ip));
} catch (banned::error& e) {
ERR_SERVER << e.message << " in is_ip_banned\n";
return false;
}
return false;
return ban != bans_.end();
}
void ban_manager::init_ban_help()
{
ban_help_ = "ban <ip|nickname> [<time>] [<reason>]\nTime is give in format ‰d[‰s[‰d‰s[...]]] (where ‰s is s, m, h, D, M or Y).\nIf no time modifier is given minutes are used.\n";
ban_help_ = "ban <ip|nickname> [<time>] <reason>\nTime is give in format ‰d[‰s[‰d‰s[...]]] (where ‰s is s, m, h, D, M or Y).\nIf no time modifier is given minutes are used.\n";
default_ban_times::iterator itor = ban_times_.begin();
if (itor != ban_times_.end())
{

View file

@ -14,6 +14,9 @@
#ifndef SERVER_GAME_HPP_INCLUDED
#define SERVER_GAME_HPP_INCLUDED
#include "game_errors.hpp"
#include <set>
#include <map>
#include <queue>
#include <ctime>
@ -30,22 +33,45 @@ namespace wesnothd {
//! We want to move the lowest value to the top
struct banned_compare {
bool operator()(const banned_ptr a, const banned_ptr b) const;
bool operator()(const banned_ptr& a, const banned_ptr& b) const;
};
typedef std::map<std::string, banned_ptr> ban_map;
class subnet_compare_setter;
struct banned_compare_subnet {
bool operator()(const banned_ptr& a, const banned_ptr& b) const;
private:
static void set_use_subnet_mask(bool);
bool less(const banned_ptr& a, const banned_ptr& b) const;
bool less_with_subnet(const banned_ptr& a, const banned_ptr& b) const;
typedef bool (banned_compare_subnet::*compare_fn)(const banned_ptr& a, const banned_ptr& b) const;
static compare_fn active_;
friend class subnet_compare_setter;
};
struct subnet_compare_setter {
subnet_compare_setter();
~subnet_compare_setter();
};
typedef std::set<banned_ptr,banned_compare_subnet > ban_set;
typedef std::priority_queue<banned_ptr,std::vector<banned_ptr>, banned_compare> ban_time_queue;
typedef std::map<std::string, size_t> default_ban_times;
class banned {
std::string ip_;
unsigned int ip_;
unsigned int mask_;
std::string ip_text_;
time_t end_time_;
std::string reason_;
bool deleted_;
std::string who_banned_;
std::string group_;
static const std::string who_banned_default_;
typedef std::pair<unsigned int, unsigned int> ip_mask;
ip_mask parse_ip(const std::string&) const;
banned(const std::string& ip);
public:
banned(const std::string& ip, const time_t end_time, const std::string& reason);
banned(const std::string& ip, const time_t end_time, const std::string& reason, const std::string& who_banned=who_banned_default_, const std::string& group="");
banned(const config&);
void read(const config&);
@ -55,27 +81,42 @@ namespace wesnothd {
{ return end_time_; }
std::string get_human_end_time() const;
static std::string get_human_end_time(const time_t&);
std::string get_reason() const
{ return reason_; }
std::string get_ip() const
std::string get_ip() const
{ return ip_text_; }
std::string get_group() const
{ return group_; }
std::string get_who_banned() const
{ return who_banned_; }
bool match_group(const std::string& group) const
{ return group_ == group; }
unsigned int get_mask_ip(unsigned int) const;
unsigned int get_int_ip() const
{ return ip_; }
void remove_ban()
{ deleted_ = true; }
unsigned int mask() const
{ return mask_; }
bool is_deleted() const
{ return deleted_; }
static banned_ptr create_dummy(const std::string& ip);
//! Notice that comparision is done wrong way to make the smallest value in top of heap
bool operator>(const banned& b) const;
struct error : public ::game::error {
error(const std::string& message) : ::game::error(message) {}
};
};
class ban_manager
{
ban_map bans_;
ban_set bans_;
ban_time_queue time_queue_;
default_ban_times ban_times_;
std::string ban_help_;
@ -97,8 +138,10 @@ namespace wesnothd {
time_t parse_time(std::string time_in) const;
void ban(const std::string&, const time_t&, const std::string&);
std::string ban(const std::string&, const time_t&, const std::string&, const std::string&, const std::string&);
void unban(std::ostringstream& os, const std::string& ip);
void unban_group(std::ostringstream& os, const std::string& group);
void check_ban_times(time_t time_now);

View file

@ -283,7 +283,7 @@ private:
void process_query(const network::connection sock,
simple_wml::node& query);
//! Process commands from admins and users.
std::string process_command(const std::string& cmd);
std::string process_command(const std::string& cmd, const std::string& issuer_name);
//! Handle private messages between players.
void process_whisper(const network::connection sock,
simple_wml::node& whisper) const;
@ -461,7 +461,7 @@ void server::run() {
// Then client would reconnect to new server automaticaly.
// This would also allow server to move to new port or address if there is need
process_command("msg All games ended. Shutting down now. Reconnect to the new server.");
process_command("msg All games ended. Shutting down now. Reconnect to the new server.", "system");
throw network::error("shut down");
}
if (config_reload == 1) {
@ -472,7 +472,7 @@ void server::run() {
// Process commands from the server socket/fifo
std::string admin_cmd;
if (input_.read_line(admin_cmd)) {
process_command(admin_cmd);
process_command(admin_cmd, "socket");
}
time_t now = time(NULL);
@ -852,15 +852,15 @@ void server::process_query(const network::connection sock,
LOG_SERVER << "Admin Command:" << "\ttype: " << command
<< "\tIP: "<< network::ip_address(sock)
<< "\tnick: "<< pl->second.name() << std::endl;
response << process_command(command.to_string());
response << process_command(command.to_string(), pl->second.name());
// Commands a player may issue.
} else if (command == "help") {
response << help_msg;
} else if (command == "status") {
response << process_command(command.to_string() + " " + pl->second.name());
response << process_command(command.to_string() + " " + pl->second.name(), pl->second.name());
} else if (command == "status " + pl->second.name() || command == "metrics"
|| command == "motd" || command == "wml" || command == "netstats") {
response << process_command(command.to_string());
|| command == "motd" || command == "wml" || command == "netstats" || command == "netstats all") {
response << process_command(command.to_string(), pl->second.name());
} else if (command == admin_passwd_) {
LOG_SERVER << "New Admin recognized:" << "\tIP: "
<< network::ip_address(sock) << "\tnick: "
@ -890,7 +890,7 @@ void server::start_new_server() {
LOG_SERVER << "New server started with command: " << restart_command << "\n";
}
std::string server::process_command(const std::string& query) {
std::string server::process_command(const std::string& query, const std::string& issuer_name) {
std::ostringstream out;
const std::string::const_iterator i = std::find(query.begin(),query.end(),' ');
const std::string command(query.begin(),i);
@ -908,7 +908,7 @@ std::string server::process_command(const std::string& query) {
server_.stop();
input_.stop();
graceful_restart = true;
process_command("msg The server is shutting down. You may finish your games but can't start new ones. Once all games have ended the server will exit.");
process_command("msg The server is shutting down. You may finish your games but can't start new ones. Once all games have ended the server will exit.", issuer_name);
out << "Server is doing graceful shut down.";
}
@ -925,7 +925,7 @@ std::string server::process_command(const std::string& query) {
input_.stop();
// start new server
start_new_server();
process_command("msg The server has been restarted. You may finish your games but can't start new ones and new players can't join this server.");
process_command("msg The server has been restarted. You may finish your games but can't start new ones and new players can't join this server.", issuer_name);
out << "New server started.";
}
#endif
@ -977,19 +977,27 @@ std::string server::process_command(const std::string& query) {
<< stats.bytes_received << " bytes\n";
}
}
} else if (command == "ban" || command == "bans" || command == "kban" || command == "kickban") {
} else if (command == "ban" || command == "bans" || command == "kban" || command == "kickban" || command == "gban") {
if (parameters == "") {
ban_manager_.list_bans(out);
} else {
bool banned_ = false;
const bool kick = (command == "kban" || command == "kickban");
const std::string::iterator first_space = std::find(parameters.begin(), parameters.end(), ' ');
const bool group_ban = command == "gban";
std::string::iterator first_space = std::find(parameters.begin(), parameters.end(), ' ');
if (first_space == parameters.end())
{
return ban_manager_.get_ban_help();
}
std::string::iterator second_space = std::find(first_space+1, parameters.end(), ' ');
const std::string target(parameters.begin(), first_space);
std::string group;
if (group_ban)
{
group = std::string(first_space+1, second_space);
first_space = second_space;
second_space = std::find(first_space+1, parameters.end(), ' ');
}
const std::string time(first_space+1,second_space);
time_t parsed_time = ban_manager_.parse_time(time);
if (parsed_time == 0)
@ -1003,13 +1011,19 @@ std::string server::process_command(const std::string& query) {
}
std::string reason(second_space + 1, parameters.end());
utils::strip(reason);
if (reason.empty())
return ban_manager_.get_ban_help();
// if we find a '.' consider it an ip mask
//! @todo FIXME: should also check for only numbers
if (std::count(target.begin(), target.end(), '.') >= 1) {
banned_ = true;
out << "Set ban on '" << target << "' with end time '" << parsed_time << "' with reason: '" << reason << "'.\n";
ban_manager_.ban(target, parsed_time, reason);
std::string err = ban_manager_.ban(target, parsed_time, reason, issuer_name, group);
if (err.empty())
out << "Set ban on '" << target << "' with end time '" << wesnothd::banned::get_human_end_time(parsed_time) << "' with reason: '" << reason << "'.\n";
else
out << err << "\n";
if (kick) {
for (player_map::const_iterator pl = players_.begin();
@ -1029,9 +1043,12 @@ std::string server::process_command(const std::string& query) {
banned_ = true;
const std::string& ip = network::ip_address(pl->first);
if (!is_ip_banned(ip)) {
ban_manager_.ban(ip,parsed_time, reason);
out << "Set ban on '" << ip << "' with end time '" << parsed_time << "' with reason: '"
<< reason << "'.\n";
std::string err = ban_manager_.ban(ip,parsed_time, reason, issuer_name, group);
if (err.empty())
out << "Set ban on '" << ip << "' with end time '" << wesnothd::banned::get_human_end_time(parsed_time) << "' with reason: '"
<< reason << "'.\n";
else
out << err << "\n";
}
if (kick) {
out << "Kicked " << pl->second.name() << ".\n";
@ -1049,6 +1066,11 @@ std::string server::process_command(const std::string& query) {
return "You must enter an ipmask to unban.";
}
ban_manager_.unban(out, parameters);
} else if (command == "ungban") {
if (parameters == "") {
return "You must enter an ipmask to unban.";
}
ban_manager_.unban_group(out, parameters);
} else if (command == "kick") {
if (parameters == "") {
return "You must enter a mask to kick.";
@ -1715,6 +1737,9 @@ int main(int argc, char** argv) {
#endif
std::string fifo_path = std::string(FIFODIR) + "/socket";
// setting path to currentworking directory
game_config::path = get_cwd();
// show 'info' by default
lg::set_log_domain_severity("server", 2);
lg::timestamps(true);