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:
+8
-2
@@ -12,7 +12,8 @@ set(CMAKE_AUTORCC ON)
|
|||||||
|
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
find_package(Qt6 6.2 REQUIRED COMPONENTS Widgets Sql)
|
find_package(Qt6 6.2 REQUIRED COMPONENTS Widgets Sql Network)
|
||||||
|
find_package(OpenSSL REQUIRED)
|
||||||
|
|
||||||
qt_standard_project_setup()
|
qt_standard_project_setup()
|
||||||
|
|
||||||
@@ -115,6 +116,8 @@ set(ORBITHUB_SOURCES
|
|||||||
src/session_tab.h
|
src/session_tab.h
|
||||||
src/rdp_display_widget.cpp
|
src/rdp_display_widget.cpp
|
||||||
src/rdp_display_widget.h
|
src/rdp_display_widget.h
|
||||||
|
src/vnc_display_widget.cpp
|
||||||
|
src/vnc_display_widget.h
|
||||||
src/terminal_view.cpp
|
src/terminal_view.cpp
|
||||||
src/terminal_view.h
|
src/terminal_view.h
|
||||||
src/session_window.cpp
|
src/session_window.cpp
|
||||||
@@ -123,6 +126,8 @@ set(ORBITHUB_SOURCES
|
|||||||
src/rdp_session_backend.h
|
src/rdp_session_backend.h
|
||||||
src/ssh_session_backend.cpp
|
src/ssh_session_backend.cpp
|
||||||
src/ssh_session_backend.h
|
src/ssh_session_backend.h
|
||||||
|
src/vnc_session_backend.cpp
|
||||||
|
src/vnc_session_backend.h
|
||||||
src/unsupported_session_backend.cpp
|
src/unsupported_session_backend.cpp
|
||||||
src/unsupported_session_backend.h
|
src/unsupported_session_backend.h
|
||||||
)
|
)
|
||||||
@@ -145,8 +150,9 @@ endif()
|
|||||||
|
|
||||||
add_executable(orbithub WIN32 MACOSX_BUNDLE ${ORBITHUB_SOURCES})
|
add_executable(orbithub WIN32 MACOSX_BUNDLE ${ORBITHUB_SOURCES})
|
||||||
|
|
||||||
target_link_libraries(orbithub PRIVATE Qt6::Widgets Qt6::Sql)
|
target_link_libraries(orbithub PRIVATE Qt6::Widgets Qt6::Sql Qt6::Network)
|
||||||
target_link_libraries(orbithub PRIVATE KodoTerm::KodoTerm)
|
target_link_libraries(orbithub PRIVATE KodoTerm::KodoTerm)
|
||||||
|
target_link_libraries(orbithub PRIVATE OpenSSL::Crypto)
|
||||||
target_compile_definitions(orbithub PRIVATE ORBITHUB_VERSION_STRING="${PROJECT_VERSION}")
|
target_compile_definitions(orbithub PRIVATE ORBITHUB_VERSION_STRING="${PROJECT_VERSION}")
|
||||||
if(TARGET freerdp AND TARGET winpr)
|
if(TARGET freerdp AND TARGET winpr)
|
||||||
target_compile_definitions(orbithub PRIVATE ORBITHUB_HAS_FREERDP)
|
target_compile_definitions(orbithub PRIVATE ORBITHUB_HAS_FREERDP)
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ Supported target platforms:
|
|||||||
|
|
||||||
OrbitHub is in active development.
|
OrbitHub is in active development.
|
||||||
|
|
||||||
- Milestones completed: M0-M5, and M7-M9
|
- Milestones completed: M0-M9
|
||||||
- Current milestone: Milestone 10 (v1.0 Stabilization)
|
- Current milestone: Milestone 10 (v1.0 Stabilization)
|
||||||
- Deferred milestone: Milestone 6 (VNC Fully Working)
|
|
||||||
- Latest checkpoint tag: `v2026.9.15`
|
- Latest checkpoint tag: `v2026.9.15`
|
||||||
- VNC implementation milestone (M6) is currently deferred
|
- VNC (M6) covers standard VNC Authentication and no-auth servers; see
|
||||||
|
[docs/PROGRESS.md](docs/PROGRESS.md) for known gaps (Apple Screen
|
||||||
|
Sharing auth, compression encodings, resize, cursor sync, clipboard)
|
||||||
|
|
||||||
Progress and milestone details:
|
Progress and milestone details:
|
||||||
- [docs/PROGRESS.md](docs/PROGRESS.md)
|
- [docs/PROGRESS.md](docs/PROGRESS.md)
|
||||||
@@ -181,4 +182,6 @@ See in-app `Help -> About OrbitHub` for license links and third-party inventory.
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Passwords are requested at connect time and are not stored in the profile database.
|
- Passwords are requested at connect time and are not stored in the profile database.
|
||||||
- This repository currently prioritizes integrated SSH and RDP workflows while VNC implementation is pending.
|
- VNC support covers standard VNC Authentication and no-auth servers (e.g. TigerVNC, x11vnc,
|
||||||
|
TightVNC); it doesn't yet reach macOS's built-in Screen Sharing server, which uses a different
|
||||||
|
authentication scheme (see docs/PROGRESS.md, Milestone 6).
|
||||||
|
|||||||
+42
-8
@@ -102,16 +102,50 @@ Delivered:
|
|||||||
Git:
|
Git:
|
||||||
- Tag: `v0-m5-done`
|
- Tag: `v0-m5-done`
|
||||||
|
|
||||||
## Milestone 6 - VNC Fully Working
|
## Milestone 6 - VNC Working (initial scope)
|
||||||
|
|
||||||
Status: Deferred (temporarily postponed)
|
Status: Completed (initial scope; see gaps below)
|
||||||
|
|
||||||
Planned Scope:
|
Delivered:
|
||||||
- Replace current unsupported VNC path with complete VNC implementation
|
- `VncSessionBackend`: an original RFB (RFC 6143) client implementation
|
||||||
- Deliver usable in-app VNC session behavior aligned to SSH/RDP UX
|
against `QTcpSocket` -- no permissively licensed VNC client library
|
||||||
- Implement VNC connect/disconnect/reconnect lifecycle handling
|
exists to vendor the way FreeRDP was for RDP (LibVNCClient is GPLv2,
|
||||||
- Extend profile/session connect options needed by VNC
|
gtk-vnc is LGPL but GTK-tied), so this is from-scratch protocol code,
|
||||||
- Standardize event log and error mapping behavior with SSH/RDP
|
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
|
||||||
|
- Full connect/disconnect/reconnect lifecycle, RFB protocol-version
|
||||||
|
negotiation (3.3/3.7/3.8 handshake differences handled explicitly),
|
||||||
|
VNC Authentication (DES challenge-response, using OpenSSL's classic DES
|
||||||
|
API) and no-auth security types, Raw + CopyRect framebuffer decoding,
|
||||||
|
keyboard (Qt key -> X11 keysym mapping) and mouse/wheel input forwarding
|
||||||
|
- `VncDisplayWidget` mirroring `RdpDisplayWidget`'s scale-to-fit rendering
|
||||||
|
and input-forwarding shape
|
||||||
|
- 19 unit tests (`tests/test_vnc_session_backend.cpp`): pure-function
|
||||||
|
coverage (DES key prep verified against an independently documented test
|
||||||
|
vector, keysym mapping, socket-error mapping) plus state-machine
|
||||||
|
coverage against a scripted in-process fake RFB server (all three
|
||||||
|
protocol-version handshake shapes, auth success/failure, unsupported
|
||||||
|
security types, pixel-accurate Raw decoding) -- caught and fixed a real
|
||||||
|
re-entrancy bug (`abort()` synchronously re-firing `disconnected()`
|
||||||
|
mid-`failConnection()`, silently overwriting a specific error with a
|
||||||
|
generic one)
|
||||||
|
- 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
|
||||||
|
|
||||||
|
Known gaps (explicit scope decisions, not oversights -- see issue #3 for
|
||||||
|
follow-up tracking):
|
||||||
|
- Apple's Screen Sharing authentication (Diffie-Hellman + AES, security
|
||||||
|
type 30) isn't implemented, so this can't yet reach macOS's built-in VNC
|
||||||
|
server -- only standard VNC Authentication (type 2) and no-auth (type 1)
|
||||||
|
- Raw + CopyRect encodings only -- no Hextile/ZRLE/Tight compression, so
|
||||||
|
bandwidth usage is higher over slow links than a full VNC client
|
||||||
|
- No dynamic resize (connects at the server's native resolution, scaled to
|
||||||
|
fit locally -- the same way `RdpDisplayWidget` already renders
|
||||||
|
regardless of server resolution, so not a UX regression vs. RDP)
|
||||||
|
- No remote cursor shape sync (local default cursor only)
|
||||||
|
- No clipboard sync
|
||||||
|
|
||||||
## Milestone 7 - Cross-Platform Protocol Hardening
|
## Milestone 7 - Cross-Platform Protocol Hardening
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "session_backend.h"
|
#include "session_backend.h"
|
||||||
#include "ssh_session_backend.h"
|
#include "ssh_session_backend.h"
|
||||||
#include "unsupported_session_backend.h"
|
#include "unsupported_session_backend.h"
|
||||||
|
#include "vnc_session_backend.h"
|
||||||
|
|
||||||
std::unique_ptr<SessionBackend> createSessionBackend(const Profile& profile)
|
std::unique_ptr<SessionBackend> createSessionBackend(const Profile& profile)
|
||||||
{
|
{
|
||||||
@@ -13,6 +14,9 @@ std::unique_ptr<SessionBackend> createSessionBackend(const Profile& profile)
|
|||||||
if (profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
|
if (profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
|
||||||
return std::make_unique<RdpSessionBackend>(profile);
|
return std::make_unique<RdpSessionBackend>(profile);
|
||||||
}
|
}
|
||||||
|
if (profile.protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0) {
|
||||||
|
return std::make_unique<VncSessionBackend>(profile);
|
||||||
|
}
|
||||||
|
|
||||||
return std::make_unique<UnsupportedSessionBackend>(profile);
|
return std::make_unique<UnsupportedSessionBackend>(profile);
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-7
@@ -3,6 +3,7 @@
|
|||||||
#include "rdp_display_widget.h"
|
#include "rdp_display_widget.h"
|
||||||
#include "session_backend_factory.h"
|
#include "session_backend_factory.h"
|
||||||
#include "terminal_view.h"
|
#include "terminal_view.h"
|
||||||
|
#include "vnc_display_widget.h"
|
||||||
|
|
||||||
#include <KodoTerm/KodoTerm.hpp>
|
#include <KodoTerm/KodoTerm.hpp>
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ SessionTab::SessionTab(const Profile& profile,
|
|||||||
== 0),
|
== 0),
|
||||||
m_sshTerminal(nullptr),
|
m_sshTerminal(nullptr),
|
||||||
m_rdpDisplay(nullptr),
|
m_rdpDisplay(nullptr),
|
||||||
|
m_vncDisplay(nullptr),
|
||||||
m_terminalOutput(nullptr),
|
m_terminalOutput(nullptr),
|
||||||
m_eventLog(nullptr),
|
m_eventLog(nullptr),
|
||||||
m_toggleEventsButton(nullptr),
|
m_toggleEventsButton(nullptr),
|
||||||
@@ -238,6 +240,8 @@ SessionTab::SessionTab(const Profile& profile,
|
|||||||
[this](const QImage& frame) {
|
[this](const QImage& frame) {
|
||||||
if (m_rdpDisplay != nullptr) {
|
if (m_rdpDisplay != nullptr) {
|
||||||
m_rdpDisplay->setFrame(frame);
|
m_rdpDisplay->setFrame(frame);
|
||||||
|
} else if (m_vncDisplay != nullptr) {
|
||||||
|
m_vncDisplay->setFrame(frame);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
@@ -247,6 +251,8 @@ SessionTab::SessionTab(const Profile& profile,
|
|||||||
[this](int width, int height) {
|
[this](int width, int height) {
|
||||||
if (m_rdpDisplay != nullptr) {
|
if (m_rdpDisplay != nullptr) {
|
||||||
m_rdpDisplay->setRemoteDesktopSize(width, height);
|
m_rdpDisplay->setRemoteDesktopSize(width, height);
|
||||||
|
} else if (m_vncDisplay != nullptr) {
|
||||||
|
m_vncDisplay->setRemoteDesktopSize(width, height);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
@@ -405,6 +411,12 @@ void SessionTab::clearTerminal()
|
|||||||
if (m_rdpDisplay != nullptr) {
|
if (m_rdpDisplay != nullptr) {
|
||||||
m_rdpDisplay->clearFrame();
|
m_rdpDisplay->clearFrame();
|
||||||
m_rdpDisplay->setFocus();
|
m_rdpDisplay->setFocus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_vncDisplay != nullptr) {
|
||||||
|
m_vncDisplay->clearFrame();
|
||||||
|
m_vncDisplay->setFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,6 +729,9 @@ void SessionTab::setupUi()
|
|||||||
} else if (m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
|
} else if (m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
|
||||||
m_rdpDisplay = new RdpDisplayWidget(this);
|
m_rdpDisplay = new RdpDisplayWidget(this);
|
||||||
rootLayout->addWidget(m_rdpDisplay, 1);
|
rootLayout->addWidget(m_rdpDisplay, 1);
|
||||||
|
} else if (m_profile.protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0) {
|
||||||
|
m_vncDisplay = new VncDisplayWidget(this);
|
||||||
|
rootLayout->addWidget(m_vncDisplay, 1);
|
||||||
} else {
|
} else {
|
||||||
m_terminalOutput = new TerminalView(this);
|
m_terminalOutput = new TerminalView(this);
|
||||||
QFont fallbackFont = defaultTerminalFont();
|
QFont fallbackFont = defaultTerminalFont();
|
||||||
@@ -726,13 +741,7 @@ void SessionTab::setupUi()
|
|||||||
m_terminalOutput->setFont(fallbackFont);
|
m_terminalOutput->setFont(fallbackFont);
|
||||||
m_terminalOutput->setMinimumHeight(260);
|
m_terminalOutput->setMinimumHeight(260);
|
||||||
m_terminalOutput->setReadOnly(true);
|
m_terminalOutput->setReadOnly(true);
|
||||||
if (m_profile.protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0) {
|
m_terminalOutput->setPlaceholderText(QStringLiteral("Session output appears here."));
|
||||||
m_terminalOutput->setPlaceholderText(
|
|
||||||
QStringLiteral("Embedded VNC session output appears here when the backend is available."));
|
|
||||||
} else {
|
|
||||||
m_terminalOutput->setPlaceholderText(
|
|
||||||
QStringLiteral("Session output appears here."));
|
|
||||||
}
|
|
||||||
rootLayout->addWidget(m_terminalOutput, 1);
|
rootLayout->addWidget(m_terminalOutput, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -891,6 +900,37 @@ void SessionTab::setupUi()
|
|||||||
[this](int x, int y, int deltaX, int deltaY) {
|
[this](int x, int y, int deltaX, int deltaY) {
|
||||||
emit requestMouseWheelEvent(x, y, deltaX, deltaY);
|
emit requestMouseWheelEvent(x, y, deltaX, deltaY);
|
||||||
});
|
});
|
||||||
|
} else if (m_vncDisplay != nullptr) {
|
||||||
|
connect(m_vncDisplay,
|
||||||
|
&VncDisplayWidget::viewportSizeChanged,
|
||||||
|
this,
|
||||||
|
[this](int width, int height) { emit requestTerminalSize(width, height); });
|
||||||
|
connect(m_vncDisplay,
|
||||||
|
&VncDisplayWidget::displayScaleChanged,
|
||||||
|
this,
|
||||||
|
[this](qreal ratio) { emit requestDisplayScale(ratio); });
|
||||||
|
connect(m_vncDisplay,
|
||||||
|
&VncDisplayWidget::keyInput,
|
||||||
|
this,
|
||||||
|
[this](int key, quint32 nativeScanCode, const QString& text, bool pressed, int modifiers) {
|
||||||
|
emit requestKeyEvent(key, nativeScanCode, text, pressed, modifiers);
|
||||||
|
});
|
||||||
|
connect(m_vncDisplay,
|
||||||
|
&VncDisplayWidget::mouseMoveInput,
|
||||||
|
this,
|
||||||
|
[this](int x, int y) { emit requestMouseMoveEvent(x, y); });
|
||||||
|
connect(m_vncDisplay,
|
||||||
|
&VncDisplayWidget::mouseButtonInput,
|
||||||
|
this,
|
||||||
|
[this](int x, int y, int button, bool pressed) {
|
||||||
|
emit requestMouseButtonEvent(x, y, button, pressed);
|
||||||
|
});
|
||||||
|
connect(m_vncDisplay,
|
||||||
|
&VncDisplayWidget::mouseWheelInput,
|
||||||
|
this,
|
||||||
|
[this](int x, int y, int deltaX, int deltaY) {
|
||||||
|
emit requestMouseWheelEvent(x, y, deltaX, deltaY);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1110,6 +1150,14 @@ void SessionTab::refreshActionButtons()
|
|||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
m_rdpDisplay->setFocus();
|
m_rdpDisplay->setFocus();
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_vncDisplay != nullptr) {
|
||||||
|
m_vncDisplay->setEnabled(isConnected);
|
||||||
|
if (isConnected) {
|
||||||
|
m_vncDisplay->setFocus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class QThread;
|
|||||||
class SessionBackend;
|
class SessionBackend;
|
||||||
class TerminalView;
|
class TerminalView;
|
||||||
class RdpDisplayWidget;
|
class RdpDisplayWidget;
|
||||||
|
class VncDisplayWidget;
|
||||||
class QToolButton;
|
class QToolButton;
|
||||||
class QLineEdit;
|
class QLineEdit;
|
||||||
class QComboBox;
|
class QComboBox;
|
||||||
@@ -109,6 +110,7 @@ private:
|
|||||||
|
|
||||||
KodoTerm* m_sshTerminal;
|
KodoTerm* m_sshTerminal;
|
||||||
RdpDisplayWidget* m_rdpDisplay;
|
RdpDisplayWidget* m_rdpDisplay;
|
||||||
|
VncDisplayWidget* m_vncDisplay;
|
||||||
TerminalView* m_terminalOutput;
|
TerminalView* m_terminalOutput;
|
||||||
QPlainTextEdit* m_eventLog;
|
QPlainTextEdit* m_eventLog;
|
||||||
QToolButton* m_toggleEventsButton;
|
QToolButton* m_toggleEventsButton;
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
#include "vnc_display_widget.h"
|
||||||
|
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QKeyEvent>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QResizeEvent>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QWheelEvent>
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
QSize sanitizeSize(const QSize& size)
|
||||||
|
{
|
||||||
|
return QSize(qMax(1, size.width()), qMax(1, size.height()));
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal sanitizeDevicePixelRatio(qreal ratio)
|
||||||
|
{
|
||||||
|
if (!(ratio > 0.0)) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
return qBound(1.0, ratio, 4.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int kResizeDebounceMs = 150;
|
||||||
|
}
|
||||||
|
|
||||||
|
VncDisplayWidget::VncDisplayWidget(QWidget* parent)
|
||||||
|
: QWidget(parent), m_remoteSize(1280, 720), m_resizeDebounceTimer(new QTimer(this))
|
||||||
|
{
|
||||||
|
setFocusPolicy(Qt::StrongFocus);
|
||||||
|
setMouseTracking(true);
|
||||||
|
setAutoFillBackground(false);
|
||||||
|
setMinimumSize(320, 200);
|
||||||
|
|
||||||
|
m_resizeDebounceTimer->setSingleShot(true);
|
||||||
|
connect(m_resizeDebounceTimer, &QTimer::timeout, this, &VncDisplayWidget::emitViewportGeometry);
|
||||||
|
|
||||||
|
scheduleViewportGeometryEmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VncDisplayWidget::setFrame(const QImage& frame)
|
||||||
|
{
|
||||||
|
if (frame.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_frame = frame;
|
||||||
|
m_remoteSize = sanitizeSize(frame.size());
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VncDisplayWidget::setRemoteDesktopSize(int width, int height)
|
||||||
|
{
|
||||||
|
if (width < 1 || height < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QSize nextSize(width, height);
|
||||||
|
if (m_remoteSize == nextSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_remoteSize = nextSize;
|
||||||
|
// Same race guarded against as RdpDisplayWidget: the next actual frame
|
||||||
|
// arrives asynchronously and isn't guaranteed to already match this
|
||||||
|
// size, so drop the stale one rather than stretch it by the wrong
|
||||||
|
// factor until a correctly-sized frame lands.
|
||||||
|
m_frame = QImage();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VncDisplayWidget::clearFrame()
|
||||||
|
{
|
||||||
|
m_frame = QImage();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VncDisplayWidget::paintEvent(QPaintEvent* event)
|
||||||
|
{
|
||||||
|
Q_UNUSED(event);
|
||||||
|
|
||||||
|
QPainter painter(this);
|
||||||
|
painter.fillRect(rect(), QColor(QStringLiteral("#101214")));
|
||||||
|
|
||||||
|
const QRectF target = renderRect();
|
||||||
|
if (!m_frame.isNull()) {
|
||||||
|
painter.drawImage(target, m_frame);
|
||||||
|
} else {
|
||||||
|
painter.setPen(QColor(QStringLiteral("#b0bec5")));
|
||||||
|
painter.drawText(rect(),
|
||||||
|
Qt::AlignCenter,
|
||||||
|
QStringLiteral("Waiting for remote desktop frame..."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VncDisplayWidget::resizeEvent(QResizeEvent* event)
|
||||||
|
{
|
||||||
|
QWidget::resizeEvent(event);
|
||||||
|
scheduleViewportGeometryEmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VncDisplayWidget::event(QEvent* event)
|
||||||
|
{
|
||||||
|
if (event->type() == QEvent::ScreenChangeInternal) {
|
||||||
|
scheduleViewportGeometryEmit();
|
||||||
|
}
|
||||||
|
return QWidget::event(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VncDisplayWidget::scheduleViewportGeometryEmit()
|
||||||
|
{
|
||||||
|
m_resizeDebounceTimer->start(kResizeDebounceMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VncDisplayWidget::emitViewportGeometry()
|
||||||
|
{
|
||||||
|
const QSize logicalSize = sanitizeSize(this->size());
|
||||||
|
const qreal ratio = sanitizeDevicePixelRatio(this->devicePixelRatioF());
|
||||||
|
const QSize physicalSize(qRound(logicalSize.width() * ratio), qRound(logicalSize.height() * ratio));
|
||||||
|
emit viewportSizeChanged(physicalSize.width(), physicalSize.height());
|
||||||
|
emit displayScaleChanged(ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VncDisplayWidget::keyPressEvent(QKeyEvent* event)
|
||||||
|
{
|
||||||
|
if (event == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit keyInput(event->key(),
|
||||||
|
event->nativeScanCode(),
|
||||||
|
event->text(),
|
||||||
|
true,
|
||||||
|
static_cast<int>(event->modifiers()));
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VncDisplayWidget::keyReleaseEvent(QKeyEvent* event)
|
||||||
|
{
|
||||||
|
if (event == nullptr || event->isAutoRepeat()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit keyInput(event->key(),
|
||||||
|
event->nativeScanCode(),
|
||||||
|
event->text(),
|
||||||
|
false,
|
||||||
|
static_cast<int>(event->modifiers()));
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VncDisplayWidget::focusNextPrevChild(bool next)
|
||||||
|
{
|
||||||
|
Q_UNUSED(next);
|
||||||
|
// Tab/Shift+Tab must reach keyPressEvent() and be forwarded to the
|
||||||
|
// remote session instead of moving focus to the next local widget.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VncDisplayWidget::mousePressEvent(QMouseEvent* event)
|
||||||
|
{
|
||||||
|
if (event == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFocus(Qt::MouseFocusReason);
|
||||||
|
const QPoint mapped = mapToRemote(event->position());
|
||||||
|
emit mouseButtonInput(mapped.x(), mapped.y(), static_cast<int>(event->button()), true);
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VncDisplayWidget::mouseReleaseEvent(QMouseEvent* event)
|
||||||
|
{
|
||||||
|
if (event == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPoint mapped = mapToRemote(event->position());
|
||||||
|
emit mouseButtonInput(mapped.x(), mapped.y(), static_cast<int>(event->button()), false);
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VncDisplayWidget::mouseMoveEvent(QMouseEvent* event)
|
||||||
|
{
|
||||||
|
if (event == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPoint mapped = mapToRemote(event->position());
|
||||||
|
emit mouseMoveInput(mapped.x(), mapped.y());
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VncDisplayWidget::wheelEvent(QWheelEvent* event)
|
||||||
|
{
|
||||||
|
if (event == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPoint mapped = mapToRemote(event->position());
|
||||||
|
const QPoint angle = event->angleDelta();
|
||||||
|
emit mouseWheelInput(mapped.x(), mapped.y(), angle.x(), angle.y());
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
QRectF VncDisplayWidget::renderRect() const
|
||||||
|
{
|
||||||
|
const QSize remote = effectiveRemoteSize();
|
||||||
|
const QRectF area = rect();
|
||||||
|
if (area.isEmpty()) {
|
||||||
|
return QRectF();
|
||||||
|
}
|
||||||
|
|
||||||
|
const qreal scale = qMin(area.width() / remote.width(), area.height() / remote.height());
|
||||||
|
const qreal drawWidth = remote.width() * scale;
|
||||||
|
const qreal drawHeight = remote.height() * scale;
|
||||||
|
const qreal x = area.x() + ((area.width() - drawWidth) * 0.5);
|
||||||
|
const qreal y = area.y() + ((area.height() - drawHeight) * 0.5);
|
||||||
|
return QRectF(x, y, drawWidth, drawHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
QPoint VncDisplayWidget::mapToRemote(const QPointF& pos) const
|
||||||
|
{
|
||||||
|
const QSize remote = effectiveRemoteSize();
|
||||||
|
const QRectF target = renderRect();
|
||||||
|
if (target.isEmpty()) {
|
||||||
|
return QPoint(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const qreal clampedX = qBound(target.left(), pos.x(), target.right());
|
||||||
|
const qreal clampedY = qBound(target.top(), pos.y(), target.bottom());
|
||||||
|
|
||||||
|
const qreal normalizedX = (clampedX - target.left()) / qMax(1.0, target.width());
|
||||||
|
const qreal normalizedY = (clampedY - target.top()) / qMax(1.0, target.height());
|
||||||
|
|
||||||
|
const int remoteX = qBound(0, static_cast<int>(normalizedX * remote.width()), remote.width() - 1);
|
||||||
|
const int remoteY = qBound(0, static_cast<int>(normalizedY * remote.height()), remote.height() - 1);
|
||||||
|
return QPoint(remoteX, remoteY);
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize VncDisplayWidget::effectiveRemoteSize() const
|
||||||
|
{
|
||||||
|
if (m_remoteSize.width() > 0 && m_remoteSize.height() > 0) {
|
||||||
|
return m_remoteSize;
|
||||||
|
}
|
||||||
|
if (!m_frame.isNull()) {
|
||||||
|
return sanitizeSize(m_frame.size());
|
||||||
|
}
|
||||||
|
return QSize(1280, 720);
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#ifndef ORBITHUB_VNC_DISPLAY_WIDGET_H
|
||||||
|
#define ORBITHUB_VNC_DISPLAY_WIDGET_H
|
||||||
|
|
||||||
|
#include <QImage>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class QKeyEvent;
|
||||||
|
class QMouseEvent;
|
||||||
|
class QPaintEvent;
|
||||||
|
class QResizeEvent;
|
||||||
|
class QTimer;
|
||||||
|
class QWheelEvent;
|
||||||
|
|
||||||
|
// Renders a VNC framebuffer and forwards local input, scaled-to-fit --
|
||||||
|
// same shape as RdpDisplayWidget, minus cursor-shape sync (out of scope for
|
||||||
|
// the initial VNC implementation; the system cursor is left alone).
|
||||||
|
class VncDisplayWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit VncDisplayWidget(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
void setFrame(const QImage& frame);
|
||||||
|
void setRemoteDesktopSize(int width, int height);
|
||||||
|
void clearFrame();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void keyInput(int key, quint32 nativeScanCode, const QString& text, bool pressed, int modifiers);
|
||||||
|
void mouseMoveInput(int x, int y);
|
||||||
|
void mouseButtonInput(int x, int y, int button, bool pressed);
|
||||||
|
void mouseWheelInput(int x, int y, int deltaX, int deltaY);
|
||||||
|
void viewportSizeChanged(int width, int height);
|
||||||
|
void displayScaleChanged(qreal devicePixelRatio);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
void resizeEvent(QResizeEvent* event) override;
|
||||||
|
bool event(QEvent* event) override;
|
||||||
|
void keyPressEvent(QKeyEvent* event) override;
|
||||||
|
void keyReleaseEvent(QKeyEvent* event) override;
|
||||||
|
void mousePressEvent(QMouseEvent* event) override;
|
||||||
|
void mouseReleaseEvent(QMouseEvent* event) override;
|
||||||
|
void mouseMoveEvent(QMouseEvent* event) override;
|
||||||
|
void wheelEvent(QWheelEvent* event) override;
|
||||||
|
bool focusNextPrevChild(bool next) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QImage m_frame;
|
||||||
|
QSize m_remoteSize;
|
||||||
|
QTimer* m_resizeDebounceTimer;
|
||||||
|
|
||||||
|
QRectF renderRect() const;
|
||||||
|
QPoint mapToRemote(const QPointF& pos) const;
|
||||||
|
QSize effectiveRemoteSize() const;
|
||||||
|
void emitViewportGeometry();
|
||||||
|
void scheduleViewportGeometryEmit();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,134 @@
|
|||||||
|
#ifndef ORBITHUB_VNC_SESSION_BACKEND_H
|
||||||
|
#define ORBITHUB_VNC_SESSION_BACKEND_H
|
||||||
|
|
||||||
|
#include "session_backend.h"
|
||||||
|
|
||||||
|
#include <QAbstractSocket>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QImage>
|
||||||
|
|
||||||
|
class QTcpSocket;
|
||||||
|
|
||||||
|
// Implements RFB (RFC 6143) directly against QTcpSocket -- there is no
|
||||||
|
// permissively licensed VNC client library to vendor the way FreeRDP was
|
||||||
|
// for RDP (LibVNCClient is GPLv2, gtk-vnc is LGPL but GTK-tied), so this is
|
||||||
|
// an original implementation. Threading follows SshSessionBackend's model
|
||||||
|
// (a QObject moved to its own QThread, driven by Qt's own async socket
|
||||||
|
// signals) rather than RdpSessionBackend's manual worker-thread/blocking
|
||||||
|
// loop, since QTcpSocket is already async -- there's no legacy synchronous
|
||||||
|
// C API to wrangle here.
|
||||||
|
//
|
||||||
|
// Scope (see plan / issue #3 for the full rationale): standard VNC
|
||||||
|
// Authentication (security type 2) and no-auth (type 1) only -- not
|
||||||
|
// Apple's Screen Sharing scheme (type 30). Raw + CopyRect encodings only.
|
||||||
|
// No dynamic resize, no remote cursor shape sync, no clipboard sync.
|
||||||
|
class VncSessionBackend : public SessionBackend
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit VncSessionBackend(const Profile& profile, QObject* parent = nullptr);
|
||||||
|
~VncSessionBackend() override;
|
||||||
|
|
||||||
|
// Pure, state-free helpers exposed as public statics purely so tests
|
||||||
|
// can exercise them without a live connection.
|
||||||
|
static QByteArray vncAuthResponse(const QByteArray& challenge, const QString& password);
|
||||||
|
static QByteArray desKeyFromPassword(const QString& password);
|
||||||
|
static QString mapSocketError(QAbstractSocket::SocketError error, const QString& rawDetail);
|
||||||
|
static quint32 keysymForQtKey(int key, const QString& text);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void connectSession(const SessionConnectOptions& options) override;
|
||||||
|
void disconnectSession() override;
|
||||||
|
void reconnectSession(const SessionConnectOptions& options) override;
|
||||||
|
void sendInput(const QString& input) override;
|
||||||
|
void confirmHostKey(bool trustHost) override;
|
||||||
|
void updateTerminalSize(int columns, int rows) override;
|
||||||
|
void sendKeyEvent(int key,
|
||||||
|
quint32 nativeScanCode,
|
||||||
|
const QString& text,
|
||||||
|
bool pressed,
|
||||||
|
int modifiers) override;
|
||||||
|
void sendMouseMoveEvent(int x, int y) override;
|
||||||
|
void sendMouseButtonEvent(int x, int y, int button, bool pressed) override;
|
||||||
|
void sendMouseWheelEvent(int x, int y, int deltaX, int deltaY) override;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onSocketConnected();
|
||||||
|
void onSocketReadyRead();
|
||||||
|
void onSocketDisconnected();
|
||||||
|
void onSocketErrorOccurred(QAbstractSocket::SocketError error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class RfbState {
|
||||||
|
Idle,
|
||||||
|
WaitingProtocolVersion,
|
||||||
|
WaitingSecurityTypeCount,
|
||||||
|
WaitingSecurityTypeList,
|
||||||
|
WaitingSecurityTypeV33,
|
||||||
|
WaitingSecurityFailureReasonLength,
|
||||||
|
WaitingSecurityFailureReason,
|
||||||
|
WaitingVncAuthChallenge,
|
||||||
|
WaitingSecurityResult,
|
||||||
|
WaitingSecurityResultReasonLength,
|
||||||
|
WaitingSecurityResultReason,
|
||||||
|
WaitingServerInitHeader,
|
||||||
|
WaitingServerName,
|
||||||
|
WaitingServerMessageType,
|
||||||
|
WaitingFramebufferUpdateHeader,
|
||||||
|
WaitingRectangleHeader,
|
||||||
|
WaitingRawPixelData,
|
||||||
|
WaitingCopyRectSource,
|
||||||
|
WaitingSetColourMapHeader,
|
||||||
|
WaitingSetColourMapData,
|
||||||
|
WaitingServerCutTextHeader,
|
||||||
|
WaitingServerCutTextData,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PendingRectangle {
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
qint32 encoding = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
QTcpSocket* m_socket;
|
||||||
|
SessionState m_state;
|
||||||
|
RfbState m_rfbState;
|
||||||
|
QByteArray m_recvBuffer;
|
||||||
|
bool m_userInitiatedDisconnect;
|
||||||
|
bool m_reconnectPending;
|
||||||
|
SessionConnectOptions m_reconnectOptions;
|
||||||
|
SessionConnectOptions m_activeOptions;
|
||||||
|
|
||||||
|
int m_negotiatedMinorVersion;
|
||||||
|
int m_securityTypeCount;
|
||||||
|
QByteArray m_offeredSecurityTypes;
|
||||||
|
quint8 m_chosenSecurityType;
|
||||||
|
quint32 m_pendingLength;
|
||||||
|
|
||||||
|
QImage m_framebuffer;
|
||||||
|
int m_pendingRectanglesRemaining;
|
||||||
|
PendingRectangle m_currentRectangle;
|
||||||
|
|
||||||
|
quint8 m_pointerButtonMask;
|
||||||
|
int m_lastPointerX;
|
||||||
|
int m_lastPointerY;
|
||||||
|
|
||||||
|
void setState(SessionState state, const QString& message);
|
||||||
|
void resetProtocolState();
|
||||||
|
void processReceiveBuffer();
|
||||||
|
bool haveBytes(int count) const;
|
||||||
|
void sendVersionReply();
|
||||||
|
void sendClientInit();
|
||||||
|
void sendSetPixelFormatAndEncodings();
|
||||||
|
void requestFramebufferUpdate(bool incremental);
|
||||||
|
void failConnection(const QString& displayMessage, const QString& rawMessage);
|
||||||
|
void finishHandshakeIntoRunningState();
|
||||||
|
void onRectangleFinished();
|
||||||
|
void sendPointerEvent();
|
||||||
|
void sendWheelClick(quint8 wheelBit);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -26,6 +26,17 @@ target_compile_definitions(test_ssh_session_backend PRIVATE
|
|||||||
)
|
)
|
||||||
add_test(NAME test_ssh_session_backend COMMAND test_ssh_session_backend)
|
add_test(NAME test_ssh_session_backend COMMAND test_ssh_session_backend)
|
||||||
|
|
||||||
|
add_executable(test_vnc_session_backend
|
||||||
|
test_vnc_session_backend.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/vnc_session_backend.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/session_backend.h
|
||||||
|
)
|
||||||
|
target_include_directories(test_vnc_session_backend PRIVATE ${CMAKE_SOURCE_DIR}/src)
|
||||||
|
target_link_libraries(test_vnc_session_backend PRIVATE
|
||||||
|
Qt6::Core Qt6::Gui Qt6::Network Qt6::Test OpenSSL::Crypto
|
||||||
|
)
|
||||||
|
add_test(NAME test_vnc_session_backend COMMAND test_vnc_session_backend)
|
||||||
|
|
||||||
if(TARGET freerdp AND TARGET winpr)
|
if(TARGET freerdp AND TARGET winpr)
|
||||||
add_executable(test_rdp_session_backend
|
add_executable(test_rdp_session_backend
|
||||||
test_rdp_session_backend.cpp
|
test_rdp_session_backend.cpp
|
||||||
|
|||||||
@@ -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