Home | History | Annotate | Download | only in examples
      1 /*
      2  * Copyright (c) 2006-2011 Christian Plattner. All rights reserved.
      3  * Please refer to the LICENSE.txt for licensing details.
      4  */
      5 import java.awt.BorderLayout;
      6 import java.awt.Color;
      7 import java.awt.FlowLayout;
      8 import java.awt.Font;
      9 import java.awt.event.ActionEvent;
     10 import java.awt.event.ActionListener;
     11 import java.awt.event.KeyAdapter;
     12 import java.awt.event.KeyEvent;
     13 import java.io.File;
     14 import java.io.IOException;
     15 import java.io.InputStream;
     16 import java.io.OutputStream;
     17 
     18 import javax.swing.BoxLayout;
     19 import javax.swing.JButton;
     20 import javax.swing.JDialog;
     21 import javax.swing.JFrame;
     22 import javax.swing.JLabel;
     23 import javax.swing.JOptionPane;
     24 import javax.swing.JPanel;
     25 import javax.swing.JPasswordField;
     26 import javax.swing.JTextArea;
     27 import javax.swing.JTextField;
     28 import javax.swing.SwingUtilities;
     29 
     30 import ch.ethz.ssh2.Connection;
     31 import ch.ethz.ssh2.InteractiveCallback;
     32 import ch.ethz.ssh2.KnownHosts;
     33 import ch.ethz.ssh2.ServerHostKeyVerifier;
     34 import ch.ethz.ssh2.Session;
     35 
     36 /**
     37  *
     38  * This is a very primitive SSH-2 dumb terminal (Swing based).
     39  *
     40  * The purpose of this class is to demonstrate:
     41  *
     42  * - Verifying server hostkeys with an existing known_hosts file
     43  * - Displaying fingerprints of server hostkeys
     44  * - Adding a server hostkey to a known_hosts file (+hashing the hostname for security)
     45  * - Authentication with DSA, RSA, password and keyboard-interactive methods
     46  *
     47  */
     48 public class SwingShell
     49 {
     50 
     51 	/*
     52 	 * NOTE: to get this feature to work, replace the "tilde" with your home directory,
     53 	 * at least my JVM does not understand it. Need to check the specs.
     54 	 */
     55 
     56 	static final String knownHostPath = "~/.ssh/known_hosts";
     57 	static final String idDSAPath = "~/.ssh/id_dsa";
     58 	static final String idRSAPath = "~/.ssh/id_rsa";
     59 
     60 	JFrame loginFrame = null;
     61 	JLabel hostLabel;
     62 	JLabel userLabel;
     63 	JTextField hostField;
     64 	JTextField userField;
     65 	JButton loginButton;
     66 
     67 	KnownHosts database = new KnownHosts();
     68 
     69 	public SwingShell()
     70 	{
     71 		File knownHostFile = new File(knownHostPath);
     72 		if (knownHostFile.exists())
     73 		{
     74 			try
     75 			{
     76 				database.addHostkeys(knownHostFile);
     77 			}
     78 			catch (IOException e)
     79 			{
     80 			}
     81 		}
     82 	}
     83 
     84 	/**
     85 	 * This dialog displays a number of text lines and a text field.
     86 	 * The text field can either be plain text or a password field.
     87 	 */
     88 	class EnterSomethingDialog extends JDialog
     89 	{
     90 		private static final long serialVersionUID = 1L;
     91 
     92 		JTextField answerField;
     93 		JPasswordField passwordField;
     94 
     95 		final boolean isPassword;
     96 
     97 		String answer;
     98 
     99 		public EnterSomethingDialog(JFrame parent, String title, String content, boolean isPassword)
    100 		{
    101 			this(parent, title, new String[] { content }, isPassword);
    102 		}
    103 
    104 		public EnterSomethingDialog(JFrame parent, String title, String[] content, boolean isPassword)
    105 		{
    106 			super(parent, title, true);
    107 
    108 			this.isPassword = isPassword;
    109 
    110 			JPanel pan = new JPanel();
    111 			pan.setLayout(new BoxLayout(pan, BoxLayout.Y_AXIS));
    112 
    113 			for (int i = 0; i < content.length; i++)
    114 			{
    115 				if ((content[i] == null) || (content[i] == ""))
    116 					continue;
    117 				JLabel contentLabel = new JLabel(content[i]);
    118 				pan.add(contentLabel);
    119 
    120 			}
    121 
    122 			answerField = new JTextField(20);
    123 			passwordField = new JPasswordField(20);
    124 
    125 			if (isPassword)
    126 				pan.add(passwordField);
    127 			else
    128 				pan.add(answerField);
    129 
    130 			KeyAdapter kl = new KeyAdapter()
    131 			{
    132 				public void keyTyped(KeyEvent e)
    133 				{
    134 					if (e.getKeyChar() == '\n')
    135 						finish();
    136 				}
    137 			};
    138 
    139 			answerField.addKeyListener(kl);
    140 			passwordField.addKeyListener(kl);
    141 
    142 			getContentPane().add(BorderLayout.CENTER, pan);
    143 
    144 			setResizable(false);
    145 			pack();
    146 			setLocationRelativeTo(null);
    147 		}
    148 
    149 		private void finish()
    150 		{
    151 			if (isPassword)
    152 				answer = new String(passwordField.getPassword());
    153 			else
    154 				answer = answerField.getText();
    155 
    156 			dispose();
    157 		}
    158 	}
    159 
    160 	/**
    161 	 * TerminalDialog is probably the worst terminal emulator ever written - implementing
    162 	 * a real vt100 is left as an exercise to the reader, i.e., to you =)
    163 	 *
    164 	 */
    165 	class TerminalDialog extends JDialog
    166 	{
    167 		private static final long serialVersionUID = 1L;
    168 
    169 		JPanel botPanel;
    170 		JButton logoffButton;
    171 		JTextArea terminalArea;
    172 
    173 		Session sess;
    174 		InputStream in;
    175 		OutputStream out;
    176 
    177 		int x, y;
    178 
    179 		/**
    180 		 * This thread consumes output from the remote server and displays it in
    181 		 * the terminal window.
    182 		 *
    183 		 */
    184 		class RemoteConsumer extends Thread
    185 		{
    186 			char[][] lines = new char[y][];
    187 			int posy = 0;
    188 			int posx = 0;
    189 
    190 			private void addText(byte[] data, int len)
    191 			{
    192 				for (int i = 0; i < len; i++)
    193 				{
    194 					char c = (char) (data[i] & 0xff);
    195 
    196 					if (c == 8) // Backspace, VERASE
    197 					{
    198 						if (posx < 0)
    199 							continue;
    200 						posx--;
    201 						continue;
    202 					}
    203 
    204 					if (c == '\r')
    205 					{
    206 						posx = 0;
    207 						continue;
    208 					}
    209 
    210 					if (c == '\n')
    211 					{
    212 						posy++;
    213 						if (posy >= y)
    214 						{
    215 							for (int k = 1; k < y; k++)
    216 								lines[k - 1] = lines[k];
    217 							posy--;
    218 							lines[y - 1] = new char[x];
    219 							for (int k = 0; k < x; k++)
    220 								lines[y - 1][k] = ' ';
    221 						}
    222 						continue;
    223 					}
    224 
    225 					if (c < 32)
    226 					{
    227 						continue;
    228 					}
    229 
    230 					if (posx >= x)
    231 					{
    232 						posx = 0;
    233 						posy++;
    234 						if (posy >= y)
    235 						{
    236 							posy--;
    237 							for (int k = 1; k < y; k++)
    238 								lines[k - 1] = lines[k];
    239 							lines[y - 1] = new char[x];
    240 							for (int k = 0; k < x; k++)
    241 								lines[y - 1][k] = ' ';
    242 						}
    243 					}
    244 
    245 					if (lines[posy] == null)
    246 					{
    247 						lines[posy] = new char[x];
    248 						for (int k = 0; k < x; k++)
    249 							lines[posy][k] = ' ';
    250 					}
    251 
    252 					lines[posy][posx] = c;
    253 					posx++;
    254 				}
    255 
    256 				StringBuffer sb = new StringBuffer(x * y);
    257 
    258 				for (int i = 0; i < lines.length; i++)
    259 				{
    260 					if (i != 0)
    261 						sb.append('\n');
    262 
    263 					if (lines[i] != null)
    264 					{
    265 						sb.append(lines[i]);
    266 					}
    267 
    268 				}
    269 				setContent(sb.toString());
    270 			}
    271 
    272 			public void run()
    273 			{
    274 				byte[] buff = new byte[8192];
    275 
    276 				try
    277 				{
    278 					while (true)
    279 					{
    280 						int len = in.read(buff);
    281 						if (len == -1)
    282 							return;
    283 						addText(buff, len);
    284 					}
    285 				}
    286 				catch (Exception e)
    287 				{
    288 				}
    289 			}
    290 		}
    291 
    292 		public TerminalDialog(JFrame parent, String title, Session sess, int x, int y) throws IOException
    293 		{
    294 			super(parent, title, true);
    295 
    296 			this.sess = sess;
    297 
    298 			in = sess.getStdout();
    299 			out = sess.getStdin();
    300 
    301 			this.x = x;
    302 			this.y = y;
    303 
    304 			botPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
    305 
    306 			logoffButton = new JButton("Logout");
    307 			botPanel.add(logoffButton);
    308 
    309 			logoffButton.addActionListener(new ActionListener()
    310 			{
    311 				public void actionPerformed(ActionEvent e)
    312 				{
    313 					/* Dispose the dialog, "setVisible(true)" method will return */
    314 					dispose();
    315 				}
    316 			});
    317 
    318 			Font f = new Font("Monospaced", Font.PLAIN, 16);
    319 
    320 			terminalArea = new JTextArea(y, x);
    321 			terminalArea.setFont(f);
    322 			terminalArea.setBackground(Color.BLACK);
    323 			terminalArea.setForeground(Color.ORANGE);
    324 			/* This is a hack. We cannot disable the caret,
    325 			 * since setting editable to false also changes
    326 			 * the meaning of the TAB key - and I want to use it in bash.
    327 			 * Again - this is a simple DEMO terminal =)
    328 			 */
    329 			terminalArea.setCaretColor(Color.BLACK);
    330 
    331 			KeyAdapter kl = new KeyAdapter()
    332 			{
    333 				public void keyTyped(KeyEvent e)
    334 				{
    335 					int c = e.getKeyChar();
    336 
    337 					try
    338 					{
    339 						out.write(c);
    340 					}
    341 					catch (IOException e1)
    342 					{
    343 					}
    344 					e.consume();
    345 				}
    346 			};
    347 
    348 			terminalArea.addKeyListener(kl);
    349 
    350 			getContentPane().add(terminalArea, BorderLayout.CENTER);
    351 			getContentPane().add(botPanel, BorderLayout.PAGE_END);
    352 
    353 			setResizable(false);
    354 			pack();
    355 			setLocationRelativeTo(parent);
    356 
    357 			new RemoteConsumer().start();
    358 		}
    359 
    360 		public void setContent(String lines)
    361 		{
    362 			// setText is thread safe, it does not have to be called from
    363 			// the Swing GUI thread.
    364 			terminalArea.setText(lines);
    365 		}
    366 	}
    367 
    368 	/**
    369 	 * This ServerHostKeyVerifier asks the user on how to proceed if a key cannot be found
    370 	 * in the in-memory database.
    371 	 *
    372 	 */
    373 	class AdvancedVerifier implements ServerHostKeyVerifier
    374 	{
    375 		public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm,
    376 				byte[] serverHostKey) throws Exception
    377 		{
    378 			final String host = hostname;
    379 			final String algo = serverHostKeyAlgorithm;
    380 
    381 			String message;
    382 
    383 			/* Check database */
    384 
    385 			int result = database.verifyHostkey(hostname, serverHostKeyAlgorithm, serverHostKey);
    386 
    387 			switch (result)
    388 			{
    389 			case KnownHosts.HOSTKEY_IS_OK:
    390 				return true;
    391 
    392 			case KnownHosts.HOSTKEY_IS_NEW:
    393 				message = "Do you want to accept the hostkey (type " + algo + ") from " + host + " ?\n";
    394 				break;
    395 
    396 			case KnownHosts.HOSTKEY_HAS_CHANGED:
    397 				message = "WARNING! Hostkey for " + host + " has changed!\nAccept anyway?\n";
    398 				break;
    399 
    400 			default:
    401 				throw new IllegalStateException();
    402 			}
    403 
    404 			/* Include the fingerprints in the message */
    405 
    406 			String hexFingerprint = KnownHosts.createHexFingerprint(serverHostKeyAlgorithm, serverHostKey);
    407 			String bubblebabbleFingerprint = KnownHosts.createBubblebabbleFingerprint(serverHostKeyAlgorithm,
    408 					serverHostKey);
    409 
    410 			message += "Hex Fingerprint: " + hexFingerprint + "\nBubblebabble Fingerprint: " + bubblebabbleFingerprint;
    411 
    412 			/* Now ask the user */
    413 
    414 			int choice = JOptionPane.showConfirmDialog(loginFrame, message);
    415 
    416 			if (choice == JOptionPane.YES_OPTION)
    417 			{
    418 				/* Be really paranoid. We use a hashed hostname entry */
    419 
    420 				String hashedHostname = KnownHosts.createHashedHostname(hostname);
    421 
    422 				/* Add the hostkey to the in-memory database */
    423 
    424 				database.addHostkey(new String[] { hashedHostname }, serverHostKeyAlgorithm, serverHostKey);
    425 
    426 				/* Also try to add the key to a known_host file */
    427 
    428 				try
    429 				{
    430 					KnownHosts.addHostkeyToFile(new File(knownHostPath), new String[] { hashedHostname },
    431 							serverHostKeyAlgorithm, serverHostKey);
    432 				}
    433 				catch (IOException ignore)
    434 				{
    435 				}
    436 
    437 				return true;
    438 			}
    439 
    440 			if (choice == JOptionPane.CANCEL_OPTION)
    441 			{
    442 				throw new Exception("The user aborted the server hostkey verification.");
    443 			}
    444 
    445 			return false;
    446 		}
    447 	}
    448 
    449 	/**
    450 	 * The logic that one has to implement if "keyboard-interactive" autentication shall be
    451 	 * supported.
    452 	 *
    453 	 */
    454 	class InteractiveLogic implements InteractiveCallback
    455 	{
    456 		int promptCount = 0;
    457 		String lastError;
    458 
    459 		public InteractiveLogic(String lastError)
    460 		{
    461 			this.lastError = lastError;
    462 		}
    463 
    464 		/* the callback may be invoked several times, depending on how many questions-sets the server sends */
    465 
    466 		public String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt,
    467 				boolean[] echo) throws IOException
    468 		{
    469 			String[] result = new String[numPrompts];
    470 
    471 			for (int i = 0; i < numPrompts; i++)
    472 			{
    473 				/* Often, servers just send empty strings for "name" and "instruction" */
    474 
    475 				String[] content = new String[] { lastError, name, instruction, prompt[i] };
    476 
    477 				if (lastError != null)
    478 				{
    479 					/* show lastError only once */
    480 					lastError = null;
    481 				}
    482 
    483 				EnterSomethingDialog esd = new EnterSomethingDialog(loginFrame, "Keyboard Interactive Authentication",
    484 						content, !echo[i]);
    485 
    486 				esd.setVisible(true);
    487 
    488 				if (esd.answer == null)
    489 					throw new IOException("Login aborted by user");
    490 
    491 				result[i] = esd.answer;
    492 				promptCount++;
    493 			}
    494 
    495 			return result;
    496 		}
    497 
    498 		/* We maintain a prompt counter - this enables the detection of situations where the ssh
    499 		 * server is signaling "authentication failed" even though it did not send a single prompt.
    500 		 */
    501 
    502 		public int getPromptCount()
    503 		{
    504 			return promptCount;
    505 		}
    506 	}
    507 
    508 	/**
    509 	 * The SSH-2 connection is established in this thread.
    510 	 * If we would not use a separate thread (e.g., put this code in
    511 	 * the event handler of the "Login" button) then the GUI would not
    512 	 * be responsive (missing window repaints if you move the window etc.)
    513 	 */
    514 	class ConnectionThread extends Thread
    515 	{
    516 		String hostname;
    517 		String username;
    518 
    519 		public ConnectionThread(String hostname, String username)
    520 		{
    521 			this.hostname = hostname;
    522 			this.username = username;
    523 		}
    524 
    525 		public void run()
    526 		{
    527 			Connection conn = new Connection(hostname);
    528 
    529 			try
    530 			{
    531 				/*
    532 				 *
    533 				 * CONNECT AND VERIFY SERVER HOST KEY (with callback)
    534 				 *
    535 				 */
    536 
    537 				String[] hostkeyAlgos = database.getPreferredServerHostkeyAlgorithmOrder(hostname);
    538 
    539 				if (hostkeyAlgos != null)
    540 					conn.setServerHostKeyAlgorithms(hostkeyAlgos);
    541 
    542 				conn.connect(new AdvancedVerifier());
    543 
    544 				/*
    545 				 *
    546 				 * AUTHENTICATION PHASE
    547 				 *
    548 				 */
    549 
    550 				boolean enableKeyboardInteractive = true;
    551 				boolean enableDSA = true;
    552 				boolean enableRSA = true;
    553 
    554 				String lastError = null;
    555 
    556 				while (true)
    557 				{
    558 					if ((enableDSA || enableRSA) && conn.isAuthMethodAvailable(username, "publickey"))
    559 					{
    560 						if (enableDSA)
    561 						{
    562 							File key = new File(idDSAPath);
    563 
    564 							if (key.exists())
    565 							{
    566 								EnterSomethingDialog esd = new EnterSomethingDialog(loginFrame, "DSA Authentication",
    567 										new String[] { lastError, "Enter DSA private key password:" }, true);
    568 								esd.setVisible(true);
    569 
    570 								boolean res = conn.authenticateWithPublicKey(username, key, esd.answer);
    571 
    572 								if (res == true)
    573 									break;
    574 
    575 								lastError = "DSA authentication failed.";
    576 							}
    577 							enableDSA = false; // do not try again
    578 						}
    579 
    580 						if (enableRSA)
    581 						{
    582 							File key = new File(idRSAPath);
    583 
    584 							if (key.exists())
    585 							{
    586 								EnterSomethingDialog esd = new EnterSomethingDialog(loginFrame, "RSA Authentication",
    587 										new String[] { lastError, "Enter RSA private key password:" }, true);
    588 								esd.setVisible(true);
    589 
    590 								boolean res = conn.authenticateWithPublicKey(username, key, esd.answer);
    591 
    592 								if (res == true)
    593 									break;
    594 
    595 								lastError = "RSA authentication failed.";
    596 							}
    597 							enableRSA = false; // do not try again
    598 						}
    599 
    600 						continue;
    601 					}
    602 
    603 					if (enableKeyboardInteractive && conn.isAuthMethodAvailable(username, "keyboard-interactive"))
    604 					{
    605 						InteractiveLogic il = new InteractiveLogic(lastError);
    606 
    607 						boolean res = conn.authenticateWithKeyboardInteractive(username, il);
    608 
    609 						if (res == true)
    610 							break;
    611 
    612 						if (il.getPromptCount() == 0)
    613 						{
    614 							// aha. the server announced that it supports "keyboard-interactive", but when
    615 							// we asked for it, it just denied the request without sending us any prompt.
    616 							// That happens with some server versions/configurations.
    617 							// We just disable the "keyboard-interactive" method and notify the user.
    618 
    619 							lastError = "Keyboard-interactive does not work.";
    620 
    621 							enableKeyboardInteractive = false; // do not try this again
    622 						}
    623 						else
    624 						{
    625 							lastError = "Keyboard-interactive auth failed."; // try again, if possible
    626 						}
    627 
    628 						continue;
    629 					}
    630 
    631 					if (conn.isAuthMethodAvailable(username, "password"))
    632 					{
    633 						final EnterSomethingDialog esd = new EnterSomethingDialog(loginFrame,
    634 								"Password Authentication",
    635 								new String[] { lastError, "Enter password for " + username }, true);
    636 
    637 						esd.setVisible(true);
    638 
    639 						if (esd.answer == null)
    640 							throw new IOException("Login aborted by user");
    641 
    642 						boolean res = conn.authenticateWithPassword(username, esd.answer);
    643 
    644 						if (res == true)
    645 							break;
    646 
    647 						lastError = "Password authentication failed."; // try again, if possible
    648 
    649 						continue;
    650 					}
    651 
    652 					throw new IOException("No supported authentication methods available.");
    653 				}
    654 
    655 				/*
    656 				 *
    657 				 * AUTHENTICATION OK. DO SOMETHING.
    658 				 *
    659 				 */
    660 
    661 				Session sess = conn.openSession();
    662 
    663 				int x_width = 90;
    664 				int y_width = 30;
    665 
    666 				sess.requestPTY("dumb", x_width, y_width, 0, 0, null);
    667 				sess.startShell();
    668 
    669 				TerminalDialog td = new TerminalDialog(loginFrame, username + "@" + hostname, sess, x_width, y_width);
    670 
    671 				/* The following call blocks until the dialog has been closed */
    672 
    673 				td.setVisible(true);
    674 
    675 			}
    676 			catch (IOException e)
    677 			{
    678 				//e.printStackTrace();
    679 				JOptionPane.showMessageDialog(loginFrame, "Exception: " + e.getMessage());
    680 			}
    681 
    682 			/*
    683 			 *
    684 			 * CLOSE THE CONNECTION.
    685 			 *
    686 			 */
    687 
    688 			conn.close();
    689 
    690 			/*
    691 			 *
    692 			 * CLOSE THE LOGIN FRAME - APPLICATION WILL BE EXITED (no more frames)
    693 			 *
    694 			 */
    695 
    696 			Runnable r = new Runnable()
    697 			{
    698 				public void run()
    699 				{
    700 					loginFrame.dispose();
    701 				}
    702 			};
    703 
    704 			SwingUtilities.invokeLater(r);
    705 		}
    706 	}
    707 
    708 	void loginPressed()
    709 	{
    710 		String hostname = hostField.getText().trim();
    711 		String username = userField.getText().trim();
    712 
    713 		if ((hostname.length() == 0) || (username.length() == 0))
    714 		{
    715 			JOptionPane.showMessageDialog(loginFrame, "Please fill out both fields!");
    716 			return;
    717 		}
    718 
    719 		loginButton.setEnabled(false);
    720 		hostField.setEnabled(false);
    721 		userField.setEnabled(false);
    722 
    723 		ConnectionThread ct = new ConnectionThread(hostname, username);
    724 
    725 		ct.start();
    726 	}
    727 
    728 	void showGUI()
    729 	{
    730 		loginFrame = new JFrame("Ganymed SSH2 SwingShell");
    731 
    732 		hostLabel = new JLabel("Hostname:");
    733 		userLabel = new JLabel("Username:");
    734 
    735 		hostField = new JTextField("", 20);
    736 		userField = new JTextField("", 10);
    737 
    738 		loginButton = new JButton("Login");
    739 
    740 		loginButton.addActionListener(new ActionListener()
    741 		{
    742 			public void actionPerformed(java.awt.event.ActionEvent e)
    743 			{
    744 				loginPressed();
    745 			}
    746 		});
    747 
    748 		JPanel loginPanel = new JPanel();
    749 
    750 		loginPanel.add(hostLabel);
    751 		loginPanel.add(hostField);
    752 		loginPanel.add(userLabel);
    753 		loginPanel.add(userField);
    754 		loginPanel.add(loginButton);
    755 
    756 		loginFrame.getRootPane().setDefaultButton(loginButton);
    757 
    758 		loginFrame.getContentPane().add(loginPanel, BorderLayout.PAGE_START);
    759 		//loginFrame.getContentPane().add(textArea, BorderLayout.CENTER);
    760 
    761 		loginFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    762 
    763 		loginFrame.pack();
    764 		loginFrame.setResizable(false);
    765 		loginFrame.setLocationRelativeTo(null);
    766 		loginFrame.setVisible(true);
    767 	}
    768 
    769 	void startGUI()
    770 	{
    771 		Runnable r = new Runnable()
    772 		{
    773 			public void run()
    774 			{
    775 				showGUI();
    776 			}
    777 		};
    778 
    779 		SwingUtilities.invokeLater(r);
    780 
    781 	}
    782 
    783 	public static void main(String[] args)
    784 	{
    785 		SwingShell client = new SwingShell();
    786 		client.startGUI();
    787 	}
    788 }
    789