diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5267492..29a0871 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 \ No newline at end of file + path: _output diff --git a/CMakeLists.txt b/CMakeLists.txt index f307c75..5aac940 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..215f3c1 --- /dev/null +++ b/tests/CMakeLists.txt @@ -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) diff --git a/tests/smoke_auth.cpp b/tests/smoke_auth.cpp new file mode 100644 index 0000000..ec84900 --- /dev/null +++ b/tests/smoke_auth.cpp @@ -0,0 +1,145 @@ +#include +#include +#include +#include +#include +#include + +#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 server_pk {}; + std::array 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 challenge {}; + std::array response {}; + server.GenerateChallenge(challenge.data()); + client.ComputeChallengeResponse(challenge.data(), response.data()); + Expect(server.VerifyChallengeResponse(challenge.data(), response.data()), "Challenge verification failed"); + + std::array token {}; + for (size_t i = 0; i < token.size(); ++i) + token[i] = static_cast(i); + + std::array ciphertext {}; + std::array nonce {}; + std::array 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 payload {}; + for (size_t i = 0; i < payload.size(); ++i) + payload[i] = static_cast(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; + } +}