Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test
- name: Run tests with coverage
run: npm run test:coverage

- name: Run SonarQube scan
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ native/rdp-spike/*.ppm
# FreeRDP static-build working dirs (see build-freerdp-static.sh)
native/rdp-spike/.freerdp-src/
native/rdp-spike/.freerdp-static/
coverage/
163 changes: 161 additions & 2 deletions native/rdp-spike/sidecar.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,23 @@
* This milestone is output-only (read-only desktop view). Input injection
* (mouse/keyboard over stdin) is the next milestone.
*
* Usage: rdp-sidecar <host> <port> <user> <password> [width] [height]
* Usage: rdp-sidecar <host> <port> <user> [width] [height]
* The password is read as the first line of stdin so it never appears in the
* process list.
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef _WIN32
#include <io.h>
#include <fcntl.h>
#endif

#include <freerdp/freerdp.h>
#include <freerdp/client.h>
#include <freerdp/error.h>
#include <freerdp/gdi/gdi.h>
#include <freerdp/codec/color.h>
#include <winpr/synch.h>
Expand Down Expand Up @@ -151,6 +159,88 @@ static BOOL sidecar_end_paint(rdpContext* context)
return TRUE;
}

/* Certificate handling. The default client callbacks
* (client_cli_verify_certificate_ex) are interactive: they print the cert
* details to *stdout* — corrupting the binary frame channel — and then read a
* Y/N answer from *stdin*, which rdp.ts closes right after the password. The
* EOF rejects the certificate, so any host not already in
* ~/.config/freerdp/known_hosts2 failed to connect. This was the "works some
* of the time" bug: only hosts trusted during earlier interactive testing
* connected, and they broke again whenever Windows rotated its self-signed
* cert.
*
* Windows RDP hosts almost universally present self-signed certs, so we
* accept for the session (return 2 = temporary trust: nothing persisted, no
* stale known_hosts state to go bad later) and log the fingerprint to stderr.
* Same trust-on-use posture as the app's SSH host-key handling; an in-app
* verification UI is a later milestone for both. */
static DWORD sidecar_verify_certificate(freerdp* instance, const char* host, UINT16 port,
const char* common_name, const char* subject,
const char* issuer, const char* fingerprint, DWORD flags)
{
(void)instance;
(void)subject;
(void)issuer;
fprintf(stderr, "[sidecar] accepting certificate for %s:%u (CN=%s)\n", host, (unsigned)port,
common_name ? common_name : "?");
if (fingerprint && !(flags & VERIFY_CERT_FLAG_FP_IS_PEM))
fprintf(stderr, "[sidecar] fingerprint: %s\n", fingerprint);
return 2; /* trust for this session only */
}

static DWORD sidecar_verify_changed_certificate(freerdp* instance, const char* host, UINT16 port,
const char* common_name, const char* subject,
const char* issuer, const char* new_fingerprint,
const char* old_subject, const char* old_issuer,
const char* old_fingerprint, DWORD flags)
{
(void)old_subject;
(void)old_issuer;
(void)old_fingerprint;
return sidecar_verify_certificate(instance, host, port, common_name, subject, issuer,
new_fingerprint, flags);
}

/* Map the common connect failures to messages a person can act on. rdp.ts
* surfaces the last "[sidecar] error: ..." stderr line in the RDP tab, so this
* is what the user sees when a connect fails. */
static const char* connect_error_message(UINT32 code)
{
switch (code)
{
case FREERDP_ERROR_CONNECT_LOGON_FAILURE:
case FREERDP_ERROR_AUTHENTICATION_FAILED:
return "Sign-in failed: the username or password is incorrect.";
case FREERDP_ERROR_CONNECT_ACCOUNT_LOCKED_OUT:
return "Sign-in failed: the account is locked out.";
case FREERDP_ERROR_CONNECT_ACCOUNT_DISABLED:
return "Sign-in failed: the account is disabled.";
case FREERDP_ERROR_CONNECT_ACCOUNT_EXPIRED:
return "Sign-in failed: the account has expired.";
case FREERDP_ERROR_CONNECT_ACCOUNT_RESTRICTION:
return "Sign-in failed: an account restriction blocked the logon.";
case FREERDP_ERROR_CONNECT_PASSWORD_EXPIRED:
case FREERDP_ERROR_CONNECT_PASSWORD_CERTAINLY_EXPIRED:
return "Sign-in failed: the password has expired and must be changed.";
case FREERDP_ERROR_CONNECT_PASSWORD_MUST_CHANGE:
return "Sign-in failed: the password must be changed before signing in.";
case FREERDP_ERROR_CONNECT_FAILED:
case FREERDP_ERROR_CONNECT_TRANSPORT_FAILED:
return "Could not reach the host. Check the address, port, and that Remote Desktop is enabled.";
case FREERDP_ERROR_DNS_NAME_NOT_FOUND:
case FREERDP_ERROR_DNS_ERROR:
return "Could not resolve the hostname. Check the address.";
case FREERDP_ERROR_TLS_CONNECT_FAILED:
return "TLS negotiation with the host failed.";
case FREERDP_ERROR_SECURITY_NEGO_CONNECT_FAILED:
return "Security negotiation failed. The host may require NLA settings this client did not offer.";
case FREERDP_ERROR_CONNECT_CANCELLED:
return "The connection was cancelled.";
default:
return NULL;
}
}

static BOOL sidecar_post_connect(freerdp* instance)
{
if (!gdi_init(instance, PIXEL_FORMAT_BGRA32))
Expand All @@ -164,6 +254,10 @@ static BOOL sidecar_client_new(freerdp* instance, rdpContext* context)
{
(void)context;
instance->PostConnect = sidecar_post_connect;
/* Replace the interactive CLI cert prompts (stdout/stdin) — see
* sidecar_verify_certificate above. */
instance->VerifyCertificateEx = sidecar_verify_certificate;
instance->VerifyChangedCertificateEx = sidecar_verify_changed_certificate;
return TRUE;
}

Expand Down Expand Up @@ -208,12 +302,32 @@ int main(int argc, char* argv[])
return 2;
}

#ifdef _WIN32
/* stdout defaults to text mode on Windows and translates \n -> \r\n,
* which corrupts the binary frame stream. */
_setmode(_fileno(stdout), _O_BINARY);
#endif

const char* host = argv[1];
const UINT32 port = (UINT32)strtoul(argv[2], NULL, 10);
const char* user = argv[3];
const UINT32 width = (argc >= 5) ? (UINT32)strtoul(argv[4], NULL, 10) : 1280;
const UINT32 height = (argc >= 6) ? (UINT32)strtoul(argv[5], NULL, 10) : 800;

/* "DOMAIN\user" must go into separate Domain/Username settings for NLA;
* xfreerdp does this split in its command-line layer, so we mirror it. UPN
* form ("user@domain") is understood natively and passes through as-is. */
const char* domain = NULL;
char userbuf[256] = { 0 };
const char* backslash = strchr(user, '\\');
if (backslash && backslash != user && (size_t)(backslash - user) < sizeof(userbuf))
{
memcpy(userbuf, user, (size_t)(backslash - user));
userbuf[backslash - user] = '\0';
domain = userbuf;
user = backslash + 1;
}

/* Read password from stdin to avoid exposing it in process list */
char pass[256];
if (!fgets(pass, sizeof(pass), stdin)) {
Expand All @@ -240,18 +354,45 @@ int main(int argc, char* argv[])
freerdp_settings_set_string(settings, FreeRDP_ServerHostname, host);
freerdp_settings_set_uint32(settings, FreeRDP_ServerPort, port);
freerdp_settings_set_string(settings, FreeRDP_Username, user);
if (domain)
freerdp_settings_set_string(settings, FreeRDP_Domain, domain);
freerdp_settings_set_string(settings, FreeRDP_Password, pass);
freerdp_settings_set_bool(settings, FreeRDP_IgnoreCertificate, FALSE);
freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, width);
freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, height);
freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth, 32);

/* The static FreeRDP we ship is trimmed: channel addins (rdpgfx, rdpdr,
* rdpsnd, cliprdr, ...) are not built in. FreeRDP's defaults still enable
* the features backed by those channels, so freerdp_client_load_addins
* tries to load them, fails, and pre-connect aborts before a TCP
* connection is even attempted. Turn every channel-backed feature off —
* this viewer is a plain GDI framebuffer and needs none of them. */
freerdp_settings_set_bool(settings, FreeRDP_SupportGraphicsPipeline, FALSE);
freerdp_settings_set_bool(settings, FreeRDP_NetworkAutoDetect, FALSE);
freerdp_settings_set_bool(settings, FreeRDP_SupportHeartbeatPdu, FALSE);
freerdp_settings_set_bool(settings, FreeRDP_SupportMultitransport, FALSE);
freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection, FALSE);
freerdp_settings_set_bool(settings, FreeRDP_RedirectClipboard, FALSE);
freerdp_settings_set_bool(settings, FreeRDP_AudioPlayback, FALSE);
freerdp_settings_set_bool(settings, FreeRDP_AudioCapture, FALSE);
freerdp_settings_set_bool(settings, FreeRDP_SupportDisplayControl, FALSE);
freerdp_settings_set_bool(settings, FreeRDP_SupportGeometryTracking, FALSE);
freerdp_settings_set_bool(settings, FreeRDP_SupportVideoOptimized, FALSE);
freerdp_settings_set_bool(settings, FreeRDP_MultiTouchInput, FALSE);

freerdp* instance = context->instance;

int rc = 0;
if (!freerdp_connect(instance))
{
fprintf(stderr, "[sidecar] connect failed err=0x%08X\n", freerdp_get_last_error(context));
const UINT32 err = freerdp_get_last_error(context);
const char* friendly = connect_error_message(err);
if (friendly)
fprintf(stderr, "[sidecar] error: %s\n", friendly);
else
fprintf(stderr, "[sidecar] error: connect failed — %s (0x%08X)\n",
freerdp_get_last_error_string(err), err);
rc = 1;
goto cleanup;
}
Expand Down Expand Up @@ -279,6 +420,24 @@ int main(int argc, char* argv[])
break;
}

/* If the server ended the session, surface why instead of a silent drop.
* A deliberate sign-out/disconnect is a normal end; everything else
* (kicked by another connection, idle timeout, license/protocol errors)
* exits nonzero so rdp.ts shows the reason in the tab. */
{
const UINT32 info = freerdp_error_info(instance);
const BOOL normalEnd = info == ERRINFO_SUCCESS ||
info == ERRINFO_RPC_INITIATED_DISCONNECT ||
info == ERRINFO_RPC_INITIATED_LOGOFF ||
info == ERRINFO_LOGOFF_BY_USER;
if (!normalEnd)
{
fprintf(stderr, "[sidecar] error: session ended by server — %s\n",
freerdp_get_error_info_string(info));
rc = 1;
}
}

freerdp_disconnect(instance);

cleanup:
Expand Down
Loading
Loading