diff --git a/VoidCat/Controllers/UserController.cs b/VoidCat/Controllers/UserController.cs index fe03a6a..7cbc06d 100644 --- a/VoidCat/Controllers/UserController.cs +++ b/VoidCat/Controllers/UserController.cs @@ -28,7 +28,9 @@ public class UserController : Controller var requestedId = isMe ? loggedUser!.Value : id.FromBase58Guid(); if (loggedUser == requestedId) { - return Json(await _store.Get(requestedId)); + var pUser = await _store.Get(requestedId); + if (pUser == default) return NotFound(); + return Json(pUser); } var user = await _store.Get(requestedId); @@ -94,7 +96,7 @@ public class UserController : Controller if (!await _emailVerification.VerifyCode(user, token)) return BadRequest(); user.Flags |= VoidUserFlags.EmailVerified; - await _store.Set(user); + await _store.Set(user.Id, user); return Accepted(); } diff --git a/VoidCat/Program.cs b/VoidCat/Program.cs index d8217a6..1d6b6a7 100644 --- a/VoidCat/Program.cs +++ b/VoidCat/Program.cs @@ -15,6 +15,7 @@ using VoidCat.Services.Paywall; using VoidCat.Services.Redis; using VoidCat.Services.Stats; using VoidCat.Services.Users; +using VoidCat.Services.VirusScanner; // setup JsonConvert default settings JsonSerializerSettings ConfigJsonSettings(JsonSerializerSettings s) @@ -99,6 +100,9 @@ services.AddTransient(); // background services services.AddHostedService(); +// virus scanner +services.AddVirusScanner(voidSettings); + if (useRedis) { services.AddTransient(); diff --git a/VoidCat/Services/Abstractions/IBasicStore.cs b/VoidCat/Services/Abstractions/IBasicStore.cs new file mode 100644 index 0000000..20eca7d --- /dev/null +++ b/VoidCat/Services/Abstractions/IBasicStore.cs @@ -0,0 +1,12 @@ +namespace VoidCat.Services.Abstractions; + +public interface IBasicStore +{ + ValueTask Get(Guid id); + + ValueTask Set(Guid id, T obj); + + ValueTask Delete(Guid id); + + string MapKey(Guid id); +} \ No newline at end of file diff --git a/VoidCat/Services/Abstractions/IFileMetadataStore.cs b/VoidCat/Services/Abstractions/IFileMetadataStore.cs index e9061a6..1ff27ef 100644 --- a/VoidCat/Services/Abstractions/IFileMetadataStore.cs +++ b/VoidCat/Services/Abstractions/IFileMetadataStore.cs @@ -2,11 +2,7 @@ using VoidCat.Model; namespace VoidCat.Services.Abstractions; -public interface IFileMetadataStore +public interface IFileMetadataStore : IPublicPrivateStore { ValueTask Get(Guid id) where TMeta : VoidFileMeta; - - ValueTask Set(Guid id, SecretVoidFileMeta meta); - - ValueTask Delete(Guid id); } diff --git a/VoidCat/Services/Abstractions/IFileStore.cs b/VoidCat/Services/Abstractions/IFileStore.cs index 14df267..3047ef3 100644 --- a/VoidCat/Services/Abstractions/IFileStore.cs +++ b/VoidCat/Services/Abstractions/IFileStore.cs @@ -11,4 +11,6 @@ public interface IFileStore ValueTask> ListFiles(PagedRequest request); ValueTask DeleteFile(Guid id); + + ValueTask Open(EgressRequest request, CancellationToken cts); } \ No newline at end of file diff --git a/VoidCat/Services/Abstractions/IPaywallStore.cs b/VoidCat/Services/Abstractions/IPaywallStore.cs index d7bc1ac..be182e5 100644 --- a/VoidCat/Services/Abstractions/IPaywallStore.cs +++ b/VoidCat/Services/Abstractions/IPaywallStore.cs @@ -2,12 +2,8 @@ using VoidCat.Model.Paywall; namespace VoidCat.Services.Abstractions; -public interface IPaywallStore +public interface IPaywallStore : IBasicStore { ValueTask GetOrder(Guid id); ValueTask SaveOrder(PaywallOrder order); - - ValueTask Get(Guid id); - ValueTask Set(Guid id, PaywallConfig config); - ValueTask Delete(Guid id); } \ No newline at end of file diff --git a/VoidCat/Services/Abstractions/IPublicPrivateStore.cs b/VoidCat/Services/Abstractions/IPublicPrivateStore.cs new file mode 100644 index 0000000..f2ca45f --- /dev/null +++ b/VoidCat/Services/Abstractions/IPublicPrivateStore.cs @@ -0,0 +1,10 @@ +namespace VoidCat.Services.Abstractions; + +public interface IPublicPrivateStore +{ + ValueTask Get(Guid id); + + ValueTask Set(Guid id, TPrivate obj); + + ValueTask Delete(Guid id); +} \ No newline at end of file diff --git a/VoidCat/Services/Abstractions/IUserStore.cs b/VoidCat/Services/Abstractions/IUserStore.cs index 1b8f18c..bed6d6a 100644 --- a/VoidCat/Services/Abstractions/IUserStore.cs +++ b/VoidCat/Services/Abstractions/IUserStore.cs @@ -2,12 +2,12 @@ namespace VoidCat.Services.Abstractions; -public interface IUserStore +public interface IUserStore : IPublicPrivateStore { - ValueTask LookupUser(string email); ValueTask Get(Guid id) where T : VoidUser; - ValueTask Set(InternalVoidUser user); + ValueTask Delete(PrivateVoidUser user); + + ValueTask LookupUser(string email); ValueTask> ListUsers(PagedRequest request); ValueTask UpdateProfile(PublicVoidUser newUser); - ValueTask Delete(PrivateVoidUser user); } \ No newline at end of file diff --git a/VoidCat/Services/Background/DeleteUnverifiedAccounts.cs b/VoidCat/Services/Background/DeleteUnverifiedAccounts.cs index 4f6876c..9a55d96 100644 --- a/VoidCat/Services/Background/DeleteUnverifiedAccounts.cs +++ b/VoidCat/Services/Background/DeleteUnverifiedAccounts.cs @@ -34,6 +34,7 @@ public class DeleteUnverifiedAccounts : BackgroundService await _userStore.Delete(account); var files = await _userUploads.ListFiles(account.Id, new(0, Int32.MinValue)); + // ReSharper disable once UseCancellationTokenForIAsyncEnumerable await foreach (var file in files.Results) { await _fileStore.DeleteFile(file.Id); diff --git a/VoidCat/Services/BasicCacheStore.cs b/VoidCat/Services/BasicCacheStore.cs new file mode 100644 index 0000000..908cfe9 --- /dev/null +++ b/VoidCat/Services/BasicCacheStore.cs @@ -0,0 +1,30 @@ +using VoidCat.Services.Abstractions; + +namespace VoidCat.Services; + +public abstract class BasicCacheStore : IBasicStore +{ + protected readonly ICache _cache; + + protected BasicCacheStore(ICache cache) + { + _cache = cache; + } + + public virtual ValueTask Get(Guid id) + { + return _cache.Get(MapKey(id)); + } + + public virtual ValueTask Set(Guid id, TStore obj) + { + return _cache.Set(MapKey(id), obj); + } + + public virtual ValueTask Delete(Guid id) + { + return _cache.Delete(MapKey(id)); + } + + public abstract string MapKey(Guid id); +} \ No newline at end of file diff --git a/VoidCat/Services/Files/LocalDiskFileMetadataStore.cs b/VoidCat/Services/Files/LocalDiskFileMetadataStore.cs index e87a739..786693a 100644 --- a/VoidCat/Services/Files/LocalDiskFileMetadataStore.cs +++ b/VoidCat/Services/Files/LocalDiskFileMetadataStore.cs @@ -28,6 +28,11 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore return GetMeta(id); } + public ValueTask Get(Guid id) + { + return GetMeta(id); + } + public async ValueTask Set(Guid id, SecretVoidFileMeta meta) { var path = MapMeta(id); diff --git a/VoidCat/Services/Files/LocalDiskFileStorage.cs b/VoidCat/Services/Files/LocalDiskFileStorage.cs index 5ee8ce4..6329223 100644 --- a/VoidCat/Services/Files/LocalDiskFileStorage.cs +++ b/VoidCat/Services/Files/LocalDiskFileStorage.cs @@ -30,10 +30,7 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore public async ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts) { - var path = MapPath(request.Id); - if (!File.Exists(path)) throw new VoidFileNotFoundException(request.Id); - - await using var fs = new FileStream(path, FileMode.Open, FileAccess.Read); + await using var fs = await Open(request, cts); await EgressFromStream(fs, request, outStream, cts); } @@ -100,6 +97,14 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore await _metadataStore.Delete(id); } + public ValueTask Open(EgressRequest request, CancellationToken cts) + { + var path = MapPath(request.Id); + if (!File.Exists(path)) throw new VoidFileNotFoundException(request.Id); + + return ValueTask.FromResult(new FileStream(path, FileMode.Open, FileAccess.Read)); + } + private string MapPath(Guid id) => Path.Join(_settings.DataDirectory, FilesDir, id.ToString()); } \ No newline at end of file diff --git a/VoidCat/Services/Files/S3FileMetadataStore.cs b/VoidCat/Services/Files/S3FileMetadataStore.cs index 91a0824..c340a8e 100644 --- a/VoidCat/Services/Files/S3FileMetadataStore.cs +++ b/VoidCat/Services/Files/S3FileMetadataStore.cs @@ -20,7 +20,33 @@ public class S3FileMetadataStore : IFileMetadataStore _client = _config.CreateClient(); } - public async ValueTask Get(Guid id) where TMeta : VoidFileMeta + public ValueTask Get(Guid id) where TMeta : VoidFileMeta + { + return GetMeta(id); + } + + public ValueTask Get(Guid id) + { + return GetMeta(id); + } + + public async ValueTask Set(Guid id, SecretVoidFileMeta meta) + { + await _client.PutObjectAsync(new() + { + BucketName = _config.BucketName, + Key = ToKey(id), + ContentBody = JsonConvert.SerializeObject(meta), + ContentType = "application/json" + }); + } + + public async ValueTask Delete(Guid id) + { + await _client.DeleteObjectAsync(_config.BucketName, ToKey(id)); + } + + private async ValueTask GetMeta(Guid id) where TMeta : VoidFileMeta { try { @@ -48,22 +74,6 @@ public class S3FileMetadataStore : IFileMetadataStore return default; } - - public async ValueTask Set(Guid id, SecretVoidFileMeta meta) - { - await _client.PutObjectAsync(new() - { - BucketName = _config.BucketName, - Key = ToKey(id), - ContentBody = JsonConvert.SerializeObject(meta), - ContentType = "application/json" - }); - } - - public async ValueTask Delete(Guid id) - { - await _client.DeleteObjectAsync(_config.BucketName, ToKey(id)); - } - + private static string ToKey(Guid id) => $"{id}-metadata"; } diff --git a/VoidCat/Services/Files/S3FileStore.cs b/VoidCat/Services/Files/S3FileStore.cs index ae67b28..0b27405 100644 --- a/VoidCat/Services/Files/S3FileStore.cs +++ b/VoidCat/Services/Files/S3FileStore.cs @@ -40,7 +40,7 @@ public class S3FileStore : StreamFileStore, IFileStore }, Headers = { - ContentLength = (long)payload.Meta.Size + ContentLength = (long) payload.Meta.Size } }; @@ -50,19 +50,8 @@ public class S3FileStore : StreamFileStore, IFileStore public async ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts) { - var req = new GetObjectRequest() - { - BucketName = _config.BucketName, - Key = request.Id.ToString() - }; - if (request.Ranges.Any()) - { - var r = request.Ranges.First(); - req.ByteRange = new ByteRange(r.OriginalString); - } - - var obj = await _client.GetObjectAsync(req, cts); - await EgressFull(request.Id, obj.ResponseStream, outStream, cts); + await using var stream = await Open(request, cts); + await EgressFull(request.Id, stream, outStream, cts); } public async ValueTask> ListFiles(PagedRequest request) @@ -124,4 +113,21 @@ public class S3FileStore : StreamFileStore, IFileStore { await _client.DeleteObjectAsync(_config.BucketName, id.ToString()); } + + public async ValueTask Open(EgressRequest request, CancellationToken cts) + { + var req = new GetObjectRequest() + { + BucketName = _config.BucketName, + Key = request.Id.ToString() + }; + if (request.Ranges.Any()) + { + var r = request.Ranges.First(); + req.ByteRange = new ByteRange(r.OriginalString); + } + + var obj = await _client.GetObjectAsync(req, cts); + return obj.ResponseStream; + } } \ No newline at end of file diff --git a/VoidCat/Services/Paywall/PaywallStore.cs b/VoidCat/Services/Paywall/PaywallStore.cs index 2b5a958..59dff70 100644 --- a/VoidCat/Services/Paywall/PaywallStore.cs +++ b/VoidCat/Services/Paywall/PaywallStore.cs @@ -3,36 +3,24 @@ using VoidCat.Services.Abstractions; namespace VoidCat.Services.Paywall; -public class PaywallStore : IPaywallStore +public class PaywallStore : BasicCacheStore, IPaywallStore { - private readonly ICache _cache; - public PaywallStore(ICache database) + : base(database) { - _cache = database; } - public async ValueTask Get(Guid id) + public override async ValueTask Get(Guid id) { - var cfg = await _cache.Get(ConfigKey(id)); + var cfg = await _cache.Get(MapKey(id)); return cfg?.Service switch { PaywallServices.None => cfg, - PaywallServices.Strike => await _cache.Get(ConfigKey(id)), + PaywallServices.Strike => await _cache.Get(MapKey(id)), _ => default }; } - public ValueTask Set(Guid id, PaywallConfig config) - { - return _cache.Set(ConfigKey(id), config); - } - - public ValueTask Delete(Guid id) - { - return _cache.Delete(ConfigKey(id)); - } - public async ValueTask GetOrder(Guid id) { return await _cache.Get(OrderKey(id)); @@ -44,6 +32,6 @@ public class PaywallStore : IPaywallStore order.Status == PaywallOrderStatus.Paid ? TimeSpan.FromDays(1) : TimeSpan.FromSeconds(5)); } - private string ConfigKey(Guid id) => $"paywall:config:{id}"; + public override string MapKey(Guid id) => $"paywall:config:{id}"; private string OrderKey(Guid id) => $"paywall:order:{id}"; } \ No newline at end of file diff --git a/VoidCat/Services/Users/UserManager.cs b/VoidCat/Services/Users/UserManager.cs index 43a3739..6172fc6 100644 --- a/VoidCat/Services/Users/UserManager.cs +++ b/VoidCat/Services/Users/UserManager.cs @@ -24,7 +24,7 @@ public class UserManager : IUserManager if (!(user?.CheckPassword(password) ?? false)) throw new InvalidOperationException("User does not exist"); user.LastLogin = DateTimeOffset.UtcNow; - await _store.Set(user); + await _store.Set(user.Id, user); return user; } @@ -51,7 +51,7 @@ public class UserManager : IUserManager } } - await _store.Set(newUser); + await _store.Set(newUser.Id, newUser); await _emailVerification.SendNewCode(newUser); return newUser; } diff --git a/VoidCat/Services/Users/UserStore.cs b/VoidCat/Services/Users/UserStore.cs index a9b5b3f..22ce819 100644 --- a/VoidCat/Services/Users/UserStore.cs +++ b/VoidCat/Services/Users/UserStore.cs @@ -20,6 +20,11 @@ public class UserStore : IUserStore return await _cache.Get(MapKey(email)); } + public async ValueTask Get(Guid id) + { + return await Get(id); + } + public async ValueTask Get(Guid id) where T : VoidUser { try @@ -34,8 +39,10 @@ public class UserStore : IUserStore return default; } - public async ValueTask Set(InternalVoidUser user) + public async ValueTask Set(Guid id, InternalVoidUser user) { + if (id != user.Id) throw new InvalidOperationException(); + await _cache.Set(MapKey(user.Id), user); await _cache.AddToList(UserList, user.Id.ToString()); await _cache.Set(MapKey(user.Email), user.Id.ToString()); @@ -84,7 +91,14 @@ public class UserStore : IUserStore oldUser.Flags = newUser.Flags; oldUser.DisplayName = newUser.DisplayName; - await Set(oldUser); + await Set(newUser.Id, oldUser); + } + + public async ValueTask Delete(Guid id) + { + var user = await Get(id); + if (user == default) throw new InvalidOperationException(); + await Delete(user); } public async ValueTask Delete(PrivateVoidUser user) diff --git a/VoidCat/spa/src/FilePreview.css b/VoidCat/spa/src/FilePreview.css index b6780fb..676d3d9 100644 --- a/VoidCat/spa/src/FilePreview.css +++ b/VoidCat/spa/src/FilePreview.css @@ -19,4 +19,10 @@ .preview .file-stats svg { vertical-align: middle; margin-right: 10px; +} + +.preview .virus-warning { + padding: 10px; + border-radius: 10px; + border: 1px solid red; } \ No newline at end of file diff --git a/VoidCat/spa/src/FilePreview.js b/VoidCat/spa/src/FilePreview.js index ab0a88e..983acf2 100644 --- a/VoidCat/spa/src/FilePreview.js +++ b/VoidCat/spa/src/FilePreview.js @@ -108,6 +108,23 @@ export function FilePreview() { return tags; } + function renderVirusWarning() { + if(info.virusScan && info.virusScan.isVirus === true) { + let scanResult = info.virusScan; + return ( +
+

+ This file apears to be a virus, take care when downloading this file. +

+ Detected as: +
+                        {scanResult.virusNames.join('\n')}
+                    
+
+ ); + } + } + useEffect(() => { loadInfo(); }, []); @@ -135,6 +152,7 @@ export function FilePreview() { {info.metadata?.description ? : null} {renderOpenGraphTags()} + {renderVirusWarning()}
{info.uploader ? : null}