109 lines
3.7 KiB
C#
109 lines
3.7 KiB
C#
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);
|
|
}
|
|
}
|