mirror of
https://github.com/Stone-Red-Code/RemoteExec.git
synced 2026-09-04 09:06:20 +02:00
Add authentication and improve server side configuration
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
namespace RemoteExec.Server.Configuration;
|
||||
|
||||
public class ApiKeyConfiguration
|
||||
{
|
||||
public required string Key { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace RemoteExec.Server.Configuration;
|
||||
|
||||
public class AuthenticationConfiguration
|
||||
{
|
||||
public List<ApiKeyConfiguration> ApiKeys { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace RemoteExec.Server.Configuration;
|
||||
|
||||
public class ExecutionConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum number of concurrent tasks. Default is ProcessorCount * 2.
|
||||
/// </summary>
|
||||
public int? MaxConcurrentTasks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timeout for assembly loading requests in seconds. Default is 30 seconds.
|
||||
/// </summary>
|
||||
public int AssemblyLoadTimeoutSeconds { get; set; } = 30;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace RemoteExec.Server.Configuration;
|
||||
|
||||
public class MetricsConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimum CPU usage difference (in percentage points) to trigger a metrics broadcast. Default is 1.0.
|
||||
/// </summary>
|
||||
public double CpuDifferenceThreshold { get; set; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum memory usage difference (in bytes) to trigger a metrics broadcast. Default is 10 MB.
|
||||
/// </summary>
|
||||
public long MemoryDifferenceThreshold { get; set; } = 10 * 1024 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast interval in milliseconds. Default is 500 ms.
|
||||
/// </summary>
|
||||
public int BroadcastIntervalMs { get; set; } = 500;
|
||||
}
|
||||
@@ -6,5 +6,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("Minor Code Smell", "S2325:Methods and properties that don't access instance data should be static", Justification = "<Pending>", Scope = "type", Target = "~T:RemoteExec.Server.Hubs.RemoteExecutionHub")]
|
||||
[assembly: SuppressMessage("Major Code Smell", "S2139:Exceptions should be either logged or rethrown but not both", Justification = "<Pending>", Scope = "member", Target = "~M:RemoteExec.Server.Hubs.RemoteExecutionHub.RequestAssemblyAsync(System.String)~System.Threading.Tasks.Task{System.Reflection.Assembly}")]
|
||||
[assembly: SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "<Pending>", Scope = "member", Target = "~M:RemoteExec.Server.Hubs.RemoteExecutionHub.Execute(RemoteExec.Shared.RemoteExecutionRequest)~System.Threading.Tasks.Task{RemoteExec.Shared.RemoteExecutionResult}")]
|
||||
[assembly: SuppressMessage("Major Code Smell", "S3010:Static fields should not be updated in constructors", Justification = "<Pending>")]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using RemoteExec.Server.Configuration;
|
||||
using RemoteExec.Shared;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
@@ -9,7 +11,7 @@ using System.Text.Json;
|
||||
|
||||
namespace RemoteExec.Server.Hubs;
|
||||
|
||||
public class RemoteExecutionHub(ILogger<RemoteExecutionHub> logger) : Hub
|
||||
public class RemoteExecutionHub : Hub
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, RemoteJobAssemblyLoadContext> connections = new();
|
||||
|
||||
@@ -23,8 +25,29 @@ public class RemoteExecutionHub(ILogger<RemoteExecutionHub> logger) : Hub
|
||||
private static TimeSpan lastTotalProcessorTime;
|
||||
|
||||
private static int activeTasks = 0;
|
||||
private static readonly int maxConcurrentTasks = Environment.ProcessorCount * 2;
|
||||
private static readonly SemaphoreSlim taskSemaphore = new SemaphoreSlim(maxConcurrentTasks, maxConcurrentTasks);
|
||||
private static int maxConcurrentTasks;
|
||||
private static SemaphoreSlim taskSemaphore = null!;
|
||||
|
||||
private static int assemblyLoadTimeoutSeconds;
|
||||
private static double cpuDifferenceThreshold;
|
||||
private static long memoryDifferenceThreshold;
|
||||
|
||||
private readonly ILogger<RemoteExecutionHub> logger;
|
||||
|
||||
public RemoteExecutionHub(ILogger<RemoteExecutionHub> logger, IOptions<ExecutionConfiguration> executionOptions, IOptions<MetricsConfiguration> metricsOptions)
|
||||
{
|
||||
this.logger = logger;
|
||||
|
||||
// Initialize static configuration values once
|
||||
if (taskSemaphore is null)
|
||||
{
|
||||
maxConcurrentTasks = executionOptions.Value.MaxConcurrentTasks ?? (Environment.ProcessorCount * 2);
|
||||
taskSemaphore = new SemaphoreSlim(maxConcurrentTasks, maxConcurrentTasks);
|
||||
assemblyLoadTimeoutSeconds = executionOptions.Value.AssemblyLoadTimeoutSeconds;
|
||||
cpuDifferenceThreshold = metricsOptions.Value.CpuDifferenceThreshold;
|
||||
memoryDifferenceThreshold = metricsOptions.Value.MemoryDifferenceThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
public override Task OnConnectedAsync()
|
||||
{
|
||||
@@ -234,7 +257,7 @@ public class RemoteExecutionHub(ILogger<RemoteExecutionHub> logger) : Hub
|
||||
int tasksDiff = Math.Abs(metrics.ActiveTasks - lastMetrics.ActiveTasks);
|
||||
int maxTasksDiff = Math.Abs(metrics.MaxConcurrentTasks - lastMetrics.MaxConcurrentTasks);
|
||||
|
||||
if (cpuDiff < 1.0 && memoryDiff < 10 * 1024 * 1024 && connectionsDiff == 0 && tasksDiff == 0 && maxTasksDiff == 0)
|
||||
if (cpuDiff < cpuDifferenceThreshold && memoryDiff < memoryDifferenceThreshold && connectionsDiff == 0 && tasksDiff == 0 && maxTasksDiff == 0)
|
||||
{
|
||||
return; // No significant change
|
||||
}
|
||||
@@ -296,7 +319,7 @@ public class RemoteExecutionHub(ILogger<RemoteExecutionHub> logger) : Hub
|
||||
|
||||
await Clients.Caller.SendAsync("RequestAssembly", key, guid);
|
||||
|
||||
byte[] assemblyBytes = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(30));
|
||||
byte[] assemblyBytes = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(assemblyLoadTimeoutSeconds));
|
||||
|
||||
using MemoryStream ms = new MemoryStream(assemblyBytes);
|
||||
return assemblyLoadContext.LoadFromStream(ms);
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
using RemoteExec.Server.Configuration;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace RemoteExec.Server.Middleware;
|
||||
|
||||
public class ApiKeyAuthenticationMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<ApiKeyAuthenticationMiddleware> _logger;
|
||||
private readonly ConcurrentDictionary<string, ApiKeyConfiguration> _apiKeys;
|
||||
|
||||
public ApiKeyAuthenticationMiddleware(
|
||||
RequestDelegate next,
|
||||
IOptionsMonitor<AuthenticationConfiguration> authOptions,
|
||||
ILogger<ApiKeyAuthenticationMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
|
||||
// Build lookup dictionary from configuration
|
||||
_apiKeys = new ConcurrentDictionary<string, ApiKeyConfiguration>();
|
||||
|
||||
// Initial load
|
||||
LoadApiKeys(authOptions.CurrentValue);
|
||||
|
||||
// Watch for configuration changes
|
||||
_ = authOptions.OnChange(LoadApiKeys);
|
||||
}
|
||||
|
||||
private void LoadApiKeys(AuthenticationConfiguration config)
|
||||
{
|
||||
_apiKeys.Clear();
|
||||
|
||||
if (config.ApiKeys == null || config.ApiKeys.Count == 0)
|
||||
{
|
||||
_logger.LogWarning("No API keys configured. All requests will be rejected.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ApiKeyConfiguration apiKey in config.ApiKeys.Where(k => k.Enabled))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(apiKey.Key))
|
||||
{
|
||||
_logger.LogWarning("Skipping empty API key configuration");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_apiKeys.TryAdd(apiKey.Key, apiKey))
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Registered API key: {Description}",
|
||||
apiKey.Description ?? "No description");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Duplicate API key found and skipped: {Description}",
|
||||
apiKey.Description ?? "No description");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Loaded {Count} active API keys", _apiKeys.Count);
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
// Skip authentication for health checks
|
||||
if (context.Request.Path.StartsWithSegments("/health"))
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if any API keys are configured
|
||||
if (_apiKeys.IsEmpty)
|
||||
{
|
||||
_logger.LogError("No API keys configured. Rejecting request to {Path}", context.Request.Path);
|
||||
|
||||
context.Response.StatusCode = 503;
|
||||
|
||||
await context.Response.WriteAsync("Service is not properly configured");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for API key in header
|
||||
if (!context.Request.Headers.TryGetValue("X-API-Key", out StringValues extractedApiKey))
|
||||
{
|
||||
_logger.LogWarning("API Key missing from request to {Path} from {RemoteIp}", context.Request.Path, context.Connection.RemoteIpAddress);
|
||||
|
||||
context.Response.StatusCode = 401;
|
||||
|
||||
await context.Response.WriteAsync("API Key is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
string providedKey = extractedApiKey.ToString();
|
||||
|
||||
// Validate API key
|
||||
if (!_apiKeys.TryGetValue(providedKey, out ApiKeyConfiguration? apiKeyConfig))
|
||||
{
|
||||
_logger.LogWarning("Invalid API Key provided for request to {Path} from {RemoteIp}",
|
||||
context.Request.Path,
|
||||
context.Connection.RemoteIpAddress);
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsync("Invalid API Key");
|
||||
return;
|
||||
}
|
||||
|
||||
// Store API key info in HttpContext for potential use in controllers
|
||||
context.Items["ApiKeyDescription"] = apiKeyConfig.Description;
|
||||
context.Items["ApiKey"] = providedKey;
|
||||
|
||||
_logger.LogDebug("Authenticated request to {Path} using key: {Description}",
|
||||
context.Request.Path,
|
||||
apiKeyConfig.Description ?? "No description");
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,26 @@
|
||||
using RemoteExec.Server.Configuration;
|
||||
using RemoteExec.Server.Hubs;
|
||||
using RemoteExec.Server.Middleware;
|
||||
using RemoteExec.Server.Services;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddSignalR();
|
||||
builder.Services.AddOpenApi();
|
||||
builder.Services.AddHealthChecks();
|
||||
|
||||
// Add metrics broadcast background service
|
||||
builder.Services.Configure<AuthenticationConfiguration>(builder.Configuration.GetSection("Authentication"));
|
||||
builder.Services.Configure<ExecutionConfiguration>(builder.Configuration.GetSection("Execution"));
|
||||
builder.Services.Configure<MetricsConfiguration>(builder.Configuration.GetSection("Metrics"));
|
||||
|
||||
builder.Services.AddHostedService<MetricsBroadcastService>();
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
app.UseMiddleware<ApiKeyAuthenticationMiddleware>();
|
||||
|
||||
app.MapHub<RemoteExecutionHub>("/remote");
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
@@ -23,7 +29,7 @@ if (app.Environment.IsDevelopment())
|
||||
_ = app.MapOpenApi();
|
||||
}
|
||||
|
||||
//app.UseHttpsRedirection();
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using RemoteExec.Server.Configuration;
|
||||
using RemoteExec.Server.Hubs;
|
||||
|
||||
namespace RemoteExec.Server.Services;
|
||||
|
||||
public class MetricsBroadcastService(IHubContext<RemoteExecutionHub> hubContext, ILogger<MetricsBroadcastService> logger) : BackgroundService
|
||||
public class MetricsBroadcastService(IHubContext<RemoteExecutionHub> hubContext, ILogger<MetricsBroadcastService> logger, IOptions<MetricsConfiguration> metricsOptions) : BackgroundService
|
||||
{
|
||||
private readonly TimeSpan _broadcastInterval = TimeSpan.FromMilliseconds(500);
|
||||
private readonly TimeSpan broadcastInterval = TimeSpan.FromMilliseconds(metricsOptions.Value.BroadcastIntervalMs);
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
logger.LogInformation("Metrics broadcast service started");
|
||||
logger.LogInformation("Metrics broadcast service started with interval {Interval}ms", broadcastInterval.TotalMilliseconds);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(_broadcastInterval, stoppingToken);
|
||||
await Task.Delay(broadcastInterval, stoppingToken);
|
||||
|
||||
await RemoteExecutionHub.BroadcastMetricsAsync(hubContext);
|
||||
}
|
||||
|
||||
@@ -15,5 +15,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
"Authentication": {
|
||||
"ApiKeys": [
|
||||
{
|
||||
"Key": "dev-api-key-1",
|
||||
"Description": "Dev client 1",
|
||||
"Enabled": true
|
||||
},
|
||||
{
|
||||
"Key": "dev-api-key-2",
|
||||
"Description": "Dev client 2",
|
||||
"Enabled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,49 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
|
||||
// Authentication configuration
|
||||
"Authentication": {
|
||||
// List of API keys for authenticating clients
|
||||
// Each entry should contain key identification and validation information
|
||||
"ApiKeys": [
|
||||
// Example:
|
||||
// {
|
||||
// "Key": "your-secret-api-key-here",
|
||||
// "Name": "Client1",
|
||||
// "Enabled": true
|
||||
// }
|
||||
]
|
||||
},
|
||||
|
||||
// Remote execution configuration
|
||||
"Execution": {
|
||||
// Maximum number of tasks that can execute concurrently on the server
|
||||
// null = uses Environment.ProcessorCount * 2 (default)
|
||||
// Increase for CPU-light tasks, decrease for resource-intensive operations
|
||||
"MaxConcurrentTasks": null,
|
||||
|
||||
// Timeout in seconds for loading assemblies from clients
|
||||
// If a client doesn't respond with assembly bytes within this time, the request fails
|
||||
// Default: 30 seconds
|
||||
"AssemblyLoadTimeoutSeconds": 30
|
||||
},
|
||||
|
||||
// Server metrics broadcasting configuration
|
||||
"Metrics": {
|
||||
// Minimum CPU usage difference (in percentage points) required to trigger a metrics broadcast
|
||||
// Lower values = more frequent updates, higher values = less network traffic
|
||||
// Default: 1.0 (1%)
|
||||
"CpuDifferenceThreshold": 1.0,
|
||||
|
||||
// Minimum memory usage difference (in bytes) required to trigger a metrics broadcast
|
||||
// Default: 10485760 (10 MB)
|
||||
"MemoryDifferenceThreshold": 10485760,
|
||||
|
||||
// Interval in milliseconds between metrics broadcasts to all connected clients
|
||||
// Lower values = more real-time updates, higher values = less server load
|
||||
// Default: 5000 (5 seconds)
|
||||
"BroadcastIntervalMs": 5000
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user