]> source.dussan.org Git - tigervnc.git/commitdiff
Overhaul of SSH tunneling features in Java viewer
authorBrian P. Hinz <bphinz@users.sf.net>
Thu, 31 Mar 2016 03:25:46 +0000 (23:25 -0400)
committerBrian P. Hinz <bphinz@users.sf.net>
Thu, 31 Mar 2016 12:31:28 +0000 (08:31 -0400)
Fixes numerous problems with the SSH tunneling implementation
on the java viewer and adds many significant new SSH-related
features.  SSH tunneling is now highly configurable via the
both the command line and GUI. The embedded client can parse
openssh config files.  An external client may be specified,
along with a custom arguments template.

java/com/tigervnc/vncviewer/CConn.java
java/com/tigervnc/vncviewer/Dialog.java
java/com/tigervnc/vncviewer/OptionsDialog.java
java/com/tigervnc/vncviewer/PasswdDialog.java
java/com/tigervnc/vncviewer/Tunnel.java [new file with mode: 0644]
java/com/tigervnc/vncviewer/VncViewer.java
java/com/tigervnc/vncviewer/tunnel.java [deleted file]

index dbb2a2935401434d40454928915485eb00ab0aa4..b9680ef7aa500a0c725e1fe736286252a6848df6 100644 (file)
@@ -78,7 +78,7 @@ public class CConn extends CConnection implements
   public CConn(VncViewer viewer_, Socket sock_,
                String vncServerName)
   {
-    serverHost = null; serverPort = 0; sock = sock_; viewer = viewer_;
+    sock = sock_; viewer = viewer_;
     pendingPFChange = false;
     currentEncoding = Encodings.encodingTight; lastServerEncoding = -1;
     fullColour = viewer.fullColour.getValue();
@@ -121,8 +121,8 @@ public class CConn extends CConnection implements
     } else {
       if (vncServerName != null &&
           !viewer.alwaysShowServerDialog.getValue()) {
-        serverHost = Hostname.getHost(vncServerName);
-        serverPort = Hostname.getPort(vncServerName);
+        setServerName(Hostname.getHost(vncServerName));
+        setServerPort(Hostname.getPort(vncServerName));
       } else {
         ServerDialog dlg = new ServerDialog(options, vncServerName, this);
         boolean ret = dlg.showDialog();
@@ -130,20 +130,27 @@ public class CConn extends CConnection implements
           close();
           return;
         }
-        serverHost = viewer.vncServerName.getValueStr();
-        serverPort = viewer.vncServerPort.getValue();
+        setServerName(viewer.vncServerName.getValueStr());
+        setServerPort(viewer.vncServerPort.getValue());
       }
 
       try {
-        sock = new TcpSocket(serverHost, serverPort);
+        if (viewer.tunnel.getValue() || (viewer.via.getValue() != null)) {
+          int localPort = TcpSocket.findFreeTcpPort();
+          if (localPort == 0)
+            throw new Exception("Could not obtain free TCP port");
+          Tunnel.createTunnel(this, localPort);
+          sock = new TcpSocket("localhost", localPort);
+        } else {
+          sock = new TcpSocket(getServerName(), getServerPort());
+        }
       } catch (java.lang.Exception e) {
         throw new Exception(e.getMessage());
       }
-      vlog.info("connected to host "+serverHost+" port "+serverPort);
+      vlog.info("connected to host "+getServerName()+" port "+getServerPort());
     }
 
     sock.inStream().setBlockCallback(this);
-    setServerName(serverHost);
     setStreams(sock.inStream(), sock.outStream());
     initialiseProtocol();
   }
@@ -896,10 +903,47 @@ public class CConn extends CConnection implements
       options.sendLocalUsername.setEnabled(false);
       options.cfLoadButton.setEnabled(false);
       options.cfSaveAsButton.setEnabled(true);
+      options.sshTunnel.setEnabled(false);
+      options.sshUseGateway.setEnabled(false);
+      options.sshUser.setEnabled(false);
+      options.sshHost.setEnabled(false);
+      options.sshPort.setEnabled(false);
+      options.sshUseExt.setEnabled(false);
+      options.sshClient.setEnabled(false);
+      options.sshClientBrowser.setEnabled(false);
+      options.sshArgsDefault.setEnabled(false);
+      options.sshArgsCustom.setEnabled(false);
+      options.sshArguments.setEnabled(false);
+      options.sshConfig.setEnabled(false);
+      options.sshConfigBrowser.setEnabled(false);
+      options.sshKeyFile.setEnabled(false);
+      options.sshKeyFileBrowser.setEnabled(false);
     } else {
       options.shared.setSelected(viewer.shared.getValue());
       options.sendLocalUsername.setSelected(viewer.sendLocalUsername.getValue());
       options.cfSaveAsButton.setEnabled(false);
+      if (viewer.tunnel.getValue() || viewer.via.getValue() != null)
+        options.sshTunnel.setSelected(true);
+      if (viewer.via.getValue() != null)
+        options.sshUseGateway.setSelected(true);
+      options.sshUser.setText(Tunnel.getSshUser(this));
+      options.sshHost.setText(Tunnel.getSshHost(this));
+      options.sshPort.setText(Integer.toString(Tunnel.getSshPort(this)));
+      options.sshUseExt.setSelected(viewer.extSSH.getValue());
+      File client = new File(viewer.extSSHClient.getValue());
+      if (client.exists() && client.canRead())
+        options.sshClient.setText(client.getAbsolutePath());
+      if (viewer.extSSHArgs.getValue() == null) {
+        options.sshArgsDefault.setSelected(true);
+        options.sshArguments.setText("");
+      } else {
+        options.sshArgsCustom.setSelected(true);
+        options.sshArguments.setText(viewer.extSSHArgs.getValue());
+      }
+      File config = new File(viewer.sshConfig.getValue());
+      if (config.exists() && config.canRead())
+        options.sshConfig.setText(config.getAbsolutePath());
+      options.sshKeyFile.setText(Tunnel.getSshKeyFile(this));
 
       /* Process non-VeNCrypt sectypes */
       java.util.List<Integer> secTypes = new ArrayList<Integer>();
@@ -990,6 +1034,46 @@ public class CConn extends CConnection implements
       options.secPlain.setEnabled(options.secVeNCrypt.isSelected());
       options.sendLocalUsername.setEnabled(options.secPlain.isSelected()||
         options.secIdent.isSelected());
+      options.sshTunnel.setEnabled(true);
+        options.sshUseGateway.setEnabled(options.sshTunnel.isSelected());
+        options.sshUser.setEnabled(options.sshTunnel.isSelected() &&
+                                   options.sshUseGateway.isEnabled() &&
+                                   options.sshUseGateway.isSelected());
+        options.sshHost.setEnabled(options.sshTunnel.isSelected() &&
+                                   options.sshUseGateway.isEnabled() &&
+                                   options.sshUseGateway.isSelected());
+        options.sshPort.setEnabled(options.sshTunnel.isSelected() &&
+                                   options.sshUseGateway.isEnabled() &&
+                                   options.sshUseGateway.isSelected());
+        options.sshUseExt.setEnabled(options.sshTunnel.isSelected());
+        options.sshClient.setEnabled(options.sshTunnel.isSelected() &&
+                                     options.sshUseExt.isEnabled() &&
+                                     options.sshUseExt.isSelected());
+        options.sshClientBrowser.setEnabled(options.sshTunnel.isSelected() &&
+                                            options.sshUseExt.isEnabled() &&
+                                            options.sshUseExt.isSelected());
+        options.sshArgsDefault.setEnabled(options.sshTunnel.isSelected() &&
+                                          options.sshUseExt.isEnabled() &&
+                                          options.sshUseExt.isSelected());
+        options.sshArgsCustom.setEnabled(options.sshTunnel.isSelected() &&
+                                         options.sshUseExt.isEnabled() &&
+                                         options.sshUseExt.isSelected());
+        options.sshArguments.setEnabled(options.sshTunnel.isSelected() &&
+                                        options.sshUseExt.isEnabled() &&
+                                        options.sshUseExt.isSelected() &&
+                                        options.sshArgsCustom.isSelected());
+        options.sshConfig.setEnabled(options.sshTunnel.isSelected() &&
+                                     options.sshUseExt.isEnabled() &&
+                                     !options.sshUseExt.isSelected());
+        options.sshConfigBrowser.setEnabled(options.sshTunnel.isSelected() &&
+                                            options.sshUseExt.isEnabled() &&
+                                            !options.sshUseExt.isSelected());
+        options.sshKeyFile.setEnabled(options.sshTunnel.isSelected() &&
+                                      options.sshUseExt.isEnabled() &&
+                                      !options.sshUseExt.isSelected());
+        options.sshKeyFileBrowser.setEnabled(options.sshTunnel.isSelected() &&
+                                             options.sshUseExt.isEnabled() &&
+                                             !options.sshUseExt.isSelected());
     }
 
     options.fullScreen.setSelected(fullScreen);
@@ -1111,6 +1195,7 @@ public class CConn extends CConnection implements
       if (desktop != null)
         desktop.resetLocalCursor();
     }
+    viewer.extSSH.setParam(options.sshUseExt.isSelected());
 
     checkEncodings();
 
@@ -1221,6 +1306,22 @@ public class CConn extends CConnection implements
         Security.DisableSecType(Security.secTypeTLSIdent);
         Security.DisableSecType(Security.secTypeX509Ident);
       }
+      if (options.sshTunnel.isSelected()) {
+        if (options.sshUseGateway.isSelected()) {
+          String user = options.sshUser.getText();
+          String host = options.sshHost.getText();
+          String port = options.sshPort.getText();
+          viewer.via.setParam(user+"@"+host+":"+port);
+        } else {
+          viewer.tunnel.setParam(true);
+        }
+      }
+      viewer.extSSH.setParam(options.sshUseExt.isSelected());
+      viewer.extSSHClient.setParam(options.sshClient.getText());
+      if (options.sshArgsCustom.isSelected())
+        viewer.extSSHArgs.setParam(options.sshArguments.getText());
+      viewer.sshConfig.setParam(options.sshConfig.getText());
+      viewer.sshKeyFile.setParam(options.sshKeyFile.getText());
     }
     String desktopSize = (options.desktopSize.isSelected()) ?
         options.desktopWidth.getText() + "x" + options.desktopHeight.getText() : "";
@@ -1472,8 +1573,6 @@ public class CConn extends CConnection implements
   // the following are only ever accessed by the GUI thread:
   int buttonMask;
 
-  private String serverHost;
-  private int serverPort;
   private Socket sock;
 
   protected DesktopWindow desktop;
index 3d24619b20522942cda2670b75285a57dccba3c2..8bf197995fd501a6e01091294b37b7fc15fdc39e 100644 (file)
@@ -99,6 +99,8 @@ class Dialog extends JDialog implements ActionListener,
     for (Component ch : c.getComponents()) {
       if (ch instanceof JCheckBox)
         ((JCheckBox)ch).addItemListener(this);
+      else if (ch instanceof JRadioButton)
+        ((JRadioButton)ch).addActionListener(this);
       else if (ch instanceof JButton)
         ((JButton)ch).addActionListener(this);
       else if (ch instanceof JComboBox)
index 1681518ddad536e99a5d5c198dffd97746e78a8b..369b965d40ca1c578ff1112cc7adb3214247ac88 100644 (file)
@@ -31,7 +31,6 @@ import javax.swing.text.*;
 import java.util.*;
 import java.util.Map.Entry;
 
-
 import com.tigervnc.rfb.*;
 
 import static java.awt.GridBagConstraints.BOTH;
@@ -50,16 +49,16 @@ class OptionsDialog extends Dialog {
   private class IntegerDocument extends PlainDocument {
     private int limit;
 
-    IntegerDocument(int limit) {
+    public IntegerDocument(int max) {
       super();
-      this.limit = limit;
+      limit = max;
     }
 
-    public void insertString(int offset, String  str, AttributeSet a)
+    public void insertString(int offset, String str, AttributeSet a)
           throws BadLocationException {
       if (str == null || !str.matches("^[0-9]+$")) return;
       if ((getLength() + str.length()) > limit)
-        java.awt.Toolkit.getDefaultToolkit().beep();
+        Toolkit.getDefaultToolkit().beep();
       else
         super.insertString(offset, str, a);
     }
@@ -68,11 +67,11 @@ class OptionsDialog extends Dialog {
   private class IntegerTextField extends JFormattedTextField {
     public IntegerTextField(int digits) {
       super();
-      this.setDocument(new IntegerDocument(digits));
+      setDocument(new IntegerDocument(digits));
       Font f = getFont();
       String template = String.format("%0"+digits+"d", 0);
       int w = getFontMetrics(f).stringWidth(template) +
-              getMargin().left + getMargin().right + 
+              getMargin().left + getMargin().right +
               getInsets().left + getInsets().right;
       int h = getPreferredSize().height;
       setPreferredSize(new Dimension(w, h));
@@ -95,17 +94,20 @@ class OptionsDialog extends Dialog {
   CConn cc;
   @SuppressWarnings({"rawtypes"})
   JComboBox menuKey, compressLevel, qualityLevel, scalingFactor;
-  ButtonGroup encodingGroup, colourGroup;
+  ButtonGroup encodingGroup, colourGroup, sshArgsGroup;
   JRadioButton zrle, hextile, tight, raw, fullColour, mediumColour,
-               lowColour, veryLowColour;
+               lowColour, veryLowColour, sshArgsDefault, sshArgsCustom;
   JCheckBox autoSelect, customCompressLevel, noJpeg, viewOnly,
             acceptClipboard, sendClipboard, acceptBell, desktopSize,
             fullScreen, fullScreenAllMonitors, shared, useLocalCursor,
             secVeNCrypt, encNone, encTLS, encX509, secNone, secVnc,
-            secPlain, secIdent, sendLocalUsername;
+            secPlain, secIdent, sendLocalUsername, sshTunnel, sshUseExt,
+            sshUseGateway;
   JButton okButton, cancelButton, caButton, crlButton, cfLoadButton,
-          cfSaveAsButton, defSaveButton, defReloadButton, defClearButton;
-  JTextField desktopWidth, desktopHeight, x509ca, x509crl;
+          cfSaveAsButton, defSaveButton, defReloadButton, defClearButton,
+          sshConfigBrowser, sshKeyFileBrowser, sshClientBrowser;
+  JTextField desktopWidth, desktopHeight, x509ca, x509crl, sshUser, sshHost,
+             sshPort, sshClient, sshArguments, sshConfig, sshKeyFile;
   JTabbedPane tabPane;
 
   @SuppressWarnings({"rawtypes","unchecked"})
@@ -123,6 +125,7 @@ class OptionsDialog extends Dialog {
 
     encodingGroup = new ButtonGroup();
     colourGroup = new ButtonGroup();
+    sshArgsGroup = new ButtonGroup();
     int indent = 0;
 
     // Compression tab
@@ -573,6 +576,248 @@ class OptionsDialog extends Dialog {
                                          new Insets(0, 0, 0, 0),
                                          NONE, NONE));
 
+    // SSH tab
+    JPanel sshPanel = new JPanel(new GridBagLayout());
+    sshPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5));
+    sshTunnel = new JCheckBox("Tunnel VNC over SSH");
+
+    JPanel tunnelPanel = new JPanel(new GridBagLayout());
+
+    sshUseGateway = new JCheckBox("Use SSH gateway");
+    JLabel sshUserLabel = new JLabel("Username");
+    sshUser = new JTextField();
+    JLabel sshUserAtLabel = new JLabel("@");
+    JLabel sshHostLabel = new JLabel("Hostname (or IP address)");
+    sshHost = new JTextField("");
+    JLabel sshPortLabel = new JLabel("Port");
+    sshPort = new IntegerTextField(5);
+
+    sshUseExt = new JCheckBox("Use external SSH client");
+    sshClient = new JTextField();
+    sshClient.setName(Configuration.getParam("extSSHClient").getName());
+    sshClientBrowser = new JButton("Browse");
+    JLabel sshConfigLabel = new JLabel("SSH config file");
+    sshConfig = new JTextField();
+    sshConfig.setName(Configuration.getParam("sshConfig").getName());
+    sshConfigBrowser = new JButton("Browse");
+    JLabel sshKeyFileLabel = new JLabel("SSH identity file");
+    sshKeyFile = new JTextField();
+    sshKeyFile.setName(Configuration.getParam("sshKeyFile").getName());
+    sshKeyFileBrowser = new JButton("Browse");
+    JPanel sshArgsPanel = new JPanel(new GridBagLayout());
+    JLabel sshArgsLabel = new JLabel("Arguments:");
+    sshArgsDefault =
+      new GroupedJRadioButton("Default", sshArgsGroup, sshArgsPanel);
+    sshArgsCustom =
+      new GroupedJRadioButton("Custom", sshArgsGroup, sshArgsPanel);
+    sshArguments = new JTextField();
+
+    JPanel gatewayPanel = new JPanel(new GridBagLayout());
+    gatewayPanel.add(sshUseGateway,
+                    new GridBagConstraints(0, 0,
+                                           REMAINDER, 1,
+                                           LIGHT, LIGHT,
+                                           LINE_START, NONE,
+                                           new Insets(0, 0, 4, 0),
+                                           NONE, NONE));
+    indent = getButtonLabelInset(sshUseGateway);
+    gatewayPanel.add(sshUserLabel,
+                 new GridBagConstraints(0, 1,
+                                        1, 1,
+                                        LIGHT, LIGHT,
+                                        LINE_START, HORIZONTAL,
+                                        new Insets(0, indent, 4, 0),
+                                        NONE, NONE));
+    gatewayPanel.add(sshHostLabel,
+                 new GridBagConstraints(2, 1,
+                                        1, 1,
+                                        HEAVY, LIGHT,
+                                        LINE_START, HORIZONTAL,
+                                        new Insets(0, 0, 4, 0),
+                                        NONE, NONE));
+    gatewayPanel.add(sshPortLabel,
+                 new GridBagConstraints(3, 1,
+                                        1, 1,
+                                        LIGHT, LIGHT,
+                                        LINE_START, HORIZONTAL,
+                                        new Insets(0, 5, 4, 0),
+                                        NONE, NONE));
+    gatewayPanel.add(sshUser,
+                 new GridBagConstraints(0, 2,
+                                        1, 1,
+                                        LIGHT, LIGHT,
+                                        LINE_START, HORIZONTAL,
+                                        new Insets(0, indent, 0, 0),
+                                        NONE, NONE));
+    gatewayPanel.add(sshUserAtLabel,
+                 new GridBagConstraints(1, 2,
+                                        1, 1,
+                                        LIGHT, LIGHT,
+                                        LINE_START, HORIZONTAL,
+                                        new Insets(0, 2, 0, 2),
+                                        NONE, NONE));
+    gatewayPanel.add(sshHost,
+                 new GridBagConstraints(2, 2,
+                                        1, 1,
+                                        HEAVY, LIGHT,
+                                        LINE_START, HORIZONTAL,
+                                        new Insets(0, 0, 0, 0),
+                                        NONE, NONE));
+    gatewayPanel.add(sshPort,
+                 new GridBagConstraints(3, 2,
+                                        1, 1,
+                                        LIGHT, LIGHT,
+                                        LINE_START, HORIZONTAL,
+                                        new Insets(0, 5, 0, 0),
+                                        NONE, NONE));
+
+    JPanel clientPanel = new JPanel(new GridBagLayout());
+    clientPanel.add(sshUseExt,
+                    new GridBagConstraints(0, 0,
+                                           1, 1,
+                                           LIGHT, LIGHT,
+                                           LINE_START, NONE,
+                                           new Insets(0, 0, 0, 0),
+                                           NONE, NONE));
+    clientPanel.add(sshClient,
+                    new GridBagConstraints(1, 0,
+                                           1, 1,
+                                           HEAVY, LIGHT,
+                                           LINE_START, HORIZONTAL,
+                                           new Insets(0, 5, 0, 0),
+                                           NONE, NONE));
+    clientPanel.add(sshClientBrowser,
+                    new GridBagConstraints(2, 0,
+                                           1, 1,
+                                           LIGHT, LIGHT,
+                                           LINE_START, NONE,
+                                           new Insets(0, 5, 0, 0),
+                                           NONE, NONE));
+    sshArgsPanel.add(sshArgsLabel,
+                    new GridBagConstraints(0, 1,
+                                           1, 1,
+                                           LIGHT, LIGHT,
+                                           LINE_START, NONE,
+                                           new Insets(0, 0, 0, 0),
+                                           NONE, NONE));
+    sshArgsPanel.add(sshArgsDefault,
+                    new GridBagConstraints(1, 1,
+                                           1, 1,
+                                           LIGHT, LIGHT,
+                                           LINE_START, NONE,
+                                           new Insets(0, 5, 0, 0),
+                                           NONE, NONE));
+    sshArgsPanel.add(sshArgsCustom,
+                    new GridBagConstraints(2, 1,
+                                           1, 1,
+                                           LIGHT, LIGHT,
+                                           LINE_START, NONE,
+                                           new Insets(0, 5, 0, 0),
+                                           NONE, NONE));
+    sshArgsPanel.add(sshArguments,
+                    new GridBagConstraints(3, 1,
+                                           1, 1,
+                                           HEAVY, LIGHT,
+                                           LINE_START, HORIZONTAL,
+                                           new Insets(0, 5, 0, 0),
+                                           NONE, NONE));
+    indent = getButtonLabelInset(sshUseExt);
+    clientPanel.add(sshArgsPanel,
+                    new GridBagConstraints(0, 1,
+                                           REMAINDER, 1,
+                                           LIGHT, LIGHT,
+                                           LINE_START, HORIZONTAL,
+                                           new Insets(4, indent, 0, 0),
+                                           NONE, NONE));
+
+    JPanel opensshPanel = new JPanel(new GridBagLayout());
+    opensshPanel.setBorder(BorderFactory.createTitledBorder("Embedded SSH client configuration"));
+    opensshPanel.add(sshConfigLabel,
+                     new GridBagConstraints(0, 0,
+                                            1, 1,
+                                            LIGHT, LIGHT,
+                                            LINE_START, NONE,
+                                            new Insets(0, 0, 5, 0),
+                                            NONE, NONE));
+    opensshPanel.add(sshConfig,
+                     new GridBagConstraints(1, 0,
+                                            1, 1,
+                                            HEAVY, LIGHT,
+                                            LINE_START, HORIZONTAL,
+                                            new Insets(0, 5, 5, 0),
+                                            NONE, NONE));
+    opensshPanel.add(sshConfigBrowser,
+                     new GridBagConstraints(2, 0,
+                                            1, 1,
+                                            LIGHT, LIGHT,
+                                            LINE_START, VERTICAL,
+                                            new Insets(0, 5, 5, 0),
+                                            NONE, NONE));
+    opensshPanel.add(sshKeyFileLabel,
+                     new GridBagConstraints(0, 1,
+                                            1, 1,
+                                            LIGHT, LIGHT,
+                                            LINE_START, NONE,
+                                            new Insets(0, 0, 0, 0),
+                                            NONE, NONE));
+    opensshPanel.add(sshKeyFile,
+                     new GridBagConstraints(1, 1,
+                                            1, 1,
+                                            HEAVY, LIGHT,
+                                            LINE_START, HORIZONTAL,
+                                            new Insets(0, 5, 0, 0),
+                                            NONE, NONE));
+    opensshPanel.add(sshKeyFileBrowser,
+                     new GridBagConstraints(2, 1,
+                                            1, 1,
+                                            LIGHT, LIGHT,
+                                            LINE_START, VERTICAL,
+                                            new Insets(0, 5, 0, 0),
+                                            NONE, NONE));
+    tunnelPanel.add(gatewayPanel,
+                    new GridBagConstraints(0, 0,
+                                           REMAINDER, 1,
+                                           HEAVY, LIGHT,
+                                           LINE_START, HORIZONTAL,
+                                           new Insets(0, 0, 4, 0),
+                                           NONE, NONE));
+    tunnelPanel.add(clientPanel,
+                    new GridBagConstraints(0, 1,
+                                           REMAINDER, 1,
+                                           HEAVY, LIGHT,
+                                           LINE_START, HORIZONTAL,
+                                           new Insets(0, 0, 4, 0),
+                                           NONE, NONE));
+    tunnelPanel.add(opensshPanel,
+                    new GridBagConstraints(0, 2,
+                                           REMAINDER, 1,
+                                           HEAVY, LIGHT,
+                                           LINE_START, HORIZONTAL,
+                                           new Insets(0, 0, 0, 0),
+                                           NONE, NONE));
+
+    sshPanel.add(sshTunnel,
+                 new GridBagConstraints(0, 0,
+                                        REMAINDER, 1,
+                                        LIGHT, LIGHT,
+                                        LINE_START, NONE,
+                                        new Insets(0, 0, 4, 0),
+                                        NONE, NONE));
+    indent = getButtonLabelInset(sshTunnel);
+    sshPanel.add(tunnelPanel,
+                 new GridBagConstraints(0, 2,
+                                        REMAINDER, 1,
+                                        LIGHT, LIGHT,
+                                        LINE_START, HORIZONTAL,
+                                        new Insets(0, indent, 4, 0),
+                                        NONE, NONE));
+    sshPanel.add(Box.createRigidArea(new Dimension(5, 0)),
+                 new GridBagConstraints(0, RELATIVE,
+                                        REMAINDER, REMAINDER,
+                                        HEAVY, HEAVY,
+                                        LINE_START, BOTH,
+                                        new Insets(0, 0, 0, 0),
+                                        NONE, NONE));
 
     // load/save tab
     JPanel loadSavePanel = new JPanel(new GridBagLayout());
@@ -659,6 +904,7 @@ class OptionsDialog extends Dialog {
     tabPane.addTab("Input", inputPanel);
     tabPane.addTab("Screen", ScreenPanel);
     tabPane.addTab("Misc", MiscPanel);
+    tabPane.addTab("SSH", sshPanel);
     tabPane.addTab("Load / Save", loadSavePanel);
     tabPane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
     // Resize the tabPane if necessary to prevent scrolling
@@ -701,8 +947,10 @@ class OptionsDialog extends Dialog {
     veryLowColour.setEnabled(!autoSelect.isSelected());
     compressLevel.setEnabled(customCompressLevel.isSelected());
     qualityLevel.setEnabled(noJpeg.isSelected());
-    sendLocalUsername.setEnabled(secVeNCrypt.isEnabled()&&
-      (secPlain.isSelected()||secIdent.isSelected()));
+    sendLocalUsername.setEnabled(secVeNCrypt.isEnabled() &&
+      (secPlain.isSelected() || secIdent.isSelected()));
+    sshArguments.setEnabled(sshTunnel.isSelected() &&
+      (sshUseExt.isSelected() && sshArgsCustom.isSelected()));
   }
 
   private void updatePreferences() {
@@ -782,6 +1030,19 @@ class OptionsDialog extends Dialog {
     if (!CSecurityTLS.x509crl.getValueStr().equals(""))
       UserPreferences.set("viewer", "x509crl",
               CSecurityTLS.x509crl.getValueStr());
+    UserPreferences.set("global", "Tunnel", sshTunnel.isSelected());
+    if (sshUseGateway.isSelected()) {
+      String via = sshUser.getText()+"@"+sshHost.getText()+":"+sshPort.getText();
+      UserPreferences.set("global", "Via", via);
+    }
+    if (sshUseExt.isSelected()) {
+      UserPreferences.set("global", "extSSH", sshUseExt.isSelected());
+      UserPreferences.set("global", "extSSHClient", sshClient.getText());
+      if (!sshArguments.getText().isEmpty())
+        UserPreferences.set("global", "extSSHArgs", sshArguments.getText());
+    }
+    UserPreferences.set("global", "SSHConfig", sshConfig.getText());
+    UserPreferences.set("global", "SSHKeyFile", sshKeyFile.getText());
   }
 
   private void restorePreferences() {
@@ -832,8 +1093,7 @@ class OptionsDialog extends Dialog {
     sendClipboard.setSelected(UserPreferences.getBool("global",
             "SendClipboard"));
     menuKey.setSelectedItem(UserPreferences.get("global", "MenuKey"));
-    desktopSize.setSelected(UserPreferences.get("global", "DesktopSize")
-            != null);
+    desktopSize.setSelected(!UserPreferences.get("global", "DesktopSize").isEmpty());
     if (desktopSize.isSelected()) {
       String desktopSizeString = UserPreferences.get("global", "DesktopSize");
       desktopWidth.setText(desktopSizeString.split("x")[0]);
@@ -874,6 +1134,70 @@ class OptionsDialog extends Dialog {
       secNone.setSelected(UserPreferences.getBool("viewer", "secNone", true));
     if (secVnc.isEnabled())
       secVnc.setSelected(UserPreferences.getBool("viewer", "secVnc", true));
+    sshTunnel.setSelected(UserPreferences.getBool("global", "Tunnel"));
+    sshUseGateway.setSelected(UserPreferences.get("global", "Via") != null);
+    if (sshUseGateway.isSelected())
+      cc.viewer.via.setParam(UserPreferences.get("global", "Via"));
+    sshUser.setText(Tunnel.getSshUser(cc));
+    sshHost.setText(Tunnel.getSshHost(cc));
+    sshPort.setText(Integer.toString(Tunnel.getSshPort(cc)));
+    sshUseExt.setSelected(UserPreferences.getBool("global", "extSSH"));
+    File f = new File(UserPreferences.get("global", "extSSHClient"));
+    if (f.exists() && f.canExecute())
+      sshClient.setText(f.getAbsolutePath());
+    sshArguments.setText(UserPreferences.get("global", "extSSHArgs"));
+    if (sshArguments.getText().isEmpty())
+      sshArgsDefault.setSelected(true);
+    else
+      sshArgsCustom.setSelected(true);
+    f = new File(UserPreferences.get("global", "SSHConfig"));
+    if (f.exists() && f.canRead())
+      sshConfig.setText(f.getAbsolutePath());
+    if (UserPreferences.get("global", "SSHKeyFile") != null) {
+      f = new File(UserPreferences.get("global", "SSHKeyFile"));
+      if (f.exists() && f.canRead())
+        sshKeyFile.setText(f.getAbsolutePath());
+    } else {
+      sshKeyFile.setText(Tunnel.getSshKeyFile(cc));
+    }
+    sshUseGateway.setEnabled(sshTunnel.isSelected());
+    sshUser.setEnabled(sshTunnel.isSelected() &&
+                       sshUseGateway.isEnabled() &&
+                       sshUseGateway.isSelected());
+    sshHost.setEnabled(sshTunnel.isSelected() &&
+                       sshUseGateway.isEnabled() &&
+                       sshUseGateway.isSelected());
+    sshPort.setEnabled(sshTunnel.isSelected() &&
+                       sshUseGateway.isEnabled() &&
+                       sshUseGateway.isSelected());
+    sshUseExt.setEnabled(sshTunnel.isSelected());
+    sshClient.setEnabled(sshTunnel.isSelected() &&
+                         sshUseExt.isEnabled());
+    sshClientBrowser.setEnabled(sshTunnel.isSelected() &&
+                                sshUseExt.isEnabled() &&
+                                sshUseExt.isSelected());
+    sshArgsDefault.setEnabled(sshTunnel.isSelected() &&
+                              sshUseExt.isEnabled() &&
+                              sshUseExt.isSelected());
+    sshArgsCustom.setEnabled(sshTunnel.isSelected() &&
+                             sshUseExt.isEnabled() &&
+                             sshUseExt.isSelected());
+    sshArguments.setEnabled(sshTunnel.isSelected() &&
+                            sshUseExt.isEnabled() &&
+                            sshUseExt.isSelected() &&
+                            sshArgsCustom.isSelected());
+    sshConfig.setEnabled(sshTunnel.isSelected() &&
+                         sshUseExt.isEnabled() &&
+                         !sshUseExt.isSelected());
+    sshConfigBrowser.setEnabled(sshTunnel.isSelected() &&
+                                sshUseExt.isEnabled() &&
+                                !sshUseExt.isSelected());
+    sshKeyFile.setEnabled(sshTunnel.isSelected() &&
+                          sshUseExt.isEnabled() &&
+                          !sshUseExt.isSelected());
+    sshKeyFileBrowser.setEnabled(sshTunnel.isSelected() &&
+                                 sshUseExt.isEnabled() &&
+                                 !sshUseExt.isSelected());
   }
 
   public void endDialog() {
@@ -890,15 +1214,15 @@ class OptionsDialog extends Dialog {
       JButton button = (JButton)s;
       if (button == okButton) {
         JTextField[] fields =
-          { x509ca, x509crl };
+          { x509ca, x509crl, sshClient, sshConfig, sshKeyFile };
         for (JTextField field : fields) {
           if (field.getText() != null && !field.getText().equals("")) {
             File f = new File(field.getText());
             if (!f.exists() || !f.canRead()) {
               String msg = new String("The file "+f.getAbsolutePath()+
                            " specified for option "+field.getName()+
-                           " does not exist or cannot be read.  Please "+
-                           "correct before proceeding.");
+                           " does not exist or cannot be read.  Please"+
+                           " correct before proceeding.");
               JOptionPane.showMessageDialog(this, msg, "WARNING",
                                             JOptionPane.WARNING_MESSAGE);
               return;
@@ -958,6 +1282,35 @@ class OptionsDialog extends Dialog {
         int ret = fc.showOpenDialog(this);
         if (ret == JFileChooser.APPROVE_OPTION)
           x509crl.setText(fc.getSelectedFile().toString());
+      } else if (button == sshClientBrowser) {
+        JFileChooser fc = new JFileChooser();
+        fc.setDialogTitle("Path to external SSH client");
+        fc.setApproveButtonText("OK");
+        fc.setFileHidingEnabled(false);
+        int ret = fc.showOpenDialog(this);
+        if (ret == JFileChooser.APPROVE_OPTION)
+          sshClient.setText(fc.getSelectedFile().toString());
+      } else if (button == sshConfigBrowser) {
+        JFileChooser fc = new JFileChooser();
+        fc.setDialogTitle("Path to OpenSSH client config file");
+        fc.setApproveButtonText("OK");
+        fc.setFileHidingEnabled(false);
+        int ret = fc.showOpenDialog(this);
+        if (ret == JFileChooser.APPROVE_OPTION)
+          sshConfig.setText(fc.getSelectedFile().toString());
+      } else if (button == sshKeyFileBrowser) {
+        JFileChooser fc = new JFileChooser();
+        fc.setDialogTitle("Path to SSH key file");
+        fc.setApproveButtonText("OK");
+        fc.setFileHidingEnabled(false);
+        int ret = fc.showOpenDialog(this);
+        if (ret == JFileChooser.APPROVE_OPTION)
+          sshKeyFile.setText(fc.getSelectedFile().toString());
+      }
+    } else if (s instanceof JRadioButton) {
+      JRadioButton button = (JRadioButton)s;
+      if (button == sshArgsCustom || button == sshArgsDefault) {
+        sshArguments.setEnabled(sshArgsCustom.isSelected());
       }
     }
   }
@@ -983,16 +1336,16 @@ class OptionsDialog extends Dialog {
         qualityLevel.setEnabled(enable);
       } else if (item == encX509) {
         x509ca.setEnabled(enable);
-        x509crl.setEnabled(enable);
         caButton.setEnabled(enable);
+        x509crl.setEnabled(enable);
         crlButton.setEnabled(enable);
       } else if (item == secVeNCrypt) {
         encNone.setEnabled(enable);
         encTLS.setEnabled(enable);
         encX509.setEnabled(enable);
         x509ca.setEnabled(enable && encX509.isSelected());
-        x509crl.setEnabled(enable && encX509.isSelected());
         caButton.setEnabled(enable && encX509.isSelected());
+        x509crl.setEnabled(enable && encX509.isSelected());
         crlButton.setEnabled(enable && encX509.isSelected());
         secIdent.setEnabled(enable);
         secPlain.setEnabled(enable);
@@ -1005,6 +1358,60 @@ class OptionsDialog extends Dialog {
       } else if (item == secIdent || item == secPlain) {
         sendLocalUsername.setEnabled(secIdent.isSelected() ||
                                      secPlain.isSelected());
+      } else if (item == sshTunnel) {
+        sshUseGateway.setEnabled(enable);
+        sshUser.setEnabled(enable &&
+                           sshUseGateway.isEnabled() &&
+                           sshUseGateway.isSelected());
+        sshHost.setEnabled(enable &&
+                           sshUseGateway.isEnabled() &&
+                           sshUseGateway.isSelected());
+        sshPort.setEnabled(enable &&
+                           sshUseGateway.isEnabled() &&
+                           sshUseGateway.isSelected());
+        sshUseExt.setEnabled(enable);
+        sshClient.setEnabled(enable &&
+                             sshUseExt.isEnabled() &&
+                             sshUseExt.isSelected());
+        sshClientBrowser.setEnabled(enable &&
+                                    sshUseExt.isEnabled() &&
+                                    sshUseExt.isSelected());
+        sshArgsDefault.setEnabled(enable &&
+                                  sshUseExt.isEnabled() &&
+                                  sshUseExt.isSelected());
+        sshArgsCustom.setEnabled(enable &&
+                                 sshUseExt.isEnabled() &&
+                                 sshUseExt.isSelected());
+        sshArguments.setEnabled(enable &&
+                                sshUseExt.isEnabled() &&
+                                sshUseExt.isSelected() &&
+                                sshArgsCustom.isSelected());
+        sshConfig.setEnabled(enable &&
+                             sshUseExt.isEnabled() &&
+                             !sshUseExt.isSelected());
+        sshConfigBrowser.setEnabled(enable &&
+                                    sshUseExt.isEnabled() &&
+                                    !sshUseExt.isSelected());
+        sshKeyFile.setEnabled(enable &&
+                              sshUseExt.isEnabled() &&
+                              !sshUseExt.isSelected());
+        sshKeyFileBrowser.setEnabled(enable &&
+                                     sshUseExt.isEnabled() &&
+                                     !sshUseExt.isSelected());
+      } else if (item == sshUseExt) {
+        sshClient.setEnabled(enable);
+        sshClientBrowser.setEnabled(enable);
+        sshArgsDefault.setEnabled(enable);
+        sshArgsCustom.setEnabled(enable);
+        sshArguments.setEnabled(enable && sshArgsCustom.isSelected());
+        sshConfig.setEnabled(!enable);
+        sshConfigBrowser.setEnabled(!enable);
+        sshKeyFile.setEnabled(!enable);
+        sshKeyFileBrowser.setEnabled(!enable);
+      } else if (item == sshUseGateway) {
+        sshUser.setEnabled(enable);
+        sshHost.setEnabled(enable);
+        sshPort.setEnabled(enable);
       }
     }
   }
index 26a138d6f6e6cca556b4058a33694b7669f07f06..fbaf99118b52f4215361d579834b0d13191f1cd8 100644 (file)
@@ -120,6 +120,8 @@ class PasswdDialog extends Dialog implements UserInfo,
     if (userEntry.isEnabled())
       if (userEntry.getText().equals(""))
         return false;
+      else if (!passwdEntry.isEnabled())
+        return true;
     if (passwdEntry.isEnabled())
       if (!passwdEntry.getText().equals(""))
         return true;
diff --git a/java/com/tigervnc/vncviewer/Tunnel.java b/java/com/tigervnc/vncviewer/Tunnel.java
new file mode 100644 (file)
index 0000000..90ab38e
--- /dev/null
@@ -0,0 +1,355 @@
+/*
+ *  Copyright (C) 2012-2016 All Rights Reserved.
+ *  Copyright (C) 2000 Const Kaplinsky.  All Rights Reserved.
+ *  Copyright (C) 1999 AT&T Laboratories Cambridge.  All Rights Reserved.
+ *
+ *  This is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This software is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this software; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+ *  USA.
+ */
+
+/*
+ * tunnel.java - SSH tunneling support
+ */
+
+package com.tigervnc.vncviewer;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.*;
+
+import com.tigervnc.rdr.*;
+import com.tigervnc.rfb.*;
+import com.tigervnc.rfb.Exception;
+import com.tigervnc.network.*;
+
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.ConfigRepository;
+import com.jcraft.jsch.Logger;
+import com.jcraft.jsch.OpenSSHConfig;
+import com.jcraft.jsch.Session;
+
+public class Tunnel {
+
+  private final static String DEFAULT_TUNNEL_TEMPLATE
+    = "-f -L %L:localhost:%R %H sleep 20";
+  private final static String DEFAULT_VIA_TEMPLATE
+    = "-f -L %L:%H:%R %G sleep 20";
+
+  public static void createTunnel(CConn cc, int localPort) throws Exception {
+    int remotePort;
+    String gatewayHost;
+    String remoteHost;
+
+    remotePort = cc.getServerPort();
+    if (cc.viewer.tunnel.getValue()) {
+      gatewayHost = cc.getServerName();
+      remoteHost = "localhost";
+    } else {
+      gatewayHost = getSshHost(cc);
+      remoteHost = cc.getServerName();
+    }
+
+    String pattern = cc.viewer.extSSHArgs.getValue();
+    if (pattern == null) {
+      if (cc.viewer.tunnel.getValue())
+        pattern = System.getProperty("VNC_TUNNEL_CMD");
+      else
+        pattern = System.getProperty("VNC_VIA_CMD");
+    }
+
+    if (cc.viewer.extSSH.getValue() || 
+        (pattern != null && pattern.length() > 0)) {
+      createTunnelExt(gatewayHost, remoteHost, remotePort, localPort, pattern, cc);
+    } else {
+      createTunnelJSch(gatewayHost, remoteHost, remotePort, localPort, cc);
+    }
+  }
+
+  private static class MyJSchLogger implements Logger {
+    public boolean isEnabled(int level){
+      return true;
+    }
+
+    public void log(int level, String msg){
+      switch (level) {
+      case Logger.INFO:
+        vlog.info(msg);
+        break;
+      case Logger.ERROR:
+        vlog.error(msg);
+        break;
+      default:
+        vlog.debug(msg);
+      }
+    }
+  }
+
+  public static String getSshHost(CConn cc) {
+    String sshHost = cc.viewer.via.getValue();
+    if (sshHost == null)
+      return cc.getServerName();
+    int end = sshHost.indexOf(":");
+    if (end < 0)
+      end = sshHost.length();
+    sshHost = sshHost.substring(sshHost.indexOf("@")+1, end);
+    return sshHost;
+  }
+
+  public static String getSshUser(CConn cc) {
+    String sshUser = (String)System.getProperties().get("user.name");
+    String via = cc.viewer.via.getValue();
+    if (via != null && via.indexOf("@") > 0)
+      sshUser = via.substring(0, via.indexOf("@"));
+    return sshUser;
+  }
+
+  public static int getSshPort(CConn cc) {
+    String sshPort = "22";
+    String via = cc.viewer.via.getValue();
+    if (via != null && via.indexOf(":") > 0)
+      sshPort = via.substring(via.indexOf(":")+1, via.length());
+    return Integer.parseInt(sshPort);
+  }
+
+  public static String getSshKeyFile(CConn cc) {
+    if (cc.viewer.sshKeyFile.getValue() != null)
+      return cc.viewer.sshKeyFile.getValue();
+    String[] ids = { "id_dsa", "id_rsa" };
+    for (String id : ids) {
+      File f = new File(FileUtils.getHomeDir()+".ssh/"+id);
+      if (f.exists() && f.canRead())
+        return(f.getAbsolutePath());
+    }
+    return null;
+  }
+
+  private static void createTunnelJSch(String gatewayHost, String remoteHost,
+                                       int remotePort, int localPort,
+                                       CConn cc) throws Exception {
+    JSch.setLogger(new MyJSchLogger());
+    JSch jsch=new JSch();
+
+    try {
+      // NOTE: jsch does not support all ciphers.  User may be
+      //       prompted to accept host key authenticy even if
+      //       the key is in the known_hosts file.
+      File knownHosts = new File(FileUtils.getHomeDir()+".ssh/known_hosts");
+      if (knownHosts.exists() && knownHosts.canRead())
+           jsch.setKnownHosts(knownHosts.getAbsolutePath());
+      ArrayList<File> privateKeys = new ArrayList<File>();
+      String sshKeyFile = cc.options.sshKeyFile.getText();
+      String sshKey = cc.viewer.sshKey.getValue();
+      if (sshKey != null) {
+        String sshKeyPass = cc.viewer.sshKeyPass.getValue();
+        byte[] keyPass = null, key;
+        if (sshKeyPass != null)
+          keyPass = sshKeyPass.getBytes();
+        sshKey = sshKey.replaceAll("\\\\n", "\n");
+        key = sshKey.getBytes();
+        jsch.addIdentity("TigerVNC", key, null, keyPass);
+      } else if (!sshKeyFile.equals("")) {
+        File f = new File(sshKeyFile);
+        if (!f.exists() || !f.canRead())
+          throw new Exception("Cannot access SSH key file "+ sshKeyFile);
+        privateKeys.add(f);
+      }
+      for (Iterator<File> i = privateKeys.iterator(); i.hasNext();) {
+        File privateKey = (File)i.next();
+        if (privateKey.exists() && privateKey.canRead())
+          if (cc.viewer.sshKeyPass.getValue() != null)
+               jsch.addIdentity(privateKey.getAbsolutePath(),
+                             cc.viewer.sshKeyPass.getValue());
+          else
+               jsch.addIdentity(privateKey.getAbsolutePath());
+      }
+  
+      String user = getSshUser(cc);
+      String label = new String("SSH Authentication");
+      PasswdDialog dlg =
+        new PasswdDialog(label, (user == null ? false : true), false);
+      dlg.userEntry.setText(user != null ? user : "");
+      File ssh_config = new File(cc.viewer.sshConfig.getValue());
+      if (ssh_config.exists() && ssh_config.canRead()) {
+        ConfigRepository repo =
+          OpenSSHConfig.parse(ssh_config.getAbsolutePath());
+        jsch.setConfigRepository(repo);
+      }
+      Session session=jsch.getSession(user, gatewayHost, getSshPort(cc));
+      session.setUserInfo(dlg);
+      // OpenSSHConfig doesn't recognize StrictHostKeyChecking
+      if (session.getConfig("StrictHostKeyChecking") == null)
+        session.setConfig("StrictHostKeyChecking", "ask");
+      session.connect();
+      session.setPortForwardingL(localPort, remoteHost, remotePort);
+    } catch (java.lang.Exception e) {
+      throw new Exception(e.getMessage()); 
+    }
+  }
+
+  private static class MyExtProcess implements Runnable {
+
+    private String cmd = null;
+    private Process pid = null;
+
+    private static class MyProcessLogger extends Thread {
+      private final BufferedReader err;
+  
+      public MyProcessLogger(Process p) {
+        InputStreamReader reader = 
+          new InputStreamReader(p.getErrorStream());
+        err = new BufferedReader(reader);
+      }
+  
+      @Override
+      public void run() {
+        try {
+          while (true) {
+            String msg = err.readLine();
+            if (msg != null)
+              vlog.info(msg);
+          }
+        } catch(java.io.IOException e) {
+          vlog.info(e.getMessage());
+        } finally {
+          try {
+            if (err != null)
+              err.close();
+          } catch (java.io.IOException e ) { }
+        }
+      }
+    }
+
+    private static class MyShutdownHook extends Thread {
+
+      private Process proc = null;
+
+      public MyShutdownHook(Process p) {
+        proc = p;
+      }
+  
+      @Override
+      public void run() {
+        try {
+          proc.exitValue();
+        } catch (IllegalThreadStateException e) {
+          try {
+            // wait for CConn to shutdown the socket
+            Thread.sleep(500);
+          } catch(InterruptedException ie) { }
+          proc.destroy();
+        }
+      }
+    }
+
+    public MyExtProcess(String command) {
+      cmd = command;  
+    }
+
+    public void run() {
+      try {
+        Runtime runtime = Runtime.getRuntime();
+        pid = runtime.exec(cmd);
+        runtime.addShutdownHook(new MyShutdownHook(pid));
+        new MyProcessLogger(pid).start();
+        pid.waitFor();
+      } catch(InterruptedException e) {
+        vlog.info(e.getMessage());
+      } catch(java.io.IOException e) {
+        vlog.info(e.getMessage());
+      }
+    }
+  }
+
+  private static void createTunnelExt(String gatewayHost, String remoteHost,
+                                      int remotePort, int localPort,
+                                      String pattern, CConn cc) throws Exception {
+    if (pattern == null || pattern.length() < 1) {
+      if (cc.viewer.tunnel.getValue())
+        pattern = DEFAULT_TUNNEL_TEMPLATE;
+      else
+        pattern = DEFAULT_VIA_TEMPLATE;
+    }
+    String cmd = fillCmdPattern(pattern, gatewayHost, remoteHost,
+                                remotePort, localPort, cc);
+    try {
+      Thread t = new Thread(new MyExtProcess(cmd));
+      t.start();
+      // wait for the ssh process to start
+      Thread.sleep(1000);
+    } catch (java.lang.Exception e) {
+      throw new Exception(e.getMessage());
+    }
+  }
+
+  private static String fillCmdPattern(String pattern, String gatewayHost,
+                                       String remoteHost, int remotePort,
+                                       int localPort, CConn cc) {
+    boolean H_found = false, G_found = false, R_found = false, L_found = false;
+    boolean P_found = false;
+    String cmd = cc.options.sshClient.getText() + " ";
+    pattern.replaceAll("^\\s+", "");
+
+    String user = getSshUser(cc);
+    int sshPort = getSshPort(cc);
+    gatewayHost = user + "@" + gatewayHost;
+
+    for (int i = 0; i < pattern.length(); i++) {
+      if (pattern.charAt(i) == '%') {
+        switch (pattern.charAt(++i)) {
+        case 'H':
+          cmd += (cc.viewer.tunnel.getValue() ? gatewayHost : remoteHost);
+             H_found = true;
+          continue;
+        case 'G':
+          cmd += gatewayHost;
+             G_found = true;
+             continue;
+        case 'R':
+          cmd += remotePort;
+             R_found = true;
+             continue;
+        case 'L':
+          cmd += localPort;
+             L_found = true;
+             continue;
+        case 'P':
+          cmd += sshPort;
+             P_found = true;
+             continue;
+        }
+      }
+      cmd += pattern.charAt(i);
+    }
+
+    if (pattern.length() > 1024)
+      throw new Exception("Tunneling command is too long.");
+
+    if (!H_found || !R_found || !L_found)
+      throw new Exception("%H, %R or %L absent in tunneling command template.");
+
+    if (!cc.viewer.tunnel.getValue() && !G_found)
+      throw new Exception("%G pattern absent in tunneling command template.");
+
+    vlog.info("SSH command line: "+cmd);
+    if (VncViewer.os.startsWith("windows"))
+      cmd.replaceAll("\\\\", "\\\\\\\\");
+    return cmd;
+  }
+
+  static LogWriter vlog = new LogWriter("Tunnel");
+}
index 31799714d5f1f1d79deecf8068147fd6d3b0d76a..fc9c7b590b4ff0ceb8c1f6db221820f878f59d24 100644 (file)
@@ -168,13 +168,6 @@ public class VncViewer extends javax.swing.JApplet
         continue;
       }
 
-      if (argv[i].equalsIgnoreCase("-tunnel") || argv[i].equalsIgnoreCase("-via")) {
-        if (!tunnel.createTunnel(argv.length, argv, i))
-          exit(1);
-        if (argv[i].equalsIgnoreCase("-via")) i++;
-        continue;
-      }
-
       if (Configuration.setParam(argv[i]))
         continue;
 
@@ -614,30 +607,71 @@ public class VncViewer extends javax.swing.JApplet
                       "Produce a system beep when requested to by the server.",
                       true);
   StringParameter via
-  = new StringParameter("via",
+  = new StringParameter("Via",
     "Automatically create an encrypted TCP tunnel to "+
-    "machine gateway, then use that tunnel to connect "+
-    "to a VNC server running on host. By default, "+
-    "this option invokes SSH local port forwarding and "+
-    "assumes that the SSH client binary is located at "+
-    "/usr/bin/ssh. Note that when using the -via "+
-    "option, the host machine name should be specified "+
-    "from the point of view of the gateway machine. "+
-    "For example, \"localhost\" denotes the gateway, "+
-    "not the machine on which vncviewer was launched. "+
+    "the gateway machine, then connect to the VNC host "+
+    "through that tunnel. By default, this option invokes "+
+    "SSH local port forwarding using the embedded JSch "+
+    "client, however an external SSH client may be specified "+
+    "using the \"-extSSH\" parameter. Note that when using "+
+    "the -via option, the VNC host machine name should be "+
+    "specified from the point of view of the gateway machine, "+
+    "e.g. \"localhost\" denotes the gateway, "+
+    "not the machine on which the viewer was launched. "+
     "See the System Properties section below for "+
-    "information on configuring the -via option.",
+    "information on configuring the -Via option.", null);
+  BoolParameter tunnel
+  = new BoolParameter("Tunnel",
+    "The -Tunnel command is basically a shorthand for the "+
+    "-via command when the VNC server and SSH gateway are "+
+    "one and the same. -Tunnel creates an SSH connection "+
+    "to the server and forwards the VNC through the tunnel "+
+    "without the need to specify anything else.", false);
+  BoolParameter extSSH
+  = new BoolParameter("extSSH",
+    "By default, SSH tunneling uses the embedded JSch client "+
+    "for tunnel creation. This option causes the client to "+
+    "invoke an external SSH client application for all tunneling "+
+    "operations. By default, \"/usr/bin/ssh\" is used, however "+
+    "the path to the external application may be specified using "+
+    "the -SSHClient option.", false);
+  StringParameter extSSHClient
+  = new StringParameter("extSSHClient",
+    "Specifies the path to an external SSH client application "+
+    "that is to be used for tunneling operations when the -extSSH "+
+    "option is in effect.", "/usr/bin/ssh");
+  StringParameter extSSHArgs
+  = new StringParameter("extSSHArgs",
+    "Specifies the arguments string or command template to be used "+
+    "by the external SSH client application when the -extSSH option "+
+    "is in effect. The string will be processed according to the same "+
+    "pattern substitution rules as the VNC_TUNNEL_CMD and VNC_VIA_CMD "+
+    "system properties, and can be used to override those in a more "+
+    "command-line friendly way. If not specified, then the appropriate "+
+    "VNC_TUNNEL_CMD or VNC_VIA_CMD command template will be used.", null);
+  StringParameter sshConfig
+  = new StringParameter("SSHConfig",
+    "Specifies the path to an OpenSSH configuration file that to "+
+    "be parsed by the embedded JSch SSH client during tunneling "+
+    "operations.", FileUtils.getHomeDir()+".ssh/config");
+  StringParameter sshKey
+  = new StringParameter("SSHKey",
+    "When using the Via or Tunnel options with the embedded SSH client, "+
+    "this parameter specifies the text of the SSH private key to use when "+
+    "authenticating with the SSH server. You can use \\n within the string "+
+    "to specify a new line.", null);
+  StringParameter sshKeyFile
+  = new StringParameter("SSHKeyFile",
+    "When using the Via or Tunnel options with the embedded SSH client, "+
+    "this parameter specifies a file that contains an SSH private key "+
+    "(or keys) to use when authenticating with the SSH server. If not "+
+    "specified, ~/.ssh/id_dsa or ~/.ssh/id_rsa will be used (if they exist). "+
+    "Otherwise, the client will fallback to prompting for an SSH password.",
     null);
-
-  StringParameter tunnelMode
-  = new StringParameter("tunnel",
-    "Automatically create an encrypted TCP tunnel to "+
-    "remote gateway, then use that tunnel to connect "+
-    "to the specified VNC server port on the remote "+
-    "host. See the System Properties section below "+
-    "for information on configuring the -tunnel option.",
-    null);
-
+  StringParameter sshKeyPass
+  = new StringParameter("SSHKeyPass",
+    "When using the Via or Tunnel options with the embedded SSH client, "+
+    "this parameter specifies the passphrase for the SSH key.", null);
   BoolParameter customCompressLevel
   = new BoolParameter("CustomCompressLevel",
                       "Use custom compression level. "+
diff --git a/java/com/tigervnc/vncviewer/tunnel.java b/java/com/tigervnc/vncviewer/tunnel.java
deleted file mode 100644 (file)
index 6d55fec..0000000
+++ /dev/null
@@ -1,327 +0,0 @@
-/*
- *  Copyright (C) 2012 Brian P. Hinz.  All Rights Reserved.
- *  Copyright (C) 2000 Const Kaplinsky.  All Rights Reserved.
- *  Copyright (C) 1999 AT&T Laboratories Cambridge.  All Rights Reserved.
- *
- *  This is free software; you can redistribute it and/or modify
- *  it under the terms of the GNU General Public License as published by
- *  the Free Software Foundation; either version 2 of the License, or
- *  (at your option) any later version.
- *
- *  This software is distributed in the hope that it will be useful,
- *  but WITHOUT ANY WARRANTY; without even the implied warranty of
- *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *  GNU General Public License for more details.
- *
- *  You should have received a copy of the GNU General Public License
- *  along with this software; if not, write to the Free Software
- *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
- *  USA.
- */
-
-/*
- * tunnel.java - SSH tunneling support
- */
-
-package com.tigervnc.vncviewer;
-
-import java.io.File;
-import java.lang.Character;
-import java.util.ArrayList;
-import java.util.Iterator;
-
-import com.tigervnc.rdr.*;
-import com.tigervnc.rfb.*;
-import com.tigervnc.network.*;
-
-import com.jcraft.jsch.JSch;
-import com.jcraft.jsch.Session;
-
-public class tunnel
-{
-  private final static Integer SERVER_PORT_OFFSET = 5900;;
-  private final static String DEFAULT_SSH_CMD = "/usr/bin/ssh";
-  private final static String DEFAULT_TUNNEL_CMD
-    = DEFAULT_SSH_CMD+" -f -L %L:localhost:%R %H sleep 20";
-  private final static String DEFAULT_VIA_CMD
-    = DEFAULT_SSH_CMD+" -f -L %L:%H:%R %G sleep 20";
-
-  private final static int H = 17;
-  private final static int G = 16;
-  private final static int R = 27;
-  private final static int L = 21;
-
-  /* True if there was -tunnel or -via option in the command line. */
-  private static boolean tunnelSpecified = false;
-
-  /* True if it was -tunnel, not -via option. */
-  private static boolean tunnelOption = false;
-
-  /* "Hostname:display" pair in the command line will be substituted
-     by this fake argument when tunneling is used. */
-  private static String lastArgv;
-
-  private static String tunnelEndpoint;
-
-  public static Boolean
-  createTunnel(int pargc, String[] argv, int tunnelArgIndex)
-  {
-    char[] pattern;
-    char[] cmd = new char[1024];
-    int[] localPort = new int[1];
-    int[] remotePort = new int[1];
-    char[] localPortStr = new char[8];
-    char[] remotePortStr = new char[8];
-    StringBuilder gatewayHost = new StringBuilder("");
-    StringBuilder remoteHost = new StringBuilder("localhost");
-
-    tunnelSpecified = true;
-    if (argv[tunnelArgIndex].equalsIgnoreCase("-tunnel"))
-      tunnelOption = true;
-
-    pattern = getCmdPattern();
-    if (pattern == null)
-      return false;
-
-    localPort[0] = TcpSocket.findFreeTcpPort();
-    if (localPort[0] == 0)
-      return false;
-
-    if (tunnelOption) {
-      processTunnelArgs(remoteHost, remotePort, localPort,
-                     pargc, argv, tunnelArgIndex);
-    } else {
-      processViaArgs(gatewayHost, remoteHost, remotePort, localPort,
-                  pargc, argv, tunnelArgIndex);
-    }
-
-    localPortStr = Integer.toString(localPort[0]).toCharArray();
-    remotePortStr = Integer.toString(remotePort[0]).toCharArray();
-
-    if (!fillCmdPattern(cmd, pattern, gatewayHost.toString().toCharArray(),
-               remoteHost.toString().toCharArray(), remotePortStr, localPortStr))
-      return false;
-
-    if (!runCommand(new String(cmd)))
-      return false;
-
-    return true;
-  }
-
-  private static void
-  processTunnelArgs(StringBuilder remoteHost, int[] remotePort,
-                    int[] localPort, int pargc, String[] argv,
-                    int tunnelArgIndex)
-  {
-    String pdisplay;
-
-    if (tunnelArgIndex >= pargc - 1)
-      VncViewer.usage();
-
-    pdisplay = argv[pargc - 1].split(":")[1];
-    if (pdisplay == null || pdisplay == argv[pargc - 1])
-      VncViewer.usage();
-
-    if (pdisplay.matches("/[^0-9]/"))
-      VncViewer.usage();
-
-    remotePort[0] = Integer.parseInt(pdisplay);
-    if (remotePort[0] < 100)
-      remotePort[0] = remotePort[0] + SERVER_PORT_OFFSET;
-
-    lastArgv = new String("localhost::"+localPort[0]);
-
-    remoteHost.setLength(0);
-    remoteHost.insert(0, argv[pargc - 1].split(":")[0]);
-    argv[pargc - 1] = lastArgv;
-
-    //removeArgs(pargc, argv, tunnelArgIndex, 1);
-  }
-
-  private static void
-  processViaArgs(StringBuilder gatewayHost, StringBuilder remoteHost,
-              int[] remotePort, int[] localPort,
-              int pargc, String[] argv, int tunnelArgIndex)
-  {
-    String colonPos;
-    int len, portOffset;
-    int disp;
-
-    if (tunnelArgIndex >= pargc - 2)
-      VncViewer.usage();
-
-    colonPos = argv[pargc - 1].split(":", 2)[1];
-    if (colonPos == null) {
-      /* No colon -- use default port number */
-      remotePort[0] = SERVER_PORT_OFFSET;
-    } else {
-      len = colonPos.length();
-      portOffset = SERVER_PORT_OFFSET;
-      if (colonPos.startsWith(":")) {
-        /* Two colons -- interpret as a port number */
-        colonPos.replaceFirst(":", "");
-        len--;
-        portOffset = 0;
-      }
-      if (len == 0 || colonPos.matches("/[^0-9]/")) {
-        VncViewer.usage();
-      }
-      disp = Integer.parseInt(colonPos);
-      if (portOffset != 0 && disp >= 100)
-        portOffset = 0;
-      remotePort[0] = disp + portOffset;
-    }
-
-    lastArgv = "localhost::"+localPort[0];
-
-    gatewayHost.setLength(0);
-    gatewayHost.insert(0, argv[tunnelArgIndex + 1]);
-
-    if (!argv[pargc - 1].split(":", 2)[0].equals("")) {
-      remoteHost.setLength(0);
-      remoteHost.insert(0, argv[pargc - 1].split(":", 2)[0]);
-    }
-
-    argv[pargc - 1] = lastArgv;
-
-    //removeArgs(pargc, argv, tunnelArgIndex, 2);
-  }
-
-  private static char[]
-  getCmdPattern()
-  {
-    String pattern = "";
-
-    try {
-      if (tunnelOption) {
-        pattern = System.getProperty("VNC_TUNNEL_CMD");
-      } else {
-        pattern = System.getProperty("VNC_VIA_CMD");
-      }
-    } catch (java.lang.Exception e) {
-      vlog.info(e.toString());
-    }
-    if (pattern == null || pattern.equals(""))
-      pattern = (tunnelOption) ? DEFAULT_TUNNEL_CMD : DEFAULT_VIA_CMD;
-
-    return pattern.toCharArray();
-  }
-
-  /* Note: in fillCmdPattern() result points to a 1024-byte buffer */
-
-  private static boolean
-  fillCmdPattern(char[] result, char[] pattern,
-              char[] gatewayHost, char[] remoteHost,
-              char[] remotePort, char[] localPort)
-  {
-    int i, j;
-    boolean H_found = false, G_found = false, R_found = false, L_found = false;
-
-    for (i=0, j=0; i < pattern.length && j<1023; i++, j++) {
-      if (pattern[i] == '%') {
-        switch (pattern[++i]) {
-        case 'H':
-       System.arraycopy(remoteHost, 0, result, j, remoteHost.length);
-       j += remoteHost.length;
-       H_found = true;
-        tunnelEndpoint = new String(remoteHost);
-       continue;
-        case 'G':
-       System.arraycopy(gatewayHost, 0, result, j, gatewayHost.length);
-       j += gatewayHost.length;
-       G_found = true;
-        tunnelEndpoint = new String(gatewayHost);
-       continue;
-        case 'R':
-       System.arraycopy(remotePort, 0, result, j, remotePort.length);
-       j += remotePort.length;
-       R_found = true;
-       continue;
-        case 'L':
-       System.arraycopy(localPort, 0, result, j, localPort.length);
-       j += localPort.length;
-       L_found = true;
-       continue;
-        case '\0':
-       i--;
-       continue;
-        }
-      }
-      result[j] = pattern[i];
-    }
-
-    if (pattern.length > 1024) {
-      vlog.error("Tunneling command is too long.");
-      return false;
-    }
-
-    if (!H_found || !R_found || !L_found) {
-      vlog.error("%H, %R or %L absent in tunneling command.");
-      return false;
-    }
-    if (!tunnelOption && !G_found) {
-      vlog.error("%G pattern absent in tunneling command.");
-      return false;
-    }
-
-    return true;
-  }
-
-  private static Boolean
-  runCommand(String cmd)
-  {
-    try{
-      JSch jsch=new JSch();
-      String homeDir = new String("");
-      try {
-        homeDir = System.getProperty("user.home");
-      } catch(java.security.AccessControlException e) {
-        System.out.println("Cannot access user.home system property");
-      }
-      // NOTE: jsch does not support all ciphers.  User may be
-      //       prompted to accept host key authenticy even if
-      //       the key is in the known_hosts file.
-      File knownHosts = new File(homeDir+"/.ssh/known_hosts");
-      if (knownHosts.exists() && knownHosts.canRead())
-             jsch.setKnownHosts(knownHosts.getAbsolutePath());
-      ArrayList<File> privateKeys = new ArrayList<File>();
-      privateKeys.add(new File(homeDir+"/.ssh/id_rsa"));
-      privateKeys.add(new File(homeDir+"/.ssh/id_dsa"));
-      for (Iterator<File> i = privateKeys.iterator(); i.hasNext();) {
-        File privateKey = (File)i.next();
-        if (privateKey.exists() && privateKey.canRead())
-               jsch.addIdentity(privateKey.getAbsolutePath());
-      }
-      // username and passphrase will be given via UserInfo interface.
-      PasswdDialog dlg = new PasswdDialog(new String("SSH Authentication"), false, false);
-      dlg.promptPassword(new String("SSH Authentication"));
-
-      Session session=jsch.getSession(dlg.userEntry.getText(), tunnelEndpoint, 22);
-      session.setPassword(new String(dlg.passwdEntry.getPassword()));
-      session.connect();
-
-      String[] tokens = cmd.split("\\s");
-      for (int i = 0; i < tokens.length; i++) {
-        if (tokens[i].equals("-L")) {
-          String[] par = tokens[++i].split(":");
-          int localPort = Integer.parseInt(par[0].trim());
-          String remoteHost = par[1].trim();
-          int remotePort = Integer.parseInt(par[2].trim());
-          session.setPortForwardingL(localPort, remoteHost, remotePort);
-        } else if (tokens[i].equals("-R")) {
-          String[] par = tokens[++i].split(":");
-          int remotePort = Integer.parseInt(par[0].trim());
-          String localHost = par[1].trim();
-          int localPort = Integer.parseInt(par[2].trim());
-          session.setPortForwardingR(remotePort, localHost, localPort);
-        }
-      }
-    } catch (java.lang.Exception e) {
-      System.out.println(" Tunneling command failed: "+e.toString());
-      return false;
-    }
-    return true;
-  }
-
-  static LogWriter vlog = new LogWriter("tunnel");
-}