namespace LarpixVoice; public class Utils { public static int PORT = 8091; public static int MAIN_PORT = 8090; public static string SERVER_KEY = "SecretThingOMGSoHardToGuess"; public static string MAIN_URL = ""; // Rate limits, requests per minute (fixed 60s window). public static int ROOM_USERS_RATE_LIMIT = 120; // Shared with the main server: authenticates internal validation requests. public static string VOICE_KEY = ""; private static readonly System.Net.Http.HttpClient AuthHttp = new() { Timeout = TimeSpan.FromSeconds(3) }; public static void LoadEnv() { if (File.Exists(".env")) { foreach (var line in File.ReadAllLines(".env")) { var trimmed = line.Trim(); if (string.IsNullOrWhiteSpace(trimmed) || trimmed.StartsWith("#")) continue; var parts = trimmed.Split('=', 2, StringSplitOptions.None); if (parts.Length == 2) { var key = parts[0].Trim(); var val = parts[1]; int commentIndex = val.IndexOf('#'); if (commentIndex >= 0) { val = val.Substring(0, commentIndex); } val = val.Trim(); Environment.SetEnvironmentVariable(key, val); } } } string? port = Environment.GetEnvironmentVariable("PORT_VOICE"); if (port != null) { PORT = int.Parse(port); } port = Environment.GetEnvironmentVariable("PORT"); if (port != null) { MAIN_PORT = int.Parse(port); } SERVER_KEY = Environment.GetEnvironmentVariable("SERVER_KEY") ?? SERVER_KEY; VOICE_KEY = Environment.GetEnvironmentVariable("VOICE_KEY") ?? VOICE_KEY; MAIN_URL = Environment.GetEnvironmentVariable("MAIN_URL") ?? $"http://127.0.0.1:{MAIN_PORT}"; string? roomUsersRateLimitStr = Environment.GetEnvironmentVariable("ROOM_USERS_RATE_LIMIT"); if (roomUsersRateLimitStr != null && int.TryParse(roomUsersRateLimitStr, out int parsedRoomUsersLimit)) { ROOM_USERS_RATE_LIMIT = parsedRoomUsersLimit; } } // Asks the main server to validate (and atomically consume) the ticket // blob presented on a connection's first frame. Fail-closed: any network // problem, bad HMAC, timeout or "deny" response rejects the connection. public static async Task AuthorizeConnection(string publicId, string iv, string ct, string roomId, ulong userId) { if (string.IsNullOrEmpty(VOICE_KEY) || string.IsNullOrEmpty(MAIN_URL)) return false; try { long ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); string body = System.Text.Json.JsonSerializer.Serialize( new VoiceAuthBody(roomId, userId.ToString(), publicId, iv, ct), JsonAppSerializerContext.Default.VoiceAuthBody ); byte[] keyBytes = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(VOICE_KEY)); using var hmac = new System.Security.Cryptography.HMACSHA256(keyBytes); string mac = System.Buffers.Text.Base64Url.EncodeToString( hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes($"{ts}.{body}")) ); using var request = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Post, $"{MAIN_URL}/_larpix/voice/auth"); request.Headers.Add("X-Voice-Ts", ts.ToString()); request.Headers.Add("X-Voice-Mac", mac); request.Content = new System.Net.Http.StringContent(body, System.Text.Encoding.UTF8, "application/json"); using System.Net.Http.HttpResponseMessage response = await AuthHttp.SendAsync(request); if (!response.IsSuccessStatusCode) { Console.WriteLine($"Voice auth: main HTTP {(int)response.StatusCode}"); return false; } string content = await response.Content.ReadAsStringAsync(); if (content.Trim() != "ok") { Console.WriteLine($"Voice auth: main denied ({content.Trim()})"); return false; } return true; } catch (Exception ex) { Console.WriteLine($"Voice auth: validation request failed ({ex.Message})"); return false; } } }