Update configuration and enhance media processing: add stream token TTL and timeout settings in .env.example, improve error handling in media endpoints, and refactor command handlers for asynchronous operations. Update documentation to reflect current application state and features.
build / backend (push) Successful in 2m19s
build / frontend (push) Successful in 53s
tests / backend-tests (push) Successful in 2m27s

This commit is contained in:
Leonid Pershin
2026-07-25 23:14:55 +03:00
parent a261e261f0
commit 6c18a9da79
42 changed files with 387 additions and 98 deletions
@@ -1,6 +1,7 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Options;
using TeleWave.Application.Streaming;
using TeleWave.Infrastructure.Identity;
namespace TeleWave.Infrastructure.Streaming;
@@ -8,29 +9,37 @@ namespace TeleWave.Infrastructure.Streaming;
/// <summary>
/// Короткоживущий подписанный токен для cookie <c>tw_stream</c>: выдаётся авторизованному зрителю,
/// проверяется на запросах плейлиста и сегментов (работает и с нативным HLS, где заголовок не
/// поставить). Подпись — HMAC-SHA256 на том же ключе, что и JWT.
/// поставить). Подпись — HMAC-SHA256 на том же ключе, что и JWT. TTL короткий (см.
/// <see cref="StreamingOptions.StreamTokenMinutes"/>), а <see cref="Validate"/> возвращает id зрителя —
/// эндпоинт плейлиста дополнительно сверяет, что пользователь не заблокирован.
/// </summary>
public sealed class StreamTokenService(IOptions<JwtOptions> jwtOptions)
public sealed class StreamTokenService(
IOptions<JwtOptions> jwtOptions,
IOptions<StreamingOptions> streamingOptions
)
{
private static readonly TimeSpan Ttl = TimeSpan.FromHours(6);
private readonly TimeSpan _ttl = TimeSpan.FromMinutes(
Math.Max(1, streamingOptions.Value.StreamTokenMinutes)
);
private readonly byte[] _key = Encoding.UTF8.GetBytes(jwtOptions.Value.SigningKey);
public (string Token, DateTimeOffset ExpiresAt) Issue(Guid userId)
{
var expiresAt = DateTimeOffset.UtcNow.Add(Ttl);
var expiresAt = DateTimeOffset.UtcNow.Add(_ttl);
var payload = $"{userId:N}.{expiresAt.ToUnixTimeSeconds()}";
var token = $"{Base64Url(Encoding.UTF8.GetBytes(payload))}.{Base64Url(Sign(payload))}";
return (token, expiresAt);
}
public bool Validate(string? token)
/// <summary>Проверяет подпись и срок; при успехе возвращает id зрителя из токена, иначе null.</summary>
public Guid? Validate(string? token)
{
if (string.IsNullOrEmpty(token))
return false;
return null;
var parts = token.Split('.');
if (parts.Length != 2)
return false;
return null;
byte[] payloadBytes;
byte[] signature;
@@ -41,19 +50,22 @@ public sealed class StreamTokenService(IOptions<JwtOptions> jwtOptions)
}
catch (FormatException)
{
return false;
return null;
}
var payload = Encoding.UTF8.GetString(payloadBytes);
var expected = Sign(payload);
if (!CryptographicOperations.FixedTimeEquals(signature, expected))
return false;
return null;
var dot = payload.LastIndexOf('.');
if (dot < 0 || !long.TryParse(payload.AsSpan(dot + 1), out var expiresUnix))
return false;
return null;
return DateTimeOffset.FromUnixTimeSeconds(expiresUnix) > DateTimeOffset.UtcNow;
if (DateTimeOffset.FromUnixTimeSeconds(expiresUnix) <= DateTimeOffset.UtcNow)
return null;
return Guid.TryParseExact(payload[..dot], "N", out var userId) ? userId : null;
}
private byte[] Sign(string payload) =>