config: harden admin page secrets

This commit is contained in:
server
2026-04-13 21:02:30 +02:00
parent 8bb5340909
commit ccc1a8899d
3 changed files with 277 additions and 185 deletions

View File

@@ -1,4 +1,8 @@
#include "stdafx.h"
#include <array>
#include <cerrno>
#include <cstdlib>
#include <limits>
#include <sstream>
#ifndef OS_WINDOWS
#include <ifaddrs.h>
@@ -20,6 +24,161 @@
using std::string;
namespace
{
enum class ESqlConfigIndex : size_t
{
Account = 0,
Player = 1,
Common = 2,
Count = 3
};
struct SqlConnectionConfig
{
std::string host;
std::string user;
std::string password;
std::string database;
int port = 0;
bool IsConfigured() const
{
return !host.empty() && !user.empty() && !password.empty() && !database.empty();
}
};
bool ReadEnvString(const char* envName, std::string& output)
{
const char* envValue = std::getenv(envName);
if (!envValue || !*envValue)
return false;
output = envValue;
return true;
}
bool ReadEnvPort(const char* envName, int& output)
{
const char* envValue = std::getenv(envName);
if (!envValue || !*envValue)
return false;
errno = 0;
char* end = nullptr;
unsigned long parsed = std::strtoul(envValue, &end, 10);
if (errno != 0 || end == envValue || *end != '\0' || parsed > std::numeric_limits<uint16_t>::max())
{
fprintf(stderr, "Invalid %s value: %s\n", envName, envValue);
exit(1);
}
output = static_cast<int>(parsed);
return true;
}
void LogEnvOverride(const char* envName)
{
fprintf(stdout, "CONFIG: using %s override\n", envName);
}
void ApplySqlEnvOverrides(const char* prefix, SqlConnectionConfig& config)
{
char envName[64];
snprintf(envName, sizeof(envName), "METIN2_%s_HOST", prefix);
if (ReadEnvString(envName, config.host))
LogEnvOverride(envName);
snprintf(envName, sizeof(envName), "METIN2_%s_USER", prefix);
if (ReadEnvString(envName, config.user))
LogEnvOverride(envName);
snprintf(envName, sizeof(envName), "METIN2_%s_PASSWORD", prefix);
if (ReadEnvString(envName, config.password))
LogEnvOverride(envName);
snprintf(envName, sizeof(envName), "METIN2_%s_DATABASE", prefix);
if (ReadEnvString(envName, config.database))
LogEnvOverride(envName);
snprintf(envName, sizeof(envName), "METIN2_%s_PORT", prefix);
if (ReadEnvPort(envName, config.port))
LogEnvOverride(envName);
}
void ParseSqlConfigOrExit(const char* tokenName, const char* value, SqlConnectionConfig& config)
{
char host[64];
char user[64];
char password[64];
char database[64];
int port = 0;
*host = '\0';
*user = '\0';
*password = '\0';
*database = '\0';
const char* line = two_arguments(value, host, sizeof(host), user, sizeof(user));
line = two_arguments(line, password, sizeof(password), database, sizeof(database));
if (line[0])
{
char portBuffer[32];
one_argument(line, portBuffer, sizeof(portBuffer));
str_to_number(port, portBuffer);
}
if (!*host || !*user || !*password || !*database)
{
fprintf(stderr, "%s syntax: %s <host user password db [port]>\n", tokenName, tokenName);
exit(1);
}
config.host = host;
config.user = user;
config.password = password;
config.database = database;
config.port = port;
}
void ValidateSqlConfigOrExit(const char* label, const SqlConnectionConfig& config)
{
if (config.IsConfigured())
return;
fprintf(stderr, "%s must be configured as <host user password db [port]>\n", label);
exit(1);
}
void ApplyAdminPagePasswordEnvOverride()
{
if (ReadEnvString("METIN2_ADMINPAGE_PASSWORD", g_stAdminPagePassword))
LogEnvOverride("METIN2_ADMINPAGE_PASSWORD");
}
void ValidateAdminPageConfigOrExit()
{
if (!IsAdminPageEnabled())
{
if (!g_stAdminPageIP.empty())
{
fprintf(stderr, "ADMIN_PAGE_PASSWORD must be configured when adminpage_ip is set\n");
exit(1);
}
fprintf(stdout, "ADMIN_PAGE: disabled\n");
return;
}
if (g_stAdminPageIP.empty())
fprintf(stdout, "ADMIN_PAGE: enabled without IP restriction\n");
else
fprintf(stdout, "ADMIN_PAGE: enabled for %zu IP entries\n", g_stAdminPageIP.size());
}
}
BYTE g_bChannel = 0;
WORD mother_port = 50080;
int passes_per_sec = 25;
@@ -75,7 +234,7 @@ string g_stDefaultQuestObjectDir = "./quest/object";
std::set<string> g_setQuestObjectDir;
std::vector<std::string> g_stAdminPageIP;
std::string g_stAdminPagePassword = "SHOWMETHEMONEY";
std::string g_stAdminPagePassword;
string g_stBlockDate = "30000705";
@@ -195,7 +354,12 @@ static void FN_log_adminpage()
++iter;
}
sys_log(1, "ADMIN_PAGE_PASSWORD = %s", g_stAdminPagePassword.c_str());
sys_log(1, "ADMIN_PAGE_PASSWORD = %s", IsAdminPageEnabled() ? "[configured]" : "[disabled]");
}
bool IsAdminPageEnabled()
{
return !g_stAdminPagePassword.empty();
}
@@ -340,35 +504,14 @@ void config_init(const string& st_localeServiceName)
exit(1);
}
char db_host[3][64], db_user[3][64], db_pwd[3][64], db_db[3][64];
// ... 아... db_port는 이미 있는데... 네이밍 어찌해야함...
int mysql_db_port[3];
for (int n = 0; n < 3; ++n)
{
*db_host[n] = '\0';
*db_user[n] = '\0';
*db_pwd[n]= '\0';
*db_db[n]= '\0';
mysql_db_port[n] = 0;
}
char log_host[64], log_user[64], log_pwd[64], log_db[64];
int log_port = 0;
*log_host = '\0';
*log_user = '\0';
*log_pwd = '\0';
*log_db = '\0';
std::array<SqlConnectionConfig, static_cast<size_t>(ESqlConfigIndex::Count)> dbConfig;
SqlConnectionConfig logConfig;
// DB에서 로케일정보를 세팅하기위해서는 다른 세팅값보다 선행되어서
// DB정보만 읽어와 로케일 세팅을 한후 다른 세팅을 적용시켜야한다.
// 이유는 로케일관련된 초기화 루틴이 곳곳에 존재하기 때문.
bool isCommonSQL = false;
bool isPlayerSQL = false;
FILE* fp_common;
if (!(fp_common = fopen("conf/game.txt", "r")))
{
@@ -381,96 +524,25 @@ void config_init(const string& st_localeServiceName)
TOKEN("account_sql")
{
const char* line = two_arguments(value_string, db_host[0], sizeof(db_host[0]), db_user[0], sizeof(db_user[0]));
line = two_arguments(line, db_pwd[0], sizeof(db_pwd[0]), db_db[0], sizeof(db_db[0]));
if (line[0])
{
char buf[256];
one_argument(line, buf, sizeof(buf));
str_to_number(mysql_db_port[0], buf);
}
if (!*db_host[0] || !*db_user[0] || !*db_pwd[0] || !*db_db[0])
{
fprintf(stderr, "PLAYER_SQL syntax: logsql <host user password db>\n");
exit(1);
}
char buf[1024];
snprintf(buf, sizeof(buf), "PLAYER_SQL: %s %s %s %s %d", db_host[0], db_user[0], db_pwd[0], db_db[0], mysql_db_port[0]);
isPlayerSQL = true;
ParseSqlConfigOrExit("account_sql", value_string, dbConfig[static_cast<size_t>(ESqlConfigIndex::Account)]);
continue;
}
TOKEN("player_sql")
{
const char* line = two_arguments(value_string, db_host[1], sizeof(db_host[1]), db_user[1], sizeof(db_user[1]));
line = two_arguments(line, db_pwd[1], sizeof(db_pwd[1]), db_db[1], sizeof(db_db[1]));
if (line[0])
{
char buf[256];
one_argument(line, buf, sizeof(buf));
str_to_number(mysql_db_port[1], buf);
}
if (!*db_host[1] || !*db_user[1] || !*db_pwd[1] || !*db_db[1])
{
fprintf(stderr, "PLAYER_SQL syntax: logsql <host user password db>\n");
exit(1);
}
char buf[1024];
snprintf(buf, sizeof(buf), "PLAYER_SQL: %s %s %s %s %d", db_host[1], db_user[1], db_pwd[1], db_db[1], mysql_db_port[1]);
isPlayerSQL = true;
ParseSqlConfigOrExit("player_sql", value_string, dbConfig[static_cast<size_t>(ESqlConfigIndex::Player)]);
continue;
}
TOKEN("common_sql")
{
const char* line = two_arguments(value_string, db_host[2], sizeof(db_host[2]), db_user[2], sizeof(db_user[2]));
line = two_arguments(line, db_pwd[2], sizeof(db_pwd[2]), db_db[2], sizeof(db_db[2]));
if (line[0])
{
char buf[256];
one_argument(line, buf, sizeof(buf));
str_to_number(mysql_db_port[2], buf);
}
if (!*db_host[2] || !*db_user[2] || !*db_pwd[2] || !*db_db[2])
{
fprintf(stderr, "COMMON_SQL syntax: logsql <host user password db>\n");
exit(1);
}
char buf[1024];
snprintf(buf, sizeof(buf), "COMMON_SQL: %s %s %s %s %d", db_host[2], db_user[2], db_pwd[2], db_db[2], mysql_db_port[2]);
isCommonSQL = true;
ParseSqlConfigOrExit("common_sql", value_string, dbConfig[static_cast<size_t>(ESqlConfigIndex::Common)]);
continue;
}
TOKEN("log_sql")
{
const char* line = two_arguments(value_string, log_host, sizeof(log_host), log_user, sizeof(log_user));
line = two_arguments(line, log_pwd, sizeof(log_pwd), log_db, sizeof(log_db));
if (line[0])
{
char buf[256];
one_argument(line, buf, sizeof(buf));
str_to_number(log_port, buf);
}
if (!*log_host || !*log_user || !*log_pwd || !*log_db)
{
fprintf(stderr, "LOG_SQL syntax: logsql <host user password db>\n");
exit(1);
}
char buf[1024];
snprintf(buf, sizeof(buf), "LOG_SQL: %s %s %s %s %d", log_host, log_user, log_pwd, log_db, log_port);
ParseSqlConfigOrExit("log_sql", value_string, logConfig);
continue;
}
@@ -753,6 +825,12 @@ void config_init(const string& st_localeServiceName)
}
fclose(fp_common);
ApplySqlEnvOverrides("ACCOUNT_SQL", dbConfig[static_cast<size_t>(ESqlConfigIndex::Account)]);
ApplySqlEnvOverrides("PLAYER_SQL", dbConfig[static_cast<size_t>(ESqlConfigIndex::Player)]);
ApplySqlEnvOverrides("COMMON_SQL", dbConfig[static_cast<size_t>(ESqlConfigIndex::Common)]);
ApplySqlEnvOverrides("LOG_SQL", logConfig);
ApplyAdminPagePasswordEnvOverride();
FILE* fpOnlyForDB;
if (!(fpOnlyForDB = fopen(st_configFileName.c_str(), "r")))
@@ -814,31 +892,15 @@ void config_init(const string& st_localeServiceName)
//처리가 끝났으니 파일을 닫자.
fclose(fpOnlyForDB);
// CONFIG_SQL_INFO_ERROR
if (!isCommonSQL)
{
puts("LOAD_COMMON_SQL_INFO_FAILURE:");
puts("");
puts("CONFIG:");
puts("------------------------------------------------");
puts("COMMON_SQL: HOST USER PASSWORD DATABASE");
puts("");
exit(1);
}
if (!isPlayerSQL)
{
puts("LOAD_PLAYER_SQL_INFO_FAILURE:");
puts("");
puts("CONFIG:");
puts("------------------------------------------------");
puts("PLAYER_SQL: HOST USER PASSWORD DATABASE");
puts("");
exit(1);
}
ValidateSqlConfigOrExit("COMMON_SQL", dbConfig[static_cast<size_t>(ESqlConfigIndex::Common)]);
ValidateSqlConfigOrExit(g_bAuthServer ? "ACCOUNT_SQL" : "PLAYER_SQL", dbConfig[static_cast<size_t>(g_bAuthServer ? ESqlConfigIndex::Account : ESqlConfigIndex::Player)]);
if (!g_bAuthServer)
ValidateSqlConfigOrExit("LOG_SQL", logConfig);
ValidateAdminPageConfigOrExit();
// Common DB 가 Locale 정보를 가지고 있기 때문에 가장 먼저 접속해야 한다.
AccountDB::instance().Connect(db_host[2], mysql_db_port[2], db_user[2], db_pwd[2], db_db[2]);
const SqlConnectionConfig& commonDb = dbConfig[static_cast<size_t>(ESqlConfigIndex::Common)];
AccountDB::instance().Connect(commonDb.host.c_str(), commonDb.port, commonDb.user.c_str(), commonDb.password.c_str(), commonDb.database.c_str());
if (false == AccountDB::instance().IsConnected())
{
@@ -884,13 +946,14 @@ void config_init(const string& st_localeServiceName)
AccountDB::instance().SetLocale(g_stLocale);
AccountDB::instance().ConnectAsync(db_host[2], mysql_db_port[2], db_user[2], db_pwd[2], db_db[2], g_stLocale.c_str());
AccountDB::instance().ConnectAsync(commonDb.host.c_str(), commonDb.port, commonDb.user.c_str(), commonDb.password.c_str(), commonDb.database.c_str(), g_stLocale.c_str());
// Player DB 접속
const SqlConnectionConfig& playerDb = dbConfig[static_cast<size_t>(g_bAuthServer ? ESqlConfigIndex::Account : ESqlConfigIndex::Player)];
if (g_bAuthServer)
DBManager::instance().Connect(db_host[0], mysql_db_port[0], db_user[0], db_pwd[0], db_db[0]);
DBManager::instance().Connect(playerDb.host.c_str(), playerDb.port, playerDb.user.c_str(), playerDb.password.c_str(), playerDb.database.c_str());
else
DBManager::instance().Connect(db_host[1], mysql_db_port[1], db_user[1], db_pwd[1], db_db[1]);
DBManager::instance().Connect(playerDb.host.c_str(), playerDb.port, playerDb.user.c_str(), playerDb.password.c_str(), playerDb.database.c_str());
if (!DBManager::instance().IsConnected())
{
@@ -903,7 +966,7 @@ void config_init(const string& st_localeServiceName)
if (false == g_bAuthServer) // 인증 서버가 아닐 경우
{
// Log DB 접속
LogManager::instance().Connect(log_host, log_port, log_user, log_pwd, log_db);
LogManager::instance().Connect(logConfig.host.c_str(), logConfig.port, logConfig.user.c_str(), logConfig.password.c_str(), logConfig.database.c_str());
if (!LogManager::instance().IsConnected())
{
@@ -1238,4 +1301,3 @@ bool IsValidFileCRC(DWORD dwCRC)
return s_set_dwFileCRC.find(dwCRC) != s_set_dwFileCRC.end();
}

View File

@@ -10,6 +10,7 @@ enum
void config_init(const std::string& st_localeServiceName); // default "" is CONFIG
extern char sql_addr[256];
bool IsAdminPageEnabled();
extern WORD mother_port;
extern WORD p2p_port;
@@ -106,4 +107,3 @@ extern int gPlayerMaxLevel;
extern bool g_BlockCharCreation;
#endif /* __INC_METIN_II_GAME_CONFIG_H__ */

View File

@@ -18,19 +18,35 @@
extern time_t get_global_time();
bool IsEmptyAdminPage()
namespace
{
return g_stAdminPageIP.empty();
}
bool IsAdminPage(const char * ip)
{
for (size_t n = 0; n < g_stAdminPageIP.size(); ++n)
bool IsEmptyAdminPage()
{
if (g_stAdminPageIP[n] == ip)
return 1;
}
return 0;
return g_stAdminPageIP.empty();
}
bool IsAdminPage(const char * ip)
{
for (size_t n = 0; n < g_stAdminPageIP.size(); ++n)
{
if (g_stAdminPageIP[n] == ip)
return 1;
}
return 0;
}
bool HasAdminPageIpAccess(const char* ip)
{
if (!IsAdminPageEnabled())
return false;
return IsEmptyAdminPage() || IsAdminPage(ip);
}
bool IsAdminCommandAuthorized(LPDESC d)
{
return d->IsAdminMode();
}
}
CInputProcessor::CInputProcessor() : m_pPacketInfo(NULL), m_iBufferLeft(0)
@@ -245,14 +261,15 @@ int CInputHandshake::HandleText(LPDESC d, const char * c_pData)
stResult = "YES";
}
//else if (!stBuf.compare("SHOWMETHEMONEY"))
else if (stBuf == g_stAdminPagePassword)
else if (IsAdminPageEnabled() && stBuf == g_stAdminPagePassword)
{
const char* hostIp = inet_ntoa(d->GetAddr().sin_addr);
if (!IsEmptyAdminPage())
{
if (!IsAdminPage(inet_ntoa(d->GetAddr().sin_addr)))
if (!IsAdminPage(hostIp))
{
char szTmp[64];
snprintf(szTmp, sizeof(szTmp), "WEBADMIN : Wrong Connector : %s", inet_ntoa(d->GetAddr().sin_addr));
snprintf(szTmp, sizeof(szTmp), "WEBADMIN : Wrong Connector : %s", hostIp);
stResult += szTmp;
}
else
@@ -270,21 +287,14 @@ int CInputHandshake::HandleText(LPDESC d, const char * c_pData)
else if (!stBuf.compare("USER_COUNT"))
{
char szTmp[64];
const char* hostIp = inet_ntoa(d->GetAddr().sin_addr);
if (!IsEmptyAdminPage())
if (!HasAdminPageIpAccess(hostIp))
{
if (!IsAdminPage(inet_ntoa(d->GetAddr().sin_addr)))
{
snprintf(szTmp, sizeof(szTmp), "WEBADMIN : Wrong Connector : %s", inet_ntoa(d->GetAddr().sin_addr));
}
if (IsAdminPageEnabled())
snprintf(szTmp, sizeof(szTmp), "WEBADMIN : Wrong Connector : %s", hostIp);
else
{
int iTotal;
int * paiEmpireUserCount;
int iLocal;
DESC_MANAGER::instance().GetUserCount(iTotal, &paiEmpireUserCount, iLocal);
snprintf(szTmp, sizeof(szTmp), "%d %d %d %d %d", iTotal, paiEmpireUserCount[1], paiEmpireUserCount[2], paiEmpireUserCount[3], iLocal);
}
strlcpy(szTmp, "WEBADMIN : Disabled", sizeof(szTmp));
}
else
{
@@ -298,48 +308,68 @@ int CInputHandshake::HandleText(LPDESC d, const char * c_pData)
}
else if (!stBuf.compare("CHECK_P2P_CONNECTIONS"))
{
std::ostringstream oss(std::ostringstream::out);
oss << "P2P CONNECTION NUMBER : " << P2P_MANAGER::instance().GetDescCount() << "\n";
std::string hostNames;
P2P_MANAGER::Instance().GetP2PHostNames(hostNames);
oss << hostNames;
stResult = oss.str();
TPacketGGCheckAwakeness packet;
packet.header = GG::CHECK_AWAKENESS;
packet.length = sizeof(packet);
if (!IsAdminCommandAuthorized(d))
stResult = "UNKNOWN";
else
{
std::ostringstream oss(std::ostringstream::out);
P2P_MANAGER::instance().Send(&packet, sizeof(packet));
oss << "P2P CONNECTION NUMBER : " << P2P_MANAGER::instance().GetDescCount() << "\n";
std::string hostNames;
P2P_MANAGER::Instance().GetP2PHostNames(hostNames);
oss << hostNames;
stResult = oss.str();
TPacketGGCheckAwakeness packet;
packet.header = GG::CHECK_AWAKENESS;
packet.length = sizeof(packet);
P2P_MANAGER::instance().Send(&packet, sizeof(packet));
}
}
else if (!stBuf.compare("PACKET_INFO"))
{
m_pMainPacketInfo->Log("packet_info.txt");
stResult = "OK";
if (!IsAdminCommandAuthorized(d))
stResult = "UNKNOWN";
else
{
m_pMainPacketInfo->Log("packet_info.txt");
stResult = "OK";
}
}
else if (!stBuf.compare("PROFILE"))
{
CProfiler::instance().Log("profile.txt");
stResult = "OK";
if (!IsAdminCommandAuthorized(d))
stResult = "UNKNOWN";
else
{
CProfiler::instance().Log("profile.txt");
stResult = "OK";
}
}
//gift notify delete command
else if (!stBuf.compare(0,15,"DELETE_AWARDID "))
{
char szTmp[64];
std::string msg = stBuf.substr(15,26); // item_award의 id범위?
TPacketDeleteAwardID p;
p.dwID = (DWORD)(atoi(msg.c_str()));
snprintf(szTmp,sizeof(szTmp),"Sent to DB cache to delete ItemAward, id: %d",p.dwID);
//sys_log(0,"%d",p.dwID);
// strlcpy(p.login, msg.c_str(), sizeof(p.login));
db_clientdesc->DBPacket(GD::DELETE_AWARDID, 0, &p, sizeof(p));
stResult += szTmp;
if (!IsAdminCommandAuthorized(d))
stResult = "UNKNOWN";
else
{
char szTmp[64];
std::string msg = stBuf.substr(15,26); // item_award id범위?
TPacketDeleteAwardID p;
p.dwID = (DWORD)(atoi(msg.c_str()));
snprintf(szTmp,sizeof(szTmp),"Sent to DB cache to delete ItemAward, id: %d",p.dwID);
//sys_log(0,"%d",p.dwID);
// strlcpy(p.login, msg.c_str(), sizeof(p.login));
db_clientdesc->DBPacket(GD::DELETE_AWARDID, 0, &p, sizeof(p));
stResult += szTmp;
}
}
else
{
stResult = "UNKNOWN";
if (d->IsAdminMode())
else
{
stResult = "UNKNOWN";
if (d->IsAdminMode())
{
// 어드민 명령들
if (!stBuf.compare(0, 7, "NOTICE "))