18 Commits

Author SHA1 Message Date
ad6ac77199 test: discord webhook delivery retry 2026-04-14 09:51:47 +02:00
bfeb31c454 test: discord webhook delivery 2026-04-14 09:49:31 +02:00
server
14e084d691 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
2026-04-14 09:41:31 +02:00
server
b46a2661df docs: add repo instructions
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 09:09:32 +02:00
server
e99b8b0520 tests: support env-backed smoke passwords
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 08:49:30 +02:00
server
b2b037fb94 tests: add end-to-end login smoke utility
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 08:42:05 +02:00
server
e2c43240ec net: add Linux epoll fdwatch backend
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 07:57:02 +02:00
server
e638816026 runtime: replace virtual checkpoint timer with watchdog
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 07:40:07 +02:00
server
cecc822777 runtime: encapsulate checkpoint progress state
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 07:12:27 +02:00
server
3272537376 net: speed up select fdwatch lookups
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 07:08:23 +02:00
server
4f1e7b0e9c net: encapsulate fdwatch backend metadata
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 07:03:44 +02:00
server
abed660256 runtime: fix clean shutdown exit codes
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 06:26:37 +02:00
server
7daf51f2da runtime: improve startup observability
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 06:04:12 +02:00
server
3e3f0918e9 tests: add socket auth smoke flow 2026-04-14 06:04:08 +02:00
server
25ec562ab0 config: harden admin page password handling
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 01:07:31 +02:00
server
c8146c0340 ci: add Linux smoke coverage 2026-04-14 01:07:27 +02:00
server
e8a33f84f4 net: fix Linux select fdwatch readiness
Some checks failed
build / Main build job (push) Has been cancelled
2026-04-14 00:42:45 +02:00
server
1ca64ce829 net: flush queued packets immediately 2026-04-14 00:31:50 +02:00
21 changed files with 2717 additions and 720 deletions

View File

@@ -4,11 +4,43 @@ on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
bsd:
linux:
runs-on: ubuntu-latest
name: Main build job
strategy:
fail-fast: false
matrix:
include:
- name: release
build_type: Release
enable_asan: OFF
- name: asan
build_type: RelWithDebInfo
enable_asan: ON
name: Linux ${{ matrix.name }}
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake ninja-build pkg-config libsodium-dev libmariadb-dev
- name: Configure
run: |
cmake -S . -B build-${{ matrix.name }} -G Ninja \
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \
-DENABLE_ASAN=${{ matrix.enable_asan }}
- name: Build
run: cmake --build build-${{ matrix.name }} --parallel
- name: Smoke tests
run: ctest --test-dir build-${{ matrix.name }} --output-on-failure
freebsd:
runs-on: ubuntu-latest
name: FreeBSD build
steps:
- uses: actions/checkout@v4
- name: FreeBSD job
@@ -24,6 +56,7 @@ jobs:
cd build
cmake ..
gmake all -j6
ctest --output-on-failure
- name: Collect outputs
run: |
mkdir _output
@@ -32,4 +65,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: output_bsd
path: _output
path: _output

67
AGENTS.md Normal file
View File

@@ -0,0 +1,67 @@
# AGENTS.md
## Repository Role
This repository contains the C++ server source and build system for:
- `game`
- `db`
- `qc`
- smoke/login test binaries
The current production VPS runs a Debian deployment built from `main`.
## Working Rules
- Prefer small, reviewable changes directly on `main`.
- Keep Debian runtime stability ahead of broad refactors.
- Do not commit `build/`, temporary captures, or host-local debug output.
- Do not commit secrets or production-only credentials.
## Build And Test
Typical local build:
```bash
cmake -S . -B build -G Ninja
cmake --build build
ctest --test-dir build --output-on-failure
```
Important test targets:
- `metin_smoke_tests`
- `metin_login_smoke`
## High-Risk Areas
Changes in these areas require extra care:
- auth/login flow
- packet headers and packet structs
- `SecureCipher`
- `fdwatch` and socket output path
- DB login/account flow
When changing login, auth, packet, or network behavior:
- build and run smoke tests
- keep protocol compatibility with `m2dev-client-src`
- keep client asset behavior in sync with `m2dev-client`
## Production Verification
The current VPS has a root-only end-to-end login check:
```bash
/usr/local/sbin/metin-login-healthcheck
```
Use it after risky auth/network/runtime changes.
## Cross-Repo Boundaries
- server code changes belong here
- runtime/config/systemd/docs changes belong in `m2dev-server`
- client protocol implementation changes belong in `m2dev-client-src`
- client asset/login screen/server list changes belong in `m2dev-client`

11
CLAUDE.md Normal file
View File

@@ -0,0 +1,11 @@
# CLAUDE.md
Follow [AGENTS.md](AGENTS.md) as the canonical repo guide.
Short version:
- this repo owns `game`, `db`, `qc`, and smoke/login test binaries
- prefer small changes on `main`
- auth/network/packet edits must be tested carefully
- keep protocol changes aligned with `m2dev-client-src` and `m2dev-client`
- use `/usr/local/sbin/metin-login-healthcheck` on the VPS after risky login/runtime changes

View File

@@ -4,6 +4,7 @@ project(m2dev-server-src)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_C_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# ASan support
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
@@ -98,3 +99,6 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/vendor/mariadb-connector-c-3.4.5
add_subdirectory(src)
add_subdirectory(vendor)
enable_testing()
add_subdirectory(tests)

View File

@@ -45,6 +45,158 @@ extern const char * _malloc_options;
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";
}
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,
"[STARTUP] locale=%s table_postfix=%s player_db=%s player_id_start=%d heart_fps=%d test_server=%s log=%s hotbackup=%s",
EmptyToLabel(g_stLocale, "<unset>"),
EmptyToLabel(g_stTablePostfix, "<none>"),
EmptyToLabel(g_stPlayerDBName, "<unset>"),
player_id_start,
heart_fps,
BoolState(g_test_server),
BoolState(g_log != 0),
BoolState(g_bHotBackup)
);
sys_log(0,
"[STARTUP] cache_flush player=%d item=%d pricelist=%d logout=%d locale_name_column=%s",
g_iPlayerCacheFlushSeconds,
g_iItemCacheFlushSeconds,
g_iItemPriceListTableCacheFlushSeconds,
g_iLogoutSeconds,
EmptyToLabel(g_stLocaleNameColumn, "<unset>")
);
}
}
void emergency_sig(int sig)
{
if (sig == SIGSEGV)
@@ -109,7 +261,7 @@ int main()
}
log_destroy();
return 1;
return 0;
}
void emptybeat(LPHEART heart, int pulse)
@@ -237,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())
{
@@ -374,6 +449,8 @@ int Start()
return false;
}
LogStartupSummary(heart_beat, iIDStart);
#ifndef OS_WINDOWS
signal(SIGUSR1, emergency_sig);
#endif
@@ -409,4 +486,3 @@ const char * GetPlayerDBName()
{
return g_stPlayerDBName.c_str();
}

View File

@@ -37,6 +37,10 @@ bool CNetPoller::Create()
return false;
}
sys_log(0, "[STARTUP] fdwatch backend=%s descriptor_limit=%d",
fdwatch_backend_name(fdwatch_get_backend(m_fdWatcher)),
fdwatch_get_descriptor_limit(m_fdWatcher));
return true;
}

View File

@@ -75,7 +75,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 +195,124 @@ static void FN_log_adminpage()
++iter;
}
sys_log(1, "ADMIN_PAGE_PASSWORD = %s", g_stAdminPagePassword.c_str());
sys_log(1, "ADMIN_PAGE_PASSWORD = %s", g_stAdminPagePassword.empty() ? "[disabled]" : "[configured]");
}
static void FN_apply_adminpage_password_env()
{
const char* env_password = std::getenv("METIN2_ADMINPAGE_PASSWORD");
if (!env_password)
return;
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");
}
}
@@ -366,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")))
@@ -381,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;
}
@@ -753,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")))
@@ -826,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]);
@@ -1128,6 +1282,7 @@ void config_init(const string& st_localeServiceName)
LoadStateUserCount();
CWarMapManager::instance().LoadWarMapInfo(NULL);
FN_apply_adminpage_password_env();
FN_log_adminpage();
}
@@ -1237,5 +1392,3 @@ bool IsValidFileCRC(DWORD dwCRC)
{
return s_set_dwFileCRC.find(dwCRC) != s_set_dwFileCRC.end();
}

View File

@@ -277,22 +277,49 @@ int DESC::ProcessOutput()
if (bytes_to_write == 0)
return 0;
int result = socket_write(m_sock, (const char *) m_outputBuffer.ReadPtr(), bytes_to_write);
int bytes_written = send(m_sock, (const char *) m_outputBuffer.ReadPtr(), bytes_to_write, 0);
if (result == 0)
if (bytes_written > 0)
{
max_bytes_written = MAX(bytes_to_write, max_bytes_written);
max_bytes_written = MAX(bytes_written, max_bytes_written);
total_bytes_written += bytes_to_write;
current_bytes_written += bytes_to_write;
total_bytes_written += bytes_written;
current_bytes_written += bytes_written;
m_outputBuffer.Discard(bytes_to_write);
m_outputBuffer.Discard(bytes_written);
if (m_outputBuffer.ReadableBytes() != 0)
fdwatch_add_fd(m_lpFdw, m_sock, this, FDW_WRITE, true);
return 0;
}
return (result);
if (bytes_written == 0)
return -1;
#ifdef EINTR
if (errno == EINTR)
return 0;
#endif
#ifdef EAGAIN
if (errno == EAGAIN)
{
fdwatch_add_fd(m_lpFdw, m_sock, this, FDW_WRITE, true);
return 0;
}
#endif
#ifdef EWOULDBLOCK
if (errno == EWOULDBLOCK)
{
fdwatch_add_fd(m_lpFdw, m_sock, this, FDW_WRITE, true);
return 0;
}
#endif
sys_err("ProcessOutput: send failed: %s (fd %d)", strerror(errno), m_sock);
return -1;
}
void DESC::BufferedPacket(const void * c_pvData, int iSize)
@@ -370,6 +397,9 @@ void DESC::Packet(const void * c_pvData, int iSize)
if (m_iPhase != PHASE_CLOSE)
fdwatch_add_fd(m_lpFdw, m_sock, this, FDW_WRITE, true);
if (m_iPhase != PHASE_CLOSE)
ProcessOutput();
}
void DESC::LargePacket(const void * c_pvData, int iSize)
@@ -779,6 +809,9 @@ void DESC::RawPacket(const void * c_pvData, int iSize)
if (m_iPhase != PHASE_CLOSE)
fdwatch_add_fd(m_lpFdw, m_sock, this, FDW_WRITE, true);
if (m_iPhase != PHASE_CLOSE)
ProcessOutput();
}
void DESC::ChatPacket(BYTE type, const char * format, ...)

View File

@@ -245,7 +245,7 @@ int CInputHandshake::HandleText(LPDESC d, const char * c_pData)
stResult = "YES";
}
//else if (!stBuf.compare("SHOWMETHEMONEY"))
else if (stBuf == g_stAdminPagePassword)
else if (!g_stAdminPagePassword.empty() && stBuf == g_stAdminPagePassword)
{
if (!IsEmptyAdminPage())
{

View File

@@ -167,6 +167,64 @@ void ShutdownOnFatalError()
namespace
{
const char* BoolState(bool value)
{
return value ? "on" : "off";
}
const char* EmptyToLabel(const char* value, const char* fallback)
{
return (value && *value) ? value : fallback;
}
const char* EmptyToLabel(const std::string& value, const char* fallback)
{
return value.empty() ? fallback : value.c_str();
}
void LogStartupSummary()
{
#ifdef ENABLE_PROXY_IP
const char* proxy_ip = EmptyToLabel(g_stProxyIP, "<disabled>");
#else
const char* proxy_ip = "<disabled>";
#endif
sys_log(0,
"[STARTUP] mode=%s channel=%u bind=%s:%u p2p=%s:%u db=%s:%u locale=%s quest_dir=%s",
g_bAuthServer ? "auth" : "game",
g_bChannel,
EmptyToLabel(g_szPublicIP, "0.0.0.0"), mother_port,
EmptyToLabel(g_szPublicIP, "0.0.0.0"), p2p_port,
EmptyToLabel(db_addr, "<unset>"), db_port,
EmptyToLabel(g_stLocale, "<unset>"),
EmptyToLabel(g_stQuestDir, "<unset>")
);
sys_log(0,
"[STARTUP] users limit=%d full=%d busy=%d local=%u p2p_peers=%d regen=%s admin_page=%s proxy=%s",
g_iUserLimit,
g_iFullUserCount,
g_iBusyUserCount,
DESC_MANAGER::instance().GetLocalUserCount(),
P2P_MANAGER::instance().GetDescCount(),
BoolState(!g_bNoRegen),
BoolState(!g_stAdminPagePassword.empty()),
proxy_ip
);
sys_log(0,
"[STARTUP] features client_version_check=%s guild_mark_server=%s mark_min_level=%u empire_whisper=%s auth_master=%s:%u test_server=%d",
BoolState(g_bCheckClientVersion),
BoolState(guild_mark_server),
guild_mark_min_level,
BoolState(g_bEmpireWhisper),
EmptyToLabel(g_stAuthMasterIP, "<disabled>"),
g_wAuthMasterPort,
test_server
);
}
struct SendDisconnectFunc
{
void operator () (LPDESC d)
@@ -344,14 +402,14 @@ int main(int argc, char **argv)
if (!start(argc, argv)) {
CleanUpForEarlyExit();
return 0;
return 1;
}
quest::CQuestManager quest_manager;
if (!quest_manager.Initialize()) {
CleanUpForEarlyExit();
return 0;
return 1;
}
MessengerManager::instance().Initialize();
@@ -428,7 +486,7 @@ int main(int argc, char **argv)
#endif
log_destroy();
return 1;
return 0;
}
void usage()
@@ -544,6 +602,16 @@ int start(int argc, char **argv)
main_fdw = fdwatch_new(4096);
if (!main_fdw)
{
sys_err("fdwatch_new failed");
return 0;
}
sys_log(0, "[STARTUP] fdwatch backend=%s descriptor_limit=%d",
fdwatch_backend_name(fdwatch_get_backend(main_fdw)),
fdwatch_get_descriptor_limit(main_fdw));
if ((tcp_socket = socket_tcp_bind(g_szPublicIP, mother_port)) == INVALID_SOCKET)
{
perror("socket_tcp_bind: tcp_socket");
@@ -594,6 +662,8 @@ int start(int argc, char **argv)
LoadSpamDB();
}
LogStartupSummary();
signal_timer_enable(30);
return 1;
}
@@ -778,4 +848,3 @@ int io_loop(LPFDWATCH fdw)
return 1;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,85 +1,36 @@
#pragma once
#ifndef __USE_SELECT__
typedef struct fdwatch FDWATCH;
typedef struct fdwatch * LPFDWATCH;
typedef struct fdwatch FDWATCH;
typedef struct fdwatch * LPFDWATCH;
enum EFdwatch
{
FDW_NONE = 0,
FDW_READ = 1,
FDW_WRITE = 2,
FDW_WRITE_ONESHOT = 4,
FDW_EOF = 8,
};
enum EFdwatch
{
FDW_NONE = 0,
FDW_READ = 1,
FDW_WRITE = 2,
FDW_WRITE_ONESHOT = 4,
FDW_EOF = 8,
};
typedef struct kevent KEVENT;
typedef struct kevent * LPKEVENT;
typedef int KQUEUE;
struct fdwatch
{
KQUEUE kq;
int nfiles;
LPKEVENT kqevents;
int nkqevents;
LPKEVENT kqrevents;
int * fd_event_idx;
void ** fd_data;
int * fd_rw;
};
#else
typedef struct fdwatch FDWATCH;
typedef struct fdwatch * LPFDWATCH;
enum EFdwatch
{
FDW_NONE = 0,
FDW_READ = 1,
FDW_WRITE = 2,
FDW_WRITE_ONESHOT = 4,
FDW_EOF = 8,
};
struct fdwatch
{
fd_set rfd_set;
fd_set wfd_set;
socket_t* select_fds;
int* select_rfdidx;
int nselect_fds;
fd_set working_rfd_set;
fd_set working_wfd_set;
int nfiles;
void** fd_data;
int* fd_rw;
};
#endif // WIN32
LPFDWATCH fdwatch_new(int nfiles);
void fdwatch_clear_fd(LPFDWATCH fdw, socket_t fd);
void fdwatch_delete(LPFDWATCH fdw);
int fdwatch_check_fd(LPFDWATCH fdw, socket_t fd);
int fdwatch_check_event(LPFDWATCH fdw, socket_t fd, unsigned int event_idx);
void fdwatch_clear_event(LPFDWATCH fdw, socket_t fd, unsigned int event_idx);
void fdwatch_add_fd(LPFDWATCH fdw, socket_t fd, void* client_data, int rw, int oneshot);
int fdwatch(LPFDWATCH fdw, struct timeval *timeout);
void * fdwatch_get_client_data(LPFDWATCH fdw, unsigned int event_idx);
void fdwatch_del_fd(LPFDWATCH fdw, socket_t fd);
int fdwatch_get_buffer_size(LPFDWATCH fdw, socket_t fd);
int fdwatch_get_ident(LPFDWATCH fdw, unsigned int event_idx);
enum EFdwatchBackend
{
FDWATCH_BACKEND_KQUEUE = 0,
FDWATCH_BACKEND_SELECT = 1,
FDWATCH_BACKEND_EPOLL = 2,
};
LPFDWATCH fdwatch_new(int nfiles);
void fdwatch_clear_fd(LPFDWATCH fdw, socket_t fd);
void fdwatch_delete(LPFDWATCH fdw);
int fdwatch_check_fd(LPFDWATCH fdw, socket_t fd);
int fdwatch_check_event(LPFDWATCH fdw, socket_t fd, unsigned int event_idx);
void fdwatch_clear_event(LPFDWATCH fdw, socket_t fd, unsigned int event_idx);
void fdwatch_add_fd(LPFDWATCH fdw, socket_t fd, void* client_data, int rw, int oneshot);
int fdwatch(LPFDWATCH fdw, struct timeval *timeout);
void * fdwatch_get_client_data(LPFDWATCH fdw, unsigned int event_idx);
void fdwatch_del_fd(LPFDWATCH fdw, socket_t fd);
int fdwatch_get_buffer_size(LPFDWATCH fdw, socket_t fd);
int fdwatch_get_ident(LPFDWATCH fdw, unsigned int event_idx);
EFdwatchBackend fdwatch_get_backend(LPFDWATCH fdw);
const char * fdwatch_backend_name(EFdwatchBackend backend);
int fdwatch_get_descriptor_limit(LPFDWATCH fdw);

View File

@@ -6,7 +6,6 @@
LPHEART thecore_heart = NULL;
std::atomic<int> shutdowned = FALSE;
std::atomic<int> tics = 0;
unsigned int thecore_profiler[NUM_PF];
static int pid_init(void)
@@ -89,6 +88,7 @@ int thecore_idle(void)
void thecore_destroy(void)
{
signal_destroy();
pid_deinit();
log_destroy();
}
@@ -115,5 +115,5 @@ int thecore_is_shutdowned(void)
void thecore_tick(void)
{
++tics;
signal_mark_progress();
}

View File

@@ -2,7 +2,6 @@
#include <atomic>
extern std::atomic<int> tics;
extern std::atomic<int> shutdowned;
#include "heart.h"
@@ -27,5 +26,4 @@ float thecore_time(void);
float thecore_pulse_per_second(void);
int thecore_is_shutdowned(void);
void thecore_tick(void); // tics 증가
void thecore_tick(void); // checkpoint progress 증가

View File

@@ -1,9 +1,125 @@
#include "stdafx.h"
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <thread>
namespace
{
std::atomic<uint64_t> s_checkpoint_progress { 0 };
#ifndef OS_WINDOWS
std::mutex s_checkpoint_mutex;
std::condition_variable s_checkpoint_cv;
std::thread s_checkpoint_thread;
bool s_checkpoint_shutdown = false;
bool s_checkpoint_enabled = false;
int s_checkpoint_timeout_seconds = 0;
uint64_t s_checkpoint_generation = 0;
void checkpoint_watchdog_loop()
{
uint64_t last_progress = s_checkpoint_progress.load(std::memory_order_relaxed);
auto last_change = std::chrono::steady_clock::now();
uint64_t observed_generation = 0;
std::unique_lock<std::mutex> lock(s_checkpoint_mutex);
while (!s_checkpoint_shutdown)
{
if (!s_checkpoint_enabled || s_checkpoint_timeout_seconds <= 0)
{
s_checkpoint_cv.wait(lock, []()
{
return s_checkpoint_shutdown || (s_checkpoint_enabled && s_checkpoint_timeout_seconds > 0);
});
last_progress = s_checkpoint_progress.load(std::memory_order_relaxed);
last_change = std::chrono::steady_clock::now();
observed_generation = s_checkpoint_generation;
continue;
}
const int timeout_seconds = s_checkpoint_timeout_seconds;
const uint64_t generation = s_checkpoint_generation;
const auto poll_interval = std::chrono::seconds(1);
const bool reconfigured = s_checkpoint_cv.wait_for(lock, poll_interval, [generation]()
{
return s_checkpoint_shutdown || s_checkpoint_generation != generation;
});
if (s_checkpoint_shutdown)
break;
if (reconfigured || observed_generation != s_checkpoint_generation)
{
last_progress = s_checkpoint_progress.load(std::memory_order_relaxed);
last_change = std::chrono::steady_clock::now();
observed_generation = s_checkpoint_generation;
continue;
}
const uint64_t current_progress = s_checkpoint_progress.load(std::memory_order_relaxed);
const auto now = std::chrono::steady_clock::now();
if (current_progress != last_progress)
{
last_progress = current_progress;
last_change = now;
continue;
}
if (now - last_change >= std::chrono::seconds(timeout_seconds))
{
lock.unlock();
sys_err("CHECKPOINT shutdown: no progress observed for %d seconds.", timeout_seconds);
abort();
}
}
}
void checkpoint_ensure_started()
{
std::lock_guard<std::mutex> lock(s_checkpoint_mutex);
if (s_checkpoint_thread.joinable())
return;
s_checkpoint_shutdown = false;
s_checkpoint_enabled = false;
s_checkpoint_timeout_seconds = 0;
s_checkpoint_generation = 0;
s_checkpoint_thread = std::thread(checkpoint_watchdog_loop);
}
#endif
const char* signal_checkpoint_backend_name_impl(ECheckpointBackend backend)
{
switch (backend)
{
case CHECKPOINT_BACKEND_NONE:
return "none";
case CHECKPOINT_BACKEND_VIRTUAL_TIMER:
return "virtual-timer";
case CHECKPOINT_BACKEND_WATCHDOG_THREAD:
return "watchdog-thread";
default:
return "unknown";
}
}
}
#ifdef OS_WINDOWS
void signal_setup() {}
void signal_destroy() {}
void signal_timer_disable() {}
void signal_timer_enable(int timeout_seconds) {}
void signal_mark_progress() {}
ECheckpointBackend signal_checkpoint_backend() { return CHECKPOINT_BACKEND_NONE; }
const char* signal_checkpoint_backend_name(ECheckpointBackend backend) { return signal_checkpoint_backend_name_impl(backend); }
#else
#define RETSIGTYPE void
@@ -14,18 +130,6 @@ RETSIGTYPE reap(int sig)
}
RETSIGTYPE checkpointing(int sig)
{
if (!tics.load())
{
sys_err("CHECKPOINT shutdown: tics did not updated.");
abort();
}
else
tics.store(0);
}
RETSIGTYPE hupsig(int sig)
{
shutdowned = TRUE;
@@ -39,46 +143,84 @@ RETSIGTYPE usrsig(int sig)
void signal_timer_disable(void)
{
struct itimerval itime;
struct timeval interval;
checkpoint_ensure_started();
interval.tv_sec = 0;
interval.tv_usec = 0;
{
std::lock_guard<std::mutex> lock(s_checkpoint_mutex);
s_checkpoint_enabled = false;
s_checkpoint_timeout_seconds = 0;
++s_checkpoint_generation;
}
itime.it_interval = interval;
itime.it_value = interval;
setitimer(ITIMER_VIRTUAL, &itime, NULL);
s_checkpoint_cv.notify_all();
}
void signal_timer_enable(int sec)
{
struct itimerval itime;
struct timeval interval;
checkpoint_ensure_started();
interval.tv_sec = sec;
interval.tv_usec = 0;
{
std::lock_guard<std::mutex> lock(s_checkpoint_mutex);
s_checkpoint_enabled = sec > 0;
s_checkpoint_timeout_seconds = sec;
++s_checkpoint_generation;
}
itime.it_interval = interval;
itime.it_value = interval;
setitimer(ITIMER_VIRTUAL, &itime, NULL);
s_checkpoint_cv.notify_all();
}
void signal_setup(void)
{
signal_timer_enable(30);
checkpoint_ensure_started();
signal_timer_enable(30);
signal(SIGVTALRM, checkpointing);
/* just to be on the safe side: */
signal(SIGHUP, hupsig);
signal(SIGCHLD, reap);
signal(SIGINT, hupsig);
signal(SIGTERM, hupsig);
signal(SIGPIPE, SIG_IGN);
signal(SIGALRM, SIG_IGN);
signal(SIGUSR1, usrsig);
/* just to be on the safe side: */
signal(SIGHUP, hupsig);
signal(SIGCHLD, reap);
signal(SIGINT, hupsig);
signal(SIGTERM, hupsig);
signal(SIGPIPE, SIG_IGN);
signal(SIGALRM, SIG_IGN);
signal(SIGUSR1, usrsig);
sys_log(0, "[STARTUP] checkpoint backend=%s", signal_checkpoint_backend_name(signal_checkpoint_backend()));
}
void signal_destroy()
{
std::thread checkpoint_thread;
{
std::lock_guard<std::mutex> lock(s_checkpoint_mutex);
if (!s_checkpoint_thread.joinable())
return;
s_checkpoint_shutdown = true;
s_checkpoint_enabled = false;
s_checkpoint_timeout_seconds = 0;
++s_checkpoint_generation;
checkpoint_thread = std::move(s_checkpoint_thread);
}
s_checkpoint_cv.notify_all();
checkpoint_thread.join();
s_checkpoint_progress.store(0, std::memory_order_relaxed);
}
void signal_mark_progress()
{
s_checkpoint_progress.fetch_add(1, std::memory_order_relaxed);
}
ECheckpointBackend signal_checkpoint_backend()
{
return CHECKPOINT_BACKEND_WATCHDOG_THREAD;
}
const char* signal_checkpoint_backend_name(ECheckpointBackend backend)
{
return signal_checkpoint_backend_name_impl(backend);
}
#endif

View File

@@ -1,4 +1,16 @@
#pragma once
enum ECheckpointBackend
{
CHECKPOINT_BACKEND_NONE = 0,
CHECKPOINT_BACKEND_VIRTUAL_TIMER = 1,
CHECKPOINT_BACKEND_WATCHDOG_THREAD = 2,
};
void signal_setup();
void signal_destroy();
void signal_timer_disable();
void signal_timer_enable(int timeout_seconds);
void signal_timer_enable(int timeout_seconds);
void signal_mark_progress();
ECheckpointBackend signal_checkpoint_backend();
const char* signal_checkpoint_backend_name(ECheckpointBackend backend);

View File

@@ -10,6 +10,31 @@ void socket_timeout(socket_t s, long sec, long usec);
void socket_reuse(socket_t s);
void socket_keepalive(socket_t s);
namespace
{
bool socket_accept_should_retry()
{
#ifdef OS_WINDOWS
const int wsa_error = WSAGetLastError();
return wsa_error == WSAEWOULDBLOCK || wsa_error == WSAEINTR;
#else
#ifdef EINTR
if (errno == EINTR)
return true;
#endif
#ifdef EAGAIN
if (errno == EAGAIN)
return true;
#endif
#ifdef EWOULDBLOCK
if (errno == EWOULDBLOCK)
return true;
#endif
return false;
#endif
}
}
int socket_read(socket_t desc, char* read_point, size_t space_left)
{
int ret;
@@ -206,6 +231,8 @@ socket_t socket_accept(socket_t s, struct sockaddr_in *peer)
if ((desc = accept(s, (struct sockaddr *) peer, &i)) == -1)
{
if (socket_accept_should_retry())
return -1;
sys_err("accept: %s (fd %d)", strerror(errno), s);
return -1;
}

View File

@@ -102,6 +102,8 @@ inline double rint(double x)
#ifdef OS_FREEBSD
#include <sys/event.h>
#elif defined(__linux__)
#include <sys/epoll.h>
#endif
#endif

32
tests/CMakeLists.txt Normal file
View File

@@ -0,0 +1,32 @@
if(WIN32)
return()
endif()
add_executable(metin_smoke_tests
smoke_auth.cpp
${CMAKE_SOURCE_DIR}/src/game/SecureCipher.cpp
)
add_executable(metin_login_smoke
login_smoke.cpp
${CMAKE_SOURCE_DIR}/src/game/SecureCipher.cpp
)
target_link_libraries(metin_smoke_tests
libthecore
sodium
pthread
)
target_link_libraries(metin_login_smoke
libthecore
sodium
pthread
)
if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
target_link_libraries(metin_smoke_tests md)
target_link_libraries(metin_login_smoke md)
endif()
add_test(NAME metin_smoke_tests COMMAND metin_smoke_tests)

422
tests/login_smoke.cpp Normal file
View File

@@ -0,0 +1,422 @@
#include <array>
#include <cerrno>
#include <cstdlib>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <poll.h>
#include <sys/socket.h>
#include <unistd.h>
#include "common/packet_headers.h"
#include "game/SecureCipher.h"
namespace
{
constexpr size_t LOGIN_MAX_LEN_LOCAL = 30;
constexpr size_t PASSWD_MAX_LEN_LOCAL = 16;
constexpr size_t ACCOUNT_STATUS_MAX_LEN_LOCAL = 8;
#pragma pack(push, 1)
struct PacketGCPhase
{
uint16_t header;
uint16_t length;
uint8_t phase;
};
struct PacketGCKeyChallenge
{
uint16_t header;
uint16_t length;
uint8_t server_pk[SecureCipher::PK_SIZE];
uint8_t challenge[SecureCipher::CHALLENGE_SIZE];
uint32_t server_time;
};
struct PacketCGKeyResponse
{
uint16_t header;
uint16_t length;
uint8_t client_pk[SecureCipher::PK_SIZE];
uint8_t challenge_response[SecureCipher::HMAC_SIZE];
};
struct PacketGCKeyComplete
{
uint16_t header;
uint16_t length;
uint8_t encrypted_token[SecureCipher::SESSION_TOKEN_SIZE + SecureCipher::TAG_SIZE];
uint8_t nonce[SecureCipher::NONCE_SIZE];
};
struct PacketCGLogin3
{
uint16_t header;
uint16_t length;
char login[LOGIN_MAX_LEN_LOCAL + 1];
char passwd[PASSWD_MAX_LEN_LOCAL + 1];
};
struct PacketCGLogin2
{
uint16_t header;
uint16_t length;
char login[LOGIN_MAX_LEN_LOCAL + 1];
uint32_t login_key;
};
struct PacketGCAuthSuccess
{
uint16_t header;
uint16_t length;
uint32_t login_key;
uint8_t result;
};
struct PacketGCLoginFailure
{
uint16_t header;
uint16_t length;
char status[ACCOUNT_STATUS_MAX_LEN_LOCAL + 1];
};
struct PacketGCEmpire
{
uint16_t header;
uint16_t length;
uint8_t empire;
};
#pragma pack(pop)
void Expect(bool condition, std::string_view message)
{
if (!condition)
throw std::runtime_error(std::string(message));
}
void WriteExact(int fd, const void* data, size_t length, std::string_view context)
{
const uint8_t* cursor = static_cast<const uint8_t*>(data);
size_t remaining = length;
while (remaining > 0)
{
const ssize_t written = send(fd, cursor, remaining, 0);
if (written <= 0)
throw std::runtime_error(std::string(context) + ": send failed: " + std::strerror(errno));
cursor += written;
remaining -= static_cast<size_t>(written);
}
}
void ReadExact(int fd, void* data, size_t length, std::string_view context)
{
uint8_t* cursor = static_cast<uint8_t*>(data);
size_t remaining = length;
while (remaining > 0)
{
const ssize_t bytes_read = recv(fd, cursor, remaining, 0);
if (bytes_read <= 0)
throw std::runtime_error(std::string(context) + ": recv failed: " + std::strerror(errno));
cursor += bytes_read;
remaining -= static_cast<size_t>(bytes_read);
}
}
bool WaitForReadable(int fd, int timeout_ms)
{
pollfd descriptor {};
descriptor.fd = fd;
descriptor.events = POLLIN;
const int rc = poll(&descriptor, 1, timeout_ms);
if (rc < 0)
throw std::runtime_error("poll failed: " + std::string(std::strerror(errno)));
return rc > 0 && (descriptor.revents & POLLIN);
}
int ConnectTcp(const std::string& host, uint16_t port)
{
const int fd = socket(AF_INET, SOCK_STREAM, 0);
Expect(fd >= 0, "socket() failed");
timeval timeout {};
timeout.tv_sec = 5;
timeout.tv_usec = 0;
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
sockaddr_in addr {};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
Expect(inet_pton(AF_INET, host.c_str(), &addr.sin_addr) == 1, "inet_pton() failed");
Expect(connect(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == 0,
"connect() failed: " + std::string(std::strerror(errno)));
return fd;
}
class EncryptedClient
{
public:
EncryptedClient(const std::string& host, uint16_t port)
: m_fd(ConnectTcp(host, port))
{
Expect(m_cipher.Initialize(), "SecureCipher init failed");
}
~EncryptedClient()
{
if (m_fd >= 0)
close(m_fd);
}
void Handshake()
{
PacketGCPhase phase {};
ReadExact(m_fd, &phase, sizeof(phase), "read initial phase");
Expect(phase.header == GC::PHASE, "unexpected initial phase header");
PacketGCKeyChallenge challenge {};
ReadExact(m_fd, &challenge, sizeof(challenge), "read key challenge");
Expect(challenge.header == GC::KEY_CHALLENGE, "unexpected key challenge header");
Expect(m_cipher.ComputeClientKeys(challenge.server_pk), "client key derivation failed");
PacketCGKeyResponse response {};
response.header = CG::KEY_RESPONSE;
response.length = sizeof(response);
m_cipher.GetPublicKey(response.client_pk);
m_cipher.ComputeChallengeResponse(challenge.challenge, response.challenge_response);
WriteExact(m_fd, &response, sizeof(response), "write key response");
PacketGCKeyComplete complete {};
ReadExact(m_fd, &complete, sizeof(complete), "read key complete");
Expect(complete.header == GC::KEY_COMPLETE, "unexpected key complete header");
std::array<uint8_t, SecureCipher::SESSION_TOKEN_SIZE> session_token {};
Expect(m_cipher.DecryptToken(complete.encrypted_token, sizeof(complete.encrypted_token), complete.nonce, session_token.data()),
"decrypt token failed");
m_cipher.SetSessionToken(session_token.data());
m_cipher.SetActivated(true);
}
template <typename TPacket>
void SendEncryptedPacket(const TPacket& packet)
{
TPacket encrypted = packet;
m_cipher.EncryptInPlace(&encrypted, sizeof(encrypted));
WriteExact(m_fd, &encrypted, sizeof(encrypted), "write encrypted packet");
}
bool WaitForFrame(std::vector<uint8_t>& frame, int timeout_ms)
{
while (true)
{
if (TryPopFrame(frame))
return true;
if (!WaitForReadable(m_fd, timeout_ms))
return false;
ReadEncryptedChunk();
}
}
private:
bool TryPopFrame(std::vector<uint8_t>& frame)
{
if (m_stream.size() < PACKET_HEADER_SIZE)
return false;
const auto header = *reinterpret_cast<const uint16_t*>(m_stream.data());
const auto length = *reinterpret_cast<const uint16_t*>(m_stream.data() + sizeof(uint16_t));
Expect(length >= PACKET_HEADER_SIZE && length <= 8192,
"invalid encrypted packet length header=" + std::to_string(header) +
" length=" + std::to_string(length));
if (m_stream.size() < length)
return false;
frame.assign(m_stream.begin(), m_stream.begin() + length);
m_stream.erase(m_stream.begin(), m_stream.begin() + length);
return true;
}
void ReadEncryptedChunk()
{
std::array<uint8_t, 4096> buffer {};
const ssize_t bytes_read = recv(m_fd, buffer.data(), buffer.size(), 0);
if (bytes_read <= 0)
throw std::runtime_error("recv failed: " + std::string(std::strerror(errno)));
const size_t old_size = m_stream.size();
m_stream.resize(old_size + static_cast<size_t>(bytes_read));
std::memcpy(m_stream.data() + old_size, buffer.data(), static_cast<size_t>(bytes_read));
m_cipher.DecryptInPlace(m_stream.data() + old_size, static_cast<size_t>(bytes_read));
}
int m_fd = -1;
SecureCipher m_cipher;
std::vector<uint8_t> m_stream;
};
uint16_t FrameHeader(const std::vector<uint8_t>& frame)
{
return *reinterpret_cast<const uint16_t*>(frame.data());
}
uint16_t FrameLength(const std::vector<uint8_t>& frame)
{
return *reinterpret_cast<const uint16_t*>(frame.data() + sizeof(uint16_t));
}
uint32_t Authenticate(const std::string& host, uint16_t auth_port, const std::string& login, const std::string& password)
{
EncryptedClient auth_client(host, auth_port);
auth_client.Handshake();
PacketCGLogin3 login_packet {};
login_packet.header = CG::LOGIN3;
login_packet.length = sizeof(login_packet);
std::strncpy(login_packet.login, login.c_str(), sizeof(login_packet.login) - 1);
std::strncpy(login_packet.passwd, password.c_str(), sizeof(login_packet.passwd) - 1);
auth_client.SendEncryptedPacket(login_packet);
std::vector<uint8_t> frame;
for (int i = 0; i < 8; ++i)
{
Expect(auth_client.WaitForFrame(frame, 5000), "timed out waiting for auth response");
const auto header = FrameHeader(frame);
if (header == GC::PHASE)
continue;
if (header == GC::LOGIN_FAILURE)
{
Expect(frame.size() == sizeof(PacketGCLoginFailure), "unexpected login failure size");
const auto* failure = reinterpret_cast<const PacketGCLoginFailure*>(frame.data());
throw std::runtime_error(std::string("auth login failed: ") + failure->status);
}
if (header == GC::AUTH_SUCCESS)
{
Expect(frame.size() == sizeof(PacketGCAuthSuccess), "unexpected auth success size");
const auto* success = reinterpret_cast<const PacketGCAuthSuccess*>(frame.data());
Expect(success->result != 0, "auth result returned failure");
std::cout << "auth_success login_key=" << success->login_key << "\n";
return success->login_key;
}
}
throw std::runtime_error("did not receive AUTH_SUCCESS");
}
void LoginToChannel(const std::string& host, uint16_t channel_port, const std::string& login, uint32_t login_key)
{
EncryptedClient channel_client(host, channel_port);
channel_client.Handshake();
PacketCGLogin2 login_packet {};
login_packet.header = CG::LOGIN2;
login_packet.length = sizeof(login_packet);
login_packet.login_key = login_key;
std::strncpy(login_packet.login, login.c_str(), sizeof(login_packet.login) - 1);
channel_client.SendEncryptedPacket(login_packet);
bool saw_empire = false;
bool saw_login_success = false;
uint8_t empire = 0;
std::vector<uint8_t> frame;
for (int i = 0; i < 16; ++i)
{
Expect(channel_client.WaitForFrame(frame, 5000), "timed out waiting for channel response");
const auto header = FrameHeader(frame);
const auto length = FrameLength(frame);
if (header == GC::PHASE)
continue;
if (header == GC::LOGIN_FAILURE)
{
Expect(frame.size() == sizeof(PacketGCLoginFailure), "unexpected channel login failure size");
const auto* failure = reinterpret_cast<const PacketGCLoginFailure*>(frame.data());
throw std::runtime_error(std::string("channel login failed: ") + failure->status);
}
if (header == GC::EMPIRE)
{
Expect(frame.size() == sizeof(PacketGCEmpire), "unexpected empire packet size");
const auto* empire_packet = reinterpret_cast<const PacketGCEmpire*>(frame.data());
saw_empire = true;
empire = empire_packet->empire;
std::cout << "channel_empire empire=" << static_cast<int>(empire) << "\n";
continue;
}
if (header == GC::LOGIN_SUCCESS4)
{
saw_login_success = true;
std::cout << "channel_login_success length=" << length << "\n";
break;
}
}
Expect(saw_empire, "did not receive EMPIRE");
Expect(saw_login_success, "did not receive LOGIN_SUCCESS4");
}
}
int main(int argc, char** argv)
{
try
{
Expect(argc == 6,
"usage: metin_login_smoke <host> <auth_port> <channel_port> <login> <password>\n"
" or: metin_login_smoke <host> <auth_port> <channel_port> <login> --password-env=ENV_NAME");
const std::string host = argv[1];
const uint16_t auth_port = static_cast<uint16_t>(std::stoi(argv[2]));
const uint16_t channel_port = static_cast<uint16_t>(std::stoi(argv[3]));
const std::string login = argv[4];
std::string password;
const std::string password_arg = argv[5];
const std::string prefix = "--password-env=";
if (password_arg.rfind(prefix, 0) == 0)
{
const char* value = std::getenv(password_arg.substr(prefix.size()).c_str());
Expect(value && *value, "password environment variable is empty");
password = value;
}
else
{
password = password_arg;
}
Expect(login.size() <= LOGIN_MAX_LEN_LOCAL, "login too long");
Expect(password.size() <= PASSWD_MAX_LEN_LOCAL, "password too long");
const uint32_t login_key = Authenticate(host, auth_port, login, password);
LoginToChannel(host, channel_port, login, login_key);
std::cout << "full_login_success\n";
return 0;
}
catch (const std::exception& e)
{
std::cerr << "metin_login_smoke failed: " << e.what() << "\n";
return 1;
}
}

390
tests/smoke_auth.cpp Normal file
View File

@@ -0,0 +1,390 @@
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <exception>
#include <iostream>
#include <stdexcept>
#include "common/packet_headers.h"
#include "game/stdafx.h"
#include "game/SecureCipher.h"
#include "libthecore/fdwatch.h"
#include "libthecore/signal.h"
namespace
{
#pragma pack(push, 1)
struct WirePhasePacket
{
uint16_t header;
uint16_t length;
uint8_t phase;
};
struct WireKeyChallengePacket
{
uint16_t header;
uint16_t length;
uint8_t server_pk[SecureCipher::PK_SIZE];
uint8_t challenge[SecureCipher::CHALLENGE_SIZE];
uint32_t server_time;
};
struct WireKeyResponsePacket
{
uint16_t header;
uint16_t length;
uint8_t client_pk[SecureCipher::PK_SIZE];
uint8_t challenge_response[SecureCipher::HMAC_SIZE];
};
struct WireKeyCompletePacket
{
uint16_t header;
uint16_t length;
uint8_t encrypted_token[SecureCipher::SESSION_TOKEN_SIZE + SecureCipher::TAG_SIZE];
uint8_t nonce[SecureCipher::NONCE_SIZE];
};
#pragma pack(pop)
void Expect(bool condition, const char* message)
{
if (!condition)
throw std::runtime_error(message);
}
void WriteExact(int fd, const void* data, size_t length, const char* message)
{
const uint8_t* cursor = static_cast<const uint8_t*>(data);
size_t remaining = length;
while (remaining > 0)
{
const ssize_t written = write(fd, cursor, remaining);
Expect(written > 0, message);
cursor += written;
remaining -= static_cast<size_t>(written);
}
}
void ReadExact(int fd, void* data, size_t length, const char* message)
{
uint8_t* cursor = static_cast<uint8_t*>(data);
size_t remaining = length;
while (remaining > 0)
{
const ssize_t bytes_read = read(fd, cursor, remaining);
Expect(bytes_read > 0, message);
cursor += bytes_read;
remaining -= static_cast<size_t>(bytes_read);
}
}
void TestPacketLayouts()
{
Expect(sizeof(WirePhasePacket) == 5, "Unexpected phase wire size");
Expect(sizeof(WireKeyChallengePacket) == 72, "Unexpected key challenge wire size");
Expect(sizeof(WireKeyResponsePacket) == 68, "Unexpected key response wire size");
Expect(sizeof(WireKeyCompletePacket) == 76, "Unexpected key complete wire size");
}
void TestSecureCipherRoundTrip()
{
SecureCipher server;
SecureCipher client;
Expect(server.Initialize(), "Server SecureCipher init failed");
Expect(client.Initialize(), "Client SecureCipher init failed");
std::array<uint8_t, SecureCipher::PK_SIZE> server_pk {};
std::array<uint8_t, SecureCipher::PK_SIZE> client_pk {};
server.GetPublicKey(server_pk.data());
client.GetPublicKey(client_pk.data());
Expect(client.ComputeClientKeys(server_pk.data()), "Client session key derivation failed");
Expect(server.ComputeServerKeys(client_pk.data()), "Server session key derivation failed");
std::array<uint8_t, SecureCipher::CHALLENGE_SIZE> challenge {};
std::array<uint8_t, SecureCipher::HMAC_SIZE> response {};
server.GenerateChallenge(challenge.data());
client.ComputeChallengeResponse(challenge.data(), response.data());
Expect(server.VerifyChallengeResponse(challenge.data(), response.data()), "Challenge verification failed");
std::array<uint8_t, SecureCipher::SESSION_TOKEN_SIZE> token {};
for (size_t i = 0; i < token.size(); ++i)
token[i] = static_cast<uint8_t>(i);
std::array<uint8_t, SecureCipher::SESSION_TOKEN_SIZE + SecureCipher::TAG_SIZE> ciphertext {};
std::array<uint8_t, SecureCipher::NONCE_SIZE> nonce {};
std::array<uint8_t, SecureCipher::SESSION_TOKEN_SIZE> plaintext {};
Expect(server.EncryptToken(token.data(), token.size(), ciphertext.data(), nonce.data()), "Token encryption failed");
Expect(client.DecryptToken(ciphertext.data(), ciphertext.size(), nonce.data(), plaintext.data()), "Token decryption failed");
Expect(std::memcmp(token.data(), plaintext.data(), token.size()) == 0, "Token round-trip mismatch");
server.SetActivated(true);
client.SetActivated(true);
std::array<uint8_t, 96> payload {};
for (size_t i = 0; i < payload.size(); ++i)
payload[i] = static_cast<uint8_t>(0xA0 + (i % 31));
auto encrypted = payload;
server.EncryptInPlace(encrypted.data(), encrypted.size());
client.DecryptInPlace(encrypted.data(), encrypted.size());
Expect(encrypted == payload, "Server to client stream cipher round-trip failed");
auto reverse = payload;
client.EncryptInPlace(reverse.data(), reverse.size());
server.DecryptInPlace(reverse.data(), reverse.size());
Expect(reverse == payload, "Client to server stream cipher round-trip failed");
}
void TestSocketAuthWireFlow()
{
SecureCipher server;
SecureCipher client;
Expect(server.Initialize(), "Server auth cipher init failed");
Expect(client.Initialize(), "Client auth cipher init failed");
int sockets[2] = { -1, -1 };
Expect(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == 0, "socketpair for auth flow failed");
WirePhasePacket phase_packet {};
phase_packet.header = GC::PHASE;
phase_packet.length = sizeof(phase_packet);
phase_packet.phase = PHASE_HANDSHAKE;
WireKeyChallengePacket key_challenge {};
key_challenge.header = GC::KEY_CHALLENGE;
key_challenge.length = sizeof(key_challenge);
server.GetPublicKey(key_challenge.server_pk);
server.GenerateChallenge(key_challenge.challenge);
key_challenge.server_time = 0x12345678;
WriteExact(sockets[0], &phase_packet, sizeof(phase_packet), "Failed to write phase packet");
WriteExact(sockets[0], &key_challenge, sizeof(key_challenge), "Failed to write key challenge");
WirePhasePacket client_phase {};
WireKeyChallengePacket client_challenge {};
ReadExact(sockets[1], &client_phase, sizeof(client_phase), "Failed to read phase packet");
ReadExact(sockets[1], &client_challenge, sizeof(client_challenge), "Failed to read key challenge");
Expect(client_phase.header == GC::PHASE, "Unexpected phase header");
Expect(client_phase.length == sizeof(client_phase), "Unexpected phase packet length");
Expect(client_phase.phase == PHASE_HANDSHAKE, "Unexpected phase value");
Expect(client_challenge.header == GC::KEY_CHALLENGE, "Unexpected key challenge header");
Expect(client_challenge.length == sizeof(client_challenge), "Unexpected key challenge length");
Expect(std::memcmp(client_challenge.server_pk, key_challenge.server_pk, sizeof(key_challenge.server_pk)) == 0,
"Server public key changed on the wire");
Expect(std::memcmp(client_challenge.challenge, key_challenge.challenge, sizeof(key_challenge.challenge)) == 0,
"Challenge bytes changed on the wire");
Expect(client.ComputeClientKeys(client_challenge.server_pk), "Client auth key derivation failed");
WireKeyResponsePacket key_response {};
key_response.header = CG::KEY_RESPONSE;
key_response.length = sizeof(key_response);
client.GetPublicKey(key_response.client_pk);
client.ComputeChallengeResponse(client_challenge.challenge, key_response.challenge_response);
WriteExact(sockets[1], &key_response, sizeof(key_response), "Failed to write key response");
WireKeyResponsePacket server_response {};
ReadExact(sockets[0], &server_response, sizeof(server_response), "Failed to read key response");
Expect(server_response.header == CG::KEY_RESPONSE, "Unexpected key response header");
Expect(server_response.length == sizeof(server_response), "Unexpected key response length");
Expect(server.ComputeServerKeys(server_response.client_pk), "Server auth key derivation failed");
Expect(server.VerifyChallengeResponse(key_challenge.challenge, server_response.challenge_response),
"Server rejected challenge response");
std::array<uint8_t, SecureCipher::SESSION_TOKEN_SIZE> session_token {};
for (size_t i = 0; i < session_token.size(); ++i)
session_token[i] = static_cast<uint8_t>(0x30 + i);
server.SetSessionToken(session_token.data());
WireKeyCompletePacket key_complete {};
key_complete.header = GC::KEY_COMPLETE;
key_complete.length = sizeof(key_complete);
Expect(server.EncryptToken(session_token.data(), session_token.size(), key_complete.encrypted_token, key_complete.nonce),
"Failed to encrypt key complete token");
WriteExact(sockets[0], &key_complete, sizeof(key_complete), "Failed to write key complete");
WireKeyCompletePacket client_complete {};
ReadExact(sockets[1], &client_complete, sizeof(client_complete), "Failed to read key complete");
Expect(client_complete.header == GC::KEY_COMPLETE, "Unexpected key complete header");
Expect(client_complete.length == sizeof(client_complete), "Unexpected key complete length");
std::array<uint8_t, SecureCipher::SESSION_TOKEN_SIZE> decrypted_token {};
Expect(client.DecryptToken(client_complete.encrypted_token, sizeof(client_complete.encrypted_token),
client_complete.nonce, decrypted_token.data()),
"Failed to decrypt key complete token");
Expect(decrypted_token == session_token, "Session token changed on the wire");
server.SetActivated(true);
client.SetSessionToken(decrypted_token.data());
client.SetActivated(true);
std::array<uint8_t, 32> payload {};
for (size_t i = 0; i < payload.size(); ++i)
payload[i] = static_cast<uint8_t>(0x41 + i);
auto encrypted_payload = payload;
server.EncryptInPlace(encrypted_payload.data(), encrypted_payload.size());
WriteExact(sockets[0], encrypted_payload.data(), encrypted_payload.size(), "Failed to write encrypted payload");
std::array<uint8_t, 32> received_payload {};
ReadExact(sockets[1], received_payload.data(), received_payload.size(), "Failed to read encrypted payload");
client.DecryptInPlace(received_payload.data(), received_payload.size());
Expect(received_payload == payload, "Encrypted payload round-trip mismatch");
close(sockets[0]);
close(sockets[1]);
}
void TestFdwatchReadAndOneshotWrite()
{
int sockets[2] = { -1, -1 };
Expect(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == 0, "socketpair failed");
LPFDWATCH fdw = fdwatch_new(64);
Expect(fdw != nullptr, "fdwatch_new failed");
int marker = 42;
fdwatch_add_fd(fdw, sockets[1], &marker, FDW_READ, false);
const uint8_t byte = 0x7F;
Expect(write(sockets[0], &byte, sizeof(byte)) == sizeof(byte), "socketpair write failed");
timeval timeout {};
timeout.tv_sec = 0;
timeout.tv_usec = 200000;
int num_events = fdwatch(fdw, &timeout);
Expect(num_events == 1, "Expected one read event");
Expect(fdwatch_get_client_data(fdw, 0) == &marker, "Unexpected client data");
Expect(fdwatch_check_event(fdw, sockets[1], 0) == FDW_READ, "Expected FDW_READ event");
uint8_t read_back = 0;
Expect(read(sockets[1], &read_back, sizeof(read_back)) == sizeof(read_back), "socketpair read failed");
Expect(read_back == byte, "Read payload mismatch");
fdwatch_add_fd(fdw, sockets[1], &marker, FDW_WRITE, true);
num_events = fdwatch(fdw, &timeout);
Expect(num_events >= 1, "Expected at least one write event");
Expect(fdwatch_check_event(fdw, sockets[1], 0) == FDW_WRITE, "Expected FDW_WRITE event");
timeout.tv_sec = 0;
timeout.tv_usec = 0;
num_events = fdwatch(fdw, &timeout);
Expect(num_events == 0, "FDW_WRITE oneshot was not cleared");
fdwatch_del_fd(fdw, sockets[1]);
fdwatch_delete(fdw);
close(sockets[0]);
close(sockets[1]);
}
void TestFdwatchBackendMetadata()
{
LPFDWATCH fdw = fdwatch_new(4096);
Expect(fdw != nullptr, "fdwatch_new for backend metadata failed");
#ifdef __linux__
Expect(fdwatch_get_backend(fdw) == FDWATCH_BACKEND_EPOLL, "Expected epoll backend");
Expect(std::strcmp(fdwatch_backend_name(fdwatch_get_backend(fdw)), "epoll") == 0, "Unexpected epoll backend name");
Expect(fdwatch_get_descriptor_limit(fdw) == 4096, "Unexpected epoll descriptor limit");
#elif defined(__USE_SELECT__)
Expect(fdwatch_get_backend(fdw) == FDWATCH_BACKEND_SELECT, "Expected select backend");
Expect(std::strcmp(fdwatch_backend_name(fdwatch_get_backend(fdw)), "select") == 0, "Unexpected select backend name");
Expect(fdwatch_get_descriptor_limit(fdw) == std::min(4096, static_cast<int>(FD_SETSIZE)), "Unexpected select descriptor limit");
#else
Expect(fdwatch_get_backend(fdw) == FDWATCH_BACKEND_KQUEUE, "Expected kqueue backend");
Expect(std::strcmp(fdwatch_backend_name(fdwatch_get_backend(fdw)), "kqueue") == 0, "Unexpected kqueue backend name");
Expect(fdwatch_get_descriptor_limit(fdw) == 4096, "Unexpected kqueue descriptor limit");
#endif
fdwatch_delete(fdw);
}
void TestCheckpointBackendMetadata()
{
#ifdef OS_WINDOWS
Expect(signal_checkpoint_backend() == CHECKPOINT_BACKEND_NONE, "Expected no checkpoint backend on Windows");
Expect(std::strcmp(signal_checkpoint_backend_name(signal_checkpoint_backend()), "none") == 0,
"Unexpected checkpoint backend name on Windows");
#else
Expect(signal_checkpoint_backend() == CHECKPOINT_BACKEND_WATCHDOG_THREAD, "Expected watchdog thread checkpoint backend");
Expect(std::strcmp(signal_checkpoint_backend_name(signal_checkpoint_backend()), "watchdog-thread") == 0,
"Unexpected checkpoint backend name");
#endif
}
void TestFdwatchSlotReuseAfterDelete()
{
int sockets_a[2] = { -1, -1 };
int sockets_b[2] = { -1, -1 };
Expect(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets_a) == 0, "socketpair A failed");
Expect(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets_b) == 0, "socketpair B failed");
LPFDWATCH fdw = fdwatch_new(64);
Expect(fdw != nullptr, "fdwatch_new for slot reuse failed");
int marker_a = 11;
int marker_b = 22;
fdwatch_add_fd(fdw, sockets_a[1], &marker_a, FDW_READ, false);
fdwatch_add_fd(fdw, sockets_b[1], &marker_b, FDW_READ, false);
fdwatch_del_fd(fdw, sockets_a[1]);
const uint8_t byte = 0x51;
Expect(write(sockets_b[0], &byte, sizeof(byte)) == sizeof(byte), "socketpair B write failed");
timeval timeout {};
timeout.tv_sec = 0;
timeout.tv_usec = 200000;
const int num_events = fdwatch(fdw, &timeout);
Expect(num_events == 1, "Expected one read event after slot reuse");
Expect(fdwatch_get_client_data(fdw, 0) == &marker_b, "Unexpected client data after slot reuse");
Expect(fdwatch_check_event(fdw, sockets_b[1], 0) == FDW_READ, "Expected FDW_READ after slot reuse");
uint8_t read_back = 0;
Expect(read(sockets_b[1], &read_back, sizeof(read_back)) == sizeof(read_back), "socketpair B read failed");
Expect(read_back == byte, "Read payload mismatch after slot reuse");
fdwatch_delete(fdw);
close(sockets_a[0]);
close(sockets_a[1]);
close(sockets_b[0]);
close(sockets_b[1]);
}
}
int main()
{
try
{
TestPacketLayouts();
TestSecureCipherRoundTrip();
TestSocketAuthWireFlow();
TestFdwatchBackendMetadata();
TestCheckpointBackendMetadata();
TestFdwatchReadAndOneshotWrite();
TestFdwatchSlotReuseAfterDelete();
std::cout << "metin smoke tests passed\n";
return 0;
}
catch (const std::exception& e)
{
std::cerr << "metin smoke tests failed: " << e.what() << '\n';
return 1;
}
}