Browse Source

Revamped ban system internals:

* Rewrote the database format using Jupiter::Database
* Replaced 'active' with 'flags' in ban entries, adding support for many types of bans
* Added 'prefix_length' to entries; netmask banning is now supported internally
* Moved 'banner' to ban entry from varData
* "bansearch" IRC command now includes type (flags) information
General bug fixes and optimization
pull/3/head
JustinAJ 9 years ago
parent
commit
0694c5146c
  1. 2
      Jupiter
  2. BIN
      Release/Bot.lib
  3. BIN
      Release/Plugins/RenX.Core.lib
  4. 119
      RenX.Commands/RenX_Commands.cpp
  5. 4
      RenX.Commands/RenX_Commands.h
  6. 288
      RenX.Core/RenX_BanDatabase.cpp
  7. 115
      RenX.Core/RenX_BanDatabase.h
  8. 3
      RenX.Core/RenX_PlayerInfo.h
  9. 169
      RenX.Core/RenX_Server.cpp
  10. 6
      RenX.Core/RenX_Server.h
  11. 18
      RenX.ExcessiveHeadshots/ExcessiveHeadshots.cpp
  12. 5
      RenX.Ladder/RenX_Ladder.cpp
  13. 4
      RenX.Logging/RenX_Logging.cpp
  14. 6
      RenX.Warn/RenX_Warn.cpp

2
Jupiter

@ -1 +1 @@
Subproject commit 5338777b91ca98bfcd4aea3620a69071d42701f0 Subproject commit 5b83899a03642d18deb6a85dabfb1afd8d76422e

BIN
Release/Bot.lib

Binary file not shown.

BIN
Release/Plugins/RenX.Core.lib

Binary file not shown.

119
RenX.Commands/RenX_Commands.cpp

@ -1,5 +1,5 @@
/** /**
* Copyright (C) 2014-2015 Jessica James. * Copyright (C) 2014-2016 Jessica James.
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@ -48,12 +48,6 @@ inline void onDie(RenX::Server *server, const RenX::PlayerInfo *player)
server->kickPlayer(player, Jupiter::StringS::empty); server->kickPlayer(player, Jupiter::StringS::empty);
} }
bool RenX_CommandsPlugin::RenX_OnBan(RenX::Server *server, const RenX::PlayerInfo *player, Jupiter::StringType &data)
{
data = player->varData.get(this->getName(), STRING_LITERAL_AS_REFERENCE("banner"));
return !data.isEmpty();
}
void RenX_CommandsPlugin::RenX_OnSuicide(RenX::Server *server, const RenX::PlayerInfo *player, const Jupiter::ReadableString &) void RenX_CommandsPlugin::RenX_OnSuicide(RenX::Server *server, const RenX::PlayerInfo *player, const Jupiter::ReadableString &)
{ {
onDie(server, player); onDie(server, player);
@ -1122,43 +1116,38 @@ void BanSearchIRCCommand::trigger(IRC_Bot *source, const Jupiter::ReadableString
default: default:
case 0: // ANY case 0: // ANY
return isMatch(1) || isMatch(2) || isMatch(3) || isMatch(4); return isMatch(1) || isMatch(2) || isMatch(3) || isMatch(4);
case 1: // IP case 1: // ALL
return true;
case 2: // IP
return entry->ip == params.asUnsignedInt(); return entry->ip == params.asUnsignedInt();
case 2: // RDNS case 3: // RDNS
return entry->rdns.equals(params); return entry->rdns.equals(params);
case 3: // STEAM case 4: // STEAM
return entry->steamid == params.asUnsignedLongLong(); return entry->steamid == params.asUnsignedLongLong();
case 4: // NAME case 5: // NAME
return entry->name.equalsi(params); return entry->name.equalsi(params);
case 5: // BANNER case 6: // BANNER
return entry->varData.get(pluginInstance.getName()).equalsi(params); return entry->varData.get(pluginInstance.getName()).equalsi(params);
case 6: // ACTIVE case 7: // ACTIVE
if (params.asBool()) // Got tired of seeing a compiler warning. return params.asBool() == entry->is_active();
return entry->active == 1;
else
return entry->active == 0;
case 7: // ALL
return true;
} }
}; };
unsigned int type; unsigned int type;
Jupiter::ReferenceString type_str = Jupiter::ReferenceString::getWord(parameters, 0, WHITESPACE); Jupiter::ReferenceString type_str = Jupiter::ReferenceString::getWord(parameters, 0, WHITESPACE);
if (type_str.equalsi(STRING_LITERAL_AS_REFERENCE("ip"))) if (type_str.equalsi(STRING_LITERAL_AS_REFERENCE("all")) || type_str.equals('*'))
type = 1; type = 1;
else if (type_str.equalsi(STRING_LITERAL_AS_REFERENCE("rdns"))) else if (type_str.equalsi(STRING_LITERAL_AS_REFERENCE("ip")))
type = 2; type = 2;
else if (type_str.equalsi(STRING_LITERAL_AS_REFERENCE("steam"))) else if (type_str.equalsi(STRING_LITERAL_AS_REFERENCE("rdns")))
type = 3; type = 3;
else if (type_str.equalsi(STRING_LITERAL_AS_REFERENCE("name"))) else if (type_str.equalsi(STRING_LITERAL_AS_REFERENCE("steam")))
type = 4; type = 4;
else if (type_str.equalsi(STRING_LITERAL_AS_REFERENCE("banner"))) else if (type_str.equalsi(STRING_LITERAL_AS_REFERENCE("name")))
type = 5; type = 5;
else if (type_str.equalsi(STRING_LITERAL_AS_REFERENCE("active"))) else if (type_str.equalsi(STRING_LITERAL_AS_REFERENCE("banner")))
type = 6; type = 6;
else if (type_str.equalsi(STRING_LITERAL_AS_REFERENCE("any"))) else if (type_str.equalsi(STRING_LITERAL_AS_REFERENCE("active")))
type = 0;
else if (type_str.equalsi(STRING_LITERAL_AS_REFERENCE("all")) || type_str.equals('*'))
type = 7; type = 7;
else else
{ {
@ -1167,17 +1156,43 @@ void BanSearchIRCCommand::trigger(IRC_Bot *source, const Jupiter::ReadableString
} }
Jupiter::String out(256); Jupiter::String out(256);
Jupiter::String types(64);
char timeStr[256]; char timeStr[256];
for (size_t i = 0; i != entries.size(); i++) for (size_t i = 0; i != entries.size(); i++)
{ {
entry = entries.get(i); entry = entries.get(i);
if (isMatch(type)) if (isMatch(type))
{ {
Jupiter::StringS ip_str = Jupiter::Socket::ntop4(entry->ip); Jupiter::StringS &ip_str = Jupiter::Socket::ntop4(entry->ip);
const Jupiter::ReadableString &banner = entry->varData.get(pluginInstance.getName()); const Jupiter::ReadableString &banner = entry->varData.get(pluginInstance.getName());
strftime(timeStr, sizeof(timeStr), "%b %d %Y; Time: %H:%M:%S", localtime(&(entry->timestamp))); strftime(timeStr, sizeof(timeStr), "%b %d %Y; Time: %H:%M:%S", localtime(&(entry->timestamp)));
out.format("ID: %lu; Status: %sactive; Date: %s; IP: %.*s; Steam: %llu; Name: %.*s%s", i, entry->active ? "" : "in", timeStr, ip_str.size(), ip_str.ptr(), entry->steamid, entry->name.size(), entry->name.ptr(), banner.isEmpty() ? "" : "; Banner: ");
out.concat(banner); if ((entry->flags & 0x7F) == 0)
types = " NULL;"_jrs;
else
{
types.erase();
if (entry->is_type_game())
types += " game"_jrs;
if (entry->is_type_chat())
types += " chat"_jrs;
if (entry->is_type_bot())
types += " bot"_jrs;
if (entry->is_type_vote())
types += " vote"_jrs;
if (entry->is_type_mine())
types += " mine"_jrs;
if (entry->is_type_ladder())
types += " ladder"_jrs;
if (entry->is_type_alert())
types += " alert"_jrs;
types += ";"_jrs;
}
out.format("ID: %lu (%sactive); Date: %s; IP: %.*s/%u; Steam: %llu; Types:%.*s Name: %.*s; Banner: %.*s",
i, entry->is_active() ? "" : "in", timeStr, ip_str.size(), ip_str.ptr(), entry->prefix_length, entry->steamid, types.size(), types.ptr(),
entry->name.size(), entry->name.ptr(), banner.size(), banner.ptr());
if (entry->rdns.isNotEmpty()) if (entry->rdns.isNotEmpty())
{ {
out.concat("; RDNS: "_jrs); out.concat("; RDNS: "_jrs);
@ -1883,8 +1898,7 @@ void TempBanIRCCommand::trigger(IRC_Bot *source, const Jupiter::ReadableString &
player = server->getPlayerByPartName(name); player = server->getPlayerByPartName(name);
if (player != nullptr) if (player != nullptr)
{ {
player->varData.set(pluginInstance.getName(), STRING_LITERAL_AS_REFERENCE("banner"), nick); server->banPlayer(player, banner, reason, pluginInstance.getTBanTime());
server->banPlayer(player, reason, pluginInstance.getTBanTime());
kicks++; kicks++;
} }
} }
@ -1941,8 +1955,7 @@ void KickBanIRCCommand::trigger(IRC_Bot *source, const Jupiter::ReadableString &
player = server->getPlayerByPartName(name); player = server->getPlayerByPartName(name);
if (player != nullptr) if (player != nullptr)
{ {
player->varData.set(pluginInstance.getName(), STRING_LITERAL_AS_REFERENCE("banner"), nick); server->banPlayer(player, banner, reason);
server->banPlayer(player, reason);
kicks++; kicks++;
} }
} }
@ -2519,37 +2532,29 @@ void ModRequestGameCommand::trigger(RenX::Server *source, RenX::PlayerInfo *play
Jupiter::IRC::Client::Channel *channel; Jupiter::IRC::Client::Channel *channel;
unsigned int channelCount; unsigned int channelCount;
unsigned int messageCount = 0; unsigned int messageCount = 0;
int type;
Jupiter::String &fmtName = RenX::getFormattedPlayerName(player); Jupiter::String &fmtName = RenX::getFormattedPlayerName(player);
Jupiter::StringL msg = Jupiter::StringL::Format(IRCCOLOR "12[Moderator Request] " IRCCOLOR IRCBOLD "%.*s" IRCBOLD IRCCOLOR "07 has requested assistance in-game; please look in ", fmtName.size(), fmtName.ptr()); Jupiter::StringL msg = Jupiter::StringL::Format(IRCCOLOR "12[Moderator Request] " IRCCOLOR IRCBOLD "%.*s" IRCBOLD IRCCOLOR "07 has requested assistance in-game; please look in ", fmtName.size(), fmtName.ptr());
Jupiter::StringS msg2 = Jupiter::StringS::Format(IRCCOLOR "12[Moderator Request] " IRCCOLOR IRCBOLD "%.*s" IRCBOLD IRCCOLOR "07 has requested assistance in-game!" IRCCOLOR, fmtName.size(), fmtName.ptr()); Jupiter::StringS msg2 = Jupiter::StringS::Format(IRCCOLOR "12[Moderator Request] " IRCCOLOR IRCBOLD "%.*s" IRCBOLD IRCCOLOR "07 has requested assistance in-game!" IRCCOLOR, fmtName.size(), fmtName.ptr());
for (unsigned int a = 0; a < serverCount; a++) for (unsigned int a = 0; a < serverCount; a++)
{ {
server = serverManager->getServer(a); server = serverManager->getServer(a);
if (server != nullptr) channelCount = server->getChannelCount();
for (unsigned int b = 0; b < channelCount; b++)
{ {
channelCount = server->getChannelCount(); channel = server->getChannel(b);
for (unsigned int b = 0; b < channelCount; b++) if (source->isLogChanType(channel->getType()))
{ {
channel = server->getChannel(b); server->sendMessage(channel->getName(), msg2);
if (channel != nullptr) msg += channel->getName();
for (unsigned int c = 0; c < channel->getUserCount(); c++)
{ {
type = channel->getType(); if (channel->getUserPrefix(c) != 0 && channel->getUser(c)->getNickname().equals(server->getNickname()) == false)
if (source->isLogChanType(type))
{ {
server->sendMessage(channel->getName(), msg2); server->sendMessage(channel->getUser(c)->getUser()->getNickname(), msg);
msg += channel->getName(); messageCount++;
for (unsigned int c = 0; c < channel->getUserCount(); c++)
{
if (channel->getUserPrefix(c) != 0 && channel->getUser(c)->getNickname().equals(server->getNickname()) == false)
{
server->sendMessage(channel->getUser(c)->getUser()->getNickname(), msg);
messageCount++;
}
}
msg -= channel->getName().size();
} }
} }
msg -= channel->getName().size();
} }
} }
} }
@ -2807,8 +2812,7 @@ void TempBanGameCommand::trigger(RenX::Server *source, RenX::PlayerInfo *player,
source->sendMessage(player, STRING_LITERAL_AS_REFERENCE("Error: You can not ban higher level moderators.")); source->sendMessage(player, STRING_LITERAL_AS_REFERENCE("Error: You can not ban higher level moderators."));
else else
{ {
target->varData.set(pluginInstance.getName(), STRING_LITERAL_AS_REFERENCE("banner"), player->name); source->banPlayer(target, player->name, reason, pluginInstance.getTBanTime());
source->banPlayer(target, reason, pluginInstance.getTBanTime());
source->sendMessage(player, STRING_LITERAL_AS_REFERENCE("Player has been temporarily banned and kicked from the game.")); source->sendMessage(player, STRING_LITERAL_AS_REFERENCE("Player has been temporarily banned and kicked from the game."));
} }
} }
@ -2850,8 +2854,7 @@ void KickBanGameCommand::trigger(RenX::Server *source, RenX::PlayerInfo *player,
source->sendMessage(player, STRING_LITERAL_AS_REFERENCE("Error: You can not ban higher level moderators.")); source->sendMessage(player, STRING_LITERAL_AS_REFERENCE("Error: You can not ban higher level moderators."));
else else
{ {
target->varData.set(pluginInstance.getName(), STRING_LITERAL_AS_REFERENCE("banner"), player->name); source->banPlayer(target, player->name, reason);
source->banPlayer(target, reason);
source->sendMessage(player, STRING_LITERAL_AS_REFERENCE("Player has been banned and kicked from the game.")); source->sendMessage(player, STRING_LITERAL_AS_REFERENCE("Player has been banned and kicked from the game."));
} }
} }

4
RenX.Commands/RenX_Commands.h

@ -1,5 +1,5 @@
/** /**
* Copyright (C) 2014-2015 Jessica James. * Copyright (C) 2014-2016 Jessica James.
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@ -27,8 +27,6 @@
class RenX_CommandsPlugin : public RenX::Plugin class RenX_CommandsPlugin : public RenX::Plugin
{ {
public: // RenX::Plugin public: // RenX::Plugin
bool RenX_OnBan(RenX::Server *server, const RenX::PlayerInfo *player, Jupiter::StringType &data);
void RenX_OnSuicide(RenX::Server *server, const RenX::PlayerInfo *player, const Jupiter::ReadableString &damageType) override; void RenX_OnSuicide(RenX::Server *server, const RenX::PlayerInfo *player, const Jupiter::ReadableString &damageType) override;
void RenX_OnKill(RenX::Server *server, const RenX::PlayerInfo *player, const RenX::PlayerInfo *victim, const Jupiter::ReadableString &damageType) override; void RenX_OnKill(RenX::Server *server, const RenX::PlayerInfo *player, const RenX::PlayerInfo *victim, const Jupiter::ReadableString &damageType) override;
void RenX_OnDie(RenX::Server *server, const RenX::PlayerInfo *player, const Jupiter::ReadableString &damageType) override; void RenX_OnDie(RenX::Server *server, const RenX::PlayerInfo *player, const Jupiter::ReadableString &damageType) override;

288
RenX.Core/RenX_BanDatabase.cpp

@ -1,5 +1,5 @@
/** /**
* Copyright (C) 2014-2015 Jessica James. * Copyright (C) 2014-2016 Jessica James.
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@ -30,170 +30,74 @@ RenX::BanDatabase _banDatabase;
RenX::BanDatabase *RenX::banDatabase = &_banDatabase; RenX::BanDatabase *RenX::banDatabase = &_banDatabase;
RenX::BanDatabase &RenX::defaultBanDatabase = _banDatabase; RenX::BanDatabase &RenX::defaultBanDatabase = _banDatabase;
const Jupiter::ReferenceString DEFAULT_REASON = "(No reason information provided)"; void RenX::BanDatabase::process_data(Jupiter::DataBuffer &buffer, FILE *file, fpos_t pos)
const uint8_t write_version = 2U;
bool RenX::BanDatabase::load(const Jupiter::ReadableString &fname)
{ {
RenX::BanDatabase::filename = fname; if (RenX::BanDatabase::read_version < 3U)
FILE *file = fopen(RenX::BanDatabase::filename.c_str(), "rb"); return; // incompatible database version
if (file != nullptr)
{ RenX::BanDatabase::Entry *entry = new RenX::BanDatabase::Entry();
RenX::BanDatabase::version = fgetc(file); entry->pos = pos;
while (!feof(file))
if (fgetc(file) == '\n') // Read data from buffer to entry
break; entry->flags = buffer.pop<uint8_t>();
Jupiter::String rdns(128); entry->timestamp = buffer.pop<time_t>();
Jupiter::String playerName(16); entry->length = buffer.pop<time_t>();
Jupiter::String key(32); entry->steamid = buffer.pop<uint64_t>();
Jupiter::String value(32); entry->ip = buffer.pop<uint32_t>();
Jupiter::String reason(128); entry->prefix_length = buffer.pop<uint32_t>();
if (RenX::BanDatabase::version == 0) entry->rdns = buffer.pop<Jupiter::String_Strict, char>();
reason = DEFAULT_REASON; entry->name = buffer.pop<Jupiter::String_Strict, char>();
Entry *entry; entry->banner = buffer.pop<Jupiter::String_Strict, char>();
int c; entry->reason = buffer.pop<Jupiter::String_Strict, char>();
while (!feof(file))
{ // Read varData from buffer to entry
entry = new Entry(); for (size_t varData_entries = buffer.pop<size_t>(); varData_entries != 0; --varData_entries)
fgetpos(file, &entry->pos); entry->varData.set(buffer.pop<Jupiter::String_Strict, char>(), buffer.pop<Jupiter::String_Strict, char>());
fread(&entry->active, 1, 1, file); }
fread(&entry->timestamp, sizeof(time_t), 1, file);
fread(&entry->length, sizeof(time_t), 1, file);
fread(&entry->steamid, sizeof(uint64_t), 1, file);
fread(&entry->ip, sizeof(uint32_t), 1, file);
if (feof(file))
{
delete entry;
break;
}
// load rdns
if (RenX::BanDatabase::version >= 2)
{
rdns.truncate(rdns.size());
c = fgetc(file);
while (c != '\n' && c != '\0')
{
if (c == EOF)
{
fprintf(stderr, "ERROR: Unexpected EOF in %s at %lu", RenX::BanDatabase::filename.c_str(), ftell(file));
break;
}
rdns += c;
c = fgetc(file);
}
entry->rdns = rdns;
}
// load name
playerName.truncate(playerName.size());
c = fgetc(file);
while (c != '\n' && c != '\0')
{
if (c == EOF)
{
fprintf(stderr, "ERROR: Unexpected EOF in %s at %lu", RenX::BanDatabase::filename.c_str(), ftell(file));
break;
}
playerName += c;
c = fgetc(file);
}
entry->name = playerName;
// load reason
if (RenX::BanDatabase::version >= 1)
{
reason.truncate(reason.size());
c = fgetc(file);
while (c != '\n' && c != '\0')
{
if (c == EOF)
{
fprintf(stderr, "ERROR: Unexpected EOF in %s at %lu", RenX::BanDatabase::filename.c_str(), ftell(file));
break;
}
reason += c;
c = fgetc(file);
}
}
entry->reason = reason;
// load variable data
while (c == '\0')
{
key.truncate(key.size());
value.truncate(value.size());
// load key void RenX::BanDatabase::process_header(FILE *file)
c = fgetc(file); {
while (c != '\0' && c != EOF) int chr = fgetc(file);
{ if (chr != EOF)
key += c; RenX::BanDatabase::read_version = chr;
c = fgetc(file); }
}
if (c == EOF)
{
fprintf(stderr, "ERROR: Unexpected EOF in %s at %lu", RenX::BanDatabase::filename.c_str(), ftell(file));
break;
}
// load value void RenX::BanDatabase::create_header(FILE *file)
c = fgetc(file); {
while (c != '\n' && c != '\0' && c != EOF) fputc(RenX::BanDatabase::write_version, file);
{ }
value += c;
c = fgetc(file);
}
if (c == EOF)
{
fprintf(stderr, "ERROR: Unexpected EOF in %s at %lu", RenX::BanDatabase::filename.c_str(), ftell(file));
break;
}
entry->varData.set(key, value); void RenX::BanDatabase::process_file_finish(FILE *file)
} {
entries.add(entry); if (RenX::BanDatabase::read_version < 3)
} {
fclose(file); if (freopen(RenX::BanDatabase::filename.c_str(), "wb", file) == nullptr)
if (RenX::BanDatabase::version != write_version) puts("FATAL ERROR: UNABLE TO REMOVE UNSUPPORTED BAN DATABASE FILE VERSION");
else
{ {
file = fopen(RenX::BanDatabase::filename.c_str(), "wb"); puts("Warning: Unsupported ban database file version. The database will be removed and rewritten.");
if (file != nullptr) this->create_header(file);
{ RenX::BanDatabase::read_version = RenX::BanDatabase::write_version;
fputc(write_version, file);
fputc('\n', file);
size_t index = 0;
while (index != RenX::BanDatabase::entries.size())
RenX::BanDatabase::write(RenX::BanDatabase::entries.get(++index), file);
fclose(file);
fprintf(stdout, "Updated BanDatabase file \"%s\" from version %d to %d." ENDL, RenX::BanDatabase::filename.c_str(), RenX::BanDatabase::version, write_version);
}
else
fprintf(stdout, "CRITICAL ERROR: BanDatabase file \"%s\" failed to update from version %d to %d." ENDL, RenX::BanDatabase::filename.c_str(), RenX::BanDatabase::version, write_version);
RenX::BanDatabase::version = write_version;
} }
return true;
} }
else }
void RenX::BanDatabase::upgrade_database()
{
FILE *file = fopen(RenX::BanDatabase::filename.c_str(), "wb");
if (file != nullptr)
{ {
RenX::BanDatabase::version = write_version; this->create_header(file);
file = fopen(RenX::BanDatabase::filename.c_str(), "ab"); for (size_t index = 0; RenX::BanDatabase::entries.size(); ++index)
if (file != nullptr) RenX::BanDatabase::write(RenX::BanDatabase::entries.get(index), file);
{
fputc(RenX::BanDatabase::version, file); fclose(file);
fputc('\n', file);
fclose(file);
return true;
}
return false;
} }
} }
void RenX::BanDatabase::write(RenX::BanDatabase::Entry *entry) void RenX::BanDatabase::write(RenX::BanDatabase::Entry *entry)
{ {
if (RenX::BanDatabase::version != write_version) FILE *file = fopen(filename.c_str(), "ab");
"CRITICAL ERROR: COULD NOT WRITE BAN ENTRY TO BAN DATABASE (VERSION MISMATCH)"_jrs.print(stdout);
FILE *file = fopen(RenX::BanDatabase::filename.c_str(), "ab");
if (file != nullptr) if (file != nullptr)
{ {
RenX::BanDatabase::write(entry, file); RenX::BanDatabase::write(entry, file);
@ -203,40 +107,50 @@ void RenX::BanDatabase::write(RenX::BanDatabase::Entry *entry)
void RenX::BanDatabase::write(RenX::BanDatabase::Entry *entry, FILE *file) void RenX::BanDatabase::write(RenX::BanDatabase::Entry *entry, FILE *file)
{ {
Jupiter::DataBuffer buffer;
fgetpos(file, &entry->pos); fgetpos(file, &entry->pos);
fwrite(&entry->active, 1, 1, file);
fwrite(&entry->timestamp, sizeof(time_t), 1, file);
fwrite(&entry->length, sizeof(time_t), 1, file);
fwrite(&entry->steamid, sizeof(uint64_t), 1, file);
fwrite(&entry->ip, sizeof(uint32_t), 1, file);
fwrite(entry->rdns.ptr(), sizeof(char), entry->rdns.size(), file);
fputc('\0', file);
fwrite(entry->name.ptr(), sizeof(char), entry->name.size(), file);
fputc('\0', file);
fwrite(entry->reason.ptr(), sizeof(char), entry->reason.size(), file);
for (size_t index = 0; index != entry->varData.size(); ++index) // push data from entry to buffer
buffer.push(entry->flags);
buffer.push(entry->timestamp);
buffer.push(entry->length);
buffer.push(entry->steamid);
buffer.push(entry->ip);
buffer.push(entry->prefix_length);
buffer.push(entry->rdns);
buffer.push(entry->name);
buffer.push(entry->banner);
buffer.push(entry->reason);
// push varData from entry to buffer
size_t varData_entries = entry->varData.size();
buffer.push(varData_entries);
Jupiter::INIFile::Section::KeyValuePair *pair;
while (varData_entries != 0)
{ {
const Jupiter::INIFile::Section::KeyValuePair *pair = entry->varData.getPair(index); pair = entry->varData.getPair(--varData_entries);
fputc('\0', file); buffer.push(pair->getKey());
fwrite(pair->getKey().ptr(), sizeof(char), pair->getKey().size(), file); buffer.push(pair->getValue());
fputc('\0', file);
fwrite(pair->getValue().ptr(), sizeof(char), pair->getValue().size(), file);
} }
fputc('\n', file); // push buffer to file
buffer.push_to(file);
} }
void RenX::BanDatabase::add(RenX::Server *server, const RenX::PlayerInfo *player, const Jupiter::ReadableString &reason, time_t length) void RenX::BanDatabase::add(RenX::Server *server, const RenX::PlayerInfo *player, const Jupiter::ReadableString &banner, const Jupiter::ReadableString &reason, time_t length, uint8_t flags)
{ {
Entry *entry = new Entry(); Entry *entry = new Entry();
entry->active = 1; entry->set_active();
entry->flags |= flags;
entry->timestamp = time(0); entry->timestamp = time(0);
entry->length = length; entry->length = length;
entry->steamid = player->steamid; entry->steamid = player->steamid;
entry->ip = player->ip32; entry->ip = player->ip32;
entry->prefix_length = 32U;
entry->rdns = player->rdns; entry->rdns = player->rdns;
entry->name = player->name; entry->name = player->name;
entry->banner = banner;
entry->reason = reason; entry->reason = reason;
// add plugin data // add plugin data
@ -244,7 +158,26 @@ void RenX::BanDatabase::add(RenX::Server *server, const RenX::PlayerInfo *player
Jupiter::ArrayList<RenX::Plugin> &xPlugins = *RenX::getCore()->getPlugins(); Jupiter::ArrayList<RenX::Plugin> &xPlugins = *RenX::getCore()->getPlugins();
for (size_t i = 0; i < xPlugins.size(); i++) for (size_t i = 0; i < xPlugins.size(); i++)
if (xPlugins.get(i)->RenX_OnBan(server, player, pluginData)) if (xPlugins.get(i)->RenX_OnBan(server, player, pluginData))
entry->varData.set(xPlugins.get(i)->getName(), pluginData); entry->varData.set(xPlugins.get(i)->getName(), pluginData);
entries.add(entry);
RenX::BanDatabase::write(entry);
}
void RenX::BanDatabase::add(const Jupiter::ReadableString &name, uint32_t ip, uint8_t prefix_length, uint64_t steamid, const Jupiter::ReadableString &rdns, Jupiter::ReadableString &banner, Jupiter::ReadableString &reason, time_t length, uint8_t flags)
{
Entry *entry = new Entry();
entry->set_active();
entry->flags |= flags;
entry->timestamp = time(0);
entry->length = length;
entry->steamid = steamid;
entry->ip = ip;
entry->prefix_length = prefix_length;
entry->rdns = rdns;
entry->name = name;
entry->banner = banner;
entry->reason = reason;
entries.add(entry); entries.add(entry);
RenX::BanDatabase::write(entry); RenX::BanDatabase::write(entry);
@ -253,14 +186,14 @@ void RenX::BanDatabase::add(RenX::Server *server, const RenX::PlayerInfo *player
bool RenX::BanDatabase::deactivate(size_t index) bool RenX::BanDatabase::deactivate(size_t index)
{ {
RenX::BanDatabase::Entry *entry = RenX::BanDatabase::entries.get(index); RenX::BanDatabase::Entry *entry = RenX::BanDatabase::entries.get(index);
if (entry->active) if (entry->is_active())
{ {
entry->active = 0; entry->unset_active();
FILE *file = fopen(RenX::BanDatabase::filename.c_str(), "r+b"); FILE *file = fopen(RenX::BanDatabase::filename.c_str(), "r+b");
if (file != nullptr) if (file != nullptr)
{ {
fsetpos(file, &entry->pos); fsetpos(file, &entry->pos);
fputc(entry->active, file); fputc(entry->flags, file);
fclose(file); fclose(file);
} }
return true; return true;
@ -270,7 +203,7 @@ bool RenX::BanDatabase::deactivate(size_t index)
uint8_t RenX::BanDatabase::getVersion() const uint8_t RenX::BanDatabase::getVersion() const
{ {
return RenX::BanDatabase::version; return RenX::BanDatabase::write_version;
} }
const Jupiter::ReadableString &RenX::BanDatabase::getFileName() const const Jupiter::ReadableString &RenX::BanDatabase::getFileName() const
@ -285,7 +218,8 @@ const Jupiter::ArrayList<RenX::BanDatabase::Entry> &RenX::BanDatabase::getEntrie
RenX::BanDatabase::BanDatabase() RenX::BanDatabase::BanDatabase()
{ {
RenX::BanDatabase::load(Jupiter::IRC::Client::Config->get(STRING_LITERAL_AS_REFERENCE("RenX"), STRING_LITERAL_AS_REFERENCE("BanDB"), STRING_LITERAL_AS_REFERENCE("Bans.db"))); RenX::BanDatabase::filename = Jupiter::IRC::Client::Config->get(STRING_LITERAL_AS_REFERENCE("RenX"), STRING_LITERAL_AS_REFERENCE("BanDB"), STRING_LITERAL_AS_REFERENCE("Bans.db"));
this->process_file(filename);
} }
RenX::BanDatabase::~BanDatabase() RenX::BanDatabase::~BanDatabase()

115
RenX.Core/RenX_BanDatabase.h

@ -1,5 +1,5 @@
/** /**
* Copyright (C) 2014-2015 Jessica James. * Copyright (C) 2014-2016 Jessica James.
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@ -21,6 +21,7 @@
#include <ctime> #include <ctime>
#include <cstdint> #include <cstdint>
#include "Jupiter/Database.h"
#include "Jupiter/String.h" #include "Jupiter/String.h"
#include "Jupiter/CString.h" #include "Jupiter/CString.h"
#include "Jupiter/ArrayList.h" #include "Jupiter/ArrayList.h"
@ -39,8 +40,39 @@ namespace RenX
/** /**
* @brief Represents the local ban database. * @brief Represents the local ban database.
*/ */
class RENX_API BanDatabase class RENX_API BanDatabase : public Jupiter::Database
{ {
public: // Jupiter::Database
/**
* @brief Processes a chunk of data in a database.
*
* @param buffer Buffer to process
* @param file File being processed
* @param pos position that the buffer starts at in the file
*/
void process_data(Jupiter::DataBuffer &buffer, FILE *file, fpos_t pos) override;
/**
* @brief Processes the header for a database.
*
* @param file File being processed
*/
void process_header(FILE *file) override;
/**
* @brief Generates a header for a database.
*
* @param file File being created
*/
void create_header(FILE *file) override;
/**
* @brief Called when process_file() is successfully completed.
*
* @param file File being processed
*/
void process_file_finish(FILE *file) override;
public: public:
/** /**
* @brief Represents a Ban entry in the database. * @brief Represents a Ban entry in the database.
@ -48,44 +80,94 @@ namespace RenX
struct RENX_API Entry struct RENX_API Entry
{ {
fpos_t pos; /** Position of the entry in the database */ fpos_t pos; /** Position of the entry in the database */
unsigned char active; /** 1 if the ban is active, 0 otherwise */ uint8_t flags /** Flags affecting this ban entry (0 = Active, 1 = Game, 2 = Chat, 3 = Command, 4 = Vote, 5 = Mine, 6 = Ladder, 7 = Alert Mods) */ = 0x00;
time_t timestamp /** Time the ban was created */; time_t timestamp /** Time the ban was created */;
time_t length /** Duration of the ban; 0 if permanent */; time_t length /** Duration of the ban; 0 if permanent */;
uint64_t steamid /** SteamID of the banned player */; uint64_t steamid /** SteamID of the banned player */;
uint32_t ip /** IPv4 address of the banned player */; uint32_t ip /** IPv4 address of the banned player */;
uint8_t prefix_length /** Prefix length for the IPv4 address block */;
Jupiter::StringS rdns /** RDNS of the banned player */; Jupiter::StringS rdns /** RDNS of the banned player */;
Jupiter::StringS name /** Name of the banned player */; Jupiter::StringS name /** Name of the banned player */;
Jupiter::StringS banner /** Name of the user who initiated the ban */;
Jupiter::StringS reason /** Reason the player was banned */; Jupiter::StringS reason /** Reason the player was banned */;
Jupiter::INIFile::Section varData; /** Variable entry data */ Jupiter::INIFile::Section varData; /** Variable entry data */
static const uint8_t FLAG_ACTIVE = 0x80;
static const uint8_t FLAG_TYPE_GAME = 0x40;
static const uint8_t FLAG_TYPE_CHAT = 0x20;
static const uint8_t FLAG_TYPE_BOT = 0x10;
static const uint8_t FLAG_TYPE_VOTE = 0x08;
static const uint8_t FLAG_TYPE_MINE = 0x04;
static const uint8_t FLAG_TYPE_LADDER = 0x02;
static const uint8_t FLAG_TYPE_ALERT = 0x01;
inline bool is_active() { return (flags & FLAG_ACTIVE) != 0; };
inline bool is_type_game() { return (flags & FLAG_TYPE_GAME) != 0; };
inline bool is_type_chat() { return (flags & FLAG_TYPE_CHAT) != 0; };
inline bool is_type_bot() { return (flags & FLAG_TYPE_BOT) != 0; };
inline bool is_type_vote() { return (flags & FLAG_TYPE_VOTE) != 0; };
inline bool is_type_mine() { return (flags & FLAG_TYPE_MINE) != 0; };
inline bool is_type_ladder() { return (flags & FLAG_TYPE_LADDER) != 0; };
inline bool is_type_alert() { return (flags & FLAG_TYPE_ALERT) != 0; };
inline bool is_type_global() { return ~(flags | 0x01) == 0; };
inline void set_active() { flags |= FLAG_ACTIVE; };
inline void set_type_game() { flags |= FLAG_TYPE_GAME; };
inline void set_type_chat() { flags |= FLAG_TYPE_CHAT; };
inline void set_type_bot() { flags |= FLAG_TYPE_BOT; };
inline void set_type_vote() { flags |= FLAG_TYPE_VOTE; };
inline void set_type_mine() { flags |= FLAG_TYPE_MINE; };
inline void set_type_ladder() { flags |= FLAG_TYPE_LADDER; };
inline void set_type_alert() { flags |= FLAG_TYPE_ALERT; };
inline void set_type_global() { flags = 0xFF; };
inline void unset_active() { flags &= ~FLAG_ACTIVE; };
inline void unset_type_game() { flags &= ~FLAG_TYPE_GAME; };
inline void unset_type_chat() { flags &= ~FLAG_TYPE_CHAT; };
inline void unset_type_bot() { flags &= ~FLAG_TYPE_BOT; };
inline void unset_type_vote() { flags &= ~FLAG_TYPE_VOTE; };
inline void unset_type_mine() { flags &= ~FLAG_TYPE_MINE; };
inline void unset_type_ladder() { flags &= ~FLAG_TYPE_LADDER; };
inline void unset_type_alert() { flags &= ~FLAG_TYPE_ALERT; };
inline void unset_type_global() { flags = 0x00; };
}; };
/** /**
* @brief Loads a file into the ban system. * @brief Adds a ban entry for a player and immediately writes it to the database.
* Note: This will generate a database file if none is found.
* *
* @param fname String containing the name of the file to load * @param server Server the player is playing in
* @return True on success, false otherwise. * @param player Data of the player to be banned
* @param length Duration of the ban
*/ */
bool load(const Jupiter::ReadableString &fname); void add(RenX::Server *server, const RenX::PlayerInfo *player, const Jupiter::ReadableString &banner, const Jupiter::ReadableString &reason, time_t length, uint8_t flags = RenX::BanDatabase::Entry::FLAG_TYPE_GAME);
/** /**
* @brief Adds a ban entry for a player and immediately writes it to the database. * @brief Adds a ban entry for a set of player information and immediately writes it to the database.
* *
* @param server Server the player is playing in * @param name Name of the player to ban
* @param player Data of the player to be banned * @param ip IPv4 address of the player to ban
* @param steamid SteamID of the player to ban
* @param rdns RDNS of the player to ban
* @param banner Person implementing the ban
* @param reason Reason the player is getting banned
* @param length Duration of the ban * @param length Duration of the ban
*/ */
void add(RenX::Server *server, const RenX::PlayerInfo *player, const Jupiter::ReadableString &reason, time_t length); void add(const Jupiter::ReadableString &name, uint32_t ip, uint8_t prefix_length, uint64_t steamid, const Jupiter::ReadableString &rdns, Jupiter::ReadableString &banner, Jupiter::ReadableString &reason, time_t length, uint8_t flags = RenX::BanDatabase::Entry::FLAG_TYPE_GAME);
/** /**
* @brief Writes a ban file to the database. * @brief Upgrades the ban database to the current write_version.
*/
void upgrade_database();
/**
* @brief Writes a ban entry to the database.
* *
* @param entry Entry to write to the database. * @param entry Entry to write to the database.
*/ */
void write(Entry *entry); void write(Entry *entry);
/** /**
* @brief Writes a ban file to the database. * @brief Writes a ban entry to the database.
* *
* @param entry Entry to write to the database. * @param entry Entry to write to the database.
* @param file FILE stream to write to. * @param file FILE stream to write to.
@ -125,7 +207,10 @@ namespace RenX
~BanDatabase(); ~BanDatabase();
private: private:
uint8_t version; /** Database version */
const uint8_t write_version = 3U;
uint8_t read_version = write_version;
Jupiter::CStringS filename; Jupiter::CStringS filename;
Jupiter::ArrayList<RenX::BanDatabase::Entry> entries; Jupiter::ArrayList<RenX::BanDatabase::Entry> entries;
}; };

3
RenX.Core/RenX_PlayerInfo.h

@ -1,5 +1,5 @@
/** /**
* Copyright (C) 2014-2015 Jessica James. * Copyright (C) 2014-2016 Jessica James.
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@ -53,6 +53,7 @@ namespace RenX
Jupiter::StringS vehicle; Jupiter::StringS vehicle;
uint64_t steamid = 0; uint64_t steamid = 0;
uint32_t ip32 = 0; uint32_t ip32 = 0;
uint8_t ban_flags = 0;
TeamType team = TeamType::Other; TeamType team = TeamType::Other;
int id = 0; int id = 0;
bool isBot = false; bool isBot = false;

169
RenX.Core/RenX_Server.cpp

@ -1,5 +1,5 @@
/** /**
* Copyright (C) 2014-2015 Jessica James. * Copyright (C) 2014-2016 Jessica James.
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@ -17,6 +17,7 @@
*/ */
#include <ctime> #include <ctime>
#include <WinSock2.h>
#include "Jupiter/INIFile.h" #include "Jupiter/INIFile.h"
#include "Jupiter/String.h" #include "Jupiter/String.h"
#include "ServerManager.h" #include "ServerManager.h"
@ -378,7 +379,7 @@ void RenX::Server::forceKickPlayer(const RenX::PlayerInfo *player, const Jupiter
RenX::Server::forceKickPlayer(player->id, reason); RenX::Server::forceKickPlayer(player->id, reason);
} }
void RenX::Server::banPlayer(int id, const Jupiter::ReadableString &reason) void RenX::Server::banPlayer(int id, const Jupiter::ReadableString &banner, const Jupiter::ReadableString &reason)
{ {
if (RenX::Server::rconBan) if (RenX::Server::rconBan)
RenX::Server::sock.send(Jupiter::StringS::Format("ckickban pid%d %.*s\n", id, reason.size(), reason.ptr())); RenX::Server::sock.send(Jupiter::StringS::Format("ckickban pid%d %.*s\n", id, reason.size(), reason.ptr()));
@ -386,22 +387,26 @@ void RenX::Server::banPlayer(int id, const Jupiter::ReadableString &reason)
{ {
RenX::PlayerInfo *player = RenX::Server::getPlayer(id); RenX::PlayerInfo *player = RenX::Server::getPlayer(id);
if (player != nullptr) if (player != nullptr)
RenX::Server::banPlayer(player, reason); RenX::Server::banPlayer(player, banner, reason);
} }
} }
void RenX::Server::banPlayer(const RenX::PlayerInfo *player, const Jupiter::ReadableString &reason, time_t length) void RenX::Server::banPlayer(const RenX::PlayerInfo *player, const Jupiter::ReadableString &banner, const Jupiter::ReadableString &reason, time_t length)
{ {
if (RenX::Server::localBan) if (RenX::Server::localBan)
RenX::banDatabase->add(this, player, reason, length); RenX::banDatabase->add(this, player, banner, reason, length);
if (length == 0) if (length == 0)
{ {
if (RenX::Server::rconBan) if (RenX::Server::rconBan)
RenX::Server::sock.send(Jupiter::StringS::Format("ckickban pid%d %.*s\n", player->id, reason.size(), reason.ptr())); RenX::Server::sock.send(Jupiter::StringS::Format("ckickban pid%d %.*s\n", player->id, reason.size(), reason.ptr()));
else if (banner.isNotEmpty())
RenX::Server::forceKickPlayer(player, Jupiter::StringS::Format("You are permanently banned from the server by %.*s for: %.*s", banner.size(), banner.ptr(), reason.size(), reason.ptr()));
else else
RenX::Server::forceKickPlayer(player, Jupiter::StringS::Format("You are permanently banned from the server for: %.*s", reason.size(), reason.ptr())); RenX::Server::forceKickPlayer(player, Jupiter::StringS::Format("You are permanently banned from the server for: %.*s", reason.size(), reason.ptr()));
} }
else if (banner.isNotEmpty())
RenX::Server::forceKickPlayer(player, Jupiter::StringS::Format("You are banned from the server by %.*s for the next %d days, %d:%d:%d for: %.*s", banner.size(), banner.ptr(), length / 86400, length % 3600, (length % 3600) / 60, length % 60, reason.size(), reason.ptr()));
else else
RenX::Server::forceKickPlayer(player, Jupiter::StringS::Format("You are banned from the server for the next %d days, %d:%d:%d for: %.*s", length/86400, length%3600, (length%3600)/60, length%60, reason.size(), reason.ptr())); RenX::Server::forceKickPlayer(player, Jupiter::StringS::Format("You are banned from the server for the next %d days, %d:%d:%d for: %.*s", length/86400, length%3600, (length%3600)/60, length%60, reason.size(), reason.ptr()));
} }
@ -988,7 +993,7 @@ void RenX::Server::processLine(const Jupiter::ReadableString &line)
auto onChat = [this](RenX::PlayerInfo *player, const Jupiter::ReadableString &message) auto onChat = [this](RenX::PlayerInfo *player, const Jupiter::ReadableString &message)
{ {
const Jupiter::ReadableString &prefix = this->getCommandPrefix(); const Jupiter::ReadableString &prefix = this->getCommandPrefix();
if (message.find(prefix) == 0 && message.size() != prefix.size()) if ((player->ban_flags & RenX::BanDatabase::Entry::FLAG_TYPE_BOT) == 0 && message.find(prefix) == 0 && message.size() != prefix.size())
{ {
Jupiter::ReferenceString command; Jupiter::ReferenceString command;
Jupiter::ReferenceString parameters; Jupiter::ReferenceString parameters;
@ -1028,34 +1033,152 @@ void RenX::Server::processLine(const Jupiter::ReadableString &line)
isBot = false; isBot = false;
id = idToken.asInt(10); id = idToken.asInt(10);
}; };
auto banCheck = [this](const RenX::PlayerInfo *player) auto banCheck = [this](RenX::PlayerInfo *player)
{ {
const Jupiter::ArrayList<RenX::BanDatabase::Entry> &entries = RenX::banDatabase->getEntries(); const Jupiter::ArrayList<RenX::BanDatabase::Entry> &entries = RenX::banDatabase->getEntries();
RenX::BanDatabase::Entry *entry = nullptr; RenX::BanDatabase::Entry *entry = nullptr;
uint32_t netmask;
RenX::BanDatabase::Entry *last_to_expire[7];
for (size_t index = 0; index != sizeof(last_to_expire); ++index)
last_to_expire[index] = nullptr;
auto handle_type = [entry](RenX::BanDatabase::Entry *&last_to_expire)
{
if (last_to_expire == nullptr)
last_to_expire = entry;
else if (last_to_expire->length == 0)
{
// favor older bans if they're also permanent
if (entry->length == 0 && entry->timestamp < last_to_expire->timestamp)
last_to_expire = entry;
}
else if (entry->length == 0 || entry->timestamp + entry->length > last_to_expire->timestamp + last_to_expire->length)
last_to_expire = entry;
};
for (size_t i = 0; i != entries.size(); i++) for (size_t i = 0; i != entries.size(); i++)
{ {
entry = entries.get(i); entry = entries.get(i);
if (entry->active) if (entry->is_active())
{ {
if (entry->length != 0 && entry->timestamp + entry->length < time(0)) if (entry->length != 0 && entry->timestamp + entry->length < time(0))
banDatabase->deactivate(i); banDatabase->deactivate(i);
else if ((this->localSteamBan && entry->steamid != 0 && entry->steamid == player->steamid) else
|| (this->localIPBan && entry->ip != 0 && entry->ip == player->ip32)
|| (this->localRDNSBan && entry->rdns.isNotEmpty() && entry->rdns.equals(player->rdns))
|| (this->localNameBan && entry->name.isNotEmpty() && entry->name.equalsi(player->name)))
{ {
char timeStr[256]; if (entry->prefix_length >= 32)
if (entry->length == 0) netmask = 0xFFFFFFFF;
{
strftime(timeStr, sizeof(timeStr), "%b %d %Y at %H:%M:%S", localtime(&(entry->timestamp)));
this->forceKickPlayer(player, Jupiter::StringS::Format("You are were permanently banned from the server on %s for: %.*s", timeStr, entry->reason.size(), entry->reason.ptr()));
}
else else
netmask = Jupiter_prefix_length_to_netmask(entry->prefix_length);
if ((this->localSteamBan && entry->steamid != 0 && entry->steamid == player->steamid)
|| (this->localIPBan && entry->ip != 0 && (entry->ip & netmask) == (player->ip32 & netmask))
|| (this->localRDNSBan && entry->rdns.isNotEmpty() && entry->rdns.equals(player->rdns))
|| (this->localNameBan && entry->name.isNotEmpty() && entry->name.equalsi(player->name)))
{
player->ban_flags |= entry->flags;
if (entry->is_type_game())
handle_type(last_to_expire[0]);
if (entry->is_type_chat())
handle_type(last_to_expire[1]);
if (entry->is_type_bot())
handle_type(last_to_expire[2]);
if (entry->is_type_vote())
handle_type(last_to_expire[3]);
if (entry->is_type_mine())
handle_type(last_to_expire[4]);
if (entry->is_type_ladder())
handle_type(last_to_expire[5]);
if (entry->is_type_alert())
handle_type(last_to_expire[6]);
}
}
}
}
char timeStr[256];
if (last_to_expire[0] != nullptr) // Game ban
{
strftime(timeStr, sizeof(timeStr), "%b %d %Y at %H:%M:%S", localtime(std::addressof<const time_t>(last_to_expire[0]->timestamp + last_to_expire[0]->length)));
if (last_to_expire[0]->length == 0)
this->forceKickPlayer(player, Jupiter::StringS::Format("You were permanently banned from the server on %s for: %.*s", timeStr, last_to_expire[0]->reason.size(), last_to_expire[0]->reason.ptr()));
else
this->forceKickPlayer(player, Jupiter::StringS::Format("You are banned from the server until %s for: %.*s", timeStr, last_to_expire[0]->reason.size(), last_to_expire[0]->reason.ptr()));
player->ban_flags |= RenX::BanDatabase::Entry::FLAG_TYPE_BOT; // implies FLAG_TYPE_BOT
}
else
{
if (last_to_expire[1] != nullptr) // Chat ban
{
strftime(timeStr, sizeof(timeStr), "%b %d %Y at %H:%M:%S", localtime(std::addressof<const time_t>(last_to_expire[1]->timestamp + last_to_expire[1]->length)));
this->mute(player);
if (last_to_expire[1]->length == 0)
this->sendMessage(player, Jupiter::StringS::Format("You were permanently muted on this server on %s for: %.*s", timeStr, last_to_expire[1]->reason.size(), last_to_expire[1]->reason.ptr()));
else
this->sendMessage(player, Jupiter::StringS::Format("You are muted on this server until %s for: %.*s", timeStr, last_to_expire[1]->reason.size(), last_to_expire[1]->reason.ptr()));
player->ban_flags |= RenX::BanDatabase::Entry::FLAG_TYPE_BOT; // implies FLAG_TYPE_BOT
}
else if (last_to_expire[2] != nullptr) // Bot ban
{
strftime(timeStr, sizeof(timeStr), "%b %d %Y at %H:%M:%S", localtime(std::addressof<const time_t>(last_to_expire[2]->timestamp + last_to_expire[2]->length)));
if (last_to_expire[2]->length == 0)
this->sendMessage(player, Jupiter::StringS::Format("You were permanently bot-muted on this server on %s for: %.*s", timeStr, last_to_expire[2]->reason.size(), last_to_expire[2]->reason.ptr()));
else
this->sendMessage(player, Jupiter::StringS::Format("You are bot-muted on this server until %s for: %.*s", timeStr, last_to_expire[2]->reason.size(), last_to_expire[2]->reason.ptr()));
}
if (last_to_expire[3] != nullptr) // Vote ban
{
strftime(timeStr, sizeof(timeStr), "%b %d %Y at %H:%M:%S", localtime(std::addressof<const time_t>(last_to_expire[3]->timestamp + last_to_expire[3]->length)));
if (last_to_expire[3]->length == 0)
this->sendMessage(player, Jupiter::StringS::Format("You were permanently vote-muted on this server on %s for: %.*s", timeStr, last_to_expire[3]->reason.size(), last_to_expire[3]->reason.ptr()));
else
this->sendMessage(player, Jupiter::StringS::Format("You are vote-muted on this server until %s for: %.*s", timeStr, last_to_expire[3]->reason.size(), last_to_expire[3]->reason.ptr()));
}
if (last_to_expire[4] != nullptr) // Mine ban
{
this->mineBan(player);
strftime(timeStr, sizeof(timeStr), "%b %d %Y at %H:%M:%S", localtime(std::addressof<const time_t>(last_to_expire[4]->timestamp + last_to_expire[4]->length)));
if (last_to_expire[4]->length == 0)
this->sendMessage(player, Jupiter::StringS::Format("You were permanently mine-banned on this server on %s for: %.*s", timeStr, last_to_expire[4]->reason.size(), last_to_expire[4]->reason.ptr()));
else
this->sendMessage(player, Jupiter::StringS::Format("You are mine-banned on this server until %s for: %.*s", timeStr, last_to_expire[4]->reason.size(), last_to_expire[4]->reason.ptr()));
}
if (last_to_expire[5] != nullptr) // Ladder ban
{
strftime(timeStr, sizeof(timeStr), "%b %d %Y at %H:%M:%S", localtime(std::addressof<const time_t>(last_to_expire[5]->timestamp + last_to_expire[5]->length)));
if (last_to_expire[5]->length == 0)
this->sendMessage(player, Jupiter::StringS::Format("You were permanently ladder-banned on this server on %s for: %.*s", timeStr, last_to_expire[5]->reason.size(), last_to_expire[5]->reason.ptr()));
else
this->sendMessage(player, Jupiter::StringS::Format("You are ladder-banned on this server until %s for: %.*s", timeStr, last_to_expire[5]->reason.size(), last_to_expire[5]->reason.ptr()));
}
if (last_to_expire[6] != nullptr) // Alert
{
unsigned int serverCount = serverManager->size();
IRC_Bot *server;
Jupiter::IRC::Client::Channel *channel;
unsigned int channelCount;
Jupiter::String &fmtName = RenX::getFormattedPlayerName(player);
Jupiter::StringL msg = Jupiter::StringL::Format(IRCCOLOR "04[Alert] " IRCCOLOR IRCBOLD "%.*s" IRCBOLD IRCCOLOR " is marked for monitoring by %.*s for: \"%.*s\". Please keep an eye on them in ", fmtName.size(), fmtName.ptr(), last_to_expire[6]->banner.size(), last_to_expire[6]->banner.ptr(), last_to_expire[6]->reason.size(), last_to_expire[6]->reason.ptr());
Jupiter::StringS msg2 = Jupiter::StringS::Format(IRCCOLOR "04[Alert] " IRCCOLOR IRCBOLD "%.*s" IRCBOLD IRCCOLOR " is marked for monitoring by %.*s for: \"%.*s\"." IRCCOLOR, fmtName.size(), fmtName.ptr(), last_to_expire[6]->banner.size(), last_to_expire[6]->banner.ptr(), last_to_expire[6]->reason.size(), last_to_expire[6]->reason.ptr());
for (unsigned int a = 0; a < serverCount; a++)
{
server = serverManager->getServer(a);
channelCount = server->getChannelCount();
for (unsigned int b = 0; b < channelCount; b++)
{ {
strftime(timeStr, sizeof(timeStr), "%b %d %Y at %H:%M:%S", localtime(std::addressof<const time_t>(entry->timestamp + entry->length))); channel = server->getChannel(b);
this->forceKickPlayer(player, Jupiter::StringS::Format("You are banned from the server until %s for: %.*s", timeStr, entry->reason.size(), entry->reason.ptr())); if (this->isAdminLogChanType(channel->getType()))
{
server->sendMessage(channel->getName(), msg2);
msg += channel->getName();
for (unsigned int c = 0; c < channel->getUserCount(); c++)
if (channel->getUserPrefix(c) != 0 && channel->getUser(c)->getNickname().equals(server->getNickname()) == false)
server->sendMessage(channel->getUser(c)->getUser()->getNickname(), msg);
msg -= channel->getName().size();
}
} }
return;
} }
} }
} }
@ -2329,6 +2452,10 @@ void RenX::Server::processLine(const Jupiter::ReadableString &line)
playerToken = tokens.getToken(5); playerToken = tokens.getToken(5);
RenX::PlayerInfo *player = parseGetPlayerOrAdd(playerToken); RenX::PlayerInfo *player = parseGetPlayerOrAdd(playerToken);
if ((player->ban_flags & RenX::BanDatabase::Entry::FLAG_TYPE_VOTE) != 0)
RenX::Server::sendData(Jupiter::StringS::Format("ccancelvote %.*s\n", teamToken.size(), teamToken.ptr()));
for (size_t i = 0; i < xPlugins.size(); i++) for (size_t i = 0; i < xPlugins.size(); i++)
xPlugins.get(i)->RenX_OnVoteCall(this, team, voteType, player, parameters); xPlugins.get(i)->RenX_OnVoteCall(this, team, voteType, player, parameters);
onAction(); onAction();

6
RenX.Core/RenX_Server.h

@ -1,5 +1,5 @@
/** /**
* Copyright (C) 2014-2015 Jessica James. * Copyright (C) 2014-2016 Jessica James.
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@ -339,7 +339,7 @@ namespace RenX
* *
* @param id Player ID of the player to ban. * @param id Player ID of the player to ban.
*/ */
void banPlayer(int id, const Jupiter::ReadableString &reason); void banPlayer(int id, const Jupiter::ReadableString &banner, const Jupiter::ReadableString &reason);
/** /**
* @brief Bans a player from the server. * @brief Bans a player from the server.
@ -347,7 +347,7 @@ namespace RenX
* @param player Data of the player to ban. * @param player Data of the player to ban.
* @param length Duration of the ban (0 for permanent). * @param length Duration of the ban (0 for permanent).
*/ */
void banPlayer(const RenX::PlayerInfo *player, const Jupiter::ReadableString &reason, time_t length = 0); void banPlayer(const RenX::PlayerInfo *player, const Jupiter::ReadableString &banner, const Jupiter::ReadableString &reason, time_t length = 0);
/** /**
* @brief Removes a player's data based on their ID number. * @brief Removes a player's data based on their ID number.

18
RenX.ExcessiveHeadshots/ExcessiveHeadshots.cpp

@ -1,5 +1,5 @@
/** /**
* Copyright (C) 2014-2015 Jessica James. * Copyright (C) 2014-2016 Jessica James.
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@ -24,6 +24,8 @@
#include "RenX_PlayerInfo.h" #include "RenX_PlayerInfo.h"
#include "RenX_Functions.h" #include "RenX_Functions.h"
using namespace Jupiter::literals;
RenX_ExcessiveHeadshotsPlugin::RenX_ExcessiveHeadshotsPlugin() RenX_ExcessiveHeadshotsPlugin::RenX_ExcessiveHeadshotsPlugin()
{ {
RenX_ExcessiveHeadshotsPlugin::OnRehash(); RenX_ExcessiveHeadshotsPlugin::OnRehash();
@ -31,18 +33,18 @@ RenX_ExcessiveHeadshotsPlugin::RenX_ExcessiveHeadshotsPlugin()
int RenX_ExcessiveHeadshotsPlugin::OnRehash() int RenX_ExcessiveHeadshotsPlugin::OnRehash()
{ {
RenX_ExcessiveHeadshotsPlugin::ratio = Jupiter::IRC::Client::Config->getDouble(RenX_ExcessiveHeadshotsPlugin::getName(), STRING_LITERAL_AS_REFERENCE("HeadshotKillRatio"), 0.5); RenX_ExcessiveHeadshotsPlugin::ratio = Jupiter::IRC::Client::Config->getDouble(RenX_ExcessiveHeadshotsPlugin::getName(), "HeadshotKillRatio"_jrs, 0.5);
RenX_ExcessiveHeadshotsPlugin::minKills = Jupiter::IRC::Client::Config->getInt(RenX_ExcessiveHeadshotsPlugin::getName(), STRING_LITERAL_AS_REFERENCE("Kills"), 10); RenX_ExcessiveHeadshotsPlugin::minKills = Jupiter::IRC::Client::Config->getInt(RenX_ExcessiveHeadshotsPlugin::getName(), "Kills"_jrs, 10);
RenX_ExcessiveHeadshotsPlugin::minKD = Jupiter::IRC::Client::Config->getDouble(RenX_ExcessiveHeadshotsPlugin::getName(), STRING_LITERAL_AS_REFERENCE("KillDeathRatio"), 5.0); RenX_ExcessiveHeadshotsPlugin::minKD = Jupiter::IRC::Client::Config->getDouble(RenX_ExcessiveHeadshotsPlugin::getName(), "KillDeathRatio"_jrs, 5.0);
RenX_ExcessiveHeadshotsPlugin::minKPS = Jupiter::IRC::Client::Config->getDouble(RenX_ExcessiveHeadshotsPlugin::getName(), STRING_LITERAL_AS_REFERENCE("KillsPerSecond"), 0.5); RenX_ExcessiveHeadshotsPlugin::minKPS = Jupiter::IRC::Client::Config->getDouble(RenX_ExcessiveHeadshotsPlugin::getName(), "KillsPerSecond"_jrs, 0.5);
RenX_ExcessiveHeadshotsPlugin::minFlags = Jupiter::IRC::Client::Config->getInt(RenX_ExcessiveHeadshotsPlugin::getName(), STRING_LITERAL_AS_REFERENCE("Flags"), 4); RenX_ExcessiveHeadshotsPlugin::minFlags = Jupiter::IRC::Client::Config->getInt(RenX_ExcessiveHeadshotsPlugin::getName(), "Flags"_jrs, 4);
return 0; return 0;
} }
void RenX_ExcessiveHeadshotsPlugin::RenX_OnKill(RenX::Server *server, const RenX::PlayerInfo *player, const RenX::PlayerInfo *victim, const Jupiter::ReadableString &damageType) void RenX_ExcessiveHeadshotsPlugin::RenX_OnKill(RenX::Server *server, const RenX::PlayerInfo *player, const RenX::PlayerInfo *victim, const Jupiter::ReadableString &damageType)
{ {
if (player->kills < 3) return; if (player->kills < 3) return;
if (damageType.equals("Rx_DmgType_Headshot")) if (damageType.equals("Rx_DmgType_Headshot"_jrs))
{ {
unsigned int flags = 0; unsigned int flags = 0;
std::chrono::milliseconds game_time = server->getGameTime(player); std::chrono::milliseconds game_time = server->getGameTime(player);
@ -56,7 +58,7 @@ void RenX_ExcessiveHeadshotsPlugin::RenX_OnKill(RenX::Server *server, const RenX
if (flags >= RenX_ExcessiveHeadshotsPlugin::minFlags) if (flags >= RenX_ExcessiveHeadshotsPlugin::minFlags)
{ {
server->banPlayer(player, STRING_LITERAL_AS_REFERENCE("Aimbot detected")); server->banPlayer(player, "Jupiter Bot"_jrs, "Aimbot detected"_jrs);
server->sendPubChan(IRCCOLOR "13[Aimbot]" IRCCOLOR " %.*s was banned from the server! Kills: %u - Deaths: %u - Headshots: %u", player->name.size(), player->name.ptr(), player->kills, player->deaths, player->headshots); server->sendPubChan(IRCCOLOR "13[Aimbot]" IRCCOLOR " %.*s was banned from the server! Kills: %u - Deaths: %u - Headshots: %u", player->name.size(), player->name.ptr(), player->kills, player->deaths, player->headshots);
const Jupiter::ReadableString &steamid = server->formatSteamID(player); const Jupiter::ReadableString &steamid = server->formatSteamID(player);
server->sendAdmChan(IRCCOLOR "13[Aimbot]" IRCCOLOR " %.*s was banned from the server! Kills: %u - Deaths: %u - Headshots: %u - IP: " IRCBOLD "%.*s" IRCBOLD " - Steam ID: " IRCBOLD "%.*s" IRCBOLD, player->name.size(), player->name.ptr(), player->kills, player->deaths, player->headshots, player->ip.size(), player->ip.ptr(), steamid.size(), steamid.ptr()); server->sendAdmChan(IRCCOLOR "13[Aimbot]" IRCCOLOR " %.*s was banned from the server! Kills: %u - Deaths: %u - Headshots: %u - IP: " IRCBOLD "%.*s" IRCBOLD " - Steam ID: " IRCBOLD "%.*s" IRCBOLD, player->name.size(), player->name.ptr(), player->kills, player->deaths, player->headshots, player->ip.size(), player->ip.ptr(), steamid.size(), steamid.ptr());

5
RenX.Ladder/RenX_Ladder.cpp

@ -1,5 +1,5 @@
/** /**
* Copyright (C) 2015 Jessica James. * Copyright (C) 2015-2016 Jessica James.
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@ -21,6 +21,7 @@
#include "RenX_Ladder.h" #include "RenX_Ladder.h"
#include "RenX_Server.h" #include "RenX_Server.h"
#include "RenX_PlayerInfo.h" #include "RenX_PlayerInfo.h"
#include "RenX_BanDatabase.h"
using namespace Jupiter::literals; using namespace Jupiter::literals;
@ -49,7 +50,7 @@ void RenX_LadderPlugin::updateLadder(RenX::Server *server, const RenX::TeamType
for (Jupiter::DLList<RenX::PlayerInfo>::Node *node = server->players.getNode(0); node != nullptr; node = node->next) for (Jupiter::DLList<RenX::PlayerInfo>::Node *node = server->players.getNode(0); node != nullptr; node = node->next)
{ {
player = node->data; player = node->data;
if (player->steamid != 0) if (player->steamid != 0 && (player->ban_flags & RenX::BanDatabase::Entry::FLAG_TYPE_LADDER) == 0)
{ {
entry = RenX_LadderPlugin::database.getPlayerEntry(player->steamid); entry = RenX_LadderPlugin::database.getPlayerEntry(player->steamid);
if (entry == nullptr) if (entry == nullptr)

4
RenX.Logging/RenX_Logging.cpp

@ -1,5 +1,5 @@
/** /**
* Copyright (C) 2014-2015 Jessica James. * Copyright (C) 2014-2016 Jessica James.
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@ -110,7 +110,7 @@ void RenX_LoggingPlugin::init()
Jupiter::StringS::Format(IRCCOLOR "12[Join] " IRCBOLD "%.*s" IRCBOLD " joined the game fighting for the %.*s from " IRCBOLD "%.*s" IRCBOLD " using Steam ID " IRCBOLD "%.*s" IRCBOLD ". Their hostname is: " IRCBOLD "%.*s", RenX::tags->nameTag.size(), RenX::tags->nameTag.ptr(), RenX::tags->teamLongTag.size(), RenX::tags->teamLongTag.ptr(), RenX::tags->ipTag.size(), RenX::tags->ipTag.ptr(), RenX::tags->steamTag.size(), RenX::tags->steamTag.ptr(), RenX::tags->rdnsTag.size(), RenX::tags->rdnsTag.ptr())); Jupiter::StringS::Format(IRCCOLOR "12[Join] " IRCBOLD "%.*s" IRCBOLD " joined the game fighting for the %.*s from " IRCBOLD "%.*s" IRCBOLD " using Steam ID " IRCBOLD "%.*s" IRCBOLD ". Their hostname is: " IRCBOLD "%.*s", RenX::tags->nameTag.size(), RenX::tags->nameTag.ptr(), RenX::tags->teamLongTag.size(), RenX::tags->teamLongTag.ptr(), RenX::tags->ipTag.size(), RenX::tags->ipTag.ptr(), RenX::tags->steamTag.size(), RenX::tags->steamTag.ptr(), RenX::tags->rdnsTag.size(), RenX::tags->rdnsTag.ptr()));
RenX_LoggingPlugin::joinNoSteamAdminFmt = Jupiter::IRC::Client::Config->get(this->getName(), STRING_LITERAL_AS_REFERENCE("JoinNoSteamAdminFormat"), RenX_LoggingPlugin::joinNoSteamAdminFmt = Jupiter::IRC::Client::Config->get(this->getName(), STRING_LITERAL_AS_REFERENCE("JoinNoSteamAdminFormat"),
Jupiter::StringS::Format(IRCCOLOR "12[Join] " IRCBOLD "%.*s" IRCBOLD " joined the game fighting for the %.*s from " IRCBOLD "%.*s" IRCBOLD ", but is not using Steam. Their hostname is: " IRCBOLD "%.*s", RenX::tags->nameTag.size(), RenX::tags->nameTag.ptr(), RenX::tags->teamLongTag.size(), RenX::tags->teamLongTag.ptr(), RenX::tags->ipTag.size(), RenX::tags->ipTag.ptr(), RenX::tags->rdnsTag.size(), RenX::tags->rdnsTag.ptr())); Jupiter::StringS::Format(IRCCOLOR "12[Join] " IRCBOLD "%.*s" IRCBOLD " joined the game fighting for the %.*s from " IRCBOLD "%.*s" IRCBOLD ", but is " IRCBOLD "not" IRCBOLD " using Steam. Their hostname is: " IRCBOLD "%.*s", RenX::tags->nameTag.size(), RenX::tags->nameTag.ptr(), RenX::tags->teamLongTag.size(), RenX::tags->teamLongTag.ptr(), RenX::tags->ipTag.size(), RenX::tags->ipTag.ptr(), RenX::tags->rdnsTag.size(), RenX::tags->rdnsTag.ptr()));
RenX_LoggingPlugin::partFmt = Jupiter::IRC::Client::Config->get(this->getName(), STRING_LITERAL_AS_REFERENCE("PartFormat"), RenX_LoggingPlugin::partFmt = Jupiter::IRC::Client::Config->get(this->getName(), STRING_LITERAL_AS_REFERENCE("PartFormat"),
Jupiter::StringS::Format(IRCCOLOR "12[Part] " IRCBOLD "%.*s" IRCBOLD " left the %.*s.", RenX::tags->nameTag.size(), RenX::tags->nameTag.ptr(), RenX::tags->teamLongTag.size(), RenX::tags->teamLongTag.ptr())); Jupiter::StringS::Format(IRCCOLOR "12[Part] " IRCBOLD "%.*s" IRCBOLD " left the %.*s.", RenX::tags->nameTag.size(), RenX::tags->nameTag.ptr(), RenX::tags->teamLongTag.size(), RenX::tags->teamLongTag.ptr()));

6
RenX.Warn/RenX_Warn.cpp

@ -1,5 +1,5 @@
/** /**
* Copyright (C) 2014-2015 Jessica James. * Copyright (C) 2014-2016 Jessica James.
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@ -77,7 +77,7 @@ void WarnIRCCommand::trigger(IRC_Bot *source, const Jupiter::ReadableString &cha
source->sendNotice(nick, Jupiter::StringS::Format("%.*s has been kicked from the server for exceeding the warning limit (%d warnings).", player->name.size(), player->name.ptr(), warns)); source->sendNotice(nick, Jupiter::StringS::Format("%.*s has been kicked from the server for exceeding the warning limit (%d warnings).", player->name.size(), player->name.ptr(), warns));
break; break;
default: default:
server->banPlayer(player, Jupiter::StringS::Format("Warning limit reached (%d warnings)", warns), pluginInstance.warnAction); server->banPlayer(player, STRING_LITERAL_AS_REFERENCE("Jupiter Bot/RenX.Warn"), Jupiter::StringS::Format("Warning limit reached (%d warnings)", warns), pluginInstance.warnAction);
source->sendNotice(nick, Jupiter::StringS::Format("%.*s has been banned from the server for exceeding the warning limit (%d warnings).", player->name.size(), player->name.ptr(), warns)); source->sendNotice(nick, Jupiter::StringS::Format("%.*s has been banned from the server for exceeding the warning limit (%d warnings).", player->name.size(), player->name.ptr(), warns));
break; break;
} }
@ -187,7 +187,7 @@ void WarnGameCommand::trigger(RenX::Server *source, RenX::PlayerInfo *player, co
source->sendMessage(player, Jupiter::StringS::Format("%.*s has been kicked from the server for exceeding the warning limit (%d warnings).", target->name.size(), target->name.ptr(), warns)); source->sendMessage(player, Jupiter::StringS::Format("%.*s has been kicked from the server for exceeding the warning limit (%d warnings).", target->name.size(), target->name.ptr(), warns));
break; break;
default: default:
source->banPlayer(target, Jupiter::StringS::Format("Warning limit reached (%d warnings)", warns), pluginInstance.warnAction); source->banPlayer(target, STRING_LITERAL_AS_REFERENCE("Jupiter Bot/RenX.Warn"), Jupiter::StringS::Format("Warning limit reached (%d warnings)", warns), pluginInstance.warnAction);
source->sendMessage(player, Jupiter::StringS::Format("%.*s has been banned from the server for exceeding the warning limit (%d warnings).", target->name.size(), target->name.ptr(), warns)); source->sendMessage(player, Jupiter::StringS::Format("%.*s has been banned from the server for exceeding the warning limit (%d warnings).", target->name.size(), target->name.ptr(), warns));
break; break;
} }

Loading…
Cancel
Save