add project
This commit is contained in:
143
Models/Configuration.cs
Normal file
143
Models/Configuration.cs
Normal file
@ -0,0 +1,143 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DatabaseSnapshotsService.Models
|
||||
{
|
||||
public class SnapshotConfiguration
|
||||
{
|
||||
[JsonPropertyName("connectionString")]
|
||||
public string ConnectionString { get; set; } = "Server=localhost;Database=trading;Uid=root;Pwd=password;";
|
||||
|
||||
[JsonPropertyName("binlogReader")]
|
||||
public BinlogReaderConfig BinlogReader { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("snapshotStorage")]
|
||||
public SnapshotStorageConfig SnapshotStorage { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("eventStore")]
|
||||
public EventStoreConfig EventStore { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("security")]
|
||||
public SecurityConfig Security { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SecurityConfig
|
||||
{
|
||||
[JsonPropertyName("encryption")]
|
||||
public bool Encryption { get; set; } = false;
|
||||
|
||||
[JsonPropertyName("encryptionKey")]
|
||||
public string? EncryptionKey { get; set; }
|
||||
}
|
||||
|
||||
public class BinlogReaderConfig
|
||||
{
|
||||
[JsonPropertyName("host")]
|
||||
public string Host { get; set; } = "localhost";
|
||||
|
||||
[JsonPropertyName("port")]
|
||||
public int Port { get; set; } = 3306;
|
||||
|
||||
[JsonPropertyName("username")]
|
||||
public string Username { get; set; } = "binlog_reader";
|
||||
|
||||
[JsonPropertyName("password")]
|
||||
public string Password { get; set; } = "secure_password";
|
||||
|
||||
[JsonPropertyName("serverId")]
|
||||
public int ServerId { get; set; } = 999;
|
||||
|
||||
[JsonPropertyName("startPosition")]
|
||||
public long StartPosition { get; set; } = 4;
|
||||
|
||||
[JsonPropertyName("heartbeatInterval")]
|
||||
public int HeartbeatInterval { get; set; } = 30;
|
||||
}
|
||||
|
||||
public class SnapshotStorageConfig
|
||||
{
|
||||
[JsonPropertyName("path")]
|
||||
public string Path { get; set; } = "./snapshots";
|
||||
|
||||
[JsonPropertyName("compression")]
|
||||
public bool Compression { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("retentionDays")]
|
||||
public int RetentionDays { get; set; } = 30;
|
||||
|
||||
[JsonPropertyName("maxFileSize")]
|
||||
public long MaxFileSize { get; set; } = 100 * 1024 * 1024; // 100MB
|
||||
|
||||
[JsonPropertyName("dumpOptimizations")]
|
||||
public DumpOptimizationConfig DumpOptimizations { get; set; } = new();
|
||||
}
|
||||
|
||||
public class DumpOptimizationConfig
|
||||
{
|
||||
[JsonPropertyName("singleTransaction")]
|
||||
public bool SingleTransaction { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("includeRoutines")]
|
||||
public bool IncludeRoutines { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("includeTriggers")]
|
||||
public bool IncludeTriggers { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("includeEvents")]
|
||||
public bool IncludeEvents { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("extendedInsert")]
|
||||
public bool ExtendedInsert { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("completeInsert")]
|
||||
public bool CompleteInsert { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("hexBlob")]
|
||||
public bool HexBlob { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("netBufferLength")]
|
||||
public int NetBufferLength { get; set; } = 16384;
|
||||
|
||||
[JsonPropertyName("maxAllowedPacket")]
|
||||
public string MaxAllowedPacket { get; set; } = "1G";
|
||||
|
||||
[JsonPropertyName("excludeTables")]
|
||||
public List<string> ExcludeTables { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("includeTables")]
|
||||
public List<string> IncludeTables { get; set; } = new();
|
||||
|
||||
// New options
|
||||
[JsonPropertyName("quick")]
|
||||
public bool Quick { get; set; } = true;
|
||||
[JsonPropertyName("orderByPrimary")]
|
||||
public bool OrderByPrimary { get; set; } = true;
|
||||
[JsonPropertyName("flushLogs")]
|
||||
public bool FlushLogs { get; set; } = true;
|
||||
[JsonPropertyName("masterData")]
|
||||
public int MasterData { get; set; } = 2;
|
||||
[JsonPropertyName("compact")]
|
||||
public bool Compact { get; set; } = false;
|
||||
[JsonPropertyName("noAutocommit")]
|
||||
public bool NoAutocommit { get; set; } = false;
|
||||
[JsonPropertyName("lockTables")]
|
||||
public bool LockTables { get; set; } = false;
|
||||
}
|
||||
|
||||
public class EventStoreConfig
|
||||
{
|
||||
[JsonPropertyName("path")]
|
||||
public string Path { get; set; } = "./events";
|
||||
|
||||
[JsonPropertyName("maxFileSize")]
|
||||
public long MaxFileSize { get; set; } = 50 * 1024 * 1024; // 50MB
|
||||
|
||||
[JsonPropertyName("retentionDays")]
|
||||
public int RetentionDays { get; set; } = 90;
|
||||
|
||||
[JsonPropertyName("batchSize")]
|
||||
public int BatchSize { get; set; } = 1000;
|
||||
|
||||
[JsonPropertyName("flushInterval")]
|
||||
public int FlushInterval { get; set; } = 5; // seconds
|
||||
}
|
||||
}
|
||||
252
Models/ConfigurationValidation.cs
Normal file
252
Models/ConfigurationValidation.cs
Normal file
@ -0,0 +1,252 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace DatabaseSnapshotsService.Models
|
||||
{
|
||||
public static class ConfigurationValidation
|
||||
{
|
||||
public static List<ValidationResult> ValidateConfiguration(SnapshotConfiguration config)
|
||||
{
|
||||
var errors = new List<ValidationResult>();
|
||||
|
||||
if (config == null)
|
||||
{
|
||||
errors.Add(new ValidationResult("Configuration cannot be null"));
|
||||
return errors;
|
||||
}
|
||||
|
||||
ValidateConnectionString(config.ConnectionString, errors);
|
||||
ValidateBinlogReader(config.BinlogReader, errors);
|
||||
ValidateSnapshotStorage(config.SnapshotStorage, errors);
|
||||
ValidateEventStore(config.EventStore, errors);
|
||||
ValidateSecurity(config.Security, errors);
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
private static void ValidateConnectionString(string connectionString, List<ValidationResult> errors)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
{
|
||||
errors.Add(new ValidationResult("Connection string cannot be empty"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic connection string validation
|
||||
var requiredParams = new[] { "Server", "Database", "Uid", "Pwd" };
|
||||
foreach (var param in requiredParams)
|
||||
{
|
||||
if (!connectionString.Contains($"{param}="))
|
||||
{
|
||||
errors.Add(new ValidationResult($"Connection string must contain {param} parameter"));
|
||||
}
|
||||
}
|
||||
|
||||
// Check for potentially dangerous patterns
|
||||
var dangerousPatterns = new[]
|
||||
{
|
||||
@"--.*", // SQL comments
|
||||
@";\s*DROP\s+", // DROP statements
|
||||
@";\s*DELETE\s+", // DELETE statements
|
||||
@";\s*TRUNCATE\s+", // TRUNCATE statements
|
||||
@";\s*ALTER\s+", // ALTER statements
|
||||
@";\s*CREATE\s+", // CREATE statements
|
||||
};
|
||||
|
||||
foreach (var pattern in dangerousPatterns)
|
||||
{
|
||||
if (Regex.IsMatch(connectionString, pattern, RegexOptions.IgnoreCase))
|
||||
{
|
||||
errors.Add(new ValidationResult($"Connection string contains potentially dangerous SQL pattern: {pattern}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateBinlogReader(BinlogReaderConfig config, List<ValidationResult> errors)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(config.Host))
|
||||
{
|
||||
errors.Add(new ValidationResult("Binlog reader host cannot be empty"));
|
||||
}
|
||||
|
||||
if (config.Port < 1 || config.Port > 65535)
|
||||
{
|
||||
errors.Add(new ValidationResult("Binlog reader port must be between 1 and 65535"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.Username))
|
||||
{
|
||||
errors.Add(new ValidationResult("Binlog reader username cannot be empty"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.Password))
|
||||
{
|
||||
errors.Add(new ValidationResult("Binlog reader password cannot be empty"));
|
||||
}
|
||||
|
||||
if (config.ServerId < 1 || config.ServerId > 4294967295)
|
||||
{
|
||||
errors.Add(new ValidationResult("Binlog reader server ID must be between 1 and 4294967295"));
|
||||
}
|
||||
|
||||
if (config.StartPosition < 4)
|
||||
{
|
||||
errors.Add(new ValidationResult("Binlog reader start position must be at least 4"));
|
||||
}
|
||||
|
||||
if (config.HeartbeatInterval < 1 || config.HeartbeatInterval > 3600)
|
||||
{
|
||||
errors.Add(new ValidationResult("Binlog reader heartbeat interval must be between 1 and 3600 seconds"));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateSnapshotStorage(SnapshotStorageConfig config, List<ValidationResult> errors)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(config.Path))
|
||||
{
|
||||
errors.Add(new ValidationResult("Snapshot storage path cannot be empty"));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check for path traversal attempts
|
||||
var normalizedPath = Path.GetFullPath(config.Path);
|
||||
if (normalizedPath.Contains("..") || normalizedPath.Contains("~"))
|
||||
{
|
||||
errors.Add(new ValidationResult("Snapshot storage path contains invalid characters"));
|
||||
}
|
||||
}
|
||||
|
||||
if (config.RetentionDays < 1 || config.RetentionDays > 3650) // Max 10 years
|
||||
{
|
||||
errors.Add(new ValidationResult("Snapshot retention days must be between 1 and 3650"));
|
||||
}
|
||||
|
||||
if (config.MaxFileSize < 1024 * 1024 || config.MaxFileSize > 10L * 1024 * 1024 * 1024) // 1MB to 10GB
|
||||
{
|
||||
errors.Add(new ValidationResult("Snapshot max file size must be between 1MB and 10GB"));
|
||||
}
|
||||
|
||||
// Validate dump optimizations
|
||||
ValidateDumpOptimizations(config.DumpOptimizations, errors);
|
||||
}
|
||||
|
||||
private static void ValidateDumpOptimizations(DumpOptimizationConfig config, List<ValidationResult> errors)
|
||||
{
|
||||
if (config.NetBufferLength < 1024 || config.NetBufferLength > 1048576) // 1KB to 1MB
|
||||
{
|
||||
errors.Add(new ValidationResult("Net buffer length must be between 1024 and 1048576 bytes"));
|
||||
}
|
||||
|
||||
// Validate max allowed packet format (e.g., "1G", "512M", "1024K")
|
||||
if (!string.IsNullOrWhiteSpace(config.MaxAllowedPacket))
|
||||
{
|
||||
var packetPattern = @"^\d+[KMGT]?$";
|
||||
if (!Regex.IsMatch(config.MaxAllowedPacket, packetPattern))
|
||||
{
|
||||
errors.Add(new ValidationResult("Max allowed packet must be in format: number[K|M|G|T] (e.g., '1G', '512M')"));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate table names in exclude/include lists
|
||||
foreach (var table in config.ExcludeTables.Concat(config.IncludeTables))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(table))
|
||||
{
|
||||
errors.Add(new ValidationResult("Table names cannot be empty"));
|
||||
}
|
||||
else if (!Regex.IsMatch(table, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
|
||||
{
|
||||
errors.Add(new ValidationResult($"Invalid table name format: {table}"));
|
||||
}
|
||||
}
|
||||
|
||||
// Check for conflicts between include and exclude tables
|
||||
var conflicts = config.IncludeTables.Intersect(config.ExcludeTables, StringComparer.OrdinalIgnoreCase);
|
||||
if (conflicts.Any())
|
||||
{
|
||||
errors.Add(new ValidationResult($"Table(s) cannot be both included and excluded: {string.Join(", ", conflicts)}"));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateEventStore(EventStoreConfig config, List<ValidationResult> errors)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(config.Path))
|
||||
{
|
||||
errors.Add(new ValidationResult("Event store path cannot be empty"));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check for path traversal attempts
|
||||
var normalizedPath = Path.GetFullPath(config.Path);
|
||||
if (normalizedPath.Contains("..") || normalizedPath.Contains("~"))
|
||||
{
|
||||
errors.Add(new ValidationResult("Event store path contains invalid characters"));
|
||||
}
|
||||
}
|
||||
|
||||
if (config.MaxFileSize < 1024 * 1024 || config.MaxFileSize > 5L * 1024 * 1024 * 1024) // 1MB to 5GB
|
||||
{
|
||||
errors.Add(new ValidationResult("Event store max file size must be between 1MB and 5GB"));
|
||||
}
|
||||
|
||||
if (config.RetentionDays < 1 || config.RetentionDays > 3650) // Max 10 years
|
||||
{
|
||||
errors.Add(new ValidationResult("Event store retention days must be between 1 and 3650"));
|
||||
}
|
||||
|
||||
if (config.BatchSize < 1 || config.BatchSize > 10000)
|
||||
{
|
||||
errors.Add(new ValidationResult("Event store batch size must be between 1 and 10000"));
|
||||
}
|
||||
|
||||
if (config.FlushInterval < 1 || config.FlushInterval > 300) // 1 second to 5 minutes
|
||||
{
|
||||
errors.Add(new ValidationResult("Event store flush interval must be between 1 and 300 seconds"));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateSecurity(SecurityConfig config, List<ValidationResult> errors)
|
||||
{
|
||||
if (config.Encryption && string.IsNullOrWhiteSpace(config.EncryptionKey))
|
||||
{
|
||||
errors.Add(new ValidationResult("Encryption key is required when encryption is enabled"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(config.EncryptionKey))
|
||||
{
|
||||
if (config.EncryptionKey.Length < 32)
|
||||
{
|
||||
errors.Add(new ValidationResult("Encryption key must be at least 32 characters long"));
|
||||
}
|
||||
|
||||
// Check for weak encryption keys
|
||||
if (IsWeakEncryptionKey(config.EncryptionKey))
|
||||
{
|
||||
errors.Add(new ValidationResult("Encryption key is too weak. Use a stronger key with mixed characters"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsWeakEncryptionKey(string key)
|
||||
{
|
||||
// Check for common weak patterns
|
||||
if (key.Length < 32) return true;
|
||||
|
||||
// Check for repeated characters
|
||||
if (Regex.IsMatch(key, @"(.)\1{10,}")) return true;
|
||||
|
||||
// Check for sequential characters
|
||||
if (Regex.IsMatch(key, @"(?:abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz)", RegexOptions.IgnoreCase)) return true;
|
||||
|
||||
// Check for common weak keys
|
||||
var weakKeys = new[]
|
||||
{
|
||||
"password", "123456", "qwerty", "admin", "root", "secret", "key",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"12345678901234567890123456789012"
|
||||
};
|
||||
|
||||
return weakKeys.Any(wk => key.Equals(wk, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
||||
242
Models/DataModels.cs
Normal file
242
Models/DataModels.cs
Normal file
@ -0,0 +1,242 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DatabaseSnapshotsService.Models
|
||||
{
|
||||
public class SnapshotInfo
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("timestamp")]
|
||||
public long Timestamp { get; set; }
|
||||
|
||||
[JsonPropertyName("dataSize")]
|
||||
public long DataSize { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
[JsonPropertyName("userId")]
|
||||
public int? UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("filePath")]
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("checksum")]
|
||||
public string Checksum { get; set; } = string.Empty;
|
||||
|
||||
// Binlog fields for incremental snapshots
|
||||
[JsonPropertyName("binlogFile")]
|
||||
public string? BinlogFile { get; set; }
|
||||
|
||||
[JsonPropertyName("binlogPosition")]
|
||||
public long? BinlogPosition { get; set; }
|
||||
|
||||
[JsonPropertyName("parentSnapshotId")]
|
||||
public int? ParentSnapshotId { get; set; }
|
||||
|
||||
[JsonPropertyName("incrementalBinlogStartFile")]
|
||||
public string? IncrementalBinlogStartFile { get; set; }
|
||||
|
||||
[JsonPropertyName("incrementalBinlogStartPosition")]
|
||||
public long? IncrementalBinlogStartPosition { get; set; }
|
||||
|
||||
[JsonPropertyName("incrementalBinlogEndFile")]
|
||||
public string? IncrementalBinlogEndFile { get; set; }
|
||||
|
||||
[JsonPropertyName("incrementalBinlogEndPosition")]
|
||||
public long? IncrementalBinlogEndPosition { get; set; }
|
||||
}
|
||||
|
||||
public class DatabaseEvent
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[JsonPropertyName("timestamp")]
|
||||
public long Timestamp { get; set; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("table")]
|
||||
public string Table { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("operation")]
|
||||
public string Operation { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("data")]
|
||||
public string Data { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("binlogPosition")]
|
||||
public long BinlogPosition { get; set; }
|
||||
|
||||
[JsonPropertyName("serverId")]
|
||||
public int ServerId { get; set; }
|
||||
|
||||
[JsonPropertyName("checksum")]
|
||||
public string Checksum { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class RecoveryPoint
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("timestamp")]
|
||||
public long Timestamp { get; set; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
[JsonPropertyName("eventCount")]
|
||||
public long EventCount { get; set; }
|
||||
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("lastEventId")]
|
||||
public long LastEventId { get; set; }
|
||||
}
|
||||
|
||||
public class ServiceStatus
|
||||
{
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; } = "Unknown";
|
||||
|
||||
[JsonPropertyName("databaseConnected")]
|
||||
public bool DatabaseConnected { get; set; }
|
||||
|
||||
[JsonPropertyName("binlogReaderStatus")]
|
||||
public string BinlogReaderStatus { get; set; } = "Unknown";
|
||||
|
||||
[JsonPropertyName("lastEventProcessed")]
|
||||
public long LastEventProcessed { get; set; }
|
||||
|
||||
[JsonPropertyName("totalEvents")]
|
||||
public long TotalEvents { get; set; }
|
||||
|
||||
[JsonPropertyName("activeSnapshots")]
|
||||
public int ActiveSnapshots { get; set; }
|
||||
|
||||
[JsonPropertyName("uptime")]
|
||||
public TimeSpan Uptime { get; set; }
|
||||
|
||||
[JsonPropertyName("lastSnapshot")]
|
||||
public DateTime? LastSnapshot { get; set; }
|
||||
}
|
||||
|
||||
public class HealthStatus
|
||||
{
|
||||
[JsonPropertyName("isHealthy")]
|
||||
public bool IsHealthy { get; set; }
|
||||
|
||||
[JsonPropertyName("errorMessage")]
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
[JsonPropertyName("checks")]
|
||||
public Dictionary<string, bool> Checks { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("timestamp")]
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class RestorePreview
|
||||
{
|
||||
[JsonPropertyName("targetTimestamp")]
|
||||
public long TargetTimestamp { get; set; }
|
||||
|
||||
[JsonPropertyName("eventCount")]
|
||||
public long EventCount { get; set; }
|
||||
|
||||
[JsonPropertyName("affectedTables")]
|
||||
public List<string> AffectedTables { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("estimatedDuration")]
|
||||
public TimeSpan EstimatedDuration { get; set; }
|
||||
|
||||
[JsonPropertyName("snapshotId")]
|
||||
public int? SnapshotId { get; set; }
|
||||
|
||||
[JsonPropertyName("warnings")]
|
||||
public List<string> Warnings { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SnapshotMetadata
|
||||
{
|
||||
[JsonPropertyName("version")]
|
||||
public string Version { get; set; } = "1.0";
|
||||
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("databaseVersion")]
|
||||
public string DatabaseVersion { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("tables")]
|
||||
public List<TableInfo> Tables { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("checksum")]
|
||||
public string Checksum { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("compression")]
|
||||
public bool Compression { get; set; }
|
||||
|
||||
[JsonPropertyName("encryption")]
|
||||
public bool Encryption { get; set; }
|
||||
}
|
||||
|
||||
public class TableInfo
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("rowCount")]
|
||||
public long RowCount { get; set; }
|
||||
|
||||
[JsonPropertyName("dataSize")]
|
||||
public long DataSize { get; set; }
|
||||
|
||||
[JsonPropertyName("indexSize")]
|
||||
public long IndexSize { get; set; }
|
||||
|
||||
[JsonPropertyName("checksum")]
|
||||
public string Checksum { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public enum SnapshotType
|
||||
{
|
||||
Full,
|
||||
Trading,
|
||||
User,
|
||||
Incremental
|
||||
}
|
||||
|
||||
public enum EventOperation
|
||||
{
|
||||
Insert,
|
||||
Update,
|
||||
Delete,
|
||||
Truncate
|
||||
}
|
||||
|
||||
public enum SnapshotStatus
|
||||
{
|
||||
Creating,
|
||||
Completed,
|
||||
Failed,
|
||||
Corrupted
|
||||
}
|
||||
}
|
||||
438
Models/InputValidation.cs
Normal file
438
Models/InputValidation.cs
Normal file
@ -0,0 +1,438 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace DatabaseSnapshotsService.Models
|
||||
{
|
||||
public static class InputValidation
|
||||
{
|
||||
// Validation patterns
|
||||
private static readonly Regex ValidFileNamePattern = new(@"^[a-zA-Z0-9._-]+$", RegexOptions.Compiled);
|
||||
private static readonly Regex ValidPathPattern = new(@"^[a-zA-Z0-9/._-]+$", RegexOptions.Compiled);
|
||||
private static readonly Regex ValidNamePattern = new(@"^[a-zA-Z0-9\s._-]{1,100}$", RegexOptions.Compiled);
|
||||
private static readonly Regex ValidDescriptionPattern = new(@"^[a-zA-Z0-9\s.,!?@#$%^&*()_+-=:;'""<>/\\|`~]{0,500}$", RegexOptions.Compiled);
|
||||
private static readonly Regex ValidTableNamePattern = new(@"^[a-zA-Z][a-zA-Z0-9_]*$", RegexOptions.Compiled);
|
||||
private static readonly Regex ValidOperationPattern = new(@"^(insert|update|delete|truncate|query)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex ValidEventTypePattern = new(@"^(binlog|status|error|info)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
// Dangerous patterns to detect
|
||||
private static readonly Regex[] DangerousPatterns = {
|
||||
new(@"\.\./", RegexOptions.Compiled), // Path traversal
|
||||
new(@"\.\.\\", RegexOptions.Compiled), // Windows path traversal
|
||||
new(@"[<>""'&]", RegexOptions.Compiled), // HTML/XML injection
|
||||
new(@"(union|select|insert|update|delete|drop|create|alter|exec|execute|script|javascript|vbscript|onload|onerror)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // SQL/script injection
|
||||
new(@"(\x00|\x01|\x02|\x03|\x04|\x05|\x06|\x07|\x08|\x0B|\x0C|\x0E|\x0F|\x10|\x11|\x12|\x13|\x14|\x15|\x16|\x17|\x18|\x19|\x1A|\x1B|\x1C|\x1D|\x1E|\x1F)", RegexOptions.Compiled), // Control characters
|
||||
};
|
||||
|
||||
public static class SnapshotValidation
|
||||
{
|
||||
public static ValidationResult ValidateSnapshotName(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return new ValidationResult("Snapshot name cannot be empty");
|
||||
}
|
||||
|
||||
if (name.Length > 100)
|
||||
{
|
||||
return new ValidationResult("Snapshot name cannot exceed 100 characters");
|
||||
}
|
||||
|
||||
if (!ValidNamePattern.IsMatch(name))
|
||||
{
|
||||
return new ValidationResult("Snapshot name contains invalid characters. Use only letters, numbers, spaces, dots, underscores, and hyphens");
|
||||
}
|
||||
|
||||
if (ContainsDangerousPatterns(name))
|
||||
{
|
||||
return new ValidationResult("Snapshot name contains potentially dangerous content");
|
||||
}
|
||||
|
||||
return ValidationResult.Success!;
|
||||
}
|
||||
|
||||
public static ValidationResult ValidateSnapshotDescription(string? description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
return ValidationResult.Success!; // Description is optional
|
||||
}
|
||||
|
||||
if (description.Length > 500)
|
||||
{
|
||||
return new ValidationResult("Snapshot description cannot exceed 500 characters");
|
||||
}
|
||||
|
||||
if (!ValidDescriptionPattern.IsMatch(description))
|
||||
{
|
||||
return new ValidationResult("Snapshot description contains invalid characters");
|
||||
}
|
||||
|
||||
if (ContainsDangerousPatterns(description))
|
||||
{
|
||||
return new ValidationResult("Snapshot description contains potentially dangerous content");
|
||||
}
|
||||
|
||||
return ValidationResult.Success!;
|
||||
}
|
||||
|
||||
public static ValidationResult ValidateSnapshotId(int id)
|
||||
{
|
||||
if (id <= 0)
|
||||
{
|
||||
return new ValidationResult("Snapshot ID must be a positive integer");
|
||||
}
|
||||
|
||||
if (id > int.MaxValue)
|
||||
{
|
||||
return new ValidationResult("Snapshot ID is too large");
|
||||
}
|
||||
|
||||
return ValidationResult.Success!;
|
||||
}
|
||||
|
||||
public static ValidationResult ValidateSnapshotType(string type)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(type))
|
||||
{
|
||||
return new ValidationResult("Snapshot type cannot be empty");
|
||||
}
|
||||
|
||||
var validTypes = new[] { "full", "incremental", "trading", "user" };
|
||||
if (!validTypes.Contains(type.ToLowerInvariant()))
|
||||
{
|
||||
return new ValidationResult($"Invalid snapshot type. Must be one of: {string.Join(", ", validTypes)}");
|
||||
}
|
||||
|
||||
return ValidationResult.Success!;
|
||||
}
|
||||
}
|
||||
|
||||
public static class EventValidation
|
||||
{
|
||||
public static ValidationResult ValidateTableName(string tableName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tableName))
|
||||
{
|
||||
return new ValidationResult("Table name cannot be empty");
|
||||
}
|
||||
|
||||
if (tableName.Length > 64)
|
||||
{
|
||||
return new ValidationResult("Table name cannot exceed 64 characters");
|
||||
}
|
||||
|
||||
if (!ValidTableNamePattern.IsMatch(tableName))
|
||||
{
|
||||
return new ValidationResult("Table name contains invalid characters. Use only letters, numbers, and underscores, starting with a letter");
|
||||
}
|
||||
|
||||
if (ContainsDangerousPatterns(tableName))
|
||||
{
|
||||
return new ValidationResult("Table name contains potentially dangerous content");
|
||||
}
|
||||
|
||||
return ValidationResult.Success!;
|
||||
}
|
||||
|
||||
public static ValidationResult ValidateOperation(string operation)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(operation))
|
||||
{
|
||||
return new ValidationResult("Operation cannot be empty");
|
||||
}
|
||||
|
||||
if (!ValidOperationPattern.IsMatch(operation))
|
||||
{
|
||||
return new ValidationResult("Invalid operation. Must be one of: insert, update, delete, truncate, query");
|
||||
}
|
||||
|
||||
return ValidationResult.Success!;
|
||||
}
|
||||
|
||||
public static ValidationResult ValidateEventType(string eventType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(eventType))
|
||||
{
|
||||
return new ValidationResult("Event type cannot be empty");
|
||||
}
|
||||
|
||||
if (!ValidEventTypePattern.IsMatch(eventType))
|
||||
{
|
||||
return new ValidationResult("Invalid event type. Must be one of: binlog, status, error, info");
|
||||
}
|
||||
|
||||
return ValidationResult.Success!;
|
||||
}
|
||||
|
||||
public static ValidationResult ValidateEventData(string data)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(data))
|
||||
{
|
||||
return new ValidationResult("Event data cannot be empty");
|
||||
}
|
||||
|
||||
if (data.Length > 10000) // 10KB limit
|
||||
{
|
||||
return new ValidationResult("Event data cannot exceed 10KB");
|
||||
}
|
||||
|
||||
if (ContainsDangerousPatterns(data))
|
||||
{
|
||||
return new ValidationResult("Event data contains potentially dangerous content");
|
||||
}
|
||||
|
||||
return ValidationResult.Success!;
|
||||
}
|
||||
|
||||
public static ValidationResult ValidateLimit(int limit)
|
||||
{
|
||||
if (limit <= 0)
|
||||
{
|
||||
return new ValidationResult("Limit must be a positive integer");
|
||||
}
|
||||
|
||||
if (limit > 10000)
|
||||
{
|
||||
return new ValidationResult("Limit cannot exceed 10000");
|
||||
}
|
||||
|
||||
return ValidationResult.Success!;
|
||||
}
|
||||
|
||||
public static ValidationResult ValidateTimestamp(long timestamp)
|
||||
{
|
||||
if (timestamp < 0)
|
||||
{
|
||||
return new ValidationResult("Timestamp cannot be negative");
|
||||
}
|
||||
|
||||
var maxTimestamp = DateTimeOffset.MaxValue.ToUnixTimeSeconds();
|
||||
if (timestamp > maxTimestamp)
|
||||
{
|
||||
return new ValidationResult("Timestamp is too far in the future");
|
||||
}
|
||||
|
||||
return ValidationResult.Success!;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RecoveryValidation
|
||||
{
|
||||
public static ValidationResult ValidateRecoveryPointName(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return new ValidationResult("Recovery point name cannot be empty");
|
||||
}
|
||||
|
||||
if (name.Length > 100)
|
||||
{
|
||||
return new ValidationResult("Recovery point name cannot exceed 100 characters");
|
||||
}
|
||||
|
||||
if (!ValidNamePattern.IsMatch(name))
|
||||
{
|
||||
return new ValidationResult("Recovery point name contains invalid characters. Use only letters, numbers, spaces, dots, underscores, and hyphens");
|
||||
}
|
||||
|
||||
if (ContainsDangerousPatterns(name))
|
||||
{
|
||||
return new ValidationResult("Recovery point name contains potentially dangerous content");
|
||||
}
|
||||
|
||||
return ValidationResult.Success!;
|
||||
}
|
||||
|
||||
public static ValidationResult ValidateRecoveryPointDescription(string? description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
return ValidationResult.Success!; // Description is optional
|
||||
}
|
||||
|
||||
if (description.Length > 500)
|
||||
{
|
||||
return new ValidationResult("Recovery point description cannot exceed 500 characters");
|
||||
}
|
||||
|
||||
if (!ValidDescriptionPattern.IsMatch(description))
|
||||
{
|
||||
return new ValidationResult("Recovery point description contains invalid characters");
|
||||
}
|
||||
|
||||
if (ContainsDangerousPatterns(description))
|
||||
{
|
||||
return new ValidationResult("Recovery point description contains potentially dangerous content");
|
||||
}
|
||||
|
||||
return ValidationResult.Success!;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FileValidation
|
||||
{
|
||||
public static ValidationResult ValidateFilePath(string filePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
{
|
||||
return new ValidationResult("File path cannot be empty");
|
||||
}
|
||||
|
||||
if (filePath.Length > 260) // Windows path limit
|
||||
{
|
||||
return new ValidationResult("File path cannot exceed 260 characters");
|
||||
}
|
||||
|
||||
if (!ValidPathPattern.IsMatch(filePath))
|
||||
{
|
||||
return new ValidationResult("File path contains invalid characters");
|
||||
}
|
||||
|
||||
if (ContainsDangerousPatterns(filePath))
|
||||
{
|
||||
return new ValidationResult("File path contains potentially dangerous content");
|
||||
}
|
||||
|
||||
// Check for path traversal attempts
|
||||
var normalizedPath = Path.GetFullPath(filePath);
|
||||
if (normalizedPath.Contains("..") || normalizedPath.Contains("~"))
|
||||
{
|
||||
return new ValidationResult("File path contains invalid path traversal characters");
|
||||
}
|
||||
|
||||
return ValidationResult.Success!;
|
||||
}
|
||||
|
||||
public static ValidationResult ValidateFileName(string fileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
return new ValidationResult("File name cannot be empty");
|
||||
}
|
||||
|
||||
if (fileName.Length > 255)
|
||||
{
|
||||
return new ValidationResult("File name cannot exceed 255 characters");
|
||||
}
|
||||
|
||||
if (!ValidFileNamePattern.IsMatch(fileName))
|
||||
{
|
||||
return new ValidationResult("File name contains invalid characters. Use only letters, numbers, dots, underscores, and hyphens");
|
||||
}
|
||||
|
||||
if (ContainsDangerousPatterns(fileName))
|
||||
{
|
||||
return new ValidationResult("File name contains potentially dangerous content");
|
||||
}
|
||||
|
||||
// Check for reserved Windows filenames
|
||||
var reservedNames = new[]
|
||||
{
|
||||
"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
|
||||
};
|
||||
|
||||
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName).ToUpperInvariant();
|
||||
if (reservedNames.Contains(fileNameWithoutExtension))
|
||||
{
|
||||
return new ValidationResult("File name is a reserved system name");
|
||||
}
|
||||
|
||||
return ValidationResult.Success!;
|
||||
}
|
||||
}
|
||||
|
||||
public static class DataValidation
|
||||
{
|
||||
public static ValidationResult ValidateDataContains(string dataContains)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dataContains))
|
||||
{
|
||||
return new ValidationResult("Data contains filter cannot be empty");
|
||||
}
|
||||
|
||||
if (dataContains.Length > 1000)
|
||||
{
|
||||
return new ValidationResult("Data contains filter cannot exceed 1000 characters");
|
||||
}
|
||||
|
||||
if (ContainsDangerousPatterns(dataContains))
|
||||
{
|
||||
return new ValidationResult("Data contains filter contains potentially dangerous content");
|
||||
}
|
||||
|
||||
return ValidationResult.Success!;
|
||||
}
|
||||
|
||||
public static ValidationResult ValidateOutputFile(string outputFile)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(outputFile))
|
||||
{
|
||||
return new ValidationResult("Output file path cannot be empty");
|
||||
}
|
||||
|
||||
return FileValidation.ValidateFilePath(outputFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ContainsDangerousPatterns(string input)
|
||||
{
|
||||
return DangerousPatterns.Any(pattern => pattern.IsMatch(input));
|
||||
}
|
||||
|
||||
public static string SanitizeString(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Remove control characters
|
||||
var sanitized = Regex.Replace(input, @"[\x00-\x1F\x7F]", "");
|
||||
|
||||
// Trim whitespace
|
||||
sanitized = sanitized.Trim();
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
public static string SanitizeFileName(string fileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Remove invalid characters for file names
|
||||
var sanitized = Regex.Replace(fileName, @"[<>:""/\\|?*\x00-\x1F]", "");
|
||||
|
||||
// Trim and limit length
|
||||
sanitized = sanitized.Trim();
|
||||
if (sanitized.Length > 255)
|
||||
{
|
||||
sanitized = sanitized.Substring(0, 255);
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
public static string SanitizePath(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Remove path traversal attempts
|
||||
var sanitized = path.Replace("..", "").Replace("~", "");
|
||||
|
||||
// Remove invalid characters
|
||||
sanitized = Regex.Replace(sanitized, @"[<>""|?\x00-\x1F]", "");
|
||||
|
||||
// Normalize path separators
|
||||
sanitized = sanitized.Replace('\\', '/');
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user