Internal
Public Access
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>
2491 lines
90 KiB
C++
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);
|
|
}
|