Skip to content

Commit c11db59

Browse files
authored
remove console IO hack (#476)
dotnet/runtime#22314 is fixed in .NET 9, so we don't need these hacks now. https://desire2learn.atlassian.net/browse/VUL-427
1 parent 01d09e0 commit c11db59

File tree

1 file changed

+41
-92
lines changed

1 file changed

+41
-92
lines changed

src/D2L.Bmx/ConsolePrompter.cs

+41-92
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Diagnostics;
21
using System.Text;
32
using D2L.Bmx.Okta.Models;
43

@@ -20,22 +19,10 @@ internal class ConsolePrompter : IConsolePrompter {
2019
private const char CTRL_C = '\u0003';
2120
private const char CTRL_U = '\u0015';
2221
private const char DEL = '\u007f';
23-
private static readonly bool IS_WINDOWS = OperatingSystem.IsWindows();
24-
25-
// When taking user console input on Unix-y platforms, .NET copies the input data to stdout, leading to incorrect
26-
// `bmx print` output. See https://github.com/dotnet/runtime/issues/22314.
27-
// We read from stdin (fd = 0) directly on these platforms to bypass .NET's incorrect handling.
28-
private readonly TextReader _stdinReader =
29-
IS_WINDOWS
30-
? Console.In
31-
: new StreamReader( new FileStream(
32-
new Microsoft.Win32.SafeHandles.SafeFileHandle( 0, ownsHandle: false ),
33-
FileAccess.Read
34-
) );
3522

3623
string IConsolePrompter.PromptOrg( bool allowEmptyInput ) {
3724
Console.Error.Write( $"{ParameterDescriptions.Org}{( allowEmptyInput ? " (optional): " : ": " )}" );
38-
string? org = _stdinReader.ReadLine();
25+
string? org = Console.ReadLine();
3926
if( org is null || ( string.IsNullOrWhiteSpace( org ) && !allowEmptyInput ) ) {
4027
throw new BmxException( "Invalid org input" );
4128
}
@@ -45,7 +32,7 @@ string IConsolePrompter.PromptOrg( bool allowEmptyInput ) {
4532

4633
string IConsolePrompter.PromptProfile() {
4734
Console.Error.Write( $"{ParameterDescriptions.Profile}: " );
48-
string? profile = _stdinReader.ReadLine();
35+
string? profile = Console.ReadLine();
4936
if( string.IsNullOrEmpty( profile ) ) {
5037
throw new BmxException( "Invalid profile input" );
5138
}
@@ -55,7 +42,7 @@ string IConsolePrompter.PromptProfile() {
5542

5643
string IConsolePrompter.PromptUser( bool allowEmptyInput ) {
5744
Console.Error.Write( $"{ParameterDescriptions.User}{( allowEmptyInput ? " (optional): " : ": " )}" );
58-
string? user = _stdinReader.ReadLine();
45+
string? user = Console.ReadLine();
5946
if( user is null || ( string.IsNullOrWhiteSpace( user ) && !allowEmptyInput ) ) {
6047
throw new BmxException( "Invalid user input" );
6148
}
@@ -69,7 +56,7 @@ string IConsolePrompter.PromptPassword() {
6956

7057
int? IConsolePrompter.PromptDuration() {
7158
Console.Error.Write( $"{ParameterDescriptions.Duration} (optional, default: 60): " );
72-
string? input = _stdinReader.ReadLine();
59+
string? input = Console.ReadLine();
7360
if( input is null || !int.TryParse( input, out int duration ) || duration <= 0 ) {
7461
return null;
7562
}
@@ -91,7 +78,7 @@ string IConsolePrompter.PromptAccount( string[] accounts ) {
9178
Console.Error.WriteLine( $"[{i + 1}] {accounts[i]}" );
9279
}
9380
Console.Error.Write( "Select an account: " );
94-
if( !int.TryParse( _stdinReader.ReadLine(), out int index ) || index > accounts.Length || index < 1 ) {
81+
if( !int.TryParse( Console.ReadLine(), out int index ) || index > accounts.Length || index < 1 ) {
9582
throw new BmxException( "Invalid account selection" );
9683
}
9784

@@ -113,7 +100,7 @@ string IConsolePrompter.PromptRole( string[] roles ) {
113100
Console.Error.WriteLine( $"[{i + 1}] {roles[i]}" );
114101
}
115102
Console.Error.Write( "Select a role: " );
116-
if( !int.TryParse( _stdinReader.ReadLine(), out int index ) || index > roles.Length || index < 1 ) {
103+
if( !int.TryParse( Console.ReadLine(), out int index ) || index > roles.Length || index < 1 ) {
117104
throw new BmxException( "Invalid role selection" );
118105
}
119106

@@ -136,7 +123,7 @@ OktaMfaFactor IConsolePrompter.SelectMfa( OktaMfaFactor[] mfaOptions ) {
136123
Console.Error.WriteLine( $"[{i + 1}] {mfaOptions[i].Provider} : {mfaOptions[i].FactorName}" );
137124
}
138125
Console.Error.Write( "Select an available MFA option: " );
139-
if( !int.TryParse( _stdinReader.ReadLine(), out int index ) || index > mfaOptions.Length || index < 1 ) {
126+
if( !int.TryParse( Console.ReadLine(), out int index ) || index > mfaOptions.Length || index < 1 ) {
140127
throw new BmxException( "Invalid MFA selection" );
141128
}
142129
return mfaOptions[index - 1];
@@ -149,7 +136,7 @@ string IConsolePrompter.GetMfaResponse( string mfaInputPrompt, bool maskInput )
149136
mfaInput = GetMaskedInput( $"{mfaInputPrompt}: " );
150137
} else {
151138
Console.Error.Write( $"{mfaInputPrompt}: " );
152-
mfaInput = _stdinReader.ReadLine();
139+
mfaInput = Console.ReadLine();
153140
}
154141

155142
if( mfaInput is not null ) {
@@ -159,91 +146,53 @@ string IConsolePrompter.GetMfaResponse( string mfaInputPrompt, bool maskInput )
159146
}
160147

161148
private string GetMaskedInput( string prompt ) {
162-
Func<char> readKey;
163-
if( IS_WINDOWS ) {
164-
// On Windows, Console.ReadKey calls native console API, and will fail without a console attached
165-
if( Console.IsInputRedirected ) {
166-
if( Environment.GetEnvironmentVariable( "TERM_PROGRAM" ) == "mintty" ) {
167-
Console.Error.WriteLine( "\x1b[93m" + """
149+
Console.Error.Write( prompt );
150+
151+
// If input is redirected, Console.ReadKey( intercept: true ) doesn't work, because it calls system console/terminal APIs.
152+
if( Console.IsInputRedirected ) {
153+
if( OperatingSystem.IsWindows() && Environment.GetEnvironmentVariable( "TERM_PROGRAM" ) == "mintty" ) {
154+
Console.Error.WriteLine( "\x1b[93m" + """
168155
====== WARNING ======
169156
Secret input won't be masked on screen!
170157
This is because you are using mintty (possibly via Git Bash, Cygwin, MSYS2 etc.).
171158
Consider switching to Windows Terminal for a better experience.
172159
If you must use mintty, prefix your bmx command with 'winpty '.
173160
=====================
174161
""" + "\x1b[0m" );
175-
}
176-
readKey = () => (char)_stdinReader.Read();
177-
} else {
178-
readKey = () => Console.ReadKey( intercept: true ).KeyChar;
179162
}
180-
} else {
181-
readKey = () => (char)_stdinReader.Read();
163+
return Console.ReadLine() ?? string.Empty;
182164
}
183165

184-
Console.Error.Write( prompt );
166+
var passwordBuilder = new StringBuilder();
167+
while( true ) {
168+
char key = Console.ReadKey( intercept: true ).KeyChar;
185169

186-
string? originalTerminalSettings = null;
187-
try {
188-
if( !IS_WINDOWS ) {
189-
originalTerminalSettings = GetCurrentTerminalSettings();
190-
EnableTerminalRawMode();
170+
if( key == CTRL_C ) {
171+
// Ctrl+C should terminate the program.
172+
// Using an empty string as the exception message because this message is displayed to the user,
173+
// but the user doesn't need to see anything when they themselves ended the program.
174+
throw new BmxException( string.Empty );
191175
}
192-
193-
var passwordBuilder = new StringBuilder();
194-
while( true ) {
195-
char key = readKey();
196-
197-
if( key == CTRL_C ) {
198-
// Ctrl+C should terminate the program.
199-
// Using an empty string as the exception message because this message is displayed to the user,
200-
// but the user doesn't need to see anything when they themselves ended the program.
201-
throw new BmxException( string.Empty );
202-
}
203-
if( key == '\n' || key == '\r' ) {
204-
// when the terminal is in raw mode, writing \r is needed to start the new line properly
205-
Console.Error.Write( "\r\n" );
206-
return passwordBuilder.ToString();
207-
}
208-
209-
if( key == CTRL_U ) {
210-
string moveLeftString = new( '\b', passwordBuilder.Length );
211-
string emptyString = new( ' ', passwordBuilder.Length );
212-
Console.Error.Write( moveLeftString + emptyString + moveLeftString );
213-
passwordBuilder.Clear();
214-
} else
215-
// The backspace key is received as the DEL character in raw mode
216-
if( ( key == '\b' || key == DEL ) && passwordBuilder.Length > 0 ) {
217-
Console.Error.Write( "\b \b" );
218-
passwordBuilder.Length--;
219-
} else if( !char.IsControl( key ) ) {
220-
Console.Error.Write( '*' );
221-
passwordBuilder.Append( key );
222-
}
176+
if( key == '\n' || key == '\r' ) {
177+
// when the terminal is in raw mode, writing \r is needed to start the new line properly
178+
Console.Error.Write( "\r\n" );
179+
return passwordBuilder.ToString();
223180
}
224-
} finally {
225-
if( !IS_WINDOWS && !string.IsNullOrEmpty( originalTerminalSettings ) ) {
226-
SetTerminalSettings( originalTerminalSettings );
181+
182+
if( key == CTRL_U ) {
183+
string moveLeftString = new( '\b', passwordBuilder.Length );
184+
string emptyString = new( ' ', passwordBuilder.Length );
185+
Console.Error.Write( moveLeftString + emptyString + moveLeftString );
186+
passwordBuilder.Clear();
187+
} else
188+
// The backspace key is received as the DEL character in raw mode
189+
if( ( key == '\b' || key == DEL ) && passwordBuilder.Length > 0 ) {
190+
Console.Error.Write( "\b \b" );
191+
passwordBuilder.Length--;
192+
} else if( !char.IsControl( key ) ) {
193+
Console.Error.Write( '*' );
194+
passwordBuilder.Append( key );
227195
}
228196
}
229197
}
230-
231-
private static string GetCurrentTerminalSettings() {
232-
var startInfo = new ProcessStartInfo( "stty" );
233-
startInfo.ArgumentList.Add( "-g" );
234-
startInfo.RedirectStandardOutput = true;
235-
using var p = Process.Start( startInfo ) ?? throw new BmxException( "Terminal error" );
236-
p.WaitForExit();
237-
return p.StandardOutput.ReadToEnd().Trim();
238-
}
239-
240-
private static void EnableTerminalRawMode() {
241-
using var p = Process.Start( "stty", new[] { "raw", "-echo" } );
242-
p.WaitForExit();
243-
}
244-
245-
private static void SetTerminalSettings( string settings ) {
246-
using var p = Process.Start( "stty", settings );
247-
p.WaitForExit();
248-
}
249198
}

0 commit comments

Comments
 (0)