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