Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,8 @@
<PackageVersion Include="System.IO.Hashing" Version="10.0.8" />
<PackageVersion Include="System.Text.Json" Version="10.0.8" />
</ItemGroup>
<ItemGroup>
<!-- Aspire Packages -->
<PackageVersion Include="Aspire.Hosting.AppHost" Version="13.4.1" />
</ItemGroup>
</Project>
2 changes: 2 additions & 0 deletions OrchardCore.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
</Folder>
<Folder Name="/src/">
<Project DefaultStartup="true" Path="src/OrchardCore.Cms.Web/OrchardCore.Cms.Web.csproj" />
<Project Path="src/OrchardCore.AspireHost/OrchardCore.AspireHost.csproj" />
</Folder>
<Folder Name="/src/OrchardCore.Frameworks/">
<Project Path="src/OrchardCore/OrchardCore.Mvc.Core/OrchardCore.Mvc.Core.csproj" />
Expand Down Expand Up @@ -116,6 +117,7 @@
<Project Path="src/OrchardCore.Modules/OrchardCore.XmlRpc/OrchardCore.XmlRpc.csproj" />
</Folder>
<Folder Name="/src/OrchardCore.Modules/">
<Project Path="src/OrchardCore.Modules/OrchardCore.Antivirus/OrchardCore.Antivirus.csproj" />
<Project Path="src/OrchardCore.Modules/OrchardCore.Admin/OrchardCore.Admin.csproj" />
<Project Path="src/OrchardCore.Modules/OrchardCore.AdminDashboard/OrchardCore.AdminDashboard.csproj" />
<Project Path="src/OrchardCore.Modules/OrchardCore.Apis.GraphQL/OrchardCore.Apis.GraphQL.csproj" />
Expand Down
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ nav:
- Media:
- Media: reference/modules/Media/README.md
- Media Slugify: reference/modules/Media.Slugify/README.md
- Antivirus: reference/modules/Antivirus/README.md
- Media Amazon S3: reference/modules/Media.AmazonS3/README.md
- Media Azure: reference/modules/Media.Azure/README.md
- ReCaptcha: reference/modules/ReCaptcha/README.md
Expand Down Expand Up @@ -252,6 +253,7 @@ nav:
- Audit Trail: reference/modules/AuditTrail/README.md
- Auto Setup: reference/modules/AutoSetup/README.md
- Features: reference/modules/Features/README.md
- File Upload Security: reference/core/file-upload-security.md
- Contents: reference/modules/Contents/README.md
- Configuration: reference/modules/Configuration/README.md
- Cors: reference/modules/Cors/README.md
Expand Down
53 changes: 53 additions & 0 deletions src/OrchardCore.AspireHost/ClamAV.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;

namespace OrchardCore.AspireHost;

public sealed class ClamAVResource : ContainerResource, IResourceWithConnectionString
{
internal const string PrimaryEndpointName = "tcp";

private EndpointReference _primaryEndpoint;

public ClamAVResource(string name)
: base(name)
{
}

public EndpointReference PrimaryEndpoint
=> _primaryEndpoint ??= new EndpointReference(this, PrimaryEndpointName);

public ReferenceExpression ConnectionStringExpression
=> ReferenceExpression.Create(
$"tcp://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}");
}

internal static class ClamAVResourceBuilderExtensions
{
public static IResourceBuilder<ClamAVResource> AddClamAV(
this IDistributedApplicationBuilder builder,
string name,
int? port = null)
{
var resource = new ClamAVResource(name);

return builder.AddResource(resource)
.WithImage("clamav/clamav")
.WithImageRegistry("docker.io")
.WithImageTag("latest")
.WithEnvironment("CLAMAV_NO_FRESHCLAMD", "true")
.WithEndpoint(port: port, name: ClamAVResource.PrimaryEndpointName, targetPort: 3310);
}

public static IResourceBuilder<ClamAVResource> WithDataVolume(
this IResourceBuilder<ClamAVResource> builder,
string name,
bool isReadOnly = false) =>
builder.WithVolume(name, "/var/lib/clamav", isReadOnly);

public static IResourceBuilder<ClamAVResource> WithDataBindMount(
this IResourceBuilder<ClamAVResource> builder,
string source,
bool isReadOnly = false) =>
builder.WithBindMount(source, "/var/lib/clamav", isReadOnly);
}
22 changes: 22 additions & 0 deletions src/OrchardCore.AspireHost/OrchardCore.AspireHost.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Aspire.AppHost.Sdk/13.4.1">

<PropertyGroup>
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
<OutputType>Exe</OutputType>
<IsAspireHost>true</IsAspireHost>
<IsPackable>false</IsPackable>
<UserSecretsId>53f65258-c4bc-45d0-ab79-c8309ff77904</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../OrchardCore.Cms.Web/OrchardCore.Cms.Web.csproj"
ReferenceOutputAssembly="false"
SkipGetTargetFrameworkProperties="true"
SetTargetFramework="TargetFramework=$(DefaultTargetFramework)" />
</ItemGroup>

</Project>
20 changes: 20 additions & 0 deletions src/OrchardCore.AspireHost/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using OrchardCore.AspireHost;

var builder = DistributedApplication.CreateBuilder(args);

var clamAv = builder.AddClamAV("antivirus")
.WithDataVolume("clamavdb");

builder.AddProject<Projects.OrchardCore_Cms_Web>("OrchardCoreCms")
.WithExternalHttpEndpoints()
.WithReference(clamAv)
.WithEnvironment("OrchardCore__Antivirus_ClamAV__Host", clamAv.Resource.PrimaryEndpoint.Property(EndpointProperty.Host))
.WithEnvironment("OrchardCore__Antivirus_ClamAV__Port", clamAv.Resource.PrimaryEndpoint.Property(EndpointProperty.Port))
.WithEnvironment("OrchardCore__Antivirus_ClamAV__ConnectTimeoutSeconds", "5")
.WithEnvironment("OrchardCore__Antivirus_ClamAV__TransferTimeoutSeconds", "30");

var app = builder.Build();

await app.RunAsync();
29 changes: 29 additions & 0 deletions src/OrchardCore.AspireHost/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:17262;http://localhost:15209",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21196",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22006"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:15209",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19032",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20030"
}
}
}
}
9 changes: 9 additions & 0 deletions src/OrchardCore.AspireHost/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}
8 changes: 8 additions & 0 deletions src/OrchardCore.AspireHost/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
5 changes: 5 additions & 0 deletions src/OrchardCore.AspireHost/aspire.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"appHost": {
"path": "OrchardCore.AspireHost.csproj"
}
}
2 changes: 1 addition & 1 deletion src/OrchardCore.Cms.Web/OrchardCore.Cms.Web.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project ToolsVersion="15.0" Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<!-- Necessary as we reference the Project and not the Package -->
<Import Project="..\OrchardCore\OrchardCore.Application.Cms.Core.Targets\OrchardCore.Application.Cms.Core.Targets.props" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace OrchardCore.Antivirus.ClamAV;

public sealed class ClamAvOptions
{
public const string ConfigSection = "Antivirus_ClamAV";

public string Host { get; set; }

public int Port { get; set; } = 3310;

public int ConnectTimeoutSeconds { get; set; } = 5;

public int TransferTimeoutSeconds { get; set; } = 30;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Logging;

namespace OrchardCore.Antivirus.ClamAV;

public sealed class ClamAvConnection : IDisposable
{
private const int BufferSize = 81920;
private static readonly byte[] _scanCommand = "nINSTREAM\n"u8.ToArray();

private readonly ClamAvOptions _options;
private readonly ILogger _logger;
private readonly SemaphoreSlim _lock = new(1, 1);

private TcpClient _client;
private NetworkStream _networkStream;

public ClamAvConnection(ClamAvOptions options, ILogger<ClamAvConnection> logger)
{
_options = options;
_logger = logger;
}

public async Task<string> ScanAsync(Stream stream, CancellationToken cancellationToken)
{
await _lock.WaitAsync(cancellationToken);

try
{
try
{
await EnsureConnectedAsync(cancellationToken);

using var transferCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
transferCancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(_options.TransferTimeoutSeconds));

await _networkStream.WriteAsync(_scanCommand, transferCancellationTokenSource.Token);

var buffer = new byte[BufferSize];
int bytesRead;

while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), transferCancellationTokenSource.Token)) > 0)
{
var prefix = BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder(bytesRead));

await _networkStream.WriteAsync(prefix, transferCancellationTokenSource.Token);
await _networkStream.WriteAsync(buffer.AsMemory(0, bytesRead), transferCancellationTokenSource.Token);
}

await _networkStream.WriteAsync(new byte[sizeof(int)], transferCancellationTokenSource.Token);
await _networkStream.FlushAsync(transferCancellationTokenSource.Token);

return await ReadResponseAsync(_networkStream, transferCancellationTokenSource.Token);
}
catch (Exception exception) when (
exception is IOException or
SocketException or
OperationCanceledException or
ObjectDisposedException)
{
ResetConnection();
throw;
}
}
finally
{
_lock.Release();
}
}

public void Dispose()
{
ResetConnection();
_lock.Dispose();
}

private async Task EnsureConnectedAsync(CancellationToken cancellationToken)
{
if (_client?.Connected == true && _networkStream is not null)
{
return;
}

ResetConnection();

var client = new TcpClient();
using var connectCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
connectCancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(_options.ConnectTimeoutSeconds));

await client.ConnectAsync(_options.Host, _options.Port, connectCancellationTokenSource.Token);

client.SendTimeout = (int)TimeSpan.FromSeconds(_options.TransferTimeoutSeconds).TotalMilliseconds;
client.ReceiveTimeout = (int)TimeSpan.FromSeconds(_options.TransferTimeoutSeconds).TotalMilliseconds;

_client = client;
_networkStream = client.GetStream();

if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Created a shared ClamAV TCP connection for '{Host}:{Port}'.", _options.Host, _options.Port);
}
}

private void ResetConnection()
{
_networkStream?.Dispose();
_networkStream = null;
_client?.Dispose();
_client = null;
}

private static async Task<string> ReadResponseAsync(NetworkStream stream, CancellationToken cancellationToken)
{
using var responseStream = new MemoryStream();
var buffer = new byte[1];

while (true)
{
var bytesRead = await stream.ReadAsync(buffer.AsMemory(0, 1), cancellationToken);

if (bytesRead == 0 || buffer[0] == '\n')
{
break;
}

responseStream.WriteByte(buffer[0]);
}

return Encoding.ASCII.GetString(responseStream.ToArray());
}
}
Loading
Loading