mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
added CORS logging and Agent max auth retry logic via config
This commit is contained in:
@@ -138,6 +138,19 @@ namespace AutopilotMonitor.Agent.Core.Configuration
|
||||
/// </summary>
|
||||
public int HelloWaitTimeoutSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum consecutive authentication failures (401/403) before the agent shuts down.
|
||||
/// Prevents endless retry traffic when the device is not authorized.
|
||||
/// 0 = disabled (retry forever). Default: 5.
|
||||
/// </summary>
|
||||
public int MaxAuthFailures { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum time in minutes the agent keeps retrying after the first auth failure.
|
||||
/// 0 = disabled (no time limit, only MaxAuthFailures applies). Default: 0.
|
||||
/// </summary>
|
||||
public int AuthFailureTimeoutMinutes { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Validates the configuration
|
||||
/// </summary>
|
||||
|
||||
@@ -48,6 +48,10 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
// Cleanup/self-destruct
|
||||
private readonly CleanupService _cleanupService;
|
||||
|
||||
// Auth failure circuit breaker
|
||||
private int _consecutiveAuthFailures = 0;
|
||||
private DateTime? _firstAuthFailureTime = null;
|
||||
|
||||
public MonitoringService(AgentConfiguration configuration, AgentLogger logger, string agentVersion)
|
||||
{
|
||||
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
@@ -242,9 +246,13 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
_configuration.ImeMatchLogPath = config.ImeMatchLogPath;
|
||||
}
|
||||
|
||||
_configuration.MaxAuthFailures = config.MaxAuthFailures;
|
||||
_configuration.AuthFailureTimeoutMinutes = config.AuthFailureTimeoutMinutes;
|
||||
|
||||
_logger.Info("Applied runtime settings from remote config");
|
||||
_logger.Info($" uploadIntervalSeconds={_configuration.UploadIntervalSeconds}, cleanupOnExit={_configuration.CleanupOnExit}, selfDestructOnComplete={_configuration.SelfDestructOnComplete}, keepLogFile={_configuration.KeepLogFile}");
|
||||
_logger.Info($" imeMatchLogPath={_configuration.ImeMatchLogPath ?? "(none)"}");
|
||||
_logger.Info($" maxAuthFailures={_configuration.MaxAuthFailures}, authFailureTimeoutMinutes={_configuration.AuthFailureTimeoutMinutes}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -377,6 +385,11 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
_logger.Warning($"Session registration failed: {response.Message}");
|
||||
}
|
||||
}
|
||||
catch (BackendAuthException ex)
|
||||
{
|
||||
_logger.Error($"Session registration authentication failed: {ex.Message}");
|
||||
HandleAuthFailure();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error("Failed to register session", ex);
|
||||
@@ -623,18 +636,66 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
{
|
||||
_spool.RemoveEvents(events);
|
||||
_logger.Debug($"Successfully uploaded {response.EventsProcessed} events");
|
||||
|
||||
// Reset auth failure counter on success
|
||||
_consecutiveAuthFailures = 0;
|
||||
_firstAuthFailureTime = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warning($"Upload failed: {response.Message}");
|
||||
}
|
||||
}
|
||||
catch (BackendAuthException ex)
|
||||
{
|
||||
_logger.Error($"Upload authentication failed: {ex.Message}");
|
||||
HandleAuthFailure();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error("Error uploading events", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles an authentication failure by incrementing the counter and shutting down
|
||||
/// the agent if the configured threshold is reached.
|
||||
/// </summary>
|
||||
private void HandleAuthFailure()
|
||||
{
|
||||
_consecutiveAuthFailures++;
|
||||
|
||||
if (_firstAuthFailureTime == null)
|
||||
_firstAuthFailureTime = DateTime.UtcNow;
|
||||
|
||||
_logger.Warning($"Authentication failure {_consecutiveAuthFailures}" +
|
||||
(_configuration.MaxAuthFailures > 0 ? $"/{_configuration.MaxAuthFailures}" : "") +
|
||||
$" (first failure at {_firstAuthFailureTime.Value:HH:mm:ss})");
|
||||
|
||||
// Check max attempts (0 = disabled)
|
||||
if (_configuration.MaxAuthFailures > 0 && _consecutiveAuthFailures >= _configuration.MaxAuthFailures)
|
||||
{
|
||||
_logger.Error($"=== AGENT SHUTDOWN: {_consecutiveAuthFailures} consecutive authentication failures (401/403). " +
|
||||
"The device is not authorized to send data to Autopilot Monitor. " +
|
||||
"Check client certificate and serial number validation in your tenant configuration. ===");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
// Check timeout (0 = disabled)
|
||||
if (_configuration.AuthFailureTimeoutMinutes > 0 && _firstAuthFailureTime.HasValue)
|
||||
{
|
||||
var elapsed = DateTime.UtcNow - _firstAuthFailureTime.Value;
|
||||
if (elapsed.TotalMinutes >= _configuration.AuthFailureTimeoutMinutes)
|
||||
{
|
||||
_logger.Error($"=== AGENT SHUTDOWN: Authentication failures persisted for {elapsed.TotalMinutes:F0} minutes " +
|
||||
$"(timeout: {_configuration.AuthFailureTimeoutMinutes} min). " +
|
||||
"The device is not authorized to send data to Autopilot Monitor. " +
|
||||
"Check client certificate and serial number validation in your tenant configuration. ===");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the persisted session ID file
|
||||
/// Should be called when enrollment is complete/failed
|
||||
|
||||
@@ -108,6 +108,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Network
|
||||
AddClientCertificateHeader(httpRequest);
|
||||
|
||||
var httpResponse = await _httpClient.SendAsync(httpRequest);
|
||||
ThrowOnAuthFailure(httpResponse);
|
||||
httpResponse.EnsureSuccessStatusCode();
|
||||
|
||||
var responseJson = await httpResponse.Content.ReadAsStringAsync();
|
||||
@@ -157,6 +158,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Network
|
||||
AddClientCertificateHeader(httpRequest);
|
||||
|
||||
var response = await _httpClient.SendAsync(httpRequest);
|
||||
ThrowOnAuthFailure(response);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var responseJson = await response.Content.ReadAsStringAsync();
|
||||
@@ -165,6 +167,21 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Network
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws BackendAuthException for 401/403 responses so callers can distinguish
|
||||
/// authentication failures from transient server errors.
|
||||
/// </summary>
|
||||
private static void ThrowOnAuthFailure(HttpResponseMessage response)
|
||||
{
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized ||
|
||||
response.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
||||
{
|
||||
throw new BackendAuthException(
|
||||
$"Backend returned {(int)response.StatusCode} {response.StatusCode}. " +
|
||||
"The device is not authorized. Check client certificate and serial number validation.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates NDJSON (newline-delimited JSON) from IngestEventsRequest
|
||||
/// Format: metadata line + one event per line
|
||||
@@ -254,4 +271,12 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Network
|
||||
_httpClient?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when the backend returns 401 or 403, indicating the device is not authorized.
|
||||
/// </summary>
|
||||
public class BackendAuthException : Exception
|
||||
{
|
||||
public BackendAuthException(string message) : base(message) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(new AgentConfigResponse
|
||||
{
|
||||
ConfigVersion = 2,
|
||||
ConfigVersion = 3,
|
||||
UploadIntervalSeconds = 30,
|
||||
CleanupOnExit = false,
|
||||
SelfDestructOnComplete = false,
|
||||
@@ -98,7 +98,9 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
ImeMatchLogPath = @"C:\ProgramData\AutopilotMonitor\Logs\ime_pattern_matches.log",
|
||||
Collectors = collectors,
|
||||
GatherRules = gatherRules,
|
||||
ImeLogPatterns = imeLogPatterns
|
||||
ImeLogPatterns = imeLogPatterns,
|
||||
MaxAuthFailures = tenantConfig.MaxAuthFailures ?? 5,
|
||||
AuthFailureTimeoutMinutes = tenantConfig.AuthFailureTimeoutMinutes ?? 0
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
@@ -107,4 +107,20 @@ builder.Services.AddSingleton<AnalyzeRuleService>();
|
||||
builder.Services.AddSingleton<MaintenanceService>();
|
||||
builder.Services.AddSingleton<AutopilotMonitor.Functions.Security.SerialNumberValidator>();
|
||||
|
||||
builder.Build().Run();
|
||||
var app = builder.Build();
|
||||
|
||||
// Log CORS configuration at startup so misconfigured origins are immediately visible
|
||||
// in the log stream. CORS is enforced by Azure infrastructure, not by function code,
|
||||
// so a blocked preflight never reaches the function worker and leaves no trace.
|
||||
var startupLogger = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger("Startup");
|
||||
var corsOrigins = builder.Configuration["Host:CORS"] // local.settings.json
|
||||
?? builder.Configuration["WEBSITE_CORS_ALLOWED_ORIGINS"] // Azure App Settings
|
||||
?? "(not configured - all cross-origin requests will be blocked!)";
|
||||
var corsCredentials = builder.Configuration["Host:CORSCredentials"]
|
||||
?? builder.Configuration["WEBSITE_CORS_SUPPORT_CREDENTIALS"]
|
||||
?? "unknown";
|
||||
startupLogger.LogInformation(
|
||||
"=== CORS CONFIG: AllowedOrigins={CorsOrigins} | SupportCredentials={CorsCredentials} ===",
|
||||
corsOrigins, corsCredentials);
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -51,6 +51,19 @@ namespace AutopilotMonitor.Shared.Models
|
||||
/// Delivered from backend so patterns can be updated without agent rebuild.
|
||||
/// </summary>
|
||||
public List<ImeLogPattern> ImeLogPatterns { get; set; } = new List<ImeLogPattern>();
|
||||
|
||||
/// <summary>
|
||||
/// Maximum consecutive authentication failures (401/403) before the agent shuts down.
|
||||
/// Prevents endless retry traffic when the device is not authorized.
|
||||
/// 0 = disabled (retry forever). Default: 5.
|
||||
/// </summary>
|
||||
public int MaxAuthFailures { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum time in minutes the agent keeps retrying after the first auth failure.
|
||||
/// 0 = disabled (no time limit, only MaxAuthFailures applies). Default: 0.
|
||||
/// </summary>
|
||||
public int AuthFailureTimeoutMinutes { get; set; } = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -138,6 +138,19 @@ namespace AutopilotMonitor.Shared.Models
|
||||
/// </summary>
|
||||
public int PerformanceCollectorIntervalSeconds { get; set; } = 60;
|
||||
|
||||
// ===== AGENT AUTH CIRCUIT BREAKER =====
|
||||
|
||||
/// <summary>
|
||||
/// Maximum consecutive authentication failures (401/403) before the agent shuts down.
|
||||
/// null = use default (5). 0 = disabled (retry forever).
|
||||
/// </summary>
|
||||
public int? MaxAuthFailures { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum time in minutes the agent keeps retrying after the first auth failure.
|
||||
/// null = use default (0 = disabled, only MaxAuthFailures applies).
|
||||
/// </summary>
|
||||
public int? AuthFailureTimeoutMinutes { get; set; }
|
||||
|
||||
// ===== HELPER METHODS =====
|
||||
|
||||
|
||||
Reference in New Issue
Block a user