#include "server.h" #include #include #define ASIO_STANDALONE 1 #include #include #include #include #include #include #include "conn_session.h" #include "core/worker_pool.h" #include "crypto/crypto.h" #include "db.h" #include "identity.h" #include "media_relay.h" #include "net/transport.h" #include "session_registry.h" namespace voicecat::server { int Server::run() { // ── Identity + cert ────────────────────────────────────────────────────── ServerIdentityManager id_mgr; std::string error; if (!id_mgr.init(cfg_.data_dir, cfg_.server_name, error)) { std::fprintf(stderr, "[server] identity init failed: %s\n", error.c_str()); return 1; } // ── Database + bootstrap admin ─────────────────────────────────────────── auto db = std::make_shared(cfg_.data_dir + "/voicecat.db"); if (!db->open(error)) { std::fprintf(stderr, "[server] db open failed: %s\n", error.c_str()); return 1; } if (db->is_empty()) { std::string pw = Database::generate_password(20); auto acc = db->create_account("admin", pw, true, error); if (!acc) { std::fprintf(stderr, "[server] failed to create admin account: %s\n", error.c_str()); return 1; } std::printf("\n"); std::printf("┌─────────────────────────────────────────────────────────┐\n"); std::printf("│ First run — admin account created │\n"); std::printf("│ username : %-44s│\n", "admin"); std::printf("│ password : %-44s│\n", pw.c_str()); std::printf("│ Change with: voicecat-admin account reset admin │\n"); std::printf("└─────────────────────────────────────────────────────────┘\n"); std::printf("\n"); } // ── Session registry ───────────────────────────────────────────────────── auto registry = std::make_shared(db); registry->load_channels(); // ── Worker pool ────────────────────────────────────────────────────────── auto workers = std::make_shared(3); // ── Asio io_context ────────────────────────────────────────────────────── asio::io_context io; // ── UDP media relay ──────────────────────────────────────────────────────── auto media_relay = std::make_shared(io, registry); // Control and media share one port number on TCP+UDP (docs/deployment.md): when media_port // is left at 0, follow bind_port so a single forward rule covers both. If bind_port is also 0 // (tests), this stays 0 and the OS picks the UDP port (reported via on_media_ready). uint16_t media_want = cfg_.media_port != 0 ? cfg_.media_port : cfg_.bind_port; if (!media_relay->bind(media_want)) { std::fprintf(stderr, "[server] failed to bind UDP media port %u\n", media_want); return 1; } media_relay->start(); uint16_t media_bound = media_relay->media_port(); if (cfg_.on_media_ready) cfg_.on_media_ready(media_bound); // Capture all locals by reference for the factory lambda (io lifetime is > factory) voicecat::net::TcpAcceptor acceptor( io, cfg_.bind_port, [&](asio::ip::tcp::socket sock) -> std::shared_ptr { auto session = std::make_shared( db, registry, workers, id_mgr.identity().fingerprint, cfg_.allow_guests, media_bound); // Use shared_ptr (not weak_ptr) so TcpServerConn keeps ConnSession alive. // cycle is broken by weak_tcp in the send/close fns below. voicecat::net::TcpChannelCallbacks cbs; cbs.on_frame = [session](std::vector f) { session->on_frame(std::move(f)); }; cbs.on_disconnected = [session] { session->on_disconnect(); }; cbs.on_error = [session](std::error_code) { session->on_disconnect(); }; // Derive media keys right after TLS handshake (server is not the client). cbs.on_tls_ready = [session](voicecat::crypto::TlsContext& tls) { auto send = voicecat::crypto::SodiumMediaCrypto::derive_send(tls, false); auto recv = voicecat::crypto::SodiumMediaCrypto::derive_recv(tls, false); if (send && recv) session->set_media_crypto(std::move(send), std::move(recv)); }; // Create a TLS context for this connection (server role). auto tls = std::make_unique( voicecat::crypto::TlsContext::Role::Server, &id_mgr.cert()); auto tcp = std::make_shared( std::move(sock), std::move(cbs), std::move(tls)); // Give session its send/close capability (weak_ptr — see the shared_ptr/cycle // note above). std::weak_ptr weak_tcp = tcp; session->set_io( [weak_tcp](std::vector frame) { if (auto t = weak_tcp.lock()) t->send_frame(std::move(frame)); }, [weak_tcp] { if (auto t = weak_tcp.lock()) t->close(); }); uint64_t sid = registry->register_session(session); session->set_session_id(sid); session->begin(); // NOTE: do NOT call tcp->start() here — TcpAcceptor::do_accept() calls it. return tcp; }); acceptor.start(); // Arm programmatic stop (for tests and embedders). { std::lock_guard lk(stop_mutex_); stop_fn_ = [&] { acceptor.stop(); media_relay->stop(); io.stop(); }; } // Notify caller of the actual bound port (matters when bind_port==0). uint16_t bound = acceptor.local_port(); if (cfg_.on_ready) cfg_.on_ready(bound); #ifndef _WIN32 // POSIX/macOS: ignore SIGPIPE — writing to a disconnected client's TCP socket returns // EPIPE instead of killing the server process. On macOS SIGPIPE is delivered by // default (unlike Windows where the signal doesn't exist). Must be set before any // async writes are posted. Process-global; safe alongside the asio::signal_set below // (which only catches SIGINT/SIGTERM). std::signal(SIGPIPE, SIG_IGN); #endif // Graceful shutdown on SIGINT/SIGTERM asio::signal_set signals(io, SIGINT, SIGTERM); signals.async_wait([&](std::error_code, int sig) { std::printf("\n[server] signal %d — shutting down\n", sig); acceptor.stop(); media_relay->stop(); io.stop(); }); // ── Keepalive reaper (docs/protocol.md §7) ───────────────────────────────── // Sweeps every reaper_sweep_ms and drops any session whose last_seen is older than // reaper_timeout_ms. Each close() broadcasts UserEvent::LEFT via the Tier 1 fix, so // peers learn about the timeout exactly like a normal disconnect — their audio engines // call remove_stream and stop PLC. This catches half-open connections (NAT timeout, // wifi loss without RST, laptop sleep) that never produce a TCP EOF and would otherwise // leave ghost users forever. Disabled when reaper_timeout_ms <= 0. asio::steady_timer reaper_timer(io); std::function arm_reaper; if (cfg_.reaper_timeout_ms > 0) { arm_reaper = [&] { reaper_timer.expires_after(std::chrono::milliseconds(cfg_.reaper_sweep_ms)); reaper_timer.async_wait([&](std::error_code ec) { if (ec || io.stopped()) return; for (auto& sess : registry->find_stale_sessions(cfg_.reaper_timeout_ms)) { std::printf("[server] reaper: dropping stale session %llu (user %u)\n", static_cast(sess->session_id()), sess->user_id()); sess->close(); } arm_reaper(); }); }; arm_reaper(); } std::printf("[voicecat-server] %s — TCP :%u UDP :%u\n", cfg_.server_name.c_str(), bound, media_bound); std::printf("[voicecat-server] fingerprint: %s\n", id_mgr.fingerprint_display().c_str()); io.run(); // Close all connections and block until their TLS I/O threads have finished. // MUST happen before io (and its reactor) is destroyed — the TLS read threads do // blocking I/O (not async on io_context) and touch the reactor on socket close. // On macOS kqueue the reactor pointer is null'd immediately on io_context destruction, // causing a segfault; latent on Windows IOCP / Linux epoll where timing is more forgiving. acceptor.shutdown(); { std::lock_guard lk(stop_mutex_); stop_fn_ = nullptr; } workers->join(); return 0; } void Server::stop() { std::lock_guard lk(stop_mutex_); if (stop_fn_) stop_fn_(); } } // namespace voicecat::server