Skip to content

Commit 4d89eeb

Browse files
committed
Add TLS options (encrypt, trust-server-certificate, hostname-in-certificate, disable TNIR); ensure target DB is used; handle missing VIEW SERVER STATE when showing connection info
1 parent 35b226d commit 4d89eeb

File tree

3 files changed

+121
-23
lines changed

3 files changed

+121
-23
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,9 @@ MigrationBackup/
361361

362362
# Fody - auto-generated XML schema
363363
FodyWeavers.xsd
364+
365+
366+
# Custom
364367
/Properties/launchSettings.json
365368

366369
# Private todo list

Commands/PingCommand.cs

Lines changed: 100 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using Microsoft.Data.SqlClient;
33
using System.Reflection;
44
using System.Text;
5-
using System.Threading;
5+
using System.Text.RegularExpressions;
66
using System.Threading.Tasks;
77
using Microsoft.Extensions.Logging;
88
using Spectre.Console;
@@ -24,34 +24,33 @@ public override async Task<int> ExecuteAsync(CommandContext context, ConsoleSett
2424
// Apply secure credential handling
2525
ApplySecureCredentials(settings);
2626

27-
//Logger.LogInformation("Connection string: {Mandatory}", connectionString);
28-
//Logger.LogInformation("SQL Command: {Optional}", settings.SQLCommand);
29-
//Logger.LogInformation("CommandOptionFlag: {CommandOptionFlag}", settings.CommandOptionFlag);
30-
//Logger.LogInformation("CommandOptionValue: {CommandOptionValue}", settings.CommandOptionValue);
31-
3227
var connString = GetConnectionString(settings);
3328

34-
//Logger.LogInformation("");
3529
AnsiConsole.MarkupLine($"ConnectionString: [teal]{RedactConnectionString(connString).EscapeMarkup()}[/]");
3630
AnsiConsole.MarkupLine($"SQL Query : [teal]{settings.SQLCommand.EscapeMarkup()}[/]");
3731

32+
// Helpful warning if using IP
33+
if (LooksLikeIp(settings.Server)
34+
&& (settings.TrustServerCertificate != true)
35+
&& string.IsNullOrWhiteSpace(settings.HostNameInCertificate))
36+
{
37+
AnsiConsole.MarkupLine("[yellow]Hint:[/] You're connecting by IP. TLS certificate name validation usually fails with IPs unless the cert has the IP in SAN. Use a DNS name, set [teal]--hostname-in-certificate[/], or [teal]--trust-server-certificate true[/] for dev.");
38+
}
39+
3840
bool running = true;
3941

4042
while (running)
4143
{
42-
4344
int sec = settings.Wait;
4445
await AnsiConsole.Status()
4546
.AutoRefresh(true)
46-
.Spinner(Spinner.Known.Dots) // https://jsfiddle.net/sindresorhus/2eLtsbey/embedded/result/
47+
.Spinner(Spinner.Known.Dots)
4748
.SpinnerStyle(Style.Parse("green bold"))
4849
.StartAsync("Please wait...", async ctx =>
4950
{
50-
// Simulate some work
5151
ctx.Status($"Trying to connect to server [teal]{settings.Server.EscapeMarkup()}[/]...");
5252
await CallDatabaseAsync(connString, settings);
5353

54-
// Update the status and spinner
5554
ctx.Status($"Waiting [teal]{sec}[/] seconds...");
5655

5756
if (settings.NonStop)
@@ -61,17 +60,12 @@ await AnsiConsole.Status()
6160
});
6261
}
6362

64-
//Console.WriteLine("\nDone. Press enter.");
65-
//Console.ReadLine();
66-
6763
return await Task.FromResult(0);
6864
}
6965

7066
// Validate as part of the command. This is a good way of validating options if you require any injected services.
7167
public override ValidationResult Validate(CommandContext context, ConsoleSettings settings)
7268
{
73-
//if (settings.Wait < 1)
74-
// return ValidationResult.Error("...");
7569
return ValidationResult.Success();
7670
}
7771

@@ -115,7 +109,6 @@ private static void ApplySecureCredentials(ConsoleSettings settings)
115109

116110
private static string GetConnectionString(ConsoleSettings settings) {
117111
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
118-
//builder.ConnectionString = settings.ConnectionString;
119112
builder.DataSource = settings.Server;
120113
if (!string.IsNullOrEmpty(settings.Username)) {
121114
builder.UserID = settings.Username;
@@ -135,6 +128,33 @@ private static string GetConnectionString(ConsoleSettings settings) {
135128
builder.WorkstationID = Environment.MachineName;
136129
builder.ApplicationName = Assembly.GetExecutingAssembly().FullName;
137130

131+
// TLS related options
132+
if (settings.TrustServerCertificate.HasValue)
133+
builder.TrustServerCertificate = settings.TrustServerCertificate.Value;
134+
135+
if (!string.IsNullOrWhiteSpace(settings.Encrypt))
136+
{
137+
var encValue = settings.Encrypt.Trim().ToLowerInvariant();
138+
if (encValue is "true" or "false" or "strict")
139+
{
140+
builder["Encrypt"] = settings.Encrypt;
141+
}
142+
else
143+
{
144+
AnsiConsole.MarkupLine("[yellow]Invalid --encrypt value. Use true|false|strict. Ignoring.[/]");
145+
}
146+
}
147+
148+
if (!string.IsNullOrWhiteSpace(settings.HostNameInCertificate))
149+
{
150+
builder["HostNameInCertificate"] = settings.HostNameInCertificate;
151+
}
152+
153+
if (settings.NoTransparentNetworkIPResolution)
154+
{
155+
builder["TransparentNetworkIPResolution"] = "false";
156+
}
157+
138158
string connString = builder.ConnectionString;
139159
return connString;
140160
}
@@ -167,39 +187,96 @@ private async Task CallDatabaseAsync(string connString, ConsoleSettings settings
167187
try
168188
{
169189

170-
using (SqlConnection connection = new SqlConnection(connString))
190+
await using (SqlConnection connection = new SqlConnection(connString))
171191
{
172192
await connection.OpenAsync();
193+
194+
// Ensure we are in the requested database even if Initial Catalog was not applied for any reason
195+
if (!string.IsNullOrWhiteSpace(settings.Database))
196+
{
197+
try
198+
{
199+
connection.ChangeDatabase(settings.Database);
200+
}
201+
catch (Exception dbEx)
202+
{
203+
AnsiConsole.MarkupLine($"[yellow]Warning:[/] Failed to change database to '[teal]{settings.Database.EscapeMarkup()}[/]': {dbEx.Message.EscapeMarkup()}");
204+
}
205+
}
206+
173207
var sb = new StringBuilder();
174-
using (SqlCommand command = new SqlCommand(settings.SQLCommand, connection))
208+
await using (SqlCommand command = new SqlCommand(settings.SQLCommand!, connection))
175209
{
176210
// Add parameter if the query contains @DatabaseName placeholder
177-
if (settings.SQLCommand.Contains("@DatabaseName"))
211+
if (settings.SQLCommand != null && settings.SQLCommand.Contains("@DatabaseName"))
178212
{
179213
command.Parameters.AddWithValue("@DatabaseName", settings.Database);
180214
}
181215

182-
using (SqlDataReader reader = await command.ExecuteReaderAsync())
216+
await using (SqlDataReader reader = await command.ExecuteReaderAsync())
183217
{
184218
while (await reader.ReadAsync())
185219
{
186220
for (int i = 0; i < reader.FieldCount; i++)
187221
if (reader.GetValue(i) != DBNull.Value)
188222
sb.Append($"{reader.GetName(i)}: {Convert.ToString(reader.GetValue(i))} ");
189-
//sb.AppendLine();
190223
}
191224
}
192225
}
193-
AnsiConsole.MarkupLine($" [green]SUCCESS[/] {sb.ToString().EscapeMarkup()}");
226+
227+
// Try to show connection encryption info, but ignore permission errors
228+
string info;
229+
string currentDbName = connection.Database;
230+
try
231+
{
232+
await using var infoCmd = new SqlCommand(
233+
"SELECT encrypt_option, net_transport FROM sys.dm_exec_connections WHERE session_id = @@SPID;", connection);
234+
await using var infoReader = await infoCmd.ExecuteReaderAsync();
235+
if (await infoReader.ReadAsync())
236+
{
237+
var encryptOption = Convert.ToString(infoReader["encrypt_option"]);
238+
var transport = Convert.ToString(infoReader["net_transport"]);
239+
info = $" (db={currentDbName}, encrypt_option={encryptOption}, net_transport={transport})";
240+
}
241+
else
242+
{
243+
info = $" (db={currentDbName})";
244+
}
245+
}
246+
catch (SqlException)
247+
{
248+
info = $" (db={currentDbName}, connection details unavailable: requires VIEW SERVER STATE)";
249+
}
250+
251+
AnsiConsole.MarkupLine($" [green]SUCCESS[/] {sb.ToString().EscapeMarkup()}{info}");
194252
}
195253
}
196254
catch (SqlException ex)
197255
{
198256
AnsiConsole.MarkupLine($" [red]ERROR: {ex.Message.EscapeMarkup()}[/] ");
257+
258+
if (ex.Message.IndexOf("certificate chain was issued by an authority that is not trusted", StringComparison.OrdinalIgnoreCase) >= 0)
259+
{
260+
AnsiConsole.MarkupLine("[yellow]Troubleshooting tips:[/]");
261+
AnsiConsole.MarkupLine("- Ensure SQL Server uses a certificate trusted by this machine's Trusted Root store.");
262+
AnsiConsole.MarkupLine("- Connect using a DNS name that matches the certificate's CN/SAN.");
263+
AnsiConsole.MarkupLine("- Or set [teal]--hostname-in-certificate[/] to the certificate subject.");
264+
AnsiConsole.MarkupLine("- For dev only, use [teal]--trust-server-certificate true[/] to bypass validation.");
265+
AnsiConsole.MarkupLine("- If name keeps flipping to an IP, try [teal]--no-tnir[/].");
266+
}
199267
}
200268

201269
}
202270

271+
private static bool LooksLikeIp(string server)
272+
{
273+
// Accept formats: "x.x.x.x" or "x.x.x.x,port"
274+
var parts = server.Split('\\')[0]; // ignore instance suffix
275+
var ipAndPort = parts.Split(',');
276+
var ip = ipAndPort[0].Trim();
277+
return Regex.IsMatch(ip, @"^\d{1,3}(\.\d{1,3}){3}$");
278+
}
279+
203280
public PingCommand(ILogger<PingCommand> logger)
204281
{
205282
Logger = logger;

Commands/Settings/ConsoleSettings.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,24 @@ public override ValidationResult Validate()
6262
[DefaultValue(10)]
6363
public int Wait { get; set; }
6464

65+
// TLS-related options
66+
[CommandOption("--encrypt <true|false|strict>")]
67+
[Description("Encrypt mode: true|false|strict. Default follows Microsoft.Data.SqlClient (true).")]
68+
public string? Encrypt { get; set; }
69+
70+
[CommandOption("--trust-server-certificate|-T <true|false>")]
71+
[Description("Set TrustServerCertificate=true (DEV ONLY). Bypasses cert validation but keeps encryption.")]
72+
public bool? TrustServerCertificate { get; set; }
73+
74+
[CommandOption("--hostname-in-certificate|-H <name>")]
75+
[Description("Override HostNameInCertificate for TLS validation (use the subject/SAN on the server certificate).")]
76+
public string? HostNameInCertificate { get; set; }
77+
78+
[CommandOption("--no-tnir")]
79+
[Description("Disable TransparentNetworkIPResolution to avoid host->IP substitution that breaks TLS name matching.")]
80+
[DefaultValue(false)]
81+
public bool NoTransparentNetworkIPResolution { get; set; } = false;
82+
6583

6684

6785
//[CommandOption("--command-option-value <value>")]

0 commit comments

Comments
 (0)