22using Microsoft . Data . SqlClient ;
33using System . Reflection ;
44using System . Text ;
5- using System . Threading ;
5+ using System . Text . RegularExpressions ;
66using System . Threading . Tasks ;
77using Microsoft . Extensions . Logging ;
88using 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 ;
0 commit comments