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,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