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:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bsd:
|
linux:
|
||||||
runs-on: ubuntu-latest
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: FreeBSD job
|
- name: FreeBSD job
|
||||||
@@ -24,6 +56,7 @@ jobs:
|
|||||||
cd build
|
cd build
|
||||||
cmake ..
|
cmake ..
|
||||||
gmake all -j6
|
gmake all -j6
|
||||||
|
ctest --output-on-failure
|
||||||
- name: Collect outputs
|
- name: Collect outputs
|
||||||
run: |
|
run: |
|
||||||
mkdir _output
|
mkdir _output
|
||||||
@@ -32,4 +65,4 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: output_bsd
|
name: output_bsd
|
||||||
path: _output
|
path: _output
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ project(m2dev-server-src)
|
|||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_C_STANDARD 17)
|
set(CMAKE_C_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
# ASan support
|
# ASan support
|
||||||
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
|
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(src)
|
||||||
add_subdirectory(vendor)
|
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