diff --git a/NETWORKING.md b/NETWORKING.md
index 126cf1c20..c4536aebc 100644
--- a/NETWORKING.md
+++ b/NETWORKING.md
@@ -150,21 +150,24 @@ The current server configuration xml looks like this:
-
+
-
+
-
+
-
+
+
+
+
```
@@ -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).
diff --git a/src/network/protocols/server_lobby.cpp b/src/network/protocols/server_lobby.cpp
index b6549757a..43c453a53 100644
--- a/src/network/protocols/server_lobby.cpp
+++ b/src/network/protocols/server_lobby.cpp
@@ -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 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 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 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 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 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 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(new Crypto(
Crypto::decode64(key), Crypto::decode64(iv)));
@@ -3424,7 +3494,7 @@ bool ServerLobby::decryptConnectionRequest(std::shared_ptr 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;
diff --git a/src/network/protocols/server_lobby.hpp b/src/network/protocols/server_lobby.hpp
index 2207701fd..d564bcb86 100644
--- a/src/network/protocols/server_lobby.hpp
+++ b/src/network/protocols/server_lobby.hpp
@@ -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 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 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 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 p);
void submitRankingsToAddons();
diff --git a/src/network/server_config.hpp b/src/network/server_config.hpp
index df4b613b2..c8a2a5bd1 100644
--- a/src/network/server_config.hpp
+++ b/src/network/server_config.hpp
@@ -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;
diff --git a/tools/generate-ip-mappings.py b/tools/generate-ip-mappings.py
new file mode 100755
index 000000000..b977ac9c1
--- /dev/null
+++ b/tools/generate-ip-mappings.py
@@ -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))