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:
@@ -4,6 +4,7 @@
|
||||
#include "session_backend.h"
|
||||
#include "ssh_session_backend.h"
|
||||
#include "unsupported_session_backend.h"
|
||||
#include "vnc_session_backend.h"
|
||||
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
+55
-7
@@ -3,6 +3,7 @@
|
||||
#include "rdp_display_widget.h"
|
||||
#include "session_backend_factory.h"
|
||||
#include "terminal_view.h"
|
||||
#include "vnc_display_widget.h"
|
||||
|
||||
#include <KodoTerm/KodoTerm.hpp>
|
||||
|
||||
@@ -79,6 +80,7 @@ SessionTab::SessionTab(const Profile& profile,
|
||||
== 0),
|
||||
m_sshTerminal(nullptr),
|
||||
m_rdpDisplay(nullptr),
|
||||
m_vncDisplay(nullptr),
|
||||
m_terminalOutput(nullptr),
|
||||
m_eventLog(nullptr),
|
||||
m_toggleEventsButton(nullptr),
|
||||
@@ -238,6 +240,8 @@ SessionTab::SessionTab(const Profile& profile,
|
||||
[this](const QImage& frame) {
|
||||
if (m_rdpDisplay != nullptr) {
|
||||
m_rdpDisplay->setFrame(frame);
|
||||
} else if (m_vncDisplay != nullptr) {
|
||||
m_vncDisplay->setFrame(frame);
|
||||
}
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
@@ -247,6 +251,8 @@ SessionTab::SessionTab(const Profile& profile,
|
||||
[this](int width, int height) {
|
||||
if (m_rdpDisplay != nullptr) {
|
||||
m_rdpDisplay->setRemoteDesktopSize(width, height);
|
||||
} else if (m_vncDisplay != nullptr) {
|
||||
m_vncDisplay->setRemoteDesktopSize(width, height);
|
||||
}
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
@@ -405,6 +411,12 @@ void SessionTab::clearTerminal()
|
||||
if (m_rdpDisplay != nullptr) {
|
||||
m_rdpDisplay->clearFrame();
|
||||
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) {
|
||||
m_rdpDisplay = new RdpDisplayWidget(this);
|
||||
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 {
|
||||
m_terminalOutput = new TerminalView(this);
|
||||
QFont fallbackFont = defaultTerminalFont();
|
||||
@@ -726,13 +741,7 @@ void SessionTab::setupUi()
|
||||
m_terminalOutput->setFont(fallbackFont);
|
||||
m_terminalOutput->setMinimumHeight(260);
|
||||
m_terminalOutput->setReadOnly(true);
|
||||
if (m_profile.protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0) {
|
||||
m_terminalOutput->setPlaceholderText(
|
||||
QStringLiteral("Embedded VNC session output appears here when the backend is available."));
|
||||
} else {
|
||||
m_terminalOutput->setPlaceholderText(
|
||||
QStringLiteral("Session output appears here."));
|
||||
}
|
||||
m_terminalOutput->setPlaceholderText(QStringLiteral("Session output appears here."));
|
||||
rootLayout->addWidget(m_terminalOutput, 1);
|
||||
}
|
||||
|
||||
@@ -891,6 +900,37 @@ void SessionTab::setupUi()
|
||||
[this](int x, int y, int deltaX, int 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) {
|
||||
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 TerminalView;
|
||||
class RdpDisplayWidget;
|
||||
class VncDisplayWidget;
|
||||
class QToolButton;
|
||||
class QLineEdit;
|
||||
class QComboBox;
|
||||
@@ -109,6 +110,7 @@ private:
|
||||
|
||||
KodoTerm* m_sshTerminal;
|
||||
RdpDisplayWidget* m_rdpDisplay;
|
||||
VncDisplayWidget* m_vncDisplay;
|
||||
TerminalView* m_terminalOutput;
|
||||
QPlainTextEdit* m_eventLog;
|
||||
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
|
||||
Reference in New Issue
Block a user