Add sqlite3 for ip / online id ban table

This commit is contained in:
Benau 2019-05-06 00:19:58 +08:00
parent 7d19ccaad8
commit bcc3aef4c8
7 changed files with 467 additions and 28 deletions

View File

@ -24,6 +24,7 @@ option(USE_SYSTEM_ANGELSCRIPT "Use system angelscript instead of built-in angels
option(USE_SYSTEM_ENET "Use system ENet instead of the built-in version, when available." ON)
option(USE_SYSTEM_GLEW "Use system GLEW instead of the built-in version, when available." ON)
option(USE_SYSTEM_WIIUSE "Use system WiiUse instead of the built-in version, when available." OFF)
option(USE_SQLITE3 "Use sqlite to manage server stats and ban list." ON)
option(USE_CRYPTO_OPENSSL "Use OpenSSL instead of Nettle for cryptography in STK." OFF)
CMAKE_DEPENDENT_OPTION(BUILD_RECORDER "Build opengl recorder" ON
@ -363,6 +364,23 @@ if(USE_LIBBFD)
endif()
endif()
# SQLITE
if(USE_SQLITE3)
find_library(SQLITE3_LIBRARY NAMES sqlite3 libsqlite3)
find_path(SQLITE3_INCLUDEDIR NAMES sqlite3.h PATHS)
if (NOT SQLITE3_LIBRARY OR NOT SQLITE3_INCLUDEDIR)
# add_subdirectory("${PROJECT_SOURCE_DIR}/lib/sqlite3")
# include_directories("${PROJECT_SOURCE_DIR}/lib/sqlite3")
# SET(SQLITE3_LIBRARY sqlite3)
set(USE_SQLITE3 OFF CACHE BOOL "Use sqlite to manage server stats and ban list." FORCE)
message(WARNING "Sqlite3 not found, disable advanced server management.")
else()
add_definitions(-DENABLE_SQLITE3)
include_directories("${SQLITE3_INCLUDEDIR}")
MESSAGE(STATUS "Use system libsqlite3: ${SQLITE3_LIBRARY}")
endif()
endif()
# Set some compiler options
if(UNIX OR MINGW)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++0x")
@ -518,6 +536,10 @@ target_link_libraries(supertuxkart
${CURL_LIBRARIES}
)
if (USE_SQLITE3)
target_link_libraries(supertuxkart ${SQLITE3_LIBRARY})
endif()
if (USE_CRYPTO_OPENSSL)
target_link_libraries(supertuxkart ${OPENSSL_CRYPTO_LIBRARY})
else()

View File

@ -202,3 +202,55 @@ Tested on a Raspberry Pi 3 Model B+, if you have 8 players connected to a server
For bad network simulation, we recommend `network traffic control` by linux kernel, see [here](https://wiki.linuxfoundation.org/networking/netem) for details.
You have the best gaming experience when choosing server having all players less than 100ms ping with no packet loss.
## Server management (Since 1.1)
Currently STK uses sqlite (if building with sqlite3 on) for server management with the following functions at the moment:
1. Server statistics
2. IP / online ID ban list
You need to create a database in sqlite first, run `sqlite3 stkservers.db` in the folder where (all) your server_config.xml(s) located.
STK will automatically create a table named `v(server database version)_(your_server_config_filename_without_.xml_extension)_stats` in your database if not exists:
```sql
CREATE TABLE IF NOT EXISTS (table name above)
(
host_id INTEGER UNSIGNED NOT NULL PRIMARY KEY, -- Unique host id in STKHost of each connection session for a STKPeer
ip INTEGER UNSIGNED NOT NULL, -- IP decimal of host
port INTEGER UNSIGNED NOT NULL, -- Port of host
online_id INTEGER UNSIGNED NOT NULL, -- Online if of the host (0 for offline account)
username TEXT NOT NULL, -- First player name in the host (if the host has splitscreen player)
player_num INEGER UNSIGNED NOT NULL, -- Number of player(s) from the host, more than 1 if it has splitscreen player
country_id TEXT NULL DEFAULT NULL, -- Country id of the host
version TEXT NOT NULL, -- SuperTuxKart version of the host (with OS info)
connected_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Time when connected
disconnected_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Time when disconnected (saved when disconnected)
ping INEGER UNSIGNED NOT NULL DEFAULT 0 -- Ping of the host
) WITHOUT ROWID;
```
For IP or online ID ban list, you need to create one yourself:
```sql
CREATE TABLE ipban
(
ip_start INTEGER UNSIGNED NOT NULL UNIQUE, -- Starting of ip decimal for banning (inclusive)
ip_end INTEGER UNSIGNED NOT NULL UNIQUE, -- Ending of ip decimal for banning (inclusive)
starting_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Starting time of this banning entry to be effective
expired_days REAL NULL DEFAULT NULL, -- Days for this banning to be expired, use NULL for a permanent ban
reason TEXT NOT NULL DEFAULT '', -- Banned reason shown in user stk menu, can be empty
description TEXT NOT NULL DEFAULT '', -- Private description for server admin
trigger_count INEGER UNSIGNED NOT NULL DEFAULT 0, -- Number of banning triggered by this ban entry
last_trigger TIMESTAMP NULL DEFAULT NULL -- Latest time this banning entry was triggered
);
CREATE TABLE onlineidban
(
online_id INTEGER UNSIGNED NOT NULL UNIQUE, -- Online id from STK addons database for banning
starting_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Starting time of this banning entry to be effective
expired_days REAL NULL DEFAULT NULL, -- Days for this banning to be expired, use NULL for a permanent ban
reason TEXT NOT NULL DEFAULT '', -- Banned reason shown in user stk menu, can be empty
description TEXT NOT NULL DEFAULT '', -- Private description for server admin
trigger_count INEGER UNSIGNED NOT NULL DEFAULT 0, -- Number of banning triggered by this ban entry
last_trigger TIMESTAMP NULL DEFAULT NULL -- Latest time this banning entry was triggered
);
```

View File

@ -91,6 +91,8 @@ void mainLoop(STKHost* host)
peer->kick();
// ATM use permanently ban
auto sl = LobbyProtocol::get<ServerLobby>();
if (sl)
sl->saveIPBanTable(peer->getAddress());
auto lock = sl->acquireConnectionMutex();
ServerConfig::m_server_ip_ban_list
[peer->getAddress().toString(false/*show_port*/) + "/32"]
@ -114,6 +116,10 @@ void mainLoop(STKHost* host)
}
else if (str == "listban")
{
auto sl = LobbyProtocol::get<ServerLobby>();
if (sl)
sl->listBanTable();
for (auto& ban : ServerConfig::m_server_ip_ban_list)
{
if (ban.first == "0.0.0.0/0")

View File

@ -55,6 +55,7 @@
#include <algorithm>
#include <fstream>
#include <iostream>
#include <iterator>
/** This is the central game setup protocol running in the server. It is
@ -144,6 +145,7 @@ ServerLobby::ServerLobby() : LobbyProtocol(NULL)
m_difficulty.store(ServerConfig::m_server_difficulty);
m_game_mode.store(ServerConfig::m_server_mode);
m_default_vote = new PeerVote();
initDatabase();
} // ServerLobby
//-----------------------------------------------------------------------------
@ -161,8 +163,105 @@ ServerLobby::~ServerLobby()
if (m_save_server_config)
ServerConfig::writeServerConfigToDisk();
delete m_default_vote;
destroyDatabase();
} // ~ServerLobby
//-----------------------------------------------------------------------------
void ServerLobby::initDatabase()
{
#ifdef ENABLE_SQLITE3
m_db = NULL;
sqlite3_stmt* stmt = NULL;;
m_ip_ban_table_exists = false;
m_online_id_ban_table_exists = false;
if (!ServerConfig::m_sql_management)
return;
int ret = sqlite3_open(ServerConfig::m_database_file.c_str(), &m_db);
if (ret != SQLITE_OK)
{
Log::error("ServerLobby", "Cannot open database: %s.",
sqlite3_errmsg(m_db));
sqlite3_close(m_db);
m_db = NULL;
return;
}
std::string query = StringUtils::insertValues(
"SELECT count(type) FROM sqlite_master "
"WHERE type='table' AND name='%s';",
ServerConfig::m_ip_ban_table.c_str());
ret = sqlite3_prepare_v2(m_db, query.c_str(), -1, &stmt, 0);
if (ret == SQLITE_OK)
{
ret = sqlite3_step(stmt);
if (ret == SQLITE_ROW)
{
int number = sqlite3_column_int(stmt, 0);
if (number == 1)
{
Log::info("ServerLobby", "%s ip ban table will used.",
ServerConfig::m_ip_ban_table.c_str());
m_ip_ban_table_exists = true;
}
}
ret = sqlite3_finalize(stmt);
if (ret != SQLITE_OK)
{
Log::error("ServerLobby", "Error finalize database: %s",
sqlite3_errmsg(m_db));
}
}
if (!m_ip_ban_table_exists)
{
Log::warn("ServerLobby", "%s ip ban table not found in database.",
ServerConfig::m_ip_ban_table.c_str());
}
query = StringUtils::insertValues(
"SELECT count(type) FROM sqlite_master "
"WHERE type='table' AND name='%s';",
ServerConfig::m_online_id_ban_table.c_str());
ret = sqlite3_prepare_v2(m_db, query.c_str(), -1, &stmt, 0);
if (ret == SQLITE_OK)
{
ret = sqlite3_step(stmt);
if (ret == SQLITE_ROW)
{
int number = sqlite3_column_int(stmt, 0);
if (number == 1)
{
Log::info("ServerLobby", "%s online id ban table will used.",
ServerConfig::m_online_id_ban_table.c_str());
m_online_id_ban_table_exists = true;
}
}
ret = sqlite3_finalize(stmt);
if (ret != SQLITE_OK)
{
Log::error("ServerLobby", "Error finalize database: %s",
sqlite3_errmsg(m_db));
}
}
if (!m_online_id_ban_table_exists)
{
Log::warn("ServerLobby",
"%s online id ban table not found in database.",
ServerConfig::m_online_id_ban_table.c_str());
}
#endif
} // initDatabase
//-----------------------------------------------------------------------------
void ServerLobby::destroyDatabase()
{
#ifdef ENABLE_SQLITE3
if (m_db != NULL)
sqlite3_close(m_db);
#endif
} // destroyDatabase
//-----------------------------------------------------------------------------
/** Called whenever server is reset or game mode is changed.
*/
@ -2075,6 +2174,51 @@ void ServerLobby::clearDisconnectedRankedPlayer()
}
} // clearDisconnectedRankedPlayer
//-----------------------------------------------------------------------------
void ServerLobby::kickPlayerWithReason(STKPeer* peer, const char* reason) const
{
NetworkString *message = getNetworkString(2);
message->setSynchronous(true);
message->addUInt8(LE_CONNECTION_REFUSED).addUInt8(RR_BANNED);
message->encodeString(std::string(reason));
peer->cleanPlayerProfiles();
peer->sendPacket(message, true/*reliable*/, false/*encrypted*/);
peer->reset();
delete message;
} // kickPlayerWithReason
//-----------------------------------------------------------------------------
void ServerLobby::saveIPBanTable(const TransportAddress& addr)
{
#ifdef ENABLE_SQLITE3
if (!m_db || !m_ip_ban_table_exists)
return;
std::string query = StringUtils::insertValues(
"INSERT INTO %s (ip_start, ip_end) "
"VALUES (%u, %u);",
ServerConfig::m_ip_ban_table.c_str(), addr.getIP(), addr.getIP());
sqlite3_stmt* stmt = NULL;
int ret = sqlite3_prepare_v2(m_db, query.c_str(), -1, &stmt, 0);
if (ret == SQLITE_OK)
{
ret = sqlite3_step(stmt);
ret = sqlite3_finalize(stmt);
if (ret != SQLITE_OK)
{
Log::error("ServerLobby", "Error finalize database: %s",
sqlite3_errmsg(m_db));
}
}
else
{
Log::error("ServerLobby", "Error preparing database: %s",
sqlite3_errmsg(m_db));
}
#endif
} // saveIPBanTable
//-----------------------------------------------------------------------------
void ServerLobby::connectionRequested(Event* event)
{
@ -2207,33 +2351,16 @@ void ServerLobby::connectionRequested(Event* event)
online_id = data.getUInt32();
encrypted_size = data.getUInt32();
bool is_banned = isBannedForIP(peer->getAddress());
if (online_id != 0 && !is_banned)
{
if (m_online_id_ban_list.find(online_id) !=
m_online_id_ban_list.end() &&
(uint32_t)StkTime::getTimeSinceEpoch() <
m_online_id_ban_list.at(online_id))
{
is_banned = true;
}
}
if (is_banned)
{
NetworkString *message = getNetworkString(2);
message->setSynchronous(true);
message->addUInt8(LE_CONNECTION_REFUSED).addUInt8(RR_BANNED);
// For future we can say the reason here
// and use encodeString instead
message->addUInt8(0);
peer->cleanPlayerProfiles();
peer->sendPacket(message, true/*reliable*/, false/*encrypted*/);
peer->reset();
delete message;
Log::verbose("ServerLobby", "Player refused: banned");
// Will be disconnected if banned by IP
testBannedForIP(peer.get());
if (peer->isDisconnected())
return;
if (online_id != 0)
testBannedForOnlineId(peer.get(), online_id);
// Will be disconnected if banned by online id
if (peer->isDisconnected())
return;
}
unsigned total_players = 0;
STKHost::get()->updatePlayers(NULL, NULL, &total_players);
@ -3253,6 +3380,192 @@ void ServerLobby::resetServer()
WAITING_FOR_START_GAME : REGISTER_SELF_ADDRESS;
} // resetServer
//-----------------------------------------------------------------------------
void ServerLobby::testBannedForIP(STKPeer* peer) const
{
#ifdef ENABLE_SQLITE3
if (!m_db || !m_ip_ban_table_exists)
return;
int row_id = -1;
unsigned ip_start = 0;
unsigned ip_end = 0;
std::string query = StringUtils::insertValues(
"SELECT rowid, ip_start, ip_end, reason, description FROM %s "
"WHERE ip_start <= %u AND ip_end >= %u "
"AND datetime('now') > datetime(starting_time) AND "
"(expired_days is NULL OR datetime"
"(starting_time, '+'||expired_days||' days') > datetime('now')) "
"LIMIT 1;",
ServerConfig::m_ip_ban_table.c_str(),
peer->getAddress().getIP(), peer->getAddress().getIP());
sqlite3_stmt* stmt = NULL;
int ret = sqlite3_prepare_v2(m_db, query.c_str(), -1, &stmt, 0);
if (ret == SQLITE_OK)
{
ret = sqlite3_step(stmt);
if (ret == SQLITE_ROW)
{
row_id = sqlite3_column_int(stmt, 0);
ip_start = (unsigned)sqlite3_column_int64(stmt, 1);
ip_end = (unsigned)sqlite3_column_int64(stmt, 2);
const char* reason = (char*)sqlite3_column_text(stmt, 3);
const char* desc = (char*)sqlite3_column_text(stmt, 4);
Log::info("ServerLobby", "%s banned by IP: %s "
"(rowid: %d, description: %s).",
peer->getAddress().toString().c_str(), reason, row_id, desc);
kickPlayerWithReason(peer, reason);
}
ret = sqlite3_finalize(stmt);
if (ret != SQLITE_OK)
{
Log::error("ServerLobby", "Error finalize database: %s",
sqlite3_errmsg(m_db));
}
}
else
{
Log::error("ServerLobby", "Error preparing database: %s",
sqlite3_errmsg(m_db));
return;
}
if (row_id != -1)
{
query = StringUtils::insertValues(
"UPDATE %s SET trigger_count = trigger_count + 1, "
"last_trigger = datetime('now') "
"WHERE ip_start = %u AND ip_end = %u;",
ServerConfig::m_ip_ban_table.c_str(), ip_start, ip_end);
int ret = sqlite3_prepare_v2(m_db, query.c_str(), -1, &stmt, 0);
if (ret == SQLITE_OK)
{
ret = sqlite3_step(stmt);
ret = sqlite3_finalize(stmt);
if (ret != SQLITE_OK)
{
Log::error("ServerLobby", "Error finalize database: %s",
sqlite3_errmsg(m_db));
}
}
else
{
Log::error("ServerLobby", "Error preparing database: %s",
sqlite3_errmsg(m_db));
}
}
#endif
} // testBannedForIP
//-----------------------------------------------------------------------------
void ServerLobby::testBannedForOnlineId(STKPeer* peer,
uint32_t online_id) const
{
#ifdef ENABLE_SQLITE3
if (!m_db || !m_online_id_ban_table_exists)
return;
int row_id = -1;
std::string query = StringUtils::insertValues(
"SELECT rowid, reason, description FROM %s "
"WHERE online_id = %u "
"AND datetime('now') > datetime(starting_time) AND "
"(expired_days is NULL OR datetime"
"(starting_time, '+'||expired_days||' days') > datetime('now')) "
"LIMIT 1;",
ServerConfig::m_online_id_ban_table.c_str(), online_id);
sqlite3_stmt* stmt = NULL;
int ret = sqlite3_prepare_v2(m_db, query.c_str(), -1, &stmt, 0);
if (ret == SQLITE_OK)
{
ret = sqlite3_step(stmt);
if (ret == SQLITE_ROW)
{
row_id = sqlite3_column_int(stmt, 0);
const char* reason = (char*)sqlite3_column_text(stmt, 1);
const char* desc = (char*)sqlite3_column_text(stmt, 2);
Log::info("ServerLobby", "%s banned by online id: %s "
"(online id: %u rowid: %d, description: %s).",
peer->getAddress().toString().c_str(), reason, online_id,
row_id, desc);
kickPlayerWithReason(peer, reason);
}
ret = sqlite3_finalize(stmt);
if (ret != SQLITE_OK)
{
Log::error("ServerLobby", "Error finalize database: %s",
sqlite3_errmsg(m_db));
}
}
else
{
Log::error("ServerLobby", "Error preparing database: %s",
sqlite3_errmsg(m_db));
return;
}
if (row_id != -1)
{
query = StringUtils::insertValues(
"UPDATE %s SET trigger_count = trigger_count + 1, "
"last_trigger = datetime('now') "
"WHERE online_id = %u;",
ServerConfig::m_online_id_ban_table.c_str(), online_id);
int ret = sqlite3_prepare_v2(m_db, query.c_str(), -1, &stmt, 0);
if (ret == SQLITE_OK)
{
ret = sqlite3_step(stmt);
ret = sqlite3_finalize(stmt);
if (ret != SQLITE_OK)
{
Log::error("ServerLobby", "Error finalize database: %s",
sqlite3_errmsg(m_db));
}
}
else
{
Log::error("ServerLobby", "Error preparing database: %s",
sqlite3_errmsg(m_db));
}
}
#endif
} // testBannedForOnlineId
//-----------------------------------------------------------------------------
void ServerLobby::listBanTable()
{
#ifdef ENABLE_SQLITE3
if (!m_db)
return;
auto printer = [](void* data, int argc, char** argv, char** name)
{
for (int i = 0; i < argc; i++)
{
std::cout << name[i] << " = " << (argv[i] ? argv[i] : "NULL")
<< "\n";
}
std::cout << "\n";
return 0;
};
if (m_ip_ban_table_exists)
{
std::string query = "SELECT * FROM ";
query += ServerConfig::m_ip_ban_table;
query += ";";
std::cout << "IP ban list:\n";
sqlite3_exec(m_db, query.c_str(), printer, NULL, NULL);
}
if (m_online_id_ban_table_exists)
{
std::string query = "SELECT * FROM ";
query += ServerConfig::m_online_id_ban_table;
query += ";";
std::cout << "Online Id ban list:\n";
sqlite3_exec(m_db, query.c_str(), printer, NULL, NULL);
}
#endif
} // listBanTable
//-----------------------------------------------------------------------------
bool ServerLobby::isBannedForIP(const TransportAddress& addr) const
{

View File

@ -34,6 +34,10 @@
#include <set>
#include <tuple>
#ifdef ENABLE_SQLITE3
#include <sqlite3.h>
#endif
class BareNetworkString;
class NetworkString;
class NetworkPlayerProfile;
@ -66,6 +70,17 @@ private:
irr::core::stringw m_name;
bool m_tried = false;
};
#ifdef ENABLE_SQLITE3
sqlite3* m_db;
bool m_ip_ban_table_exists;
bool m_online_id_ban_table_exists;
#endif
void initDatabase();
void destroyDatabase();
std::atomic<ServerState> m_state;
@ -305,6 +320,9 @@ private:
void handleKartInfo(Event* event);
void clientInGameWantsToBackLobby(Event* event);
void clientSelectingAssetsWantsToBackLobby(Event* event);
void kickPlayerWithReason(STKPeer* peer, const char* reason) const;
void testBannedForIP(STKPeer* peer) const;
void testBannedForOnlineId(STKPeer* peer, uint32_t online_id) const;
public:
ServerLobby();
virtual ~ServerLobby();
@ -333,6 +351,8 @@ public:
int getDifficulty() const { return m_difficulty.load(); }
int getGameMode() const { return m_game_mode.load(); }
void saveInitialItems();
void saveIPBanTable(const TransportAddress& addr);
void listBanTable();
}; // class ServerLobby
#endif // SERVER_LOBBY_HPP

View File

@ -319,6 +319,32 @@ namespace ServerConfig
"more rewind, which clients with slow device may have problem playing "
"this server, use the default value is recommended."));
SERVER_CFG_PREFIX BoolServerConfigParam m_sql_management
SERVER_CFG_DEFAULT(BoolServerConfigParam(false,
"sql-management",
"Use sql to manage server stats and banlist, STK needs to be compiled "
"with sqlite3 supported."));
SERVER_CFG_PREFIX StringServerConfigParam m_database_file
SERVER_CFG_DEFAULT(StringServerConfigParam("stkservers.db",
"database-file",
"Database filename for sqlite to use, it can be shared for servers "
"creating in this machine, and stk will create specific table for each "
"server. You need to create the database yourself first, see "
"NETWORKING.md for details"));
SERVER_CFG_PREFIX StringServerConfigParam m_ip_ban_table
SERVER_CFG_DEFAULT(StringServerConfigParam("ipban",
"ip-ban-table",
"Ip ban list table name, you need to create the table first, see "
"NETWORKING.md for details."));
SERVER_CFG_PREFIX StringServerConfigParam m_online_id_ban_table
SERVER_CFG_DEFAULT(StringServerConfigParam("onlineidban",
"online-id-ban-table",
"Online ID ban list table name, you need to create the table first, "
"see NETWORKING.md for details."));
SERVER_CFG_PREFIX StringToUIntServerConfigParam m_server_ip_ban_list
SERVER_CFG_DEFAULT(StringToUIntServerConfigParam("server-ip-ban-list",
"ip: IP in X.X.X.X/Y (CIDR) format for banning, use Y of 32 for a "

View File

@ -356,7 +356,7 @@ namespace StringUtils
else
{
if(sv[i][1]=='s' || sv[i][1]=='d' || sv[i][1]=='i' ||
sv[i][1]=='f')
sv[i][1]=='f' || sv[i][1]=='u')
{
if (insertValID >= all_vals.size())
{
@ -429,7 +429,7 @@ namespace StringUtils
else
{
if (sv[i][1]=='s' || sv[i][1]=='d' || sv[i][1]=='i' ||
sv[i][1]=='f')
sv[i][1]=='f' || sv[i][1]=='u')
{
if (insertValID >= all_vals.size())
{