ci: add Linux smoke coverage
This commit is contained in:
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
20
tests/CMakeLists.txt
Normal file
20
tests/CMakeLists.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
if(WIN32)
|
||||
return()
|
||||
endif()
|
||||
|
||||
add_executable(metin_smoke_tests
|
||||
smoke_auth.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/game/SecureCipher.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(metin_smoke_tests
|
||||
libthecore
|
||||
sodium
|
||||
pthread
|
||||
)
|
||||
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
|
||||
target_link_libraries(metin_smoke_tests md)
|
||||
endif()
|
||||
|
||||
add_test(NAME metin_smoke_tests COMMAND metin_smoke_tests)
|
||||
145
tests/smoke_auth.cpp
Normal file
145
tests/smoke_auth.cpp
Normal file
@@ -0,0 +1,145 @@
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "game/stdafx.h"
|
||||
#include "game/SecureCipher.h"
|
||||
#include "libthecore/fdwatch.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
void Expect(bool condition, const char* message)
|
||||
{
|
||||
if (!condition)
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
void TestPacketLayouts()
|
||||
{
|
||||
constexpr size_t key_challenge_size =
|
||||
sizeof(uint16_t) * 2 + SecureCipher::PK_SIZE + SecureCipher::CHALLENGE_SIZE + sizeof(uint32_t);
|
||||
constexpr size_t key_response_size =
|
||||
sizeof(uint16_t) * 2 + SecureCipher::PK_SIZE + SecureCipher::HMAC_SIZE;
|
||||
constexpr size_t key_complete_size =
|
||||
sizeof(uint16_t) * 2 + SecureCipher::SESSION_TOKEN_SIZE + SecureCipher::TAG_SIZE + SecureCipher::NONCE_SIZE;
|
||||
|
||||
Expect(key_challenge_size == 72, "Unexpected key challenge wire size");
|
||||
Expect(key_response_size == 68, "Unexpected key response wire size");
|
||||
Expect(key_complete_size == 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 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]);
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
try
|
||||
{
|
||||
TestPacketLayouts();
|
||||
TestSecureCipherRoundTrip();
|
||||
TestFdwatchReadAndOneshotWrite();
|
||||
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