Files
m2dev-server-src/tests/login_smoke.cpp
server ef9324025e
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
tests: extend login smoke to entergame
2026-04-14 10:30:25 +02:00

781 lines
22 KiB
C++

#include <array>
#include <cerrno>
#include <chrono>
#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;
constexpr size_t PLAYER_PER_ACCOUNT_LOCAL = 4;
constexpr size_t CHARACTER_NAME_MAX_LEN_LOCAL = 64;
constexpr size_t GUILD_NAME_MAX_LEN_LOCAL = 12;
#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 SimplePlayerLocal
{
uint32_t id;
char name[CHARACTER_NAME_MAX_LEN_LOCAL + 1];
uint8_t job;
uint8_t level;
uint32_t play_minutes;
uint8_t st;
uint8_t ht;
uint8_t dx;
uint8_t iq;
uint16_t main_part;
uint8_t change_name;
uint16_t hair_part;
uint8_t dummy[4];
int32_t x;
int32_t y;
uint32_t addr;
uint16_t port;
uint8_t skill_group;
};
struct PacketGCLoginSuccess4
{
uint16_t header;
uint16_t length;
SimplePlayerLocal players[PLAYER_PER_ACCOUNT_LOCAL];
uint32_t guild_id[PLAYER_PER_ACCOUNT_LOCAL];
char guild_name[PLAYER_PER_ACCOUNT_LOCAL][GUILD_NAME_MAX_LEN_LOCAL + 1];
uint32_t handle;
uint32_t random_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;
};
struct PacketCGPlayerCreate
{
uint16_t header;
uint16_t length;
uint8_t index;
char name[CHARACTER_NAME_MAX_LEN_LOCAL + 1];
uint16_t job;
uint8_t shape;
uint8_t con;
uint8_t intel;
uint8_t str;
uint8_t dex;
};
struct PacketGCPlayerCreateSuccess
{
uint16_t header;
uint16_t length;
uint8_t account_character_index;
SimplePlayerLocal player;
};
struct PacketGCCreateFailure
{
uint16_t header;
uint16_t length;
uint8_t type;
};
struct PacketCGPlayerSelect
{
uint16_t header;
uint16_t length;
uint8_t index;
};
struct PacketCGEnterGame
{
uint16_t header;
uint16_t length;
};
struct PacketCGClientVersion
{
uint16_t header;
uint16_t length;
char filename[32 + 1];
char timestamp[32 + 1];
};
struct PacketGCMainCharacter
{
enum
{
MUSIC_NAME_LEN = 24
};
uint16_t header;
uint16_t length;
uint32_t vid;
uint16_t race_num;
char name[CHARACTER_NAME_MAX_LEN_LOCAL + 1];
char bgm_name[MUSIC_NAME_LEN + 1];
float bgm_vol;
int32_t x;
int32_t y;
int32_t z;
uint8_t empire;
uint8_t skill_group;
};
struct PacketGCTime
{
uint16_t header;
uint16_t length;
time_t time;
};
struct PacketGCChannel
{
uint16_t header;
uint16_t length;
uint8_t channel;
};
#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));
}
int RemainingTimeoutMs(std::chrono::steady_clock::time_point deadline)
{
const auto now = std::chrono::steady_clock::now();
if (now >= deadline)
return 0;
return static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now).count());
}
uint8_t FindFirstPlayableCharacterIndex(const PacketGCLoginSuccess4& success)
{
for (uint8_t index = 0; index < PLAYER_PER_ACCOUNT_LOCAL; ++index)
{
if (success.players[index].id != 0 && success.players[index].change_name == 0)
return index;
}
throw std::runtime_error("account has no playable characters");
}
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");
}
PacketGCLoginSuccess4 LoginToChannel(EncryptedClient& channel_client, const std::string& login, uint32_t login_key)
{
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;
PacketGCLoginSuccess4 login_success {};
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)
{
Expect(frame.size() == sizeof(PacketGCLoginSuccess4), "unexpected channel login success size");
std::memcpy(&login_success, frame.data(), sizeof(login_success));
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");
return login_success;
}
uint8_t CreateCharacter(EncryptedClient& channel_client, std::string_view create_name)
{
PacketCGPlayerCreate create_packet {};
create_packet.header = CG::CHARACTER_CREATE;
create_packet.length = sizeof(create_packet);
create_packet.index = 0;
create_packet.job = 0;
create_packet.shape = 0;
create_packet.con = 4;
create_packet.intel = 4;
create_packet.str = 4;
create_packet.dex = 4;
std::strncpy(create_packet.name, create_name.data(), sizeof(create_packet.name) - 1);
channel_client.SendEncryptedPacket(create_packet);
std::vector<uint8_t> frame;
for (int i = 0; i < 16; ++i)
{
Expect(channel_client.WaitForFrame(frame, 5000), "timed out waiting for character create response");
const auto header = FrameHeader(frame);
if (header == GC::PHASE)
continue;
if (header == GC::PLAYER_CREATE_FAILURE)
{
if (frame.size() == sizeof(PacketGCCreateFailure))
{
const auto* failure = reinterpret_cast<const PacketGCCreateFailure*>(frame.data());
throw std::runtime_error("character create failed type=" + std::to_string(failure->type));
}
throw std::runtime_error("character create failed");
}
if (header == GC::PLAYER_CREATE_SUCCESS)
{
Expect(frame.size() == sizeof(PacketGCPlayerCreateSuccess), "unexpected player create success size");
const auto* success = reinterpret_cast<const PacketGCPlayerCreateSuccess*>(frame.data());
std::cout << "character_created index=" << static_cast<int>(success->account_character_index)
<< " name=" << success->player.name << "\n";
return success->account_character_index;
}
}
throw std::runtime_error("did not receive PLAYER_CREATE_SUCCESS");
}
void SelectCharacter(EncryptedClient& channel_client, uint8_t character_index)
{
PacketCGPlayerSelect select_packet {};
select_packet.header = CG::CHARACTER_SELECT;
select_packet.length = sizeof(select_packet);
select_packet.index = character_index;
channel_client.SendEncryptedPacket(select_packet);
bool saw_phase_loading = false;
bool saw_main_character = false;
std::vector<uint8_t> frame;
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (true)
{
const int timeout_ms = RemainingTimeoutMs(deadline);
Expect(timeout_ms > 0, "timed out waiting for character select response");
Expect(channel_client.WaitForFrame(frame, timeout_ms), "timed out waiting for character select response");
const auto header = FrameHeader(frame);
if (header == GC::LOGIN_FAILURE)
{
Expect(frame.size() == sizeof(PacketGCLoginFailure), "unexpected character select failure size");
const auto* failure = reinterpret_cast<const PacketGCLoginFailure*>(frame.data());
throw std::runtime_error(std::string("character select failed: ") + failure->status);
}
if (header == GC::PHASE)
{
Expect(frame.size() == sizeof(PacketGCPhase), "unexpected phase packet size");
const auto* phase = reinterpret_cast<const PacketGCPhase*>(frame.data());
if (phase->phase == PHASE_LOADING)
{
saw_phase_loading = true;
std::cout << "phase_loading\n";
}
continue;
}
if (header == GC::MAIN_CHARACTER)
{
Expect(frame.size() == sizeof(PacketGCMainCharacter), "unexpected main character packet size");
const auto* main_character = reinterpret_cast<const PacketGCMainCharacter*>(frame.data());
Expect(main_character->name[0] != '\0', "main character name is empty");
saw_main_character = true;
std::cout << "main_character name=" << main_character->name << "\n";
}
if (saw_phase_loading && saw_main_character)
return;
}
}
void SendClientVersion(EncryptedClient& channel_client, std::string_view client_version)
{
PacketCGClientVersion version_packet {};
version_packet.header = CG::CLIENT_VERSION;
version_packet.length = sizeof(version_packet);
std::strncpy(version_packet.filename, "Metin2_Debug.exe", sizeof(version_packet.filename) - 1);
std::strncpy(version_packet.timestamp, client_version.data(), sizeof(version_packet.timestamp) - 1);
channel_client.SendEncryptedPacket(version_packet);
std::cout << "client_version " << version_packet.timestamp << "\n";
}
void EnterGame(EncryptedClient& channel_client)
{
PacketCGEnterGame enter_game {};
enter_game.header = CG::ENTERGAME;
enter_game.length = sizeof(enter_game);
channel_client.SendEncryptedPacket(enter_game);
bool saw_phase_game = false;
bool saw_time = false;
bool saw_channel = false;
std::vector<uint8_t> frame;
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (true)
{
const int timeout_ms = RemainingTimeoutMs(deadline);
Expect(timeout_ms > 0, "timed out waiting for entergame response");
Expect(channel_client.WaitForFrame(frame, timeout_ms), "timed out waiting for entergame response");
const auto header = FrameHeader(frame);
if (header == GC::LOGIN_FAILURE)
{
Expect(frame.size() == sizeof(PacketGCLoginFailure), "unexpected entergame failure size");
const auto* failure = reinterpret_cast<const PacketGCLoginFailure*>(frame.data());
throw std::runtime_error(std::string("entergame failed: ") + failure->status);
}
if (header == GC::PHASE)
{
Expect(frame.size() == sizeof(PacketGCPhase), "unexpected phase packet size");
const auto* phase = reinterpret_cast<const PacketGCPhase*>(frame.data());
if (phase->phase == PHASE_GAME)
{
saw_phase_game = true;
std::cout << "phase_game\n";
}
continue;
}
if (header == GC::TIME)
{
Expect(frame.size() == sizeof(PacketGCTime), "unexpected time packet size");
saw_time = true;
continue;
}
if (header == GC::CHANNEL)
{
Expect(frame.size() == sizeof(PacketGCChannel), "unexpected channel packet size");
const auto* channel = reinterpret_cast<const PacketGCChannel*>(frame.data());
saw_channel = true;
std::cout << "game_channel channel=" << static_cast<int>(channel->channel) << "\n";
}
if (saw_phase_game && saw_time && saw_channel)
return;
}
}
}
int main(int argc, char** argv)
{
try
{
Expect(argc >= 6 && argc <= 8,
"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\n"
" optional: --create-character-name=NAME [--client-version=VERSION]");
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;
std::string create_character_name;
std::string client_version = "1215955205";
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;
}
for (int i = 6; i < argc; ++i)
{
const std::string create_arg = argv[i];
const std::string create_prefix = "--create-character-name=";
const std::string version_prefix = "--client-version=";
if (create_arg.rfind(create_prefix, 0) == 0)
{
create_character_name = create_arg.substr(create_prefix.size());
Expect(!create_character_name.empty(), "create character name is empty");
Expect(create_character_name.size() <= CHARACTER_NAME_MAX_LEN_LOCAL, "create character name too long");
continue;
}
if (create_arg.rfind(version_prefix, 0) == 0)
{
client_version = create_arg.substr(version_prefix.size());
Expect(!client_version.empty(), "client version is empty");
continue;
}
throw std::runtime_error("unknown optional argument");
}
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);
EncryptedClient channel_client(host, channel_port);
channel_client.Handshake();
const PacketGCLoginSuccess4 login_success = LoginToChannel(channel_client, login, login_key);
uint8_t character_index = 0;
try
{
character_index = FindFirstPlayableCharacterIndex(login_success);
std::cout << "selected_character index=" << static_cast<int>(character_index) << "\n";
}
catch (const std::exception&)
{
Expect(!create_character_name.empty(), "account has no playable characters and no create-character name was provided");
character_index = CreateCharacter(channel_client, create_character_name);
}
SelectCharacter(channel_client, character_index);
SendClientVersion(channel_client, client_version);
EnterGame(channel_client);
std::cout << "full_login_success\n";
return 0;
}
catch (const std::exception& e)
{
std::cerr << "metin_login_smoke failed: " << e.what() << "\n";
return 1;
}
}