Enhance configuration and routing for streaming features: update .env.example and appsettings.json to include new scheduler and streaming options, add streaming endpoint mapping in Program.cs, and integrate streaming services in DependencyInjection. Introduce segment path resolution in MediaPathResolver for asset management.

This commit is contained in:
Leonid Pershin
2026-07-24 16:07:37 +03:00
parent 4fa9dae37f
commit 1bbfd15907
18 changed files with 686 additions and 0 deletions
@@ -7,11 +7,13 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using TeleWave.Application.Broadcast.Scheduling;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Streaming;
using TeleWave.Domain.Broadcast.Scheduling;
using TeleWave.Infrastructure.Broadcast;
using TeleWave.Infrastructure.Identity;
using TeleWave.Infrastructure.Media;
using TeleWave.Infrastructure.Persistence;
using TeleWave.Infrastructure.Streaming;
namespace TeleWave.Infrastructure;
@@ -99,8 +101,10 @@ public static class DependencyInjection
private static void AddBroadcast(IServiceCollection services, IConfiguration configuration)
{
services.Configure<SchedulerOptions>(configuration.GetSection(SchedulerOptions.SectionName));
services.Configure<StreamingOptions>(configuration.GetSection(StreamingOptions.SectionName));
services.AddSingleton<IRandomSource, SystemRandomSource>();
services.AddSingleton<StreamTokenService>();
services.AddScoped<ScheduleGenerator>();
services.AddHostedService<SchedulingBackgroundService>();
}
@@ -40,6 +40,13 @@ public sealed class MediaPathResolver
public string AssetRelativePath(Guid assetId) => $"assets/{assetId:N}";
/// <summary>Путь к файлу сегмента внутри каталога ассета (имя файла проверяется на traversal).</summary>
public string SegmentPath(Guid assetId, string fileName)
{
var assetDir = AssetDir(assetId);
return EnsureWithin(assetDir, Path.Combine(assetDir, fileName));
}
/// <summary>Резолвит имя файла внутри uploads/ (токен загрузки), проверяя выход за пределы каталога.</summary>
public string UploadPath(string token) =>
EnsureWithin(UploadsDir, Path.Combine(UploadsDir, token));
@@ -0,0 +1,76 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Options;
using TeleWave.Infrastructure.Identity;
namespace TeleWave.Infrastructure.Streaming;
/// <summary>
/// Короткоживущий подписанный токен для cookie <c>tw_stream</c>: выдаётся авторизованному зрителю,
/// проверяется на запросах плейлиста и сегментов (работает и с нативным HLS, где заголовок не
/// поставить). Подпись — HMAC-SHA256 на том же ключе, что и JWT.
/// </summary>
public sealed class StreamTokenService(IOptions<JwtOptions> jwtOptions)
{
private static readonly TimeSpan Ttl = TimeSpan.FromHours(6);
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 payload = $"{userId:N}.{expiresAt.ToUnixTimeSeconds()}";
var token = $"{Base64Url(Encoding.UTF8.GetBytes(payload))}.{Base64Url(Sign(payload))}";
return (token, expiresAt);
}
public bool Validate(string? token)
{
if (string.IsNullOrEmpty(token))
return false;
var parts = token.Split('.');
if (parts.Length != 2)
return false;
byte[] payloadBytes;
byte[] signature;
try
{
payloadBytes = FromBase64Url(parts[0]);
signature = FromBase64Url(parts[1]);
}
catch (FormatException)
{
return false;
}
var payload = Encoding.UTF8.GetString(payloadBytes);
var expected = Sign(payload);
if (!CryptographicOperations.FixedTimeEquals(signature, expected))
return false;
var dot = payload.LastIndexOf('.');
if (dot < 0 || !long.TryParse(payload.AsSpan(dot + 1), out var expiresUnix))
return false;
return DateTimeOffset.FromUnixTimeSeconds(expiresUnix) > DateTimeOffset.UtcNow;
}
private byte[] Sign(string payload) =>
HMACSHA256.HashData(_key, Encoding.UTF8.GetBytes(payload));
private static string Base64Url(byte[] bytes) =>
Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
private static byte[] FromBase64Url(string value)
{
var padded = value.Replace('-', '+').Replace('_', '/');
padded += (padded.Length % 4) switch
{
2 => "==",
3 => "=",
_ => "",
};
return Convert.FromBase64String(padded);
}
}