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

Commit

Permalink
Prompt silently if the output is redirected.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sherfert committed Sep 2, 2019
1 parent f4c4954 commit 78c1500
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 54 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.neo4j.shell;

import org.junit.Before;
import org.junit.Test;
import org.neo4j.shell.cli.CliArgs;
import org.neo4j.shell.log.AnsiLogger;
Expand All @@ -16,39 +17,47 @@

public class MainIntegrationTest {

@Test
public void connectInteractivelyPromptsOnWrongAuthentication() throws Exception {
private String inputString = String.format( "neo4j%nneo%n" );
private ByteArrayOutputStream baos;
private ConnectionConfig connectionConfig;
private CypherShell shell;
private Main main;

@Before
public void setup() {
// given
// what the user inputs when prompted
String inputString = String.format( "neo4j%nneo%n" );
InputStream inputStream = new ByteArrayInputStream(inputString.getBytes());
InputStream inputStream = new ByteArrayInputStream( inputString.getBytes() );

ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(baos);
baos = new ByteArrayOutputStream();
PrintStream ps = new PrintStream( baos );

Main main = new Main(inputStream, ps);
main = new Main( inputStream, ps );

CliArgs cliArgs = new CliArgs();
cliArgs.setUsername("", "");
cliArgs.setPassword( "", "" );

Logger logger = new AnsiLogger(cliArgs.getDebugMode());
PrettyConfig prettyConfig = new PrettyConfig(cliArgs);
ConnectionConfig connectionConfig = new ConnectionConfig(
connectionConfig = new ConnectionConfig(
cliArgs.getScheme(),
cliArgs.getHost(),
cliArgs.getPort(),
cliArgs.getUsername(),
cliArgs.getPassword(),
cliArgs.getEncryption());

CypherShell shell = new CypherShell(logger, prettyConfig);
shell = new CypherShell(logger, prettyConfig);
}


@Test
public void promptsOnWrongAuthenticationIfInteractive() throws Exception {
// when
assertEquals("", connectionConfig.username());
assertEquals("", connectionConfig.password());

main.connectMaybeInteractively(shell, connectionConfig, true);
main.connectMaybeInteractively(shell, connectionConfig, true, true);

// then
// should be connected
Expand All @@ -60,4 +69,23 @@ public void connectInteractivelyPromptsOnWrongAuthentication() throws Exception
String out = baos.toString();
assertEquals( String.format( "username: neo4j%npassword: ***%n" ), out );
}

@Test
public void promptsSilentlyOnWrongAuthenticationIfOutputRedirected() throws Exception {
// when
assertEquals("", connectionConfig.username());
assertEquals("", connectionConfig.password());

main.connectMaybeInteractively(shell, connectionConfig, true, false);

// then
// should be connected
assertTrue(shell.isConnected());
// should have prompted silently and set the username and password
assertEquals("neo4j", connectionConfig.username());
assertEquals("neo", connectionConfig.password());

String out = baos.toString();
assertEquals( "", out );
}
}
62 changes: 40 additions & 22 deletions cypher-shell/src/main/java/org/neo4j/shell/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import org.neo4j.shell.build.Build;
import org.neo4j.shell.cli.CliArgHelper;
import org.neo4j.shell.cli.CliArgs;
import org.neo4j.shell.cli.Format;
import org.neo4j.shell.commands.CommandHelper;
import org.neo4j.shell.exception.CommandException;
import org.neo4j.shell.log.AnsiLogger;
Expand All @@ -15,9 +14,11 @@
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;

import static org.neo4j.shell.ShellRunner.isInputInteractive;
import static org.neo4j.shell.ShellRunner.isOutputInteractive;

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

// Construct shellrunner after connecting, due to interrupt handling
ShellRunner shellRunner = ShellRunner.getShellRunner(cliArgs, shell, logger, connectionConfig);
Expand All @@ -92,9 +93,20 @@ void startShell(@Nonnull CliArgs cliArgs) {
/**
* Connect the shell to the server, and try to handle missing passwords and such
*/
void connectMaybeInteractively(@Nonnull CypherShell shell, @Nonnull ConnectionConfig connectionConfig,
boolean interactively)
void connectMaybeInteractively(@Nonnull CypherShell shell,
@Nonnull ConnectionConfig connectionConfig,
boolean inputInteractive,
boolean outputInteractive)
throws Exception {

OutputStream outputStream = outputInteractive ? out : new ThrowawayOutputStream();

ConsoleReader consoleReader = new ConsoleReader(in, outputStream);
// Disable expansion of bangs: !
consoleReader.setExpandEvents(false);
// Ensure Reader does not handle user input for ctrl+C behaviour
consoleReader.setHandleUserInterrupt(false);

try {
shell.connect(connectionConfig);
} catch (AuthenticationException e) {
Expand All @@ -103,19 +115,24 @@ void connectMaybeInteractively(@Nonnull CypherShell shell, @Nonnull ConnectionCo
throw e;
}
// else need to prompt for username and password
if (interactively) {
if (inputInteractive) {
if (connectionConfig.username().isEmpty()) {
connectionConfig.setUsername(promptForNonEmptyText("username", null));
String username = outputInteractive ?
promptForNonEmptyText("username", consoleReader, null) :
promptForText("username", consoleReader, null);
connectionConfig.setUsername(username);
}
if (connectionConfig.password().isEmpty()) {
connectionConfig.setPassword(promptForText("password", '*'));
connectionConfig.setPassword(promptForText("password", consoleReader, '*'));
}
// try again
shell.connect(connectionConfig);
} else {
// Can't prompt because input has been redirected
throw e;
}
} finally {
consoleReader.close();
}
}

Expand All @@ -129,40 +146,41 @@ void connectMaybeInteractively(@Nonnull CypherShell shell, @Nonnull ConnectionCo
* in case of errors
*/
@Nonnull
private String promptForNonEmptyText(@Nonnull String prompt, @Nullable Character mask) throws Exception {
String text = promptForText(prompt, mask);
private String promptForNonEmptyText(@Nonnull String prompt, @Nonnull ConsoleReader consoleReader, @Nullable Character mask) throws Exception {
String text = promptForText(prompt, consoleReader, mask);
if (!text.isEmpty()) {
return text;
}
out.println(prompt + " cannot be empty");
out.println();
return promptForNonEmptyText(prompt, mask);
consoleReader.println( prompt + " cannot be empty" );
consoleReader.println();
return promptForNonEmptyText(prompt, consoleReader, mask);
}

/**
* @param prompt
* to display to the user
* @param mask
* single character to display instead of what the user is typing, use null if text is not secret
* @param consoleReader
* the reader
* @return the text which was entered
* @throws Exception
* in case of errors
*/
@Nonnull
private String promptForText(@Nonnull String prompt, @Nullable Character mask) throws Exception {
String line;
ConsoleReader consoleReader = new ConsoleReader(in, out);
// Disable expansion of bangs: !
consoleReader.setExpandEvents(false);
// Ensure Reader does not handle user input for ctrl+C behaviour
consoleReader.setHandleUserInterrupt(false);
line = consoleReader.readLine(prompt + ": ", mask);
consoleReader.close();

private String promptForText(@Nonnull String prompt, @Nonnull ConsoleReader consoleReader, @Nullable Character mask) throws Exception {
String line = consoleReader.readLine(prompt + ": ", mask);
if (line == null) {
throw new CommandException("No text could be read, exiting...");
}

return line;
}

private static class ThrowawayOutputStream extends OutputStream {
@Override
public void write( int b )
{
}
}
}
Loading

0 comments on commit 78c1500

Please sign in to comment.