Compare commits
18 Commits
0f69efeba4
...
test/disco
| Author | SHA1 | Date | |
|---|---|---|---|
| ad6ac77199 | |||
| bfeb31c454 | |||
|
|
14e084d691 | ||
|
|
b46a2661df | ||
|
|
e99b8b0520 | ||
|
|
b2b037fb94 | ||
|
|
e2c43240ec | ||
|
|
e638816026 | ||
|
|
cecc822777 | ||
|
|
3272537376 | ||
|
|
4f1e7b0e9c | ||
|
|
abed660256 | ||
|
|
7daf51f2da | ||
|
|
3e3f0918e9 | ||
|
|
25ec562ab0 | ||
|
|
c8146c0340 | ||
|
|
e8a33f84f4 | ||
|
|
1ca64ce829 |
39
.github/workflows/main.yml
vendored
39
.github/workflows/main.yml
vendored
@@ -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
67
AGENTS.md
Normal 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
11
CLAUDE.md
Normal 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
|
||||
@@ -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)
|
||||
|
||||
290
src/db/Main.cpp
290
src/db/Main.cpp
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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, ...)
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 증가
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
32
tests/CMakeLists.txt
Normal 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
422
tests/login_smoke.cpp
Normal 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
390
tests/smoke_auth.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user