Files
orbithub/tests/test_vnc_session_backend.cpp
T
ksmithandClaude Sonnet 5 776db5ec04 Allow blank usernames, asking for one at connect time instead
Closes #21. SSH and RDP profiles previously hard-required a username
to even save the profile; that validation is dropped, and
SessionTab::requestConnectOptions() now prompts for it at connect
time when blank, reusing the existing password-prompt bar in
unmasked mode -- the same pattern already used for a blank password.

VNC's username is trickier: most VNC servers never use one (plain VNC
Authentication and no-auth don't), only the two Apple auth schemes
(security types 30/33) do, and which auth method gets used isn't known
until mid-connection, after the server's security-type list has been
negotiated -- too late for the pre-connect prompt SSH/RDP uses. Adds a
new async request/response pair to SessionBackend, usernameRequested()
signal / provideUsername() slot, mirroring the existing SSH host-key-
confirmation pattern. VncSessionBackend pauses its state machine right
before computing an Apple-auth response if no username is available --
without consuming the already-buffered prime/host-key bytes, so
resuming re-parses them identically -- emits the request, and resumes
via provideUsername(). Cancelling (or submitting blank) fails the
connection cleanly instead of sending Apple auth an empty username.

The username is kept on the tab's in-memory profile copy for its
lifetime, not written back to the saved profile, matching how
passwords are already handled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 08:01:57 -06:00

2238 lines
90 KiB
C++

#include "vnc_session_backend.h"
#include "vnc_apple_dh_auth.h"
#include <QTcpServer>
#include <QTcpSocket>
#include <QTest>
#include <openssl/bn.h>
#include <openssl/evp.h>
#include <zlib.h>
extern "C" {
#include <jpeglib.h>
}
#include <cstdlib>
#include <cstring>
namespace {
// Independently documented bit-reversal example for VNC Authentication's
// DES key prep (password "COW"): 'C'=0x43, 'O'=0x4F, 'W'=0x57, each
// reversed bit-by-bit -> 0xC2, 0xF2, 0xEA, padded with zeros to 8 bytes.
// Cross-checked against multiple independent VNC client implementations'
// documented behavior, not just re-derived from this code's own logic.
QByteArray expectedCowKey()
{
return QByteArray::fromHex("c2f2ea0000000000");
}
// A tiny scripted RFB "server" for state-machine tests: accepts exactly one
// connection on 127.0.0.1 and lets the test drive the byte sequence it
// sends, while capturing whatever the real VncSessionBackend under test
// writes back.
class FakeVncServer : public QObject
{
Q_OBJECT
public:
FakeVncServer()
{
server.listen(QHostAddress::LocalHost);
connect(&server, &QTcpServer::newConnection, this, [this]() {
connection = server.nextPendingConnection();
connect(connection, &QTcpSocket::readyRead, this, [this]() {
received.append(connection->readAll());
emit dataReceived();
});
emit clientConnected();
});
}
quint16 port() const { return server.serverPort(); }
void sendWhenConnected(const QByteArray& bytes)
{
if (connection != nullptr) {
connection->write(bytes);
}
}
// Scripted steps are matched by ordinal position, not by pattern-
// matching received byte content: several distinct RFB messages (e.g.
// the 1-byte security-type selection and the 1-byte ClientInit
// shared-flag) are indistinguishable by content alone, so content
// matching is genuinely ambiguous here.
int nextStep() { return step++; }
QTcpServer server;
QTcpSocket* connection = nullptr;
QByteArray received;
int step = 0;
signals:
void clientConnected();
void dataReceived();
};
Profile makeVncProfile(quint16 port)
{
Profile profile;
profile.name = QStringLiteral("Test VNC");
profile.host = QStringLiteral("127.0.0.1");
profile.port = port;
profile.protocol = QStringLiteral("VNC");
return profile;
}
SessionConnectOptions makeOptions(const QString& password = QString())
{
SessionConnectOptions options;
options.password = password;
return options;
}
void appendU16(QByteArray& buf, quint16 value)
{
buf.append(static_cast<char>((value >> 8) & 0xFF));
buf.append(static_cast<char>(value & 0xFF));
}
// Pixel bytes as expected under our negotiated SetPixelFormat: 32bpp
// little-endian, byte order B,G,R,pad.
QByteArray pixelBytes(int r, int g, int b)
{
QByteArray bytes;
bytes.append(static_cast<char>(b));
bytes.append(static_cast<char>(g));
bytes.append(static_cast<char>(r));
bytes.append(char(0));
return bytes;
}
QByteArray serverInitBytes(int width, int height)
{
QByteArray serverInit;
appendU16(serverInit, static_cast<quint16>(width));
appendU16(serverInit, static_cast<quint16>(height));
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
return serverInit;
}
// A FramebufferUpdate message with exactly one Hextile-encoded rectangle
// (RFC 6143 encoding type 5), given the already fully-formed tile byte
// stream to follow the rectangle header.
QByteArray hextileFramebufferUpdate(int x, int y, int width, int height, const QByteArray& tileData)
{
QByteArray update;
update.append(char(0)); update.append(char(0));
update.append(char(0)); update.append(char(1)); // 1 rectangle
appendU16(update, static_cast<quint16>(x));
appendU16(update, static_cast<quint16>(y));
appendU16(update, static_cast<quint16>(width));
appendU16(update, static_cast<quint16>(height));
update.append(char(0)); update.append(char(0)); update.append(char(0));
update.append(char(5)); // encoding = Hextile
update.append(tileData);
return update;
}
void appendU32(QByteArray& buf, quint32 value)
{
buf.append(static_cast<char>((value >> 24) & 0xFF));
buf.append(static_cast<char>((value >> 16) & 0xFF));
buf.append(static_cast<char>((value >> 8) & 0xFF));
buf.append(static_cast<char>(value & 0xFF));
}
// ZRLE's CPIXEL: 3 bytes, B,G,R order (the pad byte a full 32bpp pixel
// would have is simply omitted) -- matches VncPixelCodecs::rgbFromPixelBytes.
QByteArray cpixelBytes(int r, int g, int b)
{
QByteArray bytes;
bytes.append(static_cast<char>(b));
bytes.append(static_cast<char>(g));
bytes.append(static_cast<char>(r));
return bytes;
}
// A FramebufferUpdate message with exactly one ZRLE-encoded rectangle
// (RFC 6143 encoding type 16): a 4-byte compressed length followed by that
// many already-zlib-compressed bytes.
QByteArray zrleFramebufferUpdate(int x, int y, int width, int height, const QByteArray& compressed)
{
QByteArray update;
update.append(char(0)); update.append(char(0));
update.append(char(0)); update.append(char(1)); // 1 rectangle
appendU16(update, static_cast<quint16>(x));
appendU16(update, static_cast<quint16>(y));
appendU16(update, static_cast<quint16>(width));
appendU16(update, static_cast<quint16>(height));
update.append(char(0)); update.append(char(0)); update.append(char(0));
update.append(char(16)); // encoding = ZRLE
appendU32(update, static_cast<quint32>(compressed.size()));
update.append(compressed);
return update;
}
// Compresses `input` as a single, complete, self-contained zlib stream
// (its own header + Z_FINISH) -- correct for tests where each
// VncSessionBackend under test lazily starts its own fresh inflate stream
// on the first ZRLE rectangle it ever sees.
QByteArray zlibCompressWhole(const QByteArray& input)
{
z_stream stream{};
deflateInit(&stream, Z_DEFAULT_COMPRESSION);
QByteArray output(static_cast<int>(deflateBound(&stream, static_cast<uLong>(input.size()))),
char(0));
stream.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(input.constData()));
stream.avail_in = static_cast<uInt>(input.size());
stream.next_out = reinterpret_cast<Bytef*>(output.data());
stream.avail_out = static_cast<uInt>(output.size());
deflate(&stream, Z_FINISH);
output.resize(output.size() - static_cast<int>(stream.avail_out));
deflateEnd(&stream);
return output;
}
// Compresses `input` as one chunk of an ongoing (not yet finished, unless
// `finish` is true) zlib stream carried in `stream` across calls -- used to
// build two separately-decodable compressed chunks that only both decode
// correctly against a single persistent inflate stream, for testing that
// ZRLE's zlib stream is retained across rectangles/updates rather than
// reset each time.
QByteArray zlibCompressChunk(z_stream& stream, const QByteArray& input, bool finish)
{
QByteArray output(static_cast<int>(deflateBound(&stream, static_cast<uLong>(input.size()))) + 64,
char(0));
stream.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(input.constData()));
stream.avail_in = static_cast<uInt>(input.size());
const int flushMode = finish ? Z_FINISH : Z_SYNC_FLUSH;
int totalOut = 0;
do {
stream.next_out = reinterpret_cast<Bytef*>(output.data() + totalOut);
stream.avail_out = static_cast<uInt>(output.size() - totalOut);
deflate(&stream, flushMode);
totalOut = output.size() - static_cast<int>(stream.avail_out);
} while (stream.avail_out == 0);
output.resize(totalOut);
return output;
}
// Mirrors VncSessionBackend's compact-length decode in reverse, for
// building test fixtures.
void appendTightCompactLength(QByteArray& buf, quint32 length)
{
buf.append(static_cast<char>((length & 0x7F) | ((length > 0x7F) ? 0x80 : 0)));
if (length <= 0x7F) {
return;
}
length >>= 7;
buf.append(static_cast<char>((length & 0x7F) | ((length > 0x7F) ? 0x80 : 0)));
if (length <= 0x7F) {
return;
}
length >>= 7;
buf.append(static_cast<char>(length & 0xFF));
}
// A FramebufferUpdate message with exactly one Tight-encoded rectangle
// (RFC 6143 encoding type 7), given the already fully-formed byte stream
// to follow the rectangle header (starting with the compression-control
// byte).
QByteArray tightFramebufferUpdate(int x, int y, int width, int height, const QByteArray& tightData)
{
QByteArray update;
update.append(char(0)); update.append(char(0));
update.append(char(0)); update.append(char(1)); // 1 rectangle
appendU16(update, static_cast<quint16>(x));
appendU16(update, static_cast<quint16>(y));
appendU16(update, static_cast<quint16>(width));
appendU16(update, static_cast<quint16>(height));
update.append(char(0)); update.append(char(0)); update.append(char(0));
update.append(char(7)); // encoding = Tight
update.append(tightData);
return update;
}
// Encodes a solid-colored width x height baseline JPEG using libjpeg-turbo
// directly (mirroring the decode side's use of the same library), for
// exercising Tight's JPEG sub-mode against real, valid JPEG bytes.
QByteArray encodeJpegForTest(int width, int height, QRgb color)
{
jpeg_compress_struct cinfo;
jpeg_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
unsigned char* buffer = nullptr;
unsigned long bufferSize = 0;
jpeg_mem_dest(&cinfo, &buffer, &bufferSize);
cinfo.image_width = static_cast<JDIMENSION>(width);
cinfo.image_height = static_cast<JDIMENSION>(height);
cinfo.input_components = 3;
cinfo.in_color_space = JCS_RGB;
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, 100, TRUE);
// Disable chroma subsampling so a solid color round-trips as exactly
// as JPEG's DCT quantization allows, for a tighter test tolerance.
for (int i = 0; i < cinfo.num_components; ++i) {
cinfo.comp_info[i].h_samp_factor = 1;
cinfo.comp_info[i].v_samp_factor = 1;
}
jpeg_start_compress(&cinfo, TRUE);
QVector<unsigned char> row(width * 3);
for (int x = 0; x < width; ++x) {
row[(x * 3) + 0] = static_cast<unsigned char>(qRed(color));
row[(x * 3) + 1] = static_cast<unsigned char>(qGreen(color));
row[(x * 3) + 2] = static_cast<unsigned char>(qBlue(color));
}
JSAMPROW rowPointer[1] = { row.data() };
for (int y = 0; y < height; ++y) {
jpeg_write_scanlines(&cinfo, rowPointer, 1);
}
jpeg_finish_compress(&cinfo);
const QByteArray result(reinterpret_cast<const char*>(buffer), static_cast<int>(bufferSize));
jpeg_destroy_compress(&cinfo);
std::free(buffer);
return result;
}
// A small (512-bit) DH group generated fresh at test-run time -- rather
// than a hardcoded literal prime, to avoid any transcription risk -- plus
// one keypair against it, used to exercise VncAppleDhAuth::computeResponse()
// both directly and via a fake server acting as an Apple Screen Sharing
// (security type 30) endpoint. 512 bits keeps generation fast while still
// avoiding any "key too small" policy rejection from OpenSSL's default
// provider that a truly tiny hand-picked prime might trigger.
struct ToyDhKeypair {
QByteArray generatorBytes;
QByteArray primeBytes;
QByteArray publicKeyBytes;
BIGNUM* privateExponent = nullptr; // caller must BN_free
BIGNUM* prime = nullptr; // caller must BN_free
};
ToyDhKeypair generateToyDhKeypair()
{
ToyDhKeypair result;
BIGNUM* p = BN_new();
BN_generate_prime_ex(p, 512, 0, nullptr, nullptr, nullptr);
BIGNUM* g = BN_new();
BN_set_word(g, 2);
BN_CTX* ctx = BN_CTX_new();
BIGNUM* priv = BN_new();
BN_rand(priv, 256, -1, 0);
BIGNUM* pub = BN_new();
BN_mod_exp(pub, g, priv, p, ctx);
const int primeLen = BN_num_bytes(p);
result.primeBytes = QByteArray(primeLen, char(0));
BN_bn2binpad(p, reinterpret_cast<unsigned char*>(result.primeBytes.data()), primeLen);
// The real wire format's generator field is a literal 2 bytes (see
// appleDhAuthServerMessage()), so keep this the same size here too.
result.generatorBytes = QByteArray::fromHex("0002");
result.publicKeyBytes = QByteArray(primeLen, char(0));
BN_bn2binpad(pub, reinterpret_cast<unsigned char*>(result.publicKeyBytes.data()), primeLen);
result.privateExponent = priv;
result.prime = p;
BN_free(g);
BN_free(pub);
BN_CTX_free(ctx);
return result;
}
// The bytes a real Apple Screen Sharing server sends immediately after the
// client selects security type 30, before ClientInit/ServerInit even
// happen. Format confirmed empirically against a real macOS server: a
// literal 2-byte generator (NOT length-prefixed -- there is no separate
// generator-length field, unlike a first guess at this scheme might
// assume), then a 2-byte key length applying to both the prime and the
// server's public key that follow.
QByteArray appleDhAuthServerMessage(const QByteArray& generator, const QByteArray& prime,
const QByteArray& serverPublicKey)
{
QByteArray msg;
msg += generator;
appendU16(msg, static_cast<quint16>(prime.size()));
msg += prime;
msg += serverPublicKey;
return msg;
}
}
class TestVncSessionBackend : public QObject
{
Q_OBJECT
private slots:
// Pure-function coverage.
void desKeyFromPasswordMatchesKnownVector();
void desKeyFromPasswordPadsShortPasswords();
void desKeyFromPasswordTruncatesLongPasswords();
void vncAuthResponseIsSixteenBytesAndDeterministic();
void vncAuthResponseRejectsWrongChallengeSize();
void keysymForQtKeyMapsNamedKeys();
void keysymForQtKeyMapsFunctionKeys();
void keysymForQtKeyPassesThroughPrintableText();
void keysymForQtKeyUsesUnicodeConventionBeyondLatin1();
void keysymForQtKeyReturnsZeroForUnmapped();
void mapSocketErrorCoversCommonCases();
// State-machine coverage against a scripted in-process fake server.
void init();
void cleanup();
void connectsWithNoAuthRfb38();
void connectsWithVncAuthenticationRfb38();
void authFailureRfb38ReachesFailedState();
void unsupportedSecurityTypeReachesFailedState();
void rfb33ServerWithNoAuthConnectsWithoutSecurityResult();
void rawFramebufferUpdateProducesExpectedPixels();
void copyRectEncodingDoesNotFailConnection();
void unannouncedEncodingFailsConnectionWithClearMessage();
void serverCutTextEmitsRemoteClipboardTextChanged();
void setClipboardTextSendsClientCutText();
void cursorPseudoEncodingProducesExpectedImageAndHotspot();
void zeroSizeCursorPseudoEncodingHidesCursor();
void hextileRawTileProducesExpectedPixels();
void hextileBackgroundOnlyTileFillsSolidColor();
void hextileUncolouredSubrectUsesForeground();
void hextileColouredSubrectUsesOwnColor();
void hextileMultiTilePersistsBackgroundAcrossTiles();
void zrleRawTileProducesExpectedPixels();
void zrleSolidTileFillsColor();
void zrlePackedPaletteTileProducesExpectedPixels();
void zrlePlainRleTileProducesExpectedPixels();
void zrlePaletteRleTileProducesExpectedPixels();
void zrleStreamPersistsAcrossTwoFramebufferUpdates();
void tightFillProducesExpectedPixels();
void tightBasicCopyFilterProducesExpectedPixels();
void tightBasicPaletteFilterProducesExpectedPixels();
void tightJpegProducesExpectedPixels();
void tightStreamResetFlagAllowsIndependentDecoding();
void appleDhAuthRoundTripDecryptsToOriginalCredentials();
void connectsWithAppleDhAuthenticationRfb38();
void appleDhAuthIsPreferredOverVncAuthWhenBothOffered();
void appleDhAuthAcceptsRealCapturedMacOsServerParameters();
void cancellingUsernamePromptFailsConnectionCleanly();
private:
std::unique_ptr<FakeVncServer> m_server;
std::unique_ptr<VncSessionBackend> m_backend;
SessionState m_lastState = SessionState::Disconnected;
QString m_lastErrorDisplay;
QString m_lastErrorRaw;
QImage m_lastFrame;
bool m_gotFrame = false;
};
void TestVncSessionBackend::desKeyFromPasswordMatchesKnownVector()
{
QCOMPARE(VncSessionBackend::desKeyFromPassword(QStringLiteral("COW")), expectedCowKey());
}
void TestVncSessionBackend::desKeyFromPasswordPadsShortPasswords()
{
const QByteArray key = VncSessionBackend::desKeyFromPassword(QStringLiteral(""));
QCOMPARE(key, QByteArray(8, char(0)));
}
void TestVncSessionBackend::desKeyFromPasswordTruncatesLongPasswords()
{
// Only the first 8 characters are ever used as the DES key.
const QByteArray key1 = VncSessionBackend::desKeyFromPassword(QStringLiteral("12345678"));
const QByteArray key2 = VncSessionBackend::desKeyFromPassword(QStringLiteral("12345678ignored"));
QCOMPARE(key1, key2);
QCOMPARE(key1.size(), 8);
}
void TestVncSessionBackend::vncAuthResponseIsSixteenBytesAndDeterministic()
{
const QByteArray challenge(16, char(0x42));
const QByteArray response1 = VncSessionBackend::vncAuthResponse(challenge, QStringLiteral("secret"));
const QByteArray response2 = VncSessionBackend::vncAuthResponse(challenge, QStringLiteral("secret"));
QCOMPARE(response1.size(), 16);
QCOMPARE(response1, response2);
const QByteArray differentPassword =
VncSessionBackend::vncAuthResponse(challenge, QStringLiteral("other"));
QVERIFY(response1 != differentPassword);
}
void TestVncSessionBackend::vncAuthResponseRejectsWrongChallengeSize()
{
QVERIFY(VncSessionBackend::vncAuthResponse(QByteArray(15, char(0)), QStringLiteral("x")).isEmpty());
QVERIFY(VncSessionBackend::vncAuthResponse(QByteArray(), QStringLiteral("x")).isEmpty());
}
void TestVncSessionBackend::keysymForQtKeyMapsNamedKeys()
{
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Backspace, QString()), quint32(0xff08));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Tab, QString()), quint32(0xff09));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Return, QString()), quint32(0xff0d));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Escape, QString()), quint32(0xff1b));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Delete, QString()), quint32(0xffff));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Left, QString()), quint32(0xff51));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Up, QString()), quint32(0xff52));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Right, QString()), quint32(0xff53));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Down, QString()), quint32(0xff54));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Shift, QString()), quint32(0xffe1));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Control, QString()), quint32(0xffe3));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Alt, QString()), quint32(0xffe9));
}
void TestVncSessionBackend::keysymForQtKeyMapsFunctionKeys()
{
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_F1, QString()), quint32(0xffbe));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_F12, QString()), quint32(0xffc9));
}
void TestVncSessionBackend::keysymForQtKeyPassesThroughPrintableText()
{
// Printable ASCII/Latin-1 keysyms are just the codepoint itself.
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_A, QStringLiteral("a")), quint32('a'));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_5, QStringLiteral("5")), quint32('5'));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Space, QStringLiteral(" ")), quint32(' '));
}
void TestVncSessionBackend::keysymForQtKeyUsesUnicodeConventionBeyondLatin1()
{
// X11's convention for Unicode codepoints beyond Latin-1: keysym =
// 0x01000000 + codepoint. Euro sign U+20AC as an example.
const QString euro = QString::fromUtf8("\xE2\x82\xAC");
QCOMPARE(VncSessionBackend::keysymForQtKey(0, euro), quint32(0x01000000u + 0x20ACu));
}
void TestVncSessionBackend::keysymForQtKeyReturnsZeroForUnmapped()
{
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_MediaPlay, QString()), quint32(0));
}
void TestVncSessionBackend::mapSocketErrorCoversCommonCases()
{
QCOMPARE(VncSessionBackend::mapSocketError(QAbstractSocket::ConnectionRefusedError, QString()),
QStringLiteral("Connection refused by remote host."));
QCOMPARE(VncSessionBackend::mapSocketError(QAbstractSocket::HostNotFoundError, QString()),
QStringLiteral("Host could not be resolved."));
QCOMPARE(VncSessionBackend::mapSocketError(QAbstractSocket::SocketTimeoutError, QString()),
QStringLiteral("Connection timed out."));
QVERIFY(!VncSessionBackend::mapSocketError(QAbstractSocket::UnknownSocketError,
QStringLiteral("raw detail"))
.isEmpty());
}
void TestVncSessionBackend::init()
{
m_server = std::make_unique<FakeVncServer>();
m_backend =
std::make_unique<VncSessionBackend>(makeVncProfile(m_server->port()), nullptr);
m_lastState = SessionState::Disconnected;
m_lastErrorDisplay.clear();
m_lastErrorRaw.clear();
m_lastFrame = QImage();
m_gotFrame = false;
connect(m_backend.get(), &SessionBackend::stateChanged, this,
[this](SessionState state, const QString&) { m_lastState = state; });
connect(m_backend.get(), &SessionBackend::connectionError, this,
[this](const QString& display, const QString& raw) {
m_lastErrorDisplay = display;
m_lastErrorRaw = raw;
});
connect(m_backend.get(), &SessionBackend::frameUpdated, this, [this](const QImage& frame) {
m_lastFrame = frame;
m_gotFrame = true;
});
}
void TestVncSessionBackend::cleanup()
{
if (m_backend) {
m_backend->disconnectSession();
}
m_backend.reset();
m_server.reset();
}
void TestVncSessionBackend::connectsWithNoAuthRfb38()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
m_server->received.clear();
switch (m_server->nextStep()) {
case 0: { // client's version reply
QByteArray securityTypes;
securityTypes.append(char(1)); // count = 1
securityTypes.append(char(1)); // type 1 = None
m_server->sendWhenConnected(securityTypes);
break;
}
case 1: // client's security-type selection (byte value 1)
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2: { // ClientInit (shared-flag byte)
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(4)); // width = 4
serverInit.append(char(0)); serverInit.append(char(2)); // height = 2
serverInit.append(QByteArray(16, char(0))); // pixel format (ignored by client)
serverInit.append(QByteArray(4, char(0))); // name length = 0
m_server->sendWhenConnected(serverInit);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_COMPARE(m_lastState, SessionState::Connected);
}
void TestVncSessionBackend::connectsWithVncAuthenticationRfb38()
{
const QString password = QStringLiteral("secret1");
const QByteArray challenge(16, char(0x11));
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, &challenge]() {
m_server->received.clear();
switch (m_server->nextStep()) {
case 0: { // version reply
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(2)); // type 2 = VNC Authentication
m_server->sendWhenConnected(securityTypes);
break;
}
case 1: // security-type selection
m_server->sendWhenConnected(challenge);
break;
case 2: // 16-byte DES response (content itself checked separately below)
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 3: { // ClientInit
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(2));
serverInit.append(char(0)); serverInit.append(char(2));
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
m_server->sendWhenConnected(serverInit);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions(password));
QTRY_COMPARE(m_lastState, SessionState::Connected);
// Independently verify the response the fake server actually received
// at step 2 was the correct DES-encrypted challenge -- can't capture it
// mid-script above without complicating the dispatch, so just replay
// the expected computation here for comparison purposes is redundant;
// instead this is implicitly proven by reaching Connected at all, since
// a real VncSessionBackend only proceeds past WaitingSecurityResult
// (here, an explicit `OK`) after sending *some* response and the
// server unconditionally accepts it in this script. The actual byte
// correctness of vncAuthResponse() is covered directly by
// vncAuthResponseIsSixteenBytesAndDeterministic() and the "COW" key
// vector above.
}
void TestVncSessionBackend::authFailureRfb38ReachesFailedState()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
m_server->received.clear();
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(2));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(16, char(0x22))); // challenge
break;
case 2: {
QByteArray result;
result.append(char(0)); result.append(char(0)); result.append(char(0));
result.append(char(1)); // SecurityResult: failed
const QByteArray reason = QByteArray("bad password");
result.append(char(0)); result.append(char(0)); result.append(char(0));
result.append(static_cast<char>(reason.size()));
result.append(reason);
m_server->sendWhenConnected(result);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions(QStringLiteral("wrong")));
QTRY_COMPARE(m_lastState, SessionState::Failed);
QVERIFY(m_lastErrorDisplay.contains(QStringLiteral("bad password")));
}
void TestVncSessionBackend::unsupportedSecurityTypeReachesFailedState()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
m_server->received.clear();
if (m_server->nextStep() == 0) {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(19)); // VeNCrypt (TLS) -- unsupported here
m_server->sendWhenConnected(securityTypes);
}
});
m_backend->connectSession(makeOptions());
QTRY_COMPARE(m_lastState, SessionState::Failed);
QVERIFY(m_lastErrorDisplay.contains(QStringLiteral("doesn't support")));
}
void TestVncSessionBackend::rfb33ServerWithNoAuthConnectsWithoutSecurityResult()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.003\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
m_server->received.clear();
switch (m_server->nextStep()) {
case 0: { // version reply
QByteArray securityType(4, char(0));
securityType[3] = char(1); // type 1 = None, sent directly (3.3 style)
m_server->sendWhenConnected(securityType);
break;
}
case 1: { // ClientInit arrives directly -- 3.3 has no SecurityResult at all
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
m_server->sendWhenConnected(serverInit);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_COMPARE(m_lastState, SessionState::Connected);
}
void TestVncSessionBackend::rawFramebufferUpdateProducesExpectedPixels()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
m_server->received.clear();
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2: { // ClientInit
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(2)); // width = 2
serverInit.append(char(0)); serverInit.append(char(1)); // height = 1
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
m_server->sendWhenConnected(serverInit);
break;
}
case 3: {
// First FramebufferUpdateRequest -- reply with a single Raw
// rectangle covering the whole 2x1 framebuffer: one red pixel,
// one green pixel (as bytes matching the requested 32bpp
// little-endian R@16/G@8/B@0 format: B,G,R,pad per pixel).
QByteArray update;
update.append(char(0)); // message-type: FramebufferUpdate
update.append(char(0)); // padding
update.append(char(0)); update.append(char(1)); // 1 rectangle
update.append(char(0)); update.append(char(0)); // x = 0
update.append(char(0)); update.append(char(0)); // y = 0
update.append(char(0)); update.append(char(2)); // width = 2
update.append(char(0)); update.append(char(1)); // height = 1
update.append(char(0)); update.append(char(0)); update.append(char(0));
update.append(char(0)); // encoding = 0 (Raw)
// pixel 0: red (B=0,G=0,R=255,pad=0)
update.append(char(0)); update.append(char(0)); update.append(char(0xff));
update.append(char(0));
// pixel 1: green (B=0,G=255,R=0,pad=0)
update.append(char(0)); update.append(char(0xff)); update.append(char(0));
update.append(char(0));
m_server->sendWhenConnected(update);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(2, 1));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(255, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(1, 0), QColor(0, 255, 0));
}
// Regression guard for the WaitingRectangleHeader dispatch: every encoding
// this backend announces via SetEncodings must have a working decode path.
// Raw is already exercised by rawFramebufferUpdateProducesExpectedPixels();
// this covers CopyRect. Whenever a new encoding is added to
// kAnnouncedEncodings in vnc_session_backend.cpp, a matching test belongs
// here (or nearby) so the announced list and the dispatch switch can never
// silently drift apart.
void TestVncSessionBackend::copyRectEncodingDoesNotFailConnection()
{
int frameCount = 0;
connect(m_backend.get(), &SessionBackend::frameUpdated, this,
[&frameCount](const QImage&) { ++frameCount; });
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
m_server->received.clear();
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2: { // ClientInit
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(2)); // width = 2
serverInit.append(char(0)); serverInit.append(char(2)); // height = 2
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
m_server->sendWhenConnected(serverInit);
break;
}
case 3: { // initial non-incremental request -> seed the framebuffer with Raw
QByteArray update;
update.append(char(0)); update.append(char(0));
update.append(char(0)); update.append(char(1)); // 1 rectangle
update.append(char(0)); update.append(char(0)); // x
update.append(char(0)); update.append(char(0)); // y
update.append(char(0)); update.append(char(2)); // width
update.append(char(0)); update.append(char(2)); // height
update.append(char(0)); update.append(char(0)); update.append(char(0));
update.append(char(0)); // encoding = Raw
update.append(QByteArray(2 * 2 * 4, char(0x11)));
m_server->sendWhenConnected(update);
break;
}
case 4: { // next incremental request -> a CopyRect rectangle
QByteArray update;
update.append(char(0)); update.append(char(0));
update.append(char(0)); update.append(char(1)); // 1 rectangle
update.append(char(0)); update.append(char(0)); // x
update.append(char(0)); update.append(char(0)); // y
update.append(char(0)); update.append(char(2)); // width
update.append(char(0)); update.append(char(2)); // height
update.append(char(0)); update.append(char(0)); update.append(char(0));
update.append(char(1)); // encoding = CopyRect
update.append(char(0)); update.append(char(0)); // src x = 0
update.append(char(0)); update.append(char(0)); // src y = 0
m_server->sendWhenConnected(update);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(frameCount >= 2);
QCOMPARE(m_lastState, SessionState::Connected);
}
void TestVncSessionBackend::unannouncedEncodingFailsConnectionWithClearMessage()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
m_server->received.clear();
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2: { // ClientInit
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(1)); // width = 1
serverInit.append(char(0)); serverInit.append(char(1)); // height = 1
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
m_server->sendWhenConnected(serverInit);
break;
}
case 3: { // a rectangle whose encoding was never announced via SetEncodings
QByteArray update;
update.append(char(0)); update.append(char(0));
update.append(char(0)); update.append(char(1)); // 1 rectangle
update.append(char(0)); update.append(char(0)); // x
update.append(char(0)); update.append(char(0)); // y
update.append(char(0)); update.append(char(1)); // width
update.append(char(0)); update.append(char(1)); // height
update.append(char(0)); update.append(char(0)); update.append(char(0));
update.append(char(99)); // encoding = 99, never announced
m_server->sendWhenConnected(update);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_COMPARE(m_lastState, SessionState::Failed);
QVERIFY(m_lastErrorDisplay.contains(QStringLiteral("non-compliant")));
}
void TestVncSessionBackend::serverCutTextEmitsRemoteClipboardTextChanged()
{
QString lastClipboardText;
connect(m_backend.get(), &SessionBackend::remoteClipboardTextChanged, this,
[&lastClipboardText](const QString& text) { lastClipboardText = text; });
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2: { // ClientInit -- reply with ServerInit, then a ServerCutText message
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
QByteArray cutText;
cutText.append(char(3)); // message-type: ServerCutText
cutText.append(3, char(0)); // padding
const QByteArray text = QByteArray("hello clipboard");
cutText.append(char(0)); cutText.append(char(0)); cutText.append(char(0));
cutText.append(static_cast<char>(text.size())); // length (BE, fits one byte here)
m_server->sendWhenConnected(serverInit + cutText + text);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_COMPARE(lastClipboardText, QStringLiteral("hello clipboard"));
}
void TestVncSessionBackend::setClipboardTextSendsClientCutText()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2: { // ClientInit
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
m_server->sendWhenConnected(serverInit);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_COMPARE(m_lastState, SessionState::Connected);
m_backend->setClipboardText(QStringLiteral("hi"));
// The exact ClientCutText bytes must eventually appear at the tail of
// whatever the fake server received -- checked with endsWith() rather
// than an exact match on the whole buffer, since exactly how the
// preceding handshake/SetEncodings/FramebufferUpdateRequest bytes get
// chunked into TCP reads isn't guaranteed and doesn't matter here.
QByteArray expected;
expected.append(char(6)); // message-type: ClientCutText
expected.append(3, char(0)); // padding
expected.append(char(0)); expected.append(char(0)); expected.append(char(0));
expected.append(char(2)); // length = 2
expected.append("hi");
QTRY_VERIFY(m_server->received.endsWith(expected));
}
void TestVncSessionBackend::cursorPseudoEncodingProducesExpectedImageAndHotspot()
{
QImage lastCursorImage;
QPoint lastHotspot;
bool gotCursor = false;
connect(m_backend.get(), &SessionBackend::cursorImageChanged, this,
[&](const QImage& image, const QPoint& hotspot) {
lastCursorImage = image;
lastHotspot = hotspot;
gotCursor = true;
});
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2: { // ClientInit
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(4)); // width = 4
serverInit.append(char(0)); serverInit.append(char(4)); // height = 4
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
m_server->sendWhenConnected(serverInit);
break;
}
case 3: { // first FramebufferUpdateRequest -> a Cursor pseudo-encoding rectangle
QByteArray update;
update.append(char(0)); update.append(char(0));
update.append(char(0)); update.append(char(1)); // 1 rectangle
update.append(char(0)); update.append(char(1)); // x = hotspot x = 1
update.append(char(0)); update.append(char(1)); // y = hotspot y = 1
update.append(char(0)); update.append(char(2)); // width = 2
update.append(char(0)); update.append(char(1)); // height = 1
update.append(char(0xff)); update.append(char(0xff));
update.append(char(0xff)); update.append(char(0x11)); // encoding = -239 (Cursor)
// pixel 0: red, opaque (byte order B,G,R,pad, per our negotiated format)
update.append(char(0)); update.append(char(0)); update.append(char(0xff));
update.append(char(0));
// pixel 1: blue, will be masked transparent
update.append(char(0xff)); update.append(char(0)); update.append(char(0));
update.append(char(0));
// mask: ceil(2/8)=1 byte/row * height 1 -- bit7=pixel0 opaque, bit6=pixel1 transparent
update.append(char(0x80));
m_server->sendWhenConnected(update);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(gotCursor);
QCOMPARE(lastCursorImage.size(), QSize(2, 1));
QCOMPARE(lastHotspot, QPoint(1, 1));
QCOMPARE(lastCursorImage.pixelColor(0, 0), QColor(255, 0, 0, 255));
QCOMPARE(lastCursorImage.pixelColor(1, 0).alpha(), 0);
}
void TestVncSessionBackend::zeroSizeCursorPseudoEncodingHidesCursor()
{
bool gotHidden = false;
connect(m_backend.get(), &SessionBackend::cursorHidden, this,
[&gotHidden]() { gotHidden = true; });
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2: { // ClientInit
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
m_server->sendWhenConnected(serverInit);
break;
}
case 3: { // a 0x0 Cursor rectangle means "hide the cursor"
QByteArray update;
update.append(char(0)); update.append(char(0));
update.append(char(0)); update.append(char(1)); // 1 rectangle
update.append(char(0)); update.append(char(0)); // x (hotspot, unused here)
update.append(char(0)); update.append(char(0)); // y
update.append(char(0)); update.append(char(0)); // width = 0
update.append(char(0)); update.append(char(0)); // height = 0
update.append(char(0xff)); update.append(char(0xff));
update.append(char(0xff)); update.append(char(0x11)); // encoding = -239 (Cursor)
m_server->sendWhenConnected(update);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(gotHidden);
}
void TestVncSessionBackend::hextileRawTileProducesExpectedPixels()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(2, 2));
break;
case 3: { // a single Raw-subencoding Hextile tile covering the whole 2x2 rectangle
QByteArray tile;
tile.append(char(0x01)); // HextileFlags::kRaw
tile += pixelBytes(255, 0, 0);
tile += pixelBytes(0, 255, 0);
tile += pixelBytes(0, 0, 255);
tile += pixelBytes(255, 255, 255);
m_server->sendWhenConnected(hextileFramebufferUpdate(0, 0, 2, 2, tile));
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(2, 2));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(255, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(1, 0), QColor(0, 255, 0));
QCOMPARE(m_lastFrame.pixelColor(0, 1), QColor(0, 0, 255));
QCOMPARE(m_lastFrame.pixelColor(1, 1), QColor(255, 255, 255));
}
void TestVncSessionBackend::hextileBackgroundOnlyTileFillsSolidColor()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(3, 3));
break;
case 3: { // BackgroundSpecified only, no subrects -- solid fill
QByteArray tile;
tile.append(char(0x02)); // HextileFlags::kBackgroundSpecified
tile += pixelBytes(0, 128, 255);
m_server->sendWhenConnected(hextileFramebufferUpdate(0, 0, 3, 3, tile));
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(3, 3));
for (int y = 0; y < 3; ++y) {
for (int x = 0; x < 3; ++x) {
QCOMPARE(m_lastFrame.pixelColor(x, y), QColor(0, 128, 255));
}
}
}
void TestVncSessionBackend::hextileUncolouredSubrectUsesForeground()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(4, 4));
break;
case 3: {
// Background(black) + Foreground(red) + one uncoloured 2x2
// subrect at tile-local (1,1) -- must render in the foreground
// color, everywhere else in the background color.
QByteArray tile;
tile.append(char(0x02 | 0x04 | 0x08)); // Background|Foreground|AnySubrects
tile += pixelBytes(0, 0, 0); // background
tile += pixelBytes(255, 0, 0); // foreground
tile.append(char(1)); // subrect count
tile.append(char(0x11)); // xy: x=1, y=1
tile.append(char(0x11)); // wh: width=2, height=2
m_server->sendWhenConnected(hextileFramebufferUpdate(0, 0, 4, 4, tile));
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(4, 4));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(0, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(1, 1), QColor(255, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(2, 2), QColor(255, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(3, 3), QColor(0, 0, 0));
}
void TestVncSessionBackend::hextileColouredSubrectUsesOwnColor()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(3, 3));
break;
case 3: {
// No background/foreground specified this tile -- both stay at
// the rectangle-start default (black). One individually-
// coloured 1x1 subrect at tile-local (0,0).
QByteArray tile;
tile.append(char(0x08 | 0x10)); // AnySubrects|SubrectsColoured
tile.append(char(1)); // subrect count
tile += pixelBytes(0, 255, 0); // this subrect's own color
tile.append(char(0x00)); // xy: x=0, y=0
tile.append(char(0x00)); // wh: width=1, height=1
m_server->sendWhenConnected(hextileFramebufferUpdate(0, 0, 3, 3, tile));
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(3, 3));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(0, 255, 0));
QCOMPARE(m_lastFrame.pixelColor(1, 1), QColor(0, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(2, 2), QColor(0, 0, 0));
}
void TestVncSessionBackend::hextileMultiTilePersistsBackgroundAcrossTiles()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(20, 20));
break;
case 3: {
// A 20x20 rectangle tiles as (0,0) 16x16, (16,0) 4x16,
// (0,16) 16x4, (16,16) 4x4 -- only the first tile specifies a
// background color; the other three specify nothing at all
// (subencoding 0x00) and must inherit it.
QByteArray tile1;
tile1.append(char(0x02)); // BackgroundSpecified
tile1 += pixelBytes(0, 0, 255);
QByteArray tile2(1, char(0x00));
QByteArray tile3(1, char(0x00));
QByteArray tile4(1, char(0x00));
m_server->sendWhenConnected(
hextileFramebufferUpdate(0, 0, 20, 20, tile1 + tile2 + tile3 + tile4));
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(20, 20));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(0, 0, 255));
QCOMPARE(m_lastFrame.pixelColor(19, 0), QColor(0, 0, 255));
QCOMPARE(m_lastFrame.pixelColor(0, 19), QColor(0, 0, 255));
QCOMPARE(m_lastFrame.pixelColor(19, 19), QColor(0, 0, 255));
QCOMPARE(m_lastFrame.pixelColor(10, 10), QColor(0, 0, 255));
}
void TestVncSessionBackend::zrleRawTileProducesExpectedPixels()
{
QByteArray tile;
tile.append(char(0)); // Raw
tile += cpixelBytes(255, 0, 0);
tile += cpixelBytes(0, 255, 0);
const QByteArray compressed = zlibCompressWhole(tile);
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, compressed]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(2, 1));
break;
case 3:
m_server->sendWhenConnected(zrleFramebufferUpdate(0, 0, 2, 1, compressed));
break;
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(2, 1));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(255, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(1, 0), QColor(0, 255, 0));
}
void TestVncSessionBackend::zrleSolidTileFillsColor()
{
QByteArray tile;
tile.append(char(1)); // Solid
tile += cpixelBytes(0, 128, 255);
const QByteArray compressed = zlibCompressWhole(tile);
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, compressed]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(2, 2));
break;
case 3:
m_server->sendWhenConnected(zrleFramebufferUpdate(0, 0, 2, 2, compressed));
break;
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(2, 2));
for (int y = 0; y < 2; ++y) {
for (int x = 0; x < 2; ++x) {
QCOMPARE(m_lastFrame.pixelColor(x, y), QColor(0, 128, 255));
}
}
}
void TestVncSessionBackend::zrlePackedPaletteTileProducesExpectedPixels()
{
// 4 pixels, 2-color palette, 1 bit/pixel, packed MSB-first: indices
// [0,1,1,0] -> byte 0b0110_0000.
QByteArray tile;
tile.append(char(2)); // packed palette, paletteSize = 2
tile += cpixelBytes(10, 20, 30); // palette[0]
tile += cpixelBytes(200, 210, 220); // palette[1]
tile.append(char(0x60));
const QByteArray compressed = zlibCompressWhole(tile);
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, compressed]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(4, 1));
break;
case 3:
m_server->sendWhenConnected(zrleFramebufferUpdate(0, 0, 4, 1, compressed));
break;
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(4, 1));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(10, 20, 30));
QCOMPARE(m_lastFrame.pixelColor(1, 0), QColor(200, 210, 220));
QCOMPARE(m_lastFrame.pixelColor(2, 0), QColor(200, 210, 220));
QCOMPARE(m_lastFrame.pixelColor(3, 0), QColor(10, 20, 30));
}
void TestVncSessionBackend::zrlePlainRleTileProducesExpectedPixels()
{
// One run of length 3 (encoded as total=2, single continuation byte
// since 2 != 255) covering the whole 3x1 tile.
QByteArray tile;
tile.append(char(128)); // Plain RLE
tile += cpixelBytes(50, 60, 70);
tile.append(char(2));
const QByteArray compressed = zlibCompressWhole(tile);
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, compressed]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(3, 1));
break;
case 3:
m_server->sendWhenConnected(zrleFramebufferUpdate(0, 0, 3, 1, compressed));
break;
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(3, 1));
for (int x = 0; x < 3; ++x) {
QCOMPARE(m_lastFrame.pixelColor(x, 0), QColor(50, 60, 70));
}
}
void TestVncSessionBackend::zrlePaletteRleTileProducesExpectedPixels()
{
// palette = [colorA, colorB]. Entries: index 0, run length 1 (index
// byte < 128, no run-length byte follows); then index 1, run length 2
// (index byte 0x81 = 128 + 1, followed by a run-length byte encoding
// total=1 -> actual length 2).
QByteArray tile;
tile.append(char(130)); // Palette RLE, paletteSize = 2
tile += cpixelBytes(1, 2, 3); // palette[0] = colorA
tile += cpixelBytes(4, 5, 6); // palette[1] = colorB
tile.append(char(0x00));
tile.append(char(0x81));
tile.append(char(0x01));
const QByteArray compressed = zlibCompressWhole(tile);
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, compressed]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(3, 1));
break;
case 3:
m_server->sendWhenConnected(zrleFramebufferUpdate(0, 0, 3, 1, compressed));
break;
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(3, 1));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(1, 2, 3));
QCOMPARE(m_lastFrame.pixelColor(1, 0), QColor(4, 5, 6));
QCOMPARE(m_lastFrame.pixelColor(2, 0), QColor(4, 5, 6));
}
void TestVncSessionBackend::zrleStreamPersistsAcrossTwoFramebufferUpdates()
{
QByteArray tile1;
tile1.append(char(0)); // Raw
tile1 += cpixelBytes(255, 0, 0);
tile1 += cpixelBytes(0, 255, 0);
QByteArray tile2;
tile2.append(char(1)); // Solid
tile2 += cpixelBytes(10, 20, 30);
// One continuous deflate stream split into two chunks -- the second
// chunk is only decodable by an inflate stream that already consumed
// the first, which is exactly what should happen if
// VncSessionBackend's ZRLE stream persists across separate
// FramebufferUpdate messages instead of being reset each time.
z_stream deflateStream{};
deflateInit(&deflateStream, Z_DEFAULT_COMPRESSION);
const QByteArray compressed1 = zlibCompressChunk(deflateStream, tile1, false);
const QByteArray compressed2 = zlibCompressChunk(deflateStream, tile2, true);
deflateEnd(&deflateStream);
// Capture each frame as it arrives, rather than polling m_lastFrame
// between separately-timed QTRY_VERIFY checks -- the two updates can
// both land before the first check ever gets scheduled to run, so only
// recording state exactly when each signal fires is race-free here.
QVector<QImage> frames;
connect(m_backend.get(), &SessionBackend::frameUpdated, this,
[&frames](const QImage& frame) { frames.append(frame.copy()); });
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this,
[this, compressed1, compressed2]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(2, 1));
break;
case 3: // first (non-incremental) FramebufferUpdateRequest
m_server->sendWhenConnected(zrleFramebufferUpdate(0, 0, 2, 1, compressed1));
break;
case 4: // next (incremental) FramebufferUpdateRequest
m_server->sendWhenConnected(zrleFramebufferUpdate(0, 0, 2, 1, compressed2));
break;
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(frames.size() >= 2);
QCOMPARE(m_lastState, SessionState::Connected);
QCOMPARE(frames.at(0).pixelColor(0, 0), QColor(255, 0, 0));
QCOMPARE(frames.at(0).pixelColor(1, 0), QColor(0, 255, 0));
QCOMPARE(frames.at(1).pixelColor(0, 0), QColor(10, 20, 30));
QCOMPARE(frames.at(1).pixelColor(1, 0), QColor(10, 20, 30));
}
void TestVncSessionBackend::tightFillProducesExpectedPixels()
{
QByteArray tight;
tight.append(char(0x80)); // mode = Fill, no stream-reset bits
tight += cpixelBytes(10, 200, 30);
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, tight]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(3, 3));
break;
case 3:
m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 3, 3, tight));
break;
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(3, 3));
for (int y = 0; y < 3; ++y) {
for (int x = 0; x < 3; ++x) {
QCOMPARE(m_lastFrame.pixelColor(x, y), QColor(10, 200, 30));
}
}
}
void TestVncSessionBackend::tightBasicCopyFilterProducesExpectedPixels()
{
QByteArray filtered;
filtered += cpixelBytes(255, 0, 0);
filtered += cpixelBytes(0, 255, 0);
filtered += cpixelBytes(0, 0, 255);
filtered += cpixelBytes(255, 255, 255);
const QByteArray compressed = zlibCompressWhole(filtered);
QByteArray tight;
tight.append(char(0x00)); // mode = Basic, stream 0, Copy implied
appendTightCompactLength(tight, static_cast<quint32>(compressed.size()));
tight += compressed;
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, tight]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(2, 2));
break;
case 3:
m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 2, 2, tight));
break;
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(2, 2));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(255, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(1, 0), QColor(0, 255, 0));
QCOMPARE(m_lastFrame.pixelColor(0, 1), QColor(0, 0, 255));
QCOMPARE(m_lastFrame.pixelColor(1, 1), QColor(255, 255, 255));
}
void TestVncSessionBackend::tightBasicPaletteFilterProducesExpectedPixels()
{
// 4 pixels, 2-color palette, 1 bit/pixel, packed MSB-first: indices
// [0,1,1,0] -> byte 0b0110_0000 (same packing convention as ZRLE's
// packed palette; with only one row here, continuous vs row-padded
// bit-packing happen to coincide).
QByteArray filtered;
filtered.append(char(0x01)); // paletteSize - 1 = 1 -> paletteSize = 2
filtered += cpixelBytes(10, 20, 30); // palette[0]
filtered += cpixelBytes(200, 210, 220); // palette[1]
filtered.append(char(0x60));
const QByteArray compressed = zlibCompressWhole(filtered);
QByteArray tight;
tight.append(char(0x40)); // mode = Basic, stream 0, explicit filter follows
tight.append(char(1)); // filter id = Palette
appendTightCompactLength(tight, static_cast<quint32>(compressed.size()));
tight += compressed;
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, tight]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(4, 1));
break;
case 3:
m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 4, 1, tight));
break;
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(4, 1));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(10, 20, 30));
QCOMPARE(m_lastFrame.pixelColor(1, 0), QColor(200, 210, 220));
QCOMPARE(m_lastFrame.pixelColor(2, 0), QColor(200, 210, 220));
QCOMPARE(m_lastFrame.pixelColor(3, 0), QColor(10, 20, 30));
}
void TestVncSessionBackend::tightJpegProducesExpectedPixels()
{
// 8x8 (one full JPEG MCU at 1x1 chroma sampling) solid red.
const QByteArray jpegBytes = encodeJpegForTest(8, 8, qRgb(220, 20, 20));
QByteArray tight;
tight.append(char(0x90)); // mode = JPEG
appendTightCompactLength(tight, static_cast<quint32>(jpegBytes.size()));
tight += jpegBytes;
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, tight]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(8, 8));
break;
case 3:
m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 8, 8, tight));
break;
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(8, 8));
// JPEG is lossy even at quality 100 -- assert closeness, not equality.
const QColor decoded = m_lastFrame.pixelColor(4, 4);
QVERIFY(qAbs(decoded.red() - 220) <= 8);
QVERIFY(qAbs(decoded.green() - 20) <= 8);
QVERIFY(qAbs(decoded.blue() - 20) <= 8);
}
void TestVncSessionBackend::tightStreamResetFlagAllowsIndependentDecoding()
{
QByteArray filteredA;
filteredA += cpixelBytes(10, 20, 30);
filteredA += cpixelBytes(10, 20, 30);
filteredA += cpixelBytes(10, 20, 30);
filteredA += cpixelBytes(10, 20, 30);
QByteArray filteredB;
filteredB += cpixelBytes(200, 210, 220);
filteredB += cpixelBytes(200, 210, 220);
filteredB += cpixelBytes(200, 210, 220);
filteredB += cpixelBytes(200, 210, 220);
// Each independently a complete, self-contained zlib stream (its own
// deflateInit + Z_FINISH) -- decoding the second one correctly after
// the first, on the same persistent stream slot, requires that
// stream-reset bit 0 in rectangle B's control byte actually tore down
// and re-initialized stream 0 rather than trying to keep feeding an
// already-finished stream more data.
const QByteArray compressedA = zlibCompressWhole(filteredA);
const QByteArray compressedB = zlibCompressWhole(filteredB);
QByteArray tightA;
tightA.append(char(0x00)); // mode = Basic, stream 0, Copy, no reset
appendTightCompactLength(tightA, static_cast<quint32>(compressedA.size()));
tightA += compressedA;
QByteArray tightB;
tightB.append(char(0x01)); // mode = Basic, stream 0, Copy, reset stream 0
appendTightCompactLength(tightB, static_cast<quint32>(compressedB.size()));
tightB += compressedB;
QVector<QImage> frames;
connect(m_backend.get(), &SessionBackend::frameUpdated, this,
[&frames](const QImage& frame) { frames.append(frame.copy()); });
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, tightA, tightB]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(2, 2));
break;
case 3: // first (non-incremental) FramebufferUpdateRequest
m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 2, 2, tightA));
break;
case 4: // next (incremental) FramebufferUpdateRequest
m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 2, 2, tightB));
break;
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(frames.size() >= 2);
QCOMPARE(m_lastState, SessionState::Connected);
QCOMPARE(frames.at(0).pixelColor(0, 0), QColor(10, 20, 30));
QCOMPARE(frames.at(1).pixelColor(0, 0), QColor(200, 210, 220));
}
void TestVncSessionBackend::appleDhAuthRoundTripDecryptsToOriginalCredentials()
{
ToyDhKeypair serverKeypair = generateToyDhKeypair();
const QString username = QStringLiteral("tester");
const QString password = QStringLiteral("s3cret-pass");
const VncAppleDhAuth::Response response = VncAppleDhAuth::computeResponse(
serverKeypair.generatorBytes, serverKeypair.primeBytes, serverKeypair.publicKeyBytes,
username, password);
QVERIFY(!response.clientPublicKey.isEmpty());
QCOMPARE(response.clientPublicKey.size(), serverKeypair.primeBytes.size());
QCOMPARE(response.encryptedCredentials.size(), 128);
// Derive the shared secret the way the *server* would, using the
// client's returned public key and the server's own private exponent
// -- Diffie-Hellman's commutativity means this must equal whatever
// computeResponse() used internally to derive its AES key.
BIGNUM* clientPub = BN_bin2bn(
reinterpret_cast<const unsigned char*>(response.clientPublicKey.constData()),
response.clientPublicKey.size(), nullptr);
BN_CTX* ctx = BN_CTX_new();
BIGNUM* sharedSecret = BN_new();
BN_mod_exp(sharedSecret, clientPub, serverKeypair.privateExponent, serverKeypair.prime, ctx);
const int secretLen = BN_num_bytes(sharedSecret);
QByteArray secretBytes(secretLen, char(0));
BN_bn2binpad(sharedSecret, reinterpret_cast<unsigned char*>(secretBytes.data()), secretLen);
unsigned char aesKey[16];
EVP_Digest(secretBytes.constData(), static_cast<size_t>(secretBytes.size()), aesKey, nullptr,
EVP_md5(), nullptr);
EVP_CIPHER_CTX* decCtx = EVP_CIPHER_CTX_new();
EVP_DecryptInit_ex(decCtx, EVP_aes_128_ecb(), nullptr, aesKey, nullptr);
EVP_CIPHER_CTX_set_padding(decCtx, 0);
QByteArray plain(response.encryptedCredentials.size() + 16, char(0));
int outLen1 = 0;
EVP_DecryptUpdate(
decCtx, reinterpret_cast<unsigned char*>(plain.data()), &outLen1,
reinterpret_cast<const unsigned char*>(response.encryptedCredentials.constData()),
response.encryptedCredentials.size());
int outLen2 = 0;
EVP_DecryptFinal_ex(decCtx, reinterpret_cast<unsigned char*>(plain.data()) + outLen1, &outLen2);
plain.resize(outLen1 + outLen2);
EVP_CIPHER_CTX_free(decCtx);
QByteArray expected(128, char(0));
const QByteArray userBytes = username.toLatin1().left(64);
const QByteArray passBytes = password.toLatin1().left(64);
std::memcpy(expected.data(), userBytes.constData(), static_cast<size_t>(userBytes.size()));
std::memcpy(expected.data() + 64, passBytes.constData(), static_cast<size_t>(passBytes.size()));
QCOMPARE(plain, expected);
BN_free(clientPub);
BN_free(sharedSecret);
BN_CTX_free(ctx);
BN_free(serverKeypair.privateExponent);
BN_free(serverKeypair.prime);
}
void TestVncSessionBackend::connectsWithAppleDhAuthenticationRfb38()
{
ToyDhKeypair serverKeypair = generateToyDhKeypair();
const QByteArray authMessage = appleDhAuthServerMessage(
serverKeypair.generatorBytes, serverKeypair.primeBytes, serverKeypair.publicKeyBytes);
const int keyLength = serverKeypair.primeBytes.size();
const QString password = QStringLiteral("s3cret-pass");
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, authMessage]() {
switch (m_server->nextStep()) {
case 0: { // version reply
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(30)); // Apple's scheme
m_server->sendWhenConnected(securityTypes);
break;
}
case 1: // security-type selection (byte value 30)
m_server->sendWhenConnected(authMessage);
break;
case 2: // client's response -- accept unconditionally, checked below
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 3: { // ClientInit
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
m_server->sendWhenConnected(serverInit);
break;
}
default:
break;
}
});
// makeVncProfile() sets no username, and Apple DH is the one VNC auth
// method that needs one -- expect the backend to pause and ask,
// exactly like a real profile with a blank username would hit at
// connect time (see issue #21 / SessionBackend::usernameRequested).
QString requestedPrompt;
connect(m_backend.get(), &SessionBackend::usernameRequested, this,
[this, &requestedPrompt](const QString& prompt) {
requestedPrompt = prompt;
m_backend->provideUsername(QStringLiteral("tester"));
});
m_backend->connectSession(makeOptions(password));
QTRY_COMPARE(m_lastState, SessionState::Connected);
QVERIFY(!requestedPrompt.isEmpty());
// Verify wire order: encrypted credentials (128 bytes) MUST come
// before the client's public key, per neatvnc's authoritative
// rfb_apple_dh_client_msg struct layout (this is exactly the ordering
// bug that made real-world testing fail before it was found and
// fixed). Proven by actually deriving the shared secret from the
// trailing public-key bytes -- as the server would -- and decrypting
// the leading 128 bytes with it; a swap would make this decrypt to
// garbage instead of the real credentials.
// 12 bytes for the client's version-reply line, 1 byte for its
// security-type selection, precede the response itself; more bytes
// (ClientInit, SetPixelFormat, ...) may already follow it by the time
// Connected is observed, so slice by offset rather than assuming
// total size.
const int responseOffset = 13;
QVERIFY(m_server->received.size() >= responseOffset + 128 + keyLength);
const QByteArray encryptedCredentials = m_server->received.mid(responseOffset, 128);
const QByteArray clientPublicKeyBytes =
m_server->received.mid(responseOffset + 128, keyLength);
BIGNUM* clientPub = BN_bin2bn(
reinterpret_cast<const unsigned char*>(clientPublicKeyBytes.constData()),
clientPublicKeyBytes.size(), nullptr);
BN_CTX* ctx = BN_CTX_new();
BIGNUM* sharedSecret = BN_new();
BN_mod_exp(sharedSecret, clientPub, serverKeypair.privateExponent, serverKeypair.prime, ctx);
QByteArray secretBytes(keyLength, char(0));
BN_bn2binpad(sharedSecret, reinterpret_cast<unsigned char*>(secretBytes.data()), keyLength);
unsigned char aesKey[16];
EVP_Digest(secretBytes.constData(), static_cast<size_t>(secretBytes.size()), aesKey, nullptr,
EVP_md5(), nullptr);
EVP_CIPHER_CTX* decCtx = EVP_CIPHER_CTX_new();
EVP_DecryptInit_ex(decCtx, EVP_aes_128_ecb(), nullptr, aesKey, nullptr);
EVP_CIPHER_CTX_set_padding(decCtx, 0);
QByteArray plain(encryptedCredentials.size() + 16, char(0));
int outLen1 = 0;
EVP_DecryptUpdate(decCtx, reinterpret_cast<unsigned char*>(plain.data()), &outLen1,
reinterpret_cast<const unsigned char*>(encryptedCredentials.constData()),
encryptedCredentials.size());
int outLen2 = 0;
EVP_DecryptFinal_ex(decCtx, reinterpret_cast<unsigned char*>(plain.data()) + outLen1, &outLen2);
plain.resize(outLen1 + outLen2);
EVP_CIPHER_CTX_free(decCtx);
QByteArray expected(128, char(0));
const QByteArray userBytes = QByteArrayLiteral("tester").left(64);
const QByteArray passBytes = password.toLatin1().left(64);
std::memcpy(expected.data(), userBytes.constData(), static_cast<size_t>(userBytes.size()));
std::memcpy(expected.data() + 64, passBytes.constData(), static_cast<size_t>(passBytes.size()));
QCOMPARE(plain, expected);
BN_free(clientPub);
BN_free(sharedSecret);
BN_CTX_free(ctx);
BN_free(serverKeypair.privateExponent);
BN_free(serverKeypair.prime);
}
void TestVncSessionBackend::appleDhAuthIsPreferredOverVncAuthWhenBothOffered()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
if (m_server->nextStep() == 0) {
m_server->received.clear();
QByteArray securityTypes;
securityTypes.append(char(2)); // count = 2
securityTypes.append(char(2)); // VNC Authentication
securityTypes.append(char(30)); // Apple DH
m_server->sendWhenConnected(securityTypes);
}
});
m_backend->connectSession(makeOptions(QStringLiteral("whatever")));
QTRY_VERIFY(m_server->received.size() >= 1);
QCOMPARE(static_cast<quint8>(m_server->received.at(0)), quint8(30));
}
void TestVncSessionBackend::appleDhAuthAcceptsRealCapturedMacOsServerParameters()
{
// The exact generator/prime/server-public-key bytes captured from a
// real macOS Screen Sharing server's security-type-30 auth message
// (generator=5, a 512-byte/4096-bit RFC 3526 Group 16-family prime,
// and its server public key) -- this is what caught the original
// implementation's wrong assumption that the generator was itself
// length-prefixed like the prime is; it wasn't (it's a literal 2
// bytes), and this pins that fix against the real bytes that exposed
// the bug rather than only against freshly-generated toy data.
const QByteArray captured = QByteArray::fromHex(
"00050200"
"ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a0"
"8798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a6"
"37ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0"
"598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c"
"354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06"
"f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aaac42dad33170d04507a"
"33a85521abdf1cba64ecfb850458dbef0a8aea71575d060c7db3970f85a6e1e4c7abf5ae8cdb0933d71e8"
"c94e04a25619dcee3d2261ad2ee6bf12ffa06d98a0864d87602733ec86a64521f2b18177b200cbbe11757"
"7a615d6c770988c0bad946e208e24fa074e5ab3143db5bfce0fd108e4b82d120a92108011a723c12a787e"
"6d788719a10bdba5b2699c327186af4e23c1a946834b6150bda2583e9ca2ad44ce8dbbbc2db04de8ef92e"
"8efc141fbecaa6287c59474e6bc05d99b2964fa090c3a2233ba186515be7ed1f612970cee2d7afb81bdd7"
"62170481cd0069127d5b05aa993b4ea988d8fddc186ffb7dc90a6c08f4df435c934063199"
"ffffffffffffffff"
"91c11c23a5b54389a05127d7fa94e681b7884667d770cd646fa46583d73e0a5ad09e361c30a0f9a9b765a"
"b291e2b8a13ba0088ba4bdb4a838120e649d49ed78db7fdb3397b907d1a42c0531d16796229d7b3a5a369"
"bfc639771ab1dea093ca7756b2b4a4a98f296478b331538533faead12342d6f1fe92817690ae8e2c51cbf"
"dc96c819e480c2170e6be12149691cb2bab218528c40cc5729b8a303bbe95d77eba288f3c4137d316c5b1"
"d9004c8fe3d034b1284d39e57133972700d1933465b4cc656d2dcbef67dc7eeb464f197bd5187abce304b"
"c972768dcaaa703e287f4f1e375c287b474fe0e3df9897a24bdaf06466976becbc8908fd81211537d1a44"
"e63e705d69b5c90f375fa717c978b27a21384769da67cd97e5134f71aef420dd4b36a2c4e05f80cee043f"
"ab873b478804dc126495d35f0a1df041c4b2bf0926e35ae033f6f6808bdc8a5792b7d380e7c7fc95fc80c"
"fea44c92842ed75757230f590b7dd1d9beadbabb78a96c47f21e65f6ea8566820ff9c59d078870fa1d367"
"2ff0b7f9a3d600f3479232e533bc8b64a3909fd61e070b19ac98d50a7c2a33c885294183e82a4e7777f2a"
"423002219a6094fdf51651d48550f771ede87eb95592eda604b85058cd0d8db9a92670e6e0d74400f7cd4"
"b48c8dfbace5c92c39775a60bb4e59d70878a50b609b15ae4a930ee5eb0494902d44869089349fa2ab343"
"028e");
QCOMPARE(captured.size(), 1028);
const QByteArray generator = captured.left(2);
const int keyLength = (static_cast<quint8>(captured.at(2)) << 8) | static_cast<quint8>(captured.at(3));
QCOMPARE(keyLength, 512);
const QByteArray prime = captured.mid(4, keyLength);
const QByteArray serverPublicKey = captured.mid(4 + keyLength, keyLength);
QCOMPARE(prime.size(), 512);
QCOMPARE(serverPublicKey.size(), 512);
const VncAppleDhAuth::Response response = VncAppleDhAuth::computeResponse(
generator, prime, serverPublicKey, QStringLiteral("tester"), QStringLiteral("s3cret"));
QVERIFY(!response.clientPublicKey.isEmpty());
QCOMPARE(response.clientPublicKey.size(), 512);
QCOMPARE(response.encryptedCredentials.size(), 128);
}
void TestVncSessionBackend::cancellingUsernamePromptFailsConnectionCleanly()
{
ToyDhKeypair serverKeypair = generateToyDhKeypair();
const QByteArray authMessage = appleDhAuthServerMessage(
serverKeypair.generatorBytes, serverKeypair.primeBytes, serverKeypair.publicKeyBytes);
bool gotUsernameRequest = false;
connect(m_backend.get(), &SessionBackend::usernameRequested, this,
[this, &gotUsernameRequest](const QString&) {
gotUsernameRequest = true;
// An empty response mirrors the prompt being cancelled
// (see SessionTab::onBackendUsernameRequested).
m_backend->provideUsername(QString());
});
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, authMessage]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(30)); // Apple's scheme -- no other option offered
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(authMessage);
break;
default:
break;
}
});
// No username in the profile (makeVncProfile() sets none) and no
// other security type to fall back to.
m_backend->connectSession(makeOptions(QStringLiteral("s3cret-pass")));
QTRY_COMPARE(m_lastState, SessionState::Failed);
QVERIFY(gotUsernameRequest);
QVERIFY(m_lastErrorDisplay.contains(QStringLiteral("username"), Qt::CaseInsensitive));
BN_free(serverKeypair.privateExponent);
BN_free(serverKeypair.prime);
}
QTEST_GUILESS_MAIN(TestVncSessionBackend)
#include "test_vnc_session_backend.moc"