Files
orbithub/src/rdp_session_backend.cpp
T
ksmithandClaude Sonnet 5 0f821feeed Fix RDP/VNC clipboard sync randomly stopping
Local copies were de-duplicated against the last synced text with no
expiry, so one lost send made re-copying the same text a no-op; every
open tab listened to the system clipboard, letting background sessions
overwrite it and forwarding one tab's update to all others; and RDP
never sent the initial Format List MS-RDPECLIP requires after Monitor
Ready, which the server can re-send mid-session (e.g. rdpclip.exe
restart), leaving sync dead until reconnect. Announce failures are now
logged to the event log instead of silently dropped.

Bump version to v2026.9.26.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 14:57:48 -06:00

2491 lines
90 KiB
C++

#include "rdp_session_backend.h"
#include <QImage>
#include <QMetaObject>
#include <Qt>
#include <QtGlobal>
#include <cmath>
#include <cstring>
#include <mutex>
#include <new>
#include <utility>
#ifdef ORBITHUB_HAS_FREERDP
#include <freerdp/channels/channels.h>
#include <freerdp/channels/cliprdr.h>
#include <freerdp/channels/drdynvc.h>
#include <freerdp/channels/disp.h>
#include <freerdp/client/cliprdr.h>
#include <freerdp/addin.h>
#include <freerdp/client/channels.h>
#include <freerdp/client/cmdline.h>
#include <freerdp/client/disp.h>
#include <freerdp/codec/color.h>
#include <freerdp/display.h>
#include <freerdp/error.h>
#include <freerdp/event.h>
#include <freerdp/freerdp.h>
#include <freerdp/gdi/gdi.h>
#include <freerdp/graphics.h>
#include <freerdp/input.h>
#include <freerdp/locale/keyboard.h>
#include <freerdp/scancode.h>
#include <freerdp/settings.h>
#include <freerdp/utils/signal.h>
#include <winpr/winsock.h>
#include <winpr/crt.h>
#include <winpr/synch.h>
#include <winpr/user.h>
#endif
namespace {
#ifdef ORBITHUB_HAS_FREERDP
// Mirrors wfreerdp_client_global_init() in FreeRDP's own reference Windows
// client (client/Windows/wf_client.c): every reference client calls
// WSAStartup + freerdp_handle_signals once at startup before connecting.
//
// WSAStartup: on Windows, sockets (getaddrinfo et al.) fail immediately
// with WSANOTINITIALISED until this has been called by *someone* in the
// process; OrbitHub never used Qt Network, so nothing else on Windows was
// calling it, and every connect failed instantly with
// ERRCONNECT_DNS_NAME_NOT_FOUND even for a literal IP address. WinPR
// provides a portable no-op WSAStartup shim on POSIX, so this is safe to
// call unconditionally cross-platform.
//
// freerdp_handle_signals: on Windows it's the only place that initializes
// a global CRITICAL_SECTION used by freerdp_add_signal_cleanup_handler();
// skipping it leaves that lock zero-initialized (invalid on Windows,
// tolerated on POSIX), so the first freerdp_connect() crashed with a
// null-pointer dereference inside EnterCriticalSection.
//
// Guarded so it only ever runs once.
void ensureFreeRdpRuntimeInitialized()
{
static std::once_flag onceFlag;
std::call_once(onceFlag, []() {
WSADATA wsaData;
const int wsaRc = WSAStartup(0x101, &wsaData);
Q_UNUSED(wsaRc);
const int signalRc = freerdp_handle_signals();
Q_UNUSED(signalRc);
});
}
#endif
constexpr int kDefaultDesktopWidth = 1280;
constexpr int kDefaultDesktopHeight = 720;
constexpr int kMinDesktopWidth = 640;
constexpr int kMinDesktopHeight = 360;
constexpr int kMaxDesktopWidth = 8192;
constexpr int kMaxDesktopHeight = 4320;
constexpr DWORD kWaitTimeoutMs = 25;
constexpr DWORD kMaxEventHandles = 64;
constexpr UINT16 kWheelStep = 0x78;
constexpr double kDefaultDpi = 96.0;
constexpr double kMillimetersPerInch = 25.4;
#ifdef ORBITHUB_HAS_FREERDP
bool applyRdpSecurityMode(rdpSettings* settings, const QString& mode)
{
if (settings == nullptr) {
return false;
}
const QString normalized = RdpSessionBackend::normalizedRdpSecurityMode(mode);
BOOL rdp = FALSE;
BOOL tls = FALSE;
BOOL nla = FALSE;
BOOL useRdpLayer = FALSE;
if (normalized == QStringLiteral("NLA")) {
nla = TRUE;
} else if (normalized == QStringLiteral("TLS")) {
tls = TRUE;
} else if (normalized == QStringLiteral("RDP")) {
rdp = TRUE;
useRdpLayer = TRUE;
} else {
rdp = TRUE;
tls = TRUE;
nla = TRUE;
}
return freerdp_settings_set_bool(settings, FreeRDP_AadSecurity, FALSE)
&& freerdp_settings_set_bool(settings, FreeRDP_ExtSecurity, FALSE)
&& freerdp_settings_set_bool(settings, FreeRDP_NegotiateSecurityLayer, TRUE)
&& freerdp_settings_set_bool(settings, FreeRDP_RdpSecurity, rdp)
&& freerdp_settings_set_bool(settings, FreeRDP_TlsSecurity, tls)
&& freerdp_settings_set_bool(settings, FreeRDP_NlaSecurity, nla)
&& freerdp_settings_set_bool(settings, FreeRDP_UseRdpSecurityLayer, useRdpLayer);
}
bool applyRdpPerformanceProfile(rdpSettings* settings, const QString& profile)
{
if (settings == nullptr) {
return false;
}
const QString normalized = RdpSessionBackend::normalizedRdpPerformanceProfile(profile);
UINT32 connectionType = CONNECTION_TYPE_BROADBAND_HIGH;
BOOL networkAutoDetect = FALSE;
if (normalized == QStringLiteral("Best Quality")) {
connectionType = CONNECTION_TYPE_LAN;
} else if (normalized == QStringLiteral("Best Performance")) {
connectionType = CONNECTION_TYPE_MODEM;
} else if (normalized == QStringLiteral("Auto Detect")) {
connectionType = CONNECTION_TYPE_AUTODETECT;
networkAutoDetect = TRUE;
}
return freerdp_set_connection_type(settings, connectionType)
&& freerdp_settings_set_bool(settings, FreeRDP_NetworkAutoDetect, networkAutoDetect)
&& freerdp_settings_set_uint32(settings, FreeRDP_ConnectionType, connectionType)
&& (freerdp_performance_flags_make(settings), true);
}
struct OrbitRdpContext
{
rdpContext context;
RdpSessionBackend* backend;
};
thread_local RdpSessionBackend* g_contextBackend = nullptr;
void orbitOnChannelConnectedEventHandler(void* context, const ChannelConnectedEventArgs* e);
void orbitOnChannelDisconnectedEventHandler(void* context, const ChannelDisconnectedEventArgs* e);
UINT orbitDisplayControlCaps(DispClientContext* context,
UINT32 maxNumMonitors,
UINT32 maxMonitorAreaFactorA,
UINT32 maxMonitorAreaFactorB);
UINT orbitCliprdrMonitorReady(CliprdrClientContext* context,
const CLIPRDR_MONITOR_READY* monitorReady);
UINT orbitCliprdrServerFormatList(CliprdrClientContext* context,
const CLIPRDR_FORMAT_LIST* formatList);
UINT orbitCliprdrServerFormatListResponse(
CliprdrClientContext* context, const CLIPRDR_FORMAT_LIST_RESPONSE* formatListResponse);
UINT orbitCliprdrServerFormatDataRequest(
CliprdrClientContext* context, const CLIPRDR_FORMAT_DATA_REQUEST* formatDataRequest);
UINT orbitCliprdrServerFormatDataResponse(
CliprdrClientContext* context, const CLIPRDR_FORMAT_DATA_RESPONSE* formatDataResponse);
BOOL orbitLoadChannels(freerdp* instance);
bool orbitLoadStaticChannel(rdpChannels* channels,
rdpSettings* settings,
const char* name,
void* data);
bool orbitEnsureStaticChannelProvider(RdpSessionBackend* backend);
RdpSessionBackend* backendFromContext(rdpContext* context)
{
if (context == nullptr) {
return nullptr;
}
auto* orbit = reinterpret_cast<OrbitRdpContext*>(context);
return orbit->backend;
}
BOOL orbitContextNew(freerdp*, rdpContext* context)
{
auto* orbit = reinterpret_cast<OrbitRdpContext*>(context);
orbit->backend = g_contextBackend;
return orbit->backend != nullptr;
}
void orbitContextFree(freerdp*, rdpContext* context)
{
if (context == nullptr) {
return;
}
auto* orbit = reinterpret_cast<OrbitRdpContext*>(context);
orbit->backend = nullptr;
}
BOOL orbitBeginPaint(rdpContext* context)
{
if (context == nullptr || context->gdi == nullptr || context->gdi->primary == nullptr
|| context->gdi->primary->hdc == nullptr || context->gdi->primary->hdc->hwnd == nullptr
|| context->gdi->primary->hdc->hwnd->invalid == nullptr) {
return TRUE;
}
context->gdi->primary->hdc->hwnd->invalid->null = TRUE;
return TRUE;
}
BOOL orbitEndPaint(rdpContext* context)
{
if (context == nullptr || context->gdi == nullptr || context->gdi->primary == nullptr
|| context->gdi->primary->hdc == nullptr || context->gdi->primary->hdc->hwnd == nullptr
|| context->gdi->primary->hdc->hwnd->invalid == nullptr) {
return TRUE;
}
HGDI_WND window = context->gdi->primary->hdc->hwnd;
if (window->invalid->null) {
return TRUE;
}
RdpSessionBackend* backend = backendFromContext(context);
if (backend == nullptr) {
return TRUE;
}
rdpGdi* gdi = context->gdi;
if (gdi->primary_buffer == nullptr || gdi->width <= 0 || gdi->height <= 0 || gdi->stride == 0) {
return TRUE;
}
const QImage wrapped(gdi->primary_buffer,
gdi->width,
gdi->height,
static_cast<qsizetype>(gdi->stride),
QImage::Format_ARGB32);
if (wrapped.isNull()) {
return TRUE;
}
emit backend->frameUpdated(wrapped.copy());
return TRUE;
}
BOOL orbitDesktopResize(rdpContext* context)
{
if (context == nullptr || context->settings == nullptr || context->gdi == nullptr) {
return FALSE;
}
const UINT32 width = freerdp_settings_get_uint32(context->settings, FreeRDP_DesktopWidth);
const UINT32 height = freerdp_settings_get_uint32(context->settings, FreeRDP_DesktopHeight);
if (!gdi_resize(context->gdi, width, height)) {
return FALSE;
}
if (RdpSessionBackend* backend = backendFromContext(context)) {
emit backend->remoteDesktopSizeChanged(static_cast<int>(width), static_cast<int>(height));
}
return TRUE;
}
// Custom rdpPointer subclass: FreeRDP allocates this with calloc() and only
// ever touches the base rdpPointer fields itself, so the trailing fields here
// must stay plain-old-data and be managed manually in New()/Free() (mirrors
// FreeRDP's own X11 client, client/X11/xf_graphics.c).
struct OrbitRdpPointer
{
rdpPointer pointer;
uchar* rgbaData;
UINT32 rgbaWidth;
UINT32 rgbaHeight;
};
BOOL orbitPointerNew(rdpContext* context, rdpPointer* pointer)
{
if (context == nullptr || pointer == nullptr || context->gdi == nullptr) {
return FALSE;
}
auto* orbitPointer = reinterpret_cast<OrbitRdpPointer*>(pointer);
orbitPointer->rgbaData = nullptr;
orbitPointer->rgbaWidth = 0;
orbitPointer->rgbaHeight = 0;
if (pointer->width == 0 || pointer->height == 0) {
return TRUE;
}
const size_t size = static_cast<size_t>(pointer->width) * pointer->height * 4;
auto* buffer = new (std::nothrow) uchar[size];
if (buffer == nullptr) {
return FALSE;
}
if (!freerdp_image_copy_from_pointer_data(buffer,
PIXEL_FORMAT_BGRA32,
0,
0,
0,
pointer->width,
pointer->height,
pointer->xorMaskData,
pointer->lengthXorMask,
pointer->andMaskData,
pointer->lengthAndMask,
pointer->xorBpp,
&context->gdi->palette)) {
delete[] buffer;
return FALSE;
}
orbitPointer->rgbaData = buffer;
orbitPointer->rgbaWidth = pointer->width;
orbitPointer->rgbaHeight = pointer->height;
return TRUE;
}
void orbitPointerFree(rdpContext* context, rdpPointer* pointer)
{
Q_UNUSED(context);
if (pointer == nullptr) {
return;
}
auto* orbitPointer = reinterpret_cast<OrbitRdpPointer*>(pointer);
delete[] orbitPointer->rgbaData;
orbitPointer->rgbaData = nullptr;
}
BOOL orbitPointerSet(rdpContext* context, rdpPointer* pointer)
{
if (context == nullptr || pointer == nullptr) {
return FALSE;
}
RdpSessionBackend* backend = backendFromContext(context);
if (backend == nullptr) {
return TRUE;
}
auto* orbitPointer = reinterpret_cast<OrbitRdpPointer*>(pointer);
if (orbitPointer->rgbaData == nullptr || orbitPointer->rgbaWidth == 0
|| orbitPointer->rgbaHeight == 0) {
return TRUE;
}
const QImage image(orbitPointer->rgbaData,
static_cast<int>(orbitPointer->rgbaWidth),
static_cast<int>(orbitPointer->rgbaHeight),
QImage::Format_ARGB32);
emit backend->cursorImageChanged(
image.copy(),
QPoint(static_cast<int>(pointer->xPos), static_cast<int>(pointer->yPos)));
return TRUE;
}
BOOL orbitPointerSetNull(rdpContext* context)
{
if (RdpSessionBackend* backend = backendFromContext(context)) {
emit backend->cursorHidden();
}
return TRUE;
}
BOOL orbitPointerSetDefault(rdpContext* context)
{
if (RdpSessionBackend* backend = backendFromContext(context)) {
emit backend->cursorReset();
}
return TRUE;
}
BOOL orbitPreConnect(freerdp* instance)
{
if (instance == nullptr || instance->context == nullptr || instance->context->settings == nullptr) {
return FALSE;
}
rdpSettings* settings = instance->context->settings;
// FALSE gives the verify callbacks a short SHA256 fingerprint string
// (e.g. "ab:cd:12:...") instead of the full PEM certificate blob, which
// is what actually belongs in a user-facing changed-certificate message.
freerdp_settings_set_bool(settings, FreeRDP_CertificateCallbackPreferPEM, FALSE);
freerdp_settings_set_bool(settings, FreeRDP_DesktopResize, TRUE);
// Trust servers on first connection (matching SSH's "accept-new" known-hosts
// policy) and persist that trust via FreeRDP's own certificate store, but
// actually verify it on every subsequent connection: IgnoreCertificate
// previously bypassed verification entirely, so a changed certificate
// (e.g. an active MITM) was silently accepted, indistinguishable from a
// legitimate server. See orbitVerifyChangedCertificateEx below.
freerdp_settings_set_bool(settings, FreeRDP_IgnoreCertificate, FALSE);
freerdp_settings_set_bool(settings, FreeRDP_AutoAcceptCertificate, TRUE);
return TRUE;
}
BOOL orbitPostConnect(freerdp* instance)
{
if (instance == nullptr || instance->context == nullptr || instance->context->settings == nullptr) {
return FALSE;
}
if (!gdi_init(instance, PIXEL_FORMAT_BGRA32)) {
return FALSE;
}
rdpContext* context = instance->context;
context->update->BeginPaint = orbitBeginPaint;
context->update->EndPaint = orbitEndPaint;
context->update->DesktopResize = orbitDesktopResize;
if (context->graphics != nullptr) {
rdpPointer pointerCallbacks = {};
pointerCallbacks.size = sizeof(OrbitRdpPointer);
pointerCallbacks.New = orbitPointerNew;
pointerCallbacks.Free = orbitPointerFree;
pointerCallbacks.Set = orbitPointerSet;
pointerCallbacks.SetNull = orbitPointerSetNull;
pointerCallbacks.SetDefault = orbitPointerSetDefault;
graphics_register_pointer(context->graphics, &pointerCallbacks);
}
if (RdpSessionBackend* backend = backendFromContext(context)) {
const int width = static_cast<int>(
freerdp_settings_get_uint32(context->settings, FreeRDP_DesktopWidth));
const int height = static_cast<int>(
freerdp_settings_get_uint32(context->settings, FreeRDP_DesktopHeight));
emit backend->remoteDesktopSizeChanged(width, height);
}
return TRUE;
}
void orbitPostDisconnect(freerdp* instance)
{
if (instance != nullptr) {
if (instance->context != nullptr) {
if (RdpSessionBackend* backend = backendFromContext(instance->context)) {
backend->onChannelDisconnectedEvent(DISP_DVC_CHANNEL_NAME, nullptr);
backend->onChannelDisconnectedEvent(CLIPRDR_SVC_CHANNEL_NAME, nullptr);
}
PubSub_UnsubscribeChannelConnected(instance->context->pubSub,
orbitOnChannelConnectedEventHandler);
PubSub_UnsubscribeChannelDisconnected(instance->context->pubSub,
orbitOnChannelDisconnectedEventHandler);
}
gdi_free(instance);
}
}
void orbitOnChannelConnectedEventHandler(void* context, const ChannelConnectedEventArgs* e)
{
if (context == nullptr || e == nullptr) {
return;
}
if (RdpSessionBackend* backend = backendFromContext(reinterpret_cast<rdpContext*>(context))) {
backend->onChannelConnectedEvent(e->name, e->pInterface);
}
}
void orbitOnChannelDisconnectedEventHandler(void* context, const ChannelDisconnectedEventArgs* e)
{
if (context == nullptr || e == nullptr) {
return;
}
if (RdpSessionBackend* backend = backendFromContext(reinterpret_cast<rdpContext*>(context))) {
backend->onChannelDisconnectedEvent(e->name, e->pInterface);
}
}
UINT orbitDisplayControlCaps(DispClientContext* context,
UINT32 maxNumMonitors,
UINT32 maxMonitorAreaFactorA,
UINT32 maxMonitorAreaFactorB)
{
if (context == nullptr || context->custom == nullptr) {
return CHANNEL_RC_OK;
}
auto* backend = reinterpret_cast<RdpSessionBackend*>(context->custom);
backend->onDisplayControlCaps(maxNumMonitors, maxMonitorAreaFactorA, maxMonitorAreaFactorB);
return CHANNEL_RC_OK;
}
UINT orbitCliprdrMonitorReady(CliprdrClientContext* context,
const CLIPRDR_MONITOR_READY* monitorReady)
{
Q_UNUSED(monitorReady);
if (context == nullptr || context->custom == nullptr) {
return CHANNEL_RC_OK;
}
auto* backend = reinterpret_cast<RdpSessionBackend*>(context->custom);
backend->onCliprdrMonitorReady();
return CHANNEL_RC_OK;
}
UINT orbitCliprdrServerFormatList(CliprdrClientContext* context,
const CLIPRDR_FORMAT_LIST* formatList)
{
if (context == nullptr || context->custom == nullptr || formatList == nullptr) {
return CHANNEL_RC_OK;
}
bool hasUnicodeText = false;
for (UINT32 i = 0; i < formatList->numFormats; ++i) {
if (formatList->formats[i].formatId == CF_UNICODETEXT) {
hasUnicodeText = true;
break;
}
}
auto* backend = reinterpret_cast<RdpSessionBackend*>(context->custom);
backend->onCliprdrServerFormatList(hasUnicodeText);
return CHANNEL_RC_OK;
}
UINT orbitCliprdrServerFormatListResponse(
CliprdrClientContext* context, const CLIPRDR_FORMAT_LIST_RESPONSE* formatListResponse)
{
Q_UNUSED(context);
Q_UNUSED(formatListResponse);
return CHANNEL_RC_OK;
}
UINT orbitCliprdrServerFormatDataRequest(
CliprdrClientContext* context, const CLIPRDR_FORMAT_DATA_REQUEST* formatDataRequest)
{
if (context == nullptr || context->custom == nullptr || formatDataRequest == nullptr) {
return CHANNEL_RC_OK;
}
auto* backend = reinterpret_cast<RdpSessionBackend*>(context->custom);
backend->onCliprdrServerFormatDataRequest(formatDataRequest->requestedFormatId);
return CHANNEL_RC_OK;
}
UINT orbitCliprdrServerFormatDataResponse(
CliprdrClientContext* context, const CLIPRDR_FORMAT_DATA_RESPONSE* formatDataResponse)
{
if (context == nullptr || context->custom == nullptr || formatDataResponse == nullptr) {
return CHANNEL_RC_OK;
}
auto* backend = reinterpret_cast<RdpSessionBackend*>(context->custom);
const bool success = (formatDataResponse->common.msgFlags & CB_RESPONSE_OK) != 0;
backend->onCliprdrServerFormatDataResponse(
success,
reinterpret_cast<const uint8_t*>(formatDataResponse->requestedFormatData),
formatDataResponse->common.dataLen);
return CHANNEL_RC_OK;
}
BOOL orbitLoadChannels(freerdp* instance)
{
if (instance == nullptr || instance->context == nullptr || instance->context->settings == nullptr
|| instance->context->channels == nullptr) {
return TRUE;
}
rdpSettings* settings = instance->context->settings;
rdpChannels* channels = instance->context->channels;
RdpSessionBackend* backend = backendFromContext(instance->context);
if (!orbitEnsureStaticChannelProvider(backend)) {
if (backend != nullptr) {
emit backend->eventLogged(QStringLiteral(
"RDP warning: static channel provider unavailable; dynamic resize may not work."));
}
return TRUE;
}
const char* const dynamicChannels[] = {DISP_CHANNEL_NAME};
if (!freerdp_client_add_dynamic_channel(settings, 1, dynamicChannels)) {
if (backend != nullptr) {
emit backend->eventLogged(
QStringLiteral("RDP warning: failed to register dynamic '%1' channel.")
.arg(QString::fromUtf8(DISP_CHANNEL_NAME)));
}
return TRUE;
}
if (!freerdp_settings_set_bool(settings, FreeRDP_SupportDynamicChannels, TRUE)) {
if (backend != nullptr) {
emit backend->eventLogged(
QStringLiteral("RDP warning: failed to enable dynamic channels in settings."));
}
return TRUE;
}
if (!orbitLoadStaticChannel(channels, settings, DRDYNVC_SVC_CHANNEL_NAME, settings)) {
if (backend != nullptr) {
emit backend->eventLogged(
QStringLiteral("RDP warning: failed to load static '%1' channel.")
.arg(QString::fromUtf8(DRDYNVC_SVC_CHANNEL_NAME)));
}
return TRUE;
}
if (!orbitLoadStaticChannel(channels, settings, CLIPRDR_SVC_CHANNEL_NAME, settings)) {
if (backend != nullptr) {
emit backend->eventLogged(
QStringLiteral("RDP warning: failed to load static '%1' channel; clipboard sync unavailable.")
.arg(QString::fromUtf8(CLIPRDR_SVC_CHANNEL_NAME)));
}
}
if (backend != nullptr) {
emit backend->eventLogged(QStringLiteral("RDP channel loader: display-control channels initialized."));
}
return TRUE;
}
bool orbitLoadStaticChannel(rdpChannels* channels,
rdpSettings* settings,
const char* name,
void* data)
{
if (channels == nullptr || settings == nullptr || name == nullptr) {
return false;
}
PVIRTUALCHANNELENTRY channelEntry = freerdp_load_channel_addin_entry(
name, nullptr, nullptr, FREERDP_ADDIN_CHANNEL_STATIC | FREERDP_ADDIN_CHANNEL_ENTRYEX);
PVIRTUALCHANNELENTRYEX channelEntryEx =
WINPR_FUNC_PTR_CAST(channelEntry, PVIRTUALCHANNELENTRYEX);
if (channelEntryEx != nullptr) {
return freerdp_channels_client_load_ex(channels, settings, channelEntryEx, data) == 0;
}
channelEntry =
freerdp_load_channel_addin_entry(name, nullptr, nullptr, FREERDP_ADDIN_CHANNEL_STATIC);
if (channelEntry != nullptr) {
return freerdp_channels_client_load(channels, settings, channelEntry, data) == 0;
}
return false;
}
bool orbitEnsureStaticChannelProvider(RdpSessionBackend* backend)
{
FREERDP_LOAD_CHANNEL_ADDIN_ENTRY_FN provider = freerdp_get_current_addin_provider();
if (provider == nullptr) {
const int rc = freerdp_register_addin_provider(freerdp_channels_load_static_addin_entry, 0);
if (rc != CHANNEL_RC_OK) {
if (backend != nullptr) {
emit backend->eventLogged(
QStringLiteral("RDP warning: failed to register static channel provider (rc=%1).")
.arg(rc));
}
return false;
}
provider = freerdp_get_current_addin_provider();
}
if (provider == nullptr) {
if (backend != nullptr) {
emit backend->eventLogged(
QStringLiteral("RDP warning: static channel provider registration did not persist."));
}
return false;
}
return true;
}
DWORD orbitVerifyCertificateEx(freerdp* instance,
const char* host,
UINT16 port,
const char*,
const char*,
const char*,
const char*,
DWORD)
{
// With FreeRDP_AutoAcceptCertificate set, FreeRDP accepts and stores a
// first-seen certificate itself without calling this callback at all;
// it is kept as a defensive fallback for any code path that reaches it.
if (instance != nullptr && instance->context != nullptr) {
if (RdpSessionBackend* backend = backendFromContext(instance->context)) {
emit backend->eventLogged(
QStringLiteral("Trusting new server certificate for %1:%2.")
.arg(QString::fromUtf8(host == nullptr ? "" : host))
.arg(port));
}
}
return 1;
}
DWORD orbitVerifyChangedCertificateEx(freerdp* instance,
const char* host,
UINT16 port,
const char*,
const char*,
const char*,
const char* newFingerprint,
const char*,
const char*,
const char* oldFingerprint,
DWORD)
{
// The server's certificate no longer matches the one trusted on a prior
// connection. This is exactly the scenario TLS verification exists to
// catch: either the server legitimately rotated its certificate, or an
// active man-in-the-middle is presenting a different one. Refuse the
// connection by default rather than silently trusting it.
const QString hostStr = QString::fromUtf8(host == nullptr ? "" : host);
const QString message = QStringLiteral(
"Server certificate for %1:%2 has changed since it was last trusted. "
"Connection refused for safety — this could mean the server's "
"certificate was legitimately renewed, or that a different host is "
"impersonating it. Previously trusted fingerprint: %3 — now "
"presented: %4. If this change is expected, remove the stored entry "
"for this host from FreeRDP's certificate store "
"(~/.config/freerdp/server) and reconnect.")
.arg(hostStr)
.arg(port)
.arg(QString::fromUtf8(oldFingerprint == nullptr ? "unknown" : oldFingerprint))
.arg(QString::fromUtf8(newFingerprint == nullptr ? "unknown" : newFingerprint));
// Not logged directly here: recordCertificateRejection() feeds this
// message into the generic connect-failure handling in workerMain(),
// which already reports it through eventLogged/connectionError/setState
// (the same channels every other RDP connection failure uses) — an
// extra direct log call here would just duplicate that.
if (instance != nullptr && instance->context != nullptr) {
if (RdpSessionBackend* backend = backendFromContext(instance->context)) {
backend->recordCertificateRejection(message);
}
}
return 0;
}
const char* authReasonName(rdp_auth_reason reason)
{
switch (reason) {
case AUTH_NLA:
return "NLA";
case AUTH_TLS:
return "TLS";
case AUTH_RDP:
return "RDP";
case GW_AUTH_HTTP:
return "GW_HTTP";
case GW_AUTH_RDG:
return "GW_RDG";
case GW_AUTH_RPC:
return "GW_RPC";
case AUTH_SMARTCARD_PIN:
return "SMARTCARD_PIN";
case AUTH_RDSTLS:
return "RDSTLS";
default:
return "UNKNOWN";
}
}
BOOL orbitAuthenticateEx(freerdp* instance,
char** username,
char** password,
char** domain,
rdp_auth_reason reason)
{
if (instance == nullptr || instance->context == nullptr || instance->context->settings == nullptr
|| username == nullptr || password == nullptr || domain == nullptr) {
return FALSE;
}
rdpSettings* settings = instance->context->settings;
const char* configuredUsername = freerdp_settings_get_string(settings, FreeRDP_Username);
const char* configuredPassword = freerdp_settings_get_string(settings, FreeRDP_Password);
const char* configuredDomain = freerdp_settings_get_string(settings, FreeRDP_Domain);
if (configuredUsername == nullptr || configuredUsername[0] == '\0') {
if (RdpSessionBackend* backend = backendFromContext(instance->context)) {
emit backend->eventLogged(
QStringLiteral("Authentication failed: missing username for %1.")
.arg(QString::fromUtf8(authReasonName(reason))));
}
return FALSE;
}
if (configuredPassword == nullptr || configuredPassword[0] == '\0') {
if (RdpSessionBackend* backend = backendFromContext(instance->context)) {
emit backend->eventLogged(
QStringLiteral("Authentication failed: missing password for %1.")
.arg(QString::fromUtf8(authReasonName(reason))));
}
return FALSE;
}
char* newUsername = _strdup(configuredUsername);
char* newPassword = _strdup(configuredPassword);
char* newDomain = _strdup(configuredDomain == nullptr ? "" : configuredDomain);
if (newUsername == nullptr || newPassword == nullptr || newDomain == nullptr) {
free(newUsername);
free(newPassword);
free(newDomain);
return FALSE;
}
free(*username);
free(*password);
free(*domain);
*username = newUsername;
*password = newPassword;
*domain = newDomain;
if (RdpSessionBackend* backend = backendFromContext(instance->context)) {
emit backend->eventLogged(
QStringLiteral("Authentication credentials supplied for %1.")
.arg(QString::fromUtf8(authReasonName(reason))));
}
return TRUE;
}
}
QString RdpSessionBackend::normalizedRdpSecurityMode(const QString& value)
{
const QString mode = value.trimmed();
if (mode.compare(QStringLiteral("NLA"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("NLA");
}
if (mode.compare(QStringLiteral("TLS"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("TLS");
}
if (mode.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("RDP");
}
return QStringLiteral("Negotiate");
}
// MS-RDPEDISP restricts DesktopScaleFactor/DeviceScaleFactor to exactly
// these three values; FreeRDP's own reference client enforces the same
// set (client/common/cmdline.c, parse_scale_options). Anything else is
// silently ignored by the server, so map the real, continuous
// devicePixelRatio down to the nearest one.
quint32 RdpSessionBackend::nearestFreeRdpScaleValue(qreal ratio)
{
if (ratio <= 1.2) {
return 100;
}
if (ratio <= 1.6) {
return 140;
}
return 180;
}
QString RdpSessionBackend::normalizedRdpPerformanceProfile(const QString& value)
{
const QString profile = value.trimmed();
if (profile.compare(QStringLiteral("Best Quality"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Best Quality");
}
if (profile.compare(QStringLiteral("Best Performance"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Best Performance");
}
if (profile.compare(QStringLiteral("Auto Detect"), Qt::CaseInsensitive) == 0) {
return QStringLiteral("Auto Detect");
}
return QStringLiteral("Balanced");
}
quint32 RdpSessionBackend::scancodeFromNativeScanCode(quint32 nativeScanCode)
{
if (nativeScanCode == 0) {
return RDP_SCANCODE_UNKNOWN;
}
#if defined(Q_OS_LINUX)
// On X11, Qt's nativeScanCode() is the raw X11 KeyCode, not a PC/AT set-1
// scancode. X11 keycode numbering only coincidentally overlaps with PC/AT
// scancodes for some keys and diverges for others (e.g. X11 keycode 0x30
// is the apostrophe/quote key, while PC/AT scancode 0x30 is the B key), so
// treating one as the other silently sends the wrong key. FreeRDP ships an
// authoritative X11-keycode -> RDP-scancode table; use it directly.
// The function is marked deprecated upstream ("implement yourself in
// client") but remains present and correct in this pinned FreeRDP build.
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
const DWORD mapped = freerdp_keyboard_get_rdp_scancode_from_x11_keycode(nativeScanCode);
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic pop
#endif
return mapped != 0 ? mapped : RDP_SCANCODE_UNKNOWN;
#else
quint32 code = 0;
bool extended = false;
if ((nativeScanCode & 0xFF000000u) == 0xE0000000u) {
code = (nativeScanCode >> 16) & 0xFFu;
extended = true;
} else if ((nativeScanCode & 0x00FF0000u) == 0x00E00000u) {
code = (nativeScanCode >> 8) & 0xFFu;
extended = true;
} else if ((nativeScanCode & 0xFF00u) == 0xE000u || (nativeScanCode & 0xFF00u) == 0x0100u) {
code = nativeScanCode & 0xFFu;
extended = true;
} else if (nativeScanCode <= 0xFFu) {
code = nativeScanCode;
} else {
code = nativeScanCode & 0xFFu;
const quint32 prefix = (nativeScanCode >> 8) & 0xFFu;
extended = (prefix == 0xE0u) || (prefix == 0x01u);
}
if (code == 0) {
return RDP_SCANCODE_UNKNOWN;
}
UINT32 scancode = MAKE_RDP_SCANCODE(code, extended ? TRUE : FALSE);
if (scancode == RDP_SCANCODE_NUMLOCK_EXTENDED) {
scancode = RDP_SCANCODE_NUMLOCK;
} else if (scancode == RDP_SCANCODE_RSHIFT_EXTENDED) {
scancode = RDP_SCANCODE_RSHIFT;
}
return scancode;
#endif
}
quint32 RdpSessionBackend::scancodeForQtKey(int key,
Qt::KeyboardModifiers modifiers,
quint32 nativeScanCode)
{
const bool keypad = modifiers.testFlag(Qt::KeypadModifier);
const quint32 nativeScancode = scancodeFromNativeScanCode(nativeScanCode);
switch (key) {
case Qt::Key_Escape:
return RDP_SCANCODE_ESCAPE;
case Qt::Key_Backtab:
case Qt::Key_Tab:
return RDP_SCANCODE_TAB;
case Qt::Key_Backspace:
return RDP_SCANCODE_BACKSPACE;
case Qt::Key_Return:
if (nativeScancode == RDP_SCANCODE_RETURN_KP || keypad) {
return RDP_SCANCODE_RETURN_KP;
}
return RDP_SCANCODE_RETURN;
case Qt::Key_Enter:
return RDP_SCANCODE_RETURN_KP;
case Qt::Key_Insert:
return keypad ? RDP_SCANCODE_NUMPAD0 : RDP_SCANCODE_INSERT;
case Qt::Key_Delete:
return keypad ? RDP_SCANCODE_DECIMAL : RDP_SCANCODE_DELETE;
case Qt::Key_Home:
return keypad ? RDP_SCANCODE_NUMPAD7 : RDP_SCANCODE_HOME;
case Qt::Key_End:
return keypad ? RDP_SCANCODE_NUMPAD1 : RDP_SCANCODE_END;
case Qt::Key_PageUp:
return keypad ? RDP_SCANCODE_NUMPAD9 : RDP_SCANCODE_PRIOR;
case Qt::Key_PageDown:
return keypad ? RDP_SCANCODE_NUMPAD3 : RDP_SCANCODE_NEXT;
case Qt::Key_Left:
return keypad ? RDP_SCANCODE_NUMPAD4 : RDP_SCANCODE_LEFT;
case Qt::Key_Right:
return keypad ? RDP_SCANCODE_NUMPAD6 : RDP_SCANCODE_RIGHT;
case Qt::Key_Up:
return keypad ? RDP_SCANCODE_NUMPAD8 : RDP_SCANCODE_UP;
case Qt::Key_Down:
return keypad ? RDP_SCANCODE_NUMPAD2 : RDP_SCANCODE_DOWN;
case Qt::Key_Space:
return RDP_SCANCODE_SPACE;
case Qt::Key_Control:
if (nativeScancode == RDP_SCANCODE_RCONTROL || nativeScancode == RDP_SCANCODE_LCONTROL) {
return nativeScancode;
}
return RDP_SCANCODE_LCONTROL;
case Qt::Key_Shift:
if (nativeScancode == RDP_SCANCODE_RSHIFT || nativeScancode == RDP_SCANCODE_LSHIFT) {
return nativeScancode;
}
return RDP_SCANCODE_LSHIFT;
case Qt::Key_AltGr:
return RDP_SCANCODE_RMENU;
case Qt::Key_Alt:
if (nativeScancode == RDP_SCANCODE_RMENU || nativeScancode == RDP_SCANCODE_LMENU) {
return nativeScancode;
}
return RDP_SCANCODE_LMENU;
case Qt::Key_Meta:
if (nativeScancode == RDP_SCANCODE_LWIN || nativeScancode == RDP_SCANCODE_RWIN) {
return nativeScancode;
}
return RDP_SCANCODE_LWIN;
case Qt::Key_Menu:
return RDP_SCANCODE_APPS;
case Qt::Key_Print:
return RDP_SCANCODE_PRINTSCREEN;
case Qt::Key_Pause:
return RDP_SCANCODE_PAUSE;
case Qt::Key_CapsLock:
return RDP_SCANCODE_CAPSLOCK;
case Qt::Key_NumLock:
return RDP_SCANCODE_NUMLOCK;
case Qt::Key_ScrollLock:
return RDP_SCANCODE_SCROLLLOCK;
case Qt::Key_Plus:
return keypad ? RDP_SCANCODE_ADD : RDP_SCANCODE_OEM_PLUS;
case Qt::Key_Minus:
return keypad ? RDP_SCANCODE_SUBTRACT : RDP_SCANCODE_OEM_MINUS;
case Qt::Key_Equal:
return RDP_SCANCODE_OEM_PLUS;
case Qt::Key_BracketLeft:
return RDP_SCANCODE_OEM_4;
case Qt::Key_BracketRight:
return RDP_SCANCODE_OEM_6;
case Qt::Key_Backslash:
return RDP_SCANCODE_OEM_5;
case Qt::Key_Semicolon:
return RDP_SCANCODE_OEM_1;
case Qt::Key_Apostrophe:
return RDP_SCANCODE_OEM_7;
case Qt::Key_QuoteLeft:
case Qt::Key_AsciiTilde:
return RDP_SCANCODE_OEM_3;
case Qt::Key_Comma:
return RDP_SCANCODE_OEM_COMMA;
case Qt::Key_Period:
return keypad ? RDP_SCANCODE_DECIMAL : RDP_SCANCODE_OEM_PERIOD;
case Qt::Key_Slash:
return keypad ? RDP_SCANCODE_DIVIDE : RDP_SCANCODE_OEM_2;
case Qt::Key_Asterisk:
return keypad ? RDP_SCANCODE_MULTIPLY : RDP_SCANCODE_UNKNOWN;
case Qt::Key_F1:
return RDP_SCANCODE_F1;
case Qt::Key_F2:
return RDP_SCANCODE_F2;
case Qt::Key_F3:
return RDP_SCANCODE_F3;
case Qt::Key_F4:
return RDP_SCANCODE_F4;
case Qt::Key_F5:
return RDP_SCANCODE_F5;
case Qt::Key_F6:
return RDP_SCANCODE_F6;
case Qt::Key_F7:
return RDP_SCANCODE_F7;
case Qt::Key_F8:
return RDP_SCANCODE_F8;
case Qt::Key_F9:
return RDP_SCANCODE_F9;
case Qt::Key_F10:
return RDP_SCANCODE_F10;
case Qt::Key_F11:
return RDP_SCANCODE_F11;
case Qt::Key_F12:
return RDP_SCANCODE_F12;
case Qt::Key_F13:
return RDP_SCANCODE_F13;
case Qt::Key_F14:
return RDP_SCANCODE_F14;
case Qt::Key_F15:
return RDP_SCANCODE_F15;
case Qt::Key_F16:
return RDP_SCANCODE_F16;
case Qt::Key_F17:
return RDP_SCANCODE_F17;
case Qt::Key_F18:
return RDP_SCANCODE_F18;
case Qt::Key_F19:
return RDP_SCANCODE_F19;
case Qt::Key_F20:
return RDP_SCANCODE_F20;
case Qt::Key_F21:
return RDP_SCANCODE_F21;
case Qt::Key_F22:
return RDP_SCANCODE_F22;
case Qt::Key_F23:
return RDP_SCANCODE_F23;
case Qt::Key_F24:
return RDP_SCANCODE_F24;
case Qt::Key_0:
return keypad ? RDP_SCANCODE_NUMPAD0 : RDP_SCANCODE_KEY_0;
case Qt::Key_1:
return keypad ? RDP_SCANCODE_NUMPAD1 : RDP_SCANCODE_KEY_1;
case Qt::Key_2:
return keypad ? RDP_SCANCODE_NUMPAD2 : RDP_SCANCODE_KEY_2;
case Qt::Key_3:
return keypad ? RDP_SCANCODE_NUMPAD3 : RDP_SCANCODE_KEY_3;
case Qt::Key_4:
return keypad ? RDP_SCANCODE_NUMPAD4 : RDP_SCANCODE_KEY_4;
case Qt::Key_5:
return keypad ? RDP_SCANCODE_NUMPAD5 : RDP_SCANCODE_KEY_5;
case Qt::Key_6:
return keypad ? RDP_SCANCODE_NUMPAD6 : RDP_SCANCODE_KEY_6;
case Qt::Key_7:
return keypad ? RDP_SCANCODE_NUMPAD7 : RDP_SCANCODE_KEY_7;
case Qt::Key_8:
return keypad ? RDP_SCANCODE_NUMPAD8 : RDP_SCANCODE_KEY_8;
case Qt::Key_9:
return keypad ? RDP_SCANCODE_NUMPAD9 : RDP_SCANCODE_KEY_9;
case Qt::Key_A:
return RDP_SCANCODE_KEY_A;
case Qt::Key_B:
return RDP_SCANCODE_KEY_B;
case Qt::Key_C:
return RDP_SCANCODE_KEY_C;
case Qt::Key_D:
return RDP_SCANCODE_KEY_D;
case Qt::Key_E:
return RDP_SCANCODE_KEY_E;
case Qt::Key_F:
return RDP_SCANCODE_KEY_F;
case Qt::Key_G:
return RDP_SCANCODE_KEY_G;
case Qt::Key_H:
return RDP_SCANCODE_KEY_H;
case Qt::Key_I:
return RDP_SCANCODE_KEY_I;
case Qt::Key_J:
return RDP_SCANCODE_KEY_J;
case Qt::Key_K:
return RDP_SCANCODE_KEY_K;
case Qt::Key_L:
return RDP_SCANCODE_KEY_L;
case Qt::Key_M:
return RDP_SCANCODE_KEY_M;
case Qt::Key_N:
return RDP_SCANCODE_KEY_N;
case Qt::Key_O:
return RDP_SCANCODE_KEY_O;
case Qt::Key_P:
return RDP_SCANCODE_KEY_P;
case Qt::Key_Q:
return RDP_SCANCODE_KEY_Q;
case Qt::Key_R:
return RDP_SCANCODE_KEY_R;
case Qt::Key_S:
return RDP_SCANCODE_KEY_S;
case Qt::Key_T:
return RDP_SCANCODE_KEY_T;
case Qt::Key_U:
return RDP_SCANCODE_KEY_U;
case Qt::Key_V:
return RDP_SCANCODE_KEY_V;
case Qt::Key_W:
return RDP_SCANCODE_KEY_W;
case Qt::Key_X:
return RDP_SCANCODE_KEY_X;
case Qt::Key_Y:
return RDP_SCANCODE_KEY_Y;
case Qt::Key_Z:
return RDP_SCANCODE_KEY_Z;
default:
return RDP_SCANCODE_UNKNOWN;
}
}
QString RdpSessionBackend::mapRdpError(quint32 code)
{
switch (code) {
case FREERDP_ERROR_CONNECT_LOGON_FAILURE:
case FREERDP_ERROR_CONNECT_WRONG_PASSWORD:
case FREERDP_ERROR_CONNECT_NO_OR_MISSING_CREDENTIALS:
case FREERDP_ERROR_AUTHENTICATION_FAILED:
case FREERDP_ERROR_CONNECT_ACCESS_DENIED:
case FREERDP_ERROR_CONNECT_ACCOUNT_RESTRICTION:
case FREERDP_ERROR_CONNECT_LOGON_TYPE_NOT_GRANTED:
return QStringLiteral("Authentication failed. Check username and password.");
case FREERDP_ERROR_CONNECT_ACCOUNT_DISABLED:
return QStringLiteral("Account is disabled.");
case FREERDP_ERROR_CONNECT_ACCOUNT_LOCKED_OUT:
return QStringLiteral("Account is locked out.");
case FREERDP_ERROR_CONNECT_ACCOUNT_EXPIRED:
return QStringLiteral("Account has expired.");
case FREERDP_ERROR_CONNECT_PASSWORD_EXPIRED:
case FREERDP_ERROR_CONNECT_PASSWORD_CERTAINLY_EXPIRED:
return QStringLiteral("Password has expired.");
case FREERDP_ERROR_CONNECT_PASSWORD_MUST_CHANGE:
return QStringLiteral("Password must be changed before sign-in.");
case FREERDP_ERROR_INSUFFICIENT_PRIVILEGES:
return QStringLiteral("The account does not have sufficient privileges for RDP sign-in.");
case FREERDP_ERROR_DNS_NAME_NOT_FOUND:
case FREERDP_ERROR_DNS_ERROR:
return QStringLiteral("Host could not be resolved.");
case FREERDP_ERROR_CONNECT_KDC_UNREACHABLE:
return QStringLiteral("Kerberos domain controller is unreachable.");
case FREERDP_ERROR_CONNECT_TRANSPORT_FAILED:
return QStringLiteral("Network transport failed while connecting.");
case FREERDP_ERROR_TLS_CONNECT_FAILED:
return QStringLiteral("TLS negotiation failed while connecting.");
case FREERDP_ERROR_SECURITY_NEGO_CONNECT_FAILED:
return QStringLiteral("RDP security negotiation failed. Try a different RDP security mode.");
case FREERDP_ERROR_CONNECT_FAILED:
return QStringLiteral("Could not establish an RDP session. Check host, port, and server RDP settings.");
case FREERDP_ERROR_PRE_CONNECT_FAILED:
case FREERDP_ERROR_POST_CONNECT_FAILED:
return QStringLiteral("A local RDP client configuration error prevented the connection.");
case FREERDP_ERROR_CONNECT_TARGET_BOOTING:
return QStringLiteral("The remote host is still booting. Try again shortly.");
case FREERDP_ERROR_CONNECT_ACTIVATION_TIMEOUT:
return QStringLiteral("Connection timed out.");
case FREERDP_ERROR_CONNECT_CANCELLED:
return QStringLiteral("Connection cancelled.");
default:
break;
}
const char* message = freerdp_get_last_error_string(code);
if (message != nullptr && message[0] != '\0') {
return QString::fromUtf8(message);
}
const char* name = freerdp_get_last_error_name(code);
if (name != nullptr && name[0] != '\0') {
return QString::fromUtf8(name);
}
return QStringLiteral("RDP connection failed (0x%1).").arg(code, 8, 16, QChar('0'));
}
bool RdpSessionBackend::isExpectedDisconnectCode(quint32 code)
{
switch (code) {
case FREERDP_ERROR_SUCCESS:
case FREERDP_ERROR_NONE:
case MAKE_FREERDP_ERROR(ERRINFO, ERRINFO_PEER_DISCONNECTED):
case FREERDP_ERROR_IDLE_TIMEOUT:
case FREERDP_ERROR_LOGON_TIMEOUT:
case FREERDP_ERROR_RPC_INITIATED_DISCONNECT:
case FREERDP_ERROR_RPC_INITIATED_LOGOFF:
case FREERDP_ERROR_RPC_INITIATED_DISCONNECT_BY_USER:
case FREERDP_ERROR_LOGOFF_BY_USER:
case FREERDP_ERROR_DISCONNECTED_BY_OTHER_CONNECTION:
case FREERDP_ERROR_CONNECT_CANCELLED:
case FREERDP_ERROR_CONNECT_TRANSPORT_FAILED:
case FREERDP_ERROR_TLS_CONNECT_FAILED:
case FREERDP_ERROR_MCS_CONNECT_INITIAL_ERROR:
case FREERDP_ERROR_CONNECT_FAILED:
case FREERDP_ERROR_CLOSE_STACK_ON_DRIVER_NOT_READY:
case FREERDP_ERROR_CLOSE_STACK_ON_DRIVER_FAILURE:
case FREERDP_ERROR_CLOSE_STACK_ON_DRIVER_IFACE_FAILURE:
case FREERDP_ERROR_SERVER_DWM_CRASH:
case FREERDP_ERROR_SERVER_WINLOGON_CRASH:
case FREERDP_ERROR_SERVER_CSRSS_CRASH:
return true;
default:
return false;
}
}
bool RdpSessionBackend::isExpectedConnectAbortCode(quint32 code)
{
switch (code) {
case FREERDP_ERROR_SUCCESS:
case FREERDP_ERROR_NONE:
case FREERDP_ERROR_CONNECT_CANCELLED:
return true;
default:
return false;
}
}
QString RdpSessionBackend::disconnectMessageForCode(quint32 code)
{
switch (code) {
case FREERDP_ERROR_IDLE_TIMEOUT:
return QStringLiteral("RDP session disconnected due to idle timeout.");
case FREERDP_ERROR_LOGON_TIMEOUT:
return QStringLiteral("RDP session disconnected due to logon timeout.");
case FREERDP_ERROR_CONNECT_CANCELLED:
return QStringLiteral("Connection cancelled.");
case FREERDP_ERROR_RPC_INITIATED_LOGOFF:
case FREERDP_ERROR_LOGOFF_BY_USER:
return QStringLiteral("RDP session signed out.");
case FREERDP_ERROR_RPC_INITIATED_DISCONNECT:
case FREERDP_ERROR_RPC_INITIATED_DISCONNECT_BY_USER:
return QStringLiteral("RDP session disconnected by remote host.");
case FREERDP_ERROR_DISCONNECTED_BY_OTHER_CONNECTION:
return QStringLiteral("RDP session disconnected by another connection.");
case FREERDP_ERROR_CLOSE_STACK_ON_DRIVER_NOT_READY:
case FREERDP_ERROR_CLOSE_STACK_ON_DRIVER_FAILURE:
case FREERDP_ERROR_CLOSE_STACK_ON_DRIVER_IFACE_FAILURE:
return QStringLiteral("RDP session ended while the remote display subsystem restarted.");
case FREERDP_ERROR_SERVER_DWM_CRASH:
case FREERDP_ERROR_SERVER_WINLOGON_CRASH:
case FREERDP_ERROR_SERVER_CSRSS_CRASH:
return QStringLiteral("RDP session ended because the remote Windows session restarted.");
case MAKE_FREERDP_ERROR(ERRINFO, ERRINFO_PEER_DISCONNECTED):
case FREERDP_ERROR_CONNECT_TRANSPORT_FAILED:
case FREERDP_ERROR_TLS_CONNECT_FAILED:
case FREERDP_ERROR_MCS_CONNECT_INITIAL_ERROR:
case FREERDP_ERROR_CONNECT_FAILED:
return QStringLiteral("RDP session disconnected.");
default:
return QStringLiteral("RDP session ended.");
}
}
QString RdpSessionBackend::rdpErrorRaw(quint32 code)
{
const char* name = freerdp_get_last_error_name(code);
const QString text = (name != nullptr && name[0] != '\0') ? QString::fromUtf8(name)
: QStringLiteral("UNKNOWN");
return QStringLiteral("%1 (0x%2)").arg(text).arg(code, 8, 16, QChar('0'));
}
#endif
RdpSessionBackend::RdpSessionBackend(const Profile& profile, QObject* parent)
: SessionBackend(profile, parent),
m_state(SessionState::Disconnected),
m_userInitiatedDisconnect(false),
m_requestedDesktopWidth(kDefaultDesktopWidth),
m_requestedDesktopHeight(kDefaultDesktopHeight),
m_devicePixelRatio(1.0),
m_workerRunning(false),
m_stopRequested(false),
m_instance(nullptr),
m_displayControlContext(nullptr),
m_displayControlReady(false),
m_resizeFailureLogged(false),
m_lastResizeWidth(0),
m_lastResizeHeight(0),
m_lastResizeScale(0),
m_cliprdrContext(nullptr)
{
}
RdpSessionBackend::~RdpSessionBackend()
{
stopWorker(true);
}
void RdpSessionBackend::connectSession(const SessionConnectOptions& options)
{
if (m_state == SessionState::Connecting || m_state == SessionState::Connected
|| m_workerRunning.load()) {
emit eventLogged(QStringLiteral("Connect skipped: session is already active."));
return;
}
QString validationMessage;
if (!validateProfile(validationMessage)) {
setState(SessionState::Failed, validationMessage);
emit connectionError(validationMessage, validationMessage);
return;
}
m_activeOptions = options;
m_userInitiatedDisconnect.store(false);
m_stopRequested.store(false);
m_resizeFailureLogged = false;
m_lastResizeWidth = 0;
m_lastResizeHeight = 0;
{
std::lock_guard<std::mutex> guard(m_displayControlMutex);
m_displayControlContext = nullptr;
m_displayControlReady = false;
}
{
std::lock_guard<std::mutex> guard(m_inputMutex);
m_inputEvents.clear();
}
setState(SessionState::Connecting, QStringLiteral("Starting embedded RDP session..."));
startWorker();
}
void RdpSessionBackend::disconnectSession()
{
if (m_state == SessionState::Disconnected && !m_workerRunning.load()) {
return;
}
emit eventLogged(QStringLiteral("Disconnect requested."));
stopWorker(true);
setState(SessionState::Disconnected, QStringLiteral("Session disconnected."));
}
void RdpSessionBackend::reconnectSession(const SessionConnectOptions& options)
{
emit eventLogged(QStringLiteral("Reconnect requested."));
stopWorker(true);
m_activeOptions = options;
setState(SessionState::Disconnected, QStringLiteral("Reconnecting..."));
connectSession(options);
}
void RdpSessionBackend::sendInput(const QString&)
{
emit eventLogged(QStringLiteral("Input ignored: RDP backend uses direct keyboard/mouse events."));
}
void RdpSessionBackend::confirmHostKey(bool)
{
}
void RdpSessionBackend::updateTerminalSize(int columns, int rows)
{
const int width = sanitizeDesktopWidth(columns);
const int height = sanitizeDesktopHeight(rows);
m_requestedDesktopWidth.store(width);
m_requestedDesktopHeight.store(height);
if (!m_workerRunning.load()) {
return;
}
InputEvent event;
event.type = InputEventType::Resize;
event.width = width;
event.height = height;
enqueueInputEvent(event);
}
void RdpSessionBackend::updateDisplayScale(qreal devicePixelRatio)
{
const qreal clamped = qBound(1.0, devicePixelRatio, 4.0);
m_devicePixelRatio.store(clamped);
if (!m_workerRunning.load()) {
return;
}
// Reuses the resize input-event path so the worker thread (which owns
// m_instance) picks this up safely; processInputEvents' dedup check
// also compares the quantized scale value, so this correctly triggers
// a fresh SendMonitorLayout even when width/height haven't changed
// (e.g. the window moved to a different-DPI monitor at the same
// logical size).
InputEvent event;
event.type = InputEventType::Resize;
event.width = sanitizeDesktopWidth(m_requestedDesktopWidth.load());
event.height = sanitizeDesktopHeight(m_requestedDesktopHeight.load());
enqueueInputEvent(event);
}
void RdpSessionBackend::sendKeyEvent(int key,
quint32 nativeScanCode,
const QString& text,
bool pressed,
int modifiers)
{
if (!m_workerRunning.load()) {
return;
}
InputEvent event;
event.type = InputEventType::Key;
event.key = key;
event.nativeScanCode = nativeScanCode;
event.text = text;
event.pressed = pressed;
event.modifiers = modifiers;
enqueueInputEvent(event);
}
void RdpSessionBackend::setClipboardText(const QString& text)
{
if (!m_workerRunning.load()) {
return;
}
InputEvent event;
event.type = InputEventType::SetClipboardText;
event.text = text;
enqueueInputEvent(event);
}
void RdpSessionBackend::sendMouseMoveEvent(int x, int y)
{
if (!m_workerRunning.load()) {
return;
}
InputEvent event;
event.type = InputEventType::MouseMove;
event.x = x;
event.y = y;
enqueueInputEvent(event);
}
void RdpSessionBackend::sendMouseButtonEvent(int x, int y, int button, bool pressed)
{
if (!m_workerRunning.load()) {
return;
}
InputEvent event;
event.type = InputEventType::MouseButton;
event.x = x;
event.y = y;
event.button = button;
event.pressed = pressed;
enqueueInputEvent(event);
}
void RdpSessionBackend::sendMouseWheelEvent(int x, int y, int deltaX, int deltaY)
{
if (!m_workerRunning.load()) {
return;
}
InputEvent event;
event.type = InputEventType::MouseWheel;
event.x = x;
event.y = y;
event.deltaX = deltaX;
event.deltaY = deltaY;
enqueueInputEvent(event);
}
void RdpSessionBackend::setState(SessionState state, const QString& message)
{
m_state = state;
emit stateChanged(state, message);
emit eventLogged(message);
}
void RdpSessionBackend::recordCertificateRejection(const QString& reason)
{
m_certificateRejectionReason = reason;
}
bool RdpSessionBackend::validateProfile(QString& message) const
{
const Profile& p = profile();
if (p.host.trimmed().isEmpty()) {
message = QStringLiteral("Host is required for RDP connections.");
return false;
}
if (p.port < 1 || p.port > 65535) {
message = QStringLiteral("Port must be between 1 and 65535.");
return false;
}
message.clear();
return true;
}
void RdpSessionBackend::startWorker()
{
#ifndef ORBITHUB_HAS_FREERDP
const QString message = QStringLiteral(
"Embedded RDP engine is not available in this build. External RDP tools are intentionally disabled.");
setState(SessionState::Failed, message);
emit connectionError(message, message);
return;
#else
if (m_worker.joinable()) {
m_worker.join();
}
m_workerRunning.store(true);
m_worker = std::thread([this]() { workerMain(); });
#endif
}
void RdpSessionBackend::stopWorker(bool userInitiated)
{
m_userInitiatedDisconnect.store(userInitiated);
m_stopRequested.store(true);
#ifdef ORBITHUB_HAS_FREERDP
{
std::lock_guard<std::mutex> guard(m_instanceMutex);
if (m_instance != nullptr && m_instance->context != nullptr) {
freerdp_abort_connect_context(m_instance->context);
}
}
#endif
if (m_worker.joinable()) {
m_worker.join();
}
m_workerRunning.store(false);
}
void RdpSessionBackend::workerMain()
{
#ifndef ORBITHUB_HAS_FREERDP
m_workerRunning.store(false);
return;
#else
m_certificateRejectionReason.clear();
ensureFreeRdpRuntimeInitialized();
freerdp* instance = freerdp_new();
if (instance == nullptr) {
emitConnectionFailureAsync(QStringLiteral("Failed to initialize FreeRDP runtime."),
QStringLiteral("freerdp_new returned null."));
emitStateAsync(SessionState::Failed, QStringLiteral("Failed to initialize RDP runtime."));
m_workerRunning.store(false);
return;
}
if (!orbitEnsureStaticChannelProvider(this)) {
emit eventLogged(QStringLiteral(
"RDP warning: failed to initialize static channel provider; resize may be limited."));
}
instance->ContextSize = sizeof(OrbitRdpContext);
instance->ContextNew = orbitContextNew;
instance->ContextFree = orbitContextFree;
instance->PreConnect = orbitPreConnect;
instance->PostConnect = orbitPostConnect;
instance->PostDisconnect = orbitPostDisconnect;
instance->VerifyCertificateEx = orbitVerifyCertificateEx;
instance->VerifyChangedCertificateEx = orbitVerifyChangedCertificateEx;
instance->AuthenticateEx = orbitAuthenticateEx;
instance->LoadChannels = orbitLoadChannels;
g_contextBackend = this;
const BOOL contextReady = freerdp_context_new(instance);
g_contextBackend = nullptr;
if (!contextReady || instance->context == nullptr || instance->context->settings == nullptr) {
emitConnectionFailureAsync(QStringLiteral("Failed to initialize RDP context."),
QStringLiteral("freerdp_context_new failed."));
emitStateAsync(SessionState::Failed, QStringLiteral("Failed to initialize RDP context."));
freerdp_free(instance);
m_workerRunning.store(false);
return;
}
{
std::lock_guard<std::mutex> guard(m_instanceMutex);
m_instance = instance;
}
rdpSettings* settings = instance->context->settings;
const Profile& p = profile();
const QString host = p.host.trimmed();
// m_activeOptions.username carries a value prompted for at connect time
// (see SessionTab::requestConnectOptions()) when the saved profile's
// own username was blank; profile().username never sees that edit
// since this backend's Profile copy was captured at construction time.
QString username = m_activeOptions.username.trimmed().isEmpty()
? p.username.trimmed()
: m_activeOptions.username.trimmed();
QString domain = p.domain.trimmed();
if (domain.isEmpty()) {
const int domainSeparator = username.indexOf(QLatin1Char('\\'));
if (domainSeparator > 0) {
domain = username.left(domainSeparator);
username = username.mid(domainSeparator + 1);
} else {
const int upnSeparator = username.lastIndexOf(QLatin1Char('@'));
if (upnSeparator > 0 && upnSeparator < username.size() - 1) {
domain = username.mid(upnSeparator + 1);
username = username.left(upnSeparator);
}
}
}
const QByteArray hostUtf8 = host.toUtf8();
const QByteArray userUtf8 = username.toUtf8();
const QByteArray domainUtf8 = domain.toUtf8();
const QByteArray passwordUtf8 = m_activeOptions.password.toUtf8();
freerdp_settings_set_string(settings, FreeRDP_ServerHostname, hostUtf8.constData());
freerdp_settings_set_uint32(settings, FreeRDP_ServerPort, static_cast<UINT32>(p.port));
if (!userUtf8.isEmpty()) {
freerdp_settings_set_string(settings, FreeRDP_Username, userUtf8.constData());
}
if (!domainUtf8.isEmpty()) {
freerdp_settings_set_string(settings, FreeRDP_Domain, domainUtf8.constData());
}
if (!passwordUtf8.isEmpty()) {
freerdp_settings_set_string(settings, FreeRDP_Password, passwordUtf8.constData());
}
const int desktopWidth = sanitizeDesktopWidth(m_requestedDesktopWidth.load());
const int desktopHeight = sanitizeDesktopHeight(m_requestedDesktopHeight.load());
const QString securityMode = normalizedRdpSecurityMode(p.rdpSecurityMode);
const QString performanceProfile = normalizedRdpPerformanceProfile(p.rdpPerformanceProfile);
freerdp_settings_set_bool(settings, FreeRDP_SupportDisplayControl, TRUE);
freerdp_settings_set_bool(settings, FreeRDP_DynamicResolutionUpdate, TRUE);
// Lets us actively request a full-screen repaint after a resize (see
// sendDisplayResize) — some RDP hosts (particularly VMs using a
// synthetic/virtual display driver) occasionally fail to fully redraw
// their own desktop after a resolution change; requesting a refresh
// forces them to resend everything rather than leaving stale content.
freerdp_settings_set_bool(settings, FreeRDP_RefreshRect, TRUE);
freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, static_cast<UINT32>(desktopWidth));
freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, static_cast<UINT32>(desktopHeight));
freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth, 32);
// Tell the remote session the real display scale so it renders its own
// UI/ClearType text natively at that size, instead of assuming 96 DPI
// and having the result stretched (and ClearType's subpixel hinting
// distorted) by client-side scaling. The override flags are required —
// without them FreeRDP ignores the scale factor values entirely.
{
const UINT32 scaleValue = nearestFreeRdpScaleValue(m_devicePixelRatio.load());
freerdp_settings_set_uint32(settings, FreeRDP_DesktopScaleFactor, scaleValue);
freerdp_settings_set_uint32(settings, FreeRDP_DeviceScaleFactor, scaleValue);
freerdp_settings_set_uint64(settings,
FreeRDP_MonitorOverrideFlags,
FREERDP_MONITOR_OVERRIDE_DESKTOP_SCALE
| FREERDP_MONITOR_OVERRIDE_DEVICE_SCALE);
}
freerdp_settings_set_bool(settings, FreeRDP_AuthenticationOnly, FALSE);
freerdp_settings_set_bool(settings, FreeRDP_AutoLogonEnabled, TRUE);
if (!applyRdpSecurityMode(settings, securityMode)) {
emitConnectionFailureAsync(QStringLiteral("Invalid RDP security configuration."),
QStringLiteral("Failed to apply RDP security mode settings."));
emitStateAsync(SessionState::Failed, QStringLiteral("Failed to configure RDP security."));
freerdp_context_free(instance);
freerdp_free(instance);
{
std::lock_guard<std::mutex> guard(m_instanceMutex);
m_instance = nullptr;
}
m_workerRunning.store(false);
return;
}
if (!applyRdpPerformanceProfile(settings, performanceProfile)) {
emitConnectionFailureAsync(
QStringLiteral("Invalid RDP performance configuration."),
QStringLiteral("Failed to apply RDP performance profile settings."));
emitStateAsync(SessionState::Failed, QStringLiteral("Failed to configure RDP performance."));
freerdp_context_free(instance);
freerdp_free(instance);
{
std::lock_guard<std::mutex> guard(m_instanceMutex);
m_instance = nullptr;
}
m_workerRunning.store(false);
return;
}
emit eventLogged(
QStringLiteral("Connecting RDP to %1:%2 as %3.")
.arg(host,
QString::number(p.port),
domain.isEmpty() ? username : QStringLiteral("%1\\%2").arg(domain, username)));
emit eventLogged(QStringLiteral("RDP security mode: %1.").arg(securityMode));
emit eventLogged(QStringLiteral("RDP performance profile: %1.").arg(performanceProfile));
if (!freerdp_settings_are_valid(settings)) {
emitConnectionFailureAsync(QStringLiteral("RDP settings are invalid."),
QStringLiteral("freerdp_settings_are_valid returned false."));
emitStateAsync(SessionState::Failed, QStringLiteral("Invalid RDP settings."));
freerdp_context_free(instance);
freerdp_free(instance);
{
std::lock_guard<std::mutex> guard(m_instanceMutex);
m_instance = nullptr;
}
m_workerRunning.store(false);
return;
}
if (PubSub_SubscribeChannelConnected(instance->context->pubSub,
orbitOnChannelConnectedEventHandler) < 0) {
emit eventLogged(QStringLiteral("RDP warning: failed to subscribe to channel-connected events."));
} else if (PubSub_SubscribeChannelDisconnected(instance->context->pubSub,
orbitOnChannelDisconnectedEventHandler)
< 0) {
PubSub_UnsubscribeChannelConnected(instance->context->pubSub,
orbitOnChannelConnectedEventHandler);
emit eventLogged(QStringLiteral(
"RDP warning: failed to subscribe to channel-disconnected events."));
}
if (!freerdp_connect(instance)) {
const UINT32 code = freerdp_get_last_error(instance->context);
const QString raw = rdpErrorRaw(code);
if (isExpectedConnectAbortCode(code) || m_stopRequested.load()) {
emit eventLogged(QStringLiteral("RDP connect aborted: %1").arg(raw));
emitStateAsync(SessionState::Disconnected, disconnectMessageForCode(code));
} else {
const QString mapped = m_certificateRejectionReason.isEmpty()
? mapRdpError(code)
: m_certificateRejectionReason;
emit eventLogged(QStringLiteral("RDP connect failure detail: %1").arg(raw));
emitConnectionFailureAsync(mapped, raw);
emitStateAsync(SessionState::Failed, mapped);
}
freerdp_context_free(instance);
freerdp_free(instance);
{
std::lock_guard<std::mutex> guard(m_instanceMutex);
m_instance = nullptr;
}
m_workerRunning.store(false);
return;
}
emitStateAsync(SessionState::Connected, QStringLiteral("RDP session established."));
processInputEvents(instance);
HANDLE handles[kMaxEventHandles] = {0};
bool hasFailure = false;
QString failureMessage;
QString failureRaw;
while (!m_stopRequested.load() && !freerdp_shall_disconnect_context(instance->context)) {
processInputEvents(instance);
const DWORD count =
freerdp_get_event_handles(instance->context, handles, static_cast<DWORD>(kMaxEventHandles));
if (count == 0) {
hasFailure = true;
failureMessage = QStringLiteral("RDP event loop failed to acquire event handles.");
failureRaw = QStringLiteral("freerdp_get_event_handles returned 0.");
break;
}
const DWORD status = WaitForMultipleObjects(count, handles, FALSE, kWaitTimeoutMs);
if (status == WAIT_TIMEOUT) {
continue;
}
if (status == WAIT_FAILED) {
hasFailure = true;
failureMessage = QStringLiteral("RDP event loop wait failed.");
failureRaw = QStringLiteral("WaitForMultipleObjects returned WAIT_FAILED.");
break;
}
if (!freerdp_check_event_handles(instance->context)) {
if (m_stopRequested.load()) {
break;
}
const UINT32 code = freerdp_get_last_error(instance->context);
hasFailure = !isExpectedDisconnectCode(code);
if (hasFailure) {
failureMessage = mapRdpError(code);
failureRaw = rdpErrorRaw(code);
}
break;
}
}
processInputEvents(instance);
freerdp_disconnect(instance);
const UINT32 finalCode = freerdp_get_last_error(instance->context);
const QString finalRaw = rdpErrorRaw(finalCode);
if (hasFailure) {
if (!m_stopRequested.load() && !m_userInitiatedDisconnect.load()) {
emit eventLogged(QStringLiteral("RDP failure detail: %1").arg(failureRaw));
emitConnectionFailureAsync(failureMessage, failureRaw);
emitStateAsync(SessionState::Failed, failureMessage);
} else {
emit eventLogged(QStringLiteral("RDP session ended during disconnect: %1").arg(finalRaw));
emitStateAsync(SessionState::Disconnected, QStringLiteral("Session disconnected."));
}
} else if (m_stopRequested.load() || m_userInitiatedDisconnect.load()) {
emit eventLogged(QStringLiteral("RDP disconnect detail: %1").arg(finalRaw));
emitStateAsync(SessionState::Disconnected, QStringLiteral("Session disconnected."));
} else {
if (!isExpectedDisconnectCode(finalCode)) {
const QString mapped = mapRdpError(finalCode);
emit eventLogged(QStringLiteral("RDP disconnect failure detail: %1").arg(finalRaw));
emitConnectionFailureAsync(mapped, finalRaw);
emitStateAsync(SessionState::Failed, mapped);
} else {
emit eventLogged(QStringLiteral("RDP disconnect detail: %1").arg(finalRaw));
emitStateAsync(SessionState::Disconnected, disconnectMessageForCode(finalCode));
}
}
freerdp_context_free(instance);
freerdp_free(instance);
{
std::lock_guard<std::mutex> guard(m_instanceMutex);
m_instance = nullptr;
}
m_workerRunning.store(false);
#endif
}
void RdpSessionBackend::enqueueInputEvent(const InputEvent& event)
{
std::lock_guard<std::mutex> guard(m_inputMutex);
m_inputEvents.push_back(event);
}
bool RdpSessionBackend::sendDisplayResize(rdp_freerdp* instance, int width, int height)
{
#ifndef ORBITHUB_HAS_FREERDP
Q_UNUSED(instance);
Q_UNUSED(width);
Q_UNUSED(height);
return false;
#else
if (instance == nullptr || instance->context == nullptr || instance->context->settings == nullptr
|| width <= 0 || height <= 0) {
return false;
}
const qreal ratio = m_devicePixelRatio.load();
const UINT32 scaleValue = nearestFreeRdpScaleValue(ratio);
rdpSettings* settings = instance->context->settings;
freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, static_cast<UINT32>(width));
freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, static_cast<UINT32>(height));
freerdp_settings_set_uint32(settings, FreeRDP_DesktopScaleFactor, scaleValue);
freerdp_settings_set_uint32(settings, FreeRDP_DeviceScaleFactor, scaleValue);
DispClientContext* dispContext = nullptr;
bool displayControlReady = false;
{
std::lock_guard<std::mutex> guard(m_displayControlMutex);
dispContext = reinterpret_cast<DispClientContext*>(m_displayControlContext);
displayControlReady = m_displayControlReady;
}
if (dispContext != nullptr && displayControlReady && dispContext->SendMonitorLayout != nullptr) {
DISPLAY_CONTROL_MONITOR_LAYOUT layout = {0};
layout.Flags = DISPLAY_CONTROL_MONITOR_PRIMARY;
layout.Left = 0;
layout.Top = 0;
layout.Width = static_cast<UINT32>(width);
layout.Height = static_cast<UINT32>(height);
layout.Orientation = ORIENTATION_LANDSCAPE;
layout.DesktopScaleFactor = scaleValue;
layout.DeviceScaleFactor = scaleValue;
// Physical size in mm must reflect the real DPI (96 * ratio), not
// the plain baseline, so it stays consistent with the scale factor
// above rather than implying a standard-DPI display of this size.
const double effectiveDpi = kDefaultDpi * ratio;
layout.PhysicalWidth = static_cast<UINT32>(
std::lround((static_cast<double>(width) / effectiveDpi) * kMillimetersPerInch));
layout.PhysicalHeight = static_cast<UINT32>(
std::lround((static_cast<double>(height) / effectiveDpi) * kMillimetersPerInch));
const UINT rc = dispContext->SendMonitorLayout(dispContext, 1, &layout);
if (rc == CHANNEL_RC_OK) {
m_lastResizeWidth = width;
m_lastResizeHeight = height;
m_lastResizeScale = static_cast<int>(scaleValue);
// The Display Control channel (MS-RDPEDISP) has no server
// acknowledgment PDU, and real hosts apply the new resolution
// without a Deactivation-Reactivation sequence — so
// update->DesktopResize (which only fires for that sequence, or
// for the GFX/Progressive pipeline we don't use) never runs for
// a channel-driven resize. Without a matching gdi_resize() call,
// gdi->width/height stay at the old size, and FreeRDP's own
// surface-bits handling (intersect_rect in gdi.c) then silently
// *drops* any update reaching outside those stale bounds —
// which is what produced the missing/misplaced taskbar and
// stale composited-looking content: this call is the fix, not
// just a best-effort nudge.
if (instance->context->gdi != nullptr
&& gdi_resize(instance->context->gdi, static_cast<UINT32>(width),
static_cast<UINT32>(height))) {
emit remoteDesktopSizeChanged(width, height);
} else if (instance->context->gdi != nullptr) {
emit eventLogged(QStringLiteral(
"RDP warning: local resize to %1x%2 failed; display may show stale content "
"until the next full repaint.")
.arg(width)
.arg(height));
}
// Best-effort nudge: some hosts don't fully repaint their own
// desktop after a resolution change (observed: taskbar missing
// until something else forces a redraw). Explicitly asking for
// the whole new area to be resent costs little and helps
// recover from that when it happens.
if (instance->context->update != nullptr
&& instance->context->update->RefreshRect != nullptr) {
RECTANGLE_16 fullArea = {0, 0, static_cast<UINT16>(qMin(width, 65535)),
static_cast<UINT16>(qMin(height, 65535))};
instance->context->update->RefreshRect(instance->context, 1, &fullArea);
}
return true;
}
}
return false;
#endif
}
void RdpSessionBackend::sendClipboardTextToRemote(rdp_freerdp* instance, const QString& text)
{
#ifndef ORBITHUB_HAS_FREERDP
Q_UNUSED(instance);
Q_UNUSED(text);
#else
Q_UNUSED(instance);
CliprdrClientContext* cliprdrContext = nullptr;
{
std::lock_guard<std::mutex> guard(m_cliprdrMutex);
m_pendingLocalClipboardText = text;
cliprdrContext = reinterpret_cast<CliprdrClientContext*>(m_cliprdrContext);
}
if (cliprdrContext == nullptr || cliprdrContext->ClientFormatList == nullptr) {
// Kept in m_pendingLocalClipboardText and announced once the channel
// reports MonitorReady (see onCliprdrMonitorReady()).
emit eventLogged(QStringLiteral(
"RDP clipboard: channel not ready; copied text will be sent once it is."));
return;
}
CLIPRDR_FORMAT format = {};
format.formatId = CF_UNICODETEXT;
format.formatName = nullptr;
CLIPRDR_FORMAT_LIST formatList = {};
formatList.numFormats = 1;
formatList.formats = &format;
const UINT rc = cliprdrContext->ClientFormatList(cliprdrContext, &formatList);
if (rc != CHANNEL_RC_OK) {
emit eventLogged(
QStringLiteral("RDP clipboard: failed to announce copied text to the server (0x%1).")
.arg(rc, 0, 16));
}
#endif
}
void RdpSessionBackend::processInputEvents(rdp_freerdp* instance)
{
#ifndef ORBITHUB_HAS_FREERDP
Q_UNUSED(instance);
#else
if (instance == nullptr || instance->context == nullptr || instance->context->input == nullptr) {
return;
}
rdpInput* input = instance->context->input;
std::deque<InputEvent> pending;
{
std::lock_guard<std::mutex> guard(m_inputMutex);
pending.swap(m_inputEvents);
}
bool hasResize = false;
int resizeWidth = 0;
int resizeHeight = 0;
for (const InputEvent& event : pending) {
switch (event.type) {
case InputEventType::Resize:
hasResize = true;
resizeWidth = event.width;
resizeHeight = event.height;
break;
case InputEventType::MouseMove:
freerdp_input_send_mouse_event(
input, PTR_FLAGS_MOVE, static_cast<UINT16>(event.x), static_cast<UINT16>(event.y));
break;
case InputEventType::MouseButton: {
UINT16 flags = 0;
switch (event.button) {
case Qt::LeftButton:
flags = PTR_FLAGS_BUTTON1;
break;
case Qt::RightButton:
flags = PTR_FLAGS_BUTTON2;
break;
case Qt::MiddleButton:
flags = PTR_FLAGS_BUTTON3;
break;
default:
break;
}
if (flags == 0) {
break;
}
if (event.pressed) {
flags |= PTR_FLAGS_DOWN;
}
freerdp_input_send_mouse_event(input,
flags,
static_cast<UINT16>(event.x),
static_cast<UINT16>(event.y));
break;
}
case InputEventType::MouseWheel: {
if (event.deltaY != 0) {
const int direction = event.deltaY > 0 ? 1 : -1;
const int steps = qMax(1, qAbs(event.deltaY) / 120);
for (int i = 0; i < steps; ++i) {
UINT16 flags = PTR_FLAGS_WHEEL | kWheelStep;
if (direction < 0) {
flags |= PTR_FLAGS_WHEEL_NEGATIVE;
}
freerdp_input_send_mouse_event(input,
flags,
static_cast<UINT16>(event.x),
static_cast<UINT16>(event.y));
}
}
if (event.deltaX != 0) {
const int direction = event.deltaX > 0 ? 1 : -1;
const int steps = qMax(1, qAbs(event.deltaX) / 120);
for (int i = 0; i < steps; ++i) {
UINT16 flags = PTR_FLAGS_HWHEEL | kWheelStep;
if (direction < 0) {
flags |= PTR_FLAGS_WHEEL_NEGATIVE;
}
freerdp_input_send_mouse_event(input,
flags,
static_cast<UINT16>(event.x),
static_cast<UINT16>(event.y));
}
}
break;
}
case InputEventType::Key: {
const Qt::KeyboardModifiers modifiers = Qt::KeyboardModifiers(event.modifiers);
UINT32 scancode = scancodeForQtKey(event.key, modifiers, event.nativeScanCode);
if (scancode == RDP_SCANCODE_UNKNOWN) {
scancode = scancodeFromNativeScanCode(event.nativeScanCode);
}
if (scancode != RDP_SCANCODE_UNKNOWN) {
if (scancode == RDP_SCANCODE_PAUSE) {
if (event.pressed) {
freerdp_input_send_keyboard_pause_event(input);
}
break;
}
freerdp_input_send_keyboard_event_ex(input, event.pressed, FALSE, scancode);
break;
}
const bool hasShortcutModifier = modifiers.testFlag(Qt::ControlModifier)
|| modifiers.testFlag(Qt::AltModifier)
|| modifiers.testFlag(Qt::MetaModifier);
if (event.pressed && !event.text.isEmpty() && !hasShortcutModifier) {
for (QChar ch : event.text) {
const UINT16 unicode = ch.unicode();
freerdp_input_send_unicode_keyboard_event(input, 0, unicode);
freerdp_input_send_unicode_keyboard_event(input,
KBD_FLAGS_RELEASE,
unicode);
}
}
break;
}
case InputEventType::SetClipboardText:
sendClipboardTextToRemote(instance, event.text);
break;
}
}
if (hasResize) {
const int width = sanitizeDesktopWidth(resizeWidth);
const int height = sanitizeDesktopHeight(resizeHeight);
const int scaleValue = static_cast<int>(nearestFreeRdpScaleValue(m_devicePixelRatio.load()));
if (width != m_lastResizeWidth || height != m_lastResizeHeight
|| scaleValue != m_lastResizeScale) {
if (sendDisplayResize(instance, width, height)) {
if (m_resizeFailureLogged) {
emit eventLogged(QStringLiteral("Dynamic RDP resize recovered."));
}
m_resizeFailureLogged = false;
} else if (!m_resizeFailureLogged) {
m_resizeFailureLogged = true;
emit eventLogged(QStringLiteral(
"Dynamic RDP resize unavailable (display-control channel not ready)."));
}
}
}
#endif
}
void RdpSessionBackend::onChannelConnectedEvent(const char* name, void* channelInterface)
{
#ifdef ORBITHUB_HAS_FREERDP
if (name == nullptr) {
return;
}
if (std::strcmp(name, DISP_DVC_CHANNEL_NAME) == 0) {
auto* dispContext = reinterpret_cast<DispClientContext*>(channelInterface);
if (dispContext == nullptr) {
return;
}
dispContext->custom = this;
dispContext->DisplayControlCaps = orbitDisplayControlCaps;
{
std::lock_guard<std::mutex> guard(m_displayControlMutex);
m_displayControlContext = dispContext;
m_displayControlReady = false;
}
emit eventLogged(QStringLiteral("RDP display-control channel connected."));
return;
}
if (std::strcmp(name, CLIPRDR_SVC_CHANNEL_NAME) == 0) {
auto* cliprdrContext = reinterpret_cast<CliprdrClientContext*>(channelInterface);
if (cliprdrContext == nullptr) {
return;
}
cliprdrContext->custom = this;
cliprdrContext->MonitorReady = orbitCliprdrMonitorReady;
cliprdrContext->ServerFormatList = orbitCliprdrServerFormatList;
cliprdrContext->ServerFormatListResponse = orbitCliprdrServerFormatListResponse;
cliprdrContext->ServerFormatDataRequest = orbitCliprdrServerFormatDataRequest;
cliprdrContext->ServerFormatDataResponse = orbitCliprdrServerFormatDataResponse;
{
std::lock_guard<std::mutex> guard(m_cliprdrMutex);
m_cliprdrContext = cliprdrContext;
}
emit eventLogged(QStringLiteral("RDP clipboard channel connected."));
}
#else
Q_UNUSED(name);
Q_UNUSED(channelInterface);
#endif
}
void RdpSessionBackend::onChannelDisconnectedEvent(const char* name, void* channelInterface)
{
#ifdef ORBITHUB_HAS_FREERDP
const bool matchesDisp = name == nullptr || std::strcmp(name, DISP_DVC_CHANNEL_NAME) == 0;
const bool matchesCliprdr =
name == nullptr || std::strcmp(name, CLIPRDR_SVC_CHANNEL_NAME) == 0;
if (matchesDisp) {
bool cleared = false;
bool hadDisplayControl = false;
{
std::lock_guard<std::mutex> guard(m_displayControlMutex);
hadDisplayControl = (m_displayControlContext != nullptr) || m_displayControlReady;
if (channelInterface == nullptr || m_displayControlContext == channelInterface) {
m_displayControlContext = nullptr;
m_displayControlReady = false;
cleared = true;
}
}
if (cleared && hadDisplayControl) {
emit eventLogged(QStringLiteral("RDP display-control channel disconnected."));
}
}
if (matchesCliprdr) {
bool cleared = false;
bool hadCliprdr = false;
{
std::lock_guard<std::mutex> guard(m_cliprdrMutex);
hadCliprdr = m_cliprdrContext != nullptr;
if (channelInterface == nullptr || m_cliprdrContext == channelInterface) {
m_cliprdrContext = nullptr;
cleared = true;
}
}
if (cleared && hadCliprdr) {
emit eventLogged(QStringLiteral("RDP clipboard channel disconnected."));
}
}
#else
Q_UNUSED(name);
Q_UNUSED(channelInterface);
#endif
}
void RdpSessionBackend::onDisplayControlCaps(uint32_t maxNumMonitors,
uint32_t maxMonitorAreaFactorA,
uint32_t maxMonitorAreaFactorB)
{
#ifdef ORBITHUB_HAS_FREERDP
{
std::lock_guard<std::mutex> guard(m_displayControlMutex);
if (m_displayControlContext == nullptr) {
return;
}
m_displayControlReady = true;
}
emit eventLogged(
QStringLiteral("RDP dynamic resize ready (maxMonitors=%1, maxArea=%2x%3).")
.arg(maxNumMonitors)
.arg(maxMonitorAreaFactorA)
.arg(maxMonitorAreaFactorB));
InputEvent event;
event.type = InputEventType::Resize;
event.width = m_requestedDesktopWidth.load();
event.height = m_requestedDesktopHeight.load();
enqueueInputEvent(event);
#else
Q_UNUSED(maxNumMonitors);
Q_UNUSED(maxMonitorAreaFactorA);
Q_UNUSED(maxMonitorAreaFactorB);
#endif
}
void RdpSessionBackend::onCliprdrMonitorReady()
{
#ifdef ORBITHUB_HAS_FREERDP
CliprdrClientContext* cliprdrContext = nullptr;
{
std::lock_guard<std::mutex> guard(m_cliprdrMutex);
cliprdrContext = reinterpret_cast<CliprdrClientContext*>(m_cliprdrContext);
}
if (cliprdrContext == nullptr) {
return;
}
CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet = {};
generalCapabilitySet.capabilitySetType = CB_CAPSTYPE_GENERAL;
generalCapabilitySet.capabilitySetLength = CB_CAPSTYPE_GENERAL_LEN;
generalCapabilitySet.version = CB_CAPS_VERSION_2;
generalCapabilitySet.generalFlags = CB_USE_LONG_FORMAT_NAMES;
CLIPRDR_CAPABILITIES capabilities = {};
capabilities.cCapabilitiesSets = 1;
capabilities.capabilitySets = reinterpret_cast<CLIPRDR_CAPABILITY_SET*>(&generalCapabilitySet);
if (cliprdrContext->ClientCapabilities != nullptr) {
const UINT rc = cliprdrContext->ClientCapabilities(cliprdrContext, &capabilities);
Q_UNUSED(rc);
}
emit eventLogged(QStringLiteral("RDP clipboard channel ready."));
QString pendingText;
{
std::lock_guard<std::mutex> guard(m_cliprdrMutex);
pendingText = m_pendingLocalClipboardText;
}
if (!pendingText.isEmpty()) {
sendClipboardTextToRemote(nullptr, pendingText);
} else if (cliprdrContext->ClientFormatList != nullptr) {
// MS-RDPECLIP requires the client to answer Monitor Ready with its
// capabilities *and* an initial Format List (empty is fine); the
// server doesn't treat the channel as initialized until it has
// both. The server can send Monitor Ready again mid-session -- e.g.
// when rdpclip.exe restarts -- and without this the clipboard
// stayed dead until the session was reconnected.
CLIPRDR_FORMAT_LIST emptyList = {};
const UINT rc = cliprdrContext->ClientFormatList(cliprdrContext, &emptyList);
if (rc != CHANNEL_RC_OK) {
emit eventLogged(
QStringLiteral("RDP clipboard: initial format list failed (0x%1).")
.arg(rc, 0, 16));
}
}
#endif
}
void RdpSessionBackend::onCliprdrServerFormatList(bool hasUnicodeText)
{
#ifdef ORBITHUB_HAS_FREERDP
CliprdrClientContext* cliprdrContext = nullptr;
{
std::lock_guard<std::mutex> guard(m_cliprdrMutex);
cliprdrContext = reinterpret_cast<CliprdrClientContext*>(m_cliprdrContext);
}
if (cliprdrContext == nullptr) {
return;
}
if (cliprdrContext->ClientFormatListResponse != nullptr) {
CLIPRDR_FORMAT_LIST_RESPONSE response = {};
response.common.msgFlags = CB_RESPONSE_OK;
const UINT rc = cliprdrContext->ClientFormatListResponse(cliprdrContext, &response);
Q_UNUSED(rc);
}
if (hasUnicodeText && cliprdrContext->ClientFormatDataRequest != nullptr) {
CLIPRDR_FORMAT_DATA_REQUEST request = {};
request.requestedFormatId = CF_UNICODETEXT;
const UINT rc = cliprdrContext->ClientFormatDataRequest(cliprdrContext, &request);
Q_UNUSED(rc);
}
#else
Q_UNUSED(hasUnicodeText);
#endif
}
void RdpSessionBackend::onCliprdrServerFormatDataRequest(uint32_t requestedFormatId)
{
#ifdef ORBITHUB_HAS_FREERDP
CliprdrClientContext* cliprdrContext = nullptr;
QString pendingText;
{
std::lock_guard<std::mutex> guard(m_cliprdrMutex);
cliprdrContext = reinterpret_cast<CliprdrClientContext*>(m_cliprdrContext);
pendingText = m_pendingLocalClipboardText;
}
if (cliprdrContext == nullptr || cliprdrContext->ClientFormatDataResponse == nullptr) {
return;
}
CLIPRDR_FORMAT_DATA_RESPONSE response = {};
QByteArray encoded;
if (requestedFormatId == CF_UNICODETEXT) {
const int byteLength = (pendingText.size() + 1) * static_cast<int>(sizeof(ushort));
encoded = QByteArray(reinterpret_cast<const char*>(pendingText.utf16()), byteLength);
}
response.common.msgFlags = encoded.isEmpty() ? CB_RESPONSE_FAIL : CB_RESPONSE_OK;
response.common.dataLen = static_cast<UINT32>(encoded.size());
response.requestedFormatData =
encoded.isEmpty() ? nullptr : reinterpret_cast<const BYTE*>(encoded.constData());
const UINT rc = cliprdrContext->ClientFormatDataResponse(cliprdrContext, &response);
Q_UNUSED(rc);
#else
Q_UNUSED(requestedFormatId);
#endif
}
void RdpSessionBackend::onCliprdrServerFormatDataResponse(bool success,
const uint8_t* data,
uint32_t size)
{
#ifdef ORBITHUB_HAS_FREERDP
if (!success || data == nullptr || size == 0) {
return;
}
const auto* utf16Data = reinterpret_cast<const char16_t*>(data);
const size_t charCount = size / sizeof(char16_t);
// CF_UNICODETEXT is NUL-terminated per MS-RDPECLIP; trim at the first NUL
// rather than trusting dataLen to exclude it.
size_t length = 0;
while (length < charCount && utf16Data[length] != 0) {
++length;
}
const QString text = QString::fromUtf16(utf16Data, static_cast<int>(length));
emit remoteClipboardTextChanged(text);
#else
Q_UNUSED(success);
Q_UNUSED(data);
Q_UNUSED(size);
#endif
}
void RdpSessionBackend::emitStateAsync(SessionState state, const QString& message)
{
QMetaObject::invokeMethod(
this,
[this, state, message]() {
if (state == SessionState::Disconnected) {
m_stopRequested.store(false);
m_userInitiatedDisconnect.store(false);
}
setState(state, message);
},
Qt::QueuedConnection);
}
void RdpSessionBackend::emitConnectionFailureAsync(const QString& displayMessage,
const QString& rawMessage)
{
QMetaObject::invokeMethod(
this,
[this, displayMessage, rawMessage]() { emit connectionError(displayMessage, rawMessage); },
Qt::QueuedConnection);
}
int RdpSessionBackend::sanitizeDesktopWidth(int width)
{
if (width <= 0) {
return kDefaultDesktopWidth;
}
return qBound(kMinDesktopWidth, width, kMaxDesktopWidth);
}
int RdpSessionBackend::sanitizeDesktopHeight(int height)
{
if (height <= 0) {
return kDefaultDesktopHeight;
}
return qBound(kMinDesktopHeight, height, kMaxDesktopHeight);
}