Refactor, add PostgresVirusScanStore

This commit is contained in:
Kieran 2022-06-13 11:29:16 +01:00
parent cba4d5fc80
commit 3582431640
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
28 changed files with 337 additions and 407 deletions

View File

@ -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();
}

View File

@ -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
{
/// <summary>
/// Unique Id for this scan
/// </summary>
[JsonConverter(typeof(Base58GuidConverter))]
public Guid Id { get; init; }
/// <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;
public bool IsVirus { get; init; }
/// <summary>
/// The name of the virus scanner software
/// </summary>
public string Scanner { get; init; } = null!;
public List<string> VirusNames { get; init; } = new();
/// <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);
}

View File

@ -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());
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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)
{

View File

@ -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);
}

View File

@ -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());

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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}";
}

View 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}";
}

View File

@ -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"
};
}
}

View 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});
}
}

View File

@ -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}";
}

View File

@ -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>();

View File

@ -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
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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}`),

View File

@ -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;
}

View File

@ -25,4 +25,5 @@
padding: 10px;
border-radius: 10px;
border: 1px solid red;
margin-bottom: 5px;
}

View File

@ -118,7 +118,7 @@ export function FilePreview() {
</p>
Detected as:
<pre>
{scanResult.virusNames.join('\n')}
{scanResult.names}
</pre>
</div>
);

View File

@ -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>
);
}

View File

@ -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>