#include "vnc_session_backend.h" #include #include #include 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((value >> 8) & 0xFF)); buf.append(static_cast(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(b)); bytes.append(static_cast(g)); bytes.append(static_cast(r)); bytes.append(char(0)); return bytes; } QByteArray serverInitBytes(int width, int height) { QByteArray serverInit; appendU16(serverInit, static_cast(width)); appendU16(serverInit, static_cast(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(x)); appendU16(update, static_cast(y)); appendU16(update, static_cast(width)); appendU16(update, static_cast(height)); update.append(char(0)); update.append(char(0)); update.append(char(0)); update.append(char(5)); // encoding = Hextile update.append(tileData); return update; } } 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(); private: std::unique_ptr m_server; std::unique_ptr 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(); m_backend = std::make_unique(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(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(30)); // Apple's scheme -- 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(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)); } QTEST_GUILESS_MAIN(TestVncSessionBackend) #include "test_vnc_session_backend.moc"