#include "rdp_session_backend.h" #include #include #include #include #include #include #include #include #include #ifdef ORBITHUB_HAS_FREERDP #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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(context); return orbit->backend; } BOOL orbitContextNew(freerdp*, rdpContext* context) { auto* orbit = reinterpret_cast(context); orbit->backend = g_contextBackend; return orbit->backend != nullptr; } void orbitContextFree(freerdp*, rdpContext* context) { if (context == nullptr) { return; } auto* orbit = reinterpret_cast(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(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(width), static_cast(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(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(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(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(pointer); if (orbitPointer->rgbaData == nullptr || orbitPointer->rgbaWidth == 0 || orbitPointer->rgbaHeight == 0) { return TRUE; } const QImage image(orbitPointer->rgbaData, static_cast(orbitPointer->rgbaWidth), static_cast(orbitPointer->rgbaHeight), QImage::Format_ARGB32); emit backend->cursorImageChanged( image.copy(), QPoint(static_cast(pointer->xPos), static_cast(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( freerdp_settings_get_uint32(context->settings, FreeRDP_DesktopWidth)); const int height = static_cast( 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(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(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(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(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(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(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(context->custom); const bool success = (formatDataResponse->common.msgFlags & CB_RESPONSE_OK) != 0; backend->onCliprdrServerFormatDataResponse( success, reinterpret_cast(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 guard(m_displayControlMutex); m_displayControlContext = nullptr; m_displayControlReady = false; } { std::lock_guard 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 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 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(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(desktopWidth)); freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, static_cast(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 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 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 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 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(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 guard(m_instanceMutex); m_instance = nullptr; } m_workerRunning.store(false); #endif } void RdpSessionBackend::enqueueInputEvent(const InputEvent& event) { std::lock_guard 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(width)); freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, static_cast(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 guard(m_displayControlMutex); dispContext = reinterpret_cast(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(width); layout.Height = static_cast(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( std::lround((static_cast(width) / effectiveDpi) * kMillimetersPerInch)); layout.PhysicalHeight = static_cast( std::lround((static_cast(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(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(width), static_cast(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(qMin(width, 65535)), static_cast(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 guard(m_cliprdrMutex); m_pendingLocalClipboardText = text; cliprdrContext = reinterpret_cast(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 pending; { std::lock_guard 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(event.x), static_cast(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(event.x), static_cast(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(event.x), static_cast(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(event.x), static_cast(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(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(channelInterface); if (dispContext == nullptr) { return; } dispContext->custom = this; dispContext->DisplayControlCaps = orbitDisplayControlCaps; { std::lock_guard 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(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 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 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 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 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 guard(m_cliprdrMutex); cliprdrContext = reinterpret_cast(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(&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 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 guard(m_cliprdrMutex); cliprdrContext = reinterpret_cast(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 guard(m_cliprdrMutex); cliprdrContext = reinterpret_cast(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(sizeof(ushort)); encoded = QByteArray(reinterpret_cast(pendingText.utf16()), byteLength); } response.common.msgFlags = encoded.isEmpty() ? CB_RESPONSE_FAIL : CB_RESPONSE_OK; response.common.dataLen = static_cast(encoded.size()); response.requestedFormatData = encoded.isEmpty() ? nullptr : reinterpret_cast(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(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(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); }