Files
voice-cat/server/src/conn_session.h
Talon bba605401d
Some checks failed
Build Linux Binaries / linux/amd64 (push) Has been cancelled
Build Linux Binaries / linux/arm64 (push) Has been cancelled
chore: comment cleanup pass ahead of open-sourcing
Removes leftover debug scaffolding (stray Console.WriteLine/NSLog traces,
dead nick_buf_ptr, a no-op --print-config flag now implemented for real),
fixes stale/misleading comments (channel passwords are no longer a "future
M5+" feature, a wrong cross-reference, a stale TlsContext::close() mention,
an incomplete BanRecord::subject_type doc, and a smoke test pointing at a
build/m1-dev preset that no longer exists), strips internal M1-M5 milestone
jargon from comments now that the roadmap is done, trims comments that just
restated the following line, and consolidates a few "why" explanations that
were duplicated 2-3 times in the same file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 10:20:18 +01:00

186 lines
9.1 KiB
C++

/*
* server/conn_session.h — Per-client connection state machine.
*
* State: WaitingHello → WaitingAuth → Authenticated → Disconnecting
*
* Design: ConnSession is a pure state machine. It receives frames via on_frame()
* (called from TcpServerConn's strand) and sends via a send_fn set after construction.
* The server creates the TcpServerConn first (with callbacks referencing the session),
* then calls set_tcp() to give the session its send capability.
*/
#ifndef VOICECAT_SERVER_CONN_SESSION_H
#define VOICECAT_SERVER_CONN_SESSION_H
#include <array>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#define ASIO_STANDALONE 1
#include <asio.hpp>
#include "crypto/crypto.h"
#include "proto/voicecat.pb.h"
namespace voicecat { class WorkerPool; }
namespace voicecat::server {
class Database;
class SessionRegistry;
class ConnSession : public std::enable_shared_from_this<ConnSession> {
public:
enum class State { WaitingHello, WaitingAuth, Authenticated, Disconnecting };
using SendFn = std::function<void(std::vector<uint8_t>)>;
using CloseFn = std::function<void()>;
ConnSession(std::shared_ptr<Database> db,
std::shared_ptr<SessionRegistry> registry,
std::shared_ptr<voicecat::WorkerPool> workers,
const std::array<uint8_t, 32>& server_fp,
bool allow_guests,
uint16_t udp_media_port = 0);
void set_io(SendFn send_fn, CloseFn close_fn);
void set_session_id(uint64_t id) { session_id_ = id; }
void begin();
void on_frame(std::vector<uint8_t> frame);
void on_disconnect();
void send_envelope(const voicecat::v1::Envelope& env);
void close();
// Called by SessionRegistry for kick/ban. Public so the registry can forcibly
// close a session without making it a friend class.
void send_disconnect_and_close(uint32_t code, const std::string& reason);
// ── Media key injection (called from on_tls_ready) ────────────────────────
void set_media_crypto(std::unique_ptr<voicecat::crypto::SodiumMediaCrypto> send,
std::unique_ptr<voicecat::crypto::SodiumMediaCrypto> recv);
// ── UDP endpoint (set by MediaRelay on UdpBinding) ────────────────────────
void set_udp_endpoint(asio::ip::udp::endpoint ep);
asio::ip::udp::endpoint udp_endpoint() const;
bool has_udp_endpoint() const { return has_udp_ep_.load(); }
// ── Media crypto access (for SFU relay) ───────────────────────────────────
voicecat::crypto::SodiumMediaCrypto* send_crypto();
voicecat::crypto::SodiumMediaCrypto* recv_crypto();
// ── UDP token (for binding) ────────────────────────────────────────────────
const std::array<uint8_t, 16>& udp_token() const { return udp_token_; }
// ── Accessors ──────────────────────────────────────────────────────────────
State state() const { return state_.load(); }
uint64_t session_id() const { return session_id_; }
uint32_t user_id() const { return user_id_; }
bool voice_subscribed() const { return voice_subscribed_.load(std::memory_order_acquire); }
// Keepalive: bump last_seen to "now" on any inbound activity (TCP frame or UDP voice
// frame). The server's reaper sweeps sessions whose last_seen is older than the
// timeout (docs/protocol.md §7). Stored as steady_clock ms since epoch in an atomic so
// the reaper thread can read it lock-free.
void touch_last_seen() {
last_seen_ms_.store(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count(),
std::memory_order_release);
}
int64_t last_seen_ms() const { return last_seen_ms_.load(std::memory_order_acquire); }
private:
void handle_client_hello(uint64_t req_id, const voicecat::v1::ClientHello& msg);
void handle_auth_request(uint64_t req_id, const voicecat::v1::AuthRequest& msg);
void handle_join_channel(uint64_t req_id, const voicecat::v1::JoinChannelRequest& msg);
void handle_text_message(const voicecat::v1::TextMessage& msg);
void handle_ping(const voicecat::v1::Ping& msg);
void handle_client_disconnect(const voicecat::v1::Disconnect& msg);
void handle_udp_binding(uint64_t req_id, const voicecat::v1::UdpBinding& msg);
void handle_stream_announce(uint64_t req_id, const voicecat::v1::StreamAnnounce& msg);
void handle_stream_stop(const voicecat::v1::StreamStop& msg);
void handle_leave_channel();
void handle_subscribe_voice(uint64_t req_id);
void handle_unsubscribe_voice(uint64_t req_id);
// Moderation & admin handlers
void handle_kick_request(uint64_t req_id, const voicecat::v1::KickRequest& msg);
void handle_ban_request(uint64_t req_id, const voicecat::v1::BanRequest& msg);
void handle_set_permission(uint64_t req_id, const voicecat::v1::SetPermissionRequest& msg);
void handle_server_mute_request(uint64_t req_id, const voicecat::v1::ServerMuteRequest& msg);
void handle_move_user(uint64_t req_id, const voicecat::v1::MoveUserRequest& msg);
void handle_create_channel(uint64_t req_id, const voicecat::v1::CreateChannelRequest& msg);
void handle_edit_channel(uint64_t req_id, const voicecat::v1::EditChannelRequest& msg);
void handle_delete_channel(uint64_t req_id, const voicecat::v1::DeleteChannelRequest& msg);
void handle_create_account(uint64_t req_id, const voicecat::v1::CreateAccountRequest& msg);
void handle_reset_password(uint64_t req_id, const voicecat::v1::ResetPasswordRequest& msg);
void handle_delete_account(uint64_t req_id, const voicecat::v1::DeleteAccountRequest& msg);
void handle_list_accounts(uint64_t req_id, const voicecat::v1::ListAccountsRequest& msg);
void finish_guest_auth(const voicecat::v1::GuestAuth& guest, uint64_t req_id);
void finish_password_auth(const std::string& username, const std::string& password,
uint64_t req_id);
void send_auth_result_ok(uint64_t req_id, const voicecat::v1::User& user,
const voicecat::v1::Permissions* perms = nullptr);
void send_state_snapshot();
void broadcast_user_joined(const voicecat::v1::User& user);
void send_generic_result(uint64_t req_id, bool ok, uint32_t code,
const std::string& message);
// Permission helpers.
bool has_permission(bool (voicecat::v1::Permissions::* getter)() const) const;
bool is_admin() const { return has_permission(&voicecat::v1::Permissions::is_admin); }
void set_permissions(const voicecat::v1::Permissions& perms);
std::shared_ptr<Database> db_;
std::shared_ptr<SessionRegistry> registry_;
std::shared_ptr<voicecat::WorkerPool> workers_;
std::array<uint8_t, 32> server_fp_;
bool allow_guests_;
uint16_t udp_media_port_;
SendFn send_fn_;
CloseFn close_fn_;
std::atomic<State> state_{State::WaitingHello};
uint64_t session_id_{0};
std::atomic<uint32_t> user_id_{0};
std::atomic<bool> closed_{false};
// Keepalive: steady_clock ms since epoch of the last inbound activity. Bumped by
// touch_last_seen() on every on_frame() and (via media_relay) on every UDP voice frame.
// The reaper (server.cpp) drops sessions whose last_seen is older than 45s.
std::atomic<int64_t> last_seen_ms_{0};
// UDP / media
std::array<uint8_t, 16> udp_token_{};
mutable std::mutex udp_ep_mu_;
asio::ip::udp::endpoint udp_ep_;
std::atomic<bool> has_udp_ep_{false};
mutable std::mutex crypto_mu_;
std::unique_ptr<voicecat::crypto::SodiumMediaCrypto> send_crypto_;
std::unique_ptr<voicecat::crypto::SodiumMediaCrypto> recv_crypto_;
// Locally-announced streams. The server assigns the stream_id (unique per
// session), so a per-session counter + the set of currently-active ids is enough to
// support multiple concurrent streams (MIC + SCREEN_AUDIO + AUX_DEVICE) per user.
uint32_t next_stream_id_{1};
std::vector<uint32_t> announced_stream_ids_;
// Permissions granted at auth time (server-side authority).
voicecat::v1::Permissions permissions_;
// Voice-plane subscription. When false, the SFU relay excludes this session from the
// recipient set (find_channel_sessions), and handle_stream_announce rejects new streams.
// Toggled by SubscribeVoiceRequest / UnsubscribeVoiceRequest.
std::atomic<bool> voice_subscribed_{false};
};
} // namespace voicecat::server
#endif // VOICECAT_SERVER_CONN_SESSION_H