Internal
Public Access
Add VNC support (Milestone 6, issue #3)
Implements RFB (RFC 6143) directly against QTcpSocket. No permissively licensed VNC client library exists to vendor the way FreeRDP was for RDP: LibVNCClient is GPLv2, gtk-vnc is LGPL but GTK-tied, and vendoring either would force a licensing decision on the whole (MIT) project. This is from-scratch protocol code instead, threaded like SshSessionBackend (a QObject on its own QThread driven by Qt's own async socket signals) rather than RdpSessionBackend's manual worker-thread/blocking-loop pattern, since QTcpSocket is already async. Scope, matching SessionTab's existing SSH/RDP dispatch pattern (session_backend_factory.cpp, session_tab.cpp's widget construction and signal wiring) and VncDisplayWidget mirroring RdpDisplayWidget's scale-to-fit rendering: - Protocol handshake: RFB 3.3/3.7/3.8 negotiated explicitly (the SecurityResult message only exists in 3.8; pre-3.8 servers signal auth failure by closing the socket, which the disconnect handler accounts for) - VNC Authentication (DES challenge-response, via OpenSSL's classic DES API) and no-auth security types - Raw + CopyRect framebuffer decoding into a persistent QImage, requesting a fixed 32bpp format whose byte layout matches QImage::Format_RGB32 directly (same zero-conversion trick RdpSessionBackend uses for FreeRDP's GDI buffer) - Keyboard (Qt key -> X11 keysym, including the Unicode-beyond-Latin-1 keysym convention) and mouse/wheel input forwarding Explicit non-goals for this pass (see docs/PROGRESS.md for the full list): Apple's Screen Sharing auth (so this can't yet reach macOS's built-in VNC server), compression encodings beyond Raw/CopyRect, dynamic resize, remote cursor shape sync, clipboard sync. 19 unit tests (tests/test_vnc_session_backend.cpp): pure-function coverage (DES key prep verified against an independently documented test vector for password "COW", X11 keysym mapping, socket-error mapping) plus state-machine coverage against a scripted in-process fake RFB server covering all three protocol-version handshake shapes, auth success/ failure, unsupported security types, and pixel-accurate Raw decoding. That harness caught a real re-entrancy bug: QAbstractSocket::abort() synchronously re-emits disconnected() before returning, so failConnection() calling it was silently letting a second, generic disconnected-socket handler overwrite an already-correct, specific error message. Also verified live against a real, independently implemented VNC server (TightVNC on Windows): connect with VNC Authentication, correct framebuffer dimensions and pixel data, clean disconnect, reconnect. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,495 @@
|
||||
#include "vnc_session_backend.h"
|
||||
|
||||
#include <QTcpServer>
|
||||
#include <QTcpSocket>
|
||||
#include <QTest>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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(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));
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(TestVncSessionBackend)
|
||||
#include "test_vnc_session_backend.moc"
|
||||
Reference in New Issue
Block a user