Browse Source

Overhaul of SSH tunneling features in Java viewer

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.
tags/v1.6.90
Brian P. Hinz 8 years ago
parent
commit
d6288adf71

+ 109
- 10
java/com/tigervnc/vncviewer/CConn.java View 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;

+ 2
- 0
java/com/tigervnc/vncviewer/Dialog.java View 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)

+ 428
- 21
java/com/tigervnc/vncviewer/OptionsDialog.java View 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);
}
}
}

+ 2
- 0
java/com/tigervnc/vncviewer/PasswdDialog.java View 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;

+ 355
- 0
java/com/tigervnc/vncviewer/Tunnel.java View File

@@ -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");
}

+ 62
- 28
java/com/tigervnc/vncviewer/VncViewer.java View 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. "+

+ 0
- 327
java/com/tigervnc/vncviewer/tunnel.java View File

@@ -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");
}

Loading…
Cancel
Save