feat(M1): TCP/TLS control plane -- auth, channels, ephemeral text
Implements the full M1 milestone. Two clients authenticate over TLS 1.3
(guest + Argon2id password) and exchange channel + private text messages
through a real server. All five ctest --preset m1-dev tests pass in ~1 s.
Key components added:
- vcpkg baseline + m1-dev preset (protobuf/mbedTLS/libsodium/asio/sqlite3)
- FrameCodec feed+emit, encode/decode_envelope, protobuf codegen
- TcpServerConn with blocking TLS handshake thread + tls_read_loop
- TlsContext (mbedTLS 1.3, ECDSA-P256 self-signed cert, TOFU on client)
- WorkerPool (3 threads, used for Argon2id)
- Database: SQLite + libsodium Argon2id, account lifecycle, bootstrap admin
- ServerIdentityManager: Ed25519 key + cert generate/persist/fingerprint
- ConnSession state machine: WaitingHello -> WaitingAuth -> Authenticated
- SessionRegistry: channel tree, user map, text routing, broadcast
- vc_client full M1 C ABI: connect/TLS/handshake/auth/text/disconnect
- voicecat-admin CLI: account add/reset/del/list
- test_m1_integration: M1 exit criterion, verified green
Bug fixed: double-framing in ConnSession::send_envelope -- encode_envelope
was adding the [4-byte len] prefix, then TcpServerConn::send_frame added
a second one, causing the client to parse [len][proto] as protobuf (silent
failure). Fixed by serializing raw protobuf bytes in send_envelope and
letting send_frame apply the single length prefix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 23:48:44 +02:00
|
|
|
/*
|
|
|
|
|
* test_envelope — round-trip an Envelope through FrameCodec + encode/decode.
|
build(cmake): clean up presets, add release/apple presets, cross-platform triplets
Rationalize the preset set to match the project's actual state (past M5):
- Rename dev->skeleton (no-deps stub smoke), m1-dev->dev (default dev preset)
- Drop m2-dev (cache-identical to m1-dev)
- Add release preset (optimized + tests on, symbols kept)
- Strip server-release binaries (-s linker flag)
- Add apple-dev/apple-ios/apple-ios-sim scaffolding presets for XCFramework
Add cmake/voicecat-toolchain.cmake wrapper that auto-resolves the vcpkg
triplet from the host platform (x64-mingw-static/x64-linux/arm64-osx) so
the main presets work on Windows/Linux/macOS without per-OS variants.
Update all docs (building.md, CLAUDE.md, README.md, AGENTS.md, deployment.md,
tech-stack.md, client READMEs) and stale preset-name references in code
comments. No C++ behavior changes — the core was already portable.
2026-06-18 03:16:01 +02:00
|
|
|
* Runs only under dev (requires protobuf).
|
feat(M1): TCP/TLS control plane -- auth, channels, ephemeral text
Implements the full M1 milestone. Two clients authenticate over TLS 1.3
(guest + Argon2id password) and exchange channel + private text messages
through a real server. All five ctest --preset m1-dev tests pass in ~1 s.
Key components added:
- vcpkg baseline + m1-dev preset (protobuf/mbedTLS/libsodium/asio/sqlite3)
- FrameCodec feed+emit, encode/decode_envelope, protobuf codegen
- TcpServerConn with blocking TLS handshake thread + tls_read_loop
- TlsContext (mbedTLS 1.3, ECDSA-P256 self-signed cert, TOFU on client)
- WorkerPool (3 threads, used for Argon2id)
- Database: SQLite + libsodium Argon2id, account lifecycle, bootstrap admin
- ServerIdentityManager: Ed25519 key + cert generate/persist/fingerprint
- ConnSession state machine: WaitingHello -> WaitingAuth -> Authenticated
- SessionRegistry: channel tree, user map, text routing, broadcast
- vc_client full M1 C ABI: connect/TLS/handshake/auth/text/disconnect
- voicecat-admin CLI: account add/reset/del/list
- test_m1_integration: M1 exit criterion, verified green
Bug fixed: double-framing in ConnSession::send_envelope -- encode_envelope
was adding the [4-byte len] prefix, then TcpServerConn::send_frame added
a second one, causing the client to parse [len][proto] as protobuf (silent
failure). Fixed by serializing raw protobuf bytes in send_envelope and
letting send_frame apply the single length prefix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 23:48:44 +02:00
|
|
|
*/
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
#include <cstring>
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
#include "protocol/envelope.h"
|
|
|
|
|
#include "protocol/protocol.h"
|
|
|
|
|
|
|
|
|
|
using namespace voicecat::protocol;
|
|
|
|
|
|
|
|
|
|
static int g_failures = 0;
|
|
|
|
|
|
|
|
|
|
#define CHECK(cond) \
|
|
|
|
|
do { \
|
|
|
|
|
if (!(cond)) { \
|
|
|
|
|
std::printf("FAIL [%s:%d]: %s\n", __FILE__, __LINE__, #cond); \
|
|
|
|
|
++g_failures; \
|
|
|
|
|
} \
|
|
|
|
|
} while (0)
|
|
|
|
|
|
|
|
|
|
int main() {
|
|
|
|
|
// Build a ClientHello envelope.
|
|
|
|
|
voicecat::v1::Envelope out_env;
|
|
|
|
|
out_env.set_request_id(42);
|
|
|
|
|
auto* hello = out_env.mutable_client_hello();
|
|
|
|
|
hello->set_proto_version(1);
|
|
|
|
|
hello->set_client_name("test-client");
|
|
|
|
|
hello->set_client_version("0.0.1");
|
|
|
|
|
hello->add_features("text");
|
|
|
|
|
|
|
|
|
|
// encode_envelope → framed wire bytes
|
|
|
|
|
std::vector<uint8_t> wire;
|
|
|
|
|
bool enc_ok = encode_envelope(out_env, wire);
|
|
|
|
|
CHECK(enc_ok);
|
|
|
|
|
CHECK(wire.size() > kLengthHeaderSize);
|
|
|
|
|
|
|
|
|
|
// Feed through FrameCodec to extract the payload
|
|
|
|
|
FrameCodec codec;
|
|
|
|
|
std::vector<std::vector<uint8_t>> frames;
|
|
|
|
|
bool feed_ok = codec.feed(wire.data(), wire.size(), frames);
|
|
|
|
|
CHECK(feed_ok);
|
|
|
|
|
CHECK(frames.size() == 1);
|
|
|
|
|
|
|
|
|
|
// decode_envelope from the extracted payload
|
|
|
|
|
voicecat::v1::Envelope in_env;
|
|
|
|
|
bool dec_ok = decode_envelope(frames[0], in_env);
|
|
|
|
|
CHECK(dec_ok);
|
|
|
|
|
|
|
|
|
|
// Verify round-trip fidelity
|
|
|
|
|
CHECK(in_env.request_id() == 42);
|
|
|
|
|
CHECK(in_env.has_client_hello());
|
|
|
|
|
CHECK(in_env.client_hello().proto_version() == 1);
|
|
|
|
|
CHECK(std::strcmp(in_env.client_hello().client_name().c_str(), "test-client") == 0);
|
|
|
|
|
CHECK(in_env.client_hello().features_size() == 1);
|
|
|
|
|
CHECK(std::strcmp(in_env.client_hello().features(0).c_str(), "text") == 0);
|
|
|
|
|
|
|
|
|
|
// next_request_id() is monotonically increasing
|
|
|
|
|
uint64_t r1 = next_request_id();
|
|
|
|
|
uint64_t r2 = next_request_id();
|
|
|
|
|
CHECK(r2 == r1 + 1);
|
|
|
|
|
|
|
|
|
|
// Empty envelope round-trips cleanly
|
|
|
|
|
{
|
|
|
|
|
voicecat::v1::Envelope empty;
|
|
|
|
|
std::vector<uint8_t> w2;
|
|
|
|
|
CHECK(encode_envelope(empty, w2));
|
|
|
|
|
FrameCodec c2;
|
|
|
|
|
std::vector<std::vector<uint8_t>> f2;
|
|
|
|
|
CHECK(c2.feed(w2.data(), w2.size(), f2));
|
|
|
|
|
CHECK(f2.size() == 1);
|
|
|
|
|
voicecat::v1::Envelope e2;
|
|
|
|
|
CHECK(decode_envelope(f2[0], e2));
|
|
|
|
|
CHECK(e2.request_id() == 0);
|
|
|
|
|
CHECK(e2.body_case() == voicecat::v1::Envelope::BODY_NOT_SET);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (g_failures == 0) {
|
|
|
|
|
std::printf("envelope: all checks passed\n");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
std::printf("envelope: %d failure(s)\n", g_failures);
|
|
|
|
|
return 1;
|
|
|
|
|
}
|