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>
186 lines
9.1 KiB
C++
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
|