Internal
Public Access
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>
2238 lines
90 KiB
C++
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"
|