222 lines
9.6 KiB
C++
222 lines
9.6 KiB
C++
#include "server.h"
|
|
|
|
#include <cstdio>
|
|
#include <csignal>
|
|
|
|
#define ASIO_STANDALONE 1
|
|
#include <asio.hpp>
|
|
#include <asio/signal_set.hpp>
|
|
|
|
#include <chrono>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
#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<Database>(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<SessionRegistry>(db);
|
|
registry->load_channels();
|
|
|
|
// ── Worker pool ──────────────────────────────────────────────────────────
|
|
auto workers = std::make_shared<WorkerPool>(3);
|
|
|
|
// ── Asio io_context ──────────────────────────────────────────────────────
|
|
asio::io_context io;
|
|
|
|
// ── UDP media relay ────────────────────────────────────────────────────────
|
|
auto media_relay = std::make_shared<MediaRelay>(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<voicecat::net::TcpServerConn> {
|
|
auto session = std::make_shared<ConnSession>(
|
|
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<uint8_t> 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>(
|
|
voicecat::crypto::TlsContext::Role::Server,
|
|
&id_mgr.cert());
|
|
|
|
auto tcp = std::make_shared<voicecat::net::TcpServerConn>(
|
|
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<voicecat::net::TcpServerConn> weak_tcp = tcp;
|
|
session->set_io(
|
|
[weak_tcp](std::vector<uint8_t> 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) ─────────────────────────────────
|
|
// Drops half-open sessions and broadcasts LEFT so peers remove their streams.
|
|
// Disabled when reaper_timeout_ms <= 0.
|
|
asio::steady_timer reaper_timer(io);
|
|
std::function<void()> 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<unsigned long long>(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
|