namespace GeoSus.Server; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.Net; using System.Net.Sockets; using System.Text; using System.Text.Json; // Hlavní vstupní bod serveru public class Program { static readonly ConcurrentDictionary _clients = new(); static readonly DateTime _startTime = DateTime.UtcNow; static ServerConfig _config = null!; static ServerEncryption _encryption = null!; static LobbyManager _lobbyManager = null!; static StatsDb _statsDb = null!; static Persistence _persistence = null!; static OverpassService _overpassService = null!; static AdminPanel _adminPanel = null!; static ILoggerFactory _loggerFactory = null!; static ILogger _logger = null!; static CancellationTokenSource _cts = new(); // Public accessor for admin panel public static DateTime StartTime => _startTime; public static async Task Main(string[] args) { Console.OutputEncoding = Encoding.UTF8; // Inicializace _loggerFactory = LoggerFactory.Create(builder => { builder.AddConsole(); builder.SetMinimumLevel(LogLevel.Information); }); _logger = _loggerFactory.CreateLogger(); // Načtení konfigurace _config = ServerConfig.Load("appsettings.json"); // Uložíme výchozí config pokud neexistuje if (!File.Exists("appsettings.json")) { _config.Save("appsettings.json"); _logger.LogInformation("Vytvořen výchozí appsettings.json"); } // Inicializace komponent _encryption = new ServerEncryption(_config.RsaKeySizeBits); _statsDb = new StatsDb(Path.Combine(_config.DataPath, "stats.db"), _loggerFactory.CreateLogger()); _persistence = new Persistence(_config, _loggerFactory.CreateLogger()); _overpassService = new OverpassService(_config, _loggerFactory.CreateLogger()); _lobbyManager = new LobbyManager(_config, _loggerFactory.CreateLogger(), _statsDb, _persistence, _overpassService); _adminPanel = new AdminPanel(_loggerFactory.CreateLogger(), _config, _lobbyManager, _statsDb); _logger.LogInformation("GeoSus Server spouštím..."); _logger.LogInformation("TCP port: {TcpPort}, HTTP port: {HttpPort}", _config.TcpPort, _config.HttpPort); // Handle shutdown signals - but only on explicit request var shutdownRequested = false; Console.CancelKeyPress += (s, e) => { if (!shutdownRequested) { shutdownRequested = true; e.Cancel = true; _logger.LogInformation("Shutdown requested (Ctrl+C)... press again to force"); } else { _logger.LogInformation("Force shutdown..."); _cts.Cancel(); } }; // Spustíme TCP listener a HTTP API paralelně var tcpTask = RunTcpServerAsync(); var httpTask = RunHttpServerAsync(); var cleanupTask = RunCleanupLoopAsync(); var broadcastTask = RunBroadcastLoopAsync(); _logger.LogInformation("Server běží. Stiskni Ctrl+C dvakrát pro ukončení."); // Just wait forever until cancellation - don't monitor individual tasks try { await Task.Delay(Timeout.Infinite, _cts.Token); } catch (OperationCanceledException) { _logger.LogInformation("Shutting down..."); } _statsDb.Dispose(); _encryption.Dispose(); _logger.LogInformation("Server ukončen"); } #region TCP Server static async Task RunTcpServerAsync() { var listener = new TcpListener(IPAddress.Any, _config.TcpPort); listener.Start(); _logger.LogInformation("TCP server naslouchá na portu {Port}", _config.TcpPort); try { while (!_cts.IsCancellationRequested) { try { var tcpClient = await listener.AcceptTcpClientAsync(_cts.Token); _ = HandleClientAsync(tcpClient); } catch (SocketException ex) { _logger.LogWarning(ex, "Socket error while accepting client"); } } } catch (OperationCanceledException) { _logger.LogInformation("TCP server ukončen (cancelled)"); } catch (Exception ex) { _logger.LogError(ex, "TCP server fatal error"); } finally { listener.Stop(); } } static async Task HandleClientAsync(TcpClient tcpClient) { var endpoint = tcpClient.Client.RemoteEndPoint?.ToString() ?? "unknown"; _logger.LogDebug("Nové spojení z {Endpoint}", endpoint); using var stream = tcpClient.GetStream(); ConnectedClient? client = null; try { // Handshake - plain JSON client = await PerformHandshakeAsync(stream, endpoint); if (client == null) { _logger.LogWarning("Handshake selhal pro {Endpoint}", endpoint); return; } _clients[client.ClientUuid] = client; _logger.LogInformation("Klient {Uuid} připojen z {Endpoint}", client.ClientUuid, endpoint); // Hlavní smyčka zpráv - šifrované await ProcessMessagesAsync(stream, client); } catch (Exception ex) when (ex is not OperationCanceledException) { _logger.LogError(ex, "Chyba při zpracování klienta {Endpoint}", endpoint); } finally { if (client != null) { _clients.TryRemove(client.ClientUuid, out _); // Odebrat z lobby pokud byl přihlášen if (client.Lobby != null) { client.Lobby.UnregisterConnection(client.ClientUuid); await client.Lobby.RemovePlayerAsync(client.ClientUuid, "Disconnected"); } client.Session?.Dispose(); _logger.LogInformation("Klient {Uuid} odpojen", client.ClientUuid); } tcpClient.Close(); } } static async Task PerformHandshakeAsync(NetworkStream stream, string endpoint) { // 1. Čekáme na ClientHello (plain) var helloData = await ReadMessageAsync(stream); if (helloData == null) return null; // Debug: log co přichází _logger.LogInformation("Handshake from {Endpoint}: {Length} bytes, first: 0x{First:X2}, content: {Content}", endpoint, helloData.Length, helloData[0], System.Text.Encoding.UTF8.GetString(helloData.AsSpan(0, Math.Min(200, helloData.Length)))); var hello = MessageSerializer.Deserialize(helloData) as ClientHello; if (hello == null) { _logger.LogWarning("Neplatný ClientHello z {Endpoint}", endpoint); return null; } // 2. Posíláme ServerHello s RSA public key (plain) var serverHello = new ServerHello { RsaPublicKeyPem = _encryption.PublicKeyPem, ServerId = Environment.MachineName }; await SendMessagePlainAsync(stream, serverHello); // 3. Čekáme na KeyExchange (plain - obsahuje RSA šifrovaný session key) var keyExData = await ReadMessageAsync(stream); if (keyExData == null) return null; // Debug: hex dump prvních 20 bajtů var hexDump = BitConverter.ToString(keyExData, 0, Math.Min(20, keyExData.Length)); _logger.LogInformation("KeyExchange from {Endpoint}: {Length} bytes, hex: {Hex}", endpoint, keyExData.Length, hexDump); var keyEx = MessageSerializer.Deserialize(keyExData) as KeyExchange; if (keyEx == null) { _logger.LogWarning("Neplatný KeyExchange z {Endpoint}", endpoint); return null; } // 4. Dešifrujeme session key byte[] sessionKey, sessionIv; try { (sessionKey, sessionIv) = _encryption.DecryptSessionKey(keyEx.EncryptedSessionKey, keyEx.EncryptedIV); } catch (Exception ex) { _logger.LogWarning(ex, "Chyba při dešifrování session key z {Endpoint}", endpoint); return null; } var session = new SessionCrypto(sessionKey, sessionIv); // 5. Posíláme KeyExchangeAck (šifrovaně) var ack = new KeyExchangeAck { Status = "success" }; await SendMessageEncryptedAsync(stream, ack, session); return new ConnectedClient { ClientUuid = hello.ClientUuid, DisplayName = hello.DisplayName ?? $"Player_{hello.ClientUuid[..4]}", Endpoint = endpoint, Session = session, Stream = stream }; } static async Task ProcessMessagesAsync(NetworkStream stream, ConnectedClient client) { int decryptFailures = 0; while (!_cts.IsCancellationRequested) { var encrypted = await ReadMessageAsync(stream); if (encrypted == null) break; // Dešifrovat var decrypted = client.Session!.Decrypt(encrypted); if (decrypted == null) { decryptFailures++; _logger.LogWarning("Dešifrování selhalo pro {Uuid} ({Count}x)", client.ClientUuid, decryptFailures); if (decryptFailures >= 3) { _logger.LogWarning("Příliš mnoho chyb dešifrování, odpojuji {Uuid}", client.ClientUuid); break; } continue; } decryptFailures = 0; var message = MessageSerializer.Deserialize(decrypted); if (message == null) { _logger.LogWarning("Neplatná zpráva od {Uuid}", client.ClientUuid); continue; } await HandleMessageAsync(client, message); } } static async Task HandleMessageAsync(ConnectedClient client, Message message) { switch (message) { case CreateLobby m: await HandleCreateLobbyAsync(client, m); break; case JoinLobby m: await HandleJoinLobbyAsync(client, m); break; case Reconnect m: await HandleReconnectAsync(client, m); break; default: // Předat do lobby pokud je přihlášen if (client.Lobby != null) { await client.Lobby.HandleMessageAsync(client.ClientUuid, message); } else { var error = new ErrorMessage { ErrorCode = "NOT_IN_LOBBY", ErrorText = "Nejsi v žádném lobby" }; await SendToClientAsync(client, error); } break; } } static async Task HandleCreateLobbyAsync(ConnectedClient client, CreateLobby message) { var (lobby, joinCode, error) = await _lobbyManager.CreateLobbyAsync( client.ClientUuid, client.DisplayName, message); var response = new CreateLobbyResponse { ClientSeq = message.ClientSeq, Success = lobby != null, JoinCode = joinCode, LobbyId = lobby?.Settings.LobbyId, Error = error, LobbyState = lobby?.GetLobbyState() }; if (lobby != null) { client.Lobby = lobby; lobby.RegisterConnection(client.ClientUuid, new ClientConnection { ClientUuid = client.ClientUuid, SendAsync = msg => SendToClientAsync(client, msg) }); } await SendToClientAsync(client, response); } static async Task HandleJoinLobbyAsync(ConnectedClient client, JoinLobby message) { var clientIp = client.Endpoint.Split(':')[0]; var (lobby, error) = await _lobbyManager.JoinLobbyAsync( clientIp, client.ClientUuid, client.DisplayName, message.JoinCode, message.Password); var response = new JoinLobbyResponse { ClientSeq = message.ClientSeq, Success = lobby != null, LobbyId = lobby?.Settings.LobbyId, Error = error, LobbyState = lobby?.GetLobbyState() }; if (lobby != null) { client.Lobby = lobby; lobby.RegisterConnection(client.ClientUuid, new ClientConnection { ClientUuid = client.ClientUuid, SendAsync = msg => SendToClientAsync(client, msg) }); } await SendToClientAsync(client, response); } static async Task HandleReconnectAsync(ConnectedClient client, Reconnect message) { var lobby = _lobbyManager.GetLobby(message.LobbyId); if (lobby == null) { await SendToClientAsync(client, new ReconnectResponse { Success = false, Error = "Lobby neexistuje" }); return; } // Načteme snapshot a eventy var snapshot = _persistence.LoadLatestSnapshot(message.LobbyId); var events = _persistence.ReadWal(message.LobbyId, message.LastEventId); client.Lobby = lobby; lobby.RegisterConnection(client.ClientUuid, new ClientConnection { ClientUuid = client.ClientUuid, SendAsync = msg => SendToClientAsync(client, msg) }); await lobby.AddPlayerAsync(client.ClientUuid, client.DisplayName, isOwner: false); await SendToClientAsync(client, new ReconnectResponse { Success = true, Snapshot = snapshot, MissedEvents = events }); } #endregion #region HTTP Stats API static async Task RunHttpServerAsync() { HttpListener? listener = null; bool isDocker = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true" || Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") == "Production"; // V Dockeru/produkci bindujeme na 0.0.0.0 (všechny interfaces) // Lokálně bindujeme na localhost (nevyžaduje admin práva na Windows) var prefixes = isDocker ? new[] { $"http://+:{_config.HttpPort}/", $"http://*:{_config.HttpPort}/" } : new[] { $"http://localhost:{_config.HttpPort}/", $"http://127.0.0.1:{_config.HttpPort}/" }; foreach (var prefixSet in new[] { prefixes }) { try { listener = new HttpListener(); foreach (var prefix in prefixSet) listener.Prefixes.Add(prefix); listener.Start(); _logger.LogInformation("HTTP Stats API naslouchá na portu {Port} (Docker: {IsDocker})", _config.HttpPort, isDocker); break; } catch (HttpListenerException ex) { _logger.LogWarning("Nelze spustit HTTP Stats API: {Error}", ex.Message); listener?.Close(); listener = null; } } if (listener == null) { _logger.LogWarning("HTTP Stats API není dostupné - nelze otevřít port {Port}. Server běží bez Stats API.", _config.HttpPort); // Just wait for cancellation instead of crashing try { await Task.Delay(Timeout.Infinite, _cts.Token); } catch (OperationCanceledException) { } return; } try { while (!_cts.IsCancellationRequested) { var context = await listener.GetContextAsync(); _ = HandleHttpRequestAsync(context); } } catch (HttpListenerException) when (_cts.IsCancellationRequested) { // Normal shutdown } catch (ObjectDisposedException) when (_cts.IsCancellationRequested) { // Normal shutdown } catch (Exception ex) { _logger.LogError(ex, "HTTP listener error"); } finally { listener?.Stop(); } } static async Task HandleHttpRequestAsync(HttpListenerContext context) { var request = context.Request; var response = context.Response; // CORS headers response.Headers.Add("Access-Control-Allow-Origin", "*"); response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); response.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization"); if (request.HttpMethod == "OPTIONS") { response.StatusCode = 200; response.Close(); return; } try { var path = request.Url?.AbsolutePath ?? "/"; // Admin panel routes if (path.StartsWith("/admin")) { await _adminPanel.HandleRequestAsync(context); return; } if (path.StartsWith("/stats/leaderboard")) { await HandleLeaderboardAsync(request, response); } else if (path.StartsWith("/stats/")) { var clientUuid = path[7..]; // Remove "/stats/" await HandlePlayerStatsAsync(clientUuid, response); } else if (path == "/health") { var uptime = DateTime.UtcNow - _startTime; await WriteJsonResponseAsync(response, new { status = "ok", version = "1.0.0", uptimeSeconds = (long)uptime.TotalSeconds, activeLobbies = _lobbyManager.LobbyCount, connectedPlayers = _lobbyManager.TotalPlayerCount }); } else { response.StatusCode = 404; await WriteJsonResponseAsync(response, new { error = "Not found" }); } response.Close(); } catch (Exception ex) { _logger.LogError(ex, "HTTP error"); try { response.StatusCode = 500; await WriteJsonResponseAsync(response, new { error = "Internal server error" }); response.Close(); } catch { /* Already closed */ } } } static async Task HandlePlayerStatsAsync(string clientUuid, HttpListenerResponse response) { var stats = _statsDb.GetPlayerStats(clientUuid); if (stats == null) { response.StatusCode = 404; await WriteJsonResponseAsync(response, new { error = "Player not found" }); return; } var result = new { clientUuid = stats.ClientUuid, displayName = stats.DisplayName, totalGames = stats.TotalGames, gamesAsCrew = stats.GamesAsCrew, gamesAsImpostor = stats.GamesAsImpostor, crewWins = stats.CrewWins, impostorWins = stats.ImpostorWins, crewWinRate = Math.Round(stats.CrewWinRate, 2), impostorWinRate = Math.Round(stats.ImpostorWinRate, 2), totalKills = stats.TotalKills, totalDeaths = stats.TotalDeaths, killDeathRatio = Math.Round(stats.KillDeathRatio, 2), tasksCompleted = stats.TasksCompleted, averageTasksPerGame = Math.Round(stats.AverageTasksPerGame, 2), bodiesReported = stats.BodiesReported, emergencyMeetingsCalled = stats.EmergencyMeetingsCalled, timesVotedOut = stats.TimesVotedOut, successfulVotes = stats.SuccessfulVotes, totalPlaytimeSeconds = stats.TotalPlaytimeSeconds, cheatIncidents = stats.CheatIncidents, lastSeen = stats.LastSeenUtc }; await WriteJsonResponseAsync(response, result); } static async Task HandleLeaderboardAsync(HttpListenerRequest request, HttpListenerResponse response) { var sortBy = request.QueryString["sortBy"] ?? "total_games"; var limitStr = request.QueryString["limit"] ?? "100"; var limit = int.TryParse(limitStr, out var l) ? l : 100; limit = Math.Min(limit, 1000); var leaderboard = _statsDb.GetLeaderboard(sortBy, limit); var result = leaderboard.Select(s => new { clientUuid = s.ClientUuid, displayName = s.DisplayName, totalGames = s.TotalGames, crewWins = s.CrewWins, impostorWins = s.ImpostorWins, totalKills = s.TotalKills, tasksCompleted = s.TasksCompleted }); await WriteJsonResponseAsync(response, result); } static async Task WriteJsonResponseAsync(HttpListenerResponse response, object data) { response.ContentType = "application/json"; var json = JsonSerializer.Serialize(data, JsonOptions.Default); var bytes = Encoding.UTF8.GetBytes(json); response.ContentLength64 = bytes.Length; await response.OutputStream.WriteAsync(bytes); } #endregion #region Background tasks static async Task RunCleanupLoopAsync() { try { using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5)); while (await timer.WaitForNextTickAsync(_cts.Token)) { try { await _lobbyManager.CleanupIdleLobbiesAsync(); } catch (Exception ex) { _logger.LogError(ex, "Chyba při cleanup"); } } } catch (OperationCanceledException) { // Normal shutdown } } static async Task RunBroadcastLoopAsync() { try { using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_config.PositionBroadcastRateMs)); while (await timer.WaitForNextTickAsync(_cts.Token)) { foreach (var client in _clients.Values) { client.Lobby?.BroadcastPositions(); } } } catch (OperationCanceledException) { // Normal shutdown } } #endregion #region Network helpers static async Task ReadMessageAsync(NetworkStream stream) { // 4 bytes length prefix (big-endian) var lengthBuffer = new byte[4]; var read = await stream.ReadAsync(lengthBuffer, 0, 4); if (read < 4) return null; if (BitConverter.IsLittleEndian) Array.Reverse(lengthBuffer); var length = BitConverter.ToInt32(lengthBuffer, 0); if (length <= 0 || length > _config.MaxPacketSizeBytes) return null; var buffer = new byte[length]; var totalRead = 0; while (totalRead < length) { read = await stream.ReadAsync(buffer, totalRead, length - totalRead); if (read == 0) return null; totalRead += read; } return buffer; } static async Task SendMessagePlainAsync(NetworkStream stream, Message message) { var data = MessageSerializer.Serialize(message); await SendDataAsync(stream, data); } static async Task SendMessageEncryptedAsync(NetworkStream stream, Message message, SessionCrypto session) { var plain = MessageSerializer.Serialize(message); var encrypted = session.Encrypt(plain); await SendDataAsync(stream, encrypted); } static async Task SendDataAsync(NetworkStream stream, byte[] data) { var lengthBuffer = BitConverter.GetBytes(data.Length); if (BitConverter.IsLittleEndian) Array.Reverse(lengthBuffer); await stream.WriteAsync(lengthBuffer); await stream.WriteAsync(data); await stream.FlushAsync(); } static async Task SendToClientAsync(ConnectedClient client, Message message) { if (client.Session == null || client.Stream == null) return; var plain = MessageSerializer.Serialize(message); var encrypted = client.Session.Encrypt(plain); lock (client.SendLock) { try { SendDataAsync(client.Stream, encrypted).Wait(); } catch (Exception ex) { _logger.LogWarning(ex, "Chyba při odesílání do {Uuid}", client.ClientUuid); } } } #endregion } // Connected client state class ConnectedClient { public required string ClientUuid { get; set; } public required string DisplayName { get; set; } public required string Endpoint { get; set; } public SessionCrypto? Session { get; set; } public NetworkStream? Stream { get; set; } public LobbyActor? Lobby { get; set; } public object SendLock { get; } = new(); }