Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
using System.Data.Common;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using Omika.Data.Configuration;
|
||||
using Omika.Data.HfSql.Connection;
|
||||
|
||||
namespace HfSqlOleDbAudit;
|
||||
|
||||
/// <summary>
|
||||
/// Mode <c>connection-test</c> : valide les trois connexions HFSQL de Production
|
||||
/// (OLE DB <c>PCSOFT.HFSQL</c>) en LECTURE SEULE, sans démarrer Omika.Api ni
|
||||
/// Omika.Worker.
|
||||
///
|
||||
/// <para>
|
||||
/// Parité Production exacte : la chaîne de connexion OLE DB est construite par
|
||||
/// <see cref="HfSqlConnectionOptionsFactory"/> (Create / CreateForLicense /
|
||||
/// CreateForService), <see cref="HfSqlOleDbConnectionStringBuilder"/> et
|
||||
/// <see cref="HfSqlOleDbConnectionFactory"/> — les mêmes classes que
|
||||
/// <c>BusinessOleDbConnection</c>, <c>LicenseOleDbConnection</c> et
|
||||
/// <c>ServiceOleDbConnection</c>. Les mots de passe de fichiers
|
||||
/// (ProtectionStd / ProtectionPlus) sont injectés dans les Extended Properties
|
||||
/// par le mécanisme Production (réflexion sur <c>HfSqlTableAttribute</c>),
|
||||
/// jamais dérivés, jamais calculés (aucun MD5, aucun wildcard improvisé).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Sécurité : lecture seule stricte (aucune commande d'écriture de données ni de
|
||||
/// schéma), aucune écriture de fichier, aucun secret en argument CLI, aucun
|
||||
/// secret affiché (seuls des indicateurs YES/NO et un code d'erreur numérique
|
||||
/// sont émis). Aucune chaîne de connexion complète n'est jamais affichée.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class ConnectionTest
|
||||
{
|
||||
public const string CommandName = "connection-test";
|
||||
|
||||
private static readonly Regex HfSqlErrorCodeRegex = new(
|
||||
@"Code\s+erreur\s*:\s*(\d+)|Error\s+code\s*:\s*(\d+)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
/// <summary>Sections de configuration supportées (strictement).</summary>
|
||||
public enum Section
|
||||
{
|
||||
Hfsql,
|
||||
LicenseConnection,
|
||||
ServiceConnection
|
||||
}
|
||||
|
||||
/// <summary>Arguments CLI validés du mode connection-test.</summary>
|
||||
public sealed record Arguments(string ConfigPath, Section SectionValue, string? Table);
|
||||
|
||||
/// <summary>
|
||||
/// Configuration de connexion résolue depuis appsettings.Production.json,
|
||||
/// avec la vue OLE DB Production (<see cref="HfSqlConnectionOptions"/>).
|
||||
/// Ne contient jamais les valeurs secrètes en clair dans sa représentation.
|
||||
/// </summary>
|
||||
public sealed record Config(
|
||||
Section SectionValue,
|
||||
string Server,
|
||||
int Port,
|
||||
string DatabaseName,
|
||||
string Login,
|
||||
bool DatabasePasswordPresent,
|
||||
bool ProtectionStdPresent,
|
||||
bool ProtectionPlusPresent,
|
||||
HfSqlConnectionOptions ConnectionOptions);
|
||||
|
||||
// ================================================================
|
||||
// ENTRY
|
||||
// ================================================================
|
||||
|
||||
public static int Run(string[] args) => Run(args, Console.Out);
|
||||
|
||||
public static int Run(string[] args, TextWriter output)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
ArgumentNullException.ThrowIfNull(output);
|
||||
|
||||
if (!TryParseArguments(args, out Arguments? arguments, out string? argError) || arguments is null)
|
||||
{
|
||||
output.WriteLine("ERROR=" + argError);
|
||||
PrintUsage(output);
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (!TryLoadConfig(arguments.ConfigPath, arguments.SectionValue, out Config? config, out string? configError) || config is null)
|
||||
{
|
||||
output.WriteLine("ERROR=" + configError);
|
||||
return 2;
|
||||
}
|
||||
|
||||
foreach (string line in BuildSummaryLines(config))
|
||||
{
|
||||
output.WriteLine(line);
|
||||
}
|
||||
|
||||
var factory = new HfSqlOleDbConnectionFactory();
|
||||
|
||||
// La connexion est créée puis TOUJOURS disposée (y compris si l'ouverture
|
||||
// échoue) : le provider OLE DB natif PCSOFT.HFSQL doit être libéré
|
||||
// proprement, sinon le processus peut se terminer anormalement à la
|
||||
// finalisation de l'objet non disposé.
|
||||
DbConnection connection;
|
||||
try
|
||||
{
|
||||
connection = factory.Create(config.ConnectionOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
output.WriteLine("CONNECTION_OPEN=FAIL");
|
||||
output.WriteLine("ERROR_TYPE=" + SanitizeErrorType(ex));
|
||||
output.WriteLine("HFSQL_ERROR_CODE=" + (ExtractHfSqlErrorCode(ex) ?? "NONE"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
using (connection)
|
||||
{
|
||||
try
|
||||
{
|
||||
connection.Open();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Jamais de message brut : uniquement le type et le code numérique.
|
||||
output.WriteLine("CONNECTION_OPEN=FAIL");
|
||||
output.WriteLine("ERROR_TYPE=" + SanitizeErrorType(ex));
|
||||
output.WriteLine("HFSQL_ERROR_CODE=" + (ExtractHfSqlErrorCode(ex) ?? "NONE"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
output.WriteLine("CONNECTION_OPEN=PASS");
|
||||
|
||||
if (arguments.Table is not null)
|
||||
{
|
||||
bool ok = TrySelect(connection, arguments.Table, out int fieldCount, out bool hasRows, out string? code);
|
||||
output.WriteLine("TABLE=" + arguments.Table);
|
||||
output.WriteLine("SELECT=" + (ok ? "PASS" : "FAIL"));
|
||||
output.WriteLine("FIELD_COUNT=" + (ok ? fieldCount.ToString() : "0"));
|
||||
output.WriteLine("HAS_ROWS=" + (ok && hasRows ? "YES" : "NO"));
|
||||
output.WriteLine("HFSQL_ERROR_CODE=" + (code ?? "NONE"));
|
||||
if (!ok)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// ARGUMENT PARSING
|
||||
// ================================================================
|
||||
|
||||
public static bool TryParseArguments(IReadOnlyList<string> args, out Arguments? arguments, out string? error)
|
||||
{
|
||||
arguments = null;
|
||||
error = null;
|
||||
|
||||
// args[0] = "connection-test", args[1] = <configPath>, args[2] = <section>, args[3]? = <table>.
|
||||
if (args.Count < 3)
|
||||
{
|
||||
error = "INVALID_ARGUMENTS";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryParseSection(args[2], out Section section))
|
||||
{
|
||||
error = "UNSUPPORTED_SECTION";
|
||||
return false;
|
||||
}
|
||||
|
||||
string? table = null;
|
||||
if (args.Count >= 4)
|
||||
{
|
||||
table = args[3];
|
||||
if (!IsValidTableIdentifier(table))
|
||||
{
|
||||
error = "INVALID_TABLE_IDENTIFIER";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
arguments = new Arguments(args[1], section, table);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryParseSection(string section, out Section parsed)
|
||||
{
|
||||
if (section.Equals("Hfsql", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
parsed = Section.Hfsql;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (section.Equals("LicenseConnection", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
parsed = Section.LicenseConnection;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (section.Equals("ServiceConnection", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
parsed = Section.ServiceConnection;
|
||||
return true;
|
||||
}
|
||||
|
||||
parsed = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// CONFIG LOADING (lecture seule)
|
||||
// ================================================================
|
||||
|
||||
public static bool TryLoadConfig(string configPath, Section section, out Config? config, out string? error)
|
||||
{
|
||||
config = null;
|
||||
error = null;
|
||||
|
||||
string json;
|
||||
try
|
||||
{
|
||||
json = File.ReadAllText(configPath);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
error = "CONFIG_FILE_NOT_FOUND";
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonDocument document;
|
||||
try
|
||||
{
|
||||
document = JsonDocument.Parse(json);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
error = "CONFIG_PARSE_FAILED";
|
||||
return false;
|
||||
}
|
||||
|
||||
using (document)
|
||||
{
|
||||
string sectionName = SectionName(section);
|
||||
if (!document.RootElement.TryGetProperty(sectionName, out JsonElement sectionElement)
|
||||
|| sectionElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
error = "SECTION_NOT_FOUND";
|
||||
return false;
|
||||
}
|
||||
|
||||
var jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||
|
||||
HfSqlConnectionOptions connectionOptions;
|
||||
string server;
|
||||
int port;
|
||||
string databaseName;
|
||||
string login;
|
||||
|
||||
try
|
||||
{
|
||||
switch (section)
|
||||
{
|
||||
case Section.Hfsql:
|
||||
{
|
||||
var options = sectionElement.Deserialize<HfsqlOptions>(jsonOptions) ?? new HfsqlOptions();
|
||||
connectionOptions = HfSqlConnectionOptionsFactory.Create(options, options.FilePasswords);
|
||||
server = options.Server ?? string.Empty;
|
||||
port = options.Port;
|
||||
databaseName = options.DatabaseName ?? string.Empty;
|
||||
login = options.Login ?? string.Empty;
|
||||
break;
|
||||
}
|
||||
case Section.LicenseConnection:
|
||||
{
|
||||
var options = sectionElement.Deserialize<LicenseConnectionOptions>(jsonOptions) ?? new LicenseConnectionOptions();
|
||||
connectionOptions = HfSqlConnectionOptionsFactory.CreateForLicense(options);
|
||||
server = options.Server ?? string.Empty;
|
||||
port = options.Port;
|
||||
databaseName = options.DatabaseName ?? string.Empty;
|
||||
login = options.Login ?? string.Empty;
|
||||
break;
|
||||
}
|
||||
case Section.ServiceConnection:
|
||||
{
|
||||
var options = sectionElement.Deserialize<ServiceConnectionOptions>(jsonOptions) ?? new ServiceConnectionOptions();
|
||||
connectionOptions = HfSqlConnectionOptionsFactory.CreateForService(options);
|
||||
server = options.Server ?? string.Empty;
|
||||
port = options.Port;
|
||||
databaseName = options.DatabaseName ?? string.Empty;
|
||||
login = options.Login ?? string.Empty;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
error = "UNSUPPORTED_SECTION";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
error = "CONFIG_PARSE_FAILED";
|
||||
return false;
|
||||
}
|
||||
|
||||
config = new Config(
|
||||
section,
|
||||
server,
|
||||
port,
|
||||
databaseName,
|
||||
login,
|
||||
!string.IsNullOrWhiteSpace(connectionOptions.Credential),
|
||||
!string.IsNullOrWhiteSpace(connectionOptions.FilePasswords.ProtectionStd),
|
||||
!string.IsNullOrWhiteSpace(connectionOptions.FilePasswords.ProtectionPlus),
|
||||
connectionOptions);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static string SectionName(Section section) => section switch
|
||||
{
|
||||
Section.Hfsql => HfsqlOptions.SectionName,
|
||||
Section.LicenseConnection => LicenseConnectionOptions.SectionName,
|
||||
Section.ServiceConnection => ServiceConnectionOptions.SectionName,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(section))
|
||||
};
|
||||
|
||||
// ================================================================
|
||||
// SUMMARY (jamais de secret)
|
||||
// ================================================================
|
||||
|
||||
public static IReadOnlyList<string> BuildSummaryLines(Config config)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
|
||||
return new[]
|
||||
{
|
||||
"Section=" + SectionName(config.SectionValue),
|
||||
"Server=" + config.Server,
|
||||
"Port=" + config.Port.ToString(),
|
||||
"DatabaseName=" + config.DatabaseName,
|
||||
"Login=" + config.Login,
|
||||
"Provider=" + HfSqlConnectionOptions.OleDbProvider,
|
||||
"DatabasePasswordPresent=" + (config.DatabasePasswordPresent ? "YES" : "NO"),
|
||||
"ProtectionStdPresent=" + (config.ProtectionStdPresent ? "YES" : "NO"),
|
||||
"ProtectionPlusPresent=" + (config.ProtectionPlusPresent ? "YES" : "NO")
|
||||
};
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// SELECT (lecture seule)
|
||||
// ================================================================
|
||||
|
||||
public static bool IsValidTableIdentifier(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (char c in name)
|
||||
{
|
||||
bool ok = (c >= 'A' && c <= 'Z')
|
||||
|| (c >= 'a' && c <= 'z')
|
||||
|| (c >= '0' && c <= '9')
|
||||
|| c == '_';
|
||||
if (!ok)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Unique requête autorisée : <c>SELECT TOP 1 * FROM <table></c>.</summary>
|
||||
public static string BuildSelectSql(string table) => $"SELECT TOP 1 * FROM {table}";
|
||||
|
||||
private static bool TrySelect(
|
||||
DbConnection connection,
|
||||
string table,
|
||||
out int fieldCount,
|
||||
out bool hasRows,
|
||||
out string? hfSqlErrorCode)
|
||||
{
|
||||
fieldCount = 0;
|
||||
hasRows = false;
|
||||
hfSqlErrorCode = null;
|
||||
|
||||
if (!IsValidTableIdentifier(table))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = BuildSelectSql(table);
|
||||
using var reader = command.ExecuteReader();
|
||||
fieldCount = reader.FieldCount;
|
||||
hasRows = reader.Read();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
hfSqlErrorCode = ExtractHfSqlErrorCode(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// SANITIZATION (aucun message brut, aucun secret)
|
||||
// ================================================================
|
||||
|
||||
public static string SanitizeErrorType(Exception ex) => ex.GetType().Name;
|
||||
|
||||
public static string? ExtractHfSqlErrorCode(Exception ex)
|
||||
{
|
||||
var match = HfSqlErrorCodeRegex.Match(ex.Message);
|
||||
if (!match.Success)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return !string.IsNullOrEmpty(match.Groups[1].Value)
|
||||
? match.Groups[1].Value
|
||||
: match.Groups[2].Value;
|
||||
}
|
||||
|
||||
private static void PrintUsage(TextWriter output)
|
||||
{
|
||||
output.WriteLine("Usage: HfSqlOleDbAudit connection-test <configPath> <Hfsql|LicenseConnection|ServiceConnection> [<table>]");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user