diff --git a/VoidCat/Controllers/UploadController.cs b/VoidCat/Controllers/UploadController.cs index 444aa0f..652fed9 100644 --- a/VoidCat/Controllers/UploadController.cs +++ b/VoidCat/Controllers/UploadController.cs @@ -214,12 +214,12 @@ namespace VoidCat.Controllers if (req.Strike != default) { - await _paywall.Set(gid, req.Strike!); + await _paywall.Add(gid, req.Strike!); return Ok(); } // if none set, set NoPaywallConfig - await _paywall.Set(gid, new NoPaywallConfig()); + await _paywall.Add(gid, new NoPaywallConfig()); return Ok(); } diff --git a/VoidCat/Model/VirusScanResult.cs b/VoidCat/Model/VirusScanResult.cs index f609c23..aab6c0c 100644 --- a/VoidCat/Model/VirusScanResult.cs +++ b/VoidCat/Model/VirusScanResult.cs @@ -1,10 +1,46 @@ -namespace VoidCat.Model; +using Newtonsoft.Json; +namespace VoidCat.Model; + +/// +/// Results for virus scan of a single file +/// public sealed class VirusScanResult { - public DateTimeOffset ScanTime { get; init; } = DateTimeOffset.UtcNow; - - public bool IsVirus { get; init; } + /// + /// Unique Id for this scan + /// + [JsonConverter(typeof(Base58GuidConverter))] + public Guid Id { get; init; } - public List VirusNames { get; init; } = new(); + /// + /// Id of the file that was scanned + /// + [JsonConverter(typeof(Base58GuidConverter))] + public Guid File { get; init; } + + /// + /// Time the file was scanned + /// + public DateTimeOffset ScanTime { get; init; } = DateTimeOffset.UtcNow; + + /// + /// The name of the virus scanner software + /// + public string Scanner { get; init; } = null!; + + /// + /// Virus detection score, this can mean different things for each scanner but the value should be between 0 and 1 + /// + public decimal Score { get; init; } + + /// + /// Detected virus names + /// + public string? Names { get; init; } + + /// + /// If we consider this result as a virus or not + /// + public bool IsVirus => Score >= 0.75m && !string.IsNullOrEmpty(Names); } \ No newline at end of file diff --git a/VoidCat/Program.cs b/VoidCat/Program.cs index a6b68bf..74bb0b5 100644 --- a/VoidCat/Program.cs +++ b/VoidCat/Program.cs @@ -165,7 +165,7 @@ if (!string.IsNullOrEmpty(voidSettings.Postgres)) services.AddTransient(); services.AddFluentMigratorCore() .ConfigureRunner(r => - r.AddPostgres11_0() + r.AddPostgres() .WithGlobalConnectionString(voidSettings.Postgres) .ScanIn(typeof(Program).Assembly).For.Migrations()); } diff --git a/VoidCat/Services/Abstractions/IBasicStore.cs b/VoidCat/Services/Abstractions/IBasicStore.cs index 8fa1f20..169ce69 100644 --- a/VoidCat/Services/Abstractions/IBasicStore.cs +++ b/VoidCat/Services/Abstractions/IBasicStore.cs @@ -1,14 +1,37 @@ namespace VoidCat.Services.Abstractions; +/// +/// Simple CRUD interface for data stores +/// +/// public interface IBasicStore { + /// + /// Get a single item from the store + /// + /// + /// ValueTask Get(Guid id); + /// + /// Get multiple items from the store + /// + /// + /// ValueTask> Get(Guid[] ids); - ValueTask Set(Guid id, T obj); + /// + /// Add an item to the store + /// + /// + /// + /// + ValueTask Add(Guid id, T obj); + /// + /// Delete an item from the store + /// + /// + /// ValueTask Delete(Guid id); - - string MapKey(Guid id); } \ No newline at end of file diff --git a/VoidCat/Services/Abstractions/ICache.cs b/VoidCat/Services/Abstractions/ICache.cs index 3ebbd40..8af057d 100644 --- a/VoidCat/Services/Abstractions/ICache.cs +++ b/VoidCat/Services/Abstractions/ICache.cs @@ -1,13 +1,56 @@ namespace VoidCat.Services.Abstractions; +/// +/// Basic KV cache interface +/// public interface ICache { + /// + /// Get a single object from cache by its key + /// + /// + /// + /// ValueTask Get(string key); + + /// + /// Set the the value of a key in the cache + /// + /// + /// + /// + /// + /// ValueTask Set(string key, T value, TimeSpan? expire = null); + + /// + /// Delete an object from the cache + /// + /// + /// ValueTask Delete(string key); + /// + /// Return a list of items at the specified key + /// + /// + /// ValueTask GetList(string key); + + /// + /// Add an item to the list at the specified key + /// + /// + /// + /// ValueTask AddToList(string key, string value); + + /// + /// Remove an item from the list at a the specified key + /// + /// + /// + /// ValueTask RemoveFromList(string key, string value); } diff --git a/VoidCat/Services/Abstractions/IVirusScanStore.cs b/VoidCat/Services/Abstractions/IVirusScanStore.cs index f08cdae..bacd56f 100644 --- a/VoidCat/Services/Abstractions/IVirusScanStore.cs +++ b/VoidCat/Services/Abstractions/IVirusScanStore.cs @@ -2,6 +2,15 @@ namespace VoidCat.Services.Abstractions; +/// +/// Store for virus scan results +/// public interface IVirusScanStore : IBasicStore { + /// + /// Get the latest scan result by file id + /// + /// + /// + ValueTask GetByFile(Guid id); } \ No newline at end of file diff --git a/VoidCat/Services/Abstractions/IVirusScanner.cs b/VoidCat/Services/Abstractions/IVirusScanner.cs index 2d4ca05..fea46b3 100644 --- a/VoidCat/Services/Abstractions/IVirusScanner.cs +++ b/VoidCat/Services/Abstractions/IVirusScanner.cs @@ -2,7 +2,16 @@ namespace VoidCat.Services.Abstractions; +/// +/// File virus scanning interface +/// public interface IVirusScanner { + /// + /// Scan a single file + /// + /// + /// + /// ValueTask ScanFile(Guid id, CancellationToken cts); } \ No newline at end of file diff --git a/VoidCat/Services/Background/VirusScannerService.cs b/VoidCat/Services/Background/VirusScannerService.cs index 379f4c8..f0fd93a 100644 --- a/VoidCat/Services/Background/VirusScannerService.cs +++ b/VoidCat/Services/Background/VirusScannerService.cs @@ -36,13 +36,15 @@ public class VirusScannerService : BackgroundService await foreach (var file in files.Results.WithCancellation(stoppingToken)) { // check for scans - var scan = await _scanStore.Get(file.Id); + var scan = await _scanStore.GetByFile(file.Id); if (scan == default) { try { var result = await _scanner.ScanFile(file.Id, stoppingToken); - await _scanStore.Set(file.Id, result); + await _scanStore.Add(result.Id, result); + _logger.LogInformation("Scanned file {Id}, IsVirus = {Result}", result.File, + result.IsVirus); } catch (RateLimitedException rx) { diff --git a/VoidCat/Services/BasicCacheStore.cs b/VoidCat/Services/BasicCacheStore.cs index c0ef2aa..4e02ab6 100644 --- a/VoidCat/Services/BasicCacheStore.cs +++ b/VoidCat/Services/BasicCacheStore.cs @@ -2,26 +2,29 @@ namespace VoidCat.Services; +/// public abstract class BasicCacheStore : IBasicStore { - protected readonly ICache _cache; + protected readonly ICache Cache; protected BasicCacheStore(ICache cache) { - _cache = cache; + Cache = cache; } + /// public virtual ValueTask Get(Guid id) { - return _cache.Get(MapKey(id)); + return Cache.Get(MapKey(id)); } + /// public virtual async ValueTask> Get(Guid[] ids) { var ret = new List(); foreach (var id in ids) { - var r = await _cache.Get(MapKey(id)); + var r = await Cache.Get(MapKey(id)); if (r != null) { ret.Add(r); @@ -31,15 +34,22 @@ public abstract class BasicCacheStore : IBasicStore return ret; } - public virtual ValueTask Set(Guid id, TStore obj) + /// + public virtual ValueTask Add(Guid id, TStore obj) { - return _cache.Set(MapKey(id), obj); + return Cache.Set(MapKey(id), obj); } + /// public virtual ValueTask Delete(Guid id) { - return _cache.Delete(MapKey(id)); + return Cache.Delete(MapKey(id)); } - public abstract string MapKey(Guid id); + /// + /// Map an id to a key in the KV store + /// + /// + /// + protected abstract string MapKey(Guid id); } \ No newline at end of file diff --git a/VoidCat/Services/Files/FileInfoManager.cs b/VoidCat/Services/Files/FileInfoManager.cs index 767685d..a25d159 100644 --- a/VoidCat/Services/Files/FileInfoManager.cs +++ b/VoidCat/Services/Files/FileInfoManager.cs @@ -67,7 +67,7 @@ public class FileInfoManager : IFileInfoManager var meta = _metadataStore.Get(id); var paywall = _paywallStore.Get(id); var bandwidth = _statsReporter.GetBandwidth(id); - var virusScan = _virusScanStore.Get(id); + var virusScan = _virusScanStore.GetByFile(id); var uploader = _userUploadsStore.Uploader(id); await Task.WhenAll(meta.AsTask(), paywall.AsTask(), bandwidth.AsTask(), virusScan.AsTask(), uploader.AsTask()); diff --git a/VoidCat/Services/Migrations/Database/00-Init.cs b/VoidCat/Services/Migrations/Database/00-Init.cs index e5f2cb5..051e506 100644 --- a/VoidCat/Services/Migrations/Database/00-Init.cs +++ b/VoidCat/Services/Migrations/Database/00-Init.cs @@ -1,4 +1,5 @@ -using FluentMigrator; +using System.Data; +using FluentMigrator; using VoidCat.Model; namespace VoidCat.Services.Migrations.Database; @@ -12,8 +13,8 @@ public class Init : Migration .WithColumn("Id").AsGuid().PrimaryKey() .WithColumn("Email").AsString().NotNullable().Indexed() .WithColumn("Password").AsString() - .WithColumn("Created").AsDateTime().WithDefault(SystemMethods.CurrentDateTime) - .WithColumn("LastLogin").AsDateTime().Nullable() + .WithColumn("Created").AsDateTimeOffset().WithDefault(SystemMethods.CurrentUTCDateTime) + .WithColumn("LastLogin").AsDateTimeOffset().Nullable() .WithColumn("Avatar").AsString().Nullable() .WithColumn("DisplayName").AsString().WithDefaultValue("void user") .WithColumn("Flags").AsInt32().WithDefaultValue((int) VoidUserFlags.PublicProfile); @@ -22,29 +23,54 @@ public class Init : Migration .WithColumn("Id").AsGuid().PrimaryKey() .WithColumn("Name").AsString() .WithColumn("Size").AsInt64() - .WithColumn("Uploaded").AsDateTime().Indexed().WithDefault(SystemMethods.CurrentDateTime) + .WithColumn("Uploaded").AsDateTimeOffset().Indexed().WithDefault(SystemMethods.CurrentUTCDateTime) .WithColumn("Description").AsString().Nullable() .WithColumn("MimeType").AsString().WithDefaultValue("application/octet-stream") .WithColumn("Digest").AsString() .WithColumn("EditSecret").AsGuid(); Create.Table("UserFiles") - .WithColumn("File").AsGuid().ForeignKey("Files", "Id") - .WithColumn("User").AsGuid().ForeignKey("Users", "Id").Indexed(); + .WithColumn("File").AsGuid().ForeignKey("Files", "Id").OnDelete(Rule.Cascade).Indexed() + .WithColumn("User").AsGuid().ForeignKey("Users", "Id").OnDelete(Rule.Cascade).Indexed(); Create.UniqueConstraint() .OnTable("UserFiles") .Columns("File", "User"); Create.Table("Paywall") - .WithColumn("File").AsGuid().ForeignKey("Files", "Id").Unique() + .WithColumn("File").AsGuid().ForeignKey("Files", "Id").OnDelete(Rule.Cascade).PrimaryKey() .WithColumn("Type").AsInt16() .WithColumn("Currency").AsInt16() .WithColumn("Amount").AsDecimal(); Create.Table("PaywallStrike") - .WithColumn("File").AsGuid().ForeignKey("Files", "Id").Unique() + .WithColumn("File").AsGuid().ForeignKey("Files", "Id").OnDelete(Rule.Cascade).PrimaryKey() .WithColumn("Handle").AsString(); + + Create.Table("UserRoles") + .WithColumn("User").AsGuid().ForeignKey("Users", "Id").OnDelete(Rule.Cascade).Indexed() + .WithColumn("Role").AsString().NotNullable(); + + Create.UniqueConstraint() + .OnTable("UserRoles") + .Columns("User", "Role"); + + Create.Table("EmailVerification") + .WithColumn("User").AsGuid().ForeignKey("Users", "Id").OnDelete(Rule.Cascade) + .WithColumn("Code").AsGuid() + .WithColumn("Expires").AsDateTimeOffset(); + + Create.UniqueConstraint() + .OnTable("EmailVerification") + .Columns("User", "Code"); + + Create.Table("VirusScanResult") + .WithColumn("Id").AsGuid().PrimaryKey() + .WithColumn("File").AsGuid().ForeignKey("Files", "Id").OnDelete(Rule.Cascade).Indexed() + .WithColumn("ScanTime").AsDateTimeOffset().WithDefault(SystemMethods.CurrentUTCDateTime) + .WithColumn("Scanner").AsString() + .WithColumn("Score").AsDecimal() + .WithColumn("Names").AsString().Nullable(); } public override void Down() @@ -54,5 +80,8 @@ public class Init : Migration Delete.Table("UsersFiles"); Delete.Table("Paywall"); Delete.Table("PaywallStrike"); + Delete.Table("UserRoles"); + Delete.Table("EmailVerification"); + Delete.Table("VirusScanResult"); } } \ No newline at end of file diff --git a/VoidCat/Services/Migrations/Database/01-Roles.cs b/VoidCat/Services/Migrations/Database/01-Roles.cs deleted file mode 100644 index 1c71484..0000000 --- a/VoidCat/Services/Migrations/Database/01-Roles.cs +++ /dev/null @@ -1,23 +0,0 @@ -using FluentMigrator; - -namespace VoidCat.Services.Migrations.Database; - -[Migration(20220608_1345)] -public class UserRoles : Migration -{ - public override void Up() - { - Create.Table("UserRoles") - .WithColumn("User").AsGuid().ForeignKey("Users", "Id").PrimaryKey() - .WithColumn("Role").AsString().NotNullable(); - - Create.UniqueConstraint() - .OnTable("UserRoles") - .Columns("User", "Role"); - } - - public override void Down() - { - Delete.Table("UserRoles"); - } -} \ No newline at end of file diff --git a/VoidCat/Services/Migrations/Database/02-EmailVerification.cs b/VoidCat/Services/Migrations/Database/02-EmailVerification.cs deleted file mode 100644 index 0672ac2..0000000 --- a/VoidCat/Services/Migrations/Database/02-EmailVerification.cs +++ /dev/null @@ -1,24 +0,0 @@ -using FluentMigrator; - -namespace VoidCat.Services.Migrations.Database; - -[Migration(20220608_1443)] -public class EmailVerification : Migration -{ - public override void Up() - { - Create.Table("EmailVerification") - .WithColumn("User").AsGuid().ForeignKey("Users", "Id") - .WithColumn("Code").AsGuid() - .WithColumn("Expires").AsDateTime(); - - Create.UniqueConstraint() - .OnTable("EmailVerification") - .Columns("User", "Code"); - } - - public override void Down() - { - Delete.Table("EmailVerification"); - } -} \ No newline at end of file diff --git a/VoidCat/Services/Paywall/PaywallStore.cs b/VoidCat/Services/Paywall/PaywallStore.cs index 59dff70..8dcd43a 100644 --- a/VoidCat/Services/Paywall/PaywallStore.cs +++ b/VoidCat/Services/Paywall/PaywallStore.cs @@ -10,28 +10,29 @@ public class PaywallStore : BasicCacheStore, IPaywallStore { } + /// public override async ValueTask Get(Guid id) { - var cfg = await _cache.Get(MapKey(id)); + var cfg = await Cache.Get(MapKey(id)); return cfg?.Service switch { PaywallServices.None => cfg, - PaywallServices.Strike => await _cache.Get(MapKey(id)), + PaywallServices.Strike => await Cache.Get(MapKey(id)), _ => default }; } public async ValueTask GetOrder(Guid id) { - return await _cache.Get(OrderKey(id)); + return await Cache.Get(OrderKey(id)); } public ValueTask SaveOrder(PaywallOrder order) { - return _cache.Set(OrderKey(order.Id), order, + return Cache.Set(OrderKey(order.Id), order, order.Status == PaywallOrderStatus.Paid ? TimeSpan.FromDays(1) : TimeSpan.FromSeconds(5)); } - public override string MapKey(Guid id) => $"paywall:config:{id}"; + protected 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/VirusScanner/CacheVirusScanStore.cs b/VoidCat/Services/VirusScanner/CacheVirusScanStore.cs new file mode 100644 index 0000000..cc25a59 --- /dev/null +++ b/VoidCat/Services/VirusScanner/CacheVirusScanStore.cs @@ -0,0 +1,38 @@ +using VoidCat.Model; +using VoidCat.Services.Abstractions; + +namespace VoidCat.Services.VirusScanner; + +/// +public class CacheVirusScanStore : BasicCacheStore, IVirusScanStore +{ + public CacheVirusScanStore(ICache cache) : base(cache) + { + } + + /// + public override async ValueTask Add(Guid id, VirusScanResult obj) + { + await base.Add(id, obj); + await Cache.AddToList(MapFilesKey(id), obj.Id.ToString()); + } + + /// + public async ValueTask GetByFile(Guid id) + { + var scans = await Cache.GetList(MapFilesKey(id)); + if (scans.Length > 0) + { + return await Get(Guid.Parse(scans.First())); + } + + return default; + } + + /// + protected override string MapKey(Guid id) + => $"virus-scan:{id}"; + + private string MapFilesKey(Guid id) + => $"virus-scan:file:{id}"; +} \ No newline at end of file diff --git a/VoidCat/Services/VirusScanner/ClamAvScanner.cs b/VoidCat/Services/VirusScanner/ClamAvScanner.cs index 7b78bbb..79e2f43 100644 --- a/VoidCat/Services/VirusScanner/ClamAvScanner.cs +++ b/VoidCat/Services/VirusScanner/ClamAvScanner.cs @@ -4,6 +4,9 @@ using VoidCat.Services.Abstractions; namespace VoidCat.Services.VirusScanner; +/// +/// ClamAV scanner +/// public class ClamAvScanner : IVirusScanner { private readonly ILogger _logger; @@ -17,6 +20,7 @@ public class ClamAvScanner : IVirusScanner _store = store; } + /// public async ValueTask ScanFile(Guid id, CancellationToken cts) { _logger.LogInformation("Starting scan of {Filename}", id); @@ -31,8 +35,11 @@ public class ClamAvScanner : IVirusScanner return new() { - IsVirus = result.Result == ClamScanResults.VirusDetected, - VirusNames = result.InfectedFiles?.Select(a => a.VirusName.Trim()).ToList() ?? new() + Id = Guid.NewGuid(), + File = id, + Score = result.Result == ClamScanResults.VirusDetected ? 1m : 0m, + Names = string.Join(",", result.InfectedFiles?.Select(a => a.VirusName.Trim()) ?? Array.Empty()), + Scanner = "ClamAV" }; } } \ No newline at end of file diff --git a/VoidCat/Services/VirusScanner/PostgresVirusScanStore.cs b/VoidCat/Services/VirusScanner/PostgresVirusScanStore.cs new file mode 100644 index 0000000..2c0b7b9 --- /dev/null +++ b/VoidCat/Services/VirusScanner/PostgresVirusScanStore.cs @@ -0,0 +1,63 @@ +using Dapper; +using VoidCat.Model; +using VoidCat.Services.Abstractions; + +namespace VoidCat.Services.VirusScanner; + +/// +public class PostgresVirusScanStore : IVirusScanStore +{ + private readonly PostgresConnectionFactory _connection; + + public PostgresVirusScanStore(PostgresConnectionFactory connection) + { + _connection = connection; + } + + /// + public async ValueTask Get(Guid id) + { + await using var conn = await _connection.Get(); + return await conn.QuerySingleOrDefaultAsync( + @"select * from ""VirusScanResult"" where ""Id"" = :id", new {id}); + } + + /// + public async ValueTask GetByFile(Guid id) + { + await using var conn = await _connection.Get(); + return await conn.QuerySingleOrDefaultAsync( + @"select * from ""VirusScanResult"" where ""File"" = :file", new {file = id}); + } + + /// + public async ValueTask> Get(Guid[] ids) + { + await using var conn = await _connection.Get(); + return (await conn.QueryAsync( + @"select * from ""VirusScanResult"" where ""Id"" in :ids", new {ids = ids.ToArray()})).ToList(); + } + + /// + public async ValueTask Add(Guid id, VirusScanResult obj) + { + await using var conn = await _connection.Get(); + await conn.ExecuteAsync( + @"insert into ""VirusScanResult""(""Id"", ""File"", ""Scanner"", ""Score"", ""Names"") values(:id, :file, :scanner, :score, :names)", + new + { + id, + file = obj.File, + scanner = obj.Scanner, + score = obj.Score, + names = obj.Names + }); + } + + /// + public async ValueTask Delete(Guid id) + { + await using var conn = await _connection.Get(); + await conn.ExecuteAsync(@"delete from ""VirusScanResult"" where ""Id"" = :id", new {id}); + } +} \ No newline at end of file diff --git a/VoidCat/Services/VirusScanner/VirusScanStore.cs b/VoidCat/Services/VirusScanner/VirusScanStore.cs deleted file mode 100644 index f230bd6..0000000 --- a/VoidCat/Services/VirusScanner/VirusScanStore.cs +++ /dev/null @@ -1,14 +0,0 @@ -using VoidCat.Model; -using VoidCat.Services.Abstractions; - -namespace VoidCat.Services.VirusScanner; - -public class VirusScanStore : BasicCacheStore, IVirusScanStore -{ - public VirusScanStore(ICache cache) : base(cache) - { - } - - public override string MapKey(Guid id) - => $"virus-scan:{id}"; -} \ No newline at end of file diff --git a/VoidCat/Services/VirusScanner/VirusScannerStartup.cs b/VoidCat/Services/VirusScanner/VirusScannerStartup.cs index a715569..0df1f11 100644 --- a/VoidCat/Services/VirusScanner/VirusScannerStartup.cs +++ b/VoidCat/Services/VirusScanner/VirusScannerStartup.cs @@ -1,7 +1,6 @@ using nClam; using VoidCat.Model; using VoidCat.Services.Abstractions; -using VoidCat.Services.VirusScanner.VirusTotal; namespace VoidCat.Services.VirusScanner; @@ -9,7 +8,14 @@ public static class VirusScannerStartup { public static void AddVirusScanner(this IServiceCollection services, VoidSettings settings) { - services.AddTransient(); + if (settings.Postgres != default) + { + services.AddTransient(); + } + else + { + services.AddTransient(); + } var avSettings = settings.VirusScanner; if (avSettings != default) @@ -28,15 +34,6 @@ public static class VirusScannerStartup services.AddTransient(); } - // load VirusTotal - if (avSettings.VirusTotal != default) - { - loadService = true; - services.AddTransient((svc) => - new VirusTotalClient(svc.GetRequiredService(), avSettings.VirusTotal)); - services.AddTransient(); - } - if (loadService) { services.AddHostedService(); diff --git a/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalClasses.cs b/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalClasses.cs deleted file mode 100644 index c8fe2b6..0000000 --- a/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalClasses.cs +++ /dev/null @@ -1,184 +0,0 @@ -using Newtonsoft.Json; -// ReSharper disable InconsistentNaming -#pragma warning disable CS8618 - -namespace VoidCat.Services.VirusScanner.VirusTotal; - -public class LastAnalysisStats -{ - [JsonProperty("confirmed-timeout")] - public int ConfirmedTimeout { get; set; } - - [JsonProperty("failure")] - public int Failure { get; set; } - - [JsonProperty("harmless")] - public int Harmless { get; set; } - - [JsonProperty("malicious")] - public int Malicious { get; set; } - - [JsonProperty("suspicious")] - public int Suspicious { get; set; } - - [JsonProperty("timeout")] - public int Timeout { get; set; } - - [JsonProperty("type-unsupported")] - public int TypeUnsupported { get; set; } - - [JsonProperty("undetected")] - public int Undetected { get; set; } -} - -public class TotalVotes -{ - [JsonProperty("harmless")] - public int Harmless { get; set; } - - [JsonProperty("malicious")] - public int Malicious { get; set; } -} - -public class Attributes -{ - [JsonProperty("capabilities_tags")] - public List CapabilitiesTags { get; set; } - - [JsonProperty("creation_date")] - public int CreationDate { get; set; } - - [JsonProperty("downloadable")] - public bool Downloadable { get; set; } - - [JsonProperty("first_submission_date")] - public int FirstSubmissionDate { get; set; } - - [JsonProperty("last_analysis_date")] - public int LastAnalysisDate { get; set; } - - [JsonProperty("last_analysis_stats")] - public LastAnalysisStats LastAnalysisStats { get; set; } - - [JsonProperty("last_modification_date")] - public int LastModificationDate { get; set; } - - [JsonProperty("last_submission_date")] - public int LastSubmissionDate { get; set; } - - [JsonProperty("md5")] - public string Md5 { get; set; } - - [JsonProperty("meaningful_name")] - public string MeaningfulName { get; set; } - - [JsonProperty("names")] - public List Names { get; set; } - - [JsonProperty("reputation")] - public int Reputation { get; set; } - - [JsonProperty("sha1")] - public string Sha1 { get; set; } - - [JsonProperty("sha256")] - public string Sha256 { get; set; } - - [JsonProperty("size")] - public int Size { get; set; } - - [JsonProperty("tags")] - public List Tags { get; set; } - - [JsonProperty("times_submitted")] - public int TimesSubmitted { get; set; } - - [JsonProperty("total_votes")] - public TotalVotes TotalVotes { get; set; } - - [JsonProperty("type_description")] - public string TypeDescription { get; set; } - - [JsonProperty("type_tag")] - public string TypeTag { get; set; } - - [JsonProperty("unique_sources")] - public int UniqueSources { get; set; } - - [JsonProperty("vhash")] - public string Vhash { get; set; } -} - -public class Links -{ - [JsonProperty("self")] - public string Self { get; set; } -} - -public class File -{ - [JsonProperty("attributes")] - public Attributes Attributes { get; set; } - - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("links")] - public Links Links { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } -} - -public class Error -{ - [JsonProperty("code")] - public string Code { get; set; } - - [JsonProperty("message")] - public string Message { get; set; } -} - -// ReSharper disable once InconsistentNaming -public class VTResponse -{ - [JsonProperty("data")] - public T Data { get; set; } - - [JsonProperty("error")] - public Error Error { get; set; } -} - -public class VTException : Exception -{ - public VTException(Error error) - { - ErrorCode = Enum.TryParse(error.Code, out var c) ? c : VTErrorCodes.UnknownError; - Message = error.Message; - } - - public VTErrorCodes ErrorCode { get; } - - public string Message { get; } -} - -public enum VTErrorCodes -{ - UnknownError, - BadRequestError, - InvalidArgumentError, - NotAvailableYet, - UnselectiveContentQueryError, - UnsupportedContentQueryError, - AuthenticationRequiredError, - UserNotActiveError, - WrongCredentialsError, - ForbiddenError, - NotFoundError, - AlreadyExistsError, - FailedDependencyError, - QuotaExceededError, - TooManyRequestsError, - TransientError, - DeadlineExceededError -} \ No newline at end of file diff --git a/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalClient.cs b/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalClient.cs deleted file mode 100644 index 09e22f1..0000000 --- a/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalClient.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Text; -using Newtonsoft.Json; -using VoidCat.Model; - -namespace VoidCat.Services.VirusScanner.VirusTotal; - -public class VirusTotalClient -{ - private readonly HttpClient _client; - - public VirusTotalClient(IHttpClientFactory clientFactory, VirusTotalConfig config) - { - _client = clientFactory.CreateClient(); - - _client.BaseAddress = new Uri("https://www.virustotal.com/"); - _client.DefaultRequestHeaders.Add("x-apikey", config.ApiKey); - _client.DefaultRequestHeaders.Add("accept", "application/json"); - } - - public async Task GetReport(string id) - { - return await SendRequest(HttpMethod.Get, $"/api/v3/files/{id}"); - } - - private Task SendRequest(HttpMethod method, string path) - { - return SendRequest(method, path); - } - - private async Task SendRequest(HttpMethod method, string path, TRequest? body = null) - where TRequest : class - { - var req = new HttpRequestMessage(method, path); - if (body != default) - { - var json = JsonConvert.SerializeObject(body); - req.Content = new ByteArrayContent(Encoding.UTF8.GetBytes(json)); - } - - var rsp = await _client.SendAsync(req); - var rspBody = await rsp.Content.ReadAsStringAsync(); - var vtResponse = JsonConvert.DeserializeObject>(rspBody); - if (vtResponse == default) throw new Exception("Failed?"); - if (vtResponse.Error != default) throw new VTException(vtResponse.Error); - - return vtResponse.Data; - } -} \ No newline at end of file diff --git a/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalScanner.cs b/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalScanner.cs deleted file mode 100644 index 6aded7f..0000000 --- a/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalScanner.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Security.Cryptography; -using VoidCat.Model; -using VoidCat.Services.Abstractions; -using VoidCat.Services.VirusScanner.Exceptions; - -namespace VoidCat.Services.VirusScanner.VirusTotal; - -public class VirusTotalScanner : IVirusScanner -{ - private readonly ILogger _logger; - private readonly VirusTotalClient _client; - private readonly IFileStore _fileStore; - - public VirusTotalScanner(ILogger logger, VirusTotalClient client, IFileStore fileStore) - { - _logger = logger; - _client = client; - _fileStore = fileStore; - } - - public async ValueTask ScanFile(Guid id, CancellationToken cts) - { - await using var fs = await _fileStore.Open(new(id, Enumerable.Empty()), cts); - - // hash file and check on VT - var hash = await SHA256.Create().ComputeHashAsync(fs, cts); - - try - { - var report = await _client.GetReport(hash.ToHex()); - if (report != default) - { - return new() - { - IsVirus = report.Attributes.Reputation == 0 - }; - } - } - catch (VTException vx) - { - if (vx.ErrorCode == VTErrorCodes.QuotaExceededError) - { - throw new RateLimitedException() - { - // retry tomorrow :( - // this makes it pretty much unusable unless you have a paid subscription - RetryAfter = DateTimeOffset.Now.Date.AddDays(1) - }; - } - - throw; - } - - throw new InvalidOperationException(); - } -} \ No newline at end of file diff --git a/VoidCat/spa/src/Api.js b/VoidCat/spa/src/Api.js index d24217a..b33b33e 100644 --- a/VoidCat/spa/src/Api.js +++ b/VoidCat/spa/src/Api.js @@ -31,7 +31,7 @@ export function useApi() { }, Api: { info: () => getJson("GET", "/info"), - fileInfo: (id) => getJson("GET", `/upload/${id}`), + fileInfo: (id) => getJson("GET", `/upload/${id}`, undefined, auth), setPaywallConfig: (id, cfg) => getJson("POST", `/upload/${id}/paywall`, cfg, auth), createOrder: (id) => getJson("GET", `/upload/${id}/paywall`), getOrder: (file, order) => getJson("GET", `/upload/${file}/paywall/${order}`), diff --git a/VoidCat/spa/src/FileEdit.js b/VoidCat/spa/src/FileEdit.js index 34683e8..ddc904d 100644 --- a/VoidCat/spa/src/FileEdit.js +++ b/VoidCat/spa/src/FileEdit.js @@ -16,8 +16,8 @@ export function FileEdit(props) { const [name, setName] = useState(meta?.name); const [description, setDescription] = useState(meta?.description); - const privateFile = profile?.id === meta?.uploader ? file : JSON.parse(window.localStorage.getItem(file.id)); - if (!privateFile) { + const privateFile = profile?.id === file?.uploader?.id ? file : JSON.parse(window.localStorage.getItem(file.id)); + if (!privateFile || privateFile?.metadata?.editSecret === null) { return null; } diff --git a/VoidCat/spa/src/FilePreview.css b/VoidCat/spa/src/FilePreview.css index 676d3d9..ab61828 100644 --- a/VoidCat/spa/src/FilePreview.css +++ b/VoidCat/spa/src/FilePreview.css @@ -25,4 +25,5 @@ padding: 10px; border-radius: 10px; border: 1px solid red; + margin-bottom: 5px; } \ No newline at end of file diff --git a/VoidCat/spa/src/FilePreview.js b/VoidCat/spa/src/FilePreview.js index 983acf2..87c1277 100644 --- a/VoidCat/spa/src/FilePreview.js +++ b/VoidCat/spa/src/FilePreview.js @@ -118,7 +118,7 @@ export function FilePreview() {

Detected as:
-                        {scanResult.virusNames.join('\n')}
+                        {scanResult.names}
                     
); diff --git a/VoidCat/spa/src/FooterLinks.js b/VoidCat/spa/src/FooterLinks.js index 37c6aab..50de0d7 100644 --- a/VoidCat/spa/src/FooterLinks.js +++ b/VoidCat/spa/src/FooterLinks.js @@ -1,12 +1,22 @@ import "./FooterLinks.css" import StrikeLogo from "./image/strike.png"; +import {useSelector} from "react-redux"; + +export function FooterLinks() { + const profile = useSelector(state => state.login.profile); -export function FooterLinks(){ return ( ); } \ No newline at end of file diff --git a/VoidCat/spa/src/Login.js b/VoidCat/spa/src/Login.js index 6238f29..c6a018e 100644 --- a/VoidCat/spa/src/Login.js +++ b/VoidCat/spa/src/Login.js @@ -12,7 +12,7 @@ export function Login() { const [password, setPassword] = useState(); const [error, setError] = useState(); const [captchaResponse, setCaptchaResponse] = useState(); - const captchaKey = useSelector(state => state.info.stats.captchaSiteKey); + const captchaKey = useSelector(state => state.info.stats?.captchaSiteKey); const dispatch = useDispatch(); async function login(fnLogin) { @@ -34,7 +34,8 @@ export function Login() {

Login

Username:
-
setUsername(e.target.value)} placeholder="user@example.com"/>
+
setUsername(e.target.value)} placeholder="user@example.com"/> +
Password:
setPassword(e.target.value)}/>