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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user