From ccc1a8899d6f40b80fd25ab5e7cec2a1a2dea22d Mon Sep 17 00:00:00 2001 From: server Date: Mon, 13 Apr 2026 21:02:30 +0200 Subject: [PATCH] config: harden admin page secrets --- src/game/config.cpp | 318 ++++++++++++++++++++++++++------------------ src/game/config.h | 2 +- src/game/input.cpp | 142 ++++++++++++-------- 3 files changed, 277 insertions(+), 185 deletions(-) diff --git a/src/game/config.cpp b/src/game/config.cpp index 06eea51..190a3c9 100644 --- a/src/game/config.cpp +++ b/src/game/config.cpp @@ -1,4 +1,8 @@ #include "stdafx.h" +#include +#include +#include +#include #include #ifndef OS_WINDOWS #include @@ -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::max()) + { + fprintf(stderr, "Invalid %s value: %s\n", envName, envValue); + exit(1); + } + + output = static_cast(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 \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 \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 g_setQuestObjectDir; std::vector 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(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 \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(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 \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(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 \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(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 \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(ESqlConfigIndex::Account)]); + ApplySqlEnvOverrides("PLAYER_SQL", dbConfig[static_cast(ESqlConfigIndex::Player)]); + ApplySqlEnvOverrides("COMMON_SQL", dbConfig[static_cast(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(ESqlConfigIndex::Common)]); + ValidateSqlConfigOrExit(g_bAuthServer ? "ACCOUNT_SQL" : "PLAYER_SQL", dbConfig[static_cast(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(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(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(); } - diff --git a/src/game/config.h b/src/game/config.h index ce8d3fe..cf2e8c2 100644 --- a/src/game/config.h +++ b/src/game/config.h @@ -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__ */ - diff --git a/src/game/input.cpp b/src/game/input.cpp index c2ee70f..9b173d2 100644 --- a/src/game/input.cpp +++ b/src/game/input.cpp @@ -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 "))