Files
2026-01-29 11:29:56 +08:00

756 lines
23 KiB
Java

/* Copyright (C) 2002-2005 RealVNC Ltd. 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.
*/
package com.tigervnc.rfb;
import java.awt.color.*;
import java.awt.image.*;
import java.nio.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import com.tigervnc.network.*;
import com.tigervnc.rdr.*;
abstract public class CConnection extends CMsgHandler {
static LogWriter vlog = new LogWriter("CConnection");
private static final String osName =
System.getProperty("os.name").toLowerCase(Locale.ENGLISH);
public CConnection()
{
super();
csecurity = null;
supportsLocalCursor = false; supportsDesktopResize = false;
is = null; os = null; reader_ = null; writer_ = null;
shared = false;
state_ = stateEnum.RFBSTATE_UNINITIALISED;
pendingPFChange = false; preferredEncoding = Encodings.encodingTight;
compressLevel = 2; qualityLevel = -1;
formatChange = false; encodingChange = false;
firstUpdate = true; pendingUpdate = false; continuousUpdates = false;
forceNonincremental = true;
framebuffer = null; decoder = new DecodeManager(this);
security = new SecurityClient();
}
// Methods to initialise the connection
// setServerName() is used to provide a unique(ish) name for the server to
// which we are connected. This might be the result of getPeerEndpoint on
// a TcpSocket, for example, or a host specified by DNS name & port.
// The serverName is used when verifying the Identity of a host (see RA2).
public void setServerName(String name_) { serverName = name_; }
public void setServerPort(int port_) { serverPort = port_; }
// setStreams() sets the streams to be used for the connection. These must
// be set before initialiseProtocol() and processMsg() are called. The
// CSecurity object may call setStreams() again to provide alternative
// streams over which the RFB protocol is sent (i.e. encrypting/decrypting
// streams). Ownership of the streams remains with the caller
// (i.e. SConnection will not delete them).
public final void setStreams(InStream is_, OutStream os_)
{
is = is_;
os = os_;
}
// setShared sets the value of the shared flag which will be sent to the
// server upon initialisation.
public final void setShared(boolean s) { shared = s; }
// setFramebuffer configures the PixelBuffer that the CConnection
// should render all pixel data in to. Note that the CConnection
// takes ownership of the PixelBuffer and it must not be deleted by
// anyone else. Call setFramebuffer again with NULL or a different
// PixelBuffer to delete the previous one.
public void setFramebuffer(ModifiablePixelBuffer fb)
{
decoder.flush();
if (fb != null) {
assert(fb.width() == server.width());
assert(fb.height() == server.height());
}
if ((framebuffer != null) && (fb != null)) {
Rect rect = new Rect();
Raster data;
byte[] black = new byte[4];
// Copy still valid area
rect.setXYWH(0, 0,
Math.min(fb.width(), framebuffer.width()),
Math.min(fb.height(), framebuffer.height()));
data = framebuffer.getBuffer(rect);
fb.imageRect(framebuffer.getPF(), rect, data);
// Black out any new areas
if (fb.width() > framebuffer.width()) {
rect.setXYWH(framebuffer.width(), 0,
fb.width() - framebuffer.width(),
fb.height());
fb.fillRect(rect, black);
}
if (fb.height() > framebuffer.height()) {
rect.setXYWH(0, framebuffer.height(),
fb.width(),
fb.height() - framebuffer.height());
fb.fillRect(rect, black);
}
}
framebuffer = fb;
}
// initialiseProtocol() should be called once the streams and security
// types are set. Subsequently, processMsg() should be called whenever
// there is data to read on the InStream.
public final void initialiseProtocol()
{
state_ = stateEnum.RFBSTATE_PROTOCOL_VERSION;
}
// processMsg() should be called whenever there is data to read on the
// InStream. You must have called initialiseProtocol() first.
public void processMsg()
{
switch (state_) {
case RFBSTATE_PROTOCOL_VERSION: processVersionMsg(); break;
case RFBSTATE_SECURITY_TYPES: processSecurityTypesMsg(); break;
case RFBSTATE_SECURITY: processSecurityMsg(); break;
case RFBSTATE_SECURITY_RESULT: processSecurityResultMsg(); break;
case RFBSTATE_INITIALISATION: processInitMsg(); break;
case RFBSTATE_NORMAL: reader_.readMsg(); break;
case RFBSTATE_UNINITIALISED:
throw new Exception("CConnection.processMsg: not initialised yet?");
default:
throw new Exception("CConnection.processMsg: invalid state");
}
}
private void processVersionMsg()
{
ByteBuffer verStr = ByteBuffer.allocate(12);
int majorVersion;
int minorVersion;
vlog.debug("Reading protocol version");
if (!is.checkNoWait(12))
return;
is.readBytes(verStr, 12);
if ((new String(verStr.array())).matches("RFB \\d{3}\\.\\d{3}\\n")) {
majorVersion =
Integer.parseInt((new String(verStr.array())).substring(4,7));
minorVersion =
Integer.parseInt((new String(verStr.array())).substring(8,11));
} else {
state_ = stateEnum.RFBSTATE_INVALID;
throw new Exception("reading version failed: not an RFB server?");
}
server.setVersion(majorVersion, minorVersion);
vlog.info("Server supports RFB protocol version "
+server.majorVersion+"."+ server.minorVersion);
// The only official RFB protocol versions are currently 3.3, 3.7 and 3.8
if (server.beforeVersion(3,3)) {
String msg = ("Server gave unsupported RFB protocol version "+
server.majorVersion+"."+server.minorVersion);
vlog.error(msg);
state_ = stateEnum.RFBSTATE_INVALID;
throw new Exception(msg);
} else if (server.beforeVersion(3,7)) {
server.setVersion(3,3);
} else if (server.afterVersion(3,8)) {
server.setVersion(3,8);
}
verStr.clear();
verStr.put(String.format("RFB %03d.%03d\n",
server.majorVersion, server.minorVersion).getBytes()).flip();
os.writeBytes(verStr.array(), 0, 12);
os.flush();
state_ = stateEnum.RFBSTATE_SECURITY_TYPES;
vlog.info("Using RFB protocol version "+
server.majorVersion+"."+server.minorVersion);
}
private void processSecurityTypesMsg()
{
vlog.debug("Processing security types message");
int secType = Security.secTypeInvalid;
List<Integer> secTypes = new ArrayList<Integer>();
secTypes = security.GetEnabledSecTypes();
if (server.isVersion(3,3)) {
// legacy 3.3 server may only offer "vnc authentication" or "none"
secType = is.readU32();
if (secType == Security.secTypeInvalid) {
throwConnFailedException();
} else if (secType == Security.secTypeNone || secType == Security.secTypeVncAuth) {
Iterator<Integer> i;
for (i = secTypes.iterator(); i.hasNext(); ) {
int refType = (Integer)i.next();
if (refType == secType) {
secType = refType;
break;
}
}
if (!secTypes.contains(secType))
secType = Security.secTypeInvalid;
} else {
vlog.error("Unknown 3.3 security type "+secType);
throw new Exception("Unknown 3.3 security type");
}
} else {
// 3.7 server will offer us a list
int nServerSecTypes = is.readU8();
if (nServerSecTypes == 0)
throwConnFailedException();
Iterator<Integer> j;
for (int i = 0; i < nServerSecTypes; i++) {
int serverSecType = is.readU8();
vlog.debug("Server offers security type "+
Security.secTypeName(serverSecType)+"("+serverSecType+")");
/*
* Use the first type sent by server which matches client's type.
* It means server's order specifies priority.
*/
if (secType == Security.secTypeInvalid) {
for (j = secTypes.iterator(); j.hasNext(); ) {
int refType = (Integer)j.next();
if (refType == serverSecType) {
secType = refType;
break;
}
}
}
}
// Inform the server of our decision
if (secType != Security.secTypeInvalid) {
os.writeU8(secType);
os.flush();
vlog.debug("Choosing security type "+Security.secTypeName(secType)+
"("+secType+")");
}
}
if (secType == Security.secTypeInvalid) {
state_ = stateEnum.RFBSTATE_INVALID;
vlog.error("No matching security types");
throw new Exception("No matching security types");
}
state_ = stateEnum.RFBSTATE_SECURITY;
csecurity = security.GetCSecurity(secType);
processSecurityMsg();
}
private void processSecurityMsg() {
vlog.debug("Processing security message");
if (csecurity.processMsg(this)) {
state_ = stateEnum.RFBSTATE_SECURITY_RESULT;
processSecurityResultMsg();
}
}
private void processSecurityResultMsg() {
vlog.debug("Processing security result message");
int result;
if (server.beforeVersion(3,8) && csecurity.getType() == Security.secTypeNone) {
result = Security.secResultOK;
} else {
if (!is.checkNoWait(1)) return;
result = is.readU32();
}
switch (result) {
case Security.secResultOK:
securityCompleted();
return;
case Security.secResultFailed:
vlog.debug("Auth failed");
break;
case Security.secResultTooMany:
vlog.debug("Auth failed: Too many tries");
break;
default:
throw new Exception("Unknown security result from server");
}
state_ = stateEnum.RFBSTATE_INVALID;
if (server.beforeVersion(3,8))
throw new AuthFailureException();
String reason = is.readString();
throw new AuthFailureException(reason);
}
private void processInitMsg() {
vlog.debug("Reading server initialisation");
reader_.readServerInit();
}
private void throwConnFailedException() {
state_ = stateEnum.RFBSTATE_INVALID;
String reason;
reason = is.readString();
throw new ConnFailedException(reason);
}
private void securityCompleted() {
state_ = stateEnum.RFBSTATE_INITIALISATION;
reader_ = new CMsgReader(this, is);
writer_ = new CMsgWriter(server, os);
vlog.debug("Authentication success!");
authSuccess();
writer_.writeClientInit(shared);
}
// Methods overridden from CMsgHandler
// Note: These must be called by any deriving classes
public void setDesktopSize(int w, int h) {
decoder.flush();
super.setDesktopSize(w,h);
if (continuousUpdates)
writer().writeEnableContinuousUpdates(true, 0, 0,
server.width(),
server.height());
resizeFramebuffer();
assert(framebuffer != null);
assert(framebuffer.width() == server.width());
assert(framebuffer.height() == server.height());
}
public void setExtendedDesktopSize(int reason,
int result,
int w, int h,
ScreenSet layout) {
decoder.flush();
super.setExtendedDesktopSize(reason, result, w, h, layout);
if (continuousUpdates)
writer().writeEnableContinuousUpdates(true, 0, 0,
server.width(),
server.height());
resizeFramebuffer();
assert(framebuffer != null);
assert(framebuffer.width() == server.width());
assert(framebuffer.height() == server.height());
}
public void endOfContinuousUpdates()
{
super.endOfContinuousUpdates();
// We've gotten the marker for a format change, so make the pending
// one active
if (pendingPFChange) {
server.setPF(pendingPF);
pendingPFChange = false;
// We might have another change pending
if (formatChange)
requestNewUpdate();
}
}
// serverInit() is called when the ServerInit message is received. The
// derived class must call on to CConnection::serverInit().
public void serverInit(int width, int height,
PixelFormat pf, String name)
{
super.serverInit(width, height, pf, name);
state_ = stateEnum.RFBSTATE_NORMAL;
vlog.debug("Initialisation done");
initDone();
assert(framebuffer != null);
// FIXME: even if the client is scaling?
assert(framebuffer.width() == server.width());
assert(framebuffer.height() == server.height());
// We want to make sure we call SetEncodings at least once
encodingChange = true;
requestNewUpdate();
// This initial update request is a bit of a corner case, so we need
// to help out setting the correct format here.
if (pendingPFChange) {
server.setPF(pendingPF);
pendingPFChange = false;
}
}
public void readAndDecodeRect(Rect r, int encoding,
ModifiablePixelBuffer pb)
{
decoder.decodeRect(r, encoding, pb);
decoder.flush();
}
public void framebufferUpdateStart()
{
super.framebufferUpdateStart();
assert(framebuffer != null);
// Note: This might not be true if continuous updates are supported
pendingUpdate = false;
requestNewUpdate();
}
public void framebufferUpdateEnd()
{
decoder.flush();
super.framebufferUpdateEnd();
// A format change has been scheduled and we are now past the update
// with the old format. Time to active the new one.
if (pendingPFChange && !continuousUpdates) {
server.setPF(pendingPF);
pendingPFChange = false;
}
if (firstUpdate) {
if (server.supportsContinuousUpdates) {
vlog.info("Enabling continuous updates");
continuousUpdates = true;
writer().writeEnableContinuousUpdates(true, 0, 0,
server.width(),
server.height());
}
firstUpdate = false;
}
}
public void dataRect(Rect r, int encoding)
{
decoder.decodeRect(r, encoding, framebuffer);
}
// Methods to be overridden in a derived class
// authSuccess() is called when authentication has succeeded.
public void authSuccess() { }
// initDone() is called when the connection is fully established
// and standard messages can be sent. This is called before the
// initial FramebufferUpdateRequest giving a derived class the
// chance to modify pixel format and settings. The derived class
// must also make sure it has provided a valid framebuffer before
// returning.
public void initDone() { }
// resizeFramebuffer() is called whenever the framebuffer
// dimensions or the screen layout changes. A subclass must make
// sure the pixel buffer has been updated once this call returns.
public void resizeFramebuffer()
{
assert(false);
}
// refreshFramebuffer() forces a complete refresh of the entire
// framebuffer
public void refreshFramebuffer()
{
forceNonincremental = true;
// Without fences, we cannot safely trigger an update request directly
// but must wait for the next update to arrive.
if (continuousUpdates)
requestNewUpdate();
}
// setPreferredEncoding()/getPreferredEncoding() adjusts which
// encoding is listed first as a hint to the server that it is the
// preferred one
public void setPreferredEncoding(int encoding)
{
if (preferredEncoding == encoding)
return;
preferredEncoding = encoding;
encodingChange = true;
}
public int getPreferredEncoding()
{
return preferredEncoding;
}
// setCompressLevel()/setQualityLevel() controls the encoding hints
// sent to the server
public void setCompressLevel(int level)
{
if (compressLevel == level)
return;
compressLevel = level;
encodingChange = true;
}
public void setQualityLevel(int level)
{
if (qualityLevel == level)
return;
qualityLevel = level;
encodingChange = true;
}
// setPF() controls the pixel format requested from the server.
// server.pf() will automatically be adjusted once the new format
// is active.
public void setPF(PixelFormat pf)
{
if (server.pf().equal(pf) && !formatChange)
return;
nextPF = pf;
formatChange = true;
}
public CMsgReader reader() { return reader_; }
public CMsgWriter writer() { return writer_; }
public InStream getInStream() { return is; }
public OutStream getOutStream() { return os; }
// Access method used by SSecurity implementations that can verify servers'
// Identities, to determine the unique(ish) name of the server.
public String getServerName() { return serverName; }
public int getServerPort() { return serverPort; }
boolean isSecure() { return csecurity != null ? csecurity.isSecure() : false; }
public enum stateEnum {
RFBSTATE_UNINITIALISED,
RFBSTATE_PROTOCOL_VERSION,
RFBSTATE_SECURITY_TYPES,
RFBSTATE_SECURITY,
RFBSTATE_SECURITY_RESULT,
RFBSTATE_INITIALISATION,
RFBSTATE_NORMAL,
RFBSTATE_INVALID
};
public stateEnum state() { return state_; }
protected void setState(stateEnum s) { state_ = s; }
protected void setReader(CMsgReader r) { reader_ = r; }
protected void setWriter(CMsgWriter w) { writer_ = w; }
protected ModifiablePixelBuffer getFramebuffer() { return framebuffer; }
public void fence(int flags, int len, byte[] data)
{
super.fence(flags, len, data);
if ((flags & fenceTypes.fenceFlagRequest) != 0)
return;
// We cannot guarantee any synchronisation at this level
flags = 0;
writer().writeFence(flags, len, data);
}
// requestNewUpdate() requests an update from the server, having set the
// format and encoding appropriately.
private void requestNewUpdate()
{
if (formatChange && !pendingPFChange) {
/* Catch incorrect requestNewUpdate calls */
assert(!pendingUpdate || continuousUpdates);
// We have to make sure we switch the internal format at a safe
// time. For continuous updates we temporarily disable updates and
// look for a EndOfContinuousUpdates message to see when to switch.
// For classical updates we just got a new update right before this
// function was called, so we need to make sure we finish that
// update before we can switch.
pendingPFChange = true;
pendingPF = nextPF;
if (continuousUpdates)
writer().writeEnableContinuousUpdates(false, 0, 0, 0, 0);
writer().writeSetPixelFormat(pendingPF);
if (continuousUpdates)
writer().writeEnableContinuousUpdates(true, 0, 0,
server.width(),
server.height());
formatChange = false;
}
if (encodingChange) {
updateEncodings();
encodingChange = false;
}
if (forceNonincremental || !continuousUpdates) {
pendingUpdate = true;
writer().writeFramebufferUpdateRequest(new Rect(0, 0,
server.width(),
server.height()),
!forceNonincremental);
}
forceNonincremental = false;
}
// Ask for encodings based on which decoders are supported. Assumes higher
// encoding numbers are more desirable.
private void updateEncodings()
{
List<Integer> encodings = new ArrayList<Integer>();
if (server.supportsLocalCursor) {
// JRE on Windows does not support cursors with alpha
if (!osName.contains("windows")) {
encodings.add(Encodings.pseudoEncodingCursorWithAlpha);
encodings.add(Encodings.pseudoEncodingVMwareCursor);
}
encodings.add(Encodings.pseudoEncodingCursor);
encodings.add(Encodings.pseudoEncodingXCursor);
}
if (server.supportsDesktopResize) {
encodings.add(Encodings.pseudoEncodingDesktopSize);
encodings.add(Encodings.pseudoEncodingExtendedDesktopSize);
}
if (server.supportsClientRedirect)
encodings.add(Encodings.pseudoEncodingClientRedirect);
encodings.add(Encodings.pseudoEncodingDesktopName);
encodings.add(Encodings.pseudoEncodingLastRect);
encodings.add(Encodings.pseudoEncodingContinuousUpdates);
encodings.add(Encodings.pseudoEncodingFence);
if (Decoder.supported(preferredEncoding)) {
encodings.add(preferredEncoding);
}
encodings.add(Encodings.encodingCopyRect);
for (int i = Encodings.encodingMax; i >= 0; i--) {
if ((i != preferredEncoding) && Decoder.supported(i))
encodings.add(i);
}
if (compressLevel >= 0 && compressLevel <= 9)
encodings.add(Encodings.pseudoEncodingCompressLevel0 + compressLevel);
if (qualityLevel >= 0 && qualityLevel <= 9)
encodings.add(Encodings.pseudoEncodingQualityLevel0 + qualityLevel);
writer().writeSetEncodings(encodings);
}
private void throwAuthFailureException() {
String reason;
vlog.debug("state="+state()+", ver="+server.majorVersion+"."+server.minorVersion);
if (state() == stateEnum.RFBSTATE_SECURITY_RESULT && !server.beforeVersion(3,8)) {
reason = is.readString();
} else {
reason = "Authentication failure";
}
state_ = stateEnum.RFBSTATE_INVALID;
vlog.error(reason);
throw new AuthFailureException(reason);
}
public CSecurity csecurity;
public SecurityClient security;
protected boolean supportsLocalCursor;
protected boolean supportsDesktopResize;
private InStream is;
private OutStream os;
private CMsgReader reader_;
private CMsgWriter writer_;
private boolean deleteStreamsWhenDone;
private boolean shared;
private stateEnum state_;
private String serverName;
private int serverPort;
private boolean pendingPFChange;
private PixelFormat pendingPF;
private int preferredEncoding;
private int compressLevel;
private int qualityLevel;
private boolean formatChange;
private PixelFormat nextPF;
private boolean encodingChange;
private boolean firstUpdate;
private boolean pendingUpdate;
private boolean continuousUpdates;
private boolean forceNonincremental;
protected ModifiablePixelBuffer framebuffer;
private DecodeManager decoder;
}