Skip to content
This repository was archived by the owner on Jul 6, 2023. It is now read-only.

Commit 78c1500

Browse files
committed
Prompt silently if the output is redirected.
This fixes #159. * Problem 1 is not solved: There still be no visible prompt if the output is redirected and that may be misinterpreted as a hanging process * Problem 2 is solved: Neither the prompt not username and password appear in the output if it is redirected. * Problem 3 is solved: TTY Echo is turned off earlier, so that one can start typing the password without having to fear it appearing in cleartext on the screen. Anything typed before we actually prompt is buffered and processed then, so it is totally fine now to type the password and enter as soon as the process is started.
1 parent f4c4954 commit 78c1500

File tree

3 files changed

+184
-54
lines changed

3 files changed

+184
-54
lines changed

cypher-shell/src/integration-test/java/org/neo4j/shell/MainIntegrationTest.java

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.neo4j.shell;
22

3+
import org.junit.Before;
34
import org.junit.Test;
45
import org.neo4j.shell.cli.CliArgs;
56
import org.neo4j.shell.log.AnsiLogger;
@@ -16,39 +17,47 @@
1617

1718
public class MainIntegrationTest {
1819

19-
@Test
20-
public void connectInteractivelyPromptsOnWrongAuthentication() throws Exception {
20+
private String inputString = String.format( "neo4j%nneo%n" );
21+
private ByteArrayOutputStream baos;
22+
private ConnectionConfig connectionConfig;
23+
private CypherShell shell;
24+
private Main main;
25+
26+
@Before
27+
public void setup() {
2128
// given
22-
// what the user inputs when prompted
23-
String inputString = String.format( "neo4j%nneo%n" );
24-
InputStream inputStream = new ByteArrayInputStream(inputString.getBytes());
29+
InputStream inputStream = new ByteArrayInputStream( inputString.getBytes() );
2530

26-
ByteArrayOutputStream baos = new ByteArrayOutputStream();
27-
PrintStream ps = new PrintStream(baos);
31+
baos = new ByteArrayOutputStream();
32+
PrintStream ps = new PrintStream( baos );
2833

29-
Main main = new Main(inputStream, ps);
34+
main = new Main( inputStream, ps );
3035

3136
CliArgs cliArgs = new CliArgs();
3237
cliArgs.setUsername("", "");
3338
cliArgs.setPassword( "", "" );
3439

3540
Logger logger = new AnsiLogger(cliArgs.getDebugMode());
3641
PrettyConfig prettyConfig = new PrettyConfig(cliArgs);
37-
ConnectionConfig connectionConfig = new ConnectionConfig(
42+
connectionConfig = new ConnectionConfig(
3843
cliArgs.getScheme(),
3944
cliArgs.getHost(),
4045
cliArgs.getPort(),
4146
cliArgs.getUsername(),
4247
cliArgs.getPassword(),
4348
cliArgs.getEncryption());
4449

45-
CypherShell shell = new CypherShell(logger, prettyConfig);
50+
shell = new CypherShell(logger, prettyConfig);
51+
}
4652

53+
54+
@Test
55+
public void promptsOnWrongAuthenticationIfInteractive() throws Exception {
4756
// when
4857
assertEquals("", connectionConfig.username());
4958
assertEquals("", connectionConfig.password());
5059

51-
main.connectMaybeInteractively(shell, connectionConfig, true);
60+
main.connectMaybeInteractively(shell, connectionConfig, true, true);
5261

5362
// then
5463
// should be connected
@@ -60,4 +69,23 @@ public void connectInteractivelyPromptsOnWrongAuthentication() throws Exception
6069
String out = baos.toString();
6170
assertEquals( String.format( "username: neo4j%npassword: ***%n" ), out );
6271
}
72+
73+
@Test
74+
public void promptsSilentlyOnWrongAuthenticationIfOutputRedirected() throws Exception {
75+
// when
76+
assertEquals("", connectionConfig.username());
77+
assertEquals("", connectionConfig.password());
78+
79+
main.connectMaybeInteractively(shell, connectionConfig, true, false);
80+
81+
// then
82+
// should be connected
83+
assertTrue(shell.isConnected());
84+
// should have prompted silently and set the username and password
85+
assertEquals("neo4j", connectionConfig.username());
86+
assertEquals("neo", connectionConfig.password());
87+
88+
String out = baos.toString();
89+
assertEquals( "", out );
90+
}
6391
}

cypher-shell/src/main/java/org/neo4j/shell/Main.java

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import org.neo4j.shell.build.Build;
66
import org.neo4j.shell.cli.CliArgHelper;
77
import org.neo4j.shell.cli.CliArgs;
8-
import org.neo4j.shell.cli.Format;
98
import org.neo4j.shell.commands.CommandHelper;
109
import org.neo4j.shell.exception.CommandException;
1110
import org.neo4j.shell.log.AnsiLogger;
@@ -15,9 +14,11 @@
1514
import javax.annotation.Nonnull;
1615
import javax.annotation.Nullable;
1716
import java.io.InputStream;
17+
import java.io.OutputStream;
1818
import java.io.PrintStream;
1919

2020
import static org.neo4j.shell.ShellRunner.isInputInteractive;
21+
import static org.neo4j.shell.ShellRunner.isOutputInteractive;
2122

2223
public class Main {
2324
static final String NEO_CLIENT_ERROR_SECURITY_UNAUTHORIZED = "Neo.ClientError.Security.Unauthorized";
@@ -72,7 +73,7 @@ void startShell(@Nonnull CliArgs cliArgs) {
7273
try {
7374
CypherShell shell = new CypherShell(logger, prettyConfig);
7475
// Can only prompt for password if input has not been redirected
75-
connectMaybeInteractively(shell, connectionConfig, isInputInteractive());
76+
connectMaybeInteractively(shell, connectionConfig, isInputInteractive(), isOutputInteractive());
7677

7778
// Construct shellrunner after connecting, due to interrupt handling
7879
ShellRunner shellRunner = ShellRunner.getShellRunner(cliArgs, shell, logger, connectionConfig);
@@ -92,9 +93,20 @@ void startShell(@Nonnull CliArgs cliArgs) {
9293
/**
9394
* Connect the shell to the server, and try to handle missing passwords and such
9495
*/
95-
void connectMaybeInteractively(@Nonnull CypherShell shell, @Nonnull ConnectionConfig connectionConfig,
96-
boolean interactively)
96+
void connectMaybeInteractively(@Nonnull CypherShell shell,
97+
@Nonnull ConnectionConfig connectionConfig,
98+
boolean inputInteractive,
99+
boolean outputInteractive)
97100
throws Exception {
101+
102+
OutputStream outputStream = outputInteractive ? out : new ThrowawayOutputStream();
103+
104+
ConsoleReader consoleReader = new ConsoleReader(in, outputStream);
105+
// Disable expansion of bangs: !
106+
consoleReader.setExpandEvents(false);
107+
// Ensure Reader does not handle user input for ctrl+C behaviour
108+
consoleReader.setHandleUserInterrupt(false);
109+
98110
try {
99111
shell.connect(connectionConfig);
100112
} catch (AuthenticationException e) {
@@ -103,19 +115,24 @@ void connectMaybeInteractively(@Nonnull CypherShell shell, @Nonnull ConnectionCo
103115
throw e;
104116
}
105117
// else need to prompt for username and password
106-
if (interactively) {
118+
if (inputInteractive) {
107119
if (connectionConfig.username().isEmpty()) {
108-
connectionConfig.setUsername(promptForNonEmptyText("username", null));
120+
String username = outputInteractive ?
121+
promptForNonEmptyText("username", consoleReader, null) :
122+
promptForText("username", consoleReader, null);
123+
connectionConfig.setUsername(username);
109124
}
110125
if (connectionConfig.password().isEmpty()) {
111-
connectionConfig.setPassword(promptForText("password", '*'));
126+
connectionConfig.setPassword(promptForText("password", consoleReader, '*'));
112127
}
113128
// try again
114129
shell.connect(connectionConfig);
115130
} else {
116131
// Can't prompt because input has been redirected
117132
throw e;
118133
}
134+
} finally {
135+
consoleReader.close();
119136
}
120137
}
121138

@@ -129,40 +146,41 @@ void connectMaybeInteractively(@Nonnull CypherShell shell, @Nonnull ConnectionCo
129146
* in case of errors
130147
*/
131148
@Nonnull
132-
private String promptForNonEmptyText(@Nonnull String prompt, @Nullable Character mask) throws Exception {
133-
String text = promptForText(prompt, mask);
149+
private String promptForNonEmptyText(@Nonnull String prompt, @Nonnull ConsoleReader consoleReader, @Nullable Character mask) throws Exception {
150+
String text = promptForText(prompt, consoleReader, mask);
134151
if (!text.isEmpty()) {
135152
return text;
136153
}
137-
out.println(prompt + " cannot be empty");
138-
out.println();
139-
return promptForNonEmptyText(prompt, mask);
154+
consoleReader.println( prompt + " cannot be empty" );
155+
consoleReader.println();
156+
return promptForNonEmptyText(prompt, consoleReader, mask);
140157
}
141158

142159
/**
143160
* @param prompt
144161
* to display to the user
145162
* @param mask
146163
* single character to display instead of what the user is typing, use null if text is not secret
164+
* @param consoleReader
165+
* the reader
147166
* @return the text which was entered
148167
* @throws Exception
149168
* in case of errors
150169
*/
151170
@Nonnull
152-
private String promptForText(@Nonnull String prompt, @Nullable Character mask) throws Exception {
153-
String line;
154-
ConsoleReader consoleReader = new ConsoleReader(in, out);
155-
// Disable expansion of bangs: !
156-
consoleReader.setExpandEvents(false);
157-
// Ensure Reader does not handle user input for ctrl+C behaviour
158-
consoleReader.setHandleUserInterrupt(false);
159-
line = consoleReader.readLine(prompt + ": ", mask);
160-
consoleReader.close();
161-
171+
private String promptForText(@Nonnull String prompt, @Nonnull ConsoleReader consoleReader, @Nullable Character mask) throws Exception {
172+
String line = consoleReader.readLine(prompt + ": ", mask);
162173
if (line == null) {
163174
throw new CommandException("No text could be read, exiting...");
164175
}
165176

166177
return line;
167178
}
179+
180+
private static class ThrowawayOutputStream extends OutputStream {
181+
@Override
182+
public void write( int b )
183+
{
184+
}
185+
}
168186
}

0 commit comments

Comments
 (0)