From 94653cf165d42da80d734d3d5672cc0041a976a2 Mon Sep 17 00:00:00 2001 From: Brian Hinz Date: Sun, 6 May 2012 19:18:05 +0000 Subject: [PATCH] Re-implemented SSH tunneling features in Java viewer based on TurboVNC native client method. Adds support for string substitutions of via command, as well as a '-tunnel' option for tunneling directly to VNC server host (also support string substitutions). This implementation requires the vncServerName to be the last argument specified to the viewer, however this is consistent with the behavior of the native client and does not appear to cause any problems when using the viewer in standalone, applet, or jnlp modes. git-svn-id: svn://svn.code.sf.net/p/tigervnc/code/trunk@4910 3789f03b-4d11-0410-bbf8-ca57d06f2519 --- java/com/tigervnc/vncviewer/VncViewer.java | 181 +++++------- java/com/tigervnc/vncviewer/tunnel.java | 327 +++++++++++++++++++++ 2 files changed, 406 insertions(+), 102 deletions(-) create mode 100644 java/com/tigervnc/vncviewer/tunnel.java diff --git a/java/com/tigervnc/vncviewer/VncViewer.java b/java/com/tigervnc/vncviewer/VncViewer.java index 3c990401..85da2499 100644 --- a/java/com/tigervnc/vncviewer/VncViewer.java +++ b/java/com/tigervnc/vncviewer/VncViewer.java @@ -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 privateKeys = new ArrayList(); - 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 index 00000000..2fc12a6b --- /dev/null +++ b/java/com/tigervnc/vncviewer/tunnel.java @@ -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 privateKeys = new ArrayList(); + 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"); +} -- 2.39.5