Add possibility to ban players by name

This uses the Unicode confusable data together with judging how close
two strings are by using the Levenshtein distance.

Adds the commands `name_ban`, `name_unban` and `name_bans`. Kicks
players who join using a banned name and doesn't allow ingame players to
change their names to the banned ones.
This commit is contained in:
heinrich5991 2018-03-06 18:41:18 +01:00 committed by heinrich5991
parent 710d0610d9
commit 31a3e8d4c0
7 changed files with 319 additions and 10 deletions

View file

@ -1101,6 +1101,7 @@ if(GTEST_FOUND OR DOWNLOAD_GTEST)
fs.cpp
git_revision.cpp
jobs.cpp
str.cpp
strip_path_and_extension.cpp
teehistorian.cpp
test.cpp

View file

@ -63,6 +63,23 @@ int str_utf8_skeleton_next(struct SKELETON *skel)
return ch;
}
int str_utf8_to_skeleton(const char *str, int *buf, int buf_len)
{
int i;
struct SKELETON skel;
str_utf8_skeleton_begin(&skel, str);
for(i = 0; i < buf_len; i++)
{
int ch = str_utf8_skeleton_next(&skel);
if(ch == 0)
{
break;
}
buf[i] = ch;
}
return i;
}
int str_utf8_comp_confusable(const char *str1, const char *str2)
{
struct SKELETON skel1;

View file

@ -2408,6 +2408,98 @@ int str_comp_filenames(const char *a, const char *b)
return *a - *b;
}
static int min3(int a, int b, int c)
{
int min = a;
if(b < min)
min = b;
if(c < min)
min = c;
return min;
}
int str_utf8_dist(const char *a, const char *b)
{
int buf_len = 2 * (str_length(a) + 1 + str_length(b) + 1);
int *buf = (int *)mem_alloc(buf_len * sizeof(*buf), 1);
int result = str_utf8_dist_buffer(a, b, buf, buf_len);
mem_free(buf);
return result;
}
static int str_to_utf32_unchecked(const char *str, int **out)
{
int out_len = 0;
while((**out = str_utf8_decode(&str)))
{
(*out)++;
out_len++;
}
return out_len;
}
int str_utf32_dist_buffer(const int *a, int a_len, const int *b, int b_len, int *buf, int buf_len)
{
int i, j;
dbg_assert(buf_len >= (a_len + 1) + (b_len + 1), "buffer too small");
if(a_len > b_len)
{
int tmp1 = a_len;
const int *tmp2 = a;
a_len = b_len;
a = b;
b_len = tmp1;
b = tmp2;
}
#define B(i, j) buf[((j)&1) * (a_len + 1) + (i)]
for(i = 0; i <= a_len; i++)
{
B(i, 0) = i;
}
for(j = 1; j <= b_len; j++)
{
B(0, j) = j;
for(i = 1; i <= a_len; i++)
{
int subst = (a[i - 1] != b[j - 1]);
B(i, j) = min3(
B(i - 1, j) + 1,
B(i, j - 1) + 1,
B(i - 1, j - 1) + subst
);
}
}
return B(a_len, b_len);
#undef B
}
int str_utf8_dist_buffer(const char *a_utf8, const char *b_utf8, int *buf, int buf_len)
{
int a_utf8_len = str_length(a_utf8);
int b_utf8_len = str_length(b_utf8);
int *a, *b; // UTF-32
int a_len, b_len; // UTF-32 length
dbg_assert(buf_len >= 2 * (a_utf8_len + 1 + b_utf8_len + 1), "buffer too small");
if(a_utf8_len > b_utf8_len)
{
int tmp1 = a_utf8_len;
const char *tmp2 = a_utf8;
a_utf8_len = b_utf8_len;
a_utf8 = b_utf8;
b_utf8_len = tmp1;
b_utf8 = tmp2;
}
a = buf;
a_len = str_to_utf32_unchecked(a_utf8, &buf);
b = buf;
b_len = str_to_utf32_unchecked(b_utf8, &buf);
return str_utf32_dist_buffer(a, a_len, b, b_len, buf, buf_len - b_len - a_len);
}
const char *str_find_nocase(const char *haystack, const char *needle)
{
while(*haystack) /* native implementation */

View file

@ -1181,6 +1181,64 @@ int str_comp_num(const char *a, const char *b, const int num);
*/
int str_comp_filenames(const char *a, const char *b);
/*
Function: str_utf8_dist
Computes the edit distance between two strings.
Parameters:
a - First string for the edit distance.
b - Second string for the edit distance.
Returns:
The edit distance between the both strings.
Remarks:
- The strings are treated as zero-terminated strings.
*/
int str_utf8_dist(const char *a, const char *b);
/*
Function: str_utf8_dist_buffer
Computes the edit distance between two strings, allows buffers
to be passed in.
Parameters:
a - First string for the edit distance.
b - Second string for the edit distance.
buf - Buffer for the function.
buf_len - Length of the buffer, must be at least as long as
twice the length of both strings combined plus two.
Returns:
The edit distance between the both strings.
Remarks:
- The strings are treated as zero-terminated strings.
*/
int str_utf8_dist_buffer(const char *a, const char *b, int *buf, int buf_len);
/*
Function: str_utf32_dist_buffer
Computes the edit distance between two strings, allows buffers
to be passed in.
Parameters:
a - First string for the edit distance.
a_len - Length of the first string.
b - Second string for the edit distance.
b_len - Length of the second string.
buf - Buffer for the function.
buf_len - Length of the buffer, must be at least as long as
the length of both strings combined plus two.
Returns:
The edit distance between the both strings.
Remarks:
- The strings are treated as zero-terminated strings.
*/
int str_utf32_dist_buffer(const int *a, int a_len, const int *b, int b_len, int *buf, int buf_len);
/*
Function: str_find_nocase
Finds a string inside another string case insensitive.
@ -1235,18 +1293,18 @@ void str_hex(char *dst, int dst_size, const void *data, int data_size);
Function: str_hex_decode
Takes a hex string and returns a byte array.
Parameters:
dst - Buffer for the byte array
dst_size - size of the buffer
data - String to decode
Parameters:
dst - Buffer for the byte array
dst_size - size of the buffer
data - String to decode
Returns:
2 - String doesn't exactly fit the buffer
1 - Invalid character in string
0 - Success
Returns:
2 - String doesn't exactly fit the buffer
1 - Invalid character in string
0 - Success
Remarks:
- The contents of the buffer is only valid on success
Remarks:
- The contents of the buffer is only valid on success
*/
int str_hex_decode(unsigned char *dst, int dst_size, const char *src);
/*
@ -1504,6 +1562,11 @@ int str_isspace(char c);
char str_uppercase(char c);
unsigned str_quickhash(const char *str);
struct SKELETON;
void str_utf8_skeleton_begin(struct SKELETON *skel, const char *str);
int str_utf8_skeleton_next(struct SKELETON *skel);
int str_utf8_to_skeleton(const char *str, int *buf, int buf_len);
/*
Function: str_utf8_comp_confusable
Compares two strings for visual appearance.

View file

@ -391,6 +391,29 @@ void CServer::SetClientName(int ClientID, const char *pName)
if(!pName)
return;
int Skeleton[MAX_NAME_SKELETON_LENGTH];
int SkeletonLength = str_utf8_to_skeleton(pName, Skeleton, sizeof(Skeleton) / sizeof(Skeleton[0]));
int Buffer[MAX_NAME_SKELETON_LENGTH * 2 + 2];
bool Banned = false;
for(int i = 0; i < m_aNameBans.size(); i++)
{
CNameBan *pBan = &m_aNameBans[i];
int Distance = str_utf32_dist_buffer(Skeleton, SkeletonLength, pBan->m_aSkeleton, pBan->m_SkeletonLength, Buffer, sizeof(Buffer) / sizeof(Buffer[0]));
if(Distance <= pBan->m_Distance)
{
Banned = true;
}
}
if(Banned)
{
if(m_aClients[ClientID].m_State == CClient::STATE_READY)
{
Kick(ClientID, "Kicked (your name is banned)");
}
return;
}
char aNameTry[MAX_NAME_LENGTH];
str_copy(aNameTry, pName, sizeof(aNameTry));
if(TrySetClientName(ClientID, aNameTry))
@ -2328,6 +2351,67 @@ void CServer::ConAuthList(IConsole::IResult *pResult, void *pUser)
pManager->ListKeys(ListKeysCallback, pThis);
}
void CServer::ConNameBan(IConsole::IResult *pResult, void *pUser)
{
CServer *pThis = (CServer *)pUser;
char aBuf[64];
const char *pName = pResult->GetString(0);
int Distance;
if(pResult->NumArguments() > 1)
{
Distance = pResult->GetInteger(1);
}
else
{
Distance = str_length(pName) / 3;
}
for(int i = 0; i < pThis->m_aNameBans.size(); i++)
{
CNameBan *pBan = &pThis->m_aNameBans[i];
if(str_comp(pBan->m_aName, pName) == 0)
{
str_format(aBuf, sizeof(aBuf), "changed name='%s' distance=%d old_distance=%d", pName, Distance, pBan->m_Distance);
pThis->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "name_ban", aBuf);
pBan->m_Distance = Distance;
return;
}
}
pThis->m_aNameBans.add(CNameBan(pName, Distance));
str_format(aBuf, sizeof(aBuf), "added name='%s' distance=%d", pName, Distance);
pThis->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "name_ban", aBuf);
}
void CServer::ConNameUnban(IConsole::IResult *pResult, void *pUser)
{
CServer *pThis = (CServer *)pUser;
const char *pName = pResult->GetString(0);
for(int i = 0; i < pThis->m_aNameBans.size(); i++)
{
CNameBan *pBan = &pThis->m_aNameBans[i];
if(str_comp(pBan->m_aName, pName) == 0)
{
char aBuf[64];
str_format(aBuf, sizeof(aBuf), "removed name='%s' distance=%d", pBan->m_aName, pBan->m_Distance);
pThis->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "name_ban", aBuf);
pThis->m_aNameBans.remove_index(i);
}
}
}
void CServer::ConNameBans(IConsole::IResult *pResult, void *pUser)
{
CServer *pThis = (CServer *)pUser;
for(int i = 0; i < pThis->m_aNameBans.size(); i++)
{
CNameBan *pBan = &pThis->m_aNameBans[i];
char aBuf[64];
str_format(aBuf, sizeof(aBuf), "name='%s' distance=%d", pBan->m_aName, pBan->m_Distance);
pThis->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "name_ban", aBuf);
}
}
void CServer::ConShutdown(IConsole::IResult *pResult, void *pUser)
{
((CServer *)pUser)->m_RunServer = 0;
@ -2718,6 +2802,10 @@ void CServer::RegisterCommands()
Console()->Register("auth_remove", "s[ident]", CFGFLAG_SERVER|CFGFLAG_NONTEEHISTORIC, ConAuthRemove, this, "Remove a rcon key");
Console()->Register("auth_list", "", CFGFLAG_SERVER, ConAuthList, this, "List all rcon keys");
Console()->Register("name_ban", "s[name] ?i[distance]", CFGFLAG_SERVER, ConNameBan, this, "Ban a certain nick name");
Console()->Register("name_unban", "s[name]", CFGFLAG_SERVER, ConNameUnban, this, "Unban a certain nick name");
Console()->Register("name_bans", "", CFGFLAG_SERVER, ConNameBans, this, "List all name bans");
Console()->Chain("sv_name", ConchainSpecialInfoupdate, this);
Console()->Chain("password", ConchainSpecialInfoupdate, this);

View file

@ -19,6 +19,8 @@
#include <engine/shared/netban.h>
#include <engine/shared/uuid_manager.h>
#include <base/tl/array.h>
#include "authmanager.h"
#if defined (CONF_SQL)
@ -96,6 +98,27 @@ class CServer : public IServer
UNIXSOCKET m_ConnLoggingSocket;
#endif
enum
{
MAX_NAME_SKELETON_LENGTH=MAX_NAME_LENGTH*4,
};
class CNameBan
{
public:
CNameBan() {}
CNameBan(const char *pName, int Distance) :
m_Distance(Distance)
{
str_copy(m_aName, pName, sizeof(m_aName));
m_SkeletonLength = str_utf8_to_skeleton(m_aName, m_aSkeleton, sizeof(m_aSkeleton) / sizeof(m_aSkeleton[0]));
}
char m_aName[MAX_NAME_LENGTH];
int m_aSkeleton[MAX_NAME_SKELETON_LENGTH];
int m_SkeletonLength;
int m_Distance;
};
public:
class IGameServer *GameServer() { return m_pGameServer; }
class IConsole *Console() { return m_pConsole; }
@ -217,6 +240,8 @@ public:
char m_aErrorShutdownReason[128];
array<CNameBan> m_aNameBans;
CServer();
int TrySetClientName(int ClientID, const char *pName);
@ -308,6 +333,10 @@ public:
static void ConAuthRemove(IConsole::IResult *pResult, void *pUser);
static void ConAuthList(IConsole::IResult *pResult, void *pUser);
static void ConNameBan(IConsole::IResult *pResult, void *pUser);
static void ConNameUnban(IConsole::IResult *pResult, void *pUser);
static void ConNameBans(IConsole::IResult *pResult, void *pUser);
static void StatusImpl(IConsole::IResult *pResult, void *pUser, bool DnsblBlacklistedOnly);
#if defined (CONF_SQL)

19
src/test/str.cpp Normal file
View file

@ -0,0 +1,19 @@
#include <gtest/gtest.h>
#include <base/system.h>
TEST(Str, Dist)
{
EXPECT_EQ(str_utf8_dist("aaa", "aaa"), 0);
EXPECT_EQ(str_utf8_dist("123", "123"), 0);
EXPECT_EQ(str_utf8_dist("", ""), 0);
EXPECT_EQ(str_utf8_dist("a", "b"), 1);
EXPECT_EQ(str_utf8_dist("", "aaa"), 3);
EXPECT_EQ(str_utf8_dist("123", ""), 3);
EXPECT_EQ(str_utf8_dist("ä", ""), 1);
EXPECT_EQ(str_utf8_dist("Hëllö", "Hello"), 2);
// https://en.wikipedia.org/w/index.php?title=Levenshtein_distance&oldid=828480025#Example
EXPECT_EQ(str_utf8_dist("kitten", "sitting"), 3);
EXPECT_EQ(str_utf8_dist("flaw", "lawn"), 2);
EXPECT_EQ(str_utf8_dist("saturday", "sunday"), 3);
}