cdn downloads support

This commit is contained in:
azyges
2025-10-10 07:37:33 +09:00
parent 479b80a5a0
commit b6907a2704
6 changed files with 157 additions and 4 deletions

View File

@@ -0,0 +1,108 @@
using LightlessSync.API.Routes;
using LightlessSyncShared.Services;
using LightlessSyncShared.Utils.Configuration;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.WebUtilities;
using System.Security.Cryptography;
using System.Text;
using System.Globalization;
using System.Linq;
namespace LightlessSyncStaticFilesServer.Services;
public class CDNDownloadUrlService
{
private readonly IConfigurationService<StaticFilesServerConfiguration> _staticConfig;
private readonly IConfigurationService<LightlessConfigurationBase> _globalConfig;
private readonly ILogger<CDNDownloadUrlService> _logger;
public CDNDownloadUrlService(IConfigurationService<StaticFilesServerConfiguration> staticConfig,
IConfigurationService<LightlessConfigurationBase> globalConfig, ILogger<CDNDownloadUrlService> logger)
{
_staticConfig = staticConfig;
_globalConfig = globalConfig;
_logger = logger;
}
public bool DirectDownloadsEnabled =>
_staticConfig.GetValueOrDefault(nameof(StaticFilesServerConfiguration.EnableDirectDownloads), false);
public Uri? TryCreateDirectDownloadUri(Uri? baseUri, string hash)
{
if (!DirectDownloadsEnabled || baseUri == null)
{
return null;
}
if (!IsSupportedHash(hash))
{
_logger.LogDebug("Skipping direct download link generation for invalid hash {hash}", hash);
return null;
}
var normalizedHash = hash.ToUpperInvariant();
var lifetimeSeconds = Math.Max(5,
_staticConfig.GetValueOrDefault(nameof(StaticFilesServerConfiguration.DirectDownloadTokenLifetimeSeconds), 300));
var expiresAt = DateTimeOffset.UtcNow.AddSeconds(lifetimeSeconds);
var signature = CreateSignature(normalizedHash, expiresAt.ToUnixTimeSeconds());
var directPath = $"{LightlessFiles.ServerFiles}/{LightlessFiles.ServerFiles_DirectDownload}/{normalizedHash}";
var builder = new UriBuilder(new Uri(baseUri, directPath));
var query = new QueryBuilder
{
{ "expires", expiresAt.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture) },
{ "signature", signature }
};
builder.Query = query.ToQueryString().Value!.TrimStart('?');
return builder.Uri;
}
public bool TryValidateSignature(string hash, long expiresUnixSeconds, string signature)
{
if (!DirectDownloadsEnabled)
{
return false;
}
if (string.IsNullOrEmpty(signature) || !IsSupportedHash(hash))
{
return false;
}
var normalizedHash = hash.ToUpperInvariant();
DateTimeOffset expiresAt;
try
{
expiresAt = DateTimeOffset.FromUnixTimeSeconds(expiresUnixSeconds);
}
catch (ArgumentOutOfRangeException)
{
return false;
}
if (expiresAt < DateTimeOffset.UtcNow)
{
return false;
}
var expected = CreateSignature(normalizedHash, expiresAt.ToUnixTimeSeconds());
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(signature));
}
private string CreateSignature(string hash, long expiresUnixSeconds)
{
var signingKey = _globalConfig.GetValue<string>(nameof(LightlessConfigurationBase.Jwt));
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(signingKey));
var payload = Encoding.UTF8.GetBytes($"{hash}:{expiresUnixSeconds}");
return WebEncoders.Base64UrlEncode(hmac.ComputeHash(payload));
}
private static bool IsSupportedHash(string hash)
{
return hash.Length == 40 && hash.All(char.IsAsciiLetterOrDigit);
}
}