Internal
Public Access
Add VNC clipboard sync
RFB's ServerCutText/ClientCutText messages are much simpler than RDP's CLIPRDR channel (no format-list/format-request negotiation -- text is just sent directly in both directions), so ServerCutText now emits remoteClipboardTextChanged instead of being discarded, and VncSessionBackend overrides setClipboardText to send ClientCutText immediately. Extends session_tab.cpp's clipboard-sync gate to cover VNC alongside RDP; the QClipboard wiring itself was already protocol-agnostic. Latin-1 only, per RFB's wire format -- no Unicode clipboard extension is in scope. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+3
-2
@@ -77,8 +77,9 @@ SessionTab::SessionTab(const Profile& profile,
|
|||||||
m_terminalFontPointSize(preferences.terminalFontPointSize > 0
|
m_terminalFontPointSize(preferences.terminalFontPointSize > 0
|
||||||
? preferences.terminalFontPointSize
|
? preferences.terminalFontPointSize
|
||||||
: 0),
|
: 0),
|
||||||
m_clipboardSyncSupported(profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive)
|
m_clipboardSyncSupported(
|
||||||
== 0),
|
profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0
|
||||||
|
|| profile.protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0),
|
||||||
m_sshTerminal(nullptr),
|
m_sshTerminal(nullptr),
|
||||||
m_rdpDisplay(nullptr),
|
m_rdpDisplay(nullptr),
|
||||||
m_vncDisplay(nullptr),
|
m_vncDisplay(nullptr),
|
||||||
|
|||||||
@@ -263,6 +263,16 @@ void VncSessionBackend::sendMouseWheelEvent(int x, int y, int deltaX, int deltaY
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VncSessionBackend::setClipboardText(const QString& text)
|
||||||
|
{
|
||||||
|
if (m_socket->state() != QAbstractSocket::ConnectedState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Unlike RDP's CLIPRDR (list -> server request -> response), RFB's
|
||||||
|
// ClientCutText has no negotiation: send it immediately.
|
||||||
|
sendClientCutText(text);
|
||||||
|
}
|
||||||
|
|
||||||
void VncSessionBackend::onSocketConnected()
|
void VncSessionBackend::onSocketConnected()
|
||||||
{
|
{
|
||||||
emit eventLogged(QStringLiteral("TCP connection established; waiting for VNC handshake."));
|
emit eventLogged(QStringLiteral("TCP connection established; waiting for VNC handshake."));
|
||||||
@@ -465,6 +475,19 @@ void VncSessionBackend::sendWheelClick(quint8 wheelBit)
|
|||||||
m_socket->write(release);
|
m_socket->write(release);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VncSessionBackend::sendClientCutText(const QString& text)
|
||||||
|
{
|
||||||
|
// Latin-1 only, matching ServerCutText's decode above -- RFB has no
|
||||||
|
// Unicode clipboard extension in scope here.
|
||||||
|
const QByteArray latin1 = text.toLatin1();
|
||||||
|
QByteArray msg;
|
||||||
|
msg.append(kMsgClientCutText);
|
||||||
|
msg.append(3, char(0)); // padding
|
||||||
|
appendU32BE(msg, static_cast<quint32>(latin1.size()));
|
||||||
|
msg.append(latin1);
|
||||||
|
m_socket->write(msg);
|
||||||
|
}
|
||||||
|
|
||||||
void VncSessionBackend::finishHandshakeIntoRunningState()
|
void VncSessionBackend::finishHandshakeIntoRunningState()
|
||||||
{
|
{
|
||||||
emit remoteDesktopSizeChanged(m_framebuffer.width(), m_framebuffer.height());
|
emit remoteDesktopSizeChanged(m_framebuffer.width(), m_framebuffer.height());
|
||||||
@@ -895,10 +918,13 @@ void VncSessionBackend::processReceiveBuffer()
|
|||||||
if (!haveBytes(static_cast<int>(m_pendingLength))) {
|
if (!haveBytes(static_cast<int>(m_pendingLength))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Clipboard sync is out of scope for this pass; consume and
|
// RFB's ServerCutText is Latin-1 only (RFC 6143 SS7.5.4) -- no
|
||||||
// discard so the byte stream stays in sync.
|
// Unicode extension is implemented, unlike RDP's CLIPRDR.
|
||||||
|
const QString text =
|
||||||
|
QString::fromLatin1(m_recvBuffer.left(static_cast<int>(m_pendingLength)));
|
||||||
m_recvBuffer.remove(0, static_cast<int>(m_pendingLength));
|
m_recvBuffer.remove(0, static_cast<int>(m_pendingLength));
|
||||||
m_rfbState = RfbState::WaitingServerMessageType;
|
m_rfbState = RfbState::WaitingServerMessageType;
|
||||||
|
emit remoteClipboardTextChanged(text);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ class QTcpSocket;
|
|||||||
// Scope (see plan / issue #3 for the full rationale): standard VNC
|
// Scope (see plan / issue #3 for the full rationale): standard VNC
|
||||||
// Authentication (security type 2) and no-auth (type 1) only -- not
|
// Authentication (security type 2) and no-auth (type 1) only -- not
|
||||||
// Apple's Screen Sharing scheme (type 30). Raw + CopyRect encodings only.
|
// Apple's Screen Sharing scheme (type 30). Raw + CopyRect encodings only.
|
||||||
// No dynamic resize, no remote cursor shape sync, no clipboard sync.
|
// No dynamic resize, no remote cursor shape sync. Clipboard sync (Latin-1
|
||||||
|
// only, per RFB's ServerCutText/ClientCutText) is supported.
|
||||||
class VncSessionBackend : public SessionBackend
|
class VncSessionBackend : public SessionBackend
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -52,6 +53,7 @@ public slots:
|
|||||||
void sendMouseMoveEvent(int x, int y) override;
|
void sendMouseMoveEvent(int x, int y) override;
|
||||||
void sendMouseButtonEvent(int x, int y, int button, bool pressed) override;
|
void sendMouseButtonEvent(int x, int y, int button, bool pressed) override;
|
||||||
void sendMouseWheelEvent(int x, int y, int deltaX, int deltaY) override;
|
void sendMouseWheelEvent(int x, int y, int deltaX, int deltaY) override;
|
||||||
|
void setClipboardText(const QString& text) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onSocketConnected();
|
void onSocketConnected();
|
||||||
@@ -129,6 +131,7 @@ private:
|
|||||||
void onRectangleFinished();
|
void onRectangleFinished();
|
||||||
void sendPointerEvent();
|
void sendPointerEvent();
|
||||||
void sendWheelClick(quint8 wheelBit);
|
void sendWheelClick(quint8 wheelBit);
|
||||||
|
void sendClientCutText(const QString& text);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ private slots:
|
|||||||
void rawFramebufferUpdateProducesExpectedPixels();
|
void rawFramebufferUpdateProducesExpectedPixels();
|
||||||
void copyRectEncodingDoesNotFailConnection();
|
void copyRectEncodingDoesNotFailConnection();
|
||||||
void unannouncedEncodingFailsConnectionWithClearMessage();
|
void unannouncedEncodingFailsConnectionWithClearMessage();
|
||||||
|
void serverCutTextEmitsRemoteClipboardTextChanged();
|
||||||
|
void setClipboardTextSendsClientCutText();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<FakeVncServer> m_server;
|
std::unique_ptr<FakeVncServer> m_server;
|
||||||
@@ -620,5 +622,102 @@ void TestVncSessionBackend::unannouncedEncodingFailsConnectionWithClearMessage()
|
|||||||
QVERIFY(m_lastErrorDisplay.contains(QStringLiteral("non-compliant")));
|
QVERIFY(m_lastErrorDisplay.contains(QStringLiteral("non-compliant")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestVncSessionBackend::serverCutTextEmitsRemoteClipboardTextChanged()
|
||||||
|
{
|
||||||
|
QString lastClipboardText;
|
||||||
|
connect(m_backend.get(), &SessionBackend::remoteClipboardTextChanged, this,
|
||||||
|
[&lastClipboardText](const QString& text) { lastClipboardText = text; });
|
||||||
|
|
||||||
|
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
|
||||||
|
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
|
||||||
|
});
|
||||||
|
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
|
||||||
|
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 -- reply with ServerInit, then a ServerCutText message
|
||||||
|
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)));
|
||||||
|
|
||||||
|
QByteArray cutText;
|
||||||
|
cutText.append(char(3)); // message-type: ServerCutText
|
||||||
|
cutText.append(3, char(0)); // padding
|
||||||
|
const QByteArray text = QByteArray("hello clipboard");
|
||||||
|
cutText.append(char(0)); cutText.append(char(0)); cutText.append(char(0));
|
||||||
|
cutText.append(static_cast<char>(text.size())); // length (BE, fits one byte here)
|
||||||
|
|
||||||
|
m_server->sendWhenConnected(serverInit + cutText + text);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
m_backend->connectSession(makeOptions());
|
||||||
|
QTRY_COMPARE(lastClipboardText, QStringLiteral("hello clipboard"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestVncSessionBackend::setClipboardTextSendsClientCutText()
|
||||||
|
{
|
||||||
|
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
|
||||||
|
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
|
||||||
|
});
|
||||||
|
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
|
||||||
|
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(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);
|
||||||
|
|
||||||
|
m_backend->setClipboardText(QStringLiteral("hi"));
|
||||||
|
|
||||||
|
// The exact ClientCutText bytes must eventually appear at the tail of
|
||||||
|
// whatever the fake server received -- checked with endsWith() rather
|
||||||
|
// than an exact match on the whole buffer, since exactly how the
|
||||||
|
// preceding handshake/SetEncodings/FramebufferUpdateRequest bytes get
|
||||||
|
// chunked into TCP reads isn't guaranteed and doesn't matter here.
|
||||||
|
QByteArray expected;
|
||||||
|
expected.append(char(6)); // message-type: ClientCutText
|
||||||
|
expected.append(3, char(0)); // padding
|
||||||
|
expected.append(char(0)); expected.append(char(0)); expected.append(char(0));
|
||||||
|
expected.append(char(2)); // length = 2
|
||||||
|
expected.append("hi");
|
||||||
|
QTRY_VERIFY(m_server->received.endsWith(expected));
|
||||||
|
}
|
||||||
|
|
||||||
QTEST_GUILESS_MAIN(TestVncSessionBackend)
|
QTEST_GUILESS_MAIN(TestVncSessionBackend)
|
||||||
#include "test_vnc_session_backend.moc"
|
#include "test_vnc_session_backend.moc"
|
||||||
|
|||||||
Reference in New Issue
Block a user