config: support env-based SQL overrides
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled

This commit is contained in:
server
2026-04-14 09:41:31 +02:00
parent b46a2661df
commit 14e084d691
2 changed files with 347 additions and 166 deletions

View File

@@ -47,6 +47,15 @@ extern void WriteVersion();
namespace
{
struct SQLConnectionConfig
{
char host[64];
char database[64];
char user[64];
char password[64];
int port;
};
const char* BoolState(bool value)
{
return value ? "on" : "off";
@@ -57,6 +66,112 @@ const char* EmptyToLabel(const std::string& value, const char* fallback)
return value.empty() ? fallback : value.c_str();
}
bool CopyEnvString(const char* env_name, char* dest, size_t dest_size)
{
const char* value = std::getenv(env_name);
if (!value)
return false;
strlcpy(dest, value, dest_size);
return true;
}
bool CopyEnvInt(const char* env_name, int* dest)
{
const char* value = std::getenv(env_name);
if (!value)
return false;
str_to_number(*dest, value);
return true;
}
bool HasSQLConfig(const SQLConnectionConfig& config)
{
return config.host[0] && config.database[0] && config.user[0] && config.password[0];
}
bool ParseSQLConfig(const char* value, SQLConnectionConfig* config, const char* label)
{
int token_count = sscanf(
value,
" %63s %63s %63s %63s %d ",
config->host,
config->database,
config->user,
config->password,
&config->port);
if (token_count < 4 || !HasSQLConfig(*config))
{
fprintf(stderr, "%s syntax: <host db user password [port]>\n", label);
return false;
}
return true;
}
bool LoadSQLConfig(const char* key, const char* env_prefix, SQLConnectionConfig* config)
{
char line[256 + 1];
bool loaded_from_file = false;
if (CConfig::instance().GetValue(key, line, sizeof(line) - 1))
{
if (!ParseSQLConfig(line, config, key))
return false;
loaded_from_file = true;
}
bool overridden = false;
const std::string prefix = env_prefix;
overridden |= CopyEnvString((prefix + "_HOST").c_str(), config->host, sizeof(config->host));
overridden |= CopyEnvString((prefix + "_DB").c_str(), config->database, sizeof(config->database));
overridden |= CopyEnvString((prefix + "_USER").c_str(), config->user, sizeof(config->user));
overridden |= CopyEnvString((prefix + "_PASSWORD").c_str(), config->password, sizeof(config->password));
overridden |= CopyEnvInt((prefix + "_PORT").c_str(), &config->port);
if (overridden)
sys_log(0, "CONFIG: %s overridden from environment", key);
if (!loaded_from_file && !overridden)
{
sys_err("%s not configured", key);
return false;
}
if (!HasSQLConfig(*config))
{
sys_err("%s incomplete after applying config/environment overrides", key);
return false;
}
return true;
}
bool ConnectDatabase(int slot, const SQLConnectionConfig& config, const char* label)
{
sys_log(0, "connecting to MySQL server (%s)", label);
int retry_count = 5;
do
{
if (CDBManager::instance().Connect(slot, config.host, config.port, config.database, config.user, config.password))
{
sys_log(0, " OK");
return true;
}
sys_log(0, " failed, retrying in 5 seconds");
fprintf(stderr, " failed, retrying in 5 seconds");
sleep(5);
}
while (retry_count--);
return false;
}
void LogStartupSummary(int heart_fps, int player_id_start)
{
sys_log(0,
@@ -274,120 +389,43 @@ int Start()
g_stLocaleNameColumn = szBuf;
}
char szAddr[64], szDB[64], szUser[64], szPassword[64];
int iPort;
char line[256+1];
SQLConnectionConfig player_sql = {};
SQLConnectionConfig account_sql = {};
SQLConnectionConfig common_sql = {};
SQLConnectionConfig hotbackup_sql = {};
if (CConfig::instance().GetValue("SQL_PLAYER", line, 256))
{
sscanf(line, " %s %s %s %s %d ", szAddr, szDB, szUser, szPassword, &iPort);
sys_log(0, "connecting to MySQL server (player)");
int iRetry = 5;
do
{
if (CDBManager::instance().Connect(SQL_PLAYER, szAddr, iPort, szDB, szUser, szPassword))
{
sys_log(0, " OK");
break;
}
sys_log(0, " failed, retrying in 5 seconds");
fprintf(stderr, " failed, retrying in 5 seconds");
sleep(5);
} while (iRetry--);
fprintf(stderr, "Success PLAYER\n");
SetPlayerDBName(szDB);
}
else
{
sys_err("SQL_PLAYER not configured");
if (!LoadSQLConfig("SQL_PLAYER", "METIN2_PLAYER_SQL", &player_sql))
return false;
}
if (CConfig::instance().GetValue("SQL_ACCOUNT", line, 256))
{
sscanf(line, " %s %s %s %s %d ", szAddr, szDB, szUser, szPassword, &iPort);
sys_log(0, "connecting to MySQL server (account)");
int iRetry = 5;
do
{
if (CDBManager::instance().Connect(SQL_ACCOUNT, szAddr, iPort, szDB, szUser, szPassword))
{
sys_log(0, " OK");
break;
}
sys_log(0, " failed, retrying in 5 seconds");
fprintf(stderr, " failed, retrying in 5 seconds");
sleep(5);
} while (iRetry--);
fprintf(stderr, "Success ACCOUNT\n");
}
else
{
sys_err("SQL_ACCOUNT not configured");
if (!ConnectDatabase(SQL_PLAYER, player_sql, "player"))
return false;
}
if (CConfig::instance().GetValue("SQL_COMMON", line, 256))
{
sscanf(line, " %s %s %s %s %d ", szAddr, szDB, szUser, szPassword, &iPort);
sys_log(0, "connecting to MySQL server (common)");
fprintf(stderr, "Success PLAYER\n");
SetPlayerDBName(player_sql.database);
int iRetry = 5;
do
{
if (CDBManager::instance().Connect(SQL_COMMON, szAddr, iPort, szDB, szUser, szPassword))
{
sys_log(0, " OK");
break;
}
sys_log(0, " failed, retrying in 5 seconds");
fprintf(stderr, " failed, retrying in 5 seconds");
sleep(5);
} while (iRetry--);
fprintf(stderr, "Success COMMON\n");
}
else
{
sys_err("SQL_COMMON not configured");
if (!LoadSQLConfig("SQL_ACCOUNT", "METIN2_ACCOUNT_SQL", &account_sql))
return false;
}
if (CConfig::instance().GetValue("SQL_HOTBACKUP", line, 256))
{
sscanf(line, " %s %s %s %s %d ", szAddr, szDB, szUser, szPassword, &iPort);
sys_log(0, "connecting to MySQL server (hotbackup)");
int iRetry = 5;
do
{
if (CDBManager::instance().Connect(SQL_HOTBACKUP, szAddr, iPort, szDB, szUser, szPassword))
{
sys_log(0, " OK");
break;
}
sys_log(0, " failed, retrying in 5 seconds");
fprintf(stderr, " failed, retrying in 5 seconds");
sleep(5);
}
while (iRetry--);
fprintf(stderr, "Success HOTBACKUP\n");
}
else
{
sys_err("SQL_HOTBACKUP not configured");
if (!ConnectDatabase(SQL_ACCOUNT, account_sql, "account"))
return false;
}
fprintf(stderr, "Success ACCOUNT\n");
if (!LoadSQLConfig("SQL_COMMON", "METIN2_COMMON_SQL", &common_sql))
return false;
if (!ConnectDatabase(SQL_COMMON, common_sql, "common"))
return false;
fprintf(stderr, "Success COMMON\n");
if (!LoadSQLConfig("SQL_HOTBACKUP", "METIN2_HOTBACKUP_SQL", &hotbackup_sql))
return false;
if (!ConnectDatabase(SQL_HOTBACKUP, hotbackup_sql, "hotbackup"))
return false;
fprintf(stderr, "Success HOTBACKUP\n");
if (!CNetPoller::instance().Create())
{

View File

@@ -208,6 +208,113 @@ static void FN_apply_adminpage_password_env()
g_stAdminPagePassword = env_password;
}
static bool FN_copy_env_string(const char* env_name, char* dest, size_t dest_size)
{
const char* value = std::getenv(env_name);
if (!value)
return false;
strlcpy(dest, value, dest_size);
return true;
}
static bool FN_copy_env_int(const char* env_name, int* dest)
{
const char* value = std::getenv(env_name);
if (!value)
return false;
str_to_number(*dest, value);
return true;
}
static bool FN_has_sql_config(const char* host, const char* user, const char* password, const char* database)
{
return host[0] && user[0] && password[0] && database[0];
}
static bool FN_parse_game_sql_config(
const char* value,
char* host,
size_t host_size,
char* user,
size_t user_size,
char* password,
size_t password_size,
char* database,
size_t database_size,
int* port,
const char* label)
{
const char* line = two_arguments(value, host, host_size, user, user_size);
line = two_arguments(line, password, password_size, database, database_size);
if (line[0])
{
char port_buf[256];
one_argument(line, port_buf, sizeof(port_buf));
str_to_number(*port, port_buf);
}
if (!FN_has_sql_config(host, user, password, database))
{
fprintf(stderr, "%s syntax: <host user password db [port]>\n", label);
return false;
}
return true;
}
static bool FN_apply_game_sql_env(
const char* env_prefix,
char* host,
size_t host_size,
char* user,
size_t user_size,
char* password,
size_t password_size,
char* database,
size_t database_size,
int* port,
const char* label)
{
bool overridden = false;
std::string prefix = env_prefix;
overridden |= FN_copy_env_string((prefix + "_HOST").c_str(), host, host_size);
overridden |= FN_copy_env_string((prefix + "_USER").c_str(), user, user_size);
overridden |= FN_copy_env_string((prefix + "_PASSWORD").c_str(), password, password_size);
overridden |= FN_copy_env_string((prefix + "_DB").c_str(), database, database_size);
overridden |= FN_copy_env_int((prefix + "_PORT").c_str(), port);
if (overridden)
sys_log(0, "CONFIG: %s overridden from environment", label);
return overridden;
}
static void FN_apply_db_runtime_env()
{
const char* env_addr = std::getenv("METIN2_DB_ADDR");
if (env_addr)
{
strlcpy(db_addr, env_addr, sizeof(db_addr));
for (int n = 0; n < ADDRESS_MAX_LEN; ++n)
{
if (db_addr[n] == ' ')
db_addr[n] = '\0';
}
sys_log(0, "CONFIG: DB_ADDR overridden from environment");
}
int env_port = 0;
if (FN_copy_env_int("METIN2_DB_PORT", &env_port))
{
db_port = static_cast<WORD>(env_port);
sys_log(0, "CONFIG: DB_PORT overridden from environment");
}
}
bool GetIPInfo()
{
@@ -376,8 +483,10 @@ void config_init(const string& st_localeServiceName)
// DB정보만 읽어와 로케일 세팅을 한후 다른 세팅을 적용시켜야한다.
// 이유는 로케일관련된 초기화 루틴이 곳곳에 존재하기 때문.
bool isCommonSQL = false;
bool isAccountSQL = false;
bool isCommonSQL = false;
bool isPlayerSQL = false;
bool isLogSQL = false;
FILE* fp_common;
if (!(fp_common = fopen("conf/game.txt", "r")))
@@ -391,96 +500,69 @@ 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])
if (!FN_parse_game_sql_config(value_string,
db_host[0], sizeof(db_host[0]),
db_user[0], sizeof(db_user[0]),
db_pwd[0], sizeof(db_pwd[0]),
db_db[0], sizeof(db_db[0]),
&mysql_db_port[0],
"ACCOUNT_SQL"))
{
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;
isAccountSQL = true;
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])
if (!FN_parse_game_sql_config(value_string,
db_host[1], sizeof(db_host[1]),
db_user[1], sizeof(db_user[1]),
db_pwd[1], sizeof(db_pwd[1]),
db_db[1], sizeof(db_db[1]),
&mysql_db_port[1],
"PLAYER_SQL"))
{
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;
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])
if (!FN_parse_game_sql_config(value_string,
db_host[2], sizeof(db_host[2]),
db_user[2], sizeof(db_user[2]),
db_pwd[2], sizeof(db_pwd[2]),
db_db[2], sizeof(db_db[2]),
&mysql_db_port[2],
"COMMON_SQL"))
{
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;
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])
if (!FN_parse_game_sql_config(value_string,
log_host, sizeof(log_host),
log_user, sizeof(log_user),
log_pwd, sizeof(log_pwd),
log_db, sizeof(log_db),
&log_port,
"LOG_SQL"))
{
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);
isLogSQL = true;
continue;
}
@@ -763,6 +845,44 @@ void config_init(const string& st_localeServiceName)
}
fclose(fp_common);
const bool account_env_override = FN_apply_game_sql_env(
"METIN2_ACCOUNT_SQL",
db_host[0], sizeof(db_host[0]),
db_user[0], sizeof(db_user[0]),
db_pwd[0], sizeof(db_pwd[0]),
db_db[0], sizeof(db_db[0]),
&mysql_db_port[0],
"ACCOUNT_SQL");
const bool player_env_override = FN_apply_game_sql_env(
"METIN2_PLAYER_SQL",
db_host[1], sizeof(db_host[1]),
db_user[1], sizeof(db_user[1]),
db_pwd[1], sizeof(db_pwd[1]),
db_db[1], sizeof(db_db[1]),
&mysql_db_port[1],
"PLAYER_SQL");
const bool common_env_override = FN_apply_game_sql_env(
"METIN2_COMMON_SQL",
db_host[2], sizeof(db_host[2]),
db_user[2], sizeof(db_user[2]),
db_pwd[2], sizeof(db_pwd[2]),
db_db[2], sizeof(db_db[2]),
&mysql_db_port[2],
"COMMON_SQL");
const bool log_env_override = FN_apply_game_sql_env(
"METIN2_LOG_SQL",
log_host, sizeof(log_host),
log_user, sizeof(log_user),
log_pwd, sizeof(log_pwd),
log_db, sizeof(log_db),
&log_port,
"LOG_SQL");
isAccountSQL = isAccountSQL || account_env_override || FN_has_sql_config(db_host[0], db_user[0], db_pwd[0], db_db[0]);
isPlayerSQL = isPlayerSQL || player_env_override || FN_has_sql_config(db_host[1], db_user[1], db_pwd[1], db_db[1]);
isCommonSQL = isCommonSQL || common_env_override || FN_has_sql_config(db_host[2], db_user[2], db_pwd[2], db_db[2]);
isLogSQL = isLogSQL || log_env_override || FN_has_sql_config(log_host, log_user, log_pwd, log_db);
FILE* fpOnlyForDB;
if (!(fpOnlyForDB = fopen(st_configFileName.c_str(), "r")))
@@ -836,17 +956,41 @@ void config_init(const string& st_localeServiceName)
exit(1);
}
if (!isPlayerSQL)
if (g_bAuthServer && !isAccountSQL)
{
puts("LOAD_ACCOUNT_SQL_INFO_FAILURE:");
puts("");
puts("CONFIG:");
puts("------------------------------------------------");
puts("ACCOUNT_SQL: HOST USER PASSWORD DATABASE [PORT]");
puts("");
exit(1);
}
if (!g_bAuthServer && !isPlayerSQL)
{
puts("LOAD_PLAYER_SQL_INFO_FAILURE:");
puts("");
puts("CONFIG:");
puts("------------------------------------------------");
puts("PLAYER_SQL: HOST USER PASSWORD DATABASE");
puts("PLAYER_SQL: HOST USER PASSWORD DATABASE [PORT]");
puts("");
exit(1);
}
if (!g_bAuthServer && !isLogSQL)
{
puts("LOAD_LOG_SQL_INFO_FAILURE:");
puts("");
puts("CONFIG:");
puts("------------------------------------------------");
puts("LOG_SQL: HOST USER PASSWORD DATABASE [PORT]");
puts("");
exit(1);
}
FN_apply_db_runtime_env();
// Common DB 가 Locale 정보를 가지고 있기 때문에 가장 먼저 접속해야 한다.
AccountDB::instance().Connect(db_host[2], mysql_db_port[2], db_user[2], db_pwd[2], db_db[2]);
@@ -1248,4 +1392,3 @@ bool IsValidFileCRC(DWORD dwCRC)
{
return s_set_dwFileCRC.find(dwCRC) != s_set_dwFileCRC.end();
}