import com.trilead.ssh2.*; import javax.swing.*; import java.awt.*; 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; /** * This is a very primitive SSH-2 dumb terminal (Swing based). * <p> * The purpose of this class is to demonstrate: * <ul> * <li>Verifying server hostkeys with an existing known_hosts file</li> * <li>Displaying fingerprints of server hostkeys</li> * <li>Adding a server hostkey to a known_hosts file (+hashing the hostname for security)</li> * <li>Authentication with DSA, RSA, password and keyboard-interactive methods</li> * </ul> * * @author Christian Plattner, plattner@trilead.com * @version $Id: SwingShell.java,v 1.10 2007/10/15 12:49:57 cplattne Exp $ * */ 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("Trilead SSH-2 for Java 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(); } }