Add optional ip geolocation table support
This commit is contained in:
parent
005418d611
commit
21a23dc9e6
@ -150,21 +150,24 @@ The current server configuration xml looks like this:
|
||||
<!-- Use sql database for handling server stats and maintenance, STK needs to be compiled with sqlite3 supported. -->
|
||||
<sql-management value="false" />
|
||||
|
||||
<!-- 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 -->
|
||||
<!-- Database filename for sqlite to use, it can be shared for all servers created 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 -->
|
||||
<database-file value="stkservers.db" />
|
||||
|
||||
<!-- Ip ban list table name, you need to create the table first, see NETWORKING.md for details, empty to disable. -->
|
||||
<!-- Ip ban list table name, you need to create the table first, see NETWORKING.md for details, empty to disable. This table can be shared for all servers if you use the same name. -->
|
||||
<ip-ban-table value="ip_ban" />
|
||||
|
||||
<!-- Online ID ban list table name, you need to create the table first, see NETWORKING.md for details, empty to disable. -->
|
||||
<!-- Online ID ban list table name, you need to create the table first, see NETWORKING.md for details, empty to disable. This table can be shared for all servers if you use the same name. -->
|
||||
<online-id-ban-table value="online_id_ban" />
|
||||
|
||||
<!-- Player reports table name, which will be written when a player reports player in the network user dialog, you need to create the table first, see NETWORKING.md for details, empty to disable. -->
|
||||
<!-- Player reports table name, which will be written when a player reports player in the network user dialog, you need to create the table first, see NETWORKING.md for details, empty to disable. This table can be shared for all servers if you use the same name. -->
|
||||
<player-reports-table value="player_reports" />
|
||||
|
||||
<!-- Days to keep player reports, older than that will be auto cleared, 0 to keep them forever. -->
|
||||
<player-reports-expired-days value="3" />
|
||||
|
||||
<!-- IP geolocation table, you only need this table if you want to geolocate IP from non-stk-addons connection, as all validated players connecting from stk-addons will provide the location info, you need to create the table first, see NETWORKING.md for details, empty to disable. This table can be shared for all servers if you use the same name. -->
|
||||
<ip-geolocation-table value="ip_mapping" />
|
||||
|
||||
</server-config>
|
||||
|
||||
```
|
||||
@ -216,6 +219,8 @@ You have the best gaming experience when choosing server having all players less
|
||||
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
|
||||
3. Player reports
|
||||
4. IP geolocation
|
||||
|
||||
You need to create a database in sqlite first, run `sqlite3 stkservers.db` in the folder where (all) your server_config.xml(s) located.
|
||||
|
||||
@ -285,4 +290,15 @@ CREATE TABLE player_reports
|
||||
reporting_online_id INTEGER UNSIGNED NOT NULL, -- Online id of player being reported, 0 for offline player
|
||||
reporting_username TEXT NOT NULL -- Player name being reported
|
||||
);
|
||||
|
||||
CREATE TABLE ip_mapping
|
||||
(
|
||||
ip_start INTEGER UNSIGNED NOT NULL PRIMARY KEY UNIQUE, -- IP decimal start
|
||||
ip_end INTEGER UNSIGNED NOT NULL UNIQUE, -- IP decimal end
|
||||
latitude REAL NOT NULL, -- Latitude of this IP range
|
||||
longitude REAL NOT NULL, -- Longitude of this IP range
|
||||
country_code TEXT NOT NULL -- 2-letter country code
|
||||
) WITHOUT ROWID;
|
||||
```
|
||||
|
||||
For initialization of `ip_mapping` table, check [this script](tools/generate-ip-mappings.py).
|
||||
|
@ -174,6 +174,7 @@ void ServerLobby::initDatabase()
|
||||
m_db = NULL;
|
||||
m_ip_ban_table_exists = false;
|
||||
m_online_id_ban_table_exists = false;
|
||||
m_ip_geolocation_table_exists = false;
|
||||
if (!ServerConfig::m_sql_management)
|
||||
return;
|
||||
int ret = sqlite3_open(ServerConfig::m_database_file.c_str(), &m_db);
|
||||
@ -191,6 +192,8 @@ void ServerLobby::initDatabase()
|
||||
m_online_id_ban_table_exists);
|
||||
checkTableExists(ServerConfig::m_player_reports_table,
|
||||
m_player_reports_table_exists);
|
||||
checkTableExists(ServerConfig::m_ip_geolocation_table,
|
||||
m_ip_geolocation_table_exists);
|
||||
#endif
|
||||
} // initDatabase
|
||||
|
||||
@ -780,6 +783,47 @@ void ServerLobby::checkTableExists(const std::string& table, bool& result)
|
||||
table.c_str());
|
||||
}
|
||||
} // checkTableExists
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
std::string ServerLobby::ip2Country(const TransportAddress& addr) const
|
||||
{
|
||||
if (!m_db || !m_ip_geolocation_table_exists || addr.isLAN())
|
||||
return "";
|
||||
|
||||
std::string query = StringUtils::insertValues(
|
||||
"SELECT country_code FROM %s "
|
||||
"WHERE `ip_start` <= %d AND `ip_end` >= %d "
|
||||
"ORDER BY `ip_start` DESC LIMIT 1;",
|
||||
ServerConfig::m_ip_geolocation_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);
|
||||
if (ret == SQLITE_ROW)
|
||||
{
|
||||
const char* country_code = (char*)sqlite3_column_text(stmt, 0);
|
||||
return std::string(country_code);
|
||||
}
|
||||
ret = sqlite3_finalize(stmt);
|
||||
if (ret != SQLITE_OK)
|
||||
{
|
||||
Log::error("ServerLobby",
|
||||
"Error finalize database for query %s: %s",
|
||||
query.c_str(), sqlite3_errmsg(m_db));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log::error("ServerLobby", "Error preparing database for query %s: %s",
|
||||
query.c_str(), sqlite3_errmsg(m_db));
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
} // ip2Country
|
||||
|
||||
#endif
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
@ -2063,6 +2107,8 @@ void ServerLobby::checkIncomingConnectionRequests()
|
||||
users_xml->getNode(i)->get("aes-key", &keys[id].m_aes_key);
|
||||
users_xml->getNode(i)->get("aes-iv", &keys[id].m_aes_iv);
|
||||
users_xml->getNode(i)->get("username", &keys[id].m_name);
|
||||
users_xml->getNode(i)->get("country-code",
|
||||
&keys[id].m_country_code);
|
||||
keys[id].m_tried = false;
|
||||
if (ServerConfig::m_firewalled_server)
|
||||
{
|
||||
@ -2717,7 +2763,7 @@ void ServerLobby::connectionRequested(Event* event)
|
||||
//-----------------------------------------------------------------------------
|
||||
void ServerLobby::handleUnencryptedConnection(std::shared_ptr<STKPeer> peer,
|
||||
BareNetworkString& data, uint32_t online_id,
|
||||
const core::stringw& online_name)
|
||||
const core::stringw& online_name, std::string country_code)
|
||||
{
|
||||
if (data.size() < 2) return;
|
||||
|
||||
@ -2755,6 +2801,11 @@ void ServerLobby::handleUnencryptedConnection(std::shared_ptr<STKPeer> peer,
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_SQLITE3
|
||||
if (country_code.empty())
|
||||
country_code = ip2Country(peer->getAddress());
|
||||
#endif
|
||||
|
||||
unsigned player_count = data.getUInt8();
|
||||
auto red_blue = STKHost::get()->getAllPlayersTeamInfo();
|
||||
for (unsigned i = 0; i < player_count; i++)
|
||||
@ -2770,7 +2821,7 @@ void ServerLobby::handleUnencryptedConnection(std::shared_ptr<STKPeer> peer,
|
||||
(peer, i == 0 && !online_name.empty() ? online_name : name,
|
||||
peer->getHostId(), default_kart_color, i == 0 ? online_id : 0,
|
||||
per_player_difficulty, (uint8_t)i, KART_TEAM_NONE,
|
||||
""/* reserved for country id */);
|
||||
country_code);
|
||||
if (ServerConfig::m_team_choosing)
|
||||
{
|
||||
KartTeam cur_team = KART_TEAM_NONE;
|
||||
@ -2862,12 +2913,13 @@ void ServerLobby::handleUnencryptedConnection(std::shared_ptr<STKPeer> peer,
|
||||
return;
|
||||
std::string query = StringUtils::insertValues(
|
||||
"INSERT INTO %s "
|
||||
"(host_id, ip, port, online_id, username, player_num, version, ping) "
|
||||
"VALUES (%u, %u, %u, %u, ?, %u, ?, %u);",
|
||||
"(host_id, ip, port, online_id, username, player_num, "
|
||||
"country_code, version, ping) "
|
||||
"VALUES (%u, %u, %u, %u, ?, %u, ?, ?, %u);",
|
||||
m_server_stats_table.c_str(), peer->getHostId(),
|
||||
peer->getAddress().getIP(), peer->getAddress().getPort(), online_id,
|
||||
player_count, peer->getAveragePing());
|
||||
easySQLQuery(query, [peer](sqlite3_stmt* stmt)
|
||||
easySQLQuery(query, [peer, country_code](sqlite3_stmt* stmt)
|
||||
{
|
||||
if (sqlite3_bind_text(stmt, 1, StringUtils::wideToUtf8(
|
||||
peer->getPlayerProfiles()[0]->getName()).c_str(),
|
||||
@ -2877,7 +2929,24 @@ void ServerLobby::handleUnencryptedConnection(std::shared_ptr<STKPeer> peer,
|
||||
StringUtils::wideToUtf8(
|
||||
peer->getPlayerProfiles()[0]->getName()).c_str());
|
||||
}
|
||||
if (sqlite3_bind_text(stmt, 2, peer->getUserVersion().c_str(),
|
||||
if (country_code.empty())
|
||||
{
|
||||
if (sqlite3_bind_null(stmt, 2) != SQLITE_OK)
|
||||
{
|
||||
Log::error("easySQLQuery",
|
||||
"Failed to bind NULL for country code.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (sqlite3_bind_text(stmt, 2, country_code.c_str(),
|
||||
-1, SQLITE_TRANSIENT) != SQLITE_OK)
|
||||
{
|
||||
Log::error("easySQLQuery", "Failed to bind country: %s.",
|
||||
country_code.c_str());
|
||||
}
|
||||
}
|
||||
if (sqlite3_bind_text(stmt, 3, peer->getUserVersion().c_str(),
|
||||
-1, SQLITE_TRANSIENT) != SQLITE_OK)
|
||||
{
|
||||
Log::error("easySQLQuery", "Failed to bind %s.",
|
||||
@ -3390,7 +3459,7 @@ void ServerLobby::handlePendingConnection()
|
||||
{
|
||||
if (decryptConnectionRequest(peer, it->second.second,
|
||||
key->second.m_aes_key, key->second.m_aes_iv, online_id,
|
||||
key->second.m_name))
|
||||
key->second.m_name, key->second.m_country_code))
|
||||
{
|
||||
it = m_pending_connection.erase(it);
|
||||
m_keys.erase(online_id);
|
||||
@ -3414,7 +3483,8 @@ void ServerLobby::handlePendingConnection()
|
||||
//-----------------------------------------------------------------------------
|
||||
bool ServerLobby::decryptConnectionRequest(std::shared_ptr<STKPeer> peer,
|
||||
BareNetworkString& data, const std::string& key, const std::string& iv,
|
||||
uint32_t online_id, const core::stringw& online_name)
|
||||
uint32_t online_id, const core::stringw& online_name,
|
||||
const std::string& country_code)
|
||||
{
|
||||
auto crypto = std::unique_ptr<Crypto>(new Crypto(
|
||||
Crypto::decode64(key), Crypto::decode64(iv)));
|
||||
@ -3424,7 +3494,7 @@ bool ServerLobby::decryptConnectionRequest(std::shared_ptr<STKPeer> peer,
|
||||
Log::info("ServerLobby", "%s validated",
|
||||
StringUtils::wideToUtf8(online_name).c_str());
|
||||
handleUnencryptedConnection(peer, data, online_id,
|
||||
online_name);
|
||||
online_name, country_code);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -68,6 +68,7 @@ private:
|
||||
std::string m_aes_key;
|
||||
std::string m_aes_iv;
|
||||
irr::core::stringw m_name;
|
||||
std::string m_country_code;
|
||||
bool m_tried = false;
|
||||
};
|
||||
bool m_player_reports_table_exists;
|
||||
@ -81,6 +82,8 @@ private:
|
||||
|
||||
bool m_online_id_ban_table_exists;
|
||||
|
||||
bool m_ip_geolocation_table_exists;
|
||||
|
||||
uint64_t m_last_cleanup_db_time;
|
||||
|
||||
void cleanupDatabase();
|
||||
@ -89,6 +92,8 @@ private:
|
||||
std::function<void(sqlite3_stmt* stmt)> bind_function = nullptr) const;
|
||||
|
||||
void checkTableExists(const std::string& table, bool& result);
|
||||
|
||||
std::string ip2Country(const TransportAddress& addr) const;
|
||||
#endif
|
||||
void initDatabase();
|
||||
|
||||
@ -276,13 +281,15 @@ private:
|
||||
void handleUnencryptedConnection(std::shared_ptr<STKPeer> peer,
|
||||
BareNetworkString& data,
|
||||
uint32_t online_id,
|
||||
const irr::core::stringw& online_name);
|
||||
const irr::core::stringw& online_name,
|
||||
std::string country_code = "");
|
||||
bool decryptConnectionRequest(std::shared_ptr<STKPeer> peer,
|
||||
BareNetworkString& data,
|
||||
const std::string& key,
|
||||
const std::string& iv,
|
||||
uint32_t online_id,
|
||||
const irr::core::stringw& online_name);
|
||||
const irr::core::stringw& online_name,
|
||||
const std::string& country_code);
|
||||
bool handleAllVotes(PeerVote* winner, uint32_t* winner_peer_id);
|
||||
void getRankingForPlayer(std::shared_ptr<NetworkPlayerProfile> p);
|
||||
void submitRankingsToAddons();
|
||||
|
@ -328,35 +328,48 @@ namespace ServerConfig
|
||||
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 "
|
||||
"Database filename for sqlite to use, it can be shared for all "
|
||||
"servers created 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("ip_ban",
|
||||
"ip-ban-table",
|
||||
"Ip ban list table name, you need to create the table first, see "
|
||||
"NETWORKING.md for details, empty to disable."));
|
||||
"NETWORKING.md for details, empty to disable. "
|
||||
"This table can be shared for all servers if you use the same name."));
|
||||
|
||||
SERVER_CFG_PREFIX StringServerConfigParam m_online_id_ban_table
|
||||
SERVER_CFG_DEFAULT(StringServerConfigParam("online_id_ban",
|
||||
"online-id-ban-table",
|
||||
"Online ID ban list table name, you need to create the table first, "
|
||||
"see NETWORKING.md for details, empty to disable."));
|
||||
"see NETWORKING.md for details, empty to disable. "
|
||||
"This table can be shared for all servers if you use the same name."));
|
||||
|
||||
SERVER_CFG_PREFIX StringServerConfigParam m_player_reports_table
|
||||
SERVER_CFG_DEFAULT(StringServerConfigParam("player_reports",
|
||||
"player-reports-table",
|
||||
"Player reports table name, which will be written when a player "
|
||||
"reports player in the network user dialog, you need to create the "
|
||||
"table first, see NETWORKING.md for details, empty to disable."));
|
||||
"table first, see NETWORKING.md for details, empty to disable. "
|
||||
"This table can be shared for all servers if you use the same name."));
|
||||
|
||||
SERVER_CFG_PREFIX FloatServerConfigParam m_player_reports_expired_days
|
||||
SERVER_CFG_DEFAULT(FloatServerConfigParam(3.0f,
|
||||
"player-reports-expired-days", "Days to keep player reports, "
|
||||
"older than that will be auto cleared, 0 to keep them forever."));
|
||||
|
||||
SERVER_CFG_PREFIX StringServerConfigParam m_ip_geolocation_table
|
||||
SERVER_CFG_DEFAULT(StringServerConfigParam("ip_mapping",
|
||||
"ip-geolocation-table",
|
||||
"IP geolocation table, you only need this table if you want to "
|
||||
"geolocate IP from non-stk-addons connection, as all validated "
|
||||
"players connecting from stk-addons will provide the location info, "
|
||||
"you need to create the table first, see NETWORKING.md for details, "
|
||||
"empty to disable. "
|
||||
"This table can be shared for all servers if you use the same name."));
|
||||
|
||||
// ========================================================================
|
||||
/** Server version, will be advanced if there are protocol changes. */
|
||||
static const uint32_t m_server_version = 6;
|
||||
|
63
tools/generate-ip-mappings.py
Executable file
63
tools/generate-ip-mappings.py
Executable file
@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
# Download from https://dev.maxmind.com/geoip/geoip2/geolite2/
|
||||
# You need GeoLite2-City-Blocks-IPv4.csv and GeoLite2-City-Locations-en.csv from GeoLite2 City
|
||||
# license of the DB is CC-BY-SA 4.0
|
||||
#
|
||||
# This product includes GeoLite2 data created by MaxMind, available from
|
||||
# http://www.maxmind.com
|
||||
|
||||
# usage: generate-ip-mappings.py > ip.csv
|
||||
# in sqlite3 terminal:
|
||||
#
|
||||
# .mode csv
|
||||
# .import `full path to ip.csv` ip_mapping
|
||||
#
|
||||
|
||||
# For query by ip:
|
||||
# SELECT * FROM ip_mapping WHERE `ip_start` <= ip-in-decimal AND `ip_end` >= ip-in-decimal ORDER BY `ip_start` DESC LIMIT 1;
|
||||
import socket
|
||||
import struct
|
||||
import csv
|
||||
import os
|
||||
import sys
|
||||
# import zipfile
|
||||
# import urllib.request
|
||||
|
||||
CSV_WEB_LINK = 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-City-CSV.zip'
|
||||
CSV_FILE = 'GeoLite2-City-Blocks-IPv4.csv'
|
||||
CSV_LOCATION = 'GeoLite2-City-Locations-en.csv'
|
||||
|
||||
if not os.path.exists(CSV_LOCATION):
|
||||
print("File = {} does not exist. Download it from = {} ".format(CSV_FILE, CSV_WEB_LINK))
|
||||
sys.exit(1)
|
||||
|
||||
COUNTRY_DICT = {}
|
||||
with open(CSV_LOCATION, 'r') as csvfile:
|
||||
locationlist = csv.reader(csvfile, delimiter=',', quotechar='"')
|
||||
# Skip header
|
||||
next(locationlist)
|
||||
for row in locationlist:
|
||||
COUNTRY_DICT[row[0]] = row[4]
|
||||
|
||||
if not os.path.exists(CSV_FILE):
|
||||
print("File = {} does not exist. Download it from = {} ".format(CSV_FILE, CSV_WEB_LINK))
|
||||
sys.exit(1)
|
||||
|
||||
with open(CSV_FILE, 'r') as csvfile:
|
||||
iplist = csv.reader(csvfile, delimiter=',', quotechar='"')
|
||||
# Skip header
|
||||
next(iplist)
|
||||
for row in iplist:
|
||||
if row[7] is "" or row[8] is "":
|
||||
continue
|
||||
|
||||
ip, net_bits = row[0].split('/')
|
||||
|
||||
# Convert submask ip to range
|
||||
ip_start = (struct.unpack("!I", socket.inet_aton(ip)))[0]
|
||||
ip_end = ip_start + ((1 << (32 - int(net_bits))) - 1)
|
||||
|
||||
latitude = float(row[7])
|
||||
longitude = float(row[8])
|
||||
country = COUNTRY_DICT.get(row[1], "")
|
||||
print('%d,%d,%f,%f,%s' % (ip_start, ip_end, latitude, longitude, country))
|
Loading…
Reference in New Issue
Block a user