aboutsummaryrefslogtreecommitdiff
path: root/examples/SwingShell.java
diff options
context:
space:
mode:
Diffstat (limited to 'examples/SwingShell.java')
-rw-r--r--examples/SwingShell.java788
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();
+ }
+}