using System.Net; using System.Text.Json; namespace MusicBridge.Agent.Network; // QR payload contract only. Rendering/scanning and the native client are separate steps. internal sealed record ConnectionInvitation(int Version, string Endpoint, string CertificateSha256, string Code, DateTimeOffset ExpiresAt) { public static ConnectionInvitation Create(Uri endpoint, string fingerprint, PairingWindow window, DateTimeOffset now) { if (!IsLocalEndpoint(endpoint)) throw new ArgumentException("Нужен HTTPS IPv4-адрес домашней сети без пути и параметров."); if (fingerprint.Length != 64 || !fingerprint.All(Uri.IsHexDigit)) throw new ArgumentException("Неверный отпечаток сертификата."); if (window.Code.Length != 8 || !window.Code.All(char.IsAsciiDigit) || window.ExpiresAt <= now) throw new ArgumentException("Окно сопряжения закрыто или код неверен."); return new(1, endpoint.GetLeftPart(UriPartial.Authority), fingerprint.ToUpperInvariant(), window.Code, window.ExpiresAt); } public string ToQrPayload() { var json = JsonSerializer.SerializeToUtf8Bytes(this, new JsonSerializerOptions(JsonSerializerDefaults.Web)); var data = Convert.ToBase64String(json).TrimEnd('=').Replace('+', '-').Replace('/', '_'); return "musicbridge://pair?data=" + data; } public static bool IsLocalEndpoint(Uri endpoint) { if (!endpoint.IsAbsoluteUri || endpoint.Scheme != "https" || endpoint.UserInfo != "" || endpoint.AbsolutePath != "/" || endpoint.Query != "" || endpoint.Fragment != "" || !IPAddress.TryParse(endpoint.Host, out var ip)) return false; var bytes = ip.GetAddressBytes(); return bytes.Length == 4 && (bytes[0] == 10 || bytes[0] == 172 && bytes[1] is >= 16 and <= 31 || bytes[0] == 192 && bytes[1] == 168); } }