mirror of
https://git.v0l.io/Kieran/void.cat.git
synced 2025-04-09 18:29:03 +02:00
Refactor, add PostgresVirusScanStore
This commit is contained in:
parent
cba4d5fc80
commit
3582431640
@ -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();
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,46 @@
|
||||
namespace VoidCat.Model;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace VoidCat.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Results for virus scan of a single file
|
||||
/// </summary>
|
||||
public sealed class VirusScanResult
|
||||
{
|
||||
public DateTimeOffset ScanTime { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
public bool IsVirus { get; init; }
|
||||
/// <summary>
|
||||
/// Unique Id for this scan
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(Base58GuidConverter))]
|
||||
public Guid Id { get; init; }
|
||||
|
||||
public List<string> VirusNames { get; init; } = new();
|
||||
/// <summary>
|
||||
/// Id of the file that was scanned
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(Base58GuidConverter))]
|
||||
public Guid File { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Time the file was scanned
|
||||
/// </summary>
|
||||
public DateTimeOffset ScanTime { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the virus scanner software
|
||||
/// </summary>
|
||||
public string Scanner { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Virus detection score, this can mean different things for each scanner but the value should be between 0 and 1
|
||||
/// </summary>
|
||||
public decimal Score { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Detected virus names
|
||||
/// </summary>
|
||||
public string? Names { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// If we consider this result as a virus or not
|
||||
/// </summary>
|
||||
public bool IsVirus => Score >= 0.75m && !string.IsNullOrEmpty(Names);
|
||||
}
|
@ -165,7 +165,7 @@ if (!string.IsNullOrEmpty(voidSettings.Postgres))
|
||||
services.AddTransient<IMigration, FluentMigrationRunner>();
|
||||
services.AddFluentMigratorCore()
|
||||
.ConfigureRunner(r =>
|
||||
r.AddPostgres11_0()
|
||||
r.AddPostgres()
|
||||
.WithGlobalConnectionString(voidSettings.Postgres)
|
||||
.ScanIn(typeof(Program).Assembly).For.Migrations());
|
||||
}
|
||||
|
@ -1,14 +1,37 @@
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Simple CRUD interface for data stores
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public interface IBasicStore<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a single item from the store
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask<T?> Get(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Get multiple items from the store
|
||||
/// </summary>
|
||||
/// <param name="ids"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask<IReadOnlyList<T>> Get(Guid[] ids);
|
||||
|
||||
ValueTask Set(Guid id, T obj);
|
||||
/// <summary>
|
||||
/// Add an item to the store
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="obj"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask Add(Guid id, T obj);
|
||||
|
||||
/// <summary>
|
||||
/// Delete an item from the store
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask Delete(Guid id);
|
||||
|
||||
string MapKey(Guid id);
|
||||
}
|
@ -1,13 +1,56 @@
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Basic KV cache interface
|
||||
/// </summary>
|
||||
public interface ICache
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a single object from cache by its key
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
ValueTask<T?> Get<T>(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Set the the value of a key in the cache
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="expire"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
ValueTask Set<T>(string key, T value, TimeSpan? expire = null);
|
||||
|
||||
/// <summary>
|
||||
/// Delete an object from the cache
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask Delete(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Return a list of items at the specified key
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask<string[]> GetList(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Add an item to the list at the specified key
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask AddToList(string key, string value);
|
||||
|
||||
/// <summary>
|
||||
/// Remove an item from the list at a the specified key
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask RemoveFromList(string key, string value);
|
||||
|
||||
}
|
||||
|
@ -2,6 +2,15 @@
|
||||
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Store for virus scan results
|
||||
/// </summary>
|
||||
public interface IVirusScanStore : IBasicStore<VirusScanResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the latest scan result by file id
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask<VirusScanResult?> GetByFile(Guid id);
|
||||
}
|
@ -2,7 +2,16 @@
|
||||
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// File virus scanning interface
|
||||
/// </summary>
|
||||
public interface IVirusScanner
|
||||
{
|
||||
/// <summary>
|
||||
/// Scan a single file
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="cts"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask<VirusScanResult> ScanFile(Guid id, CancellationToken cts);
|
||||
}
|
@ -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)
|
||||
{
|
||||
|
@ -2,26 +2,29 @@
|
||||
|
||||
namespace VoidCat.Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract class BasicCacheStore<TStore> : IBasicStore<TStore>
|
||||
{
|
||||
protected readonly ICache _cache;
|
||||
protected readonly ICache Cache;
|
||||
|
||||
protected BasicCacheStore(ICache cache)
|
||||
{
|
||||
_cache = cache;
|
||||
Cache = cache;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual ValueTask<TStore?> Get(Guid id)
|
||||
{
|
||||
return _cache.Get<TStore>(MapKey(id));
|
||||
return Cache.Get<TStore>(MapKey(id));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async ValueTask<IReadOnlyList<TStore>> Get(Guid[] ids)
|
||||
{
|
||||
var ret = new List<TStore>();
|
||||
foreach (var id in ids)
|
||||
{
|
||||
var r = await _cache.Get<TStore>(MapKey(id));
|
||||
var r = await Cache.Get<TStore>(MapKey(id));
|
||||
if (r != null)
|
||||
{
|
||||
ret.Add(r);
|
||||
@ -31,15 +34,22 @@ public abstract class BasicCacheStore<TStore> : IBasicStore<TStore>
|
||||
return ret;
|
||||
}
|
||||
|
||||
public virtual ValueTask Set(Guid id, TStore obj)
|
||||
/// <inheritdoc />
|
||||
public virtual ValueTask Add(Guid id, TStore obj)
|
||||
{
|
||||
return _cache.Set(MapKey(id), obj);
|
||||
return Cache.Set(MapKey(id), obj);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual ValueTask Delete(Guid id)
|
||||
{
|
||||
return _cache.Delete(MapKey(id));
|
||||
return Cache.Delete(MapKey(id));
|
||||
}
|
||||
|
||||
public abstract string MapKey(Guid id);
|
||||
/// <summary>
|
||||
/// Map an id to a key in the KV store
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
protected abstract string MapKey(Guid id);
|
||||
}
|
@ -67,7 +67,7 @@ public class FileInfoManager : IFileInfoManager
|
||||
var meta = _metadataStore.Get<TMeta>(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());
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -10,28 +10,29 @@ public class PaywallStore : BasicCacheStore<PaywallConfig>, IPaywallStore
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<PaywallConfig?> Get(Guid id)
|
||||
{
|
||||
var cfg = await _cache.Get<NoPaywallConfig>(MapKey(id));
|
||||
var cfg = await Cache.Get<NoPaywallConfig>(MapKey(id));
|
||||
return cfg?.Service switch
|
||||
{
|
||||
PaywallServices.None => cfg,
|
||||
PaywallServices.Strike => await _cache.Get<StrikePaywallConfig>(MapKey(id)),
|
||||
PaywallServices.Strike => await Cache.Get<StrikePaywallConfig>(MapKey(id)),
|
||||
_ => default
|
||||
};
|
||||
}
|
||||
|
||||
public async ValueTask<PaywallOrder?> GetOrder(Guid id)
|
||||
{
|
||||
return await _cache.Get<PaywallOrder>(OrderKey(id));
|
||||
return await Cache.Get<PaywallOrder>(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}";
|
||||
}
|
38
VoidCat/Services/VirusScanner/CacheVirusScanStore.cs
Normal file
38
VoidCat/Services/VirusScanner/CacheVirusScanStore.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.VirusScanner;
|
||||
|
||||
/// <inheritdoc cref="IVirusScanStore"/>
|
||||
public class CacheVirusScanStore : BasicCacheStore<VirusScanResult>, IVirusScanStore
|
||||
{
|
||||
public CacheVirusScanStore(ICache cache) : base(cache)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask Add(Guid id, VirusScanResult obj)
|
||||
{
|
||||
await base.Add(id, obj);
|
||||
await Cache.AddToList(MapFilesKey(id), obj.Id.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<VirusScanResult?> GetByFile(Guid id)
|
||||
{
|
||||
var scans = await Cache.GetList(MapFilesKey(id));
|
||||
if (scans.Length > 0)
|
||||
{
|
||||
return await Get(Guid.Parse(scans.First()));
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string MapKey(Guid id)
|
||||
=> $"virus-scan:{id}";
|
||||
|
||||
private string MapFilesKey(Guid id)
|
||||
=> $"virus-scan:file:{id}";
|
||||
}
|
@ -4,6 +4,9 @@ using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.VirusScanner;
|
||||
|
||||
/// <summary>
|
||||
/// ClamAV scanner
|
||||
/// </summary>
|
||||
public class ClamAvScanner : IVirusScanner
|
||||
{
|
||||
private readonly ILogger<ClamAvScanner> _logger;
|
||||
@ -17,6 +20,7 @@ public class ClamAvScanner : IVirusScanner
|
||||
_store = store;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<VirusScanResult> 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<string>()),
|
||||
Scanner = "ClamAV"
|
||||
};
|
||||
}
|
||||
}
|
63
VoidCat/Services/VirusScanner/PostgresVirusScanStore.cs
Normal file
63
VoidCat/Services/VirusScanner/PostgresVirusScanStore.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using Dapper;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.VirusScanner;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class PostgresVirusScanStore : IVirusScanStore
|
||||
{
|
||||
private readonly PostgresConnectionFactory _connection;
|
||||
|
||||
public PostgresVirusScanStore(PostgresConnectionFactory connection)
|
||||
{
|
||||
_connection = connection;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<VirusScanResult?> Get(Guid id)
|
||||
{
|
||||
await using var conn = await _connection.Get();
|
||||
return await conn.QuerySingleOrDefaultAsync<VirusScanResult>(
|
||||
@"select * from ""VirusScanResult"" where ""Id"" = :id", new {id});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<VirusScanResult?> GetByFile(Guid id)
|
||||
{
|
||||
await using var conn = await _connection.Get();
|
||||
return await conn.QuerySingleOrDefaultAsync<VirusScanResult>(
|
||||
@"select * from ""VirusScanResult"" where ""File"" = :file", new {file = id});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IReadOnlyList<VirusScanResult>> Get(Guid[] ids)
|
||||
{
|
||||
await using var conn = await _connection.Get();
|
||||
return (await conn.QueryAsync<VirusScanResult>(
|
||||
@"select * from ""VirusScanResult"" where ""Id"" in :ids", new {ids = ids.ToArray()})).ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask Delete(Guid id)
|
||||
{
|
||||
await using var conn = await _connection.Get();
|
||||
await conn.ExecuteAsync(@"delete from ""VirusScanResult"" where ""Id"" = :id", new {id});
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.VirusScanner;
|
||||
|
||||
public class VirusScanStore : BasicCacheStore<VirusScanResult>, IVirusScanStore
|
||||
{
|
||||
public VirusScanStore(ICache cache) : base(cache)
|
||||
{
|
||||
}
|
||||
|
||||
public override string MapKey(Guid id)
|
||||
=> $"virus-scan:{id}";
|
||||
}
|
@ -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<IVirusScanStore, VirusScanStore>();
|
||||
if (settings.Postgres != default)
|
||||
{
|
||||
services.AddTransient<IVirusScanStore, PostgresVirusScanStore>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddTransient<IVirusScanStore, CacheVirusScanStore>();
|
||||
}
|
||||
|
||||
var avSettings = settings.VirusScanner;
|
||||
if (avSettings != default)
|
||||
@ -28,15 +34,6 @@ public static class VirusScannerStartup
|
||||
services.AddTransient<IVirusScanner, ClamAvScanner>();
|
||||
}
|
||||
|
||||
// load VirusTotal
|
||||
if (avSettings.VirusTotal != default)
|
||||
{
|
||||
loadService = true;
|
||||
services.AddTransient((svc) =>
|
||||
new VirusTotalClient(svc.GetRequiredService<IHttpClientFactory>(), avSettings.VirusTotal));
|
||||
services.AddTransient<IVirusScanner, VirusTotalScanner>();
|
||||
}
|
||||
|
||||
if (loadService)
|
||||
{
|
||||
services.AddHostedService<Background.VirusScannerService>();
|
||||
|
@ -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<string> 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<string> 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<string> 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<T>
|
||||
{
|
||||
[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<VTErrorCodes>(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
|
||||
}
|
@ -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<File?> GetReport(string id)
|
||||
{
|
||||
return await SendRequest<File>(HttpMethod.Get, $"/api/v3/files/{id}");
|
||||
}
|
||||
|
||||
private Task<TResponse> SendRequest<TResponse>(HttpMethod method, string path)
|
||||
{
|
||||
return SendRequest<TResponse, object>(method, path);
|
||||
}
|
||||
|
||||
private async Task<TResponse> SendRequest<TResponse, TRequest>(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<VTResponse<TResponse>>(rspBody);
|
||||
if (vtResponse == default) throw new Exception("Failed?");
|
||||
if (vtResponse.Error != default) throw new VTException(vtResponse.Error);
|
||||
|
||||
return vtResponse.Data;
|
||||
}
|
||||
}
|
@ -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<VirusTotalScanner> _logger;
|
||||
private readonly VirusTotalClient _client;
|
||||
private readonly IFileStore _fileStore;
|
||||
|
||||
public VirusTotalScanner(ILogger<VirusTotalScanner> logger, VirusTotalClient client, IFileStore fileStore)
|
||||
{
|
||||
_logger = logger;
|
||||
_client = client;
|
||||
_fileStore = fileStore;
|
||||
}
|
||||
|
||||
public async ValueTask<VirusScanResult> ScanFile(Guid id, CancellationToken cts)
|
||||
{
|
||||
await using var fs = await _fileStore.Open(new(id, Enumerable.Empty<RangeRequest>()), 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();
|
||||
}
|
||||
}
|
@ -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}`),
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -25,4 +25,5 @@
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid red;
|
||||
margin-bottom: 5px;
|
||||
}
|
@ -118,7 +118,7 @@ export function FilePreview() {
|
||||
</p>
|
||||
Detected as:
|
||||
<pre>
|
||||
{scanResult.virusNames.join('\n')}
|
||||
{scanResult.names}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
|
@ -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 (
|
||||
<div className="footer">
|
||||
<a href="https://discord.gg/8BkxTGs" target="_blank">Discord</a>
|
||||
<a href="https://invite.strike.me/KS0FYF" target="_blank">Get Strike <img src={StrikeLogo} alt="Strike logo"/> </a>
|
||||
<a href="https://github.com/v0l/void.cat" target="_blank">GitHub</a>
|
||||
<a href="https://discord.gg/8BkxTGs" target="_blank">
|
||||
Discord
|
||||
</a>
|
||||
<a href="https://invite.strike.me/KS0FYF" target="_blank">
|
||||
Get Strike <img src={StrikeLogo} alt="Strike logo"/>
|
||||
</a>
|
||||
<a href="https://github.com/v0l/void.cat" target="_blank">
|
||||
GitHub
|
||||
</a>
|
||||
{profile ? <a href="/admin">Admin</a> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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() {
|
||||
<h2>Login</h2>
|
||||
<dl>
|
||||
<dt>Username:</dt>
|
||||
<dd><input type="text" onChange={(e) => setUsername(e.target.value)} placeholder="user@example.com"/></dd>
|
||||
<dd><input type="text" onChange={(e) => setUsername(e.target.value)} placeholder="user@example.com"/>
|
||||
</dd>
|
||||
<dt>Password:</dt>
|
||||
<dd><input type="password" onChange={(e) => setPassword(e.target.value)}/></dd>
|
||||
</dl>
|
||||
|
Loading…
x
Reference in New Issue
Block a user