feat(domain): virus-scan and PDF-check uploaded diplomas with ClamAV (closes #192) #194

Open
not wants to merge 13 commits from feat/192-scan-uploads into main
Showing only changes of commit 254b1796dc - Show all commits
@@ -17,13 +17,14 @@ public class ClamdDocumentScannerTests
public Task<byte[]> Received { get; } public Task<byte[]> Received { get; }
public int Port => ((IPEndPoint)_listener.LocalEndpoint).Port; public int Port => ((IPEndPoint)_listener.LocalEndpoint).Port;
public FakeClamd(string reply) /// <param name="reply">The answer, or null to accept the request and never answer (a hung clamd).</param>
public FakeClamd(string? reply)
{ {
_listener.Start(); _listener.Start();
Received = Serve(reply); Received = Serve(reply);
} }
private async Task<byte[]> Serve(string reply) private async Task<byte[]> Serve(string? reply)
{ {
using var client = await _listener.AcceptTcpClientAsync(); using var client = await _listener.AcceptTcpClientAsync();
var stream = client.GetStream(); var stream = client.GetStream();
@@ -35,7 +36,10 @@ public class ClamdDocumentScannerTests
if (n == 0) break; if (n == 0) break;
received.Write(buffer, 0, n); received.Write(buffer, 0, n);
} }
await stream.WriteAsync(System.Text.Encoding.ASCII.GetBytes(reply + "\0")); if (reply is null)
await Task.Delay(TimeSpan.FromSeconds(10)); // long past any test timeout
else
await stream.WriteAsync(System.Text.Encoding.ASCII.GetBytes(reply + "\0"));
return received.ToArray(); return received.ToArray();
} }
@@ -82,4 +86,32 @@ public class ClamdDocumentScannerTests
Assert.Equal(ScanVerdict.Unavailable, await ScannerFor(port).ScanAsync([1, 2, 3])); Assert.Equal(ScanVerdict.Unavailable, await ScannerFor(port).ScanAsync([1, 2, 3]));
} }
[Fact]
public async Task A_clamd_that_never_answers_is_unavailable_after_the_timeout()
{
using var clamd = new FakeClamd(reply: null);
var scanner = new ClamdDocumentScanner(
new ClamAvOptions { Host = "127.0.0.1", Port = clamd.Port, Timeout = TimeSpan.FromMilliseconds(300) });
Assert.Equal(ScanVerdict.Unavailable, await scanner.ScanAsync([1, 2, 3]));
}
[Fact]
public async Task A_cancelled_request_is_cancelled_not_reported_unavailable()
{
// The caller giving up is not a scanner outage: it propagates instead of becoming a 503.
using var clamd = new FakeClamd(reply: null);
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(300));
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => ScannerFor(clamd.Port).ScanAsync([1, 2, 3], cts.Token));
}
[Fact]
public async Task Rejects_null_content()
=> await Assert.ThrowsAsync<ArgumentNullException>(() => ScannerFor(1).ScanAsync(null!));
[Fact]
public void Defaults_to_the_clamav_service_on_the_clamd_port()
=> Assert.Equal(("clamav", 3310), (new ClamAvOptions().Host, new ClamAvOptions().Port));
} }