Permalink
Browse files

Recognize and kick for illegal non-player characters

  • Loading branch information...
RussellLVP committed Oct 21, 2016
1 parent c5e8fe2 commit deee1da6d918d921496c2196d14a6d7318c08335
View
@@ -2,6 +2,7 @@
"TEST_MESSAGE": "Hello, world!",
"ABUSE_ANNOUNCE_DETECTED": "%s (Id: %d) has just abused %s (%s time).",
"ABUSE_ANNOUNCE_KICKED": "%s (Id: %d) has been kicked for %s.",
"ANNOUNCE_ADMINISTRATORS": "{FFFF00}Admin notice{FFFFFF}: %s",
"ANNOUNCE_ALL": "{80CBC4}*** %s",
@@ -12,6 +12,7 @@ class Player {
this.id_ = playerId;
this.name_ = pawnInvoke('GetPlayerName', 'iS', playerId);
this.ipAddress_ = pawnInvoke('GetPlayerIp', 'iS', playerId);
this.nonPlayerCharacter_ = !!pawnInvoke('IsPlayerNPC', 'i', playerId);
this.gpci_ = pawnInvoke('gpci', 'iS', playerId);
this.connected_ = true;
@@ -38,6 +39,9 @@ class Player {
// Returns whether the player is still connected to the server.
isConnected() { return this.connected_; }
// Returns whether the player is a non-player character.
isNonPlayerCharacter() { return this.nonPlayerCharacter_; }
// Returns whether the player is connected, but has minimized their game.
isMinimized() { return isPlayerMinimized(this.id_); }
@@ -173,6 +177,10 @@ class Player {
get drunkLevel() { return pawnInvoke('GetPlayerDrunkLevel', 'i', this.id_); }
set drunkLevel(value) { pawnInvoke('SetPlayerDrunkLevel', 'ii', this.id_, value); }
// Kicks the player from the server. The user of this function is responsible for making sure
// that the reason for the kick is properly recorded.
kick() { pawnInvoke('Kick', 'i', this.id_); }
// Gets or sets the special action the player is currently engaged in. The values must be one of
// the Player.SPECIAL_ACTION_* constants static to this class.
get specialAction() { return pawnInvoke('GetPlayerSpecialAction', 'i', this.id_); }
@@ -17,13 +17,15 @@ class MockPlayer {
this.undercover_ = false;
this.gangId_ = null;
this.nonPlayerCharacter_ = event.npc || false;
this.health_ = 100;
this.armour_ = 100;
this.interiorId_ = 0;
this.virtualWorld_ = 0;
this.userId_ = null;
this.ipAddress_ = event.ipAddress || '127.0.0.1';
this.ipAddress_ = event.ip || '127.0.0.1';
this.position_ = new Vector(0, 0, 0);
this.specialAction_ = Player.SPECIAL_ACTION_NONE;
@@ -60,6 +62,10 @@ class MockPlayer {
isConnected() { return this.connected_; }
// Returns whether the player is a non-player character.
isNonPlayerCharacter() { return this.nonPlayerCharacter_; }
setNonPlayerCharacter(value) { this.nonPlayerCharacter_ = value; }
isMinimized() { return this.minimized_; }
setMinimized(minimized) { this.minimized_ = minimized; }
@@ -171,6 +177,10 @@ class MockPlayer {
return true;
}
// Kicks the player from the server. The user of this function is responsible for making sure
// that the reason for the kick is properly recorded.
kick() { this.disconnect(2 /* reason */); }
// Gets or sets the special action the player is currently engaged in. The values must be one of
// the Player.SPECIAL_ACTION_* constants static to this class.
get specialAction() { return this.specialAction_; }
@@ -6,14 +6,16 @@
// and informs administrators of the event when something has been detected.
//
// The abuse monitor is able to detect the following kinds of abuse:
// 1) Illegal vehicle entry: entering a vehicle that is locked for the player.
// 1) Illegal non-player character: connecting a non-player character from a remote host.
// 2) Illegal vehicle entry: entering a vehicle that is locked for the player.
//
class AbuseMonitor {
constructor(announce, settings) {
this.announce_ = announce;
this.settings_ = settings;
this.detected_ = new Map([
[ AbuseMonitor.TYPE_ILLEGAL_NON_PLAYER_CHARACTER, new WeakMap() ],
[ AbuseMonitor.TYPE_ILLEGAL_VEHICLE_ENTRY, new WeakMap() ]
]);
@@ -29,15 +31,27 @@ class AbuseMonitor {
// Reports that the |player| has been detected for abusing |type|. Administrators will receive
// a configurable amount of warnings for the |player, type| tuple as well.
reportAbuse(player, type) {
reportAbuse(player, type, { kick = false } = {}) {
const incidents = (this.detected_.get(type).get(player) || 0) + 1;
if (incidents <= this.getReportLimit()) {
if (kick || incidents <= this.getReportLimit()) {
const incidentDescription = this.getTypeDescription(type);
const incidentOrdinal = incidents.toOrdinalString();
this.announce_().announceToAdministrators(
Message.ABUSE_ANNOUNCE_DETECTED, player.name, player.id, incidentDescription,
incidentOrdinal);
// TODO: Make sure that the reason of the player's kick is recorded in the database.
// Process the incidents by order of severity. The reasons for kicks and bans will
// automagically be recorded in the database, so that administrators have references.
if (kick) {
this.announce_().announceToAdministrators(
Message.ABUSE_ANNOUNCE_KICKED, player.name, player.id, incidentDescription);
player.kick();
} else {
this.announce_().announceToAdministrators(
Message.ABUSE_ANNOUNCE_DETECTED, player.name, player.id, incidentDescription,
incidentOrdinal);
}
}
// Do keep track of the new number of incidents for the |player|.
@@ -58,6 +72,8 @@ class AbuseMonitor {
// Gets the textual description for the abuse |type|.
getTypeDescription(type) {
switch (type) {
case AbuseMonitor.TYPE_ILLEGAL_NON_PLAYER_CHARACTER:
return 'illegal non-player character';
case AbuseMonitor.TYPE_ILLEGAL_VEHICLE_ENTRY:
return 'illegal vehicle entry';
default:
@@ -67,6 +83,16 @@ class AbuseMonitor {
// ---------------------------------------------------------------------------------------------
// Called when the |player| has connected to the server. Will verify that non-player characters
// only connect from local addresses.
onPlayerConnect(player) {
if (player.isNonPlayerCharacter() && player.ip !== '127.0.0.1') {
this.reportAbuse(player, AbuseMonitor.TYPE_ILLEGAL_NON_PLAYER_CHARACTER, {
kick: true
});
}
}
// Called when the |player| enters the |vehicle|. Will report them for abuse when the vehicle
// was locked for them, and entry therefore shouldn't have been possible.
onPlayerEnterVehicle(player, vehicle) {
@@ -82,6 +108,7 @@ class AbuseMonitor {
}
// The different sorts of abuse that can be detected by the monitor.
AbuseMonitor.TYPE_ILLEGAL_VEHICLE_ENTRY = 0;
AbuseMonitor.TYPE_ILLEGAL_NON_PLAYER_CHARACTER = 0;
AbuseMonitor.TYPE_ILLEGAL_VEHICLE_ENTRY = 1;
exports = AbuseMonitor;
@@ -13,6 +13,30 @@ describe('AbuseMonitor', (it, beforeEach) => {
settings = server.featureManager.loadFeature('settings');
});
it('should be able to detect and kick fake non-player characters', assert => {
const russell = server.playerManager.getById(1 /* Russell */);
russell.identify();
russell.level = Player.LEVEL_ADMINISTRATOR;
// Connect the evil bot to the server. They should be kicked immediately after.
server.playerManager.onPlayerConnect({
playerid: 42,
name: 'EvilBot',
ip: '42.42.42.42',
npc: true
});
assert.isNull(server.playerManager.getById(42 /* evilbot */));
assert.equal(russell.messages.length, 1);
assert.isTrue(
russell.messages[0].includes(
Message.format(Message.ABUSE_ANNOUNCE_KICKED, 'EvilBot', 42,
'illegal non-player character')));
});
it('should be able to detect and report illegal vehicle entry', assert => {
const gunther = server.playerManager.getById(0 /* Gunther */);
const russell = server.playerManager.getById(1 /* Russell */);
@@ -70,7 +94,6 @@ describe('AbuseMonitor', (it, beforeEach) => {
const statistics = monitor.getPlayerStatistics(gunther);
assert.equal(statistics.get('illegal vehicle entry'), 14);
}
});
it('should gather and have names for all sorts of abuse', assert => {
@@ -90,12 +113,19 @@ describe('AbuseMonitor', (it, beforeEach) => {
assert.equal(typeof monitor.getTypeDescription(type), 'string');
}
// (2) Verify that all types are included in the statistics.
// (2) Verify that most types are included in the statistics.
{
const statistics = monitor.getPlayerStatistics(gunther);
const excluded = new Set([
AbuseMonitor.TYPE_ILLEGAL_NON_PLAYER_CHARACTER, // checked once on connect
]);
for (const type of types) {
if (excluded.has(type))
continue;
for (const type of types)
assert.isTrue(statistics.has(monitor.getTypeDescription(type)));
}
}
});
});

0 comments on commit deee1da

Please sign in to comment.