Files
orbithub/src/vnc_session_backend.h
T
ksmithandClaude Sonnet 5 6da9dc6ca5 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>
2026-09-15 18:02:49 -06:00

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