From a8cfe36552d6aa1226ab843b1ea29c3199ae6f7a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:08:47 +0000 Subject: [PATCH 1/6] Initial plan From bb98e40b772eebfc171f9f8237712b8b7b112036 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:13:01 +0000 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20add=20RdpBridge=20SDK=20=E2=80=93?= =?UTF-8?q?=20C=20ABI,=20CMake=20build,=20CI=20workflow,=20and=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 151 ++++++ CMakeLists.txt | 92 ++++ README.md | 90 +++- examples/minimal/CMakeLists.txt | 17 + examples/minimal/main.c | 107 ++++ include/rdp_bridge.h | 154 ++++++ src/RdpBridge/rdp_bridge.cpp | 868 ++++++++++++++++++++++++++++++++ 7 files changed, 1478 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build.yml create mode 100644 CMakeLists.txt create mode 100644 examples/minimal/CMakeLists.txt create mode 100644 examples/minimal/main.c create mode 100644 include/rdp_bridge.h create mode 100644 src/RdpBridge/rdp_bridge.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..1f1ec7d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,151 @@ +name: Build + +on: + push: + branches: [ "main" ] + tags: [ "v*" ] + pull_request: + branches: [ "main" ] + +jobs: + # --------------------------------------------------------------------------- + # Linux build + # --------------------------------------------------------------------------- + build-linux: + name: Linux (GCC) + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y \ + build-essential cmake ninja-build pkg-config \ + libssl-dev \ + libfreerdp3-dev libwinpr3-dev \ + libfreerdp-client3-dev + + - name: Configure (Release) + run: | + cmake -S . -B build \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DRDPBRIDGE_BUILD_EXAMPLES=ON + + - name: Build + run: cmake --build build --config Release + + - name: Verify shared library exists + run: ls -lh build/libRdpBridge.so + + # ----------------------------------------------------------------------- + # Package the SDK: header + shared library + # ----------------------------------------------------------------------- + - name: Package SDK (Linux) + run: | + mkdir -p sdk-linux/include sdk-linux/lib + cp include/rdp_bridge.h sdk-linux/include/ + cp build/libRdpBridge.so sdk-linux/lib/ + tar -czf RdpBridge-linux-x64.tar.gz -C sdk-linux . + + - name: Upload artifact (Linux) + uses: actions/upload-artifact@v4 + with: + name: RdpBridge-linux-x64 + path: RdpBridge-linux-x64.tar.gz + + # --------------------------------------------------------------------------- + # Windows build + # --------------------------------------------------------------------------- + build-windows: + name: Windows (MSVC) + runs-on: windows-2022 + + steps: + - uses: actions/checkout@v4 + + # Install FreeRDP via vcpkg (bundled on the runner) + - name: Set up vcpkg + run: | + git clone --depth=1 https://github.com/microsoft/vcpkg "${{ github.workspace }}/vcpkg" + "${{ github.workspace }}/vcpkg/bootstrap-vcpkg.bat" -disableMetrics + shell: cmd + + - name: Install FreeRDP via vcpkg + run: | + "${{ github.workspace }}/vcpkg/vcpkg.exe" install freerdp:x64-windows openssl:x64-windows + shell: cmd + + - name: Configure (Release) + run: | + cmake -S . -B build ` + -G "Visual Studio 17 2022" -A x64 ` + -DCMAKE_BUILD_TYPE=Release ` + -DRDPBRIDGE_BUILD_EXAMPLES=ON ` + -DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake" + shell: pwsh + + - name: Build + run: cmake --build build --config Release + shell: pwsh + + - name: Verify DLL exists + run: | + if (!(Test-Path "build/Release/RdpBridge.dll")) { + Write-Error "RdpBridge.dll not found" + exit 1 + } + Get-Item "build/Release/RdpBridge.dll" + shell: pwsh + + # ----------------------------------------------------------------------- + # Package the SDK: header + DLL + import library + # ----------------------------------------------------------------------- + - name: Package SDK (Windows) + run: | + New-Item -ItemType Directory -Force sdk-windows/include | Out-Null + New-Item -ItemType Directory -Force sdk-windows/lib | Out-Null + New-Item -ItemType Directory -Force sdk-windows/bin | Out-Null + Copy-Item include/rdp_bridge.h sdk-windows/include/ + Copy-Item build/Release/RdpBridge.dll sdk-windows/bin/ + Copy-Item build/Release/RdpBridge.lib sdk-windows/lib/ -ErrorAction SilentlyContinue + Compress-Archive -Path sdk-windows/* -DestinationPath RdpBridge-windows-x64.zip + shell: pwsh + + - name: Upload artifact (Windows) + uses: actions/upload-artifact@v4 + with: + name: RdpBridge-windows-x64 + path: RdpBridge-windows-x64.zip + + # --------------------------------------------------------------------------- + # GitHub Release (tag pushes only) + # --------------------------------------------------------------------------- + release: + name: Publish Release + if: startsWith(github.ref, 'refs/tags/v') + needs: [ build-linux, build-windows ] + runs-on: ubuntu-22.04 + permissions: + contents: write + + steps: + - name: Download Linux artifact + uses: actions/download-artifact@v4 + with: + name: RdpBridge-linux-x64 + + - name: Download Windows artifact + uses: actions/download-artifact@v4 + with: + name: RdpBridge-windows-x64 + + - name: Publish GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: | + RdpBridge-linux-x64.tar.gz + RdpBridge-windows-x64.zip + generate_release_notes: true diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..55c802f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,92 @@ +cmake_minimum_required(VERSION 3.22) +project(RdpBridge VERSION 1.0.0 LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# --------------------------------------------------------------------------- +# Options +# --------------------------------------------------------------------------- +option(RDPBRIDGE_BUILD_EXAMPLES "Build usage examples" ON) + +# --------------------------------------------------------------------------- +# Dependencies +# --------------------------------------------------------------------------- +find_package(FreeRDP REQUIRED CONFIG) +find_package(WinPR REQUIRED CONFIG) +find_package(OpenSSL REQUIRED) + +find_path(FREERDP_INCLUDE_DIR NAMES freerdp/freerdp.h PATH_SUFFIXES freerdp3) + +# --------------------------------------------------------------------------- +# RdpBridge shared library +# --------------------------------------------------------------------------- +add_library(RdpBridge SHARED + src/RdpBridge/rdp_bridge.cpp + include/rdp_bridge.h +) + +target_include_directories(RdpBridge + PUBLIC + $ + $ + PRIVATE + ${FREERDP_INCLUDE_DIR} +) + +target_link_libraries(RdpBridge + PRIVATE + freerdp + winpr + OpenSSL::Crypto +) + +target_compile_definitions(RdpBridge PRIVATE RDP_BRIDGE_EXPORTS) + +# Platform-specific library naming and RPATH +if(WIN32) + set_target_properties(RdpBridge PROPERTIES PREFIX "") + set_target_properties(RdpBridge PROPERTIES OUTPUT_NAME "RdpBridge") +elseif(APPLE) + set_target_properties(RdpBridge PROPERTIES + INSTALL_RPATH "@loader_path" + BUILD_WITH_INSTALL_RPATH TRUE + OUTPUT_NAME "RdpBridge" + ) +else() + set_target_properties(RdpBridge PROPERTIES + INSTALL_RPATH "$ORIGIN" + BUILD_WITH_INSTALL_RPATH TRUE + OUTPUT_NAME "RdpBridge" + ) +endif() + +# --------------------------------------------------------------------------- +# Install rules +# --------------------------------------------------------------------------- +include(GNUInstallDirs) + +install(TARGETS RdpBridge + EXPORT RdpBridgeTargets + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} +) + +install(FILES include/rdp_bridge.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) + +install(EXPORT RdpBridgeTargets + FILE RdpBridgeTargets.cmake + NAMESPACE RdpBridge:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/RdpBridge +) + +# --------------------------------------------------------------------------- +# Examples +# --------------------------------------------------------------------------- +if(RDPBRIDGE_BUILD_EXAMPLES) + add_subdirectory(examples/minimal) +endif() diff --git a/README.md b/README.md index e6f1aeb..e16a1fc 100644 --- a/README.md +++ b/README.md @@ -1 +1,89 @@ -# rdp-bridge \ No newline at end of file +# RdpBridge + +A production-grade native RDP SDK built on [FreeRDP](https://www.freerdp.com/), +exposing a clean **C ABI** suitable for P/Invoke from C#, Avalonia, and other +languages. + +## Features + +- **Session lifecycle** – create, connect, disconnect, destroy +- **BGRA32 framebuffer callbacks** – zero-copy raw pixel delivery +- **Input** – mouse (move / click / wheel), keyboard scancode, Unicode input +- **Security profiles** – automatic NLA → TLS → RDP → negotiate fallback +- **Thread-safe** – connection on a dedicated worker thread, atomic session state +- **Cross-platform** – Windows (MSVC → `RdpBridge.dll`) and Linux (GCC/Clang → `libRdpBridge.so`) + +--- + +## Quick Start + +### Build (Linux) + +```bash +sudo apt-get install -y build-essential cmake ninja-build \ + libssl-dev libfreerdp3-dev libwinpr3-dev + +cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release +cmake --build build +``` + +### Build (Windows) + +```powershell +# Prerequisites: Visual Studio 2022, CMake, vcpkg +vcpkg install freerdp:x64-windows openssl:x64-windows + +cmake -S . -B build -G "Visual Studio 17 2022" -A x64 ` + -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" +cmake --build build --config Release +``` + +--- + +## C API + +```c +#include "rdp_bridge.h" + +void* session = RdpBridge_create(); + +RdpBridge_set_callbacks(session, on_frame, on_status, on_disconnect, user_data); + +RdpBridge_connect(session, "192.168.1.1", 3389, "Administrator", "password", 1920, 1080); + +// ... run your event loop ... + +RdpBridge_disconnect(session); +RdpBridge_destroy(session); +``` + +Full API reference: [`include/rdp_bridge.h`](include/rdp_bridge.h) + +--- + +## Repository Layout + +``` +/include Public header (rdp_bridge.h) +/src/RdpBridge Core implementation (rdp_bridge.cpp) +/cmake (reserved for future CMake helpers) +/examples/minimal Minimal C usage example +/.github/workflows CI/CD (build.yml) +``` + +--- + +## CI / CD + +| Event | Action | +|---|---| +| Push to `main` | Build Linux SO + Windows DLL | +| Tag `v*` | Build + package + upload to GitHub Release | + +--- + +## License + +This project is derived from +[CxShell/CxRdpBridge](https://github.com/xiaochengzjc/CxShell/tree/master/native/CxRdpBridge). +See [LICENSE](LICENSE) for details. \ No newline at end of file diff --git a/examples/minimal/CMakeLists.txt b/examples/minimal/CMakeLists.txt new file mode 100644 index 0000000..921a404 --- /dev/null +++ b/examples/minimal/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.22) +project(RdpBridgeMinimalExample LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# When built standalone (outside the SDK tree) find the installed package. +# When built as part of the top-level CMakeLists.txt the target already exists. +if(NOT TARGET RdpBridge) + find_package(RdpBridge REQUIRED CONFIG) + set(RDPBRIDGE_TARGET RdpBridge::RdpBridge) +else() + set(RDPBRIDGE_TARGET RdpBridge) +endif() + +add_executable(rdp_example main.c) +target_link_libraries(rdp_example PRIVATE ${RDPBRIDGE_TARGET}) diff --git a/examples/minimal/main.c b/examples/minimal/main.c new file mode 100644 index 0000000..4ae5ca2 --- /dev/null +++ b/examples/minimal/main.c @@ -0,0 +1,107 @@ +/** + * Minimal RdpBridge usage example. + * + * This program does NOT actually connect to a real RDP server; it merely + * demonstrates how to initialise a session, register callbacks, and tear + * everything down cleanly. The connection attempt will fail (no real server + * is present) but all lifecycle calls are exercised. + * + * Build: + * cmake -S . -B build && cmake --build build + * + * Usage: + * rdp_example [host [port [username [password]]]] + */ + +#include +#include +#include + +#include "rdp_bridge.h" + +/* ------------------------------------------------------------------------- + * Callback implementations + * ---------------------------------------------------------------------- */ + +static void on_frame( + void* user_data, + int width, + int height, + int stride, + const uint8_t* bgra_pixels) +{ + (void)user_data; + (void)bgra_pixels; + printf("[frame] %dx%d stride=%d\n", width, height, stride); +} + +static void on_status(void* user_data, const char* message) +{ + (void)user_data; + printf("[status] %s\n", message); +} + +static void on_disconnect(void* user_data) +{ + (void)user_data; + printf("[disconnect] session closed\n"); +} + +/* ------------------------------------------------------------------------- + * Entry point + * ---------------------------------------------------------------------- */ + +int main(int argc, char* argv[]) +{ + const char* host = argc > 1 ? argv[1] : "127.0.0.1"; + int port = argc > 2 ? atoi(argv[2]) : 3389; + const char* username = argc > 3 ? argv[3] : "Administrator"; + const char* password = argc > 4 ? argv[4] : ""; + + printf("RdpBridge minimal example\n"); + printf(" host=%s port=%d user=%s\n\n", host, port, username); + + /* 1. Create session -------------------------------------------------- */ + void* session = RdpBridge_create(); + if (!session) + { + fprintf(stderr, "RdpBridge_create() failed\n"); + return 1; + } + + /* 2. Register callbacks ---------------------------------------------- */ + RdpBridge_set_callbacks(session, on_frame, on_status, on_disconnect, NULL); + + /* 3. Connect (non-blocking: starts a worker thread) ------------------ */ + int rc = RdpBridge_connect(session, host, port, username, password, 1024, 768); + if (rc != 0) + { + fprintf(stderr, "RdpBridge_connect() returned %d: %s\n", + rc, RdpBridge_get_last_error(session)); + RdpBridge_destroy(session); + return 1; + } + + /* 4. In a real application the event loop runs here. + * For this demo we just wait a moment then disconnect. */ +#if defined(_WIN32) + Sleep(3000); +#else + { + struct timespec ts = { 3, 0 }; + nanosleep(&ts, NULL); + } +#endif + + /* 5. Disconnect & destroy -------------------------------------------- */ + printf("\nDisconnecting...\n"); + RdpBridge_disconnect(session); + + const char* lastErr = RdpBridge_get_last_error(session); + if (lastErr && lastErr[0] != '\0') + printf("Last error: %s\n", lastErr); + + RdpBridge_destroy(session); + printf("Done.\n"); + return 0; +} diff --git a/include/rdp_bridge.h b/include/rdp_bridge.h new file mode 100644 index 0000000..12481ef --- /dev/null +++ b/include/rdp_bridge.h @@ -0,0 +1,154 @@ +#pragma once + +#include + +#if defined(_WIN32) +#if defined(RDP_BRIDGE_EXPORTS) +#define RDP_BRIDGE_API __declspec(dllexport) +#else +#define RDP_BRIDGE_API __declspec(dllimport) +#endif +#else +#define RDP_BRIDGE_API __attribute__((visibility("default"))) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Framebuffer callback: called whenever a new frame is available. + * + * @param user_data Opaque pointer supplied via RdpBridge_set_callbacks. + * @param width Frame width in pixels. + * @param height Frame height in pixels. + * @param stride Row stride in bytes (= width * 4 for BGRA32). + * @param bgra_pixels Pointer to the raw BGRA32 pixel buffer (valid only + * during the callback). + */ +typedef void (*RdpBridge_FrameCallback)( + void* user_data, + int width, + int height, + int stride, + const uint8_t* bgra_pixels); + +/** + * Status callback: called for log / state-change messages. + * + * @param user_data Opaque pointer supplied via RdpBridge_set_callbacks. + * @param message Null-terminated UTF-8 status string. + */ +typedef void (*RdpBridge_StatusCallback)(void* user_data, const char* message); + +/** + * Disconnect callback: called when the remote session is closed. + * + * @param user_data Opaque pointer supplied via RdpBridge_set_callbacks. + */ +typedef void (*RdpBridge_DisconnectCallback)(void* user_data); + +/** + * Create a new RDP bridge session. + * + * @return Opaque session handle, or NULL on failure. + */ +RDP_BRIDGE_API void* RdpBridge_create(void); + +/** + * Destroy a session previously created with RdpBridge_create. + * Calls RdpBridge_disconnect internally if the session is still active. + * + * @param handle Session handle returned by RdpBridge_create. + */ +RDP_BRIDGE_API void RdpBridge_destroy(void* handle); + +/** + * Register event callbacks for a session. + * May be called before or after RdpBridge_connect. + * + * @param handle Session handle. + * @param frame_callback Called on every rendered frame (may be NULL). + * @param status_callback Called for status / log messages (may be NULL). + * @param disconnect_callback Called when the session disconnects (may be NULL). + * @param user_data Passed unchanged to every callback. + */ +RDP_BRIDGE_API void RdpBridge_set_callbacks( + void* handle, + RdpBridge_FrameCallback frame_callback, + RdpBridge_StatusCallback status_callback, + RdpBridge_DisconnectCallback disconnect_callback, + void* user_data); + +/** + * Connect to an RDP server. + * Attempts NLA, TLS, RDP-legacy, and negotiate security profiles in order. + * The connection runs on a dedicated background thread. + * + * @param handle Session handle. + * @param host Server hostname or IP address (UTF-8, must not be NULL). + * @param port TCP port (use 3389 for the default). + * @param username Login username (UTF-8, must not be NULL). + * @param password Login password (UTF-8, must not be NULL). + * @param width Initial desktop width in pixels. + * @param height Initial desktop height in pixels. + * @return 0 – connection thread started successfully. + * -1 – invalid handle. + * -2 – Winsock initialisation failed (Windows only). + */ +RDP_BRIDGE_API int RdpBridge_connect( + void* handle, + const char* host, + int port, + const char* username, + const char* password, + int width, + int height); + +/** + * Disconnect an active session and wait for the worker thread to exit. + * + * @param handle Session handle. + */ +RDP_BRIDGE_API void RdpBridge_disconnect(void* handle); + +/** + * Send a mouse event to the remote session. + * + * @param handle Session handle. + * @param flags PTR_FLAGS_* bitmask (FreeRDP / MS-RDPBCGR §2.2.8.1.1.3.1.1). + * @param x Pointer X coordinate in desktop pixels. + * @param y Pointer Y coordinate in desktop pixels. + */ +RDP_BRIDGE_API void RdpBridge_send_pointer(void* handle, uint16_t flags, uint16_t x, uint16_t y); + +/** + * Send a keyboard scancode event to the remote session. + * + * @param handle Session handle. + * @param key RDP scancode / virtual key (depends on FreeRDP API layer). + * @param down Non-zero for key-press, zero for key-release. + */ +RDP_BRIDGE_API void RdpBridge_send_key(void* handle, uint32_t key, int down); + +/** + * Send a Unicode character event to the remote session. + * + * @param handle Session handle. + * @param code UTF-16 code unit to inject. + * @param down Non-zero for key-press, zero for key-release. + */ +RDP_BRIDGE_API void RdpBridge_send_unicode_key(void* handle, uint16_t code, int down); + +/** + * Return the last error message for this session. + * + * @param handle Session handle. + * @return Null-terminated UTF-8 string. Returns "" when handle is NULL. + * The pointer is valid until the next API call on the same handle. + */ +RDP_BRIDGE_API const char* RdpBridge_get_last_error(void* handle); + +#ifdef __cplusplus +} +#endif diff --git a/src/RdpBridge/rdp_bridge.cpp b/src/RdpBridge/rdp_bridge.cpp new file mode 100644 index 0000000..e25e57b --- /dev/null +++ b/src/RdpBridge/rdp_bridge.cpp @@ -0,0 +1,868 @@ +#include "rdp_bridge.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#include +#else +#include +#endif + +#include +#include + +extern "C" { +#include +#include +#include +#include +#include +#include +#include +#include +} + +namespace +{ + +// --------------------------------------------------------------------------- +// Internal session state +// --------------------------------------------------------------------------- + +struct RdpSession +{ + freerdp* instance = nullptr; + std::thread worker; + std::atomic_bool running{false}; + std::atomic_bool connected{false}; + std::mutex callback_mutex; + std::string host; + std::string username; + std::string password; + int port = 3389; + int width = 1024; + int height = 768; + bool wsa_started = false; + bool openssl_providers_loaded = false; + RdpBridge_FrameCallback frame_callback = nullptr; + RdpBridge_StatusCallback status_callback = nullptr; + RdpBridge_DisconnectCallback disconnect_callback = nullptr; + void* user_data = nullptr; + std::string last_error; +}; + +struct RdpContext +{ + rdpContext context; + RdpSession* session = nullptr; +}; + +struct SecurityProfile +{ + const char* name; + bool nla; + bool tls; + bool rdp; + bool negotiate; + bool useRdpSecurityLayer; +}; + +// --------------------------------------------------------------------------- +// Forward declarations +// --------------------------------------------------------------------------- + +BOOL rdp_pre_connect(freerdp* instance); +BOOL rdp_post_connect(freerdp* instance); +void rdp_post_disconnect(freerdp* instance); +BOOL rdp_authenticate(freerdp* instance, char** username, char** password, char** domain, rdp_auth_reason reason); +DWORD rdp_verify_certificate( + freerdp* instance, + const char* host, + UINT16 port, + const char* common_name, + const char* subject, + const char* issuer, + const char* fingerprint, + DWORD flags); +DWORD rdp_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); +int rdp_verify_x509_certificate( + freerdp* instance, + const BYTE* data, + size_t length, + const char* hostname, + UINT16 port, + DWORD flags); +int rdp_logon_error(freerdp* instance, UINT32 data, UINT32 type); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +RdpSession* get_session(rdpContext* context) +{ + if (!context) + return nullptr; + return reinterpret_cast(context)->session; +} + +rdpSettings* get_settings(RdpSession* session) +{ + if (!session || !session->instance || !session->instance->context) + return nullptr; + return session->instance->context->settings; +} + +bool initialize_instance(RdpSession* session) +{ + if (!session) + return false; + + session->instance = freerdp_new(); + if (!session->instance) + return false; + + session->instance->PreConnect = rdp_pre_connect; + session->instance->PostConnect = rdp_post_connect; + session->instance->PostDisconnect = rdp_post_disconnect; + session->instance->AuthenticateEx = rdp_authenticate; + session->instance->VerifyCertificateEx = rdp_verify_certificate; + session->instance->VerifyChangedCertificateEx = rdp_verify_changed_certificate; + session->instance->VerifyX509Certificate = rdp_verify_x509_certificate; + session->instance->LogonErrorInfo = rdp_logon_error; + session->instance->ContextSize = sizeof(RdpContext); + + if (!freerdp_context_new(session->instance)) + { + freerdp_free(session->instance); + session->instance = nullptr; + return false; + } + + reinterpret_cast(session->instance->context)->session = session; + return true; +} + +void free_instance(RdpSession* session) +{ + if (!session || !session->instance) + return; + + freerdp_context_free(session->instance); + freerdp_free(session->instance); + session->instance = nullptr; +} + +void set_error(RdpSession* session, const std::string& message) +{ + if (session) + session->last_error = message; +} + +void notify_status(RdpSession* session, const char* message) +{ + if (!session) + return; + + std::lock_guard lock(session->callback_mutex); + if (session->status_callback) + session->status_callback(session->user_data, message); +} + +std::string get_library_directory() +{ +#if defined(_WIN32) + HMODULE module = nullptr; + char path[MAX_PATH]{}; + if (!GetModuleHandleExA( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&get_library_directory), + &module)) + return {}; + + const DWORD length = GetModuleFileNameA(module, path, static_cast(sizeof(path))); + if (length == 0 || length >= sizeof(path)) + return {}; + + std::string value(path, length); + const auto index = value.find_last_of("\\/"); + return index == std::string::npos ? std::string{} : value.substr(0, index); +#else + Dl_info info{}; + if (dladdr(reinterpret_cast(&get_library_directory), &info) == 0 || !info.dli_fname) + return {}; + + std::string value(info.dli_fname); + const auto index = value.find_last_of("\\/"); + return index == std::string::npos ? std::string{} : value.substr(0, index); +#endif +} + +void ensure_openssl_providers(RdpSession* session) +{ + if (!session || session->openssl_providers_loaded) + return; + + const auto libDirectory = get_library_directory(); + std::string providerDirectory = libDirectory; + +#if defined(_WIN32) + if (!libDirectory.empty()) + { + SetDllDirectoryA(libDirectory.c_str()); + const auto moduleDirectory = libDirectory + "\\ossl-modules"; + const DWORD attributes = GetFileAttributesA(moduleDirectory.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY)) + providerDirectory = moduleDirectory; + } +#endif + + if (!providerDirectory.empty()) + OSSL_PROVIDER_set_default_search_path(nullptr, providerDirectory.c_str()); + +#if defined(_WIN32) + std::string nativeLoadStatus; + if (!providerDirectory.empty()) + { + const auto providerPath = providerDirectory + "\\legacy.dll"; + SetLastError(0); + HMODULE providerModule = LoadLibraryA(providerPath.c_str()); + if (providerModule) + { + nativeLoadStatus = " nativeLoad=ok"; + } + else + { + std::ostringstream nativeError; + nativeError << " nativeLoad=failed(" << GetLastError() << ")"; + nativeLoadStatus = nativeError.str(); + } + } +#endif + + ERR_clear_error(); + auto* defaultProvider = OSSL_PROVIDER_load(nullptr, "default"); + std::vector opensslErrors; + auto* legacyProvider = OSSL_PROVIDER_load(nullptr, "legacy"); + unsigned long errorCode = 0; + while ((errorCode = ERR_get_error()) != 0) + { + char buffer[256]{}; + ERR_error_string_n(errorCode, buffer, sizeof(buffer)); + opensslErrors.emplace_back(buffer); + } + + std::ostringstream message; + message << "OpenSSL providers default=" + << (defaultProvider ? "loaded" : "failed") + << " legacy=" + << (legacyProvider ? "loaded" : "failed") + << " path=" + << (providerDirectory.empty() ? "" : providerDirectory); +#if defined(_WIN32) + message << nativeLoadStatus; +#endif + if (!opensslErrors.empty()) + { + message << " errors="; + for (size_t i = 0; i < opensslErrors.size(); ++i) + { + if (i > 0) + message << " | "; + message << opensslErrors[i]; + } + } + + notify_status(session, message.str().c_str()); + session->openssl_providers_loaded = true; +} + +char* duplicate_string(const std::string& value) +{ + const auto length = value.size() + 1; + auto* copy = static_cast(std::malloc(length)); + if (!copy) + return nullptr; + std::memcpy(copy, value.c_str(), length); + return copy; +} + +const char* auth_reason_name(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 "GatewayHttp"; + case GW_AUTH_RDG: return "GatewayRdg"; + case GW_AUTH_RPC: return "GatewayRpc"; + case AUTH_SMARTCARD_PIN: return "SmartcardPin"; +#if FREERDP_VERSION_MAJOR > 3 || (FREERDP_VERSION_MAJOR == 3 && FREERDP_VERSION_MINOR >= 26) + case AUTH_RDSTLS: return "Rdstls"; + case AUTH_FIDO_PIN: return "FidoPin"; +#endif + default: return "Unknown"; + } +} + +std::string describe_last_error(RdpSession* session) +{ + if (!session || !session->instance || !session->instance->context) + return "FreeRDP connection failed."; + + const UINT32 code = freerdp_get_last_error(session->instance->context); + const char* name = freerdp_get_last_error_name(code); + const char* text = freerdp_get_last_error_string(code); + const char* category = freerdp_get_last_error_category(code); + + std::ostringstream stream; + stream << "FreeRDP connection failed."; + stream << " code=0x" << std::hex << std::setw(8) << std::setfill('0') << code; + if (name && name[0] != '\0') + stream << " name=" << name; + if (category && category[0] != '\0') + stream << " category=" << category; + if (text && text[0] != '\0') + stream << " message=" << text; + + return stream.str(); +} + +// --------------------------------------------------------------------------- +// FreeRDP callbacks +// --------------------------------------------------------------------------- + +BOOL rdp_authenticate( + freerdp* instance, + char** username, + char** password, + char** domain, + rdp_auth_reason reason) +{ + if (!instance || !instance->context) + return FALSE; + + auto* session = get_session(instance->context); + if (!session) + return FALSE; + + std::ostringstream message; + message << "RDP authenticate callback reason=" << auth_reason_name(reason) + << " usernameLen=" << session->username.size() + << " passwordLen=" << session->password.size(); + notify_status(session, message.str().c_str()); + + if (username) + { + std::free(*username); + *username = duplicate_string(session->username); + } + if (password) + { + std::free(*password); + *password = duplicate_string(session->password); + } + if (domain) + { + std::free(*domain); + *domain = duplicate_string(""); + } + + return TRUE; +} + +DWORD rdp_verify_certificate( + freerdp* instance, + const char* host, + UINT16 port, + const char* common_name, + const char* /*subject*/, + const char* /*issuer*/, + const char* fingerprint, + DWORD flags) +{ + if (instance && instance->context) + { + auto* session = get_session(instance->context); + std::ostringstream message; + message << "RDP certificate accepted host=" << (host ? host : "") + << " port=" << port + << " commonName=" << (common_name ? common_name : "") + << " fingerprint=" << (fingerprint ? fingerprint : "") + << " flags=0x" << std::hex << flags; + notify_status(session, message.str().c_str()); + } + return 2; +} + +DWORD rdp_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) +{ + return rdp_verify_certificate( + instance, host, port, common_name, subject, issuer, new_fingerprint, flags); +} + +int rdp_verify_x509_certificate( + freerdp* instance, + const BYTE* /*data*/, + size_t length, + const char* hostname, + UINT16 port, + DWORD flags) +{ + if (instance && instance->context) + { + auto* session = get_session(instance->context); + std::ostringstream message; + message << "RDP X509 certificate accepted host=" << (hostname ? hostname : "") + << " port=" << port + << " length=" << length + << " flags=0x" << std::hex << flags; + notify_status(session, message.str().c_str()); + } + return 2; +} + +int rdp_logon_error(freerdp* instance, UINT32 data, UINT32 type) +{ + if (instance && instance->context) + { + auto* session = get_session(instance->context); + std::ostringstream message; + message << "RDP logon error data=0x" << std::hex << data << " type=0x" << type; + notify_status(session, message.str().c_str()); + } + return 1; +} + +BOOL rdp_begin_paint(rdpContext* context) +{ + if (!context || !context->gdi) + return FALSE; + + context->gdi->primary->hdc->hwnd->invalid->null = TRUE; + return TRUE; +} + +BOOL rdp_end_paint(rdpContext* context) +{ + if (!context || !context->gdi || !context->instance) + return FALSE; + + auto* session = get_session(context); + if (!session) + return TRUE; + + auto* gdi = context->gdi; + if (!gdi->primary_buffer || gdi->width <= 0 || gdi->height <= 0) + return TRUE; + + const int width = static_cast(gdi->width); + const int height = static_cast(gdi->height); + const int stride = width * 4; + const auto* pixels = static_cast(gdi->primary_buffer); + if (!pixels) + return TRUE; + + std::lock_guard lock(session->callback_mutex); + if (session->frame_callback) + session->frame_callback(session->user_data, width, height, stride, pixels); + + return TRUE; +} + +BOOL rdp_pre_connect(freerdp* instance) +{ + if (!instance || !instance->context || !instance->context->settings) + return FALSE; + + freerdp_settings_set_uint32( + instance->context->settings, FreeRDP_OsMajorType, OSMAJORTYPE_WINDOWS); + freerdp_settings_set_uint32( + instance->context->settings, FreeRDP_OsMinorType, OSMINORTYPE_WINDOWS_NT); + return TRUE; +} + +BOOL rdp_post_connect(freerdp* instance) +{ + if (!instance || !instance->context) + return FALSE; + + auto* session = get_session(instance->context); + if (!session) + return FALSE; + + if (!gdi_init(instance, PIXEL_FORMAT_BGRA32)) + { + set_error(session, "FreeRDP GDI initialization failed."); + return FALSE; + } + + instance->context->update->BeginPaint = rdp_begin_paint; + instance->context->update->EndPaint = rdp_end_paint; + notify_status(session, "RDP connected."); + return TRUE; +} + +void rdp_post_disconnect(freerdp* instance) +{ + if (!instance || !instance->context) + return; + + auto* session = get_session(instance->context); + gdi_free(instance); + + if (session) + { + const bool wasConnected = session->connected.exchange(false); + notify_status(session, "RDP disconnected."); + std::lock_guard lock(session->callback_mutex); + if (wasConnected && session->disconnect_callback) + session->disconnect_callback(session->user_data); + } +} + +// --------------------------------------------------------------------------- +// Connection worker thread +// --------------------------------------------------------------------------- + +void connection_thread(RdpSession* session) +{ + notify_status(session, "Connecting RDP..."); + + { + std::ostringstream target; + target << "RDP target host=" << session->host + << " port=" << session->port + << " user=" << session->username + << " size=" << session->width << "x" << session->height; + notify_status(session, target.str().c_str()); + } + + const SecurityProfile profiles[] = { + { "nla", true, false, false, false, false }, + { "tls", false, true, false, false, false }, + { "rdp", false, false, true, false, true }, + { "negotiate", true, true, true, true, false } + }; + + std::string lastError; + for (const auto& profile : profiles) + { + if (!session->running) + break; + + if (!initialize_instance(session)) + { + lastError = "FreeRDP instance initialization failed."; + set_error(session, lastError); + notify_status(session, lastError.c_str()); + break; + } + + auto* settings = get_settings(session); + if (!settings) + { + lastError = "FreeRDP settings unavailable."; + set_error(session, lastError); + notify_status(session, lastError.c_str()); + free_instance(session); + break; + } + + freerdp_settings_set_string(settings, FreeRDP_ServerHostname, session->host.c_str()); + freerdp_settings_set_string(settings, FreeRDP_UserSpecifiedServerName, session->host.c_str()); + freerdp_settings_set_uint32(settings, FreeRDP_ServerPort, + static_cast(session->port > 0 ? session->port : 3389)); + freerdp_settings_set_string(settings, FreeRDP_Username, session->username.c_str()); + freerdp_settings_set_string(settings, FreeRDP_Password, session->password.c_str()); + freerdp_settings_set_string(settings, FreeRDP_Domain, ""); + freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, + static_cast(session->width > 0 ? session->width : 1024)); + freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, + static_cast(session->height > 0 ? session->height : 768)); + freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth, 32); + freerdp_settings_set_bool(settings, FreeRDP_IgnoreCertificate, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_AutoAcceptCertificate, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_Authentication, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_AutoLogonEnabled, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_LogonNotify, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_LogonErrors, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_LongCredentialsSupported, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_SoftwareGdi, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_RemoteFxCodec, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_GfxThinClient, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_NegotiateSecurityLayer, + profile.negotiate ? TRUE : FALSE); + freerdp_settings_set_bool(settings, FreeRDP_NlaSecurity, + profile.nla ? TRUE : FALSE); + freerdp_settings_set_bool(settings, FreeRDP_TlsSecurity, + profile.tls ? TRUE : FALSE); + freerdp_settings_set_bool(settings, FreeRDP_RdpSecurity, + profile.rdp ? TRUE : FALSE); + freerdp_settings_set_bool(settings, FreeRDP_UseRdpSecurityLayer, + profile.useRdpSecurityLayer ? TRUE : FALSE); + + { + const char* parsedServerName = + freerdp_settings_get_server_name(settings); + const char* parsedHostName = + freerdp_settings_get_string(settings, FreeRDP_ServerHostname); + const char* parsedUserSpecifiedName = + freerdp_settings_get_string(settings, FreeRDP_UserSpecifiedServerName); + const UINT32 parsedPort = + freerdp_settings_get_uint32(settings, FreeRDP_ServerPort); + std::ostringstream parsedTarget; + parsedTarget << "RDP parsed target server=" + << (parsedServerName ? parsedServerName : "") + << " host=" + << (parsedHostName ? parsedHostName : "") + << " userSpecified=" + << (parsedUserSpecifiedName ? parsedUserSpecifiedName : "") + << " port=" << parsedPort; + notify_status(session, parsedTarget.str().c_str()); + } + + { + std::ostringstream security; + security << "RDP security attempt mode=" << profile.name + << " nla=" << (freerdp_settings_get_bool(settings, FreeRDP_NlaSecurity) ? "true" : "false") + << " tls=" << (freerdp_settings_get_bool(settings, FreeRDP_TlsSecurity) ? "true" : "false") + << " rdp=" << (freerdp_settings_get_bool(settings, FreeRDP_RdpSecurity) ? "true" : "false") + << " negotiate=" << (freerdp_settings_get_bool(settings, FreeRDP_NegotiateSecurityLayer) ? "true" : "false"); + notify_status(session, security.str().c_str()); + } + + if (!freerdp_connect(session->instance)) + { + lastError = describe_last_error(session); + std::ostringstream attemptError; + attemptError << lastError << " mode=" << profile.name; + set_error(session, attemptError.str()); + notify_status(session, attemptError.str().c_str()); + free_instance(session); + continue; + } + + session->connected = true; + break; + } + + if (!session->connected) + { + session->running = false; + if (!lastError.empty()) + set_error(session, lastError); + return; + } + + while (session->running) + { + if (freerdp_check_fds(session->instance) != TRUE) + { + set_error(session, "FreeRDP transport closed."); + break; + } + } + + freerdp_disconnect(session->instance); + free_instance(session); + session->running = false; +} + +} // anonymous namespace + +// --------------------------------------------------------------------------- +// Public C ABI +// --------------------------------------------------------------------------- + +extern "C" +{ + +RDP_BRIDGE_API void* RdpBridge_create(void) +{ + return new RdpSession(); +} + +RDP_BRIDGE_API void RdpBridge_destroy(void* handle) +{ + auto* session = static_cast(handle); + if (!session) + return; + + RdpBridge_disconnect(handle); + free_instance(session); + delete session; +} + +RDP_BRIDGE_API void RdpBridge_set_callbacks( + void* handle, + RdpBridge_FrameCallback frame_callback, + RdpBridge_StatusCallback status_callback, + RdpBridge_DisconnectCallback disconnect_callback, + void* user_data) +{ + auto* session = static_cast(handle); + if (!session) + return; + + std::lock_guard lock(session->callback_mutex); + session->frame_callback = frame_callback; + session->status_callback = status_callback; + session->disconnect_callback = disconnect_callback; + session->user_data = user_data; +} + +RDP_BRIDGE_API int RdpBridge_connect( + void* handle, + const char* host, + int port, + const char* username, + const char* password, + int width, + int height) +{ + auto* session = static_cast(handle); + if (!session) + return -1; + + if (session->running) + return 0; + +#if defined(_WIN32) + if (!session->wsa_started) + { + WSADATA wsaData{}; + const int wsaResult = WSAStartup(MAKEWORD(2, 2), &wsaData); + if (wsaResult != 0) + { + std::ostringstream error; + error << "Winsock initialization failed. code=" << wsaResult; + set_error(session, error.str()); + notify_status(session, error.str().c_str()); + return -2; + } + session->wsa_started = true; + } +#endif + + session->host = host ? host : ""; + session->port = port > 0 ? port : 3389; + session->username = username ? username : ""; + session->password = password ? password : ""; + session->width = width > 0 ? width : 1024; + session->height = height > 0 ? height : 768; + + ensure_openssl_providers(session); + + session->running = true; + session->worker = std::thread(connection_thread, session); + return 0; +} + +RDP_BRIDGE_API void RdpBridge_disconnect(void* handle) +{ + auto* session = static_cast(handle); + if (!session) + return; + + session->running = false; + if (session->instance) + freerdp_abort_connect(session->instance); + + if (session->worker.joinable()) + session->worker.join(); + +#if defined(_WIN32) + if (session->wsa_started) + { + WSACleanup(); + session->wsa_started = false; + } +#endif +} + +RDP_BRIDGE_API void RdpBridge_send_pointer(void* handle, uint16_t flags, uint16_t x, uint16_t y) +{ + auto* session = static_cast(handle); + if (!session || !session->connected) + return; + + auto* input = (session->instance && session->instance->context) + ? session->instance->context->input + : nullptr; + if (!input) + return; + + freerdp_input_send_mouse_event(input, flags, x, y); +} + +RDP_BRIDGE_API void RdpBridge_send_key(void* handle, uint32_t key, int down) +{ + auto* session = static_cast(handle); + if (!session || !session->connected) + return; + + auto* input = (session->instance && session->instance->context) + ? session->instance->context->input + : nullptr; + if (!input) + return; + + freerdp_input_send_keyboard_event_ex(input, down ? TRUE : FALSE, FALSE, key); +} + +RDP_BRIDGE_API void RdpBridge_send_unicode_key(void* handle, uint16_t code, int down) +{ + auto* session = static_cast(handle); + if (!session || !session->connected || code == 0) + return; + + auto* input = (session->instance && session->instance->context) + ? session->instance->context->input + : nullptr; + if (!input) + return; + + freerdp_input_send_unicode_keyboard_event(input, down ? 0 : KBD_FLAGS_RELEASE, code); +} + +RDP_BRIDGE_API const char* RdpBridge_get_last_error(void* handle) +{ + auto* session = static_cast(handle); + if (!session) + return ""; + return session->last_error.c_str(); +} + +} // extern "C" From d4913f6fce9fb29b5b1bf7378960e45d9b76e951 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:15:17 +0000 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20address=20code=20review=20=E2=80=93?= =?UTF-8?q?=20named=20constant,=20includes,=20and=20workflow=20permissions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 4 ++++ examples/minimal/main.c | 6 ++++++ src/RdpBridge/rdp_bridge.cpp | 5 ++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1f1ec7d..f08a021 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,6 +14,8 @@ jobs: build-linux: name: Linux (GCC) runs-on: ubuntu-22.04 + permissions: + contents: read steps: - uses: actions/checkout@v4 @@ -62,6 +64,8 @@ jobs: build-windows: name: Windows (MSVC) runs-on: windows-2022 + permissions: + contents: read steps: - uses: actions/checkout@v4 diff --git a/examples/minimal/main.c b/examples/minimal/main.c index 4ae5ca2..47bddd0 100644 --- a/examples/minimal/main.c +++ b/examples/minimal/main.c @@ -17,6 +17,12 @@ #include #include +#if defined(_WIN32) +#include +#else +#include +#endif + #include "rdp_bridge.h" /* ------------------------------------------------------------------------- diff --git a/src/RdpBridge/rdp_bridge.cpp b/src/RdpBridge/rdp_bridge.cpp index e25e57b..4b08aec 100644 --- a/src/RdpBridge/rdp_bridge.cpp +++ b/src/RdpBridge/rdp_bridge.cpp @@ -34,6 +34,9 @@ extern "C" { namespace { +// Bytes per pixel for the BGRA32 pixel format used by this SDK. +static constexpr int BGRA32_BYTES_PER_PIXEL = 4; + // --------------------------------------------------------------------------- // Internal session state // --------------------------------------------------------------------------- @@ -489,7 +492,7 @@ BOOL rdp_end_paint(rdpContext* context) const int width = static_cast(gdi->width); const int height = static_cast(gdi->height); - const int stride = width * 4; + const int stride = width * BGRA32_BYTES_PER_PIXEL; const auto* pixels = static_cast(gdi->primary_buffer); if (!pixels) return TRUE; From 621df18ed51e9bd44686a49898d1140c0372feb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:16:02 +0000 Subject: [PATCH 4/6] fix: use ubuntu-24.04 runner for Linux build (libfreerdp3-dev availability) --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f08a021..f2420a2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: # --------------------------------------------------------------------------- build-linux: name: Linux (GCC) - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 permissions: contents: read @@ -131,7 +131,7 @@ jobs: name: Publish Release if: startsWith(github.ref, 'refs/tags/v') needs: [ build-linux, build-windows ] - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 permissions: contents: write From a0fdda44c23b9c1e429e4c0a46e0a0b886f91b95 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:55:14 +0000 Subject: [PATCH 5/6] Initial plan From 0fe18bedf5edf96f4c76205161c63fca1c159060 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:58:02 +0000 Subject: [PATCH 6/6] fix: use correct FreeRDP dev package name on Ubuntu 24.04 --- .github/workflows/build.yml | 3 +-- README.md | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f2420a2..9a39e24 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,8 +26,7 @@ jobs: sudo apt-get install -y \ build-essential cmake ninja-build pkg-config \ libssl-dev \ - libfreerdp3-dev libwinpr3-dev \ - libfreerdp-client3-dev + freerdp3-dev libwinpr3-dev - name: Configure (Release) run: | diff --git a/README.md b/README.md index e16a1fc..fcde548 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ languages. ```bash sudo apt-get install -y build-essential cmake ninja-build \ - libssl-dev libfreerdp3-dev libwinpr3-dev + libssl-dev freerdp3-dev libwinpr3-dev cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release cmake --build build