using System; using System.Security.Cryptography; using System.Text; namespace Jellyfin.Plugin.Floatplane.Api; /// /// Signs the URLs the channel hands to Jellyfin's ffmpeg, so the anonymous stream /// endpoints only serve what this plugin selected. The secret lives for the process: /// media sources are handed out per playback, so a restart just means fresh signatures. /// public static class ProxySignature { public const string QueryParameter = "t"; private static readonly byte[] Secret = RandomNumberGenerator.GetBytes(32); public static string Create(params string[] parts) { var payload = string.Join('\n', parts); var hash = HMACSHA256.HashData(Secret, Encoding.UTF8.GetBytes(payload)); return Convert.ToHexString(hash).ToLowerInvariant(); } public static bool Verify(string? signature, params string[] parts) { if (string.IsNullOrEmpty(signature)) { return false; } var expected = Create(parts); return CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(signature)); } /// URL-safe base64 without padding (for embedding upstream URLs in proxy URLs). public static string EncodeUrl(string value) => Convert.ToBase64String(Encoding.UTF8.GetBytes(value)).TrimEnd('=').Replace('+', '-').Replace('/', '_'); public static string? DecodeUrl(string? encoded) { if (string.IsNullOrEmpty(encoded)) { return null; } var s = encoded.Replace('-', '+').Replace('_', '/'); switch (s.Length % 4) { case 2: s += "=="; break; case 3: s += "="; break; } try { return Encoding.UTF8.GetString(Convert.FromBase64String(s)); } catch (FormatException) { return null; } } }