]> source.dussan.org Git - tigervnc.git/commitdiff
Re-implemented SSH tunneling features in Java viewer based on TurboVNC native client...
authorBrian Hinz <bphinz@users.sourceforge.net>
Sun, 6 May 2012 19:18:05 +0000 (19:18 +0000)
committerBrian Hinz <bphinz@users.sourceforge.net>
Sun, 6 May 2012 19:18:05 +0000 (19:18 +0000)
git-svn-id: svn://svn.code.sf.net/p/tigervnc/code/trunk@4910 3789f03b-4d11-0410-bbf8-ca57d06f2519

java/com/tigervnc/vncviewer/VncViewer.java
java/com/tigervnc/vncviewer/tunnel.java [new file with mode: 0644]

index 3c990401aa1f3b87ef14923e6dbc352edb735d85..85da24997291871bcd23a382155f69cf287a79c9 100644 (file)
@@ -50,9 +50,6 @@ import com.tigervnc.rdr.*;
 import com.tigervnc.rfb.*;
 import com.tigervnc.network.*;
 
-import com.jcraft.jsch.JSch;
-import com.jcraft.jsch.Session;
-
 public class VncViewer extends java.applet.Applet implements Runnable
 {
   public static final String about1 = "TigerVNC Viewer for Java";
@@ -127,6 +124,12 @@ public class VncViewer extends java.applet.Applet implements Runnable
         continue;
       }
 
+      if (argv[i].equalsIgnoreCase("-tunnel") || argv[i].equalsIgnoreCase("-via")) {
+        if (!tunnel.createTunnel(argv.length, argv, i))
+          System.exit(1);
+        continue;
+      }
+
       if (Configuration.setParam(argv[i]))
         continue;
 
@@ -140,15 +143,15 @@ public class VncViewer extends java.applet.Applet implements Runnable
         usage();
       }
 
-      if (vncServerName.getValue() != null)
-        usage();
-      vncServerName.setParam(argv[i]);
+      if (vncServerName.getValue() == null)
+        vncServerName.setParam(argv[i]);
     }
+
   }
 
   public static void usage() {
     String usage = ("\nusage: vncviewer [options/parameters] "+
-                    "[host:displayNum] [options/parameters]\n"+
+                    "[host:displayNum]\n"+
                     "       vncviewer [options/parameters] -listen [port] "+
                     "[options/parameters]\n"+
                     "\n"+
@@ -177,85 +180,52 @@ public class VncViewer extends java.applet.Applet implements Runnable
       System.err.format("%-14s%-64s%n"," ", "[default="+current.getDefaultStr()+"]");
       current = current.next;
     }
+    String propertiesString = ("\n"+
+"\u001B[1mSystem Properties\u001B[0m (adapted from the TurboVNC vncviewer man page)\n"+
+"\tWhen started with the -via option, vncviewer reads the\n"+
+"\t\u001B[1mcom.tigervnc.VNC_VIA_CMD\u001B[0m System property, expands\n"+
+"\tpatterns beginning with the \"%\" character, and uses the resulting\n"+
+"\tcommand line to establish the secure tunnel to the VNC gateway.\n"+
+"\tIf \u001B[1mcom.tigervnc.VNC_VIA_CMD\u001B[0m is not set, this \n"+
+"\tcommand line defaults to \"/usr/bin/ssh -f -L %L:%H:%R %G sleep 20\".\n"+
+"\n"+
+"\tThe following patterns are recognized in the VNC_VIA_CMD property\n"+
+"\t(note that all of the patterns %G, %H, %L and %R must be present in \n"+
+"\tthe command template):\n"+
+"\n"+
+"\t\t%%     A literal \"%\";\n"+
+"\n"+
+"\t\t%G     gateway machine name;\n"+
+"\n"+
+"\t\t%H     remote VNC machine name, (as known to the gateway);\n"+
+"\n"+
+"\t\t%L     local TCP port number;\n"+
+"\n"+
+"\t\t%R     remote TCP port number.\n"+
+"\n"+
+"\tWhen started with the -tunnel option, vncviewer reads the\n"+
+"\t\u001B[1mcom.tigervnc.VNC_TUNNEL_CMD\u001B[0m System property, expands\n"+
+"\tpatterns beginning with the \"%\" character, and uses the resulting\n"+
+"\tcommand line to establish the secure tunnel to the VNC server.\n"+
+"\tIf \u001B[1mcom.tigervnc.VNC_TUNNEL_CMD\u001B[0m is not set, this command \n"+
+"\tline defaults to \"/usr/bin/ssh -f -L %L:localhost:%R %H sleep 20\".\n"+
+"\n"+
+"\tThe following patterns are recognized in the VNC_TUNNEL_CMD property\n"+
+"\t(note that all of the patterns %H, %L and %R must be present in \n"+
+"\tthe command template):\n"+
+"\n"+
+"\t\t%%     A literal \"%\";\n"+
+"\n"+
+"\t\t%H     remote VNC machine name (as known to the client);\n"+
+"\n"+
+"\t\t%L     local TCP port number;\n"+
+"\n"+
+"\t\t%R     remote TCP port number.\n"+
+"\n");
+    System.err.print(propertiesString);
     System.exit(1);
   }
 
-  /* Tunnelling support. */
-  private void interpretViaParam(StringParameter gatewayHost,
-    StringParameter remoteHost, IntParameter remotePort, 
-    StringParameter vncServerName, IntParameter localPort)
-  {
-    final int SERVER_PORT_OFFSET = 5900;;
-    int pos = vncServerName.getValueStr().indexOf(":");
-    if (pos == -1)
-      remotePort.setParam(""+SERVER_PORT_OFFSET+"");
-    else {
-      int portOffset = SERVER_PORT_OFFSET;
-      int len;
-      pos++;
-      len =  vncServerName.getValueStr().substring(pos).length();
-      if (vncServerName.getValueStr().substring(pos, pos).equals(":")) {
-        /* Two colons is an absolute port number, not an offset. */
-        pos++;
-        len--;
-        portOffset = 0;
-      }
-      try {
-        if (len <= 0 || !vncServerName.getValueStr().substring(pos).matches("[0-9]+"))
-          usage();
-        portOffset += Integer.parseInt(vncServerName.getValueStr().substring(pos));
-        remotePort.setParam(""+portOffset+"");
-      } catch (java.lang.NumberFormatException e) {
-        usage();
-      }
-    }
-  
-    if (vncServerName != null)
-      remoteHost.setParam(vncServerName.getValueStr().split(":")[0]);
-  
-    gatewayHost.setParam(via.getValueStr());
-    vncServerName.setParam("localhost::"+localPort.getValue());
-  }
-
-  private void
-  createTunnel(String gatewayHost, String remoteHost,
-          int remotePort, int localPort)
-  {
-    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 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.userEntry.setText((String)System.getProperties().get("user.name"));
-      Session session=jsch.getSession(dlg.userEntry.getText(), gatewayHost, 22);
-      session.setUserInfo(dlg);
-      session.connect();
-
-      session.setPortForwardingL(localPort, remoteHost, remotePort);
-    } catch (java.lang.Exception e) {
-      System.out.println(e);
-    }
-  }
-
   public VncViewer() {
     applet = true;
     firstApplet = true;
@@ -333,21 +303,6 @@ public class VncViewer extends java.applet.Applet implements Runnable
   public void run() {
     CConn cc = null;
 
-    /* Tunnelling support. */
-    if (via.getValueStr() != null) {
-      StringParameter gatewayHost = new StringParameter("", "", "");
-      StringParameter remoteHost = new StringParameter("", "", "localhost");
-      IntParameter localPort = 
-        new IntParameter("", "", TcpSocket.findFreeTcpPort());
-      IntParameter remotePort = new IntParameter("", "", 5900);
-      if (vncServerName.getValueStr() == null)
-        usage();
-      interpretViaParam(gatewayHost, remoteHost, remotePort, 
-        vncServerName, localPort);
-      createTunnel(gatewayHost.getValueStr(), remoteHost.getValueStr(), 
-        remotePort.getValue(), localPort.getValue());
-    }
-
     if (listenMode.getValue()) {
       int port = 5500;
 
@@ -469,11 +424,11 @@ public class VncViewer extends java.applet.Applet implements Runnable
   = new StringParameter("ScalingFactor",
                         "Reduce or enlarge the remote desktop image. "+
                         "The value is interpreted as a scaling factor "+
-                        "in percent.  If the parameter is set to "+
+                        "in percent. If the parameter is set to "+
                         "\"Auto\", then automatic scaling is "+
-                        "performed.  Auto-scaling tries to choose a "+
+                        "performed. Auto-scaling tries to choose a "+
                         "scaling factor in such a way that the whole "+
-                        "remote desktop will fit on the local screen.  "+
+                        "remote desktop will fit on the local screen. "+
                         "If the parameter is set to \"FixedRatio\", "+
                         "then automatic scaling is performed, but the "+
                         "original aspect ratio is preserved.",
@@ -499,7 +454,29 @@ public class VncViewer extends java.applet.Applet implements Runnable
                       "Produce a system beep when requested to by the server.", 
                       true);
   StringParameter via
-  = new StringParameter("via", "Gateway to tunnel via", null);
+  = 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. "+
+    "See the System Properties section below for "+
+    "information on configuring the -via option.",
+    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);
 
   BoolParameter customCompressLevel
   = new BoolParameter("CustomCompressLevel",
diff --git a/java/com/tigervnc/vncviewer/tunnel.java b/java/com/tigervnc/vncviewer/tunnel.java
new file mode 100644 (file)
index 0000000..2fc12a6
--- /dev/null
@@ -0,0 +1,327 @@
+/*
+ *  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("com.tigervnc.VNC_TUNNEL_CMD");
+      } else {
+        pattern = System.getProperty("com.tigervnc.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 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.userEntry.setText((String)System.getProperties().get("user.name"));
+
+      Session session=jsch.getSession(dlg.userEntry.getText(), tunnelEndpoint, 22);
+      session.setUserInfo(dlg);
+      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");
+}