#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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(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(written); } } void ReadExact(int fd, void* data, size_t length, std::string_view context) { uint8_t* cursor = static_cast(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(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(&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 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 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& 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& frame) { if (m_stream.size() < PACKET_HEADER_SIZE) return false; const auto header = *reinterpret_cast(m_stream.data()); const auto length = *reinterpret_cast(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 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(bytes_read)); std::memcpy(m_stream.data() + old_size, buffer.data(), static_cast(bytes_read)); m_cipher.DecryptInPlace(m_stream.data() + old_size, static_cast(bytes_read)); } int m_fd = -1; SecureCipher m_cipher; std::vector m_stream; }; uint16_t FrameHeader(const std::vector& frame) { return *reinterpret_cast(frame.data()); } uint16_t FrameLength(const std::vector& frame) { return *reinterpret_cast(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 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(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(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 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(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(frame.data()); saw_empire = true; empire = empire_packet->empire; std::cout << "channel_empire empire=" << static_cast(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 \n" " or: metin_login_smoke --password-env=ENV_NAME"); const std::string host = argv[1]; const uint16_t auth_port = static_cast(std::stoi(argv[2])); const uint16_t channel_port = static_cast(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; } }