diff options
Diffstat (limited to 'examples/SwingShell.java')
-rw-r--r-- | examples/SwingShell.java | 788 |
1 files changed, 788 insertions, 0 deletions
diff --git a/examples/SwingShell.java b/examples/SwingShell.java new file mode 100644 index 0000000..12554cf --- /dev/null +++ b/examples/SwingShell.java @@ -0,0 +1,788 @@ +/* + * 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(); + } +} |