diff --git a/LightlessAPI b/LightlessAPI index 4ce70be..44fbe10 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 4ce70bee8354d0c96d73e65312d39a826810dc60 +Subproject commit 44fbe1045872fcae4df45e43625a9ff1a79bc2ef diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ServerFilesController.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ServerFilesController.cs index a81ca17..d0b5d95 100644 --- a/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ServerFilesController.cs +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ServerFilesController.cs @@ -10,6 +10,7 @@ using LightlessSyncShared.Services; using LightlessSyncShared.Utils.Configuration; using LightlessSyncStaticFilesServer.Services; using LightlessSyncStaticFilesServer.Utils; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; @@ -32,12 +33,15 @@ public class ServerFilesController : ControllerBase private readonly IDbContextFactory _lightlessDbContext; private readonly LightlessMetrics _metricsClient; private readonly MainServerShardRegistrationService _shardRegistrationService; + private readonly CDNDownloadUrlService _cdnDownloadUrlService; + private readonly CDNDownloadsService _cdnDownloadsService; public ServerFilesController(ILogger logger, CachedFileProvider cachedFileProvider, IConfigurationService configuration, IHubContext hubContext, IDbContextFactory lightlessDbContext, LightlessMetrics metricsClient, - MainServerShardRegistrationService shardRegistrationService) : base(logger) + MainServerShardRegistrationService shardRegistrationService, CDNDownloadUrlService cdnDownloadUrlService, + CDNDownloadsService cdnDownloadsService) : base(logger) { _basePath = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false) ? configuration.GetValue(nameof(StaticFilesServerConfiguration.ColdStorageDirectory)) @@ -48,6 +52,8 @@ public class ServerFilesController : ControllerBase _lightlessDbContext = lightlessDbContext; _metricsClient = metricsClient; _shardRegistrationService = shardRegistrationService; + _cdnDownloadUrlService = cdnDownloadUrlService; + _cdnDownloadsService = cdnDownloadsService; } [HttpPost(LightlessFiles.ServerFiles_DeleteAll)] @@ -105,6 +111,16 @@ public class ServerFilesController : ControllerBase baseUrl = shard.Value ?? _configuration.GetValue(nameof(StaticFilesServerConfiguration.CdnFullUrl)); } + var cdnDownloadUrl = string.Empty; + if (forbiddenFile == null) + { + var directUri = _cdnDownloadUrlService.TryCreateDirectDownloadUri(baseUrl, file.Hash); + if (directUri != null) + { + cdnDownloadUrl = directUri.ToString(); + } + } + response.Add(new DownloadFileDto { FileExists = file.Size > 0, @@ -113,6 +129,7 @@ public class ServerFilesController : ControllerBase Hash = file.Hash, Size = file.Size, Url = baseUrl?.ToString() ?? string.Empty, + CDNDownloadUrl = cdnDownloadUrl, RawSize = file.RawSize }); } @@ -127,6 +144,22 @@ public class ServerFilesController : ControllerBase return Ok(JsonSerializer.Serialize(allFileShards.SelectMany(t => t.RegionUris.Select(v => v.Value.ToString())))); } + [HttpGet(LightlessFiles.ServerFiles_DirectDownload + "/{hash}")] + [AllowAnonymous] + public async Task DownloadFileDirect(string hash, [FromQuery] long expires, [FromQuery] string signature) + { + var result = await _cdnDownloadsService.GetDownloadAsync(hash, expires, signature).ConfigureAwait(false); + + return result.Status switch + { + CDNDownloadsService.ResultStatus.Disabled => NotFound(), + CDNDownloadsService.ResultStatus.Unauthorized => Unauthorized(), + CDNDownloadsService.ResultStatus.NotFound => NotFound(), + CDNDownloadsService.ResultStatus.Success => PhysicalFile(result.File!.FullName, "application/octet-stream"), + _ => NotFound() + }; + } + [HttpPost(LightlessFiles.ServerFiles_FilesSend)] public async Task FilesSend([FromBody] FilesSendDto filesSendDto) { @@ -360,4 +393,4 @@ public class ServerFilesController : ControllerBase buffer[i] ^= 42; } } -} \ No newline at end of file +} diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ShardServerFilesController.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ShardServerFilesController.cs new file mode 100644 index 0000000..819597e --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ShardServerFilesController.cs @@ -0,0 +1,34 @@ +using LightlessSync.API.Routes; +using LightlessSyncStaticFilesServer.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace LightlessSyncStaticFilesServer.Controllers; + +[Route(LightlessFiles.ServerFiles)] +public class ShardServerFilesController : ControllerBase +{ + private readonly CDNDownloadsService _cdnDownloadsService; + + public ShardServerFilesController(ILogger logger, + CDNDownloadsService cdnDownloadsService) : base(logger) + { + _cdnDownloadsService = cdnDownloadsService; + } + + [HttpGet(LightlessFiles.ServerFiles_DirectDownload + "/{hash}")] + [AllowAnonymous] + public async Task DownloadFileDirect(string hash, [FromQuery] long expires, [FromQuery] string signature) + { + var result = await _cdnDownloadsService.GetDownloadAsync(hash, expires, signature).ConfigureAwait(false); + + return result.Status switch + { + CDNDownloadsService.ResultStatus.Disabled => NotFound(), + CDNDownloadsService.ResultStatus.Unauthorized => Unauthorized(), + CDNDownloadsService.ResultStatus.NotFound => NotFound(), + CDNDownloadsService.ResultStatus.Success => PhysicalFile(result.File!.FullName, "application/octet-stream"), + _ => NotFound() + }; + } +} diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/CDNDownloadsService.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/CDNDownloadsService.cs new file mode 100644 index 0000000..3cbd661 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/CDNDownloadsService.cs @@ -0,0 +1,56 @@ +using System.IO; +using System.Threading.Tasks; + +namespace LightlessSyncStaticFilesServer.Services; + +public class CDNDownloadsService +{ + public enum ResultStatus + { + Disabled, + Unauthorized, + NotFound, + Success + } + + public readonly record struct Result(ResultStatus Status, FileInfo? File); + + private readonly CDNDownloadUrlService _cdnDownloadUrlService; + private readonly CachedFileProvider _cachedFileProvider; + + public CDNDownloadsService(CDNDownloadUrlService cdnDownloadUrlService, CachedFileProvider cachedFileProvider) + { + _cdnDownloadUrlService = cdnDownloadUrlService; + _cachedFileProvider = cachedFileProvider; + } + + public bool DownloadsEnabled => _cdnDownloadUrlService.DirectDownloadsEnabled; + + public async Task GetDownloadAsync(string hash, long expiresUnixSeconds, string signature) + { + if (!_cdnDownloadUrlService.DirectDownloadsEnabled) + { + return new Result(ResultStatus.Disabled, null); + } + + if (string.IsNullOrEmpty(signature) || string.IsNullOrEmpty(hash)) + { + return new Result(ResultStatus.Unauthorized, null); + } + + hash = hash.ToUpperInvariant(); + + if (!_cdnDownloadUrlService.TryValidateSignature(hash, expiresUnixSeconds, signature)) + { + return new Result(ResultStatus.Unauthorized, null); + } + + var fileInfo = await _cachedFileProvider.DownloadAndGetLocalFileInfo(hash).ConfigureAwait(false); + if (fileInfo == null) + { + return new Result(ResultStatus.NotFound, null); + } + + return new Result(ResultStatus.Success, fileInfo); + } +} diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Startup.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Startup.cs index 1ed3ec3..a3f2469 100644 --- a/LightlessSyncServer/LightlessSyncStaticFilesServer/Startup.cs +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Startup.cs @@ -87,6 +87,8 @@ public class Startup services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddHostedService(p => p.GetService()); services.AddHostedService(m => m.GetService()); services.AddSingleton, LightlessConfigurationServiceClient>(); @@ -204,7 +206,8 @@ public class Startup } else if (_isDistributionNode) { - a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(CacheController), typeof(RequestController), typeof(DistributionController), typeof(SpeedTestController))); + a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(CacheController), typeof(RequestController), + typeof(DistributionController), typeof(ShardServerFilesController), typeof(SpeedTestController))); } else {