Internal
Public Access
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>
135 lines
4.6 KiB
C++
135 lines
4.6 KiB
C++
#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
|