551 lines
17 KiB
Java
551 lines
17 KiB
Java
/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved.
|
|
* Copyright 2009-2013 Pierre Ossman <ossman@cendio.se> for Cendio AB
|
|
* Copyright (C) 2011-2013 D. R. Commander. All Rights Reserved.
|
|
* Copyright (C) 2011-2019 Brian P. Hinz
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
//
|
|
// CConn
|
|
//
|
|
// Methods on CConn are called from both the GUI thread and the thread which
|
|
// processes incoming RFB messages ("the RFB thread"). This means we need to
|
|
// be careful with synchronization here.
|
|
//
|
|
// Any access to writer() must not only be synchronized, but we must also make
|
|
// sure that the connection is in RFBSTATE_NORMAL. We are guaranteed this for
|
|
// any code called after serverInit() has been called. Since the DesktopWindow
|
|
// isn't created until then, any methods called only from DesktopWindow can
|
|
// assume that we are in RFBSTATE_NORMAL.
|
|
|
|
package com.tigervnc.vncviewer;
|
|
|
|
import java.awt.*;
|
|
import java.awt.datatransfer.StringSelection;
|
|
import java.awt.event.*;
|
|
import java.awt.Toolkit;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileNotFoundException;
|
|
import java.util.jar.Attributes;
|
|
import java.util.jar.Manifest;
|
|
import javax.swing.*;
|
|
import javax.swing.ImageIcon;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.SocketException;
|
|
import java.util.*;
|
|
import java.util.prefs.*;
|
|
|
|
import com.tigervnc.rdr.*;
|
|
import com.tigervnc.rfb.*;
|
|
import com.tigervnc.rfb.Point;
|
|
import com.tigervnc.rfb.Exception;
|
|
import com.tigervnc.network.Socket;
|
|
import com.tigervnc.network.TcpSocket;
|
|
|
|
import static com.tigervnc.vncviewer.Parameters.*;
|
|
|
|
public class CConn extends CConnection implements
|
|
FdInStreamBlockCallback, ActionListener {
|
|
|
|
// 8 colours (1 bit per component)
|
|
static final PixelFormat verylowColorPF =
|
|
new PixelFormat(8, 3, false, true, 1, 1, 1, 2, 1, 0);
|
|
|
|
// 64 colours (2 bits per component)
|
|
static final PixelFormat lowColorPF =
|
|
new PixelFormat(8, 6, false, true, 3, 3, 3, 4, 2, 0);
|
|
|
|
// 256 colours (2-3 bits per component)
|
|
static final PixelFormat mediumColorPF =
|
|
new PixelFormat(8, 8, false, true, 7, 7, 3, 5, 2, 0);
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// The following methods are all called from the RFB thread
|
|
|
|
public CConn(String vncServerName, Socket socket)
|
|
{
|
|
serverHost = null; serverPort = 0; desktop = null;
|
|
updateCount = 0; pixelCount = 0;
|
|
lastServerEncoding = -1;
|
|
|
|
setShared(shared.getValue());
|
|
sock = socket;
|
|
|
|
server.supportsLocalCursor = true;
|
|
server.supportsDesktopResize = true;
|
|
server.supportsClientRedirect = true;
|
|
|
|
if (customCompressLevel.getValue())
|
|
setCompressLevel(compressLevel.getValue());
|
|
|
|
if (!noJpeg.getValue())
|
|
setQualityLevel(qualityLevel.getValue());
|
|
|
|
if (sock == null) {
|
|
setServerName(Hostname.getHost(vncServerName));
|
|
setServerPort(Hostname.getPort(vncServerName));
|
|
try {
|
|
if (tunnel.getValue() || !via.getValue().isEmpty()) {
|
|
int localPort = TcpSocket.findFreeTcpPort();
|
|
if (localPort == 0)
|
|
throw new Exception("Could not obtain free TCP port");
|
|
String gatewayHost = Tunnel.getSshHost();
|
|
if (gatewayHost.isEmpty())
|
|
gatewayHost = getServerName();
|
|
Tunnel.createTunnel(gatewayHost, getServerName(),
|
|
getServerPort(), localPort);
|
|
sock = new TcpSocket("localhost", localPort);
|
|
vlog.info("Connected to localhost port "+localPort);
|
|
} else {
|
|
sock = new TcpSocket(getServerName(), getServerPort());
|
|
vlog.info("Connected to host "+getServerName()+" port "+getServerPort());
|
|
}
|
|
} catch (java.lang.Exception e) {
|
|
throw new Exception(e.getMessage());
|
|
}
|
|
} else {
|
|
String name = sock.getPeerEndpoint();
|
|
if (listenMode.getValue())
|
|
vlog.info("Accepted connection from " + name);
|
|
else
|
|
vlog.info("Connected to host "+Hostname.getHost(name)+" port "+Hostname.getPort(name));
|
|
}
|
|
|
|
// See callback below
|
|
sock.inStream().setBlockCallback(this);
|
|
|
|
setStreams(sock.inStream(), sock.outStream());
|
|
|
|
initialiseProtocol();
|
|
|
|
OptionsDialog.addCallback("handleOptions", this);
|
|
}
|
|
|
|
public String connectionInfo() {
|
|
String info = new String("Desktop name: %s%n"+
|
|
"Host: %s:%d%n"+
|
|
"Size: %dx%d%n"+
|
|
"Pixel format: %s%n"+
|
|
" (server default: %s)%n"+
|
|
"Requested encoding: %s%n"+
|
|
"Last used encoding: %s%n"+
|
|
"Line speed estimate: %d kbit/s%n"+
|
|
"Protocol version: %d.%d%n"+
|
|
"Security method: %s [%s]%n");
|
|
String infoText =
|
|
String.format(info, server.name(),
|
|
sock.getPeerName(), sock.getPeerPort(),
|
|
server.width(), server.height(),
|
|
server.pf().print(),
|
|
serverPF.print(),
|
|
Encodings.encodingName(getPreferredEncoding()),
|
|
Encodings.encodingName(lastServerEncoding),
|
|
sock.inStream().kbitsPerSecond(),
|
|
server.majorVersion, server.minorVersion,
|
|
Security.secTypeName(csecurity.getType()),
|
|
csecurity.description());
|
|
|
|
return infoText;
|
|
}
|
|
|
|
public int getUpdateCount()
|
|
{
|
|
return updateCount;
|
|
}
|
|
|
|
public int getPixelCount()
|
|
{
|
|
return pixelCount;
|
|
}
|
|
|
|
// The RFB core is not properly asynchronous, so it calls this callback
|
|
// whenever it needs to block to wait for more data. Since FLTK is
|
|
// monitoring the socket, we just make sure FLTK gets to run.
|
|
|
|
public void blockCallback() {
|
|
try {
|
|
synchronized(this) {
|
|
wait(1);
|
|
}
|
|
} catch (java.lang.InterruptedException e) {
|
|
throw new Exception(e.getMessage());
|
|
}
|
|
}
|
|
|
|
////////////////////// CConnection callback methods //////////////////////
|
|
|
|
// serverInit() is called when the serverInit message has been received. At
|
|
// this point we create the desktop window and display it. We also tell the
|
|
// server the pixel format and encodings to use and request the first update.
|
|
public void initDone()
|
|
{
|
|
// If using AutoSelect with old servers, start in FullColor
|
|
// mode. See comment in autoSelectFormatAndEncoding.
|
|
if (server.beforeVersion(3, 8) && autoSelect.getValue())
|
|
fullColor.setParam(true);
|
|
|
|
serverPF = server.pf();
|
|
|
|
desktop = new DesktopWindow(server.width(), server.height(),
|
|
server.name(), serverPF, this);
|
|
fullColorPF = desktop.getPreferredPF();
|
|
|
|
// Force a switch to the format and encoding we'd like
|
|
updatePixelFormat();
|
|
int encNum = Encodings.encodingNum(preferredEncoding.getValue());
|
|
if (encNum != -1)
|
|
setPreferredEncoding(encNum);
|
|
}
|
|
|
|
// setDesktopSize() is called when the desktop size changes (including when
|
|
// it is set initially).
|
|
public void setDesktopSize(int w, int h)
|
|
{
|
|
super.setDesktopSize(w, h);
|
|
resizeFramebuffer();
|
|
}
|
|
|
|
// setExtendedDesktopSize() is a more advanced version of setDesktopSize()
|
|
public void setExtendedDesktopSize(int reason, int result,
|
|
int w, int h, ScreenSet layout)
|
|
{
|
|
super.setExtendedDesktopSize(reason, result, w, h, layout);
|
|
|
|
if ((reason == screenTypes.reasonClient) &&
|
|
(result != screenTypes.resultSuccess)) {
|
|
vlog.error("SetDesktopSize failed: " + result);
|
|
return;
|
|
}
|
|
|
|
resizeFramebuffer();
|
|
}
|
|
|
|
// clientRedirect() migrates the client to another host/port
|
|
public void clientRedirect(int port, String host, String x509subject)
|
|
{
|
|
try {
|
|
sock.close();
|
|
sock = new TcpSocket(host, port);
|
|
vlog.info("Redirected to "+host+":"+port);
|
|
setServerName(host);
|
|
setServerPort(port);
|
|
sock.inStream().setBlockCallback(this);
|
|
setStreams(sock.inStream(), sock.outStream());
|
|
if (desktop != null)
|
|
desktop.dispose();
|
|
initialiseProtocol();
|
|
} catch (java.lang.Exception e) {
|
|
throw new Exception(e.getMessage());
|
|
}
|
|
}
|
|
|
|
// setName() is called when the desktop name changes
|
|
public void setName(String name)
|
|
{
|
|
super.setName(name);
|
|
if (desktop != null)
|
|
desktop.setName(name);
|
|
}
|
|
|
|
// framebufferUpdateStart() is called at the beginning of an update.
|
|
// Here we try to send out a new framebuffer update request so that the
|
|
// next update can be sent out in parallel with us decoding the current
|
|
// one.
|
|
public void framebufferUpdateStart()
|
|
{
|
|
|
|
super.framebufferUpdateStart();
|
|
|
|
}
|
|
|
|
// framebufferUpdateEnd() is called at the end of an update.
|
|
// For each rectangle, the FdInStream will have timed the speed
|
|
// of the connection, allowing us to select format and encoding
|
|
// appropriately, and then request another incremental update.
|
|
public void framebufferUpdateEnd()
|
|
{
|
|
super.framebufferUpdateEnd();
|
|
|
|
updateCount++;
|
|
|
|
desktop.updateWindow();
|
|
|
|
// Compute new settings based on updated bandwidth values
|
|
if (autoSelect.getValue())
|
|
autoSelectFormatAndEncoding();
|
|
}
|
|
|
|
// The rest of the callbacks are fairly self-explanatory...
|
|
|
|
public void setColourMapEntries(int firstColor, int nColors, int[] rgbs)
|
|
{
|
|
vlog.error("Invalid SetColourMapEntries from server!");
|
|
}
|
|
|
|
public void bell()
|
|
{
|
|
if (acceptBell.getValue())
|
|
desktop.getToolkit().beep();
|
|
}
|
|
|
|
public void serverCutText(String str, int len)
|
|
{
|
|
StringSelection buffer;
|
|
|
|
if (!acceptClipboard.getValue())
|
|
return;
|
|
|
|
ClipboardDialog.serverCutText(str);
|
|
}
|
|
|
|
public void dataRect(Rect r, int encoding)
|
|
{
|
|
sock.inStream().startTiming();
|
|
|
|
if (encoding != Encodings.encodingCopyRect)
|
|
lastServerEncoding = encoding;
|
|
|
|
super.dataRect(r, encoding);
|
|
|
|
sock.inStream().stopTiming();
|
|
|
|
pixelCount += r.area();
|
|
}
|
|
|
|
public void setCursor(int width, int height, Point hotspot,
|
|
byte[] data)
|
|
{
|
|
desktop.setCursor(width, height, hotspot, data);
|
|
}
|
|
|
|
public void fence(int flags, int len, byte[] data)
|
|
{
|
|
// can't call super.super.fence(flags, len, data);
|
|
server.supportsFence = true;
|
|
|
|
if ((flags & fenceTypes.fenceFlagRequest) != 0) {
|
|
// We handle everything synchronously so we trivially honor these modes
|
|
flags = flags & (fenceTypes.fenceFlagBlockBefore | fenceTypes.fenceFlagBlockAfter);
|
|
|
|
writer().writeFence(flags, len, data);
|
|
return;
|
|
}
|
|
}
|
|
|
|
////////////////////// Internal methods //////////////////////
|
|
public void resizeFramebuffer()
|
|
{
|
|
if (desktop == null)
|
|
return;
|
|
|
|
desktop.resizeFramebuffer(server.width(), server.height());
|
|
}
|
|
|
|
// autoSelectFormatAndEncoding() chooses the format and encoding appropriate
|
|
// to the connection speed:
|
|
//
|
|
// First we wait for at least one second of bandwidth measurement.
|
|
//
|
|
// Above 16Mbps (i.e. LAN), we choose the second highest JPEG quality,
|
|
// which should be perceptually lossless.
|
|
//
|
|
// If the bandwidth is below that, we choose a more lossy JPEG quality.
|
|
//
|
|
// If the bandwidth drops below 256 Kbps, we switch to palette mode.
|
|
//
|
|
// Note: The system here is fairly arbitrary and should be replaced
|
|
// with something more intelligent at the server end.
|
|
//
|
|
private void autoSelectFormatAndEncoding()
|
|
{
|
|
long kbitsPerSecond = sock.inStream().kbitsPerSecond();
|
|
long timeWaited = sock.inStream().timeWaited();
|
|
boolean newFullColour = fullColor.getValue();
|
|
int newQualityLevel = qualityLevel.getValue();
|
|
|
|
// Always use Tight
|
|
setPreferredEncoding(Encodings.encodingTight);
|
|
|
|
// Check that we have a decent bandwidth measurement
|
|
if ((kbitsPerSecond == 0) || (timeWaited < 100))
|
|
return;
|
|
|
|
// Select appropriate quality level
|
|
if (!noJpeg.getValue()) {
|
|
if (kbitsPerSecond > 16000)
|
|
newQualityLevel = 8;
|
|
else
|
|
newQualityLevel = 6;
|
|
|
|
if (newQualityLevel != qualityLevel.getValue()) {
|
|
vlog.info("Throughput "+kbitsPerSecond+
|
|
" kbit/s - changing to quality "+newQualityLevel);
|
|
qualityLevel.setParam(newQualityLevel);
|
|
setQualityLevel(newQualityLevel);
|
|
}
|
|
}
|
|
|
|
if (server.beforeVersion(3, 8)) {
|
|
// Xvnc from TightVNC 1.2.9 sends out FramebufferUpdates with
|
|
// cursors "asynchronously". If this happens in the middle of a
|
|
// pixel format change, the server will encode the cursor with
|
|
// the old format, but the client will try to decode it
|
|
// according to the new format. This will lead to a
|
|
// crash. Therefore, we do not allow automatic format change for
|
|
// old servers.
|
|
return;
|
|
}
|
|
|
|
// Select best color level
|
|
newFullColour = (kbitsPerSecond > 256);
|
|
if (newFullColour != fullColor.getValue()) {
|
|
if (newFullColour)
|
|
vlog.info("Throughput "+kbitsPerSecond+ " kbit/s - full color is now enabled");
|
|
else
|
|
vlog.info("Throughput "+kbitsPerSecond+ " kbit/s - full color is now disabled");
|
|
fullColor.setParam(newFullColour);
|
|
updatePixelFormat();
|
|
}
|
|
}
|
|
|
|
// updatePixelFormat() requests an update from the server, having set the
|
|
// format and encoding appropriately.
|
|
private void updatePixelFormat()
|
|
{
|
|
PixelFormat pf;
|
|
|
|
if (fullColor.getValue()) {
|
|
pf = fullColorPF;
|
|
} else {
|
|
if (lowColorLevel.getValue() == 0) {
|
|
pf = verylowColorPF;
|
|
} else if (lowColorLevel.getValue() == 1) {
|
|
pf = lowColorPF;
|
|
} else {
|
|
pf = mediumColorPF;
|
|
}
|
|
}
|
|
|
|
String str = pf.print();
|
|
vlog.info("Using pixel format " + str);
|
|
setPF(pf);
|
|
}
|
|
|
|
public void handleOptions()
|
|
{
|
|
|
|
// Checking all the details of the current set of encodings is just
|
|
// a pain. Assume something has changed, as resending the encoding
|
|
// list is cheap. Avoid overriding what the auto logic has selected
|
|
// though.
|
|
if (!autoSelect.getValue()) {
|
|
int encNum = Encodings.encodingNum(preferredEncoding.getValue());
|
|
|
|
if (encNum != -1)
|
|
this.setPreferredEncoding(encNum);
|
|
}
|
|
|
|
if (customCompressLevel.getValue())
|
|
this.setCompressLevel(compressLevel.getValue());
|
|
else
|
|
this.setCompressLevel(-1);
|
|
|
|
if (!noJpeg.getValue() && !autoSelect.getValue())
|
|
this.setQualityLevel(qualityLevel.getValue());
|
|
else
|
|
this.setQualityLevel(-1);
|
|
|
|
this.updatePixelFormat();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// The following methods are all called from the GUI thread
|
|
|
|
// close() shuts down the socket, thus waking up the RFB thread.
|
|
public void close() {
|
|
if (closeListener != null) {
|
|
JFrame f =
|
|
(JFrame)SwingUtilities.getAncestorOfClass(JFrame.class, desktop);
|
|
if (f != null)
|
|
f.dispatchEvent(new WindowEvent(f, WindowEvent.WINDOW_CLOSING));
|
|
}
|
|
shuttingDown = true;
|
|
try {
|
|
if (sock != null)
|
|
sock.shutdown();
|
|
} catch (java.lang.Exception e) {
|
|
throw new Exception(e.getMessage());
|
|
}
|
|
}
|
|
|
|
// writeClientCutText() is called from the clipboard dialog
|
|
public void writeClientCutText(String str, int len) {
|
|
if ((state() != stateEnum.RFBSTATE_NORMAL) || shuttingDown)
|
|
return;
|
|
writer().writeClientCutText(str, len);
|
|
}
|
|
|
|
public void actionPerformed(ActionEvent e) {}
|
|
|
|
public Socket getSocket() {
|
|
return sock;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// The following methods are called from both RFB and GUI threads
|
|
|
|
// the following never change so need no synchronization:
|
|
|
|
// access to desktop by different threads is specified in DesktopWindow
|
|
|
|
// the following need no synchronization:
|
|
|
|
// shuttingDown is set by the GUI thread and only ever tested by the RFB
|
|
// thread after the window has been destroyed.
|
|
boolean shuttingDown = false;
|
|
|
|
// reading and writing int and boolean is atomic in java, so no
|
|
// synchronization of the following flags is needed:
|
|
|
|
|
|
// All menu, options, about and info stuff is done in the GUI thread (apart
|
|
// from when constructed).
|
|
|
|
// the following are only ever accessed by the GUI thread:
|
|
private String serverHost;
|
|
private int serverPort;
|
|
private Socket sock;
|
|
|
|
protected DesktopWindow desktop;
|
|
|
|
private int updateCount;
|
|
private int pixelCount;
|
|
|
|
private PixelFormat serverPF;
|
|
private PixelFormat fullColorPF;
|
|
|
|
private int lastServerEncoding;
|
|
|
|
public ActionListener closeListener = null;
|
|
|
|
static LogWriter vlog = new LogWriter("CConn");
|
|
}
|