tests: add end-to-end login smoke utility
This commit is contained in:
@@ -7,14 +7,26 @@ add_executable(metin_smoke_tests
|
||||
${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)
|
||||
|
||||
406
tests/login_smoke.cpp
Normal file
406
tests/login_smoke.cpp
Normal file
@@ -0,0 +1,406 @@
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#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>");
|
||||
|
||||
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];
|
||||
const std::string password = argv[5];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user