/*
* Copyright (c) 2006-2011 Christian Plattner. All rights reserved.
* Please refer to the LICENSE.txt for licensing details.
*/
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.InteractiveCallback;
import ch.ethz.ssh2.KnownHosts;
import ch.ethz.ssh2.ServerHostKeyVerifier;
import ch.ethz.ssh2.Session;
/**
*
* This is a very primitive SSH-2 dumb terminal (Swing based).
*
* The purpose of this class is to demonstrate:
*
* - Verifying server hostkeys with an existing known_hosts file
* - Displaying fingerprints of server hostkeys
* - Adding a server hostkey to a known_hosts file (+hashing the hostname for security)
* - Authentication with DSA, RSA, password and keyboard-interactive methods
*
*/
public class SwingShell
{
/*
* NOTE: to get this feature to work, replace the "tilde" with your home directory,
* at least my JVM does not understand it. Need to check the specs.
*/
static final String knownHostPath = "~/.ssh/known_hosts";
static final String idDSAPath = "~/.ssh/id_dsa";
static final String idRSAPath = "~/.ssh/id_rsa";
JFrame loginFrame = null;
JLabel hostLabel;
JLabel userLabel;
JTextField hostField;
JTextField userField;
JButton loginButton;
KnownHosts database = new KnownHosts();
public SwingShell()
{
File knownHostFile = new File(knownHostPath);
if (knownHostFile.exists())
{
try
{
database.addHostkeys(knownHostFile);
}
catch (IOException e)
{
}
}
}
/**
* This dialog displays a number of text lines and a text field.
* The text field can either be plain text or a password field.
*/
class EnterSomethingDialog extends JDialog
{
private static final long serialVersionUID = 1L;
JTextField answerField;
JPasswordField passwordField;
final boolean isPassword;
String answer;
public EnterSomethingDialog(JFrame parent, String title, String content, boolean isPassword)
{
this(parent, title, new String[] { content }, isPassword);
}
public EnterSomethingDialog(JFrame parent, String title, String[] content, boolean isPassword)
{
super(parent, title, true);
this.isPassword = isPassword;
JPanel pan = new JPanel();
pan.setLayout(new BoxLayout(pan, BoxLayout.Y_AXIS));
for (int i = 0; i < content.length; i++)
{
if ((content[i] == null) || (content[i] == ""))
continue;
JLabel contentLabel = new JLabel(content[i]);
pan.add(contentLabel);
}
answerField = new JTextField(20);
passwordField = new JPasswordField(20);
if (isPassword)
pan.add(passwordField);
else
pan.add(answerField);
KeyAdapter kl = new KeyAdapter()
{
public void keyTyped(KeyEvent e)
{
if (e.getKeyChar() == '\n')
finish();
}
};
answerField.addKeyListener(kl);
passwordField.addKeyListener(kl);
getContentPane().add(BorderLayout.CENTER, pan);
setResizable(false);
pack();
setLocationRelativeTo(null);
}
private void finish()
{
if (isPassword)
answer = new String(passwordField.getPassword());
else
answer = answerField.getText();
dispose();
}
}
/**
* TerminalDialog is probably the worst terminal emulator ever written - implementing
* a real vt100 is left as an exercise to the reader, i.e., to you =)
*
*/
class TerminalDialog extends JDialog
{
private static final long serialVersionUID = 1L;
JPanel botPanel;
JButton logoffButton;
JTextArea terminalArea;
Session sess;
InputStream in;
OutputStream out;
int x, y;
/**
* This thread consumes output from the remote server and displays it in
* the terminal window.
*
*/
class RemoteConsumer extends Thread
{
char[][] lines = new char[y][];
int posy = 0;
int posx = 0;
private void addText(byte[] data, int len)
{
for (int i = 0; i < len; i++)
{
char c = (char) (data[i] & 0xff);
if (c == 8) // Backspace, VERASE
{
if (posx < 0)
continue;
posx--;
continue;
}
if (c == '\r')
{
posx = 0;
continue;
}
if (c == '\n')
{
posy++;
if (posy >= y)
{
for (int k = 1; k < y; k++)
lines[k - 1] = lines[k];
posy--;
lines[y - 1] = new char[x];
for (int k = 0; k < x; k++)
lines[y - 1][k] = ' ';
}
continue;
}
if (c < 32)
{
continue;
}
if (posx >= x)
{
posx = 0;
posy++;
if (posy >= y)
{
posy--;
for (int k = 1; k < y; k++)
lines[k - 1] = lines[k];
lines[y - 1] = new char[x];
for (int k = 0; k < x; k++)
lines[y - 1][k] = ' ';
}
}
if (lines[posy] == null)
{
lines[posy] = new char[x];
for (int k = 0; k < x; k++)
lines[posy][k] = ' ';
}
lines[posy][posx] = c;
posx++;
}
StringBuffer sb = new StringBuffer(x * y);
for (int i = 0; i < lines.length; i++)
{
if (i != 0)
sb.append('\n');
if (lines[i] != null)
{
sb.append(lines[i]);
}
}
setContent(sb.toString());
}
public void run()
{
byte[] buff = new byte[8192];
try
{
while (true)
{
int len = in.read(buff);
if (len == -1)
return;
addText(buff, len);
}
}
catch (Exception e)
{
}
}
}
public TerminalDialog(JFrame parent, String title, Session sess, int x, int y) throws IOException
{
super(parent, title, true);
this.sess = sess;
in = sess.getStdout();
out = sess.getStdin();
this.x = x;
this.y = y;
botPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
logoffButton = new JButton("Logout");
botPanel.add(logoffButton);
logoffButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
/* Dispose the dialog, "setVisible(true)" method will return */
dispose();
}
});
Font f = new Font("Monospaced", Font.PLAIN, 16);
terminalArea = new JTextArea(y, x);
terminalArea.setFont(f);
terminalArea.setBackground(Color.BLACK);
terminalArea.setForeground(Color.ORANGE);
/* This is a hack. We cannot disable the caret,
* since setting editable to false also changes
* the meaning of the TAB key - and I want to use it in bash.
* Again - this is a simple DEMO terminal =)
*/
terminalArea.setCaretColor(Color.BLACK);
KeyAdapter kl = new KeyAdapter()
{
public void keyTyped(KeyEvent e)
{
int c = e.getKeyChar();
try
{
out.write(c);
}
catch (IOException e1)
{
}
e.consume();
}
};
terminalArea.addKeyListener(kl);
getContentPane().add(terminalArea, BorderLayout.CENTER);
getContentPane().add(botPanel, BorderLayout.PAGE_END);
setResizable(false);
pack();
setLocationRelativeTo(parent);
new RemoteConsumer().start();
}
public void setContent(String lines)
{
// setText is thread safe, it does not have to be called from
// the Swing GUI thread.
terminalArea.setText(lines);
}
}
/**
* This ServerHostKeyVerifier asks the user on how to proceed if a key cannot be found
* in the in-memory database.
*
*/
class AdvancedVerifier implements ServerHostKeyVerifier
{
public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm,
byte[] serverHostKey) throws Exception
{
final String host = hostname;
final String algo = serverHostKeyAlgorithm;
String message;
/* Check database */
int result = database.verifyHostkey(hostname, serverHostKeyAlgorithm, serverHostKey);
switch (result)
{
case KnownHosts.HOSTKEY_IS_OK:
return true;
case KnownHosts.HOSTKEY_IS_NEW:
message = "Do you want to accept the hostkey (type " + algo + ") from " + host + " ?\n";
break;
case KnownHosts.HOSTKEY_HAS_CHANGED:
message = "WARNING! Hostkey for " + host + " has changed!\nAccept anyway?\n";
break;
default:
throw new IllegalStateException();
}
/* Include the fingerprints in the message */
String hexFingerprint = KnownHosts.createHexFingerprint(serverHostKeyAlgorithm, serverHostKey);
String bubblebabbleFingerprint = KnownHosts.createBubblebabbleFingerprint(serverHostKeyAlgorithm,
serverHostKey);
message += "Hex Fingerprint: " + hexFingerprint + "\nBubblebabble Fingerprint: " + bubblebabbleFingerprint;
/* Now ask the user */
int choice = JOptionPane.showConfirmDialog(loginFrame, message);
if (choice == JOptionPane.YES_OPTION)
{
/* Be really paranoid. We use a hashed hostname entry */
String hashedHostname = KnownHosts.createHashedHostname(hostname);
/* Add the hostkey to the in-memory database */
database.addHostkey(new String[] { hashedHostname }, serverHostKeyAlgorithm, serverHostKey);
/* Also try to add the key to a known_host file */
try
{
KnownHosts.addHostkeyToFile(new File(knownHostPath), new String[] { hashedHostname },
serverHostKeyAlgorithm, serverHostKey);
}
catch (IOException ignore)
{
}
return true;
}
if (choice == JOptionPane.CANCEL_OPTION)
{
throw new Exception("The user aborted the server hostkey verification.");
}
return false;
}
}
/**
* The logic that one has to implement if "keyboard-interactive" autentication shall be
* supported.
*
*/
class InteractiveLogic implements InteractiveCallback
{
int promptCount = 0;
String lastError;
public InteractiveLogic(String lastError)
{
this.lastError = lastError;
}
/* the callback may be invoked several times, depending on how many questions-sets the server sends */
public String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt,
boolean[] echo) throws IOException
{
String[] result = new String[numPrompts];
for (int i = 0; i < numPrompts; i++)
{
/* Often, servers just send empty strings for "name" and "instruction" */
String[] content = new String[] { lastError, name, instruction, prompt[i] };
if (lastError != null)
{
/* show lastError only once */
lastError = null;
}
EnterSomethingDialog esd = new EnterSomethingDialog(loginFrame, "Keyboard Interactive Authentication",
content, !echo[i]);
esd.setVisible(true);
if (esd.answer == null)
throw new IOException("Login aborted by user");
result[i] = esd.answer;
promptCount++;
}
return result;
}
/* We maintain a prompt counter - this enables the detection of situations where the ssh
* server is signaling "authentication failed" even though it did not send a single prompt.
*/
public int getPromptCount()
{
return promptCount;
}
}
/**
* The SSH-2 connection is established in this thread.
* If we would not use a separate thread (e.g., put this code in
* the event handler of the "Login" button) then the GUI would not
* be responsive (missing window repaints if you move the window etc.)
*/
class ConnectionThread extends Thread
{
String hostname;
String username;
public ConnectionThread(String hostname, String username)
{
this.hostname = hostname;
this.username = username;
}
public void run()
{
Connection conn = new Connection(hostname);
try
{
/*
*
* CONNECT AND VERIFY SERVER HOST KEY (with callback)
*
*/
String[] hostkeyAlgos = database.getPreferredServerHostkeyAlgorithmOrder(hostname);
if (hostkeyAlgos != null)
conn.setServerHostKeyAlgorithms(hostkeyAlgos);
conn.connect(new AdvancedVerifier());
/*
*
* AUTHENTICATION PHASE
*
*/
boolean enableKeyboardInteractive = true;
boolean enableDSA = true;
boolean enableRSA = true;
String lastError = null;
while (true)
{
if ((enableDSA || enableRSA) && conn.isAuthMethodAvailable(username, "publickey"))
{
if (enableDSA)
{
File key = new File(idDSAPath);
if (key.exists())
{
EnterSomethingDialog esd = new EnterSomethingDialog(loginFrame, "DSA Authentication",
new String[] { lastError, "Enter DSA private key password:" }, true);
esd.setVisible(true);
boolean res = conn.authenticateWithPublicKey(username, key, esd.answer);
if (res == true)
break;
lastError = "DSA authentication failed.";
}
enableDSA = false; // do not try again
}
if (enableRSA)
{
File key = new File(idRSAPath);
if (key.exists())
{
EnterSomethingDialog esd = new EnterSomethingDialog(loginFrame, "RSA Authentication",
new String[] { lastError, "Enter RSA private key password:" }, true);
esd.setVisible(true);
boolean res = conn.authenticateWithPublicKey(username, key, esd.answer);
if (res == true)
break;
lastError = "RSA authentication failed.";
}
enableRSA = false; // do not try again
}
continue;
}
if (enableKeyboardInteractive && conn.isAuthMethodAvailable(username, "keyboard-interactive"))
{
InteractiveLogic il = new InteractiveLogic(lastError);
boolean res = conn.authenticateWithKeyboardInteractive(username, il);
if (res == true)
break;
if (il.getPromptCount() == 0)
{
// aha. the server announced that it supports "keyboard-interactive", but when
// we asked for it, it just denied the request without sending us any prompt.
// That happens with some server versions/configurations.
// We just disable the "keyboard-interactive" method and notify the user.
lastError = "Keyboard-interactive does not work.";
enableKeyboardInteractive = false; // do not try this again
}
else
{
lastError = "Keyboard-interactive auth failed."; // try again, if possible
}
continue;
}
if (conn.isAuthMethodAvailable(username, "password"))
{
final EnterSomethingDialog esd = new EnterSomethingDialog(loginFrame,
"Password Authentication",
new String[] { lastError, "Enter password for " + username }, true);
esd.setVisible(true);
if (esd.answer == null)
throw new IOException("Login aborted by user");
boolean res = conn.authenticateWithPassword(username, esd.answer);
if (res == true)
break;
lastError = "Password authentication failed."; // try again, if possible
continue;
}
throw new IOException("No supported authentication methods available.");
}
/*
*
* AUTHENTICATION OK. DO SOMETHING.
*
*/
Session sess = conn.openSession();
int x_width = 90;
int y_width = 30;
sess.requestPTY("dumb", x_width, y_width, 0, 0, null);
sess.startShell();
TerminalDialog td = new TerminalDialog(loginFrame, username + "@" + hostname, sess, x_width, y_width);
/* The following call blocks until the dialog has been closed */
td.setVisible(true);
}
catch (IOException e)
{
//e.printStackTrace();
JOptionPane.showMessageDialog(loginFrame, "Exception: " + e.getMessage());
}
/*
*
* CLOSE THE CONNECTION.
*
*/
conn.close();
/*
*
* CLOSE THE LOGIN FRAME - APPLICATION WILL BE EXITED (no more frames)
*
*/
Runnable r = new Runnable()
{
public void run()
{
loginFrame.dispose();
}
};
SwingUtilities.invokeLater(r);
}
}
void loginPressed()
{
String hostname = hostField.getText().trim();
String username = userField.getText().trim();
if ((hostname.length() == 0) || (username.length() == 0))
{
JOptionPane.showMessageDialog(loginFrame, "Please fill out both fields!");
return;
}
loginButton.setEnabled(false);
hostField.setEnabled(false);
userField.setEnabled(false);
ConnectionThread ct = new ConnectionThread(hostname, username);
ct.start();
}
void showGUI()
{
loginFrame = new JFrame("Ganymed SSH2 SwingShell");
hostLabel = new JLabel("Hostname:");
userLabel = new JLabel("Username:");
hostField = new JTextField("", 20);
userField = new JTextField("", 10);
loginButton = new JButton("Login");
loginButton.addActionListener(new ActionListener()
{
public void actionPerformed(java.awt.event.ActionEvent e)
{
loginPressed();
}
});
JPanel loginPanel = new JPanel();
loginPanel.add(hostLabel);
loginPanel.add(hostField);
loginPanel.add(userLabel);
loginPanel.add(userField);
loginPanel.add(loginButton);
loginFrame.getRootPane().setDefaultButton(loginButton);
loginFrame.getContentPane().add(loginPanel, BorderLayout.PAGE_START);
//loginFrame.getContentPane().add(textArea, BorderLayout.CENTER);
loginFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
loginFrame.pack();
loginFrame.setResizable(false);
loginFrame.setLocationRelativeTo(null);
loginFrame.setVisible(true);
}
void startGUI()
{
Runnable r = new Runnable()
{
public void run()
{
showGUI();
}
};
SwingUtilities.invokeLater(r);
}
public static void main(String[] args)
{
SwingShell client = new SwingShell();
client.startGUI();
}
}