• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
SwingShell()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 
EnterSomethingDialog(JFrame parent, String title, String content, boolean isPassword)99 		public EnterSomethingDialog(JFrame parent, String title, String content, boolean isPassword)
100 		{
101 			this(parent, title, new String[] { content }, isPassword);
102 		}
103 
EnterSomethingDialog(JFrame parent, String title, String[] content, boolean isPassword)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 
finish()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 
addText(byte[] data, int len)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 
run()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 
TerminalDialog(JFrame parent, String title, Session sess, int x, int y)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 
setContent(String lines)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 	{
verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey)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 
InteractiveLogic(String lastError)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 
replyToChallenge(String name, String instruction, int numPrompts, String[] prompt, boolean[] echo)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 
getPromptCount()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 
ConnectionThread(String hostname, String username)519 		public ConnectionThread(String hostname, String username)
520 		{
521 			this.hostname = hostname;
522 			this.username = username;
523 		}
524 
run()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 
loginPressed()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 
showGUI()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 
startGUI()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 
main(String[] args)783 	public static void main(String[] args)
784 	{
785 		SwingShell client = new SwingShell();
786 		client.startGUI();
787 	}
788 }
789