diff options
Diffstat (limited to '')
150 files changed, 47554 insertions, 0 deletions
diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt new file mode 100644 index 0000000..fcb0d40 --- /dev/null +++ b/client/CMakeLists.txt @@ -0,0 +1,113 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP Clients +# +# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Clients + +if(WITH_CLIENT_COMMON) + add_subdirectory(common) +endif() + +if(FREERDP_VENDOR AND WITH_CLIENT) + if(WIN32 AND NOT UWP) + add_subdirectory(Windows) + else() + if(WITH_SAMPLE) + add_subdirectory(Sample) + endif() + endif() + + if(WITH_CLIENT_SDL) + find_package(SDL2) + if (SDL2_FOUND) + add_subdirectory(SDL) + else() + message(WARNING "SDL2 requested but not found, continuing build without SDL client") + endif() + endif() + + if(WITH_X11) + add_subdirectory(X11) + endif() + + if(WITH_WAYLAND AND WAYLAND_FOUND) + add_subdirectory(Wayland) + endif() + + if(APPLE) + if(IOS) + if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/iOS") + message(STATUS "Adding iOS client") + add_subdirectory(iOS) + endif() + else() + option(WITH_CLIENT_MAC "Build native mac client" ON) + if (WITH_CLIENT_MAC) + add_subdirectory(Mac) + endif() + endif() + endif() + + if(ANDROID) + message(STATUS "Android client module is built with Android Studio project") + endif() +endif() + +# Pick up other clients +if(WITH_CLIENT) + set(FILENAME "ModuleOptions.cmake") + file(GLOB FILEPATHS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*/${FILENAME}") + + foreach(FILEPATH ${FILEPATHS}) + if(${FILEPATH} MATCHES "^([^/]*)/+${FILENAME}") + string(REGEX REPLACE "^([^/]*)/+${FILENAME}" "\\1" FREERDP_CLIENT ${FILEPATH}) + set(FREERDP_CLIENT_ENABLED 0) + include(${FILEPATH}) + if(FREERDP_CLIENT_ENABLED) + if(NOT (${FREERDP_CLIENT_VENDOR} MATCHES "FreeRDP")) + list(APPEND FREERDP_EXTRA_CLIENTS ${FREERDP_CLIENT}) + if(${FREERDP_CLIENT_VENDOR} MATCHES "${VENDOR}") + set(CLIENT_VENDOR_PATH "client/${FREERDP_CLIENT}" PARENT_SCOPE) + endif() + endif() + endif() + endif() + endforeach() + + foreach(FREERDP_CLIENT ${FREERDP_EXTRA_CLIENTS}) + add_subdirectory(${FREERDP_CLIENT}) + endforeach() +endif() + +include(pkg-config-install-prefix) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/freerdp-client.pc.in ${CMAKE_CURRENT_BINARY_DIR}/freerdp-client${FREERDP_VERSION_MAJOR}.pc @ONLY) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/freerdp-client${FREERDP_VERSION_MAJOR}.pc DESTINATION ${PKG_CONFIG_PC_INSTALL_DIR}) + +export(PACKAGE freerdp-client) + +SetFreeRDPCMakeInstallDir(FREERDP_CLIENT_CMAKE_INSTALL_DIR "FreeRDP-Client${FREERDP_VERSION_MAJOR}") + +configure_package_config_file(FreeRDP-ClientConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/FreeRDP-ClientConfig.cmake + INSTALL_DESTINATION ${FREERDP_CLIENT_CMAKE_INSTALL_DIR} + PATH_VARS FREERDP_INCLUDE_DIR) + +write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/FreeRDP-ClientConfigVersion.cmake + VERSION ${FREERDP_VERSION} COMPATIBILITY SameMajorVersion) + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/FreeRDP-ClientConfig.cmake ${CMAKE_CURRENT_BINARY_DIR}/FreeRDP-ClientConfigVersion.cmake + DESTINATION ${FREERDP_CLIENT_CMAKE_INSTALL_DIR}) + +install(EXPORT FreeRDP-ClientTargets DESTINATION ${FREERDP_CLIENT_CMAKE_INSTALL_DIR}) diff --git a/client/FreeRDP-ClientConfig.cmake.in b/client/FreeRDP-ClientConfig.cmake.in new file mode 100644 index 0000000..35b74c1 --- /dev/null +++ b/client/FreeRDP-ClientConfig.cmake.in @@ -0,0 +1,13 @@ +include(CMakeFindDependencyMacro) +find_dependency(WinPR @FREERDP_VERSION@) +find_dependency(FreeRDP @FREERDP_VERSION@) + +@PACKAGE_INIT@ + +set(FreeRDP-Client_VERSION_MAJOR "@FREERDP_VERSION_MAJOR@") +set(FreeRDP-Client_VERSION_MINOR "@FREERDP_VERSION_MINOR@") +set(FreeRDP-Client_VERSION_REVISION "@FREERDP_VERSION_REVISION@") + +set_and_check(FreeRDP-Client_INCLUDE_DIR "@PACKAGE_FREERDP_INCLUDE_DIR@") + +include("${CMAKE_CURRENT_LIST_DIR}/FreeRDP-ClientTargets.cmake") diff --git a/client/SDL/CMakeLists.txt b/client/SDL/CMakeLists.txt new file mode 100644 index 0000000..6d2b778 --- /dev/null +++ b/client/SDL/CMakeLists.txt @@ -0,0 +1,128 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP SDL Client +# +# Copyright 2022 Armin Novak <anovak@thincast.com> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.13) + +if(POLICY CMP0091) + cmake_policy(SET CMP0091 NEW) +endif() +if (NOT FREERDP_DEFAULT_PROJECT_VERSION) + set(FREERDP_DEFAULT_PROJECT_VERSION "1.0.0.0") +endif() + +project(sdl-freerdp + LANGUAGES CXX + VERSION ${FREERDP_DEFAULT_PROJECT_VERSION} +) + +message("project ${PROJECT_NAME} is using version ${PROJECT_VERSION}") + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS ON) + +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/) +include(CommonConfigOptions) + +include(ConfigureFreeRDP) + +option(WITH_DEBUG_SDL_EVENTS "[dangerous, not for release builds!] Debug SDL events" ${DEFAULT_DEBUG_OPTION}) +option(WITH_DEBUG_SDL_KBD_EVENTS "[dangerous, not for release builds!] Debug SDL keyboard events" ${DEFAULT_DEBUG_OPTION}) +option(WITH_WIN_CONSOLE "Build ${PROJECT_NAME} with console support" ON) +option(WITH_SDL_LINK_SHARED "link SDL dynamic or static" ON) + +if(WITH_WIN_CONSOLE) + set(WIN32_GUI_FLAG "") +else() + set(WIN32_GUI_FLAG "WIN32") +endif() + + +if (WITH_DEBUG_SDL_EVENTS) + add_definitions(-DWITH_DEBUG_SDL_EVENTS) +endif() +if (WITH_DEBUG_SDL_KBD_EVENTS) + add_definitions(-DWITH_DEBUG_SDL_KBD_EVENTS) +endif() + +find_package(SDL2 REQUIRED COMPONENTS) +include_directories(${SDL2_INCLUDE_DIR}) +include_directories(${SDL2_INCLUDE_DIRS}) +find_package(cJSON) + +set(LIBS "") +if (cJSON_FOUND) + include_directories(${CJSON_INCLUDE_DIRS}) + list(APPEND LIBS ${CJSON_LIBRARIES}) + add_compile_definitions(CJSON_FOUND) +endif() + +find_package(Threads REQUIRED) + +add_subdirectory(dialogs) +set(SRCS + sdl_types.hpp + sdl_utils.cpp + sdl_utils.hpp + sdl_kbd.cpp + sdl_kbd.hpp + sdl_touch.cpp + sdl_touch.hpp + sdl_pointer.cpp + sdl_pointer.hpp + sdl_disp.cpp + sdl_disp.hpp + sdl_monitor.cpp + sdl_monitor.hpp + sdl_freerdp.hpp + sdl_freerdp.cpp + sdl_channels.hpp + sdl_channels.cpp + sdl_window.hpp + sdl_window.cpp +) + +add_subdirectory(aad) +list(APPEND LIBS + winpr + freerdp + freerdp-client + Threads::Threads + sdl_client_res + dialogs + aad-view + ) + +if (NOT WITH_SDL_LINK_SHARED) + list(APPEND LIBS ${SDL2_STATIC_LIBRARIES}) +else() + list(APPEND LIBS ${SDL2_LIBRARIES}) +endif() + +add_executable(${PROJECT_NAME} + ${WIN32_GUI_FLAG} + ${SRCS} + ) + +if (WITH_BINARY_VERSIONING) + set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "${PROJECT_NAME}${PROJECT_VERSION_MAJOR}") +endif() +target_link_libraries(${PROJECT_NAME} PRIVATE ${LIBS}) +set_property(TARGET ${PROJECT_NAME} PROPERTY FOLDER "Client/SDL") +install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT client) + +add_subdirectory(man) diff --git a/client/SDL/aad/CMakeLists.txt b/client/SDL/aad/CMakeLists.txt new file mode 100644 index 0000000..2286542 --- /dev/null +++ b/client/SDL/aad/CMakeLists.txt @@ -0,0 +1,75 @@ +set(WITH_WEBVIEW_DEFAULT OFF) +if (UNIX AND NOT APPLE) + set(WITH_WEBVIEW_DEFAULT ON) +endif() + +option(WITH_WEBVIEW "Build with WebView support for AAD login popup browser" ${WITH_WEBVIEW_DEFAULT}) +if (WITH_WEBVIEW) + option(WITH_WEBVIEW_QT "Build with QtWebEngine support for AAD login broweser popup" OFF) + + set(SRCS + sdl_webview.hpp + webview_impl.hpp + sdl_webview.cpp + ) + set(LIBS + winpr + ) + + if (WITH_WEBVIEW_QT) + find_package(Qt5 COMPONENTS WebEngineWidgets REQUIRED) + + list(APPEND SRCS + qt/webview_impl.cpp + ) + + list(APPEND LIBS + Qt5::WebEngineWidgets + ) + else() + list(APPEND SRCS + wrapper/webview.h + wrapper/webview_impl.cpp + ) + + if (WIN32) + find_package(unofficial-webview2 CONFIG REQUIRED) + list(APPEND LIBS + unofficial::webview2::webview2 + ) + elseif(APPLE) + find_library(WEBKIT Webkit REQUIRED) + list(APPEND LIBS + ${WEBKIT} + ) + else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(WEBVIEW_GTK webkit2gtk-4.0 REQUIRED) + include_directories(${WEBVIEW_GTK_INCLUDE_DIRS}) + list(APPEND LIBS + ${WEBVIEW_GTK_LIBRARIES} + ) + endif() + endif() +else() + set(SRCS + dummy.cpp + ) +endif() + +configure_file(sdl_config.hpp.in sdl_config.hpp @ONLY) + +add_library(aad-view STATIC + ${SRCS} +) +target_include_directories(aad-view PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) +target_link_libraries(aad-view + PRIVATE + ${LIBS} +) +target_compile_definitions( + aad-view + PUBLIC + ${DEFINITIONS} +) + diff --git a/client/SDL/aad/dummy.cpp b/client/SDL/aad/dummy.cpp new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/client/SDL/aad/dummy.cpp diff --git a/client/SDL/aad/qt/webview_impl.cpp b/client/SDL/aad/qt/webview_impl.cpp new file mode 100644 index 0000000..e70cc46 --- /dev/null +++ b/client/SDL/aad/qt/webview_impl.cpp @@ -0,0 +1,105 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Popup browser for AAD authentication + * + * Copyright 2023 Isaac Klein <fifthdegree@protonmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QApplication> +#include <QWebEngineView> +#include <QWebEngineProfile> +#include <QWebEngineUrlScheme> +#include <QWebEngineUrlSchemeHandler> +#include <QWebEngineUrlRequestJob> + +#include <string> +#include <cstdlib> +#include <cstdarg> +#include <winpr/string.h> +#include <winpr/assert.h> +#include <freerdp/log.h> +#include <freerdp/build-config.h> + +#include "../webview_impl.hpp" + +#define TAG CLIENT_TAG("sdl.webview") + +class SchemeHandler : public QWebEngineUrlSchemeHandler +{ + public: + explicit SchemeHandler(QObject* parent = nullptr) : QWebEngineUrlSchemeHandler(parent) + { + } + + void requestStarted(QWebEngineUrlRequestJob* request) override + { + QUrl url = request->requestUrl(); + + int rc = -1; + for (auto& param : url.query().split('&')) + { + QStringList pair = param.split('='); + + if (pair.size() != 2 || pair[0] != QLatin1String("code")) + continue; + + auto qc = pair[1]; + m_code = qc.toStdString(); + rc = 0; + break; + } + qApp->exit(rc); + } + + [[nodiscard]] std::string code() const + { + return m_code; + } + + private: + std::string m_code{}; +}; + +bool webview_impl_run(const std::string& title, const std::string& url, std::string& code) +{ + int argc = 1; + const auto vendor = QString::fromUtf8(FREERDP_VENDOR_STRING); + const auto product = QString::fromUtf8(FREERDP_PRODUCT_STRING); + QWebEngineUrlScheme::registerScheme(QWebEngineUrlScheme("ms-appx-web")); + + std::string wtitle = title; + char* argv[] = { wtitle.data() }; + QCoreApplication::setOrganizationName(vendor); + QCoreApplication::setApplicationName(product); + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QApplication app(argc, argv); + + SchemeHandler handler; + QWebEngineProfile::defaultProfile()->installUrlSchemeHandler("ms-appx-web", &handler); + + QWebEngineView webview; + webview.load(QUrl(QString::fromStdString(url))); + webview.show(); + + if (app.exec() != 0) + return false; + + auto val = handler.code(); + if (val.empty()) + return false; + code = val; + + return !code.empty(); +} diff --git a/client/SDL/aad/sdl_config.hpp.in b/client/SDL/aad/sdl_config.hpp.in new file mode 100644 index 0000000..34d0751 --- /dev/null +++ b/client/SDL/aad/sdl_config.hpp.in @@ -0,0 +1,3 @@ +#pragma once + +#cmakedefine WITH_WEBVIEW diff --git a/client/SDL/aad/sdl_webview.cpp b/client/SDL/aad/sdl_webview.cpp new file mode 100644 index 0000000..b4df75b --- /dev/null +++ b/client/SDL/aad/sdl_webview.cpp @@ -0,0 +1,129 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Popup browser for AAD authentication + * + * Copyright 2023 Isaac Klein <fifthdegree@protonmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <string> +#include <sstream> +#include <cstdlib> +#include <winpr/string.h> +#include <freerdp/log.h> + +#include "sdl_webview.hpp" +#include "webview_impl.hpp" + +#define TAG CLIENT_TAG("SDL.webview") + +static BOOL sdl_webview_get_rdsaad_access_token(freerdp* instance, const char* scope, + const char* req_cnf, char** token) +{ + WINPR_ASSERT(instance); + WINPR_ASSERT(scope); + WINPR_ASSERT(req_cnf); + WINPR_ASSERT(token); + + WINPR_UNUSED(instance); + + std::string client_id = "5177bc73-fd99-4c77-a90c-76844c9b6999"; + std::string redirect_uri = + "ms-appx-web%3a%2f%2fMicrosoft.AAD.BrokerPlugin%2f5177bc73-fd99-4c77-a90c-76844c9b6999"; + + *token = nullptr; + + auto url = + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=" + client_id + + "&response_type=code&scope=" + scope + "&redirect_uri=" + redirect_uri; + + const std::string title = "FreeRDP WebView - AAD access token"; + std::string code; + auto rc = webview_impl_run(title, url, code); + if (!rc || code.empty()) + return FALSE; + + auto token_request = "grant_type=authorization_code&code=" + code + "&client_id=" + client_id + + "&scope=" + scope + "&redirect_uri=" + redirect_uri + + "&req_cnf=" + req_cnf; + return client_common_get_access_token(instance, token_request.c_str(), token); +} + +static BOOL sdl_webview_get_avd_access_token(freerdp* instance, char** token) +{ + WINPR_ASSERT(token); + + std::string client_id = "a85cf173-4192-42f8-81fa-777a763e6e2c"; + std::string redirect_uri = + "ms-appx-web%3a%2f%2fMicrosoft.AAD.BrokerPlugin%2fa85cf173-4192-42f8-81fa-777a763e6e2c"; + std::string scope = "https%3A%2F%2Fwww.wvd.microsoft.com%2F.default"; + + *token = nullptr; + + auto url = + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=" + client_id + + "&response_type=code&scope=" + scope + "&redirect_uri=" + redirect_uri; + const std::string title = "FreeRDP WebView - AVD access token"; + std::string code; + auto rc = webview_impl_run(title, url, code); + if (!rc || code.empty()) + return FALSE; + + auto token_request = "grant_type=authorization_code&code=" + code + "&client_id=" + client_id + + "&scope=" + scope + "&redirect_uri=" + redirect_uri; + return client_common_get_access_token(instance, token_request.c_str(), token); +} + +BOOL sdl_webview_get_access_token(freerdp* instance, AccessTokenType tokenType, char** token, + size_t count, ...) +{ + WINPR_ASSERT(instance); + WINPR_ASSERT(token); + switch (tokenType) + { + case ACCESS_TOKEN_TYPE_AAD: + { + if (count < 2) + { + WLog_ERR(TAG, + "ACCESS_TOKEN_TYPE_AAD expected 2 additional arguments, but got %" PRIuz + ", aborting", + count); + return FALSE; + } + else if (count > 2) + WLog_WARN(TAG, + "ACCESS_TOKEN_TYPE_AAD expected 2 additional arguments, but got %" PRIuz + ", ignoring", + count); + va_list ap; + va_start(ap, count); + const char* scope = va_arg(ap, const char*); + const char* req_cnf = va_arg(ap, const char*); + const BOOL rc = sdl_webview_get_rdsaad_access_token(instance, scope, req_cnf, token); + va_end(ap); + return rc; + } + case ACCESS_TOKEN_TYPE_AVD: + if (count != 0) + WLog_WARN(TAG, + "ACCESS_TOKEN_TYPE_AVD expected 0 additional arguments, but got %" PRIuz + ", ignoring", + count); + return sdl_webview_get_avd_access_token(instance, token); + default: + WLog_ERR(TAG, "Unexpected value for AccessTokenType [%" PRIuz "], aborting", tokenType); + return FALSE; + } +} diff --git a/client/SDL/aad/sdl_webview.hpp b/client/SDL/aad/sdl_webview.hpp new file mode 100644 index 0000000..49461d6 --- /dev/null +++ b/client/SDL/aad/sdl_webview.hpp @@ -0,0 +1,38 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Popup browser for AAD authentication + * + * Copyright 2023 Isaac Klein <fifthdegree@protonmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <freerdp/freerdp.h> + +#include <sdl_config.hpp> + +#if defined(WITH_WEBVIEW) +#ifdef __cplusplus +extern "C" +{ +#endif + + BOOL sdl_webview_get_access_token(freerdp* instance, AccessTokenType tokenType, char** token, + size_t count, ...); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/client/SDL/aad/webview_impl.hpp b/client/SDL/aad/webview_impl.hpp new file mode 100644 index 0000000..25bca3c --- /dev/null +++ b/client/SDL/aad/webview_impl.hpp @@ -0,0 +1,24 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Popup browser for AAD authentication + * + * Copyright 2023 Isaac Klein <fifthdegree@protonmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <string> + +bool webview_impl_run(const std::string& title, const std::string& url, std::string& code); diff --git a/client/SDL/aad/wrapper/README b/client/SDL/aad/wrapper/README new file mode 100644 index 0000000..da906ba --- /dev/null +++ b/client/SDL/aad/wrapper/README @@ -0,0 +1 @@ +upstream at https://github.com/webview/webview/ diff --git a/client/SDL/aad/wrapper/webview.h b/client/SDL/aad/wrapper/webview.h new file mode 100644 index 0000000..4919265 --- /dev/null +++ b/client/SDL/aad/wrapper/webview.h @@ -0,0 +1,2781 @@ +/* + * MIT License + * + * Copyright (c) 2017 Serge Zaitsev + * Copyright (c) 2022 Steffen André Langnes + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef WEBVIEW_H +#define WEBVIEW_H + +#ifndef WEBVIEW_API +#define WEBVIEW_API extern +#endif + +#ifndef WEBVIEW_VERSION_MAJOR +// The current library major version. +#define WEBVIEW_VERSION_MAJOR 0 +#endif + +#ifndef WEBVIEW_VERSION_MINOR +// The current library minor version. +#define WEBVIEW_VERSION_MINOR 10 +#endif + +#ifndef WEBVIEW_VERSION_PATCH +// The current library patch version. +#define WEBVIEW_VERSION_PATCH 0 +#endif + +#ifndef WEBVIEW_VERSION_PRE_RELEASE +// SemVer 2.0.0 pre-release labels prefixed with "-". +#define WEBVIEW_VERSION_PRE_RELEASE "" +#endif + +#ifndef WEBVIEW_VERSION_BUILD_METADATA +// SemVer 2.0.0 build metadata prefixed with "+". +#define WEBVIEW_VERSION_BUILD_METADATA "" +#endif + +// Utility macro for stringifying a macro argument. +#define WEBVIEW_STRINGIFY(x) #x + +// Utility macro for stringifying the result of a macro argument expansion. +#define WEBVIEW_EXPAND_AND_STRINGIFY(x) WEBVIEW_STRINGIFY(x) + +// SemVer 2.0.0 version number in MAJOR.MINOR.PATCH format. +#define WEBVIEW_VERSION_NUMBER \ + WEBVIEW_EXPAND_AND_STRINGIFY(WEBVIEW_VERSION_MAJOR) \ + "." WEBVIEW_EXPAND_AND_STRINGIFY(WEBVIEW_VERSION_MINOR) "." WEBVIEW_EXPAND_AND_STRINGIFY( \ + WEBVIEW_VERSION_PATCH) + +// Holds the elements of a MAJOR.MINOR.PATCH version number. +typedef struct +{ + // Major version. + unsigned int major; + // Minor version. + unsigned int minor; + // Patch version. + unsigned int patch; +} webview_version_t; + +// Holds the library's version information. +typedef struct +{ + // The elements of the version number. + webview_version_t version; + // SemVer 2.0.0 version number in MAJOR.MINOR.PATCH format. + char version_number[32]; + // SemVer 2.0.0 pre-release labels prefixed with "-" if specified, otherwise + // an empty string. + char pre_release[48]; + // SemVer 2.0.0 build metadata prefixed with "+", otherwise an empty string. + char build_metadata[48]; +} webview_version_info_t; + +#ifdef __cplusplus +extern "C" +{ +#endif + + typedef void* webview_t; + + // Creates a new webview instance. If debug is non-zero - developer tools will + // be enabled (if the platform supports them). Window parameter can be a + // pointer to the native window handle. If it's non-null - then child WebView + // is embedded into the given parent window. Otherwise a new window is created. + // Depending on the platform, a GtkWindow, NSWindow or HWND pointer can be + // passed here. Returns null on failure. Creation can fail for various reasons + // such as when required runtime dependencies are missing or when window creation + // fails. + WEBVIEW_API webview_t webview_create(int debug, void* window); + + // Destroys a webview and closes the native window. + WEBVIEW_API void webview_destroy(webview_t w); + + // Runs the main loop until it's terminated. After this function exits - you + // must destroy the webview. + WEBVIEW_API void webview_run(webview_t w); + + // Stops the main loop. It is safe to call this function from another other + // background thread. + WEBVIEW_API void webview_terminate(webview_t w); + + // Posts a function to be executed on the main thread. You normally do not need + // to call this function, unless you want to tweak the native window. + WEBVIEW_API void webview_dispatch(webview_t w, void (*fn)(webview_t w, void* arg), void* arg); + + // Returns a native window handle pointer. When using GTK backend the pointer + // is GtkWindow pointer, when using Cocoa backend the pointer is NSWindow + // pointer, when using Win32 backend the pointer is HWND pointer. + WEBVIEW_API void* webview_get_window(webview_t w); + + // Updates the title of the native window. Must be called from the UI thread. + WEBVIEW_API void webview_set_title(webview_t w, const char* title); + +// Window size hints +#define WEBVIEW_HINT_NONE 0 // Width and height are default size +#define WEBVIEW_HINT_MIN 1 // Width and height are minimum bounds +#define WEBVIEW_HINT_MAX 2 // Width and height are maximum bounds +#define WEBVIEW_HINT_FIXED 3 // Window size can not be changed by a user + // Updates native window size. See WEBVIEW_HINT constants. + WEBVIEW_API void webview_set_size(webview_t w, int width, int height, int hints); + + // Navigates webview to the given URL. URL may be a properly encoded data URI. + // Examples: + // webview_navigate(w, "https://github.com/webview/webview"); + // webview_navigate(w, "data:text/html,%3Ch1%3EHello%3C%2Fh1%3E"); + // webview_navigate(w, "data:text/html;base64,PGgxPkhlbGxvPC9oMT4="); + WEBVIEW_API void webview_navigate(webview_t w, const char* url); + + // Set webview HTML directly. + // Example: webview_set_html(w, "<h1>Hello</h1>"); + WEBVIEW_API void webview_set_html(webview_t w, const char* html); + + // Injects JavaScript code at the initialization of the new page. Every time + // the webview will open a the new page - this initialization code will be + // executed. It is guaranteed that code is executed before window.onload. + WEBVIEW_API void webview_init(webview_t w, const char* js); + + // Evaluates arbitrary JavaScript code. Evaluation happens asynchronously, also + // the result of the expression is ignored. Use RPC bindings if you want to + // receive notifications about the results of the evaluation. + WEBVIEW_API void webview_eval(webview_t w, const char* js); + + // Binds a native C callback so that it will appear under the given name as a + // global JavaScript function. Internally it uses webview_init(). Callback + // receives a request string and a user-provided argument pointer. Request + // string is a JSON array of all the arguments passed to the JavaScript + // function. + WEBVIEW_API void webview_bind(webview_t w, const char* name, + void (*fn)(const char* seq, const char* req, void* arg), + void* arg); + + // Removes a native C callback that was previously set by webview_bind. + WEBVIEW_API void webview_unbind(webview_t w, const char* name); + + // Allows to return a value from the native binding. Original request pointer + // must be provided to help internal RPC engine match requests with responses. + // If status is zero - result is expected to be a valid JSON result value. + // If status is not zero - result is an error JSON object. + WEBVIEW_API void webview_return(webview_t w, const char* seq, int status, const char* result); + + // Get the library's version information. + // @since 0.10 + WEBVIEW_API const webview_version_info_t* webview_version(); + +#ifdef __cplusplus +} + +#ifndef WEBVIEW_HEADER + +#if !defined(WEBVIEW_GTK) && !defined(WEBVIEW_COCOA) && !defined(WEBVIEW_EDGE) +#if defined(__APPLE__) +#define WEBVIEW_COCOA +#elif defined(__unix__) +#define WEBVIEW_GTK +#elif defined(_WIN32) +#define WEBVIEW_EDGE +#else +#error "please, specify webview backend" +#endif +#endif + +#ifndef WEBVIEW_DEPRECATED +#if __cplusplus >= 201402L +#define WEBVIEW_DEPRECATED(reason) [[deprecated(reason)]] +#elif defined(_MSC_VER) +#define WEBVIEW_DEPRECATED(reason) __declspec(deprecated(reason)) +#else +#define WEBVIEW_DEPRECATED(reason) __attribute__((deprecated(reason))) +#endif +#endif + +#ifndef WEBVIEW_DEPRECATED_PRIVATE +#define WEBVIEW_DEPRECATED_PRIVATE WEBVIEW_DEPRECATED("Private API should not be used") +#endif + +#include <array> +#include <atomic> +#include <functional> +#include <future> +#include <map> +#include <string> +#include <utility> +#include <vector> +#include <locale> +#include <codecvt> +#include <cstring> + +namespace webview +{ + + using dispatch_fn_t = std::function<void()>; + + namespace detail + { + + // The library's version information. + constexpr const webview_version_info_t library_version_info{ + { WEBVIEW_VERSION_MAJOR, WEBVIEW_VERSION_MINOR, WEBVIEW_VERSION_PATCH }, + WEBVIEW_VERSION_NUMBER, + WEBVIEW_VERSION_PRE_RELEASE, + WEBVIEW_VERSION_BUILD_METADATA + }; + + inline int json_parse_c(const char* s, size_t sz, const char* key, size_t keysz, + const char** value, size_t* valuesz) + { + enum + { + JSON_STATE_VALUE, + JSON_STATE_LITERAL, + JSON_STATE_STRING, + JSON_STATE_ESCAPE, + JSON_STATE_UTF8 + } state = JSON_STATE_VALUE; + const char* k = nullptr; + int index = 1; + int depth = 0; + int utf8_bytes = 0; + + *value = nullptr; + *valuesz = 0; + + if (key == nullptr) + { + index = static_cast<decltype(index)>(keysz); + if (index < 0) + { + return -1; + } + keysz = 0; + } + + for (; sz > 0; s++, sz--) + { + enum + { + JSON_ACTION_NONE, + JSON_ACTION_START, + JSON_ACTION_END, + JSON_ACTION_START_STRUCT, + JSON_ACTION_END_STRUCT + } action = JSON_ACTION_NONE; + auto c = static_cast<unsigned char>(*s); + switch (state) + { + case JSON_STATE_VALUE: + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' || c == ':') + { + continue; + } + else if (c == '"') + { + action = JSON_ACTION_START; + state = JSON_STATE_STRING; + } + else if (c == '{' || c == '[') + { + action = JSON_ACTION_START_STRUCT; + } + else if (c == '}' || c == ']') + { + action = JSON_ACTION_END_STRUCT; + } + else if (c == 't' || c == 'f' || c == 'n' || c == '-' || + (c >= '0' && c <= '9')) + { + action = JSON_ACTION_START; + state = JSON_STATE_LITERAL; + } + else + { + return -1; + } + break; + case JSON_STATE_LITERAL: + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' || + c == ']' || c == '}' || c == ':') + { + state = JSON_STATE_VALUE; + s--; + sz++; + action = JSON_ACTION_END; + } + else if (c < 32 || c > 126) + { + return -1; + } // fallthrough + case JSON_STATE_STRING: + if (c < 32 || (c > 126 && c < 192)) + { + return -1; + } + else if (c == '"') + { + action = JSON_ACTION_END; + state = JSON_STATE_VALUE; + } + else if (c == '\\') + { + state = JSON_STATE_ESCAPE; + } + else if (c >= 192 && c < 224) + { + utf8_bytes = 1; + state = JSON_STATE_UTF8; + } + else if (c >= 224 && c < 240) + { + utf8_bytes = 2; + state = JSON_STATE_UTF8; + } + else if (c >= 240 && c < 247) + { + utf8_bytes = 3; + state = JSON_STATE_UTF8; + } + else if (c >= 128 && c < 192) + { + return -1; + } + break; + case JSON_STATE_ESCAPE: + if (c == '"' || c == '\\' || c == '/' || c == 'b' || c == 'f' || c == 'n' || + c == 'r' || c == 't' || c == 'u') + { + state = JSON_STATE_STRING; + } + else + { + return -1; + } + break; + case JSON_STATE_UTF8: + if (c < 128 || c > 191) + { + return -1; + } + utf8_bytes--; + if (utf8_bytes == 0) + { + state = JSON_STATE_STRING; + } + break; + default: + return -1; + } + + if (action == JSON_ACTION_END_STRUCT) + { + depth--; + } + + if (depth == 1) + { + if (action == JSON_ACTION_START || action == JSON_ACTION_START_STRUCT) + { + if (index == 0) + { + *value = s; + } + else if (keysz > 0 && index == 1) + { + k = s; + } + else + { + index--; + } + } + else if (action == JSON_ACTION_END || action == JSON_ACTION_END_STRUCT) + { + if (*value != nullptr && index == 0) + { + *valuesz = (size_t)(s + 1 - *value); + return 0; + } + else if (keysz > 0 && k != nullptr) + { + if (keysz == (size_t)(s - k - 1) && memcmp(key, k + 1, keysz) == 0) + { + index = 0; + } + else + { + index = 2; + } + k = nullptr; + } + } + } + + if (action == JSON_ACTION_START_STRUCT) + { + depth++; + } + } + return -1; + } + + inline std::string json_escape(const std::string& s) + { + // TODO: implement + return '"' + s + '"'; + } + + inline int json_unescape(const char* s, size_t n, char* out) + { + int r = 0; + if (*s++ != '"') + { + return -1; + } + while (n > 2) + { + char c = *s; + if (c == '\\') + { + s++; + n--; + switch (*s) + { + case 'b': + c = '\b'; + break; + case 'f': + c = '\f'; + break; + case 'n': + c = '\n'; + break; + case 'r': + c = '\r'; + break; + case 't': + c = '\t'; + break; + case '\\': + c = '\\'; + break; + case '/': + c = '/'; + break; + case '\"': + c = '\"'; + break; + default: // TODO: support unicode decoding + return -1; + } + } + if (out != nullptr) + { + *out++ = c; + } + s++; + n--; + r++; + } + if (*s != '"') + { + return -1; + } + if (out != nullptr) + { + *out = '\0'; + } + return r; + } + + inline std::string json_parse(const std::string& s, const std::string& key, const int index) + { + const char* value = nullptr; + size_t value_sz = 0; + if (key.empty()) + { + json_parse_c(s.c_str(), s.length(), nullptr, index, &value, &value_sz); + } + else + { + json_parse_c(s.c_str(), s.length(), key.c_str(), key.length(), &value, &value_sz); + } + if (value != nullptr) + { + if (value[0] != '"') + { + return { value, value_sz }; + } + int n = json_unescape(value, value_sz, nullptr); + if (n > 0) + { + char* decoded = new char[n + 1]; + json_unescape(value, value_sz, decoded); + std::string result(decoded, n); + delete[] decoded; + return result; + } + } + return ""; + } + + } // namespace detail + + WEBVIEW_DEPRECATED_PRIVATE + inline int json_parse_c(const char* s, size_t sz, const char* key, size_t keysz, + const char** value, size_t* valuesz) + { + return detail::json_parse_c(s, sz, key, keysz, value, valuesz); + } + + WEBVIEW_DEPRECATED_PRIVATE + inline std::string json_escape(const std::string& s) + { + return detail::json_escape(s); + } + + WEBVIEW_DEPRECATED_PRIVATE + inline int json_unescape(const char* s, size_t n, char* out) + { + return detail::json_unescape(s, n, out); + } + + WEBVIEW_DEPRECATED_PRIVATE + inline std::string json_parse(const std::string& s, const std::string& key, const int index) + { + return detail::json_parse(s, key, index); + } + +} // namespace webview + +#if defined(WEBVIEW_GTK) +// +// ==================================================================== +// +// This implementation uses webkit2gtk backend. It requires gtk+3.0 and +// webkit2gtk-4.0 libraries. Proper compiler flags can be retrieved via: +// +// pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.0 +// +// ==================================================================== +// +#include <JavaScriptCore/JavaScript.h> +#include <gtk/gtk.h> +#include <webkit2/webkit2.h> + +namespace webview +{ + namespace detail + { + + class gtk_webkit_engine + { + public: + gtk_webkit_engine(bool debug, void* window) : m_window(static_cast<GtkWidget*>(window)) + { + if (gtk_init_check(nullptr, nullptr) == FALSE) + { + return; + } + m_window = static_cast<GtkWidget*>(window); + if (m_window == nullptr) + { + m_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + } + g_signal_connect(G_OBJECT(m_window), "destroy", + G_CALLBACK(+[](GtkWidget*, gpointer arg) { + static_cast<gtk_webkit_engine*>(arg)->terminate(); + }), + this); + // Initialize webview widget + m_webview = webkit_web_view_new(); + WebKitUserContentManager* manager = + webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview)); + g_signal_connect(manager, "script-message-received::external", + G_CALLBACK(+[](WebKitUserContentManager*, + WebKitJavascriptResult* r, gpointer arg) { + auto* w = static_cast<gtk_webkit_engine*>(arg); + char* s = get_string_from_js_result(r); + w->on_message(s); + g_free(s); + }), + this); + webkit_user_content_manager_register_script_message_handler(manager, "external"); + init("window.external={invoke:function(s){window.webkit.messageHandlers." + "external.postMessage(s);}}"); + + gtk_container_add(GTK_CONTAINER(m_window), GTK_WIDGET(m_webview)); + gtk_widget_grab_focus(GTK_WIDGET(m_webview)); + + WebKitSettings* settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(m_webview)); + webkit_settings_set_javascript_can_access_clipboard(settings, true); + if (debug) + { + webkit_settings_set_enable_write_console_messages_to_stdout(settings, true); + webkit_settings_set_enable_developer_extras(settings, true); + } + + gtk_widget_show_all(m_window); + } + virtual ~gtk_webkit_engine() = default; + void* window() + { + return (void*)m_window; + } + void run() + { + gtk_main(); + } + void terminate() + { + gtk_main_quit(); + } + void dispatch(std::function<void()> f) + { + g_idle_add_full(G_PRIORITY_HIGH_IDLE, (GSourceFunc)([](void* f) -> int { + (*static_cast<dispatch_fn_t*>(f))(); + return G_SOURCE_REMOVE; + }), + new std::function<void()>(f), + [](void* f) { delete static_cast<dispatch_fn_t*>(f); }); + } + + void set_title(const std::string& title) + { + gtk_window_set_title(GTK_WINDOW(m_window), title.c_str()); + } + + void set_size(int width, int height, int hints) + { + gtk_window_set_resizable(GTK_WINDOW(m_window), hints != WEBVIEW_HINT_FIXED); + if (hints == WEBVIEW_HINT_NONE) + { + gtk_window_resize(GTK_WINDOW(m_window), width, height); + } + else if (hints == WEBVIEW_HINT_FIXED) + { + gtk_widget_set_size_request(m_window, width, height); + } + else + { + GdkGeometry g; + g.min_width = g.max_width = width; + g.min_height = g.max_height = height; + GdkWindowHints h = + (hints == WEBVIEW_HINT_MIN ? GDK_HINT_MIN_SIZE : GDK_HINT_MAX_SIZE); + // This defines either MIN_SIZE, or MAX_SIZE, but not both: + gtk_window_set_geometry_hints(GTK_WINDOW(m_window), nullptr, &g, h); + } + } + + void navigate(const std::string& url) + { + webkit_web_view_load_uri(WEBKIT_WEB_VIEW(m_webview), url.c_str()); + } + + void add_navigate_listener(std::function<void(const std::string&, void*)> callback, + void* arg) + { + g_signal_connect(WEBKIT_WEB_VIEW(m_webview), "load-changed", + G_CALLBACK(on_load_changed), this); + navigateCallbackArg = arg; + navigateCallback = std::move(callback); + } + + void add_scheme_handler(const std::string& scheme, + std::function<void(const std::string&, void*)> callback, + void* arg) + { + auto view = WEBKIT_WEB_VIEW(m_webview); + auto context = webkit_web_view_get_context(view); + + scheme_handlers.insert({ scheme, { .arg = arg, .fkt = callback } }); + webkit_web_context_register_uri_scheme(context, scheme.c_str(), scheme_handler, + static_cast<gpointer>(this), nullptr); + } + + void set_html(const std::string& html) + { + webkit_web_view_load_html(WEBKIT_WEB_VIEW(m_webview), html.c_str(), nullptr); + } + + void init(const std::string& js) + { + WebKitUserContentManager* manager = + webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview)); + webkit_user_content_manager_add_script( + manager, webkit_user_script_new( + js.c_str(), WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, + WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nullptr, nullptr)); + } + + void eval(const std::string& js) + { + webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(m_webview), js.c_str(), nullptr, + nullptr, nullptr); + } + + private: + virtual void on_message(const std::string& msg) = 0; + + struct handler_t + { + void* arg; + std::function<void(const std::string&, void*)> fkt; + }; + + std::map<std::string, handler_t> scheme_handlers; + + void scheme_handler_call(const std::string& scheme, const std::string& url) + { + auto handler = scheme_handlers.find(scheme); + if (handler != scheme_handlers.end()) + { + const auto& arg = handler->second; + arg.fkt(url, arg.arg); + } + } + + static void scheme_handler(WebKitURISchemeRequest* request, gpointer user_data) + { + auto _this = static_cast<gtk_webkit_engine*>(user_data); + + auto scheme = webkit_uri_scheme_request_get_scheme(request); + auto uri = webkit_uri_scheme_request_get_uri(request); + _this->scheme_handler_call(scheme, uri); + } + + static char* get_string_from_js_result(WebKitJavascriptResult* r) + { + char* s = nullptr; +#if WEBKIT_MAJOR_VERSION >= 2 && WEBKIT_MINOR_VERSION >= 22 + JSCValue* value = webkit_javascript_result_get_js_value(r); + s = jsc_value_to_string(value); +#else + JSGlobalContextRef ctx = webkit_javascript_result_get_global_context(r); + JSValueRef value = webkit_javascript_result_get_value(r); + JSStringRef js = JSValueToStringCopy(ctx, value, nullptr); + size_t n = JSStringGetMaximumUTF8CStringSize(js); + s = g_new(char, n); + JSStringGetUTF8CString(js, s, n); + JSStringRelease(js); +#endif + return s; + } + + GtkWidget* m_window; + GtkWidget* m_webview; + + void* navigateCallbackArg = nullptr; + std::function<void(const std::string&, void*)> navigateCallback = nullptr; + + static void on_load_changed(WebKitWebView* web_view, WebKitLoadEvent load_event, + gpointer arg) + { + if (load_event == WEBKIT_LOAD_FINISHED) + { + auto inst = static_cast<gtk_webkit_engine*>(arg); + inst->navigateCallback(webkit_web_view_get_uri(web_view), + inst->navigateCallbackArg); + } + } + }; + + } // namespace detail + + using browser_engine = detail::gtk_webkit_engine; + +} // namespace webview + +#elif defined(WEBVIEW_COCOA) + +// +// ==================================================================== +// +// This implementation uses Cocoa WKWebView backend on macOS. It is +// written using ObjC runtime and uses WKWebView class as a browser runtime. +// You should pass "-framework Webkit" flag to the compiler. +// +// ==================================================================== +// + +#include <CoreGraphics/CoreGraphics.h> +#include <objc/NSObjCRuntime.h> +#include <objc/objc-runtime.h> + +namespace webview +{ + namespace detail + { + namespace objc + { + + // A convenient template function for unconditionally casting the specified + // C-like function into a function that can be called with the given return + // type and arguments. Caller takes full responsibility for ensuring that + // the function call is valid. It is assumed that the function will not + // throw exceptions. + template <typename Result, typename Callable, typename... Args> + Result invoke(Callable callable, Args... args) noexcept + { + return reinterpret_cast<Result (*)(Args...)>(callable)(args...); + } + + // Calls objc_msgSend. + template <typename Result, typename... Args> Result msg_send(Args... args) noexcept + { + return invoke<Result>(objc_msgSend, args...); + } + + } // namespace objc + + enum NSBackingStoreType : NSUInteger + { + NSBackingStoreBuffered = 2 + }; + + enum NSWindowStyleMask : NSUInteger + { + NSWindowStyleMaskTitled = 1, + NSWindowStyleMaskClosable = 2, + NSWindowStyleMaskMiniaturizable = 4, + NSWindowStyleMaskResizable = 8 + }; + + enum NSApplicationActivationPolicy : NSInteger + { + NSApplicationActivationPolicyRegular = 0 + }; + + enum WKUserScriptInjectionTime : NSInteger + { + WKUserScriptInjectionTimeAtDocumentStart = 0 + }; + + enum NSModalResponse : NSInteger + { + NSModalResponseOK = 1 + }; + + // Convenient conversion of string literals. + inline id operator"" _cls(const char* s, std::size_t) + { + return (id)objc_getClass(s); + } + inline SEL operator"" _sel(const char* s, std::size_t) + { + return sel_registerName(s); + } + inline id operator"" _str(const char* s, std::size_t) + { + return objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, s); + } + + class cocoa_wkwebview_engine + { + public: + cocoa_wkwebview_engine(bool debug, void* window) + : m_debug{ debug }, m_parent_window{ window } + { + auto app = get_shared_application(); + auto delegate = create_app_delegate(); + objc_setAssociatedObject(delegate, "webview", (id)this, OBJC_ASSOCIATION_ASSIGN); + objc::msg_send<void>(app, "setDelegate:"_sel, delegate); + + // See comments related to application lifecycle in create_app_delegate(). + if (window) + { + on_application_did_finish_launching(delegate, app); + } + else + { + // Start the main run loop so that the app delegate gets the + // NSApplicationDidFinishLaunchingNotification notification after the run + // loop has started in order to perform further initialization. + // We need to return from this constructor so this run loop is only + // temporary. + objc::msg_send<void>(app, "run"_sel); + } + } + virtual ~cocoa_wkwebview_engine() = default; + void* window() + { + return (void*)m_window; + } + void terminate() + { + auto app = get_shared_application(); + objc::msg_send<void>(app, "terminate:"_sel, nullptr); + } + void run() + { + auto app = get_shared_application(); + objc::msg_send<void>(app, "run"_sel); + } + void dispatch(std::function<void()> f) + { + dispatch_async_f(dispatch_get_main_queue(), new dispatch_fn_t(f), + (dispatch_function_t)([](void* arg) { + auto f = static_cast<dispatch_fn_t*>(arg); + (*f)(); + delete f; + })); + } + void set_title(const std::string& title) + { + objc::msg_send<void>( + m_window, "setTitle:"_sel, + objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, title.c_str())); + } + void set_size(int width, int height, int hints) + { + auto style = static_cast<NSWindowStyleMask>(NSWindowStyleMaskTitled | + NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable); + if (hints != WEBVIEW_HINT_FIXED) + { + style = static_cast<NSWindowStyleMask>(style | NSWindowStyleMaskResizable); + } + objc::msg_send<void>(m_window, "setStyleMask:"_sel, style); + + if (hints == WEBVIEW_HINT_MIN) + { + objc::msg_send<void>(m_window, "setContentMinSize:"_sel, + CGSizeMake(width, height)); + } + else if (hints == WEBVIEW_HINT_MAX) + { + objc::msg_send<void>(m_window, "setContentMaxSize:"_sel, + CGSizeMake(width, height)); + } + else + { + objc::msg_send<void>(m_window, "setFrame:display:animate:"_sel, + CGRectMake(0, 0, width, height), YES, NO); + } + objc::msg_send<void>(m_window, "center"_sel); + } + void navigate(const std::string& url) + { + auto nsurl = objc::msg_send<id>( + "NSURL"_cls, "URLWithString:"_sel, + objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, url.c_str())); + + objc::msg_send<void>( + m_webview, "loadRequest:"_sel, + objc::msg_send<id>("NSURLRequest"_cls, "requestWithURL:"_sel, nsurl)); + } + + void add_navigate_listener(std::function<void(const std::string&, void*)> callback, + void* arg) + { + m_navigateCallback = callback; + m_navigateCallbackArg = arg; + } + + void set_html(const std::string& html) + { + objc::msg_send<void>( + m_webview, "loadHTMLString:baseURL:"_sel, + objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, html.c_str()), + nullptr); + } + void init(const std::string& js) + { + // Equivalent Obj-C: + // [m_manager addUserScript:[[WKUserScript alloc] initWithSource:[NSString + // stringWithUTF8String:js.c_str()] + // injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]] + objc::msg_send<void>( + m_manager, "addUserScript:"_sel, + objc::msg_send<id>( + objc::msg_send<id>("WKUserScript"_cls, "alloc"_sel), + "initWithSource:injectionTime:forMainFrameOnly:"_sel, + objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, js.c_str()), + WKUserScriptInjectionTimeAtDocumentStart, YES)); + } + void eval(const std::string& js) + { + objc::msg_send<void>( + m_webview, "evaluateJavaScript:completionHandler:"_sel, + objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, js.c_str()), + nullptr); + } + + private: + virtual void on_message(const std::string& msg) = 0; + id create_app_delegate() + { + // Note: Avoid registering the class name "AppDelegate" as it is the + // default name in projects created with Xcode, and using the same name + // causes objc_registerClassPair to crash. + auto cls = + objc_allocateClassPair((Class) "NSResponder"_cls, "WebviewAppDelegate", 0); + class_addProtocol(cls, objc_getProtocol("NSTouchBarProvider")); + class_addMethod(cls, "applicationShouldTerminateAfterLastWindowClosed:"_sel, + (IMP)(+[](id, SEL, id) -> BOOL { return 1; }), "c@:@"); + // If the library was not initialized with an existing window then the user + // is likely managing the application lifecycle and we would not get the + // "applicationDidFinishLaunching:" message and therefore do not need to + // add this method. + if (!m_parent_window) + { + class_addMethod(cls, "applicationDidFinishLaunching:"_sel, + (IMP)(+[](id self, SEL, id notification) { + auto app = objc::msg_send<id>(notification, "object"_sel); + auto w = get_associated_webview(self); + w->on_application_did_finish_launching(self, app); + }), + "v@:@"); + } + objc_registerClassPair(cls); + return objc::msg_send<id>((id)cls, "new"_sel); + } + id create_script_message_handler() + { + auto cls = objc_allocateClassPair((Class) "NSResponder"_cls, + "WebkitScriptMessageHandler", 0); + class_addProtocol(cls, objc_getProtocol("WKScriptMessageHandler")); + class_addMethod(cls, "userContentController:didReceiveScriptMessage:"_sel, + (IMP)(+[](id self, SEL, id, id msg) { + auto w = get_associated_webview(self); + w->on_message(objc::msg_send<const char*>( + objc::msg_send<id>(msg, "body"_sel), "UTF8String"_sel)); + }), + "v@:@@"); + objc_registerClassPair(cls); + auto instance = objc::msg_send<id>((id)cls, "new"_sel); + objc_setAssociatedObject(instance, "webview", (id)this, OBJC_ASSOCIATION_ASSIGN); + return instance; + } + static id create_webkit_ui_delegate() + { + auto cls = objc_allocateClassPair((Class) "NSObject"_cls, "WebkitUIDelegate", 0); + class_addProtocol(cls, objc_getProtocol("WKUIDelegate")); + class_addMethod( + cls, + "webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:"_sel, + (IMP)(+[](id, SEL, id, id parameters, id, id completion_handler) { + auto allows_multiple_selection = + objc::msg_send<BOOL>(parameters, "allowsMultipleSelection"_sel); + auto allows_directories = + objc::msg_send<BOOL>(parameters, "allowsDirectories"_sel); + + // Show a panel for selecting files. + auto panel = objc::msg_send<id>("NSOpenPanel"_cls, "openPanel"_sel); + objc::msg_send<void>(panel, "setCanChooseFiles:"_sel, YES); + objc::msg_send<void>(panel, "setCanChooseDirectories:"_sel, + allows_directories); + objc::msg_send<void>(panel, "setAllowsMultipleSelection:"_sel, + allows_multiple_selection); + auto modal_response = + objc::msg_send<NSModalResponse>(panel, "runModal"_sel); + + // Get the URLs for the selected files. If the modal was canceled + // then we pass null to the completion handler to signify + // cancellation. + id urls = modal_response == NSModalResponseOK + ? objc::msg_send<id>(panel, "URLs"_sel) + : nullptr; + + // Invoke the completion handler block. + auto sig = objc::msg_send<id>("NSMethodSignature"_cls, + "signatureWithObjCTypes:"_sel, "v@?@"); + auto invocation = objc::msg_send<id>( + "NSInvocation"_cls, "invocationWithMethodSignature:"_sel, sig); + objc::msg_send<void>(invocation, "setTarget:"_sel, completion_handler); + objc::msg_send<void>(invocation, "setArgument:atIndex:"_sel, &urls, 1); + objc::msg_send<void>(invocation, "invoke"_sel); + }), + "v@:@@@@"); + objc_registerClassPair(cls); + return objc::msg_send<id>((id)cls, "new"_sel); + } + id create_webkit_navigation_delegate() + { + auto cls = + objc_allocateClassPair((Class) "NSObject"_cls, "WebkitNavigationDelegate", 0); + class_addProtocol(cls, objc_getProtocol("WKNavigationDelegate")); + class_addMethod(cls, "webView:didFinishNavigation:"_sel, + (IMP)(+[](id delegate, SEL sel, id webview, id navigation) { + auto w = get_associated_webview(delegate); + auto url = objc::msg_send<id>(webview, "URL"_sel); + auto nstr = objc::msg_send<id>(url, "absoluteString"_sel); + auto str = objc::msg_send<char*>(nstr, "UTF8String"_sel); + w->m_navigateCallback(str, w->m_navigateCallbackArg); + }), + "v@:@"); + objc_registerClassPair(cls); + auto instance = objc::msg_send<id>((id)cls, "new"_sel); + objc_setAssociatedObject(instance, "webview", (id)this, OBJC_ASSOCIATION_ASSIGN); + return instance; + } + static id get_shared_application() + { + return objc::msg_send<id>("NSApplication"_cls, "sharedApplication"_sel); + } + static cocoa_wkwebview_engine* get_associated_webview(id object) + { + auto w = (cocoa_wkwebview_engine*)objc_getAssociatedObject(object, "webview"); + assert(w); + return w; + } + static id get_main_bundle() noexcept + { + return objc::msg_send<id>("NSBundle"_cls, "mainBundle"_sel); + } + static bool is_app_bundled() noexcept + { + auto bundle = get_main_bundle(); + if (!bundle) + { + return false; + } + auto bundle_path = objc::msg_send<id>(bundle, "bundlePath"_sel); + auto bundled = objc::msg_send<BOOL>(bundle_path, "hasSuffix:"_sel, ".app"_str); + return !!bundled; + } + void on_application_did_finish_launching(id /*delegate*/, id app) + { + // See comments related to application lifecycle in create_app_delegate(). + if (!m_parent_window) + { + // Stop the main run loop so that we can return + // from the constructor. + objc::msg_send<void>(app, "stop:"_sel, nullptr); + } + + // Activate the app if it is not bundled. + // Bundled apps launched from Finder are activated automatically but + // otherwise not. Activating the app even when it has been launched from + // Finder does not seem to be harmful but calling this function is rarely + // needed as proper activation is normally taken care of for us. + // Bundled apps have a default activation policy of + // NSApplicationActivationPolicyRegular while non-bundled apps have a + // default activation policy of NSApplicationActivationPolicyProhibited. + if (!is_app_bundled()) + { + // "setActivationPolicy:" must be invoked before + // "activateIgnoringOtherApps:" for activation to work. + objc::msg_send<void>(app, "setActivationPolicy:"_sel, + NSApplicationActivationPolicyRegular); + // Activate the app regardless of other active apps. + // This can be obtrusive so we only do it when necessary. + objc::msg_send<void>(app, "activateIgnoringOtherApps:"_sel, YES); + } + + // Main window + if (!m_parent_window) + { + m_window = objc::msg_send<id>("NSWindow"_cls, "alloc"_sel); + auto style = NSWindowStyleMaskTitled; + m_window = objc::msg_send<id>( + m_window, "initWithContentRect:styleMask:backing:defer:"_sel, + CGRectMake(0, 0, 0, 0), style, NSBackingStoreBuffered, NO); + } + else + { + m_window = (id)m_parent_window; + } + + // Webview + auto config = objc::msg_send<id>("WKWebViewConfiguration"_cls, "new"_sel); + m_manager = objc::msg_send<id>(config, "userContentController"_sel); + m_webview = objc::msg_send<id>("WKWebView"_cls, "alloc"_sel); + + if (m_debug) + { + // Equivalent Obj-C: + // [[config preferences] setValue:@YES forKey:@"developerExtrasEnabled"]; + objc::msg_send<id>( + objc::msg_send<id>(config, "preferences"_sel), "setValue:forKey:"_sel, + objc::msg_send<id>("NSNumber"_cls, "numberWithBool:"_sel, YES), + "developerExtrasEnabled"_str); + } + + // Equivalent Obj-C: + // [[config preferences] setValue:@YES forKey:@"fullScreenEnabled"]; + objc::msg_send<id>(objc::msg_send<id>(config, "preferences"_sel), + "setValue:forKey:"_sel, + objc::msg_send<id>("NSNumber"_cls, "numberWithBool:"_sel, YES), + "fullScreenEnabled"_str); + + // Equivalent Obj-C: + // [[config preferences] setValue:@YES forKey:@"javaScriptCanAccessClipboard"]; + objc::msg_send<id>(objc::msg_send<id>(config, "preferences"_sel), + "setValue:forKey:"_sel, + objc::msg_send<id>("NSNumber"_cls, "numberWithBool:"_sel, YES), + "javaScriptCanAccessClipboard"_str); + + // Equivalent Obj-C: + // [[config preferences] setValue:@YES forKey:@"DOMPasteAllowed"]; + objc::msg_send<id>(objc::msg_send<id>(config, "preferences"_sel), + "setValue:forKey:"_sel, + objc::msg_send<id>("NSNumber"_cls, "numberWithBool:"_sel, YES), + "DOMPasteAllowed"_str); + + auto ui_delegate = create_webkit_ui_delegate(); + objc::msg_send<void>(m_webview, "initWithFrame:configuration:"_sel, + CGRectMake(0, 0, 0, 0), config); + objc::msg_send<void>(m_webview, "setUIDelegate:"_sel, ui_delegate); + + auto navigation_delegate = create_webkit_navigation_delegate(); + objc::msg_send<void>(m_webview, "setNavigationDelegate:"_sel, navigation_delegate); + auto script_message_handler = create_script_message_handler(); + objc::msg_send<void>(m_manager, "addScriptMessageHandler:name:"_sel, + script_message_handler, "external"_str); + + init(R""( + window.external = { + invoke: function(s) { + window.webkit.messageHandlers.external.postMessage(s); + }, + }; + )""); + objc::msg_send<void>(m_window, "setContentView:"_sel, m_webview); + objc::msg_send<void>(m_window, "makeKeyAndOrderFront:"_sel, nullptr); + } + bool m_debug; + void* m_parent_window; + id m_window; + id m_webview; + id m_manager; + void* m_navigateCallbackArg = nullptr; + std::function<void(const std::string&, void*)> m_navigateCallback = 0; + }; + + } // namespace detail + + using browser_engine = detail::cocoa_wkwebview_engine; + +} // namespace webview + +#elif defined(WEBVIEW_EDGE) + +// +// ==================================================================== +// +// This implementation uses Win32 API to create a native window. It +// uses Edge/Chromium webview2 backend as a browser engine. +// +// ==================================================================== +// + +#define WIN32_LEAN_AND_MEAN +#include <shlobj.h> +#include <shlwapi.h> +#include <stdlib.h> +#include <windows.h> + +#include "WebView2.h" + +#ifdef _MSC_VER +#pragma comment(lib, "advapi32.lib") +#pragma comment(lib, "ole32.lib") +#pragma comment(lib, "shell32.lib") +#pragma comment(lib, "shlwapi.lib") +#pragma comment(lib, "user32.lib") +#pragma comment(lib, "version.lib") +#endif + +namespace webview +{ + namespace detail + { + + using msg_cb_t = std::function<void(const std::string)>; + + // Converts a narrow (UTF-8-encoded) string into a wide (UTF-16-encoded) string. + inline std::wstring widen_string(const std::string& input) + { + if (input.empty()) + { + return std::wstring(); + } + UINT cp = CP_UTF8; + DWORD flags = MB_ERR_INVALID_CHARS; + auto input_c = input.c_str(); + auto input_length = static_cast<int>(input.size()); + auto required_length = + MultiByteToWideChar(cp, flags, input_c, input_length, nullptr, 0); + if (required_length > 0) + { + std::wstring output(static_cast<std::size_t>(required_length), L'\0'); + if (MultiByteToWideChar(cp, flags, input_c, input_length, &output[0], + required_length) > 0) + { + return output; + } + } + // Failed to convert string from UTF-8 to UTF-16 + return std::wstring(); + } + + // Converts a wide (UTF-16-encoded) string into a narrow (UTF-8-encoded) string. + inline std::string narrow_string(const std::wstring& input) + { + if (input.empty()) + { + return std::string(); + } + UINT cp = CP_UTF8; + DWORD flags = WC_ERR_INVALID_CHARS; + auto input_c = input.c_str(); + auto input_length = static_cast<int>(input.size()); + auto required_length = + WideCharToMultiByte(cp, flags, input_c, input_length, nullptr, 0, nullptr, nullptr); + if (required_length > 0) + { + std::string output(static_cast<std::size_t>(required_length), '\0'); + if (WideCharToMultiByte(cp, flags, input_c, input_length, &output[0], + required_length, nullptr, nullptr) > 0) + { + return output; + } + } + // Failed to convert string from UTF-16 to UTF-8 + return std::string(); + } + + // Parses a version string with 1-4 integral components, e.g. "1.2.3.4". + // Missing or invalid components default to 0, and excess components are ignored. + template <typename T> + std::array<unsigned int, 4> parse_version(const std::basic_string<T>& version) noexcept + { + auto parse_component = [](auto sb, auto se) -> unsigned int { + try + { + auto n = std::stol(std::basic_string<T>(sb, se)); + return n < 0 ? 0 : n; + } + catch (std::exception&) + { + return 0; + } + }; + auto end = version.end(); + auto sb = version.begin(); // subrange begin + auto se = sb; // subrange end + unsigned int ci = 0; // component index + std::array<unsigned int, 4> components{}; + while (sb != end && se != end && ci < components.size()) + { + if (*se == static_cast<T>('.')) + { + components[ci++] = parse_component(sb, se); + sb = ++se; + continue; + } + ++se; + } + if (sb < se && ci < components.size()) + { + components[ci] = parse_component(sb, se); + } + return components; + } + + template <typename T, std::size_t Length> + auto parse_version(const T (&version)[Length]) noexcept + { + return parse_version(std::basic_string<T>(version, Length)); + } + + std::wstring get_file_version_string(const std::wstring& file_path) noexcept + { + DWORD dummy_handle; // Unused + DWORD info_buffer_length = GetFileVersionInfoSizeW(file_path.c_str(), &dummy_handle); + if (info_buffer_length == 0) + { + return std::wstring(); + } + std::vector<char> info_buffer; + info_buffer.reserve(info_buffer_length); + if (!GetFileVersionInfoW(file_path.c_str(), 0, info_buffer_length, info_buffer.data())) + { + return std::wstring(); + } + auto sub_block = L"\\StringFileInfo\\040904B0\\ProductVersion"; + LPWSTR version = nullptr; + unsigned int version_length = 0; + if (!VerQueryValueW(info_buffer.data(), sub_block, reinterpret_cast<LPVOID*>(&version), + &version_length)) + { + return std::wstring(); + } + if (!version || version_length == 0) + { + return std::wstring(); + } + return std::wstring(version, version_length); + } + + // A wrapper around COM library initialization. Calls CoInitializeEx in the + // constructor and CoUninitialize in the destructor. + class com_init_wrapper + { + public: + com_init_wrapper(DWORD dwCoInit) + { + // We can safely continue as long as COM was either successfully + // initialized or already initialized. + // RPC_E_CHANGED_MODE means that CoInitializeEx was already called with + // a different concurrency model. + switch (CoInitializeEx(nullptr, dwCoInit)) + { + case S_OK: + case S_FALSE: + m_initialized = true; + break; + } + } + + ~com_init_wrapper() + { + if (m_initialized) + { + CoUninitialize(); + m_initialized = false; + } + } + + com_init_wrapper(const com_init_wrapper& other) = delete; + com_init_wrapper& operator=(const com_init_wrapper& other) = delete; + com_init_wrapper(com_init_wrapper&& other) = delete; + com_init_wrapper& operator=(com_init_wrapper&& other) = delete; + + bool is_initialized() const + { + return m_initialized; + } + + private: + bool m_initialized = false; + }; + + // Holds a symbol name and associated type for code clarity. + template <typename T> class library_symbol + { + public: + using type = T; + + constexpr explicit library_symbol(const char* name) : m_name(name) + { + } + constexpr const char* get_name() const + { + return m_name; + } + + private: + const char* m_name; + }; + + // Loads a native shared library and allows one to get addresses for those + // symbols. + class native_library + { + public: + explicit native_library(const wchar_t* name) : m_handle(LoadLibraryW(name)) + { + } + + ~native_library() + { + if (m_handle) + { + FreeLibrary(m_handle); + m_handle = nullptr; + } + } + + native_library(const native_library& other) = delete; + native_library& operator=(const native_library& other) = delete; + native_library(native_library&& other) = default; + native_library& operator=(native_library&& other) = default; + + // Returns true if the library is currently loaded; otherwise false. + operator bool() const + { + return is_loaded(); + } + + // Get the address for the specified symbol or nullptr if not found. + template <typename Symbol> typename Symbol::type get(const Symbol& symbol) const + { + if (is_loaded()) + { + return reinterpret_cast<typename Symbol::type>( + GetProcAddress(m_handle, symbol.get_name())); + } + return nullptr; + } + + // Returns true if the library is currently loaded; otherwise false. + bool is_loaded() const + { + return !!m_handle; + } + + void detach() + { + m_handle = nullptr; + } + + private: + HMODULE m_handle = nullptr; + }; + + struct user32_symbols + { + using DPI_AWARENESS_CONTEXT = HANDLE; + using SetProcessDpiAwarenessContext_t = BOOL(WINAPI*)(DPI_AWARENESS_CONTEXT); + using SetProcessDPIAware_t = BOOL(WINAPI*)(); + + static constexpr auto SetProcessDpiAwarenessContext = + library_symbol<SetProcessDpiAwarenessContext_t>("SetProcessDpiAwarenessContext"); + static constexpr auto SetProcessDPIAware = + library_symbol<SetProcessDPIAware_t>("SetProcessDPIAware"); + }; + + struct shcore_symbols + { + typedef enum + { + PROCESS_PER_MONITOR_DPI_AWARE = 2 + } PROCESS_DPI_AWARENESS; + using SetProcessDpiAwareness_t = HRESULT(WINAPI*)(PROCESS_DPI_AWARENESS); + + static constexpr auto SetProcessDpiAwareness = + library_symbol<SetProcessDpiAwareness_t>("SetProcessDpiAwareness"); + }; + + class reg_key + { + public: + explicit reg_key(HKEY root_key, const wchar_t* sub_key, DWORD options, + REGSAM sam_desired) + { + HKEY handle; + auto status = RegOpenKeyExW(root_key, sub_key, options, sam_desired, &handle); + if (status == ERROR_SUCCESS) + { + m_handle = handle; + } + } + + explicit reg_key(HKEY root_key, const std::wstring& sub_key, DWORD options, + REGSAM sam_desired) + : reg_key(root_key, sub_key.c_str(), options, sam_desired) + { + } + + virtual ~reg_key() + { + if (m_handle) + { + RegCloseKey(m_handle); + m_handle = nullptr; + } + } + + reg_key(const reg_key& other) = delete; + reg_key& operator=(const reg_key& other) = delete; + reg_key(reg_key&& other) = delete; + reg_key& operator=(reg_key&& other) = delete; + + bool is_open() const + { + return !!m_handle; + } + bool get_handle() const + { + return m_handle; + } + + std::wstring query_string(const wchar_t* name) const + { + DWORD buf_length = 0; + // Get the size of the data in bytes. + auto status = + RegQueryValueExW(m_handle, name, nullptr, nullptr, nullptr, &buf_length); + if (status != ERROR_SUCCESS && status != ERROR_MORE_DATA) + { + return std::wstring(); + } + // Read the data. + std::wstring result(buf_length / sizeof(wchar_t), 0); + auto buf = reinterpret_cast<LPBYTE>(&result[0]); + status = RegQueryValueExW(m_handle, name, nullptr, nullptr, buf, &buf_length); + if (status != ERROR_SUCCESS) + { + return std::wstring(); + } + // Remove trailing null-characters. + for (std::size_t length = result.size(); length > 0; --length) + { + if (result[length - 1] != 0) + { + result.resize(length); + break; + } + } + return result; + } + + private: + HKEY m_handle = nullptr; + }; + + inline bool enable_dpi_awareness() + { + auto user32 = native_library(L"user32.dll"); + if (auto fn = user32.get(user32_symbols::SetProcessDpiAwarenessContext)) + { + if (fn(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) + { + return true; + } + return GetLastError() == ERROR_ACCESS_DENIED; + } + if (auto shcore = native_library(L"shcore.dll")) + { + if (auto fn = shcore.get(shcore_symbols::SetProcessDpiAwareness)) + { + auto result = fn(shcore_symbols::PROCESS_PER_MONITOR_DPI_AWARE); + return result == S_OK || result == E_ACCESSDENIED; + } + } + if (auto fn = user32.get(user32_symbols::SetProcessDPIAware)) + { + return !!fn(); + } + return true; + } + +// Enable built-in WebView2Loader implementation by default. +#ifndef WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL +#define WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL 1 +#endif + +// Link WebView2Loader.dll explicitly by default only if the built-in +// implementation is enabled. +#ifndef WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK +#define WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL +#endif + +// Explicit linking of WebView2Loader.dll should be used along with +// the built-in implementation. +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 && WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK != 1 +#undef WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK +#error Please set WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK=1. +#endif + +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 + // Gets the last component of a Windows native file path. + // For example, if the path is "C:\a\b" then the result is "b". + template <typename T> + std::basic_string<T> get_last_native_path_component(const std::basic_string<T>& path) + { + if (auto pos = path.find_last_of(static_cast<T>('\\')); + pos != std::basic_string<T>::npos) + { + return path.substr(pos + 1); + } + return std::basic_string<T>(); + } +#endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */ + + template <typename T> struct cast_info_t + { + using type = T; + IID iid; + }; + + namespace mswebview2 + { + static constexpr IID IID_ICoreWebView2CreateCoreWebView2ControllerCompletedHandler{ + 0x6C4819F3, 0xC9B7, 0x4260, 0x81, 0x27, 0xC9, 0xF5, 0xBD, 0xE7, 0xF6, 0x8C + }; + static constexpr IID IID_ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler{ + 0x4E8A3389, 0xC9D8, 0x4BD2, 0xB6, 0xB5, 0x12, 0x4F, 0xEE, 0x6C, 0xC1, 0x4D + }; + static constexpr IID IID_ICoreWebView2PermissionRequestedEventHandler{ + 0x15E1C6A3, 0xC72A, 0x4DF3, 0x91, 0xD7, 0xD0, 0x97, 0xFB, 0xEC, 0x6B, 0xFD + }; + static constexpr IID IID_ICoreWebView2WebMessageReceivedEventHandler{ + 0x57213F19, 0x00E6, 0x49FA, 0x8E, 0x07, 0x89, 0x8E, 0xA0, 0x1E, 0xCB, 0xD2 + }; + +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 + enum class webview2_runtime_type + { + installed = 0, + embedded = 1 + }; + + namespace webview2_symbols + { + using CreateWebViewEnvironmentWithOptionsInternal_t = HRESULT(STDMETHODCALLTYPE*)( + bool, webview2_runtime_type, PCWSTR, IUnknown*, + ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler*); + using DllCanUnloadNow_t = HRESULT(STDMETHODCALLTYPE*)(); + + static constexpr auto CreateWebViewEnvironmentWithOptionsInternal = + library_symbol<CreateWebViewEnvironmentWithOptionsInternal_t>( + "CreateWebViewEnvironmentWithOptionsInternal"); + static constexpr auto DllCanUnloadNow = + library_symbol<DllCanUnloadNow_t>("DllCanUnloadNow"); + } // namespace webview2_symbols +#endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */ + +#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1 + namespace webview2_symbols + { + using CreateCoreWebView2EnvironmentWithOptions_t = HRESULT(STDMETHODCALLTYPE*)( + PCWSTR, PCWSTR, ICoreWebView2EnvironmentOptions*, + ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler*); + using GetAvailableCoreWebView2BrowserVersionString_t = + HRESULT(STDMETHODCALLTYPE*)(PCWSTR, LPWSTR*); + + static constexpr auto CreateCoreWebView2EnvironmentWithOptions = + library_symbol<CreateCoreWebView2EnvironmentWithOptions_t>( + "CreateCoreWebView2EnvironmentWithOptions"); + static constexpr auto GetAvailableCoreWebView2BrowserVersionString = + library_symbol<GetAvailableCoreWebView2BrowserVersionString_t>( + "GetAvailableCoreWebView2BrowserVersionString"); + } // namespace webview2_symbols +#endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */ + + class loader + { + public: + HRESULT create_environment_with_options( + PCWSTR browser_dir, PCWSTR user_data_dir, + ICoreWebView2EnvironmentOptions* env_options, + ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler* created_handler) + const + { +#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1 + if (m_lib.is_loaded()) + { + if (auto fn = m_lib.get( + webview2_symbols::CreateCoreWebView2EnvironmentWithOptions)) + { + return fn(browser_dir, user_data_dir, env_options, created_handler); + } + } +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 + return create_environment_with_options_impl(browser_dir, user_data_dir, + env_options, created_handler); +#else + return S_FALSE; +#endif +#else + return ::CreateCoreWebView2EnvironmentWithOptions(browser_dir, user_data_dir, + env_options, created_handler); +#endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */ + } + + HRESULT + get_available_browser_version_string(PCWSTR browser_dir, LPWSTR* version) const + { +#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1 + if (m_lib.is_loaded()) + { + if (auto fn = m_lib.get( + webview2_symbols::GetAvailableCoreWebView2BrowserVersionString)) + { + return fn(browser_dir, version); + } + } +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 + return get_available_browser_version_string_impl(browser_dir, version); +#else + return S_FALSE; +#endif +#else + return ::GetAvailableCoreWebView2BrowserVersionString(browser_dir, version); +#endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */ + } + + private: +#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 + struct client_info_t + { + bool found = false; + std::wstring dll_path; + std::wstring version; + webview2_runtime_type runtime_type; + }; + + HRESULT create_environment_with_options_impl( + PCWSTR browser_dir, PCWSTR user_data_dir, + ICoreWebView2EnvironmentOptions* env_options, + ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler* created_handler) + const + { + auto found_client = find_available_client(browser_dir); + if (!found_client.found) + { + return -1; + } + auto client_dll = native_library(found_client.dll_path.c_str()); + if (auto fn = client_dll.get( + webview2_symbols::CreateWebViewEnvironmentWithOptionsInternal)) + { + return fn(true, found_client.runtime_type, user_data_dir, env_options, + created_handler); + } + if (auto fn = client_dll.get(webview2_symbols::DllCanUnloadNow)) + { + if (!fn()) + { + client_dll.detach(); + } + } + return ERROR_SUCCESS; + } + + HRESULT + get_available_browser_version_string_impl(PCWSTR browser_dir, LPWSTR* version) const + { + if (!version) + { + return -1; + } + auto found_client = find_available_client(browser_dir); + if (!found_client.found) + { + return -1; + } + auto info_length_bytes = + found_client.version.size() * sizeof(found_client.version[0]); + auto info = static_cast<LPWSTR>(CoTaskMemAlloc(info_length_bytes)); + if (!info) + { + return -1; + } + CopyMemory(info, found_client.version.c_str(), info_length_bytes); + *version = info; + return 0; + } + + client_info_t find_available_client(PCWSTR browser_dir) const + { + if (browser_dir) + { + return find_embedded_client(api_version, browser_dir); + } + auto found_client = + find_installed_client(api_version, true, default_release_channel_guid); + if (!found_client.found) + { + found_client = + find_installed_client(api_version, false, default_release_channel_guid); + } + return found_client; + } + + std::wstring make_client_dll_path(const std::wstring& dir) const + { + auto dll_path = dir; + if (!dll_path.empty()) + { + auto last_char = dir[dir.size() - 1]; + if (last_char != L'\\' && last_char != L'/') + { + dll_path += L'\\'; + } + } + dll_path += L"EBWebView\\"; +#if defined(_M_X64) || defined(__x86_64__) + dll_path += L"x64"; +#elif defined(_M_IX86) || defined(__i386__) + dll_path += L"x86"; +#elif defined(_M_ARM64) || defined(__aarch64__) + dll_path += L"arm64"; +#else +#error WebView2 integration for this platform is not yet supported. +#endif + dll_path += L"\\EmbeddedBrowserWebView.dll"; + return dll_path; + } + + client_info_t find_installed_client(unsigned int min_api_version, bool system, + const std::wstring& release_channel) const + { + std::wstring sub_key = client_state_reg_sub_key; + sub_key += release_channel; + auto root_key = system ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER; + reg_key key(root_key, sub_key, 0, KEY_READ | KEY_WOW64_32KEY); + if (!key.is_open()) + { + return {}; + } + auto ebwebview_value = key.query_string(L"EBWebView"); + + auto client_version_string = get_last_native_path_component(ebwebview_value); + auto client_version = parse_version(client_version_string); + if (client_version[2] < min_api_version) + { + // Our API version is greater than the runtime API version. + return {}; + } + + auto client_dll_path = make_client_dll_path(ebwebview_value); + return { true, client_dll_path, client_version_string, + webview2_runtime_type::installed }; + } + + client_info_t find_embedded_client(unsigned int min_api_version, + const std::wstring& dir) const + { + auto client_dll_path = make_client_dll_path(dir); + + auto client_version_string = get_file_version_string(client_dll_path); + auto client_version = parse_version(client_version_string); + if (client_version[2] < min_api_version) + { + // Our API version is greater than the runtime API version. + return {}; + } + + return { true, client_dll_path, client_version_string, + webview2_runtime_type::embedded }; + } + + // The minimum WebView2 API version we need regardless of the SDK release + // actually used. The number comes from the SDK release version, + // e.g. 1.0.1150.38. To be safe the SDK should have a number that is greater + // than or equal to this number. The Edge browser webview client must + // have a number greater than or equal to this number. + static constexpr unsigned int api_version = 1150; + + static constexpr auto client_state_reg_sub_key = + L"SOFTWARE\\Microsoft\\EdgeUpdate\\ClientState\\"; + + // GUID for the stable release channel. + static constexpr auto stable_release_guid = + L"{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"; + + static constexpr auto default_release_channel_guid = stable_release_guid; +#endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */ + +#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1 + native_library m_lib{ L"WebView2Loader.dll" }; +#endif + }; + + namespace cast_info + { + static constexpr auto controller_completed = + cast_info_t<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>{ + IID_ICoreWebView2CreateCoreWebView2ControllerCompletedHandler + }; + + static constexpr auto environment_completed = + cast_info_t<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>{ + IID_ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler + }; + + static constexpr auto message_received = + cast_info_t<ICoreWebView2WebMessageReceivedEventHandler>{ + IID_ICoreWebView2WebMessageReceivedEventHandler + }; + + static constexpr auto permission_requested = + cast_info_t<ICoreWebView2PermissionRequestedEventHandler>{ + IID_ICoreWebView2PermissionRequestedEventHandler + }; + } // namespace cast_info + } // namespace mswebview2 + + class webview2_com_handler + : public ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler, + public ICoreWebView2CreateCoreWebView2ControllerCompletedHandler, + public ICoreWebView2WebMessageReceivedEventHandler, + public ICoreWebView2PermissionRequestedEventHandler, + public ICoreWebView2NavigationCompletedEventHandler + { + using webview2_com_handler_cb_t = + std::function<void(ICoreWebView2Controller*, ICoreWebView2* webview)>; + + public: + webview2_com_handler(HWND hwnd, msg_cb_t msgCb, webview2_com_handler_cb_t cb) + : m_window(hwnd), m_msgCb(msgCb), m_cb(cb) + { + } + + virtual ~webview2_com_handler() = default; + webview2_com_handler(const webview2_com_handler& other) = delete; + webview2_com_handler& operator=(const webview2_com_handler& other) = delete; + webview2_com_handler(webview2_com_handler&& other) = delete; + webview2_com_handler& operator=(webview2_com_handler&& other) = delete; + + ULONG STDMETHODCALLTYPE AddRef() + { + return ++m_ref_count; + } + ULONG STDMETHODCALLTYPE Release() + { + if (m_ref_count > 1) + { + return --m_ref_count; + } + delete this; + return 0; + } + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID* ppv) + { + using namespace mswebview2::cast_info; + + if (!ppv) + { + return E_POINTER; + } + + // All of the COM interfaces we implement should be added here regardless + // of whether they are required. + // This is just to be on the safe side in case the WebView2 Runtime ever + // requests a pointer to an interface we implement. + // The WebView2 Runtime must at the very least be able to get a pointer to + // ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler when we use + // our custom WebView2 loader implementation, and observations have shown + // that it is the only interface requested in this case. None have been + // observed to be requested when using the official WebView2 loader. + + if (cast_if_equal_iid(riid, controller_completed, ppv) || + cast_if_equal_iid(riid, environment_completed, ppv) || + cast_if_equal_iid(riid, message_received, ppv) || + cast_if_equal_iid(riid, permission_requested, ppv)) + { + return S_OK; + } + + return E_NOINTERFACE; + } + HRESULT STDMETHODCALLTYPE Invoke(HRESULT res, ICoreWebView2Environment* env) + { + if (SUCCEEDED(res)) + { + res = env->CreateCoreWebView2Controller(m_window, this); + if (SUCCEEDED(res)) + { + return S_OK; + } + } + try_create_environment(); + return S_OK; + } + HRESULT STDMETHODCALLTYPE Invoke(HRESULT res, ICoreWebView2Controller* controller) + { + if (FAILED(res)) + { + // See try_create_environment() regarding + // HRESULT_FROM_WIN32(ERROR_INVALID_STATE). + // The result is E_ABORT if the parent window has been destroyed already. + switch (res) + { + case HRESULT_FROM_WIN32(ERROR_INVALID_STATE): + case E_ABORT: + return S_OK; + } + try_create_environment(); + return S_OK; + } + + ICoreWebView2* webview; + ::EventRegistrationToken token; + controller->get_CoreWebView2(&webview); + webview->add_WebMessageReceived(this, &token); + webview->add_PermissionRequested(this, &token); + webview->add_NavigationCompleted(this, &token); + + m_cb(controller, webview); + return S_OK; + } + HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2* sender, + ICoreWebView2WebMessageReceivedEventArgs* args) + { + LPWSTR message; + args->TryGetWebMessageAsString(&message); + m_msgCb(narrow_string(message)); + sender->PostWebMessageAsString(message); + + CoTaskMemFree(message); + return S_OK; + } + HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2* sender, + ICoreWebView2PermissionRequestedEventArgs* args) + { + COREWEBVIEW2_PERMISSION_KIND kind; + args->get_PermissionKind(&kind); + if (kind == COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ) + { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); + } + return S_OK; + } + HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2* sender, + ICoreWebView2NavigationCompletedEventArgs* args) + { + PWSTR uri = nullptr; + auto hr = sender->get_Source(&uri); + if (SUCCEEDED(hr)) + { + auto curi = std::wstring_convert<std::codecvt_utf8<wchar_t> >().to_bytes(uri); + if (navigateCallback) + navigateCallback(curi, navigateCallbackArg); + } + CoTaskMemFree(uri); + return hr; + } + + // Checks whether the specified IID equals the IID of the specified type and + // if so casts the "this" pointer to T and returns it. Returns nullptr on + // mismatching IIDs. + // If ppv is specified then the pointer will also be assigned to *ppv. + template <typename T> + T* cast_if_equal_iid(REFIID riid, const cast_info_t<T>& info, + LPVOID* ppv = nullptr) noexcept + { + T* ptr = nullptr; + if (IsEqualIID(riid, info.iid)) + { + ptr = static_cast<T*>(this); + ptr->AddRef(); + } + if (ppv) + { + *ppv = ptr; + } + return ptr; + } + + // Set the function that will perform the initiating logic for creating + // the WebView2 environment. + void set_attempt_handler(std::function<HRESULT()> attempt_handler) noexcept + { + m_attempt_handler = attempt_handler; + } + + // Retry creating a WebView2 environment. + // The initiating logic for creating the environment is defined by the + // caller of set_attempt_handler(). + void try_create_environment() noexcept + { + // WebView creation fails with HRESULT_FROM_WIN32(ERROR_INVALID_STATE) if + // a running instance using the same user data folder exists, and the + // Environment objects have different EnvironmentOptions. + // Source: + // https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2environment?view=webview2-1.0.1150.38 + if (m_attempts < m_max_attempts) + { + ++m_attempts; + auto res = m_attempt_handler(); + if (SUCCEEDED(res)) + { + return; + } + // Not entirely sure if this error code only applies to + // CreateCoreWebView2Controller so we check here as well. + if (res == HRESULT_FROM_WIN32(ERROR_INVALID_STATE)) + { + return; + } + try_create_environment(); + return; + } + // Give up. + m_cb(nullptr, nullptr); + } + + void STDMETHODCALLTYPE add_navigate_listener( + std::function<void(const std::string&, void*)> callback, void* arg) + { + navigateCallback = std::move(callback); + navigateCallbackArg = arg; + } + + private: + HWND m_window; + msg_cb_t m_msgCb; + webview2_com_handler_cb_t m_cb; + std::atomic<ULONG> m_ref_count{ 1 }; + std::function<HRESULT()> m_attempt_handler; + unsigned int m_max_attempts = 5; + unsigned int m_attempts = 0; + void* navigateCallbackArg = nullptr; + std::function<void(const std::string&, void*)> navigateCallback = 0; + }; + + class win32_edge_engine + { + public: + win32_edge_engine(bool debug, void* window) + { + if (!is_webview2_available()) + { + return; + } + if (!m_com_init.is_initialized()) + { + return; + } + enable_dpi_awareness(); + if (window == nullptr) + { + HINSTANCE hInstance = GetModuleHandle(nullptr); + HICON icon = (HICON)LoadImage(hInstance, IDI_APPLICATION, IMAGE_ICON, + GetSystemMetrics(SM_CXICON), + GetSystemMetrics(SM_CYICON), LR_DEFAULTCOLOR); + + WNDCLASSEXW wc; + ZeroMemory(&wc, sizeof(WNDCLASSEX)); + wc.cbSize = sizeof(WNDCLASSEX); + wc.hInstance = hInstance; + wc.lpszClassName = L"webview"; + wc.hIcon = icon; + wc.lpfnWndProc = + (WNDPROC)(+[](HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) -> LRESULT { + auto w = (win32_edge_engine*)GetWindowLongPtr(hwnd, GWLP_USERDATA); + switch (msg) + { + case WM_SIZE: + w->resize(hwnd); + break; + case WM_CLOSE: + DestroyWindow(hwnd); + break; + case WM_DESTROY: + w->terminate(); + break; + case WM_GETMINMAXINFO: + { + auto lpmmi = (LPMINMAXINFO)lp; + if (w == nullptr) + { + return 0; + } + if (w->m_maxsz.x > 0 && w->m_maxsz.y > 0) + { + lpmmi->ptMaxSize = w->m_maxsz; + lpmmi->ptMaxTrackSize = w->m_maxsz; + } + if (w->m_minsz.x > 0 && w->m_minsz.y > 0) + { + lpmmi->ptMinTrackSize = w->m_minsz; + } + } + break; + default: + return DefWindowProcW(hwnd, msg, wp, lp); + } + return 0; + }); + RegisterClassExW(&wc); + m_window = CreateWindowW(L"webview", L"", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, + CW_USEDEFAULT, 640, 480, nullptr, nullptr, hInstance, + nullptr); + if (m_window == nullptr) + { + return; + } + SetWindowLongPtr(m_window, GWLP_USERDATA, (LONG_PTR)this); + } + else + { + m_window = *(static_cast<HWND*>(window)); + } + + ShowWindow(m_window, SW_SHOW); + UpdateWindow(m_window); + SetFocus(m_window); + + auto cb = std::bind(&win32_edge_engine::on_message, this, std::placeholders::_1); + + embed(m_window, debug, cb); + resize(m_window); + m_controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC); + } + + virtual ~win32_edge_engine() + { + if (m_com_handler) + { + m_com_handler->Release(); + m_com_handler = nullptr; + } + if (m_webview) + { + m_webview->Release(); + m_webview = nullptr; + } + if (m_controller) + { + m_controller->Release(); + m_controller = nullptr; + } + } + + win32_edge_engine(const win32_edge_engine& other) = delete; + win32_edge_engine& operator=(const win32_edge_engine& other) = delete; + win32_edge_engine(win32_edge_engine&& other) = delete; + win32_edge_engine& operator=(win32_edge_engine&& other) = delete; + + void run() + { + MSG msg; + BOOL res; + while ((res = GetMessage(&msg, nullptr, 0, 0)) != -1) + { + if (msg.hwnd) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + continue; + } + if (msg.message == WM_APP) + { + auto f = (dispatch_fn_t*)(msg.lParam); + (*f)(); + delete f; + } + else if (msg.message == WM_QUIT) + { + return; + } + } + } + void* window() + { + return (void*)m_window; + } + void terminate() + { + PostQuitMessage(0); + } + void dispatch(dispatch_fn_t f) + { + PostThreadMessage(m_main_thread, WM_APP, 0, (LPARAM) new dispatch_fn_t(f)); + } + + void set_title(const std::string& title) + { + SetWindowTextW(m_window, widen_string(title).c_str()); + } + + void set_size(int width, int height, int hints) + { + auto style = GetWindowLong(m_window, GWL_STYLE); + if (hints == WEBVIEW_HINT_FIXED) + { + style &= ~(WS_THICKFRAME | WS_MAXIMIZEBOX); + } + else + { + style |= (WS_THICKFRAME | WS_MAXIMIZEBOX); + } + SetWindowLong(m_window, GWL_STYLE, style); + + if (hints == WEBVIEW_HINT_MAX) + { + m_maxsz.x = width; + m_maxsz.y = height; + } + else if (hints == WEBVIEW_HINT_MIN) + { + m_minsz.x = width; + m_minsz.y = height; + } + else + { + RECT r; + r.left = r.top = 0; + r.right = width; + r.bottom = height; + AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, 0); + SetWindowPos(m_window, nullptr, r.left, r.top, r.right - r.left, + r.bottom - r.top, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE | SWP_FRAMECHANGED); + resize(m_window); + } + } + + void navigate(const std::string& url) + { + auto wurl = widen_string(url); + m_webview->Navigate(wurl.c_str()); + } + + void init(const std::string& js) + { + auto wjs = widen_string(js); + m_webview->AddScriptToExecuteOnDocumentCreated(wjs.c_str(), nullptr); + } + + void eval(const std::string& js) + { + auto wjs = widen_string(js); + m_webview->ExecuteScript(wjs.c_str(), nullptr); + } + + void add_navigate_listener(std::function<void(const std::string&, void*)> callback, + void* arg) + { + m_com_handler->add_navigate_listener(callback, arg); + } + + void set_html(const std::string& html) + { + m_webview->NavigateToString(widen_string(html).c_str()); + } + + private: + bool embed(HWND wnd, bool debug, msg_cb_t cb) + { + std::atomic_flag flag = ATOMIC_FLAG_INIT; + flag.test_and_set(); + + wchar_t currentExePath[MAX_PATH]; + GetModuleFileNameW(nullptr, currentExePath, MAX_PATH); + wchar_t* currentExeName = PathFindFileNameW(currentExePath); + + wchar_t dataPath[MAX_PATH]; + if (!SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_APPDATA, nullptr, 0, dataPath))) + { + return false; + } + wchar_t userDataFolder[MAX_PATH]; + PathCombineW(userDataFolder, dataPath, currentExeName); + + m_com_handler = new webview2_com_handler( + wnd, cb, [&](ICoreWebView2Controller* controller, ICoreWebView2* webview) { + if (!controller || !webview) + { + flag.clear(); + return; + } + controller->AddRef(); + webview->AddRef(); + m_controller = controller; + m_webview = webview; + flag.clear(); + }); + + m_com_handler->set_attempt_handler([&] { + return m_webview2_loader.create_environment_with_options( + nullptr, userDataFolder, nullptr, m_com_handler); + }); + m_com_handler->try_create_environment(); + + MSG msg = {}; + while (flag.test_and_set() && GetMessage(&msg, nullptr, 0, 0)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + if (!m_controller || !m_webview) + { + return false; + } + ICoreWebView2Settings* settings = nullptr; + auto res = m_webview->get_Settings(&settings); + if (res != S_OK) + { + return false; + } + res = settings->put_AreDevToolsEnabled(debug ? TRUE : FALSE); + if (res != S_OK) + { + return false; + } + init("window.external={invoke:s=>window.chrome.webview.postMessage(s)}"); + return true; + } + + void resize(HWND wnd) + { + if (m_controller == nullptr) + { + return; + } + RECT bounds; + GetClientRect(wnd, &bounds); + m_controller->put_Bounds(bounds); + } + + bool is_webview2_available() const noexcept + { + LPWSTR version_info = nullptr; + auto res = + m_webview2_loader.get_available_browser_version_string(nullptr, &version_info); + // The result will be equal to HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) + // if the WebView2 runtime is not installed. + auto ok = SUCCEEDED(res) && version_info; + if (version_info) + { + CoTaskMemFree(version_info); + } + return ok; + } + + virtual void on_message(const std::string& msg) = 0; + + // The app is expected to call CoInitializeEx before + // CreateCoreWebView2EnvironmentWithOptions. + // Source: + // https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/webview2-idl#createcorewebview2environmentwithoptions + com_init_wrapper m_com_init{ COINIT_APARTMENTTHREADED }; + HWND m_window = nullptr; + POINT m_minsz = POINT{ 0, 0 }; + POINT m_maxsz = POINT{ 0, 0 }; + DWORD m_main_thread = GetCurrentThreadId(); + ICoreWebView2* m_webview = nullptr; + ICoreWebView2Controller* m_controller = nullptr; + webview2_com_handler* m_com_handler = nullptr; + mswebview2::loader m_webview2_loader; + }; + + } // namespace detail + + using browser_engine = detail::win32_edge_engine; + +} // namespace webview + +#endif /* WEBVIEW_GTK, WEBVIEW_COCOA, WEBVIEW_EDGE */ + +namespace webview +{ + + class webview : public browser_engine + { + public: + webview(bool debug = false, void* wnd = nullptr) : browser_engine(debug, wnd) + { + } + + void navigate(const std::string& url) + { + if (url.empty()) + { + browser_engine::navigate("about:blank"); + return; + } + browser_engine::navigate(url); + } + + using binding_t = std::function<void(std::string, std::string, void*)>; + class binding_ctx_t + { + public: + binding_ctx_t(binding_t callback, void* arg) : callback(callback), arg(arg) + { + } + // This function is called upon execution of the bound JS function + binding_t callback; + // This user-supplied argument is passed to the callback + void* arg; + }; + + using sync_binding_t = std::function<std::string(std::string)>; + + // Synchronous bind + void bind(const std::string& name, sync_binding_t fn) + { + auto wrapper = [this, fn](const std::string& seq, const std::string& req, + void* /*arg*/) { resolve(seq, 0, fn(req)); }; + bind(name, wrapper, nullptr); + } + + // Asynchronous bind + void bind(const std::string& name, binding_t fn, void* arg) + { + if (bindings.count(name) > 0) + { + return; + } + bindings.emplace(name, binding_ctx_t(fn, arg)); + auto js = "(function() { var name = '" + name + "';" + R""( + var RPC = window._rpc = (window._rpc || {nextSeq: 1}); + window[name] = function() { + var seq = RPC.nextSeq++; + var promise = new Promise(function(resolve, reject) { + RPC[seq] = { + resolve: resolve, + reject: reject, + }; + }); + window.external.invoke(JSON.stringify({ + id: seq, + method: name, + params: Array.prototype.slice.call(arguments), + })); + return promise; + } + })())""; + init(js); + eval(js); + } + + void unbind(const std::string& name) + { + auto found = bindings.find(name); + if (found != bindings.end()) + { + auto js = "delete window['" + name + "'];"; + init(js); + eval(js); + bindings.erase(found); + } + } + + void resolve(const std::string& seq, int status, const std::string& result) + { + dispatch([seq, status, result, this]() { + if (status == 0) + { + eval("window._rpc[" + seq + "].resolve(" + result + "); delete window._rpc[" + + seq + "]"); + } + else + { + eval("window._rpc[" + seq + "].reject(" + result + "); delete window._rpc[" + + seq + "]"); + } + }); + } + + private: + void on_message(const std::string& msg) override + { + auto seq = detail::json_parse(msg, "id", 0); + auto name = detail::json_parse(msg, "method", 0); + auto args = detail::json_parse(msg, "params", 0); + auto found = bindings.find(name); + if (found == bindings.end()) + { + return; + } + const auto& context = found->second; + context.callback(seq, args, context.arg); + } + + std::map<std::string, binding_ctx_t> bindings; + }; +} // namespace webview + +WEBVIEW_API webview_t webview_create(int debug, void* wnd) +{ + auto w = new webview::webview(debug, wnd); + if (!w->window()) + { + delete w; + return nullptr; + } + return w; +} + +WEBVIEW_API void webview_destroy(webview_t w) +{ + delete static_cast<webview::webview*>(w); +} + +WEBVIEW_API void webview_run(webview_t w) +{ + static_cast<webview::webview*>(w)->run(); +} + +WEBVIEW_API void webview_terminate(webview_t w) +{ + static_cast<webview::webview*>(w)->terminate(); +} + +WEBVIEW_API void webview_dispatch(webview_t w, void (*fn)(webview_t, void*), void* arg) +{ + static_cast<webview::webview*>(w)->dispatch([=]() { fn(w, arg); }); +} + +WEBVIEW_API void* webview_get_window(webview_t w) +{ + return static_cast<webview::webview*>(w)->window(); +} + +WEBVIEW_API void webview_set_title(webview_t w, const char* title) +{ + static_cast<webview::webview*>(w)->set_title(title); +} + +WEBVIEW_API void webview_set_size(webview_t w, int width, int height, int hints) +{ + static_cast<webview::webview*>(w)->set_size(width, height, hints); +} + +WEBVIEW_API void webview_navigate(webview_t w, const char* url) +{ + static_cast<webview::webview*>(w)->navigate(url); +} + +WEBVIEW_API void webview_set_html(webview_t w, const char* html) +{ + static_cast<webview::webview*>(w)->set_html(html); +} + +WEBVIEW_API void webview_init(webview_t w, const char* js) +{ + static_cast<webview::webview*>(w)->init(js); +} + +WEBVIEW_API void webview_eval(webview_t w, const char* js) +{ + static_cast<webview::webview*>(w)->eval(js); +} + +WEBVIEW_API void webview_bind(webview_t w, const char* name, + void (*fn)(const char* seq, const char* req, void* arg), void* arg) +{ + static_cast<webview::webview*>(w)->bind( + name, + [=](const std::string& seq, const std::string& req, void* arg) { + fn(seq.c_str(), req.c_str(), arg); + }, + arg); +} + +WEBVIEW_API void webview_unbind(webview_t w, const char* name) +{ + static_cast<webview::webview*>(w)->unbind(name); +} + +WEBVIEW_API void webview_return(webview_t w, const char* seq, int status, const char* result) +{ + static_cast<webview::webview*>(w)->resolve(seq, status, result); +} + +WEBVIEW_API const webview_version_info_t* webview_version() +{ + return &webview::detail::library_version_info; +} + +#endif /* WEBVIEW_HEADER */ +#endif /* __cplusplus */ +#endif /* WEBVIEW_H */ diff --git a/client/SDL/aad/wrapper/webview_impl.cpp b/client/SDL/aad/wrapper/webview_impl.cpp new file mode 100644 index 0000000..5f4d3d5 --- /dev/null +++ b/client/SDL/aad/wrapper/webview_impl.cpp @@ -0,0 +1,82 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Popup browser for AAD authentication + * + * Copyright 2023 Isaac Klein <fifthdegree@protonmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "webview.h" + +#include <assert.h> +#include <string> +#include <vector> +#include <map> +#include <regex> +#include <sstream> +#include "../webview_impl.hpp" + +static std::vector<std::string> split(const std::string& input, const std::string& regex) +{ + // passing -1 as the submatch index parameter performs splitting + std::regex re(regex); + std::sregex_token_iterator first{ input.begin(), input.end(), re, -1 }; + std::sregex_token_iterator last; + return { first, last }; +} + +static std::map<std::string, std::string> urlsplit(const std::string& url) +{ + auto pos = url.find("?"); + if (pos == std::string::npos) + return {}; + auto surl = url.substr(pos); + auto args = split(surl, "&"); + + std::map<std::string, std::string> argmap; + for (const auto& arg : args) + { + auto kv = split(arg, "="); + if (kv.size() == 2) + argmap.insert({ kv[0], kv[1] }); + } + return argmap; +} + +static void fkt(const std::string& url, void* arg) +{ + auto args = urlsplit(url); + auto val = args.find("code"); + if (val == args.end()) + return; + + assert(arg); + auto rcode = static_cast<std::string*>(arg); + *rcode = val->second; +} + +bool webview_impl_run(const std::string& title, const std::string& url, std::string& code) +{ + webview::webview w(false, nullptr); + + w.set_title(title); + w.set_size(640, 480, WEBVIEW_HINT_NONE); + + std::string scheme; + w.add_scheme_handler("ms-appx-web", fkt, &scheme); + w.add_navigate_listener(fkt, &code); + w.navigate(url); + w.run(); + return !code.empty(); +} diff --git a/client/SDL/dialogs/CMakeLists.txt b/client/SDL/dialogs/CMakeLists.txt new file mode 100644 index 0000000..4cf2a16 --- /dev/null +++ b/client/SDL/dialogs/CMakeLists.txt @@ -0,0 +1,75 @@ +set(SRCS + sdl_button.hpp + sdl_button.cpp + sdl_buttons.hpp + sdl_buttons.cpp + sdl_dialogs.cpp + sdl_dialogs.hpp + sdl_widget.hpp + sdl_widget.cpp + sdl_input.hpp + sdl_input.cpp + sdl_input_widgets.cpp + sdl_input_widgets.hpp + sdl_select.hpp + sdl_select.cpp + sdl_selectlist.hpp + sdl_selectlist.cpp + sdl_connection_dialog.cpp + sdl_connection_dialog.hpp +) + +list(APPEND LIBS + sdl_client_res + winpr +) + +if (NOT WITH_SDL_LINK_SHARED) + list(APPEND LIBS ${SDL2_STATIC_LIBRARIES}) +else() + list(APPEND LIBS ${SDL2_LIBRARIES}) +endif() + +macro(find_sdl_component name) + find_package(${name}) + if (NOT ${name}_FOUND) + find_package(PkgConfig REQUIRED) + pkg_check_modules(${name} REQUIRED ${name}) + + if (BUILD_SHARED_LIBS) + list(APPEND LIBS ${${name}_LIBRARIES}) + link_directories(${${name}_LIBRARY_DIRS}) + include_directories(${${name}_INCLUDE_DIRS}) + else() + list(APPEND LIBS ${${name}_STATIC_LIBRARIES}) + link_directories(${${name}_STATIC_LIBRARY_DIRS}) + include_directories(${${name}_STATIC_INCLUDE_DIRS}) + endif() + else() + if (WITH_SDL_LINK_SHARED) + list(APPEND LIBS ${name}::${name}) + else() + list(APPEND LIBS ${name}::${name}-static) + endif() + endif() +endmacro() + +find_sdl_component(SDL2_ttf) + +option(WITH_SDL_IMAGE_DIALOGS "Build with SDL_image support (recommended)" OFF) +if (WITH_SDL_IMAGE_DIALOGS) + find_sdl_component(SDL2_image) + add_definitions(-DWITH_SDL_IMAGE_DIALOGS) +endif() + +add_subdirectory(res) + +add_library(dialogs STATIC + ${SRCS} +) + +target_link_libraries(dialogs PRIVATE ${LIBS}) + +if(BUILD_TESTING) +# add_subdirectory(test) +endif() diff --git a/client/SDL/dialogs/font/OFL.txt b/client/SDL/dialogs/font/OFL.txt new file mode 100644 index 0000000..9b448d4 --- /dev/null +++ b/client/SDL/dialogs/font/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/client/SDL/dialogs/font/OpenSans-Italic-VariableFont_wdth,wght.ttf b/client/SDL/dialogs/font/OpenSans-Italic-VariableFont_wdth,wght.ttf Binary files differnew file mode 100644 index 0000000..5bda9cc --- /dev/null +++ b/client/SDL/dialogs/font/OpenSans-Italic-VariableFont_wdth,wght.ttf diff --git a/client/SDL/dialogs/font/OpenSans-VariableFont_wdth,wght.ttf b/client/SDL/dialogs/font/OpenSans-VariableFont_wdth,wght.ttf Binary files differnew file mode 100644 index 0000000..e4142bf --- /dev/null +++ b/client/SDL/dialogs/font/OpenSans-VariableFont_wdth,wght.ttf diff --git a/client/SDL/dialogs/font/README.txt b/client/SDL/dialogs/font/README.txt new file mode 100644 index 0000000..2548322 --- /dev/null +++ b/client/SDL/dialogs/font/README.txt @@ -0,0 +1,100 @@ +Open Sans Variable Font +======================= + +This download contains Open Sans as both variable fonts and static fonts. + +Open Sans is a variable font with these axes: + wdth + wght + +This means all the styles are contained in these files: + OpenSans-VariableFont_wdth,wght.ttf + OpenSans-Italic-VariableFont_wdth,wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for Open Sans: + static/OpenSans_Condensed-Light.ttf + static/OpenSans_Condensed-Regular.ttf + static/OpenSans_Condensed-Medium.ttf + static/OpenSans_Condensed-SemiBold.ttf + static/OpenSans_Condensed-Bold.ttf + static/OpenSans_Condensed-ExtraBold.ttf + static/OpenSans_SemiCondensed-Light.ttf + static/OpenSans_SemiCondensed-Regular.ttf + static/OpenSans_SemiCondensed-Medium.ttf + static/OpenSans_SemiCondensed-SemiBold.ttf + static/OpenSans_SemiCondensed-Bold.ttf + static/OpenSans_SemiCondensed-ExtraBold.ttf + static/OpenSans-Light.ttf + static/OpenSans-Regular.ttf + static/OpenSans-Medium.ttf + static/OpenSans-SemiBold.ttf + static/OpenSans-Bold.ttf + static/OpenSans-ExtraBold.ttf + static/OpenSans_Condensed-LightItalic.ttf + static/OpenSans_Condensed-Italic.ttf + static/OpenSans_Condensed-MediumItalic.ttf + static/OpenSans_Condensed-SemiBoldItalic.ttf + static/OpenSans_Condensed-BoldItalic.ttf + static/OpenSans_Condensed-ExtraBoldItalic.ttf + static/OpenSans_SemiCondensed-LightItalic.ttf + static/OpenSans_SemiCondensed-Italic.ttf + static/OpenSans_SemiCondensed-MediumItalic.ttf + static/OpenSans_SemiCondensed-SemiBoldItalic.ttf + static/OpenSans_SemiCondensed-BoldItalic.ttf + static/OpenSans_SemiCondensed-ExtraBoldItalic.ttf + static/OpenSans-LightItalic.ttf + static/OpenSans-Italic.ttf + static/OpenSans-MediumItalic.ttf + static/OpenSans-SemiBoldItalic.ttf + static/OpenSans-BoldItalic.ttf + static/OpenSans-ExtraBoldItalic.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (OFL.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them in your products & projects – print or digital, +commercial or otherwise. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/client/SDL/dialogs/res/CMakeLists.txt b/client/SDL/dialogs/res/CMakeLists.txt new file mode 100644 index 0000000..5591e4a --- /dev/null +++ b/client/SDL/dialogs/res/CMakeLists.txt @@ -0,0 +1,89 @@ + +add_executable(freerdp-res2bin + convert_res_to_c.cpp +) + +set(SRCS + sdl_resource_manager.cpp + sdl_resource_manager.hpp +) + +set(RES_SVG_FILES + ${CMAKE_SOURCE_DIR}/resources/FreeRDP_Icon.svg + ${CMAKE_SOURCE_DIR}/resources/icon_info.svg + ${CMAKE_SOURCE_DIR}/resources/icon_warning.svg + ${CMAKE_SOURCE_DIR}/resources/icon_error.svg +) + +set(RES_FONT_FILES + ${CMAKE_SOURCE_DIR}/client/SDL/dialogs/font/OpenSans-VariableFont_wdth,wght.ttf +) + +macro(convert_to_bin FILE FILE_TYPE) + get_filename_component(FILE_NAME ${FILE} NAME) + string(REGEX REPLACE "[^a-zA-Z0-9]" "_" TARGET_NAME ${FILE_NAME}) + + set(FILE_BIN_DIR ${CMAKE_CURRENT_BINARY_DIR}/bin) + set(FILE_BYPRODUCTS ${FILE_BIN_DIR}/${TARGET_NAME}.hpp ${FILE_BIN_DIR}/${TARGET_NAME}.cpp) + + list(APPEND FACTORY_SRCS + ${FILE_BYPRODUCTS} + ) + + add_custom_command( + OUTPUT ${FILE_BYPRODUCTS} + COMMAND ${CMAKE_COMMAND} -E make_directory ${FILE_BIN_DIR} + COMMAND $<TARGET_FILE:freerdp-res2bin> ${FILE} ${FILE_TYPE} ${TARGET_NAME} ${FILE_BIN_DIR} + COMMENT "create image resources" + DEPENDS freerdp-res2bin + DEPENDS ${FILE} + ) +endmacro() + +option(SDL_USE_COMPILED_RESOURCES "Compile in images/fonts" ON) + +if (SDL_USE_COMPILED_RESOURCES) + list(APPEND SRCS + sdl_resource_file.cpp + sdl_resource_file.hpp + ) + + include_directories(${CMAKE_CURRENT_SOURCE_DIR}) + + if (WITH_SDL_IMAGE_DIALOGS) + foreach(FILE ${RES_SVG_FILES}) + convert_to_bin("${FILE}" "images") + endforeach() + endif() + + foreach(FILE ${RES_FONT_FILES}) + convert_to_bin("${FILE}" "fonts") + endforeach() + add_definitions(-DSDL_USE_COMPILED_RESOURCES) +else() + set(SDL_RESOURCE_ROOT ${CMAKE_INSTALL_FULL_DATAROOTDIR}/FreeRDP) + if (WITH_BINARY_VERSIONING) + string(APPEND SDL_RESOURCE_ROOT "${PROJECT_VERSION_MAJOR}") + endif() + + add_definitions(-DSDL_RESOURCE_ROOT="${SDL_RESOURCE_ROOT}") + + if (WITH_SDL_IMAGE_DIALOGS) + install( + FILES ${RES_SVG_FILES} + DESTINATION ${SDL_RESOURCE_ROOT}/images + ) + endif() + + install( + FILES ${RES_FONT_FILES} + DESTINATION ${SDL_RESOURCE_ROOT}/fonts + ) +endif() + +add_library(sdl_client_res OBJECT + ${RES_FILES} + ${SRCS} + ${FACTORY_SRCS} +) +set_property(TARGET sdl_client_res PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/client/SDL/dialogs/res/convert_res_to_c.cpp b/client/SDL/dialogs/res/convert_res_to_c.cpp new file mode 100644 index 0000000..07309d5 --- /dev/null +++ b/client/SDL/dialogs/res/convert_res_to_c.cpp @@ -0,0 +1,184 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <string> +#include <fstream> +#include <algorithm> +#include <iomanip> +#include <iostream> +#if __has_include(<filesystem>) +#include <filesystem> +namespace fs = std::filesystem; +#elif __has_include(<experimental/filesystem>) +#include <experimental/filesystem> +namespace fs = std::experimental::filesystem; +#else +#error Could not find system header "<filesystem>" or "<experimental/filesystem>" +#endif + +static void usage(const char* prg) +{ + std::cerr << prg << " <file> <type> <class name> <dst path>" << std::endl; +} + +static int write_comment_header(std::ostream& out, const fs::path& prg, const std::string& fname) +{ + out << "/* AUTOGENERATED file, do not edit" << std::endl + << " *" << std::endl + << " * generated by '" << prg.filename() << "'" << std::endl + << " *" << std::endl + << " * contains the converted file '" << fname << "'" << std::endl + << " */" << std::endl + << std::endl; + return 0; +} + +static int write_cpp_header(std::ostream& out, const fs::path& prg, const fs::path& file, + const std::string& name, const std::string& type) +{ + auto fname = file.filename().string(); + auto rc = write_comment_header(out, prg, fname); + if (rc != 0) + return rc; + + out << "#include <vector>" << std::endl + << "#include \"" << name << ".hpp\"" << std::endl + << std::endl + << "std::string " << name << "::name() {" << std::endl + << "return \"" << fname << "\";" << std::endl + << "}" << std::endl + << "std::string " << name << "::type() {" << std::endl + << "return \"" << type << "\";" << std::endl + << "}" << std::endl + << std::endl + << "const SDLResourceFile " << name << "::_initializer(type(), name(), init());" + << std::endl + << std::endl + << "std::vector<unsigned char> " << name << "::init() {" << std::endl + << "static const unsigned char data[] = {" << std::endl; + + return 0; +} + +static int readwrite(std::ofstream& out, std::ifstream& ifs) +{ + size_t pos = 0; + char c = 0; + while (ifs.read(&c, 1) && ifs.good()) + { + unsigned val = c & 0xff; + out << "0x" << std::hex << std::setfill('0') << std::setw(2) << val; + if (ifs.peek() != EOF) + out << ","; + if ((pos++ % 16) == 15) + out << std::endl; + } + + return 0; +} + +static int write_cpp_trailer(std::ostream& out) +{ + out << std::endl; + out << "};" << std::endl; + out << std::endl; + out << "return std::vector<unsigned char>(data, data + sizeof(data));" << std::endl; + out << "}" << std::endl; + return 0; +} + +static int write_hpp_header(const fs::path& prg, const fs::path& file, const std::string& name, + const std::string& fname) +{ + std::ofstream out(file, std::ios::out); + if (!out.is_open()) + { + std::cerr << "Failed to open output file '" << file << "'" << std::endl; + return -1; + } + auto rc = write_comment_header(out, prg, fname); + if (rc != 0) + return rc; + + out << "#pragma once" << std::endl + << std::endl + << "#include <vector>" << std::endl + << "#include <string>" << std::endl + << "#include \"sdl_resource_file.hpp\"" << std::endl + << std::endl + << "class " << name << " {" << std::endl + << "public:" << std::endl + << name << "() = delete;" << std::endl + << std::endl + << "static std::string name();" << std::endl + << "static std::string type();" << std::endl + << std::endl + << "private:" << std::endl + << "static std::vector<unsigned char> init();" << std::endl + << "static const SDLResourceFile _initializer;" << std::endl + << std::endl + << "};" << std::endl; + return 0; +} + +int main(int argc, char* argv[]) +{ + fs::path prg(argv[0]); + if (argc != 5) + { + usage(argv[0]); + return -1; + } + + fs::path file(argv[1]); + std::string etype = argv[2]; + std::string cname = argv[3]; + fs::path dst(argv[4]); + fs::path hdr(argv[4]); + + dst /= cname + ".cpp"; + hdr /= cname + ".hpp"; + + std::ofstream out; + out.open(dst); + if (!out.is_open()) + { + std::cerr << "Failed to open output file '" << dst << "'" << std::endl; + return -2; + } + + std::ifstream ifs(file, std::ios::in | std::ios::binary); + if (!ifs.is_open()) + { + std::cerr << "Failed to open input file '" << file << "'" << std::endl; + return -3; + } + + auto rc = write_cpp_header(out, prg, file, cname, etype); + if (rc != 0) + return -1; + + rc = readwrite(out, ifs); + if (rc != 0) + return rc; + + rc = write_cpp_trailer(out); + if (rc != 0) + return rc; + return write_hpp_header(prg, hdr, cname, file.filename().string()); +} diff --git a/client/SDL/dialogs/res/sdl_resource_file.cpp b/client/SDL/dialogs/res/sdl_resource_file.cpp new file mode 100644 index 0000000..c48612d --- /dev/null +++ b/client/SDL/dialogs/res/sdl_resource_file.cpp @@ -0,0 +1,25 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "sdl_resource_file.hpp" +#include "sdl_resource_manager.hpp" + +SDLResourceFile::SDLResourceFile(const std::string& type, const std::string& id, + const std::vector<unsigned char>& data) +{ + SDLResourceManager::insert(type, id, data); +} diff --git a/client/SDL/dialogs/res/sdl_resource_file.hpp b/client/SDL/dialogs/res/sdl_resource_file.hpp new file mode 100644 index 0000000..5846921 --- /dev/null +++ b/client/SDL/dialogs/res/sdl_resource_file.hpp @@ -0,0 +1,33 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <string> +#include <vector> + +class SDLResourceFile +{ + public: + SDLResourceFile(const std::string& type, const std::string& id, + const std::vector<unsigned char>& data); + virtual ~SDLResourceFile() = default; + + private: + SDLResourceFile(const SDLResourceFile& other) = delete; + SDLResourceFile(const SDLResourceFile&& other) = delete; +}; diff --git a/client/SDL/dialogs/res/sdl_resource_manager.cpp b/client/SDL/dialogs/res/sdl_resource_manager.cpp new file mode 100644 index 0000000..90ccf31 --- /dev/null +++ b/client/SDL/dialogs/res/sdl_resource_manager.cpp @@ -0,0 +1,78 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "sdl_resource_manager.hpp" +#include <iostream> +#if __has_include(<filesystem>) +#include <filesystem> +namespace fs = std::filesystem; +#elif __has_include(<experimental/filesystem>) +#include <experimental/filesystem> +namespace fs = std::experimental::filesystem; +#else +#error Could not find system header "<filesystem>" or "<experimental/filesystem>" +#endif + +SDL_RWops* SDLResourceManager::get(const std::string& type, const std::string& id) +{ + std::string uuid = type + "/" + id; + +#if defined(SDL_USE_COMPILED_RESOURCES) + auto val = resources().find(uuid); + if (val == resources().end()) + return nullptr; + + const auto& v = val->second; + return SDL_RWFromConstMem(v.data(), v.size()); +#else + fs::path path(SDL_RESOURCE_ROOT); + path /= type; + path /= id; + + if (!fs::exists(path)) + { + std::cerr << "sdl-freerdp expects resource '" << uuid << "' at location " + << fs::absolute(path) << std::endl; + std::cerr << "file not found, application will fail" << std::endl; + } + return SDL_RWFromFile(path.native().c_str(), "rb"); +#endif +} + +const std::string SDLResourceManager::typeFonts() +{ + return "fonts"; +} + +const std::string SDLResourceManager::typeImages() +{ + return "images"; +} + +void SDLResourceManager::insert(const std::string& type, const std::string& id, + const std::vector<unsigned char>& data) +{ + std::string uuid = type + "/" + id; + resources().emplace(uuid, data); +} + +std::map<std::string, std::vector<unsigned char>>& SDLResourceManager::resources() +{ + + static std::map<std::string, std::vector<unsigned char>> resources = {}; + return resources; +} diff --git a/client/SDL/dialogs/res/sdl_resource_manager.hpp b/client/SDL/dialogs/res/sdl_resource_manager.hpp new file mode 100644 index 0000000..b4f463c --- /dev/null +++ b/client/SDL/dialogs/res/sdl_resource_manager.hpp @@ -0,0 +1,46 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <string> +#include <map> +#include <vector> +#include <SDL.h> + +class SDLResourceManager +{ + friend class SDLResourceFile; + + public: + static SDL_RWops* get(const std::string& type, const std::string& id); + + static const std::string typeFonts(); + static const std::string typeImages(); + + protected: + static void insert(const std::string& type, const std::string& id, + const std::vector<unsigned char>& data); + + private: + SDLResourceManager() = delete; + SDLResourceManager(const SDLResourceManager& other) = delete; + SDLResourceManager(const SDLResourceManager&& other) = delete; + ~SDLResourceManager() = delete; + + static std::map<std::string, std::vector<unsigned char>>& resources(); +}; diff --git a/client/SDL/dialogs/sdl_button.cpp b/client/SDL/dialogs/sdl_button.cpp new file mode 100644 index 0000000..cfa2107 --- /dev/null +++ b/client/SDL/dialogs/sdl_button.cpp @@ -0,0 +1,71 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client Channels + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * Copyright 2023 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <cassert> + +#include "sdl_button.hpp" + +static const SDL_Color buttonbackgroundcolor = { 0x69, 0x66, 0x63, 0xff }; +static const SDL_Color buttonhighlightcolor = { 0xcd, 0xca, 0x35, 0x60 }; +static const SDL_Color buttonmouseovercolor = { 0x66, 0xff, 0x66, 0x60 }; +static const SDL_Color buttonfontcolor = { 0xd1, 0xcf, 0xcd, 0xff }; + +SdlButton::SdlButton(SDL_Renderer* renderer, const std::string& label, int id, const SDL_Rect& rect) + : SdlWidget(renderer, rect, false), _name(label), _id(id) +{ + assert(renderer); + + update_text(renderer, _name, buttonfontcolor, buttonbackgroundcolor); +} + +SdlButton::SdlButton(SdlButton&& other) noexcept + : SdlWidget(std::move(other)), _name(std::move(other._name)), _id(std::move(other._id)) +{ +} + +bool SdlButton::highlight(SDL_Renderer* renderer) +{ + assert(renderer); + + std::vector<SDL_Color> colors = { buttonbackgroundcolor, buttonhighlightcolor }; + if (!fill(renderer, colors)) + return false; + return update_text(renderer, _name, buttonfontcolor); +} + +bool SdlButton::mouseover(SDL_Renderer* renderer) +{ + std::vector<SDL_Color> colors = { buttonbackgroundcolor, buttonmouseovercolor }; + if (!fill(renderer, colors)) + return false; + return update_text(renderer, _name, buttonfontcolor); +} + +bool SdlButton::update(SDL_Renderer* renderer) +{ + assert(renderer); + + return update_text(renderer, _name, buttonfontcolor, buttonbackgroundcolor); +} + +int SdlButton::id() const +{ + return _id; +} diff --git a/client/SDL/dialogs/sdl_button.hpp b/client/SDL/dialogs/sdl_button.hpp new file mode 100644 index 0000000..350e7db --- /dev/null +++ b/client/SDL/dialogs/sdl_button.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include <string> + +#include "sdl_widget.hpp" + +class SdlButton : public SdlWidget +{ + public: + SdlButton(SDL_Renderer* renderer, const std::string& label, int id, const SDL_Rect& rect); + SdlButton(SdlButton&& other) noexcept; + ~SdlButton() override = default; + + bool highlight(SDL_Renderer* renderer); + bool mouseover(SDL_Renderer* renderer); + bool update(SDL_Renderer* renderer); + + [[nodiscard]] int id() const; + + private: + SdlButton(const SdlButton& other) = delete; + + private: + std::string _name; + int _id; +}; diff --git a/client/SDL/dialogs/sdl_buttons.cpp b/client/SDL/dialogs/sdl_buttons.cpp new file mode 100644 index 0000000..8190cbe --- /dev/null +++ b/client/SDL/dialogs/sdl_buttons.cpp @@ -0,0 +1,105 @@ +#include <cassert> +#include <algorithm> + +#include "sdl_buttons.hpp" + +static const Uint32 hpadding = 10; + +bool SdlButtonList::populate(SDL_Renderer* renderer, const std::vector<std::string>& labels, + const std::vector<int>& ids, Sint32 total_width, Sint32 offsetY, + Sint32 width, Sint32 height) +{ + assert(renderer); + assert(width >= 0); + assert(height >= 0); + assert(labels.size() == ids.size()); + + _list.clear(); + size_t button_width = ids.size() * (width + hpadding) + hpadding; + size_t offsetX = total_width - std::min<size_t>(total_width, button_width); + for (size_t x = 0; x < ids.size(); x++) + { + const size_t curOffsetX = offsetX + x * (static_cast<size_t>(width) + hpadding); + const SDL_Rect rect = { static_cast<int>(curOffsetX), offsetY, width, height }; + _list.emplace_back(renderer, labels[x], ids[x], rect); + } + return true; +} + +SdlButton* SdlButtonList::get_selected(const SDL_MouseButtonEvent& button) +{ + const Sint32 x = button.x; + const Sint32 y = button.y; + + return get_selected(x, y); +} + +SdlButton* SdlButtonList::get_selected(Sint32 x, Sint32 y) +{ + for (auto& btn : _list) + { + auto r = btn.rect(); + if ((x >= r.x) && (x <= r.x + r.w) && (y >= r.y) && (y <= r.y + r.h)) + return &btn; + } + return nullptr; +} + +bool SdlButtonList::set_highlight_next(bool reset) +{ + if (reset) + _highlighted = nullptr; + else + { + auto next = _highlight_index++; + _highlight_index %= _list.size(); + auto& element = _list[next]; + _highlighted = &element; + } + return true; +} + +bool SdlButtonList::set_highlight(size_t index) +{ + if (index >= _list.size()) + { + _highlighted = nullptr; + return false; + } + auto& element = _list[index]; + _highlighted = &element; + _highlight_index = ++index % _list.size(); + return true; +} + +bool SdlButtonList::set_mouseover(Sint32 x, Sint32 y) +{ + _mouseover = get_selected(x, y); + return _mouseover != nullptr; +} + +void SdlButtonList::clear() +{ + _list.clear(); + _mouseover = nullptr; + _highlighted = nullptr; + _highlight_index = 0; +} + +bool SdlButtonList::update(SDL_Renderer* renderer) +{ + assert(renderer); + + for (auto& btn : _list) + { + if (!btn.update(renderer)) + return false; + } + + if (_highlighted) + _highlighted->highlight(renderer); + + if (_mouseover) + _mouseover->mouseover(renderer); + return true; +} diff --git a/client/SDL/dialogs/sdl_buttons.hpp b/client/SDL/dialogs/sdl_buttons.hpp new file mode 100644 index 0000000..7f82903 --- /dev/null +++ b/client/SDL/dialogs/sdl_buttons.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include <vector> +#include <cstdint> + +#include "sdl_button.hpp" + +class SdlButtonList +{ + public: + SdlButtonList() = default; + virtual ~SdlButtonList() = default; + + bool populate(SDL_Renderer* renderer, const std::vector<std::string>& labels, + const std::vector<int>& ids, Sint32 total_width, Sint32 offsetY, Sint32 width, + Sint32 heigth); + + bool update(SDL_Renderer* renderer); + SdlButton* get_selected(const SDL_MouseButtonEvent& button); + SdlButton* get_selected(Sint32 x, Sint32 y); + + bool set_highlight_next(bool reset = false); + bool set_highlight(size_t index); + bool set_mouseover(Sint32 x, Sint32 y); + + void clear(); + + private: + SdlButtonList(const SdlButtonList& other) = delete; + SdlButtonList(SdlButtonList&& other) = delete; + + private: + std::vector<SdlButton> _list; + SdlButton* _highlighted = nullptr; + size_t _highlight_index = 0; + SdlButton* _mouseover = nullptr; +}; diff --git a/client/SDL/dialogs/sdl_connection_dialog.cpp b/client/SDL/dialogs/sdl_connection_dialog.cpp new file mode 100644 index 0000000..cbb6349 --- /dev/null +++ b/client/SDL/dialogs/sdl_connection_dialog.cpp @@ -0,0 +1,536 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include <cassert> +#include <thread> + +#include "sdl_connection_dialog.hpp" +#include "../sdl_utils.hpp" +#include "../sdl_freerdp.hpp" +#include "res/sdl_resource_manager.hpp" + +static const SDL_Color backgroundcolor = { 0x38, 0x36, 0x35, 0xff }; +static const SDL_Color textcolor = { 0xd1, 0xcf, 0xcd, 0xff }; +static const SDL_Color infocolor = { 0x43, 0xe0, 0x0f, 0x60 }; +static const SDL_Color warncolor = { 0xcd, 0xca, 0x35, 0x60 }; +static const SDL_Color errorcolor = { 0xf7, 0x22, 0x30, 0x60 }; + +static const Uint32 vpadding = 5; +static const Uint32 hpadding = 5; + +SDLConnectionDialog::SDLConnectionDialog(rdpContext* context) + : _context(context), _window(nullptr), _renderer(nullptr) +{ + SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO); + hide(); +} + +SDLConnectionDialog::~SDLConnectionDialog() +{ + resetTimer(); + destroyWindow(); + SDL_Quit(); +} + +bool SDLConnectionDialog::visible() const +{ + return _window && _renderer; +} + +bool SDLConnectionDialog::setTitle(const char* fmt, ...) +{ + std::lock_guard lock(_mux); + va_list ap; + va_start(ap, fmt); + _title = print(fmt, ap); + va_end(ap); + + return show(MSG_NONE); +} + +bool SDLConnectionDialog::showInfo(const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + auto rc = show(MSG_INFO, fmt, ap); + va_end(ap); + return rc; +} + +bool SDLConnectionDialog::showWarn(const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + auto rc = show(MSG_WARN, fmt, ap); + va_end(ap); + return rc; +} + +bool SDLConnectionDialog::showError(const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + auto rc = show(MSG_ERROR, fmt, ap); + va_end(ap); + return setTimer(); +} + +bool SDLConnectionDialog::show() +{ + std::lock_guard lock(_mux); + return show(_type_active); +} + +bool SDLConnectionDialog::hide() +{ + std::lock_guard lock(_mux); + return show(MSG_DISCARD); +} + +bool SDLConnectionDialog::running() const +{ + std::lock_guard lock(_mux); + return _running; +} + +bool SDLConnectionDialog::update() +{ + std::lock_guard lock(_mux); + switch (_type) + { + case MSG_INFO: + case MSG_WARN: + case MSG_ERROR: + _type_active = _type; + createWindow(); + break; + case MSG_DISCARD: + resetTimer(); + destroyWindow(); + break; + default: + if (_window) + { + SDL_SetWindowTitle(_window, _title.c_str()); + } + break; + } + _type = MSG_NONE; + return true; +} + +bool SDLConnectionDialog::setModal() +{ + if (_window) + { + auto sdl = get_context(_context); + if (sdl->windows.empty()) + return true; + + auto parent = sdl->windows.begin()->second.window(); + SDL_SetWindowModalFor(_window, parent); + SDL_RaiseWindow(_window); + } + return true; +} + +bool SDLConnectionDialog::clearWindow(SDL_Renderer* renderer) +{ + assert(renderer); + + const int drc = SDL_SetRenderDrawColor(renderer, backgroundcolor.r, backgroundcolor.g, + backgroundcolor.b, backgroundcolor.a); + if (widget_log_error(drc, "SDL_SetRenderDrawColor")) + return false; + + const int rcls = SDL_RenderClear(renderer); + return !widget_log_error(rcls, "SDL_RenderClear"); +} + +bool SDLConnectionDialog::update(SDL_Renderer* renderer) +{ + if (!renderer) + return false; + + if (!clearWindow(renderer)) + return false; + + for (auto& btn : _list) + { + if (!btn.widget.update_text(renderer, _msg, btn.fgcolor, btn.bgcolor)) + return false; + } + + if (!_buttons.update(renderer)) + return false; + + SDL_RenderPresent(renderer); + return true; +} + +bool SDLConnectionDialog::wait(bool ignoreRdpContext) +{ + while (running()) + { + if (!ignoreRdpContext) + { + if (freerdp_shall_disconnect_context(_context)) + return false; + } + std::this_thread::yield(); + } + return true; +} + +bool SDLConnectionDialog::handle(const SDL_Event& event) +{ + Uint32 windowID = 0; + if (_window) + { + windowID = SDL_GetWindowID(_window); + } + + switch (event.type) + { + case SDL_USEREVENT_RETRY_DIALOG: + return update(); + case SDL_QUIT: + resetTimer(); + destroyWindow(); + return false; + case SDL_KEYDOWN: + case SDL_KEYUP: + if (visible()) + { + auto ev = reinterpret_cast<const SDL_KeyboardEvent&>(event); + update(_renderer); + switch (event.key.keysym.sym) + { + case SDLK_RETURN: + case SDLK_RETURN2: + case SDLK_ESCAPE: + case SDLK_KP_ENTER: + if (event.type == SDL_KEYUP) + { + freerdp_abort_event(_context); + sdl_push_quit(); + } + break; + case SDLK_TAB: + _buttons.set_highlight_next(); + break; + default: + break; + } + + return windowID == ev.windowID; + } + return false; + case SDL_MOUSEMOTION: + if (visible()) + { + auto ev = reinterpret_cast<const SDL_MouseMotionEvent&>(event); + + _buttons.set_mouseover(event.button.x, event.button.y); + update(_renderer); + return windowID == ev.windowID; + } + return false; + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + if (visible()) + { + auto ev = reinterpret_cast<const SDL_MouseButtonEvent&>(event); + update(_renderer); + + auto button = _buttons.get_selected(event.button); + if (button) + { + if (event.type == SDL_MOUSEBUTTONUP) + { + freerdp_abort_event(_context); + sdl_push_quit(); + } + } + + return windowID == ev.windowID; + } + return false; + case SDL_MOUSEWHEEL: + if (visible()) + { + auto ev = reinterpret_cast<const SDL_MouseWheelEvent&>(event); + update(_renderer); + return windowID == ev.windowID; + } + return false; + case SDL_FINGERUP: + case SDL_FINGERDOWN: + if (visible()) + { + auto ev = reinterpret_cast<const SDL_TouchFingerEvent&>(event); + update(_renderer); +#if SDL_VERSION_ATLEAST(2, 0, 18) + return windowID == ev.windowID; +#else + return false; +#endif + } + return false; + case SDL_WINDOWEVENT: + { + auto ev = reinterpret_cast<const SDL_WindowEvent&>(event); + switch (ev.event) + { + case SDL_WINDOWEVENT_CLOSE: + if (windowID == ev.windowID) + { + freerdp_abort_event(_context); + sdl_push_quit(); + } + break; + default: + update(_renderer); + setModal(); + break; + } + + return windowID == ev.windowID; + } + default: + return false; + } +} + +bool SDLConnectionDialog::createWindow() +{ + destroyWindow(); + + const size_t widget_height = 50; + const size_t widget_width = 600; + const size_t total_height = 300; + + _window = SDL_CreateWindow( + _title.c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, widget_width, + total_height, SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_MOUSE_FOCUS | SDL_WINDOW_INPUT_FOCUS); + if (_window == nullptr) + { + widget_log_error(-1, "SDL_CreateWindow"); + return false; + } + setModal(); + + _renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_ACCELERATED); + if (_renderer == nullptr) + { + widget_log_error(-1, "SDL_CreateRenderer"); + return false; + } + + SDL_Color res_bgcolor; + switch (_type_active) + { + case MSG_INFO: + res_bgcolor = infocolor; + break; + case MSG_WARN: + res_bgcolor = warncolor; + break; + case MSG_ERROR: + res_bgcolor = errorcolor; + break; + case MSG_DISCARD: + default: + res_bgcolor = backgroundcolor; + break; + } + +#if defined(WITH_SDL_IMAGE_DIALOGS) + std::string res_name; + switch (_type_active) + { + case MSG_INFO: + res_name = "icon_info.svg"; + break; + case MSG_WARN: + res_name = "icon_warning.svg"; + break; + case MSG_ERROR: + res_name = "icon_error.svg"; + break; + case MSG_DISCARD: + default: + res_name = ""; + break; + } + + int height = (total_height - 3ul * vpadding) / 2ul; + SDL_Rect iconRect{ hpadding, vpadding, widget_width / 4ul - 2ul * hpadding, height }; + widget_cfg_t icon{ textcolor, + res_bgcolor, + { _renderer, iconRect, + SDLResourceManager::get(SDLResourceManager::typeImages(), res_name) } }; + _list.emplace_back(std::move(icon)); + + iconRect.y += height; + + widget_cfg_t logo{ textcolor, + backgroundcolor, + { _renderer, iconRect, + SDLResourceManager::get(SDLResourceManager::typeImages(), + "FreeRDP_Icon.svg") } }; + _list.emplace_back(std::move(logo)); + + SDL_Rect rect = { widget_width / 4ul, vpadding, widget_width * 3ul / 4ul, + total_height - 3ul * vpadding - widget_height }; +#else + SDL_Rect rect = { hpadding, vpadding, widget_width - 2ul * hpadding, + total_height - 2ul * vpadding }; +#endif + + widget_cfg_t w{ textcolor, backgroundcolor, { _renderer, rect, false } }; + w.widget.set_wrap(true, widget_width); + _list.emplace_back(std::move(w)); + rect.y += widget_height + vpadding; + + const std::vector<int> buttonids = { 1 }; + const std::vector<std::string> buttonlabels = { "cancel" }; + _buttons.populate(_renderer, buttonlabels, buttonids, widget_width, + total_height - widget_height - vpadding, + static_cast<Sint32>(widget_width / 2), static_cast<Sint32>(widget_height)); + _buttons.set_highlight(0); + + SDL_ShowWindow(_window); + SDL_RaiseWindow(_window); + + return true; +} + +void SDLConnectionDialog::destroyWindow() +{ + _buttons.clear(); + _list.clear(); + SDL_DestroyRenderer(_renderer); + SDL_DestroyWindow(_window); + _renderer = nullptr; + _window = nullptr; +} + +bool SDLConnectionDialog::show(MsgType type, const char* fmt, va_list ap) +{ + std::lock_guard lock(_mux); + _msg = print(fmt, ap); + return show(type); +} + +bool SDLConnectionDialog::show(MsgType type) +{ + _type = type; + return sdl_push_user_event(SDL_USEREVENT_RETRY_DIALOG); +} + +std::string SDLConnectionDialog::print(const char* fmt, va_list ap) +{ + int size = -1; + std::string res; + + do + { + res.resize(128); + if (size > 0) + res.resize(size); + + va_list copy; + va_copy(copy, ap); + size = vsnprintf(res.data(), res.size(), fmt, copy); + va_end(copy); + + } while ((size > 0) && (size > res.size())); + + return res; +} + +bool SDLConnectionDialog::setTimer(Uint32 timeoutMS) +{ + std::lock_guard lock(_mux); + resetTimer(); + + _timer = SDL_AddTimer(timeoutMS, &SDLConnectionDialog::timeout, this); + _running = true; + return true; +} + +void SDLConnectionDialog::resetTimer() +{ + if (_running) + SDL_RemoveTimer(_timer); + _running = false; +} + +Uint32 SDLConnectionDialog::timeout(Uint32 intervalMS, void* pvthis) +{ + auto ths = static_cast<SDLConnectionDialog*>(pvthis); + ths->hide(); + ths->_running = false; + return 0; +} + +SDLConnectionDialogHider::SDLConnectionDialogHider(freerdp* instance) + : SDLConnectionDialogHider(get(instance)) +{ +} + +SDLConnectionDialogHider::SDLConnectionDialogHider(rdpContext* context) + : SDLConnectionDialogHider(get(context)) +{ +} + +SDLConnectionDialogHider::SDLConnectionDialogHider(SDLConnectionDialog* dialog) : _dialog(dialog) +{ + if (_dialog) + { + _visible = _dialog->visible(); + if (_visible) + { + _dialog->hide(); + } + } +} + +SDLConnectionDialogHider::~SDLConnectionDialogHider() +{ + if (_dialog && _visible) + { + _dialog->show(); + } +} + +SDLConnectionDialog* SDLConnectionDialogHider::get(freerdp* instance) +{ + if (!instance) + return nullptr; + return get(instance->context); +} + +SDLConnectionDialog* SDLConnectionDialogHider::get(rdpContext* context) +{ + auto sdl = get_context(context); + if (!sdl) + return nullptr; + return sdl->connection_dialog.get(); +} diff --git a/client/SDL/dialogs/sdl_connection_dialog.hpp b/client/SDL/dialogs/sdl_connection_dialog.hpp new file mode 100644 index 0000000..f21f538 --- /dev/null +++ b/client/SDL/dialogs/sdl_connection_dialog.hpp @@ -0,0 +1,129 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <memory> +#include <mutex> +#include <string> +#include <vector> + +#include <SDL.h> + +#include <freerdp/freerdp.h> + +#include "sdl_widget.hpp" +#include "sdl_buttons.hpp" + +class SDLConnectionDialog +{ + public: + explicit SDLConnectionDialog(rdpContext* context); + SDLConnectionDialog(const SDLConnectionDialog& other) = delete; + SDLConnectionDialog(const SDLConnectionDialog&& other) = delete; + virtual ~SDLConnectionDialog(); + + bool visible() const; + + bool setTitle(const char* fmt, ...); + bool showInfo(const char* fmt, ...); + bool showWarn(const char* fmt, ...); + bool showError(const char* fmt, ...); + + bool show(); + bool hide(); + + bool running() const; + bool wait(bool ignoreRdpContextQuit = false); + + bool handle(const SDL_Event& event); + + private: + enum MsgType + { + MSG_NONE, + MSG_INFO, + MSG_WARN, + MSG_ERROR, + MSG_DISCARD + }; + + private: + bool createWindow(); + void destroyWindow(); + + bool update(); + + bool setModal(); + + bool clearWindow(SDL_Renderer* renderer); + + bool update(SDL_Renderer* renderer); + + bool show(MsgType type, const char* fmt, va_list ap); + bool show(MsgType type); + + std::string print(const char* fmt, va_list ap); + bool setTimer(Uint32 timeoutMS = 15000); + void resetTimer(); + + private: + static Uint32 timeout(Uint32 intervalMS, void* _this); + + private: + struct widget_cfg_t + { + SDL_Color fgcolor; + SDL_Color bgcolor; + SdlWidget widget; + }; + + private: + rdpContext* _context; + SDL_Window* _window; + SDL_Renderer* _renderer; + mutable std::mutex _mux; + std::string _title; + std::string _msg; + MsgType _type = MSG_NONE; + MsgType _type_active = MSG_NONE; + SDL_TimerID _timer = -1; + bool _running = false; + std::vector<widget_cfg_t> _list; + SdlButtonList _buttons; +}; + +class SDLConnectionDialogHider +{ + public: + explicit SDLConnectionDialogHider(freerdp* instance); + explicit SDLConnectionDialogHider(rdpContext* context); + + explicit SDLConnectionDialogHider(SDLConnectionDialog* dialog); + + ~SDLConnectionDialogHider(); + + private: + SDLConnectionDialog* get(freerdp* instance); + SDLConnectionDialog* get(rdpContext* context); + + private: + SDLConnectionDialog* _dialog; + bool _visible; +}; diff --git a/client/SDL/dialogs/sdl_dialogs.cpp b/client/SDL/dialogs/sdl_dialogs.cpp new file mode 100644 index 0000000..32f8457 --- /dev/null +++ b/client/SDL/dialogs/sdl_dialogs.cpp @@ -0,0 +1,621 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <vector> +#include <string> +#include <cassert> + +#include <freerdp/log.h> +#include <freerdp/utils/smartcardlogon.h> + +#include <SDL.h> + +#include "../sdl_freerdp.hpp" +#include "sdl_dialogs.hpp" +#include "sdl_input.hpp" +#include "sdl_input_widgets.hpp" +#include "sdl_select.hpp" +#include "sdl_selectlist.hpp" + +#define TAG CLIENT_TAG("SDL.dialogs") + +enum +{ + SHOW_DIALOG_ACCEPT_REJECT = 1, + SHOW_DIALOG_TIMED_ACCEPT = 2 +}; + +static const char* type_str_for_flags(UINT32 flags) +{ + const char* type = "RDP-Server"; + + if (flags & VERIFY_CERT_FLAG_GATEWAY) + type = "RDP-Gateway"; + + if (flags & VERIFY_CERT_FLAG_REDIRECT) + type = "RDP-Redirect"; + return type; +} + +static BOOL sdl_wait_for_result(rdpContext* context, Uint32 type, SDL_Event* result) +{ + const SDL_Event empty = { 0 }; + + WINPR_ASSERT(context); + WINPR_ASSERT(result); + + while (!freerdp_shall_disconnect_context(context)) + { + *result = empty; + const int rc = SDL_PeepEvents(result, 1, SDL_GETEVENT, type, type); + if (rc > 0) + return TRUE; + Sleep(1); + } + return FALSE; +} + +static int sdl_show_dialog(rdpContext* context, const char* title, const char* message, + Sint32 flags) +{ + SDL_Event event = { 0 }; + + if (!sdl_push_user_event(SDL_USEREVENT_SHOW_DIALOG, title, message, flags)) + return 0; + + if (!sdl_wait_for_result(context, SDL_USEREVENT_SHOW_RESULT, &event)) + return 0; + + return event.user.code; +} + +BOOL sdl_authenticate_ex(freerdp* instance, char** username, char** password, char** domain, + rdp_auth_reason reason) +{ + SDL_Event event = { 0 }; + BOOL res = FALSE; + + SDLConnectionDialogHider hider(instance); + + const char* target = freerdp_settings_get_server_name(instance->context->settings); + switch (reason) + { + case AUTH_NLA: + break; + + case AUTH_TLS: + case AUTH_RDP: + case AUTH_SMARTCARD_PIN: /* in this case password is pin code */ + if ((*username) && (*password)) + return TRUE; + break; + case GW_AUTH_HTTP: + case GW_AUTH_RDG: + case GW_AUTH_RPC: + target = + freerdp_settings_get_string(instance->context->settings, FreeRDP_GatewayHostname); + break; + default: + break; + } + + char* title = nullptr; + size_t titlesize = 0; + winpr_asprintf(&title, &titlesize, "Credentials required for %s", target); + + char* u = nullptr; + char* d = nullptr; + char* p = nullptr; + + assert(username); + assert(domain); + assert(password); + + u = *username; + d = *domain; + p = *password; + + if (!sdl_push_user_event(SDL_USEREVENT_AUTH_DIALOG, title, u, d, p, reason)) + goto fail; + + if (!sdl_wait_for_result(instance->context, SDL_USEREVENT_AUTH_RESULT, &event)) + goto fail; + else + { + auto arg = reinterpret_cast<SDL_UserAuthArg*>(event.padding); + + res = arg->result > 0 ? TRUE : FALSE; + + free(*username); + free(*domain); + free(*password); + *username = arg->user; + *domain = arg->domain; + *password = arg->password; + } + +fail: + free(title); + return res; +} + +BOOL sdl_choose_smartcard(freerdp* instance, SmartcardCertInfo** cert_list, DWORD count, + DWORD* choice, BOOL gateway) +{ + BOOL res = FALSE; + + WINPR_ASSERT(instance); + WINPR_ASSERT(cert_list); + WINPR_ASSERT(choice); + + SDLConnectionDialogHider hider(instance); + std::vector<std::string> strlist; + std::vector<const char*> list; + for (DWORD i = 0; i < count; i++) + { + const SmartcardCertInfo* cert = cert_list[i]; + char* reader = ConvertWCharToUtf8Alloc(cert->reader, nullptr); + char* container_name = ConvertWCharToUtf8Alloc(cert->containerName, nullptr); + + char* msg = nullptr; + size_t len = 0; + + winpr_asprintf(&msg, &len, + "%s\n\tReader: %s\n\tUser: %s@%s\n\tSubject: %s\n\tIssuer: %s\n\tUPN: %s", + container_name, reader, cert->userHint, cert->domainHint, cert->subject, + cert->issuer, cert->upn); + + strlist.emplace_back(msg); + free(msg); + free(reader); + free(container_name); + + auto& m = strlist.back(); + list.push_back(m.c_str()); + } + + SDL_Event event = { 0 }; + const char* title = "Select a logon smartcard certificate"; + if (gateway) + title = "Select a gateway logon smartcard certificate"; + if (!sdl_push_user_event(SDL_USEREVENT_SCARD_DIALOG, title, list.data(), count)) + goto fail; + + if (!sdl_wait_for_result(instance->context, SDL_USEREVENT_SCARD_RESULT, &event)) + goto fail; + + res = (event.user.code >= 0) ? TRUE : FALSE; + *choice = static_cast<DWORD>(event.user.code); + +fail: + return res; +} + +SSIZE_T sdl_retry_dialog(freerdp* instance, const char* what, size_t current, void* userarg) +{ + WINPR_ASSERT(instance); + WINPR_ASSERT(instance->context); + WINPR_ASSERT(what); + + auto sdl = get_context(instance->context); + std::lock_guard<CriticalSection> lock(sdl->critical); + WINPR_ASSERT(sdl->connection_dialog); + + sdl->connection_dialog->setTitle("Retry connection to %s", + freerdp_settings_get_server_name(instance->context->settings)); + + if ((strcmp(what, "arm-transport") != 0) && (strcmp(what, "connection") != 0)) + { + sdl->connection_dialog->showError("Unknown module %s, aborting", what); + return -1; + } + + if (current == 0) + { + if (strcmp(what, "arm-transport") == 0) + sdl->connection_dialog->showWarn("[%s] Starting your VM. It may take up to 5 minutes", + what); + } + + auto settings = instance->context->settings; + const BOOL enabled = freerdp_settings_get_bool(settings, FreeRDP_AutoReconnectionEnabled); + + if (!enabled) + { + sdl->connection_dialog->showError( + "Automatic reconnection disabled, terminating. Try to connect again later"); + return -1; + } + + const size_t max = freerdp_settings_get_uint32(settings, FreeRDP_AutoReconnectMaxRetries); + const size_t delay = freerdp_settings_get_uint32(settings, FreeRDP_TcpConnectTimeout); + if (current >= max) + { + sdl->connection_dialog->showError( + "[%s] retries exceeded. Your VM failed to start. Try again later or contact your " + "tech support for help if this keeps happening.", + what); + return -1; + } + + sdl->connection_dialog->showInfo("[%s] retry %" PRIuz "/%" PRIuz ", delaying %" PRIuz + "ms before next attempt", + what, current, max, delay); + return delay; +} + +BOOL sdl_present_gateway_message(freerdp* instance, UINT32 type, BOOL isDisplayMandatory, + BOOL isConsentMandatory, size_t length, const WCHAR* wmessage) +{ + if (!isDisplayMandatory) + return TRUE; + + char* title = nullptr; + size_t len = 0; + winpr_asprintf(&title, &len, "[gateway]"); + + Sint32 flags = 0; + if (isConsentMandatory) + flags = SHOW_DIALOG_ACCEPT_REJECT; + else if (isDisplayMandatory) + flags = SHOW_DIALOG_TIMED_ACCEPT; + char* message = ConvertWCharNToUtf8Alloc(wmessage, length, nullptr); + + SDLConnectionDialogHider hider(instance); + const int rc = sdl_show_dialog(instance->context, title, message, flags); + free(title); + free(message); + return rc > 0 ? TRUE : FALSE; +} + +int sdl_logon_error_info(freerdp* instance, UINT32 data, UINT32 type) +{ + int rc = -1; + const char* str_data = freerdp_get_logon_error_info_data(data); + const char* str_type = freerdp_get_logon_error_info_type(type); + + if (!instance || !instance->context) + return -1; + + /* ignore LOGON_MSG_SESSION_CONTINUE message */ + if (type == LOGON_MSG_SESSION_CONTINUE) + return 0; + + SDLConnectionDialogHider hider(instance); + + char* title = nullptr; + size_t tlen = 0; + winpr_asprintf(&title, &tlen, "[%s] info", + freerdp_settings_get_server_name(instance->context->settings)); + + char* message = nullptr; + size_t mlen = 0; + winpr_asprintf(&message, &mlen, "Logon Error Info %s [%s]", str_data, str_type); + + rc = sdl_show_dialog(instance->context, title, message, SHOW_DIALOG_ACCEPT_REJECT); + free(title); + free(message); + return rc; +} + +static DWORD sdl_show_ceritifcate_dialog(rdpContext* context, const char* title, + const char* message) +{ + SDLConnectionDialogHider hider(context); + if (!sdl_push_user_event(SDL_USEREVENT_CERT_DIALOG, title, message)) + return 0; + + SDL_Event event = { 0 }; + if (!sdl_wait_for_result(context, SDL_USEREVENT_CERT_RESULT, &event)) + return 0; + return static_cast<DWORD>(event.user.code); +} + +DWORD sdl_verify_changed_certificate_ex(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) +{ + const char* type = type_str_for_flags(flags); + + WINPR_ASSERT(instance); + WINPR_ASSERT(instance->context); + WINPR_ASSERT(instance->context->settings); + + SDLConnectionDialogHider hider(instance); + /* Newer versions of FreeRDP allow exposing the whole PEM by setting + * FreeRDP_CertificateCallbackPreferPEM to TRUE + */ + char* new_fp_str = nullptr; + size_t len = 0; + if (flags & VERIFY_CERT_FLAG_FP_IS_PEM) + { + winpr_asprintf(&new_fp_str, &len, + "----------- Certificate --------------\n" + "%s\n" + "--------------------------------------\n", + new_fingerprint); + } + else + winpr_asprintf(&new_fp_str, &len, "Thumbprint: %s\n", new_fingerprint); + + /* Newer versions of FreeRDP allow exposing the whole PEM by setting + * FreeRDP_CertificateCallbackPreferPEM to TRUE + */ + char* old_fp_str = nullptr; + size_t olen = 0; + if (flags & VERIFY_CERT_FLAG_FP_IS_PEM) + { + winpr_asprintf(&old_fp_str, &olen, + "----------- Certificate --------------\n" + "%s\n" + "--------------------------------------\n", + old_fingerprint); + } + else + winpr_asprintf(&old_fp_str, &olen, "Thumbprint: %s\n", old_fingerprint); + + const char* collission_str = ""; + if (flags & VERIFY_CERT_FLAG_MATCH_LEGACY_SHA1) + { + collission_str = + "A matching entry with legacy SHA1 was found in local known_hosts2 store.\n" + "If you just upgraded from a FreeRDP version before 2.0 this is expected.\n" + "The hashing algorithm has been upgraded from SHA1 to SHA256.\n" + "All manually accepted certificates must be reconfirmed!\n" + "\n"; + } + + char* title = nullptr; + size_t tlen = 0; + winpr_asprintf(&title, &tlen, "Certificate for %s:%" PRIu16 " (%s) has changed", host, port, + type); + + char* message = nullptr; + size_t mlen = 0; + winpr_asprintf(&message, &mlen, + "New Certificate details:\n" + "Common Name: %s\n" + "Subject: %s\n" + "Issuer: %s\n" + "%s\n" + "Old Certificate details:\n" + "Subject: %s\n" + "Issuer: %s\n" + "%s\n" + "%s\n" + "The above X.509 certificate does not match the certificate used for previous " + "connections.\n" + "This may indicate that the certificate has been tampered with.\n" + "Please contact the administrator of the RDP server and clarify.\n", + common_name, subject, issuer, new_fp_str, old_subject, old_issuer, old_fp_str, + collission_str); + + const DWORD rc = sdl_show_ceritifcate_dialog(instance->context, title, message); + free(title); + free(message); + free(new_fp_str); + free(old_fp_str); + + return rc; +} + +DWORD sdl_verify_certificate_ex(freerdp* instance, const char* host, UINT16 port, + const char* common_name, const char* subject, const char* issuer, + const char* fingerprint, DWORD flags) +{ + const char* type = type_str_for_flags(flags); + + /* Newer versions of FreeRDP allow exposing the whole PEM by setting + * FreeRDP_CertificateCallbackPreferPEM to TRUE + */ + char* fp_str = nullptr; + size_t len = 0; + if (flags & VERIFY_CERT_FLAG_FP_IS_PEM) + { + winpr_asprintf(&fp_str, &len, + "----------- Certificate --------------\n" + "%s\n" + "--------------------------------------\n", + fingerprint); + } + else + winpr_asprintf(&fp_str, &len, "Thumbprint: %s\n", fingerprint); + + char* title = nullptr; + size_t tlen = 0; + winpr_asprintf(&title, &tlen, "New certificate for %s:%" PRIu16 " (%s)", host, port, type); + + char* message = nullptr; + size_t mlen = 0; + winpr_asprintf( + &message, &mlen, + "Common Name: %s\n" + "Subject: %s\n" + "Issuer: %s\n" + "%s\n" + "The above X.509 certificate could not be verified, possibly because you do not have\n" + "the CA certificate in your certificate store, or the certificate has expired.\n" + "Please look at the OpenSSL documentation on how to add a private CA to the store.\n", + common_name, subject, issuer, fp_str); + + SDLConnectionDialogHider hider(instance); + const DWORD rc = sdl_show_ceritifcate_dialog(instance->context, title, message); + free(fp_str); + free(title); + free(message); + return rc; +} + +BOOL sdl_cert_dialog_show(const char* title, const char* message) +{ + int buttonid = -1; + enum + { + BUTTONID_CERT_ACCEPT_PERMANENT = 23, + BUTTONID_CERT_ACCEPT_TEMPORARY = 24, + BUTTONID_CERT_DENY = 25 + }; + const SDL_MessageBoxButtonData buttons[] = { + { 0, BUTTONID_CERT_ACCEPT_PERMANENT, "permanent" }, + { SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, BUTTONID_CERT_ACCEPT_TEMPORARY, "temporary" }, + { SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, BUTTONID_CERT_DENY, "cancel" } + }; + + const SDL_MessageBoxData data = { SDL_MESSAGEBOX_WARNING, nullptr, title, message, + ARRAYSIZE(buttons), buttons, nullptr }; + const int rc = SDL_ShowMessageBox(&data, &buttonid); + + Sint32 value = -1; + if (rc < 0) + value = 0; + else + { + switch (buttonid) + { + case BUTTONID_CERT_ACCEPT_PERMANENT: + value = 1; + break; + case BUTTONID_CERT_ACCEPT_TEMPORARY: + value = 2; + break; + default: + value = 0; + break; + } + } + + return sdl_push_user_event(SDL_USEREVENT_CERT_RESULT, value); +} + +BOOL sdl_message_dialog_show(const char* title, const char* message, Sint32 flags) +{ + int buttonid = -1; + enum + { + BUTTONID_SHOW_ACCEPT = 24, + BUTTONID_SHOW_DENY = 25 + }; + const SDL_MessageBoxButtonData buttons[] = { + { SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, BUTTONID_SHOW_ACCEPT, "accept" }, + { SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, BUTTONID_SHOW_DENY, "cancel" } + }; + + const int button_cnt = (flags & SHOW_DIALOG_ACCEPT_REJECT) ? 2 : 1; + const SDL_MessageBoxData data = { + SDL_MESSAGEBOX_WARNING, nullptr, title, message, button_cnt, buttons, nullptr + }; + const int rc = SDL_ShowMessageBox(&data, &buttonid); + + Sint32 value = -1; + if (rc < 0) + value = 0; + else + { + switch (buttonid) + { + case BUTTONID_SHOW_ACCEPT: + value = 1; + break; + default: + value = 0; + break; + } + } + + return sdl_push_user_event(SDL_USEREVENT_SHOW_RESULT, value); +} + +BOOL sdl_auth_dialog_show(const SDL_UserAuthArg* args) +{ + const std::vector<std::string> auth = { "Username: ", "Domain: ", + "Password: " }; + const std::vector<std::string> authPin = { "Device: ", "PIN: " }; + const std::vector<std::string> gw = { "GatewayUsername: ", "GatewayDomain: ", + "GatewayPassword: " }; + std::vector<std::string> prompt; + Sint32 rc = -1; + + switch (args->result) + { + case AUTH_SMARTCARD_PIN: + prompt = authPin; + break; + case AUTH_TLS: + case AUTH_RDP: + case AUTH_NLA: + prompt = auth; + break; + case GW_AUTH_HTTP: + case GW_AUTH_RDG: + case GW_AUTH_RPC: + prompt = gw; + break; + default: + break; + } + + std::vector<std::string> result; + + if (!prompt.empty()) + { + std::vector<std::string> initial{ args->user ? args->user : "Smartcard", "" }; + std::vector<Uint32> flags = { SdlInputWidget::SDL_INPUT_READONLY, + SdlInputWidget::SDL_INPUT_MASK }; + if (args->result != AUTH_SMARTCARD_PIN) + { + initial = { args->user ? args->user : "", args->domain ? args->domain : "", + args->password ? args->password : "" }; + flags = { 0, 0, SdlInputWidget::SDL_INPUT_MASK }; + } + SdlInputWidgetList ilist(args->title, prompt, initial, flags); + rc = ilist.run(result); + } + + if ((result.size() < prompt.size())) + rc = -1; + + char* user = nullptr; + char* domain = nullptr; + char* pwd = nullptr; + if (rc > 0) + { + user = _strdup(result[0].c_str()); + if (args->result == AUTH_SMARTCARD_PIN) + pwd = _strdup(result[1].c_str()); + else + { + domain = _strdup(result[1].c_str()); + pwd = _strdup(result[2].c_str()); + } + } + return sdl_push_user_event(SDL_USEREVENT_AUTH_RESULT, user, domain, pwd, rc); +} + +BOOL sdl_scard_dialog_show(const char* title, Sint32 count, const char** list) +{ + std::vector<std::string> vlist; + vlist.reserve(count); + for (Sint32 x = 0; x < count; x++) + vlist.emplace_back(list[x]); + SdlSelectList slist(title, vlist); + Sint32 value = slist.run(); + return sdl_push_user_event(SDL_USEREVENT_SCARD_RESULT, value); +} diff --git a/client/SDL/dialogs/sdl_dialogs.hpp b/client/SDL/dialogs/sdl_dialogs.hpp new file mode 100644 index 0000000..ae9bbe6 --- /dev/null +++ b/client/SDL/dialogs/sdl_dialogs.hpp @@ -0,0 +1,53 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <winpr/wtypes.h> +#include <freerdp/freerdp.h> + +#include "../sdl_types.hpp" +#include "../sdl_utils.hpp" + +BOOL sdl_authenticate_ex(freerdp* instance, char** username, char** password, char** domain, + rdp_auth_reason reason); +BOOL sdl_choose_smartcard(freerdp* instance, SmartcardCertInfo** cert_list, DWORD count, + DWORD* choice, BOOL gateway); + +SSIZE_T sdl_retry_dialog(freerdp* instance, const char* what, size_t current, void* userarg); + +DWORD sdl_verify_certificate_ex(freerdp* instance, const char* host, UINT16 port, + const char* common_name, const char* subject, const char* issuer, + const char* fingerprint, DWORD flags); + +DWORD sdl_verify_changed_certificate_ex(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 sdl_logon_error_info(freerdp* instance, UINT32 data, UINT32 type); + +BOOL sdl_present_gateway_message(freerdp* instance, UINT32 type, BOOL isDisplayMandatory, + BOOL isConsentMandatory, size_t length, const WCHAR* message); + +BOOL sdl_message_dialog_show(const char* title, const char* message, Sint32 flags); +BOOL sdl_cert_dialog_show(const char* title, const char* message); +BOOL sdl_scard_dialog_show(const char* title, Sint32 count, const char** list); +BOOL sdl_auth_dialog_show(const SDL_UserAuthArg* args); diff --git a/client/SDL/dialogs/sdl_input.cpp b/client/SDL/dialogs/sdl_input.cpp new file mode 100644 index 0000000..6e7bf12 --- /dev/null +++ b/client/SDL/dialogs/sdl_input.cpp @@ -0,0 +1,177 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "sdl_input.hpp" + +#include <cassert> + +#include <string> + +#include <SDL.h> +#include <SDL_ttf.h> + +#include "sdl_widget.hpp" +#include "sdl_button.hpp" +#include "sdl_buttons.hpp" + +static const SDL_Color inputbackgroundcolor = { 0x56, 0x56, 0x56, 0xff }; +static const SDL_Color inputhighlightcolor = { 0x80, 0, 0, 0x60 }; +static const SDL_Color inputmouseovercolor = { 0, 0x80, 0, 0x60 }; +static const SDL_Color inputfontcolor = { 0xd1, 0xcf, 0xcd, 0xff }; +static const SDL_Color labelbackgroundcolor = { 0x56, 0x56, 0x56, 0xff }; +static const SDL_Color labelfontcolor = { 0xd1, 0xcf, 0xcd, 0xff }; +static const Uint32 vpadding = 5; +static const Uint32 hpadding = 10; + +SdlInputWidget::SdlInputWidget(SDL_Renderer* renderer, const std::string& label, + const std::string& initial, Uint32 flags, size_t offset, + size_t width, size_t height) + : _flags(flags), _text(initial), _text_label(label), + _label(renderer, + { 0, static_cast<int>(offset * (height + vpadding)), static_cast<int>(width), + static_cast<int>(height) }, + false), + _input(renderer, + { static_cast<int>(width + hpadding), static_cast<int>(offset * (height + vpadding)), + static_cast<int>(width), static_cast<int>(height) }, + true), + _highlight(false), _mouseover(false) +{ +} + +SdlInputWidget::SdlInputWidget(SdlInputWidget&& other) noexcept + : _flags(std::move(other._flags)), _text(std::move(other._text)), + _text_label(std::move(other._text_label)), _label(std::move(other._label)), + _input(std::move(other._input)), _highlight(other._highlight), _mouseover(other._mouseover) +{ +} + +bool SdlInputWidget::fill_label(SDL_Renderer* renderer, SDL_Color color) +{ + if (!_label.fill(renderer, color)) + return false; + return _label.update_text(renderer, _text_label, labelfontcolor); +} + +bool SdlInputWidget::update_label(SDL_Renderer* renderer) +{ + return _label.update_text(renderer, _text_label, labelfontcolor, labelbackgroundcolor); +} + +bool SdlInputWidget::set_mouseover(SDL_Renderer* renderer, bool mouseOver) +{ + if (readonly()) + return true; + _mouseover = mouseOver; + return update_input(renderer); +} + +bool SdlInputWidget::set_highlight(SDL_Renderer* renderer, bool highlight) +{ + if (readonly()) + return true; + _highlight = highlight; + return update_input(renderer); +} + +bool SdlInputWidget::update_input(SDL_Renderer* renderer) +{ + std::vector<SDL_Color> colors = { inputbackgroundcolor }; + if (_highlight) + colors.push_back(inputhighlightcolor); + if (_mouseover) + colors.push_back(inputmouseovercolor); + + if (!_input.fill(renderer, colors)) + return false; + return update_input(renderer, inputfontcolor); +} + +bool SdlInputWidget::resize_input(size_t size) +{ + _text.resize(size); + + return true; +} + +bool SdlInputWidget::set_str(SDL_Renderer* renderer, const std::string& text) +{ + if (readonly()) + return true; + _text = text; + if (!resize_input(_text.size())) + return false; + return update_input(renderer); +} + +bool SdlInputWidget::remove_str(SDL_Renderer* renderer, size_t count) +{ + if (readonly()) + return true; + + assert(renderer); + if (_text.empty()) + return true; + + if (!resize_input(_text.size() - count)) + return false; + return update_input(renderer); +} + +bool SdlInputWidget::append_str(SDL_Renderer* renderer, const std::string& str) +{ + assert(renderer); + if (readonly()) + return true; + + _text.append(str); + if (!resize_input(_text.size())) + return false; + return update_input(renderer); +} + +const SDL_Rect& SdlInputWidget::input_rect() const +{ + return _input.rect(); +} + +std::string SdlInputWidget::value() const +{ + return _text; +} + +bool SdlInputWidget::readonly() const +{ + return (_flags & SDL_INPUT_READONLY) != 0; +} + +bool SdlInputWidget::update_input(SDL_Renderer* renderer, SDL_Color fgcolor) +{ + std::string text = _text; + if (!text.empty()) + { + if (_flags & SDL_INPUT_MASK) + { + for (char& x : text) + x = '*'; + } + } + + return _input.update_text(renderer, text, fgcolor); +} diff --git a/client/SDL/dialogs/sdl_input.hpp b/client/SDL/dialogs/sdl_input.hpp new file mode 100644 index 0000000..11492d1 --- /dev/null +++ b/client/SDL/dialogs/sdl_input.hpp @@ -0,0 +1,73 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <vector> +#include <string> + +#include <SDL.h> +#include "sdl_widget.hpp" + +class SdlInputWidget +{ + public: + enum + { + SDL_INPUT_MASK = 1, + SDL_INPUT_READONLY = 2 + }; + + public: + SdlInputWidget(SDL_Renderer* renderer, const std::string& label, const std::string& initial, + Uint32 flags, size_t offset, size_t width, size_t height); + SdlInputWidget(SdlInputWidget&& other) noexcept; + + bool fill_label(SDL_Renderer* renderer, SDL_Color color); + bool update_label(SDL_Renderer* renderer); + + bool set_mouseover(SDL_Renderer* renderer, bool mouseOver); + bool set_highlight(SDL_Renderer* renderer, bool hightlight); + bool update_input(SDL_Renderer* renderer); + bool resize_input(size_t size); + + bool set_str(SDL_Renderer* renderer, const std::string& text); + bool remove_str(SDL_Renderer* renderer, size_t count); + bool append_str(SDL_Renderer* renderer, const std::string& text); + + [[nodiscard]] const SDL_Rect& input_rect() const; + [[nodiscard]] std::string value() const; + + [[nodiscard]] bool readonly() const; + + protected: + bool update_input(SDL_Renderer* renderer, SDL_Color fgclor); + + private: + SdlInputWidget(const SdlInputWidget& other) = delete; + + private: + Uint32 _flags; + std::string _text; + std::string _text_label; + SdlWidget _label; + SdlWidget _input; + bool _highlight; + bool _mouseover; +}; diff --git a/client/SDL/dialogs/sdl_input_widgets.cpp b/client/SDL/dialogs/sdl_input_widgets.cpp new file mode 100644 index 0000000..5846308 --- /dev/null +++ b/client/SDL/dialogs/sdl_input_widgets.cpp @@ -0,0 +1,299 @@ +#include <cassert> +#include <algorithm> + +#include "sdl_input_widgets.hpp" + +static const Uint32 vpadding = 5; + +SdlInputWidgetList::SdlInputWidgetList(const std::string& title, + const std::vector<std::string>& labels, + const std::vector<std::string>& initial, + const std::vector<Uint32>& flags) + : _window(nullptr), _renderer(nullptr) +{ + assert(labels.size() == initial.size()); + assert(labels.size() == flags.size()); + const std::vector<int> buttonids = { INPUT_BUTTON_ACCEPT, INPUT_BUTTON_CANCEL }; + const std::vector<std::string> buttonlabels = { "accept", "cancel" }; + + const size_t widget_width = 300; + const size_t widget_heigth = 50; + + const size_t total_width = widget_width + widget_width; + const size_t input_height = labels.size() * (widget_heigth + vpadding) + vpadding; + const size_t total_height = input_height + widget_heigth; + _window = SDL_CreateWindow( + title.c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, total_width, total_height, + SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_MOUSE_FOCUS | SDL_WINDOW_INPUT_FOCUS); + if (_window == nullptr) + { + widget_log_error(-1, "SDL_CreateWindow"); + } + else + { + + _renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_ACCELERATED); + if (_renderer == nullptr) + { + widget_log_error(-1, "SDL_CreateRenderer"); + } + else + { + for (size_t x = 0; x < labels.size(); x++) + _list.emplace_back(_renderer, labels[x], initial[x], flags[x], x, widget_width, + widget_heigth); + + _buttons.populate(_renderer, buttonlabels, buttonids, total_width, + static_cast<Sint32>(input_height), static_cast<Sint32>(widget_width), + static_cast<Sint32>(widget_heigth)); + _buttons.set_highlight(0); + } + } +} + +ssize_t SdlInputWidgetList::next(ssize_t current) +{ + size_t iteration = 0; + auto val = static_cast<size_t>(current); + + do + { + if (iteration >= _list.size()) + return -1; + + if (iteration == 0) + { + if (current < 0) + val = 0; + else + val++; + } + else + val++; + iteration++; + val %= _list.size(); + } while (!valid(static_cast<ssize_t>(val))); + return static_cast<ssize_t>(val); +} + +bool SdlInputWidgetList::valid(ssize_t current) const +{ + if (current < 0) + return false; + auto s = static_cast<size_t>(current); + if (s >= _list.size()) + return false; + return !_list[s].readonly(); +} + +SdlInputWidget* SdlInputWidgetList::get(ssize_t index) +{ + if (index < 0) + return nullptr; + auto s = static_cast<size_t>(index); + if (s >= _list.size()) + return nullptr; + return &_list[s]; +} + +SdlInputWidgetList::~SdlInputWidgetList() +{ + _list.clear(); + _buttons.clear(); + SDL_DestroyRenderer(_renderer); + SDL_DestroyWindow(_window); +} + +bool SdlInputWidgetList::update(SDL_Renderer* renderer) +{ + for (auto& btn : _list) + { + if (!btn.update_label(renderer)) + return false; + if (!btn.update_input(renderer)) + return false; + } + + return _buttons.update(renderer); +} + +ssize_t SdlInputWidgetList::get_index(const SDL_MouseButtonEvent& button) +{ + const Sint32 x = button.x; + const Sint32 y = button.y; + for (size_t i = 0; i < _list.size(); i++) + { + auto& cur = _list[i]; + auto r = cur.input_rect(); + + if ((x >= r.x) && (x <= r.x + r.w) && (y >= r.y) && (y <= r.y + r.h)) + return i; + } + return -1; +} + +int SdlInputWidgetList::run(std::vector<std::string>& result) +{ + int res = -1; + ssize_t LastActiveTextInput = -1; + ssize_t CurrentActiveTextInput = next(-1); + + if (!_window || !_renderer) + return -2; + + try + { + bool running = true; + std::vector<SDL_Keycode> pressed; + while (running) + { + if (!clear_window(_renderer)) + throw; + + if (!update(_renderer)) + throw; + + if (!_buttons.update(_renderer)) + throw; + + SDL_Event event = {}; + SDL_WaitEvent(&event); + switch (event.type) + { + case SDL_KEYUP: + { + auto it = std::remove(pressed.begin(), pressed.end(), event.key.keysym.sym); + pressed.erase(it, pressed.end()); + } + break; + case SDL_KEYDOWN: + pressed.push_back(event.key.keysym.sym); + switch (event.key.keysym.sym) + { + case SDLK_BACKSPACE: + { + auto cur = get(CurrentActiveTextInput); + if (cur) + { + if (!cur->remove_str(_renderer, 1)) + throw; + } + } + break; + case SDLK_TAB: + CurrentActiveTextInput = next(CurrentActiveTextInput); + break; + case SDLK_RETURN: + case SDLK_RETURN2: + case SDLK_KP_ENTER: + running = false; + res = INPUT_BUTTON_ACCEPT; + break; + case SDLK_ESCAPE: + running = false; + res = INPUT_BUTTON_CANCEL; + break; + case SDLK_v: + if (pressed.size() == 2) + { + if ((pressed[0] == SDLK_LCTRL) || (pressed[0] == SDLK_RCTRL)) + { + auto cur = get(CurrentActiveTextInput); + if (cur) + { + auto text = SDL_GetClipboardText(); + cur->set_str(_renderer, text); + } + } + } + break; + default: + break; + } + break; + case SDL_TEXTINPUT: + { + auto cur = get(CurrentActiveTextInput); + if (cur) + { + if (!cur->append_str(_renderer, event.text.text)) + throw; + } + } + break; + case SDL_MOUSEMOTION: + { + auto TextInputIndex = get_index(event.button); + for (auto& cur : _list) + { + if (!cur.set_mouseover(_renderer, false)) + throw; + } + if (TextInputIndex >= 0) + { + auto& cur = _list[static_cast<size_t>(TextInputIndex)]; + if (!cur.set_mouseover(_renderer, true)) + throw; + } + + _buttons.set_mouseover(event.button.x, event.button.y); + } + break; + case SDL_MOUSEBUTTONDOWN: + { + auto val = get_index(event.button); + if (valid(val)) + CurrentActiveTextInput = val; + + auto button = _buttons.get_selected(event.button); + if (button) + { + running = false; + if (button->id() == INPUT_BUTTON_CANCEL) + res = INPUT_BUTTON_CANCEL; + else + res = INPUT_BUTTON_ACCEPT; + } + } + break; + case SDL_QUIT: + res = INPUT_BUTTON_CANCEL; + running = false; + break; + default: + break; + } + + if (LastActiveTextInput != CurrentActiveTextInput) + { + if (CurrentActiveTextInput < 0) + SDL_StopTextInput(); + else + SDL_StartTextInput(); + LastActiveTextInput = CurrentActiveTextInput; + } + + for (auto& cur : _list) + { + if (!cur.set_highlight(_renderer, false)) + throw; + } + auto cur = get(CurrentActiveTextInput); + if (cur) + { + if (!cur->set_highlight(_renderer, true)) + throw; + } + + SDL_RenderPresent(_renderer); + } + + for (auto& cur : _list) + result.push_back(cur.value()); + } + catch (...) + { + } + + return res; +} diff --git a/client/SDL/dialogs/sdl_input_widgets.hpp b/client/SDL/dialogs/sdl_input_widgets.hpp new file mode 100644 index 0000000..83568ba --- /dev/null +++ b/client/SDL/dialogs/sdl_input_widgets.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include <string> +#include <vector> +#include <SDL.h> + +#include "sdl_input.hpp" +#include "sdl_buttons.hpp" + +class SdlInputWidgetList +{ + public: + SdlInputWidgetList(const std::string& title, const std::vector<std::string>& labels, + const std::vector<std::string>& initial, const std::vector<Uint32>& flags); + virtual ~SdlInputWidgetList(); + + int run(std::vector<std::string>& result); + + protected: + bool update(SDL_Renderer* renderer); + ssize_t get_index(const SDL_MouseButtonEvent& button); + + private: + SdlInputWidgetList(const SdlInputWidgetList& other) = delete; + SdlInputWidgetList(SdlInputWidgetList&& other) = delete; + + private: + enum + { + INPUT_BUTTON_ACCEPT = 1, + INPUT_BUTTON_CANCEL = -2 + }; + + private: + ssize_t next(ssize_t current); + [[nodiscard]] bool valid(ssize_t current) const; + SdlInputWidget* get(ssize_t index); + + private: + SDL_Window* _window; + SDL_Renderer* _renderer; + std::vector<SdlInputWidget> _list; + SdlButtonList _buttons; +}; diff --git a/client/SDL/dialogs/sdl_select.cpp b/client/SDL/dialogs/sdl_select.cpp new file mode 100644 index 0000000..f0e0327 --- /dev/null +++ b/client/SDL/dialogs/sdl_select.cpp @@ -0,0 +1,74 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <cassert> + +#include <string> + +#include <SDL.h> +#include <SDL_ttf.h> + +#include "sdl_select.hpp" +#include "sdl_widget.hpp" +#include "sdl_button.hpp" +#include "sdl_buttons.hpp" +#include "sdl_input_widgets.hpp" + +static const SDL_Color labelmouseovercolor = { 0, 0x80, 0, 0x60 }; +static const SDL_Color labelbackgroundcolor = { 0x69, 0x66, 0x63, 0xff }; +static const SDL_Color labelhighlightcolor = { 0xcd, 0xca, 0x35, 0x60 }; +static const SDL_Color labelfontcolor = { 0xd1, 0xcf, 0xcd, 0xff }; + +SdlSelectWidget::SdlSelectWidget(SDL_Renderer* renderer, const std::string& label, + const SDL_Rect& rect) + : SdlWidget(renderer, rect, true), _text(label), _mouseover(false), _highlight(false) +{ + update_text(renderer); +} + +SdlSelectWidget::SdlSelectWidget(SdlSelectWidget&& other) noexcept + : SdlWidget(std::move(other)), _text(std::move(other._text)), _mouseover(other._mouseover), + _highlight(other._highlight) +{ +} + +bool SdlSelectWidget::set_mouseover(SDL_Renderer* renderer, bool mouseOver) +{ + _mouseover = mouseOver; + return update_text(renderer); +} + +bool SdlSelectWidget::set_highlight(SDL_Renderer* renderer, bool highlight) +{ + _highlight = highlight; + return update_text(renderer); +} + +bool SdlSelectWidget::update_text(SDL_Renderer* renderer) +{ + assert(renderer); + std::vector<SDL_Color> colors = { labelbackgroundcolor }; + if (_highlight) + colors.push_back(labelhighlightcolor); + if (_mouseover) + colors.push_back(labelmouseovercolor); + if (!fill(renderer, colors)) + return false; + return SdlWidget::update_text(renderer, _text, labelfontcolor); +} diff --git a/client/SDL/dialogs/sdl_select.hpp b/client/SDL/dialogs/sdl_select.hpp new file mode 100644 index 0000000..af67b74 --- /dev/null +++ b/client/SDL/dialogs/sdl_select.hpp @@ -0,0 +1,46 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <string> +#include <vector> + +#include <SDL.h> +#include "sdl_widget.hpp" + +class SdlSelectWidget : public SdlWidget +{ + public: + SdlSelectWidget(SDL_Renderer* renderer, const std::string& label, const SDL_Rect& rect); + SdlSelectWidget(SdlSelectWidget&& other) noexcept; + ~SdlSelectWidget() override = default; + + bool set_mouseover(SDL_Renderer* renderer, bool mouseOver); + bool set_highlight(SDL_Renderer* renderer, bool highlight); + bool update_text(SDL_Renderer* renderer); + + private: + SdlSelectWidget(const SdlSelectWidget& other) = delete; + + private: + std::string _text; + bool _mouseover; + bool _highlight; +}; diff --git a/client/SDL/dialogs/sdl_selectlist.cpp b/client/SDL/dialogs/sdl_selectlist.cpp new file mode 100644 index 0000000..20437cc --- /dev/null +++ b/client/SDL/dialogs/sdl_selectlist.cpp @@ -0,0 +1,208 @@ +#include "sdl_selectlist.hpp" + +static const Uint32 vpadding = 5; + +SdlSelectList::SdlSelectList(const std::string& title, const std::vector<std::string>& labels) + : _window(nullptr), _renderer(nullptr) +{ + const size_t widget_height = 50; + const size_t widget_width = 600; + + const size_t total_height = labels.size() * (widget_height + vpadding) + vpadding; + _window = SDL_CreateWindow(title.c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, + widget_width, total_height + widget_height, + SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_MOUSE_FOCUS | + SDL_WINDOW_INPUT_FOCUS); + if (_window == nullptr) + { + widget_log_error(-1, "SDL_CreateWindow"); + } + else + { + _renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_ACCELERATED); + if (_renderer == nullptr) + { + widget_log_error(-1, "SDL_CreateRenderer"); + } + else + { + SDL_Rect rect = { 0, 0, widget_width, widget_height }; + for (auto& label : labels) + { + _list.emplace_back(_renderer, label, rect); + rect.y += widget_height + vpadding; + } + + const std::vector<int> buttonids = { INPUT_BUTTON_ACCEPT, INPUT_BUTTON_CANCEL }; + const std::vector<std::string> buttonlabels = { "accept", "cancel" }; + _buttons.populate( + _renderer, buttonlabels, buttonids, widget_width, static_cast<Sint32>(total_height), + static_cast<Sint32>(widget_width / 2), static_cast<Sint32>(widget_height)); + _buttons.set_highlight(0); + } + } +} + +SdlSelectList::~SdlSelectList() +{ + _list.clear(); + _buttons.clear(); + SDL_DestroyRenderer(_renderer); + SDL_DestroyWindow(_window); +} + +int SdlSelectList::run() +{ + int res = -2; + ssize_t CurrentActiveTextInput = 0; + bool running = true; + + if (!_window || !_renderer) + return -2; + try + { + while (running) + { + if (!clear_window(_renderer)) + throw; + + if (!update_text()) + throw; + + if (!_buttons.update(_renderer)) + throw; + + SDL_Event event = { 0 }; + SDL_WaitEvent(&event); + switch (event.type) + { + case SDL_KEYDOWN: + switch (event.key.keysym.sym) + { + case SDLK_UP: + case SDLK_BACKSPACE: + if (CurrentActiveTextInput > 0) + CurrentActiveTextInput--; + else + CurrentActiveTextInput = _list.size() - 1; + break; + case SDLK_DOWN: + case SDLK_TAB: + if (CurrentActiveTextInput < 0) + CurrentActiveTextInput = 0; + else + CurrentActiveTextInput++; + CurrentActiveTextInput = CurrentActiveTextInput % _list.size(); + break; + case SDLK_RETURN: + case SDLK_RETURN2: + case SDLK_KP_ENTER: + running = false; + res = CurrentActiveTextInput; + break; + case SDLK_ESCAPE: + running = false; + res = INPUT_BUTTON_CANCEL; + break; + default: + break; + } + break; + case SDL_MOUSEMOTION: + { + ssize_t TextInputIndex = get_index(event.button); + reset_mouseover(); + if (TextInputIndex >= 0) + { + auto& cur = _list[TextInputIndex]; + if (!cur.set_mouseover(_renderer, true)) + throw; + } + + _buttons.set_mouseover(event.button.x, event.button.y); + } + break; + case SDL_MOUSEBUTTONDOWN: + { + auto button = _buttons.get_selected(event.button); + if (button) + { + running = false; + if (button->id() == INPUT_BUTTON_CANCEL) + res = INPUT_BUTTON_CANCEL; + else + res = CurrentActiveTextInput; + } + else + { + CurrentActiveTextInput = get_index(event.button); + } + } + break; + case SDL_QUIT: + res = INPUT_BUTTON_CANCEL; + running = false; + break; + default: + break; + } + + reset_highlight(); + if (CurrentActiveTextInput >= 0) + { + auto& cur = _list[CurrentActiveTextInput]; + if (!cur.set_highlight(_renderer, true)) + throw; + } + + SDL_RenderPresent(_renderer); + } + } + catch (...) + { + return -1; + } + return res; +} + +ssize_t SdlSelectList::get_index(const SDL_MouseButtonEvent& button) +{ + const Sint32 x = button.x; + const Sint32 y = button.y; + for (size_t i = 0; i < _list.size(); i++) + { + auto& cur = _list[i]; + auto r = cur.rect(); + + if ((x >= r.x) && (x <= r.x + r.w) && (y >= r.y) && (y <= r.y + r.h)) + return i; + } + return -1; +} + +bool SdlSelectList::update_text() +{ + for (auto& cur : _list) + { + if (!cur.update_text(_renderer)) + return false; + } + + return true; +} + +void SdlSelectList::reset_mouseover() +{ + for (auto& cur : _list) + { + cur.set_mouseover(_renderer, false); + } +} + +void SdlSelectList::reset_highlight() +{ + for (auto& cur : _list) + { + cur.set_highlight(_renderer, false); + } +} diff --git a/client/SDL/dialogs/sdl_selectlist.hpp b/client/SDL/dialogs/sdl_selectlist.hpp new file mode 100644 index 0000000..3da0e14 --- /dev/null +++ b/client/SDL/dialogs/sdl_selectlist.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include <string> +#include <vector> + +#include <SDL.h> + +#include "sdl_select.hpp" +#include "sdl_button.hpp" +#include "sdl_buttons.hpp" + +class SdlSelectList +{ + public: + SdlSelectList(const std::string& title, const std::vector<std::string>& labels); + virtual ~SdlSelectList(); + + int run(); + + private: + SdlSelectList(const SdlSelectList& other) = delete; + SdlSelectList(SdlSelectList&& other) = delete; + + private: + enum + { + INPUT_BUTTON_ACCEPT = 0, + INPUT_BUTTON_CANCEL = -2 + }; + + private: + ssize_t get_index(const SDL_MouseButtonEvent& button); + bool update_text(); + void reset_mouseover(); + void reset_highlight(); + + private: + SDL_Window* _window; + SDL_Renderer* _renderer; + std::vector<SdlSelectWidget> _list; + SdlButtonList _buttons; +}; diff --git a/client/SDL/dialogs/sdl_widget.cpp b/client/SDL/dialogs/sdl_widget.cpp new file mode 100644 index 0000000..6e11b5a --- /dev/null +++ b/client/SDL/dialogs/sdl_widget.cpp @@ -0,0 +1,280 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <cassert> +#include <cstdio> +#include <cstdlib> + +#include <SDL.h> +#include <SDL_ttf.h> + +#include "sdl_widget.hpp" +#include "../sdl_utils.hpp" + +#include "res/sdl_resource_manager.hpp" + +#include <freerdp/log.h> + +#if defined(WITH_SDL_IMAGE_DIALOGS) +#include <SDL_image.h> +#endif + +#define TAG CLIENT_TAG("SDL.widget") + +static const SDL_Color backgroundcolor = { 0x38, 0x36, 0x35, 0xff }; + +static const Uint32 hpadding = 10; + +SdlWidget::SdlWidget(SDL_Renderer* renderer, const SDL_Rect& rect, bool input) + : _rect(rect), _input(input) +{ + assert(renderer); + + auto ops = SDLResourceManager::get(SDLResourceManager::typeFonts(), + "OpenSans-VariableFont_wdth,wght.ttf"); + if (!ops) + widget_log_error(-1, "SDLResourceManager::get"); + else + { + _font = TTF_OpenFontRW(ops, 1, 64); + if (!_font) + widget_log_error(-1, "TTF_OpenFontRW"); + } +} + +#if defined(WITH_SDL_IMAGE_DIALOGS) +SdlWidget::SdlWidget(SDL_Renderer* renderer, const SDL_Rect& rect, SDL_RWops* ops) : _rect(rect) +{ + if (ops) + { + _image = IMG_LoadTexture_RW(renderer, ops, 1); + if (!_image) + widget_log_error(-1, "IMG_LoadTextureTyped_RW"); + } +} +#endif + +SdlWidget::SdlWidget(SdlWidget&& other) noexcept + : _font(std::move(other._font)), _image(other._image), _rect(std::move(other._rect)), + _input(other._input), _wrap(other._wrap), _text_width(other._text_width) +{ + other._font = nullptr; + other._image = nullptr; +} + +SDL_Texture* SdlWidget::render_text(SDL_Renderer* renderer, const std::string& text, + SDL_Color fgcolor, SDL_Rect& src, SDL_Rect& dst) +{ + auto surface = TTF_RenderUTF8_Blended(_font, text.c_str(), fgcolor); + if (!surface) + { + widget_log_error(-1, "TTF_RenderText_Blended"); + return nullptr; + } + + auto texture = SDL_CreateTextureFromSurface(renderer, surface); + SDL_FreeSurface(surface); + if (!texture) + { + widget_log_error(-1, "SDL_CreateTextureFromSurface"); + return nullptr; + } + + TTF_SizeUTF8(_font, text.c_str(), &src.w, &src.h); + + /* Do some magic: + * - Add padding before and after text + * - if text is too long only show the last elements + * - if text is too short only update used space + */ + dst = _rect; + dst.x += hpadding; + dst.w -= 2 * hpadding; + const float scale = static_cast<float>(dst.h) / static_cast<float>(src.h); + const float sws = static_cast<float>(src.w) * scale; + const float dws = static_cast<float>(dst.w) / scale; + if (static_cast<float>(dst.w) > sws) + dst.w = static_cast<int>(sws); + if (static_cast<float>(src.w) > dws) + { + src.x = src.w - static_cast<int>(dws); + src.w = static_cast<int>(dws); + } + return texture; +} + +SDL_Texture* SdlWidget::render_text_wrapped(SDL_Renderer* renderer, const std::string& text, + SDL_Color fgcolor, SDL_Rect& src, SDL_Rect& dst) +{ + Sint32 w = 0; + Sint32 h = 0; + TTF_SizeUTF8(_font, " ", &w, &h); + auto surface = TTF_RenderUTF8_Blended_Wrapped(_font, text.c_str(), fgcolor, _text_width); + if (!surface) + { + widget_log_error(-1, "TTF_RenderText_Blended"); + return nullptr; + } + + src.w = surface->w; + src.h = surface->h; + + auto texture = SDL_CreateTextureFromSurface(renderer, surface); + SDL_FreeSurface(surface); + if (!texture) + { + widget_log_error(-1, "SDL_CreateTextureFromSurface"); + return nullptr; + } + + /* Do some magic: + * - Add padding before and after text + * - if text is too long only show the last elements + * - if text is too short only update used space + */ + dst = _rect; + dst.x += hpadding; + dst.w -= 2 * hpadding; + const float scale = static_cast<float>(src.h) / static_cast<float>(src.w); + auto dh = src.h * scale; + if (dh < dst.h) + dst.h = dh; + + return texture; +} + +SdlWidget::~SdlWidget() +{ + TTF_CloseFont(_font); + if (_image) + SDL_DestroyTexture(_image); +} + +bool SdlWidget::error_ex(Uint32 res, const char* what, const char* file, size_t line, + const char* fkt) +{ + static wLog* log = nullptr; + if (!log) + log = WLog_Get(TAG); + return sdl_log_error_ex(res, log, what, file, line, fkt); +} + +static bool draw_rect(SDL_Renderer* renderer, const SDL_Rect* rect, SDL_Color color) +{ + const int drc = SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); + if (widget_log_error(drc, "SDL_SetRenderDrawColor")) + return false; + + const int rc = SDL_RenderFillRect(renderer, rect); + return !widget_log_error(rc, "SDL_RenderFillRect"); +} + +bool SdlWidget::fill(SDL_Renderer* renderer, SDL_Color color) +{ + std::vector<SDL_Color> colors = { color }; + return fill(renderer, colors); +} + +bool SdlWidget::fill(SDL_Renderer* renderer, const std::vector<SDL_Color>& colors) +{ + assert(renderer); + SDL_BlendMode mode = SDL_BLENDMODE_INVALID; + SDL_GetRenderDrawBlendMode(renderer, &mode); + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); + for (auto color : colors) + { + draw_rect(renderer, &_rect, color); + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD); + } + SDL_SetRenderDrawBlendMode(renderer, mode); + return true; +} + +bool SdlWidget::update_text(SDL_Renderer* renderer, const std::string& text, SDL_Color fgcolor, + SDL_Color bgcolor) +{ + assert(renderer); + + if (!fill(renderer, bgcolor)) + return false; + return update_text(renderer, text, fgcolor); +} + +bool SdlWidget::wrap() const +{ + return _wrap; +} + +bool SdlWidget::set_wrap(bool wrap, size_t width) +{ + _wrap = wrap; + _text_width = width; + return _wrap; +} + +const SDL_Rect& SdlWidget::rect() const +{ + return _rect; +} + +bool SdlWidget::update_text(SDL_Renderer* renderer, const std::string& text, SDL_Color fgcolor) +{ + + if (text.empty()) + return true; + + SDL_Rect src{}; + SDL_Rect dst{}; + + SDL_Texture* texture = nullptr; + if (_image) + { + texture = _image; + dst = _rect; + auto rc = SDL_QueryTexture(_image, nullptr, nullptr, &src.w, &src.h); + if (rc < 0) + widget_log_error(rc, "SDL_QueryTexture"); + } + else if (_wrap) + texture = render_text_wrapped(renderer, text, fgcolor, src, dst); + else + texture = render_text(renderer, text, fgcolor, src, dst); + if (!texture) + return false; + + const int rc = SDL_RenderCopy(renderer, texture, &src, &dst); + if (!_image) + SDL_DestroyTexture(texture); + if (rc < 0) + return !widget_log_error(rc, "SDL_RenderCopy"); + return true; +} + +bool clear_window(SDL_Renderer* renderer) +{ + assert(renderer); + + const int drc = SDL_SetRenderDrawColor(renderer, backgroundcolor.r, backgroundcolor.g, + backgroundcolor.b, backgroundcolor.a); + if (widget_log_error(drc, "SDL_SetRenderDrawColor")) + return false; + + const int rcls = SDL_RenderClear(renderer); + return !widget_log_error(rcls, "SDL_RenderClear"); +} diff --git a/client/SDL/dialogs/sdl_widget.hpp b/client/SDL/dialogs/sdl_widget.hpp new file mode 100644 index 0000000..ebc7dbd --- /dev/null +++ b/client/SDL/dialogs/sdl_widget.hpp @@ -0,0 +1,88 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <string> + +#include <vector> +#include <SDL.h> +#include <SDL_ttf.h> + +#if defined(_MSC_VER) +#include <BaseTsd.h> +typedef SSIZE_T ssize_t; +#endif + +#if !defined(HAS_NOEXCEPT) +#if defined(__clang__) +#if __has_feature(cxx_noexcept) +#define HAS_NOEXCEPT +#endif +#elif defined(__GXX_EXPERIMENTAL_CXX0X__) && __GNUC__ * 10 + __GNUC_MINOR__ >= 46 || \ + defined(_MSC_FULL_VER) && _MSC_FULL_VER >= 190023026 +#define HAS_NOEXCEPT +#endif +#endif + +#ifndef HAS_NOEXCEPT +#define noexcept +#endif + +class SdlWidget +{ + public: + SdlWidget(SDL_Renderer* renderer, const SDL_Rect& rect, bool input); + SdlWidget(SDL_Renderer* renderer, const SDL_Rect& rect, SDL_RWops* ops); + SdlWidget(SdlWidget&& other) noexcept; + virtual ~SdlWidget(); + + bool fill(SDL_Renderer* renderer, SDL_Color color); + bool fill(SDL_Renderer* renderer, const std::vector<SDL_Color>& colors); + bool update_text(SDL_Renderer* renderer, const std::string& text, SDL_Color fgcolor); + bool update_text(SDL_Renderer* renderer, const std::string& text, SDL_Color fgcolor, + SDL_Color bgcolor); + + bool wrap() const; + bool set_wrap(bool wrap = true, size_t width = 0); + const SDL_Rect& rect() const; + + public: +#define widget_log_error(res, what) SdlWidget::error_ex(res, what, __FILE__, __LINE__, __func__) + static bool error_ex(Uint32 res, const char* what, const char* file, size_t line, + const char* fkt); + + private: + SdlWidget(const SdlWidget& other) = delete; + + SDL_Texture* render_text(SDL_Renderer* renderer, const std::string& text, SDL_Color fgcolor, + SDL_Rect& src, SDL_Rect& dst); + SDL_Texture* render_text_wrapped(SDL_Renderer* renderer, const std::string& text, + SDL_Color fgcolor, SDL_Rect& src, SDL_Rect& dst); + + private: + TTF_Font* _font = nullptr; + SDL_Texture* _image = nullptr; + SDL_Rect _rect; + bool _input = false; + bool _wrap = false; + size_t _text_width = 0; +}; + +bool clear_window(SDL_Renderer* renderer); diff --git a/client/SDL/dialogs/test/CMakeLists.txt b/client/SDL/dialogs/test/CMakeLists.txt new file mode 100644 index 0000000..c1003d4 --- /dev/null +++ b/client/SDL/dialogs/test/CMakeLists.txt @@ -0,0 +1,30 @@ +set(MODULE_NAME "TestSDL") +set(MODULE_PREFIX "TEST_SDL") + +set(DRIVER ${MODULE_NAME}.cpp) + +set(TEST_SRCS + TestSDLDialogs.cpp +) + +create_test_sourcelist(SRCS + ${DRIVER} + ${TEST_SRCS}) + +add_library(${MODULE_NAME} ${SRCS}) + +list(APPEND LIBS + dialogs +) + +target_link_libraries(${MODULE_NAME} ${LIBS}) + +set_target_properties(${MODULE_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${TESTING_OUTPUT_DIRECTORY}") + +foreach(test ${TESTS}) + get_filename_component(TestName ${test} NAME_WE) + add_test(${TestName} ${TESTING_OUTPUT_DIRECTORY}/${MODULE_NAME} ${TestName}) +endforeach() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "FreeRDP/Client/Test") + diff --git a/client/SDL/dialogs/test/TestSDLDialogs.cpp b/client/SDL/dialogs/test/TestSDLDialogs.cpp new file mode 100644 index 0000000..558fb4c --- /dev/null +++ b/client/SDL/dialogs/test/TestSDLDialogs.cpp @@ -0,0 +1,99 @@ +#include "../sdl_selectlist.hpp" +#include "../sdl_input_widgets.hpp" + +#include <freerdp/api.h> +#include <winpr/wlog.h> + +BOOL sdl_log_error_ex(Uint32 res, wLog* log, const char* what, const char* file, size_t line, + const char* fkt) +{ + return FALSE; +} + +static bool test_input_dialog() +{ + const auto title = "sometitle"; + std::vector<std::string> labels; + std::vector<std::string> initial; + std::vector<Uint32> flags; + for (size_t x = 0; x < 12; x++) + { + labels.push_back("label" + std::to_string(x)); + initial.push_back(std::to_string(x)); + + Uint32 flag = 0; + if ((x % 2) != 0) + flag |= SdlInputWidget::SDL_INPUT_MASK; + if ((x % 3) == 0) + flag |= SdlInputWidget::SDL_INPUT_READONLY; + + flags.push_back(flag); + } + SdlInputWidgetList list{ title, labels, initial, flags }; + std::vector<std::string> result; + auto rc = list.run(result); + if (rc < 0) + { + return false; + } + if (result.size() != labels.size()) + { + return false; + } + return true; +} + +static bool test_select_dialog() +{ + const auto title = "sometitle"; + std::vector<std::string> labels; + for (size_t x = 0; x < 12; x++) + { + labels.push_back("label" + std::to_string(x)); + } + SdlSelectList list{ title, labels }; + auto rc = list.run(); + if (rc < 0) + { + return false; + } + if (static_cast<size_t>(rc) >= labels.size()) + return false; + + return true; +} + +extern "C" +{ + FREERDP_API int TestSDLDialogs(int argc, char* argv[]); +} + +int TestSDLDialogs(int argc, char* argv[]) +{ + int rc = 0; + + (void)argc; + (void)argv; + +#if 0 + SDL_Init(SDL_INIT_VIDEO); + try + { +#if 1 + if (!test_input_dialog()) + throw -1; +#endif +#if 1 + if (!test_select_dialog()) + throw -2; +#endif + } + catch (int e) + { + rc = e; + } + SDL_Quit(); + +#endif + return rc; +} diff --git a/client/SDL/man/CMakeLists.txt b/client/SDL/man/CMakeLists.txt new file mode 100644 index 0000000..1fb2adc --- /dev/null +++ b/client/SDL/man/CMakeLists.txt @@ -0,0 +1,12 @@ +set(DEPS + sdl-freerdp-channels.1.xml + sdl-freerdp-config.1.xml + sdl-freerdp-examples.1.xml + sdl-freerdp-envvar.1.xml + ) + +set(MANPAGE_NAME ${PROJECT_NAME}) +if (WITH_BINARY_VERSIONING) + set(MANPAGE_NAME ${PROJECT_NAME}${PROJECT_VERSION_MAJOR}) +endif() +generate_and_install_freerdp_man_from_xml(${PROJECT_NAME}.1 ${MANPAGE_NAME}.1 "${DEPS}") diff --git a/client/SDL/man/sdl-freerdp-config.1.xml.in b/client/SDL/man/sdl-freerdp-config.1.xml.in new file mode 100644 index 0000000..3bace73 --- /dev/null +++ b/client/SDL/man/sdl-freerdp-config.1.xml.in @@ -0,0 +1,81 @@ +<refsect1> + <title>Configuration file</title> + + <variablelist> + <varlistentry> + <term>Format and Location:</term> + <listitem> + <para>The configuration file is stored per user.<sbr/> + The <replaceable>XDG_CONFIG_HOME</replaceable> environment variable can be used to override the base directory.<sbr/> + This defaults to <replaceable>~/.config</replaceable> + The location relative to <replaceable>XDG_CONFIG_HOME</replaceable> is <replaceable>$XDG_CONFIG_HOME/@VENDOR@/@PRODUCT@/@PROJECT_NAME@.json</replaceable><sbr/> + The configuration is stored in JSON format</para> + </listitem> + </varlistentry> + <varlistentry> + <term>Supported options:</term> + <listitem> + <varlistentry> + <term><replaceable>SDL_KeyModMask</replaceable></term> + <listitem> + <varlistentry> + <listitem> + <para>Defines the key combination required for SDL client shortcuts.<sbr/> + Default <replaceable>KMOD_RSHIFT</replaceable><sbr/> + An array of <replaceable>SDL_Keymod</replaceable> strings as defined at <replaceable>https://wiki.libsdl.org/SDL2/SDL_Keymod</replaceable></para> + </listitem> + </varlistentry> + </listitem> + </varlistentry> + <varlistentry> + <term><replaceable>SDL_Fullscreen</replaceable></term> + <listitem> + <varlistentry> + <listitem> + <para>Toggles client fullscreen state.<sbr/> + Default <replaceable>SDL_SCANCODE_RETURN</replaceable>.<sbr/> + A string as defined at <replaceable>https://wiki.libsdl.org/SDL2/SDLScancodeLookup</replaceable></para> + </listitem> + </varlistentry> + </listitem> + </varlistentry> + <varlistentry> + <term><replaceable>SDL_Resizeable</replaceable></term> + <listitem> + <varlistentry> + <listitem> + <para>Toggles local window resizeable state.<sbr/> + Default <replaceable>SDL_SCANCODE_R</replaceable>.<sbr/> + A string as defined at <replaceable>https://wiki.libsdl.org/SDL2/SDLScancodeLookup</replaceable></para> + </listitem> + </varlistentry> + </listitem> + </varlistentry> + <varlistentry> + <term><replaceable>SDL_Grab</replaceable></term> + <listitem> + <varlistentry> + <listitem> + <para>Toggles keyboard and mouse grab state.<sbr/> + Default <replaceable>SDL_SCANCODE_G</replaceable>.<sbr/> + A string as defined at <replaceable>https://wiki.libsdl.org/SDL2/SDLScancodeLookup</replaceable></para> + </listitem> + </varlistentry> + </listitem> + </varlistentry> + <varlistentry> + <term><replaceable>SDL_Disconnect</replaceable></term> + <listitem> + <varlistentry> + <listitem> + <para>Disconnects from the RDP session.<sbr/> + Default <replaceable>SDL_SCANCODE_D</replaceable>.<sbr/> + A string as defined at <replaceable>https://wiki.libsdl.org/SDL2/SDLScancodeLookup</replaceable></para> + </listitem> + </varlistentry> + </listitem> + </varlistentry> + </listitem> + </varlistentry> + </variablelist> +</refsect1> diff --git a/client/SDL/man/sdl-freerdp-envvar.1.xml.in b/client/SDL/man/sdl-freerdp-envvar.1.xml.in new file mode 100644 index 0000000..ab6c8c5 --- /dev/null +++ b/client/SDL/man/sdl-freerdp-envvar.1.xml.in @@ -0,0 +1,15 @@ +<refsect1> + <title>Environment variables</title> + + <variablelist> + <varlistentry> + <term>wlog environment variable</term> + <listitem> + <para>sdl-freerdp uses wLog as its log facility, you can refer to the + corresponding man page (wlog(7)) for more informations. Arguments passed + via the <replaceable>/log-level</replaceable> or <replaceable>/log-filters</replaceable> + have precedence over the environment variables.</para> + </listitem> + </varlistentry> + </variablelist> +</refsect1> diff --git a/client/SDL/man/sdl-freerdp-examples.1.xml.in b/client/SDL/man/sdl-freerdp-examples.1.xml.in new file mode 100644 index 0000000..7b0f873 --- /dev/null +++ b/client/SDL/man/sdl-freerdp-examples.1.xml.in @@ -0,0 +1,95 @@ +<refsect1> + <title>Examples</title> + <variablelist> + <varlistentry> + <term><command>sdl-freerdp connection.rdp /p:Pwd123! /f</command></term> + <listitem> + <para>Connect in fullscreen mode using a stored configuration <replaceable>connection.rdp</replaceable> and the password <replaceable>Pwd123!</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>sdl-freerdp /u:USER /size:50%h /v:rdp.contoso.com</command></term> + <listitem> + <para>Connect to host <replaceable>rdp.contoso.com</replaceable> with user <replaceable>USER</replaceable> and a size of <replaceable>50 percent of the height</replaceable>. If width (w) is set instead of height (h) like /size:50%w. 50 percent of the width is used.</para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>sdl-freerdp /u:CONTOSO\\JohnDoe /p:Pwd123! /v:rdp.contoso.com</command></term> + <listitem> + <para>Connect to host <replaceable>rdp.contoso.com</replaceable> with user <replaceable>CONTOSO\\JohnDoe</replaceable> and password <replaceable>Pwd123!</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>sdl-freerdp /u:JohnDoe /p:Pwd123! /w:1366 /h:768 /v:192.168.1.100:4489</command></term> + <listitem> + <para>Connect to host <replaceable>192.168.1.100</replaceable> on port <replaceable>4489</replaceable> with user <replaceable>JohnDoe</replaceable>, password <replaceable>Pwd123!</replaceable>. The screen width is set to <replaceable>1366</replaceable> and the height to <replaceable>768</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>sdl-freerdp /u:JohnDoe /p:Pwd123! /vmconnect:C824F53E-95D2-46C6-9A18-23A5BB403532 /v:192.168.1.100</command></term> + <listitem> + <para>Establish a connection to host <replaceable>192.168.1.100</replaceable> with user <replaceable>JohnDoe</replaceable>, password <replaceable>Pwd123!</replaceable> and connect to Hyper-V console (use port 2179, disable negotiation) with VMID <replaceable>C824F53E-95D2-46C6-9A18-23A5BB403532</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>+clipboard</command></term> + <listitem> + <para>Activate clipboard redirection</para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/drive:home,/home/user</command></term> + <listitem> + <para>Activate drive redirection of <replaceable>/home/user</replaceable> as home drive</para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/smartcard:<device></command></term> + <listitem> + <para>Activate smartcard redirection for device <replaceable>device</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/printer:<device>,<driver></command></term> + <listitem> + <para>Activate printer redirection for printer <replaceable>device</replaceable> using driver <replaceable>driver</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/serial:<device></command></term> + <listitem> + <para>Activate serial port redirection for port <replaceable>device</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/parallel:<device></command></term> + <listitem> + <para>Activate parallel port redirection for port <replaceable>device</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/sound:sys:alsa</command></term> + <listitem> + <para>Activate audio output redirection using device <replaceable>sys:alsa</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/microphone:sys:alsa</command></term> + <listitem> + <para>Activate audio input redirection using device <replaceable>sys:alsa</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/multimedia:sys:alsa</command></term> + <listitem> + <para>Activate multimedia redirection using device <replaceable>sys:alsa</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/usb:id,dev:054c:0268</command></term> + <listitem> + <para>Activate USB device redirection for the device identified by <replaceable>054c:0268</replaceable></para> + </listitem> + </varlistentry> + </variablelist> +</refsect1> diff --git a/client/SDL/man/sdl-freerdp.1.xml.in b/client/SDL/man/sdl-freerdp.1.xml.in new file mode 100644 index 0000000..c4b9918 --- /dev/null +++ b/client/SDL/man/sdl-freerdp.1.xml.in @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE refentry +PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" +"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [ + <!ENTITY syntax SYSTEM "freerdp-argument.1.xml"> + <!ENTITY channels SYSTEM "sdl-freerdp-channels.1.xml"> + <!ENTITY config SYSTEM "sdl-freerdp-config.1.xml"> + <!ENTITY envvar SYSTEM "sdl-freerdp-envvar.1.xml"> + <!ENTITY examples SYSTEM "sdl-freerdp-examples.1.xml"> + ] +> + +<refentry> + <refentryinfo> + <date>@MAN_TODAY@</date> + <author> + <authorblurb><para>The FreeRDP Team</para></authorblurb> + </author> + </refentryinfo> + <refmeta> + <refentrytitle>@MANPAGE_NAME@</refentrytitle> + <manvolnum>1</manvolnum> + <refmiscinfo class="source">freerdp</refmiscinfo> + <refmiscinfo class="manual">@MANPAGE_NAME@</refmiscinfo> + </refmeta> + <refnamediv> + <refname><application>@MANPAGE_NAME@</application></refname> + <refpurpose>FreeRDP SDL client</refpurpose> + </refnamediv> + <refsynopsisdiv> + <refsynopsisdivinfo> + <date>@MAN_TODAY@</date> + </refsynopsisdivinfo> + <para> + <command>@MANPAGE_NAME@</command> [file] [options] [/v:server[:port]] + </para> + </refsynopsisdiv> + <refsect1> + <refsect1info> + <date>@MAN_TODAY@</date> + </refsect1info> + <title>DESCRIPTION</title> + <para> + <command>@MANPAGE_NAME@</command> is an SDL Remote Desktop Protocol (RDP) + client which is part of the FreeRDP project. An RDP server is built-in + to many editions of Windows. Alternative servers included ogon, gnome-remote-desktop, + xrdp and VRDP (VirtualBox). + </para> + </refsect1> + + &syntax; + + &channels; + + &config; + + &envvar; + + &examples; + + <refsect1> + <title>LINKS</title> + <para> + <ulink url="http://www.freerdp.com/">http://www.freerdp.com/</ulink> + </para> + </refsect1> +</refentry> diff --git a/client/SDL/sdl_channels.cpp b/client/SDL/sdl_channels.cpp new file mode 100644 index 0000000..958c5e7 --- /dev/null +++ b/client/SDL/sdl_channels.cpp @@ -0,0 +1,83 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client Channels + * + * Copyright 2022 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <winpr/assert.h> + +#include <freerdp/client/rail.h> +#include <freerdp/client/cliprdr.h> +#include <freerdp/client/disp.h> + +#include "sdl_channels.hpp" +#include "sdl_freerdp.hpp" +#include "sdl_disp.hpp" + +void sdl_OnChannelConnectedEventHandler(void* context, const ChannelConnectedEventArgs* e) +{ + auto sdl = get_context(context); + + WINPR_ASSERT(sdl); + WINPR_ASSERT(e); + + if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0) + { + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + auto clip = reinterpret_cast<CliprdrClientContext*>(e->pInterface); + WINPR_ASSERT(clip); + clip->custom = context; + } + else if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) == 0) + { + auto disp = reinterpret_cast<DispClientContext*>(e->pInterface); + WINPR_ASSERT(disp); + sdl->disp.init(disp); + } + else + freerdp_client_OnChannelConnectedEventHandler(context, e); +} + +void sdl_OnChannelDisconnectedEventHandler(void* context, const ChannelDisconnectedEventArgs* e) +{ + auto sdl = get_context(context); + + WINPR_ASSERT(sdl); + WINPR_ASSERT(e); + + // TODO: Set resizeable depending on disp channel and /dynamic-resolution + if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0) + { + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + auto clip = reinterpret_cast<CliprdrClientContext*>(e->pInterface); + WINPR_ASSERT(clip); + clip->custom = nullptr; + } + else if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) == 0) + { + auto disp = reinterpret_cast<DispClientContext*>(e->pInterface); + WINPR_ASSERT(disp); + sdl->disp.uninit(disp); + } + else + freerdp_client_OnChannelDisconnectedEventHandler(context, e); +} diff --git a/client/SDL/sdl_channels.hpp b/client/SDL/sdl_channels.hpp new file mode 100644 index 0000000..a5c9f7d --- /dev/null +++ b/client/SDL/sdl_channels.hpp @@ -0,0 +1,29 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client Channels + * + * Copyright 2022 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <freerdp/freerdp.h> +#include <freerdp/client/channels.h> + +int sdl_on_channel_connected(freerdp* instance, const char* name, void* pInterface); +int sdl_on_channel_disconnected(freerdp* instance, const char* name, void* pInterface); + +void sdl_OnChannelConnectedEventHandler(void* context, const ChannelConnectedEventArgs* e); +void sdl_OnChannelDisconnectedEventHandler(void* context, const ChannelDisconnectedEventArgs* e); diff --git a/client/SDL/sdl_disp.cpp b/client/SDL/sdl_disp.cpp new file mode 100644 index 0000000..ffd13c8 --- /dev/null +++ b/client/SDL/sdl_disp.cpp @@ -0,0 +1,471 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Display Control Channel + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <vector> +#include <winpr/sysinfo.h> +#include <winpr/assert.h> + +#include <freerdp/gdi/gdi.h> + +#include <SDL.h> + +#include "sdl_disp.hpp" +#include "sdl_kbd.hpp" +#include "sdl_utils.hpp" +#include "sdl_freerdp.hpp" + +#include <freerdp/log.h> +#define TAG CLIENT_TAG("sdl.disp") + +#define RESIZE_MIN_DELAY 200 /* minimum delay in ms between two resizes */ +#define MAX_RETRIES 5 + +BOOL sdlDispContext::settings_changed() +{ + auto settings = _sdl->context()->settings; + WINPR_ASSERT(settings); + + if (_lastSentWidth != _targetWidth) + return TRUE; + + if (_lastSentHeight != _targetHeight) + return TRUE; + + if (_lastSentDesktopOrientation != + freerdp_settings_get_uint16(settings, FreeRDP_DesktopOrientation)) + return TRUE; + + if (_lastSentDesktopScaleFactor != + freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor)) + return TRUE; + + if (_lastSentDeviceScaleFactor != + freerdp_settings_get_uint32(settings, FreeRDP_DeviceScaleFactor)) + return TRUE; + /* TODO + if (_fullscreen != _sdl->fullscreen) + return TRUE; + */ + return FALSE; +} + +BOOL sdlDispContext::update_last_sent() +{ + WINPR_ASSERT(_sdl); + + auto settings = _sdl->context()->settings; + WINPR_ASSERT(settings); + + _lastSentWidth = _targetWidth; + _lastSentHeight = _targetHeight; + _lastSentDesktopOrientation = freerdp_settings_get_uint16(settings, FreeRDP_DesktopOrientation); + _lastSentDesktopScaleFactor = freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor); + _lastSentDeviceScaleFactor = freerdp_settings_get_uint32(settings, FreeRDP_DeviceScaleFactor); + // TODO _fullscreen = _sdl->fullscreen; + return TRUE; +} + +BOOL sdlDispContext::sendResize() +{ + DISPLAY_CONTROL_MONITOR_LAYOUT layout = {}; + auto settings = _sdl->context()->settings; + + if (!settings) + return FALSE; + + if (!_activated || !_disp) + return TRUE; + + if (GetTickCount64() - _lastSentDate < RESIZE_MIN_DELAY) + return TRUE; + + _lastSentDate = GetTickCount64(); + + if (!settings_changed()) + return TRUE; + + const UINT32 mcount = freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount); + if (_sdl->fullscreen && (mcount > 0)) + { + auto monitors = static_cast<const rdpMonitor*>( + freerdp_settings_get_pointer(settings, FreeRDP_MonitorDefArray)); + if (sendLayout(monitors, mcount) != CHANNEL_RC_OK) + return FALSE; + } + else + { + _waitingResize = TRUE; + layout.Flags = DISPLAY_CONTROL_MONITOR_PRIMARY; + layout.Top = layout.Left = 0; + layout.Width = _targetWidth; + layout.Height = _targetHeight; + layout.Orientation = freerdp_settings_get_uint16(settings, FreeRDP_DesktopOrientation); + layout.DesktopScaleFactor = + freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor); + layout.DeviceScaleFactor = freerdp_settings_get_uint32(settings, FreeRDP_DeviceScaleFactor); + layout.PhysicalWidth = _targetWidth; + layout.PhysicalHeight = _targetHeight; + + if (IFCALLRESULT(CHANNEL_RC_OK, _disp->SendMonitorLayout, _disp, 1, &layout) != + CHANNEL_RC_OK) + return FALSE; + } + return update_last_sent(); +} + +BOOL sdlDispContext::set_window_resizable() +{ + _sdl->update_resizeable(TRUE); + return TRUE; +} + +static BOOL sdl_disp_check_context(void* context, SdlContext** ppsdl, sdlDispContext** ppsdlDisp, + rdpSettings** ppSettings) +{ + if (!context) + return FALSE; + + auto sdl = get_context(context); + + if (!sdl->context()->settings) + return FALSE; + + *ppsdl = sdl; + *ppsdlDisp = &sdl->disp; + *ppSettings = sdl->context()->settings; + return TRUE; +} + +void sdlDispContext::OnActivated(void* context, const ActivatedEventArgs* e) +{ + SdlContext* sdl = nullptr; + sdlDispContext* sdlDisp = nullptr; + rdpSettings* settings = nullptr; + + if (!sdl_disp_check_context(context, &sdl, &sdlDisp, &settings)) + return; + + sdlDisp->_waitingResize = FALSE; + + if (sdlDisp->_activated && !freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)) + { + sdlDisp->set_window_resizable(); + + if (e->firstActivation) + return; + + sdlDisp->addTimer(); + } +} + +void sdlDispContext::OnGraphicsReset(void* context, const GraphicsResetEventArgs* e) +{ + SdlContext* sdl = nullptr; + sdlDispContext* sdlDisp = nullptr; + rdpSettings* settings = nullptr; + + WINPR_UNUSED(e); + if (!sdl_disp_check_context(context, &sdl, &sdlDisp, &settings)) + return; + + sdlDisp->_waitingResize = FALSE; + + if (sdlDisp->_activated && !freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)) + { + sdlDisp->set_window_resizable(); + sdlDisp->addTimer(); + } +} + +Uint32 sdlDispContext::OnTimer(Uint32 interval, void* param) +{ + auto ctx = static_cast<sdlDispContext*>(param); + if (!ctx) + return 0; + + SdlContext* sdl = ctx->_sdl; + sdlDispContext* sdlDisp = nullptr; + rdpSettings* settings = nullptr; + + if (!sdl_disp_check_context(sdl->context(), &sdl, &sdlDisp, &settings)) + return 0; + + WLog_Print(sdl->log, WLOG_TRACE, "checking for display changes..."); + if (!sdlDisp->_activated || freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)) + return 0; + else + { + auto rc = sdlDisp->sendResize(); + if (!rc) + WLog_Print(sdl->log, WLOG_TRACE, "sent new display layout, result %d", rc); + } + if (sdlDisp->_timer_retries++ >= MAX_RETRIES) + { + WLog_Print(sdl->log, WLOG_TRACE, "deactivate timer, retries exceeded"); + return 0; + } + + WLog_Print(sdl->log, WLOG_TRACE, "fire timer one more time"); + return interval; +} + +UINT sdlDispContext::sendLayout(const rdpMonitor* monitors, size_t nmonitors) +{ + UINT ret = CHANNEL_RC_OK; + + WINPR_ASSERT(monitors); + WINPR_ASSERT(nmonitors > 0); + + auto settings = _sdl->context()->settings; + WINPR_ASSERT(settings); + + std::vector<DISPLAY_CONTROL_MONITOR_LAYOUT> layouts; + layouts.resize(nmonitors); + + for (size_t i = 0; i < nmonitors; i++) + { + auto monitor = &monitors[i]; + auto layout = &layouts[i]; + + layout->Flags = (monitor->is_primary ? DISPLAY_CONTROL_MONITOR_PRIMARY : 0); + layout->Left = monitor->x; + layout->Top = monitor->y; + layout->Width = monitor->width; + layout->Height = monitor->height; + layout->Orientation = ORIENTATION_LANDSCAPE; + layout->PhysicalWidth = monitor->attributes.physicalWidth; + layout->PhysicalHeight = monitor->attributes.physicalHeight; + + switch (monitor->attributes.orientation) + { + case 90: + layout->Orientation = ORIENTATION_PORTRAIT; + break; + + case 180: + layout->Orientation = ORIENTATION_LANDSCAPE_FLIPPED; + break; + + case 270: + layout->Orientation = ORIENTATION_PORTRAIT_FLIPPED; + break; + + case 0: + default: + /* MS-RDPEDISP - 2.2.2.2.1: + * Orientation (4 bytes): A 32-bit unsigned integer that specifies the + * orientation of the monitor in degrees. Valid values are 0, 90, 180 + * or 270 + * + * So we default to ORIENTATION_LANDSCAPE + */ + layout->Orientation = ORIENTATION_LANDSCAPE; + break; + } + + layout->DesktopScaleFactor = + freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor); + layout->DeviceScaleFactor = + freerdp_settings_get_uint32(settings, FreeRDP_DeviceScaleFactor); + } + + WINPR_ASSERT(_disp); + ret = IFCALLRESULT(CHANNEL_RC_OK, _disp->SendMonitorLayout, _disp, layouts.size(), + layouts.data()); + return ret; +} + +BOOL sdlDispContext::addTimer() +{ + if (SDL_WasInit(SDL_INIT_TIMER) == 0) + return FALSE; + + SDL_RemoveTimer(_timer); + WLog_Print(_sdl->log, WLOG_TRACE, "adding new display check timer"); + + _timer_retries = 0; + sendResize(); + _timer = SDL_AddTimer(1000, sdlDispContext::OnTimer, this); + return TRUE; +} + +#if SDL_VERSION_ATLEAST(2, 0, 10) +BOOL sdlDispContext::handle_display_event(const SDL_DisplayEvent* ev) +{ + WINPR_ASSERT(ev); + + switch (ev->event) + { +#if SDL_VERSION_ATLEAST(2, 0, 14) + case SDL_DISPLAYEVENT_CONNECTED: + SDL_Log("A new display with id %d was connected", ev->display); + return TRUE; + case SDL_DISPLAYEVENT_DISCONNECTED: + SDL_Log("The display with id %d was disconnected", ev->display); + return TRUE; +#endif + case SDL_DISPLAYEVENT_ORIENTATION: + SDL_Log("The orientation of display with id %d was changed", ev->display); + return TRUE; + default: + return TRUE; + } +} +#endif + +BOOL sdlDispContext::handle_window_event(const SDL_WindowEvent* ev) +{ + WINPR_ASSERT(ev); + + auto bordered = freerdp_settings_get_bool(_sdl->context()->settings, FreeRDP_Decorations) + ? SDL_TRUE + : SDL_FALSE; + + auto it = _sdl->windows.find(ev->windowID); + if (it != _sdl->windows.end()) + it->second.setBordered(bordered); + + switch (ev->event) + { + case SDL_WINDOWEVENT_HIDDEN: + case SDL_WINDOWEVENT_MINIMIZED: + gdi_send_suppress_output(_sdl->context()->gdi, TRUE); + + return TRUE; + + case SDL_WINDOWEVENT_EXPOSED: + case SDL_WINDOWEVENT_SHOWN: + case SDL_WINDOWEVENT_MAXIMIZED: + case SDL_WINDOWEVENT_RESTORED: + gdi_send_suppress_output(_sdl->context()->gdi, FALSE); + return TRUE; + + case SDL_WINDOWEVENT_RESIZED: + case SDL_WINDOWEVENT_SIZE_CHANGED: + _targetWidth = ev->data1; + _targetHeight = ev->data2; + return addTimer(); + + case SDL_WINDOWEVENT_LEAVE: + WINPR_ASSERT(_sdl); + _sdl->input.keyboard_grab(ev->windowID, SDL_FALSE); + return TRUE; + case SDL_WINDOWEVENT_ENTER: + WINPR_ASSERT(_sdl); + _sdl->input.keyboard_grab(ev->windowID, SDL_TRUE); + return _sdl->input.keyboard_focus_in(); + case SDL_WINDOWEVENT_FOCUS_GAINED: + case SDL_WINDOWEVENT_TAKE_FOCUS: + return _sdl->input.keyboard_focus_in(); + + default: + return TRUE; + } +} + +UINT sdlDispContext::DisplayControlCaps(DispClientContext* disp, UINT32 maxNumMonitors, + UINT32 maxMonitorAreaFactorA, UINT32 maxMonitorAreaFactorB) +{ + /* we're called only if dynamic resolution update is activated */ + WINPR_ASSERT(disp); + + auto sdlDisp = reinterpret_cast<sdlDispContext*>(disp->custom); + return sdlDisp->DisplayControlCaps(maxNumMonitors, maxMonitorAreaFactorA, + maxMonitorAreaFactorB); +} + +UINT sdlDispContext::DisplayControlCaps(UINT32 maxNumMonitors, UINT32 maxMonitorAreaFactorA, + UINT32 maxMonitorAreaFactorB) +{ + auto settings = _sdl->context()->settings; + WINPR_ASSERT(settings); + + WLog_DBG(TAG, + "DisplayControlCapsPdu: MaxNumMonitors: %" PRIu32 " MaxMonitorAreaFactorA: %" PRIu32 + " MaxMonitorAreaFactorB: %" PRIu32 "", + maxNumMonitors, maxMonitorAreaFactorA, maxMonitorAreaFactorB); + _activated = TRUE; + + if (freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)) + return CHANNEL_RC_OK; + + WLog_DBG(TAG, "DisplayControlCapsPdu: setting the window as resizable"); + return set_window_resizable() ? CHANNEL_RC_OK : CHANNEL_RC_NO_MEMORY; +} + +BOOL sdlDispContext::init(DispClientContext* disp) +{ + if (!disp) + return FALSE; + + auto settings = _sdl->context()->settings; + + if (!settings) + return FALSE; + + _disp = disp; + disp->custom = this; + + if (freerdp_settings_get_bool(settings, FreeRDP_DynamicResolutionUpdate)) + { + disp->DisplayControlCaps = sdlDispContext::DisplayControlCaps; + } + + _sdl->update_resizeable(TRUE); + return TRUE; +} + +BOOL sdlDispContext::uninit(DispClientContext* disp) +{ + if (!disp) + return FALSE; + + _disp = nullptr; + _sdl->update_resizeable(FALSE); + return TRUE; +} + +sdlDispContext::sdlDispContext(SdlContext* sdl) : _sdl(sdl), _timer(0) +{ + SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO); + + WINPR_ASSERT(_sdl); + WINPR_ASSERT(_sdl->context()->settings); + WINPR_ASSERT(_sdl->context()->pubSub); + + auto settings = _sdl->context()->settings; + auto pubSub = _sdl->context()->pubSub; + + _lastSentWidth = _targetWidth = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth); + _lastSentHeight = _targetHeight = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight); + PubSub_SubscribeActivated(pubSub, sdlDispContext::OnActivated); + PubSub_SubscribeGraphicsReset(pubSub, sdlDispContext::OnGraphicsReset); + addTimer(); +} + +sdlDispContext::~sdlDispContext() +{ + wPubSub* pubSub = _sdl->context()->pubSub; + WINPR_ASSERT(pubSub); + + PubSub_UnsubscribeActivated(pubSub, sdlDispContext::OnActivated); + PubSub_UnsubscribeGraphicsReset(pubSub, sdlDispContext::OnGraphicsReset); + SDL_RemoveTimer(_timer); + SDL_Quit(); +} diff --git a/client/SDL/sdl_disp.hpp b/client/SDL/sdl_disp.hpp new file mode 100644 index 0000000..fbe6362 --- /dev/null +++ b/client/SDL/sdl_disp.hpp @@ -0,0 +1,82 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Display Control Channel + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <freerdp/types.h> +#include <freerdp/event.h> +#include <freerdp/client/disp.h> + +#include "sdl_types.hpp" + +#include <SDL.h> + +class sdlDispContext +{ + + public: + explicit sdlDispContext(SdlContext* sdl); + ~sdlDispContext(); + + BOOL init(DispClientContext* disp); + BOOL uninit(DispClientContext* disp); + +#if SDL_VERSION_ATLEAST(2, 0, 10) + BOOL handle_display_event(const SDL_DisplayEvent* ev); +#endif + + BOOL handle_window_event(const SDL_WindowEvent* ev); + + private: + UINT DisplayControlCaps(UINT32 maxNumMonitors, UINT32 maxMonitorAreaFactorA, + UINT32 maxMonitorAreaFactorB); + BOOL set_window_resizable(); + + BOOL sendResize(); + BOOL settings_changed(); + BOOL update_last_sent(); + UINT sendLayout(const rdpMonitor* monitors, size_t nmonitors); + + BOOL addTimer(); + + private: + static UINT DisplayControlCaps(DispClientContext* disp, UINT32 maxNumMonitors, + UINT32 maxMonitorAreaFactorA, UINT32 maxMonitorAreaFactorB); + static void OnActivated(void* context, const ActivatedEventArgs* e); + static void OnGraphicsReset(void* context, const GraphicsResetEventArgs* e); + static Uint32 SDLCALL OnTimer(Uint32 interval, void* param); + + private: + SdlContext* _sdl = nullptr; + DispClientContext* _disp = nullptr; + int _eventBase = -1; + int _errorBase = -1; + int _lastSentWidth = -1; + int _lastSentHeight = -1; + UINT64 _lastSentDate = 0; + int _targetWidth = -1; + int _targetHeight = -1; + BOOL _activated = FALSE; + BOOL _waitingResize = FALSE; + BOOL _fullscreen = FALSE; + UINT16 _lastSentDesktopOrientation = 0; + UINT32 _lastSentDesktopScaleFactor = 0; + UINT32 _lastSentDeviceScaleFactor = 0; + SDL_TimerID _timer = 0; + unsigned _timer_retries = 0; +}; diff --git a/client/SDL/sdl_freerdp.cpp b/client/SDL/sdl_freerdp.cpp new file mode 100644 index 0000000..890bf77 --- /dev/null +++ b/client/SDL/sdl_freerdp.cpp @@ -0,0 +1,1704 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP SDL UI + * + * Copyright 2022 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <memory> +#include <mutex> +#include <iostream> + +#include <freerdp/config.h> + +#include <cerrno> +#include <cstdio> +#include <cstring> + +#include <freerdp/freerdp.h> +#include <freerdp/constants.h> +#include <freerdp/gdi/gdi.h> +#include <freerdp/streamdump.h> +#include <freerdp/utils/signal.h> + +#include <freerdp/client/file.h> +#include <freerdp/client/cmdline.h> +#include <freerdp/client/cliprdr.h> +#include <freerdp/client/channels.h> +#include <freerdp/channels/channels.h> + +#include <winpr/crt.h> +#include <winpr/assert.h> +#include <winpr/synch.h> +#include <freerdp/log.h> + +#include <SDL.h> +#include <SDL_hints.h> +#include <SDL_video.h> + +#include "sdl_channels.hpp" +#include "sdl_freerdp.hpp" +#include "sdl_utils.hpp" +#include "sdl_disp.hpp" +#include "sdl_monitor.hpp" +#include "sdl_kbd.hpp" +#include "sdl_touch.hpp" +#include "sdl_pointer.hpp" +#include "dialogs/sdl_dialogs.hpp" + +#include "aad/sdl_webview.hpp" + +#define SDL_TAG CLIENT_TAG("SDL") + +enum SDL_EXIT_CODE +{ + /* section 0-15: protocol-independent codes */ + SDL_EXIT_SUCCESS = 0, + SDL_EXIT_DISCONNECT = 1, + SDL_EXIT_LOGOFF = 2, + SDL_EXIT_IDLE_TIMEOUT = 3, + SDL_EXIT_LOGON_TIMEOUT = 4, + SDL_EXIT_CONN_REPLACED = 5, + SDL_EXIT_OUT_OF_MEMORY = 6, + SDL_EXIT_CONN_DENIED = 7, + SDL_EXIT_CONN_DENIED_FIPS = 8, + SDL_EXIT_USER_PRIVILEGES = 9, + SDL_EXIT_FRESH_CREDENTIALS_REQUIRED = 10, + SDL_EXIT_DISCONNECT_BY_USER = 11, + + /* section 16-31: license error set */ + SDL_EXIT_LICENSE_INTERNAL = 16, + SDL_EXIT_LICENSE_NO_LICENSE_SERVER = 17, + SDL_EXIT_LICENSE_NO_LICENSE = 18, + SDL_EXIT_LICENSE_BAD_CLIENT_MSG = 19, + SDL_EXIT_LICENSE_HWID_DOESNT_MATCH = 20, + SDL_EXIT_LICENSE_BAD_CLIENT = 21, + SDL_EXIT_LICENSE_CANT_FINISH_PROTOCOL = 22, + SDL_EXIT_LICENSE_CLIENT_ENDED_PROTOCOL = 23, + SDL_EXIT_LICENSE_BAD_CLIENT_ENCRYPTION = 24, + SDL_EXIT_LICENSE_CANT_UPGRADE = 25, + SDL_EXIT_LICENSE_NO_REMOTE_CONNECTIONS = 26, + + /* section 32-127: RDP protocol error set */ + SDL_EXIT_RDP = 32, + + /* section 128-254: xfreerdp specific exit codes */ + SDL_EXIT_PARSE_ARGUMENTS = 128, + SDL_EXIT_MEMORY = 129, + SDL_EXIT_PROTOCOL = 130, + SDL_EXIT_CONN_FAILED = 131, + SDL_EXIT_AUTH_FAILURE = 132, + SDL_EXIT_NEGO_FAILURE = 133, + SDL_EXIT_LOGON_FAILURE = 134, + SDL_EXIT_ACCOUNT_LOCKED_OUT = 135, + SDL_EXIT_PRE_CONNECT_FAILED = 136, + SDL_EXIT_CONNECT_UNDEFINED = 137, + SDL_EXIT_POST_CONNECT_FAILED = 138, + SDL_EXIT_DNS_ERROR = 139, + SDL_EXIT_DNS_NAME_NOT_FOUND = 140, + SDL_EXIT_CONNECT_FAILED = 141, + SDL_EXIT_MCS_CONNECT_INITIAL_ERROR = 142, + SDL_EXIT_TLS_CONNECT_FAILED = 143, + SDL_EXIT_INSUFFICIENT_PRIVILEGES = 144, + SDL_EXIT_CONNECT_CANCELLED = 145, + + SDL_EXIT_CONNECT_TRANSPORT_FAILED = 147, + SDL_EXIT_CONNECT_PASSWORD_EXPIRED = 148, + SDL_EXIT_CONNECT_PASSWORD_MUST_CHANGE = 149, + SDL_EXIT_CONNECT_KDC_UNREACHABLE = 150, + SDL_EXIT_CONNECT_ACCOUNT_DISABLED = 151, + SDL_EXIT_CONNECT_PASSWORD_CERTAINLY_EXPIRED = 152, + SDL_EXIT_CONNECT_CLIENT_REVOKED = 153, + SDL_EXIT_CONNECT_WRONG_PASSWORD = 154, + SDL_EXIT_CONNECT_ACCESS_DENIED = 155, + SDL_EXIT_CONNECT_ACCOUNT_RESTRICTION = 156, + SDL_EXIT_CONNECT_ACCOUNT_EXPIRED = 157, + SDL_EXIT_CONNECT_LOGON_TYPE_NOT_GRANTED = 158, + SDL_EXIT_CONNECT_NO_OR_MISSING_CREDENTIALS = 159, + + SDL_EXIT_UNKNOWN = 255, +}; + +struct sdl_exit_code_map_t +{ + DWORD error; + int code; + const char* code_tag; +}; + +#define ENTRY(x, y) \ + { \ + x, y, #y \ + } +static const struct sdl_exit_code_map_t sdl_exit_code_map[] = { + ENTRY(FREERDP_ERROR_SUCCESS, SDL_EXIT_SUCCESS), ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_DISCONNECT), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LOGOFF), ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_IDLE_TIMEOUT), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LOGON_TIMEOUT), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_CONN_REPLACED), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_OUT_OF_MEMORY), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_CONN_DENIED), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_CONN_DENIED_FIPS), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_USER_PRIVILEGES), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_FRESH_CREDENTIALS_REQUIRED), + ENTRY(ERRINFO_LOGOFF_BY_USER, SDL_EXIT_DISCONNECT_BY_USER), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_UNKNOWN), + + /* section 16-31: license error set */ + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_INTERNAL), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_NO_LICENSE_SERVER), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_NO_LICENSE), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_BAD_CLIENT_MSG), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_HWID_DOESNT_MATCH), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_BAD_CLIENT), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_CANT_FINISH_PROTOCOL), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_CLIENT_ENDED_PROTOCOL), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_BAD_CLIENT_ENCRYPTION), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_CANT_UPGRADE), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_NO_REMOTE_CONNECTIONS), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_CANT_UPGRADE), + + /* section 32-127: RDP protocol error set */ + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_RDP), + + /* section 128-254: xfreerdp specific exit codes */ + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_PARSE_ARGUMENTS), ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_MEMORY), + ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_PROTOCOL), ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_CONN_FAILED), + + ENTRY(FREERDP_ERROR_AUTHENTICATION_FAILED, SDL_EXIT_AUTH_FAILURE), + ENTRY(FREERDP_ERROR_SECURITY_NEGO_CONNECT_FAILED, SDL_EXIT_NEGO_FAILURE), + ENTRY(FREERDP_ERROR_CONNECT_LOGON_FAILURE, SDL_EXIT_LOGON_FAILURE), + ENTRY(FREERDP_ERROR_CONNECT_ACCOUNT_LOCKED_OUT, SDL_EXIT_ACCOUNT_LOCKED_OUT), + ENTRY(FREERDP_ERROR_PRE_CONNECT_FAILED, SDL_EXIT_PRE_CONNECT_FAILED), + ENTRY(FREERDP_ERROR_CONNECT_UNDEFINED, SDL_EXIT_CONNECT_UNDEFINED), + ENTRY(FREERDP_ERROR_POST_CONNECT_FAILED, SDL_EXIT_POST_CONNECT_FAILED), + ENTRY(FREERDP_ERROR_DNS_ERROR, SDL_EXIT_DNS_ERROR), + ENTRY(FREERDP_ERROR_DNS_NAME_NOT_FOUND, SDL_EXIT_DNS_NAME_NOT_FOUND), + ENTRY(FREERDP_ERROR_CONNECT_FAILED, SDL_EXIT_CONNECT_FAILED), + ENTRY(FREERDP_ERROR_MCS_CONNECT_INITIAL_ERROR, SDL_EXIT_MCS_CONNECT_INITIAL_ERROR), + ENTRY(FREERDP_ERROR_TLS_CONNECT_FAILED, SDL_EXIT_TLS_CONNECT_FAILED), + ENTRY(FREERDP_ERROR_INSUFFICIENT_PRIVILEGES, SDL_EXIT_INSUFFICIENT_PRIVILEGES), + ENTRY(FREERDP_ERROR_CONNECT_CANCELLED, SDL_EXIT_CONNECT_CANCELLED), + ENTRY(FREERDP_ERROR_CONNECT_TRANSPORT_FAILED, SDL_EXIT_CONNECT_TRANSPORT_FAILED), + ENTRY(FREERDP_ERROR_CONNECT_PASSWORD_EXPIRED, SDL_EXIT_CONNECT_PASSWORD_EXPIRED), + ENTRY(FREERDP_ERROR_CONNECT_PASSWORD_MUST_CHANGE, SDL_EXIT_CONNECT_PASSWORD_MUST_CHANGE), + ENTRY(FREERDP_ERROR_CONNECT_KDC_UNREACHABLE, SDL_EXIT_CONNECT_KDC_UNREACHABLE), + ENTRY(FREERDP_ERROR_CONNECT_ACCOUNT_DISABLED, SDL_EXIT_CONNECT_ACCOUNT_DISABLED), + ENTRY(FREERDP_ERROR_CONNECT_PASSWORD_CERTAINLY_EXPIRED, + SDL_EXIT_CONNECT_PASSWORD_CERTAINLY_EXPIRED), + ENTRY(FREERDP_ERROR_CONNECT_CLIENT_REVOKED, SDL_EXIT_CONNECT_CLIENT_REVOKED), + ENTRY(FREERDP_ERROR_CONNECT_WRONG_PASSWORD, SDL_EXIT_CONNECT_WRONG_PASSWORD), + ENTRY(FREERDP_ERROR_CONNECT_ACCESS_DENIED, SDL_EXIT_CONNECT_ACCESS_DENIED), + ENTRY(FREERDP_ERROR_CONNECT_ACCOUNT_RESTRICTION, SDL_EXIT_CONNECT_ACCOUNT_RESTRICTION), + ENTRY(FREERDP_ERROR_CONNECT_ACCOUNT_EXPIRED, SDL_EXIT_CONNECT_ACCOUNT_EXPIRED), + ENTRY(FREERDP_ERROR_CONNECT_LOGON_TYPE_NOT_GRANTED, SDL_EXIT_CONNECT_LOGON_TYPE_NOT_GRANTED), + ENTRY(FREERDP_ERROR_CONNECT_NO_OR_MISSING_CREDENTIALS, + SDL_EXIT_CONNECT_NO_OR_MISSING_CREDENTIALS) +}; + +static const struct sdl_exit_code_map_t* sdl_map_entry_by_code(int exit_code) +{ + for (size_t x = 0; x < ARRAYSIZE(sdl_exit_code_map); x++) + { + const struct sdl_exit_code_map_t* cur = &sdl_exit_code_map[x]; + if (cur->code == exit_code) + return cur; + } + return nullptr; +} + +static const struct sdl_exit_code_map_t* sdl_map_entry_by_error(DWORD error) +{ + for (size_t x = 0; x < ARRAYSIZE(sdl_exit_code_map); x++) + { + const struct sdl_exit_code_map_t* cur = &sdl_exit_code_map[x]; + if (cur->error == error) + return cur; + } + return nullptr; +} + +static int sdl_map_error_to_exit_code(DWORD error) +{ + const struct sdl_exit_code_map_t* entry = sdl_map_entry_by_error(error); + if (entry) + return entry->code; + + return SDL_EXIT_CONN_FAILED; +} + +static const char* sdl_map_error_to_code_tag(DWORD error) +{ + const struct sdl_exit_code_map_t* entry = sdl_map_entry_by_error(error); + if (entry) + return entry->code_tag; + return nullptr; +} + +static const char* sdl_map_to_code_tag(int code) +{ + const struct sdl_exit_code_map_t* entry = sdl_map_entry_by_code(code); + if (entry) + return entry->code_tag; + return nullptr; +} + +static int error_info_to_error(freerdp* instance, DWORD* pcode, char** msg, size_t* len) +{ + const DWORD code = freerdp_error_info(instance); + const char* name = freerdp_get_error_info_name(code); + const char* str = freerdp_get_error_info_string(code); + const int exit_code = sdl_map_error_to_exit_code(code); + + winpr_asprintf(msg, len, "Terminate with %s due to ERROR_INFO %s [0x%08" PRIx32 "]: %s", + sdl_map_error_to_code_tag(exit_code), name, code, str); + WLog_DBG(SDL_TAG, "%s", *msg); + if (pcode) + *pcode = code; + return exit_code; +} + +/* This function is called whenever a new frame starts. + * It can be used to reset invalidated areas. */ +static BOOL sdl_begin_paint(rdpContext* context) +{ + rdpGdi* gdi = nullptr; + auto sdl = get_context(context); + + WINPR_ASSERT(sdl); + + HANDLE handles[] = { sdl->update_complete.handle(), freerdp_abort_event(context) }; + const DWORD status = WaitForMultipleObjects(ARRAYSIZE(handles), handles, FALSE, INFINITE); + switch (status) + { + case WAIT_OBJECT_0: + break; + default: + return FALSE; + } + sdl->update_complete.clear(); + + gdi = context->gdi; + WINPR_ASSERT(gdi); + WINPR_ASSERT(gdi->primary); + WINPR_ASSERT(gdi->primary->hdc); + WINPR_ASSERT(gdi->primary->hdc->hwnd); + WINPR_ASSERT(gdi->primary->hdc->hwnd->invalid); + gdi->primary->hdc->hwnd->invalid->null = TRUE; + gdi->primary->hdc->hwnd->ninvalid = 0; + + return TRUE; +} + +static BOOL sdl_redraw(SdlContext* sdl) +{ + WINPR_ASSERT(sdl); + + auto gdi = sdl->context()->gdi; + return gdi_send_suppress_output(gdi, FALSE); +} + +class SdlEventUpdateTriggerGuard +{ + private: + SdlContext* _sdl; + + public: + explicit SdlEventUpdateTriggerGuard(SdlContext* sdl) : _sdl(sdl) + { + } + ~SdlEventUpdateTriggerGuard() + { + _sdl->update_complete.set(); + } +}; + +static bool sdl_draw_to_window_rect(SdlContext* sdl, SdlWindow& window, SDL_Surface* surface, + SDL_Point offset, const SDL_Rect& srcRect) +{ + SDL_Rect dstRect = { offset.x + srcRect.x, offset.y + srcRect.y, srcRect.w, srcRect.h }; + return window.blit(surface, srcRect, dstRect); +} + +static bool sdl_draw_to_window_rect(SdlContext* sdl, SdlWindow& window, SDL_Surface* surface, + SDL_Point offset, const std::vector<SDL_Rect>& rects = {}) +{ + if (rects.empty()) + { + return sdl_draw_to_window_rect(sdl, window, surface, offset, + { 0, 0, surface->w, surface->h }); + } + for (auto& srcRect : rects) + { + if (!sdl_draw_to_window_rect(sdl, window, surface, offset, srcRect)) + return false; + } + return true; +} + +static bool sdl_draw_to_window_scaled_rect(SdlContext* sdl, SdlWindow& window, SDL_Surface* surface, + const SDL_Rect& srcRect) +{ + SDL_Rect dstRect = srcRect; + sdl_scale_coordinates(sdl, window.id(), &dstRect.x, &dstRect.y, FALSE, TRUE); + sdl_scale_coordinates(sdl, window.id(), &dstRect.w, &dstRect.h, FALSE, TRUE); + return window.blit(surface, srcRect, dstRect); +} + +static BOOL sdl_draw_to_window_scaled_rect(SdlContext* sdl, SdlWindow& window, SDL_Surface* surface, + const std::vector<SDL_Rect>& rects = {}) +{ + if (rects.empty()) + { + return sdl_draw_to_window_scaled_rect(sdl, window, surface, + { 0, 0, surface->w, surface->h }); + } + for (const auto& srcRect : rects) + { + if (!sdl_draw_to_window_scaled_rect(sdl, window, surface, srcRect)) + return FALSE; + } + return TRUE; +} + +static BOOL sdl_draw_to_window(SdlContext* sdl, SdlWindow& window, + const std::vector<SDL_Rect>& rects = {}) +{ + WINPR_ASSERT(sdl); + + auto context = sdl->context(); + auto gdi = context->gdi; + + auto size = window.rect(); + + if (!freerdp_settings_get_bool(context->settings, FreeRDP_SmartSizing)) + { + if (gdi->width < size.w) + { + window.setOffsetX((size.w - gdi->width) / 2); + } + if (gdi->height < size.h) + { + window.setOffsetY((size.h - gdi->height) / 2); + } + + auto surface = sdl->primary.get(); + if (!sdl_draw_to_window_rect(sdl, window, surface, { window.offsetX(), window.offsetY() }, + rects)) + return FALSE; + } + else + { + if (!sdl_draw_to_window_scaled_rect(sdl, window, sdl->primary.get(), rects)) + return FALSE; + } + window.updateSurface(); + return TRUE; +} + +static BOOL sdl_draw_to_window(SdlContext* sdl, std::map<Uint32, SdlWindow>& windows, + const std::vector<SDL_Rect>& rects = {}) +{ + for (auto& window : windows) + { + if (!sdl_draw_to_window(sdl, window.second, rects)) + return FALSE; + } + + return TRUE; +} + +static BOOL sdl_end_paint_process(rdpContext* context) +{ + rdpGdi* gdi = nullptr; + auto sdl = get_context(context); + + WINPR_ASSERT(context); + + SdlEventUpdateTriggerGuard guard(sdl); + + gdi = context->gdi; + WINPR_ASSERT(gdi); + WINPR_ASSERT(gdi->primary); + WINPR_ASSERT(gdi->primary->hdc); + WINPR_ASSERT(gdi->primary->hdc->hwnd); + WINPR_ASSERT(gdi->primary->hdc->hwnd->invalid); + if (gdi->suppressOutput || gdi->primary->hdc->hwnd->invalid->null) + return TRUE; + + const INT32 ninvalid = gdi->primary->hdc->hwnd->ninvalid; + const GDI_RGN* cinvalid = gdi->primary->hdc->hwnd->cinvalid; + + if (ninvalid < 1) + return TRUE; + + std::vector<SDL_Rect> rects; + for (INT32 x = 0; x < ninvalid; x++) + { + auto& rgn = cinvalid[x]; + rects.push_back({ rgn.x, rgn.y, rgn.w, rgn.h }); + } + + return sdl_draw_to_window(sdl, sdl->windows, rects); +} + +/* This function is called when the library completed composing a new + * frame. Read out the changed areas and blit them to your output device. + * The image buffer will have the format specified by gdi_init + */ +static BOOL sdl_end_paint(rdpContext* context) +{ + auto sdl = get_context(context); + WINPR_ASSERT(sdl); + + std::lock_guard<CriticalSection> lock(sdl->critical); + const BOOL rc = sdl_push_user_event(SDL_USEREVENT_UPDATE, context); + + return rc; +} + +static void sdl_destroy_primary(SdlContext* sdl) +{ + if (!sdl) + return; + sdl->primary.reset(); + sdl->primary_format.reset(); +} + +/* Create a SDL surface from the GDI buffer */ +static BOOL sdl_create_primary(SdlContext* sdl) +{ + rdpGdi* gdi = nullptr; + + WINPR_ASSERT(sdl); + + gdi = sdl->context()->gdi; + WINPR_ASSERT(gdi); + + sdl_destroy_primary(sdl); + sdl->primary = SDLSurfacePtr( + SDL_CreateRGBSurfaceWithFormatFrom(gdi->primary_buffer, static_cast<int>(gdi->width), + static_cast<int>(gdi->height), + static_cast<int>(FreeRDPGetBitsPerPixel(gdi->dstFormat)), + static_cast<int>(gdi->stride), sdl->sdl_pixel_format), + SDL_FreeSurface); + sdl->primary_format = SDLPixelFormatPtr(SDL_AllocFormat(sdl->sdl_pixel_format), SDL_FreeFormat); + + if (!sdl->primary || !sdl->primary_format) + return FALSE; + + SDL_SetSurfaceBlendMode(sdl->primary.get(), SDL_BLENDMODE_NONE); + SDL_FillRect(sdl->primary.get(), nullptr, + SDL_MapRGBA(sdl->primary_format.get(), 0, 0, 0, 0xff)); + + return TRUE; +} + +static BOOL sdl_desktop_resize(rdpContext* context) +{ + rdpGdi* gdi = nullptr; + rdpSettings* settings = nullptr; + auto sdl = get_context(context); + + WINPR_ASSERT(sdl); + WINPR_ASSERT(context); + + settings = context->settings; + WINPR_ASSERT(settings); + + std::lock_guard<CriticalSection> lock(sdl->critical); + gdi = context->gdi; + if (!gdi_resize(gdi, freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth), + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight))) + return FALSE; + return sdl_create_primary(sdl); +} + +/* This function is called to output a System BEEP */ +static BOOL sdl_play_sound(rdpContext* context, const PLAY_SOUND_UPDATE* play_sound) +{ + /* TODO: Implement */ + WINPR_UNUSED(context); + WINPR_UNUSED(play_sound); + return TRUE; +} + +static BOOL sdl_wait_for_init(SdlContext* sdl) +{ + WINPR_ASSERT(sdl); + sdl->initialize.set(); + + HANDLE handles[] = { sdl->initialized.handle(), freerdp_abort_event(sdl->context()) }; + + const DWORD rc = WaitForMultipleObjects(ARRAYSIZE(handles), handles, FALSE, INFINITE); + switch (rc) + { + case WAIT_OBJECT_0: + return TRUE; + default: + return FALSE; + } +} + +/* Called before a connection is established. + * Set all configuration options to support and load channels here. */ +static BOOL sdl_pre_connect(freerdp* instance) +{ + WINPR_ASSERT(instance); + WINPR_ASSERT(instance->context); + + auto sdl = get_context(instance->context); + + auto settings = instance->context->settings; + WINPR_ASSERT(settings); + + /* Optional OS identifier sent to server */ + if (!freerdp_settings_set_uint32(settings, FreeRDP_OsMajorType, OSMAJORTYPE_UNIX)) + return FALSE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_OsMinorType, OSMINORTYPE_NATIVE_SDL)) + return FALSE; + /* OrderSupport is initialized at this point. + * Only override it if you plan to implement custom order + * callbacks or deactiveate certain features. */ + /* Register the channel listeners. + * They are required to set up / tear down channels if they are loaded. */ + PubSub_SubscribeChannelConnected(instance->context->pubSub, sdl_OnChannelConnectedEventHandler); + PubSub_SubscribeChannelDisconnected(instance->context->pubSub, + sdl_OnChannelDisconnectedEventHandler); + + if (!freerdp_settings_get_bool(settings, FreeRDP_AuthenticationOnly)) + { + UINT32 maxWidth = 0; + UINT32 maxHeight = 0; + + if (!sdl_wait_for_init(sdl)) + return FALSE; + + std::lock_guard<CriticalSection> lock(sdl->critical); + if (!freerdp_settings_get_bool(settings, FreeRDP_UseCommonStdioCallbacks)) + sdl->connection_dialog = std::make_unique<SDLConnectionDialog>(instance->context); + if (sdl->connection_dialog) + { + sdl->connection_dialog->setTitle("Connecting to '%s'", + freerdp_settings_get_server_name(settings)); + sdl->connection_dialog->showInfo( + "The connection is being established\n\nPlease wait..."); + } + if (!sdl_detect_monitors(sdl, &maxWidth, &maxHeight)) + return FALSE; + + if ((maxWidth != 0) && (maxHeight != 0) && + !freerdp_settings_get_bool(settings, FreeRDP_SmartSizing)) + { + WLog_Print(sdl->log, WLOG_INFO, "Update size to %ux%u", maxWidth, maxHeight); + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, maxWidth)) + return FALSE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, maxHeight)) + return FALSE; + } + } + else + { + /* Check +auth-only has a username and password. */ + if (!freerdp_settings_get_string(settings, FreeRDP_Password)) + { + WLog_Print(sdl->log, WLOG_INFO, "auth-only, but no password set. Please provide one."); + return FALSE; + } + + if (!freerdp_settings_set_bool(settings, FreeRDP_DeactivateClientDecoding, TRUE)) + return FALSE; + + WLog_Print(sdl->log, WLOG_INFO, "Authentication only. Don't connect SDL."); + } + + /* TODO: Any code your client requires */ + return TRUE; +} + +static const char* sdl_window_get_title(rdpSettings* settings) +{ + const char* windowTitle = nullptr; + UINT32 port = 0; + BOOL addPort = 0; + const char* name = nullptr; + const char* prefix = "FreeRDP:"; + + if (!settings) + return nullptr; + + windowTitle = freerdp_settings_get_string(settings, FreeRDP_WindowTitle); + if (windowTitle) + return windowTitle; + + name = freerdp_settings_get_server_name(settings); + port = freerdp_settings_get_uint32(settings, FreeRDP_ServerPort); + + addPort = (port != 3389); + + char buffer[MAX_PATH + 64] = { 0 }; + + if (!addPort) + sprintf_s(buffer, sizeof(buffer), "%s %s", prefix, name); + else + sprintf_s(buffer, sizeof(buffer), "%s %s:%" PRIu32, prefix, name, port); + + if (!freerdp_settings_set_string(settings, FreeRDP_WindowTitle, buffer)) + return nullptr; + return freerdp_settings_get_string(settings, FreeRDP_WindowTitle); +} + +static void sdl_term_handler(int signum, const char* signame, void* context) +{ + sdl_push_quit(); +} + +static void sdl_cleanup_sdl(SdlContext* sdl) +{ + if (!sdl) + return; + + std::lock_guard<CriticalSection> lock(sdl->critical); + sdl->windows.clear(); + sdl->connection_dialog.reset(); + + sdl_destroy_primary(sdl); + + freerdp_del_signal_cleanup_handler(sdl->context(), sdl_term_handler); + TTF_Quit(); + SDL_Quit(); +} + +static BOOL sdl_create_windows(SdlContext* sdl) +{ + WINPR_ASSERT(sdl); + + auto settings = sdl->context()->settings; + auto title = sdl_window_get_title(settings); + BOOL rc = FALSE; + + UINT32 windowCount = freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount); + + for (UINT32 x = 0; x < windowCount; x++) + { + auto monitor = static_cast<rdpMonitor*>( + freerdp_settings_get_pointer_array_writable(settings, FreeRDP_MonitorDefArray, x)); + + Uint32 w = monitor->width; + Uint32 h = monitor->height; + if (!(freerdp_settings_get_bool(settings, FreeRDP_UseMultimon) || + freerdp_settings_get_bool(settings, FreeRDP_Fullscreen))) + { + w = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth); + h = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight); + } + + Uint32 flags = SDL_WINDOW_SHOWN; + Uint32 startupX = SDL_WINDOWPOS_CENTERED_DISPLAY(x); + Uint32 startupY = SDL_WINDOWPOS_CENTERED_DISPLAY(x); + + if (monitor->attributes.desktopScaleFactor > 100) + { +#if SDL_VERSION_ATLEAST(2, 0, 1) + flags |= SDL_WINDOW_ALLOW_HIGHDPI; +#endif + } + + if (freerdp_settings_get_bool(settings, FreeRDP_Fullscreen) && + !freerdp_settings_get_bool(settings, FreeRDP_UseMultimon)) + { + flags |= SDL_WINDOW_FULLSCREEN; + } + + if (freerdp_settings_get_bool(settings, FreeRDP_UseMultimon)) + { + flags |= SDL_WINDOW_BORDERLESS; + } + + if (!freerdp_settings_get_bool(settings, FreeRDP_Decorations)) + flags |= SDL_WINDOW_BORDERLESS; + + SdlWindow window{ title, + static_cast<int>(startupX), + static_cast<int>(startupY), + static_cast<int>(w), + static_cast<int>(h), + flags }; + if (!window.window()) + goto fail; + + if (freerdp_settings_get_bool(settings, FreeRDP_UseMultimon)) + { + auto r = window.rect(); + window.setOffsetX(0 - r.x); + window.setOffsetY(0 - r.y); + } + + sdl->windows.insert({ window.id(), std::move(window) }); + } + + rc = TRUE; +fail: + + sdl->windows_created.set(); + return rc; +} + +static BOOL sdl_wait_create_windows(SdlContext* sdl) +{ + std::lock_guard<CriticalSection> lock(sdl->critical); + sdl->windows_created.clear(); + if (!sdl_push_user_event(SDL_USEREVENT_CREATE_WINDOWS, sdl)) + return FALSE; + + HANDLE handles[] = { sdl->initialized.handle(), freerdp_abort_event(sdl->context()) }; + + const DWORD rc = WaitForMultipleObjects(ARRAYSIZE(handles), handles, FALSE, INFINITE); + switch (rc) + { + case WAIT_OBJECT_0: + return TRUE; + default: + return FALSE; + } +} + +static bool shall_abort(SdlContext* sdl) +{ + std::lock_guard<CriticalSection> lock(sdl->critical); + if (freerdp_shall_disconnect_context(sdl->context())) + { + if (!sdl->connection_dialog) + return true; + return !sdl->connection_dialog->running(); + } + return false; +} + +static int sdl_run(SdlContext* sdl) +{ + int rc = -1; + WINPR_ASSERT(sdl); + + HANDLE handles[] = { sdl->initialize.handle(), freerdp_abort_event(sdl->context()) }; + const DWORD status = WaitForMultipleObjects(ARRAYSIZE(handles), handles, FALSE, INFINITE); + switch (status) + { + case WAIT_OBJECT_0: + break; + default: + return -1; + } + + SDL_Init(SDL_INIT_VIDEO); + TTF_Init(); +#if SDL_VERSION_ATLEAST(2, 0, 16) + SDL_SetHint(SDL_HINT_ALLOW_ALT_TAB_WHILE_GRABBED, "0"); +#endif +#if SDL_VERSION_ATLEAST(2, 0, 8) + SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0"); +#endif + + freerdp_add_signal_cleanup_handler(sdl->context(), sdl_term_handler); + + sdl->initialized.set(); + + while (!shall_abort(sdl)) + { + SDL_Event windowEvent = { 0 }; + while (!shall_abort(sdl) && SDL_WaitEventTimeout(nullptr, 1000)) + { + /* Only poll standard SDL events and SDL_USEREVENTS meant to create dialogs. + * do not process the dialog return value events here. + */ + const int prc = SDL_PeepEvents(&windowEvent, 1, SDL_GETEVENT, SDL_FIRSTEVENT, + SDL_USEREVENT_RETRY_DIALOG); + if (prc < 0) + { + if (sdl_log_error(prc, sdl->log, "SDL_PeepEvents")) + continue; + } + +#if defined(WITH_DEBUG_SDL_EVENTS) + SDL_Log("got event %s [0x%08" PRIx32 "]", sdl_event_type_str(windowEvent.type), + windowEvent.type); +#endif + std::lock_guard<CriticalSection> lock(sdl->critical); + /* The session might have been disconnected while we were waiting for a new SDL event. + * In that case ignore the SDL event and terminate. */ + if (freerdp_shall_disconnect_context(sdl->context())) + continue; + + if (sdl->connection_dialog) + { + if (sdl->connection_dialog->handle(windowEvent)) + { + continue; + } + } + + switch (windowEvent.type) + { + case SDL_QUIT: + freerdp_abort_connect_context(sdl->context()); + break; + case SDL_KEYDOWN: + case SDL_KEYUP: + { + const SDL_KeyboardEvent* ev = &windowEvent.key; + sdl->input.keyboard_handle_event(ev); + } + break; + case SDL_KEYMAPCHANGED: + { + } + break; // TODO: Switch keyboard layout + case SDL_MOUSEMOTION: + { + const SDL_MouseMotionEvent* ev = &windowEvent.motion; + sdl_handle_mouse_motion(sdl, ev); + } + break; + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + { + const SDL_MouseButtonEvent* ev = &windowEvent.button; + sdl_handle_mouse_button(sdl, ev); + } + break; + case SDL_MOUSEWHEEL: + { + const SDL_MouseWheelEvent* ev = &windowEvent.wheel; + sdl_handle_mouse_wheel(sdl, ev); + } + break; + case SDL_FINGERDOWN: + { + const SDL_TouchFingerEvent* ev = &windowEvent.tfinger; + sdl_handle_touch_down(sdl, ev); + } + break; + case SDL_FINGERUP: + { + const SDL_TouchFingerEvent* ev = &windowEvent.tfinger; + sdl_handle_touch_up(sdl, ev); + } + break; + case SDL_FINGERMOTION: + { + const SDL_TouchFingerEvent* ev = &windowEvent.tfinger; + sdl_handle_touch_motion(sdl, ev); + } + break; +#if SDL_VERSION_ATLEAST(2, 0, 10) + case SDL_DISPLAYEVENT: + { + const SDL_DisplayEvent* ev = &windowEvent.display; + sdl->disp.handle_display_event(ev); + } + break; +#endif + case SDL_WINDOWEVENT: + { + const SDL_WindowEvent* ev = &windowEvent.window; + sdl->disp.handle_window_event(ev); + + switch (ev->event) + { + case SDL_WINDOWEVENT_RESIZED: + case SDL_WINDOWEVENT_SIZE_CHANGED: + { + auto window = sdl->windows.find(ev->windowID); + if (window != sdl->windows.end()) + { + window->second.fill(); + window->second.updateSurface(); + } + } + break; + case SDL_WINDOWEVENT_MOVED: + { + auto window = sdl->windows.find(ev->windowID); + if (window != sdl->windows.end()) + { + auto r = window->second.rect(); + auto id = window->second.id(); + WLog_DBG(SDL_TAG, "%lu: %dx%d-%dx%d", id, r.x, r.y, r.w, r.h); + } + } + break; + default: + break; + } + } + break; + + case SDL_RENDER_TARGETS_RESET: + sdl_redraw(sdl); + break; + case SDL_RENDER_DEVICE_RESET: + sdl_redraw(sdl); + break; + case SDL_APP_WILLENTERFOREGROUND: + sdl_redraw(sdl); + break; + case SDL_USEREVENT_CERT_DIALOG: + { + auto title = static_cast<const char*>(windowEvent.user.data1); + auto msg = static_cast<const char*>(windowEvent.user.data2); + sdl_cert_dialog_show(title, msg); + } + break; + case SDL_USEREVENT_SHOW_DIALOG: + { + auto title = static_cast<const char*>(windowEvent.user.data1); + auto msg = static_cast<const char*>(windowEvent.user.data2); + sdl_message_dialog_show(title, msg, windowEvent.user.code); + } + break; + case SDL_USEREVENT_SCARD_DIALOG: + { + auto title = static_cast<const char*>(windowEvent.user.data1); + auto msg = static_cast<const char**>(windowEvent.user.data2); + sdl_scard_dialog_show(title, windowEvent.user.code, msg); + } + break; + case SDL_USEREVENT_AUTH_DIALOG: + sdl_auth_dialog_show( + reinterpret_cast<const SDL_UserAuthArg*>(windowEvent.padding)); + break; + case SDL_USEREVENT_UPDATE: + { + auto context = static_cast<rdpContext*>(windowEvent.user.data1); + sdl_end_paint_process(context); + } + break; + case SDL_USEREVENT_CREATE_WINDOWS: + { + auto ctx = static_cast<SdlContext*>(windowEvent.user.data1); + sdl_create_windows(ctx); + } + break; + case SDL_USEREVENT_WINDOW_RESIZEABLE: + { + auto window = static_cast<SdlWindow*>(windowEvent.user.data1); + const SDL_bool use = windowEvent.user.code != 0 ? SDL_TRUE : SDL_FALSE; + if (window) + window->resizeable(use); + } + break; + case SDL_USEREVENT_WINDOW_FULLSCREEN: + { + auto window = static_cast<SdlWindow*>(windowEvent.user.data1); + const SDL_bool enter = windowEvent.user.code != 0 ? SDL_TRUE : SDL_FALSE; + if (window) + window->fullscreen(enter); + } + break; + case SDL_USEREVENT_POINTER_NULL: + SDL_ShowCursor(SDL_DISABLE); + break; + case SDL_USEREVENT_POINTER_DEFAULT: + { + SDL_Cursor* def = SDL_GetDefaultCursor(); + SDL_SetCursor(def); + SDL_ShowCursor(SDL_ENABLE); + } + break; + case SDL_USEREVENT_POINTER_POSITION: + { + const auto x = + static_cast<INT32>(reinterpret_cast<uintptr_t>(windowEvent.user.data1)); + const auto y = + static_cast<INT32>(reinterpret_cast<uintptr_t>(windowEvent.user.data2)); + + SDL_Window* window = SDL_GetMouseFocus(); + if (window) + { + const Uint32 id = SDL_GetWindowID(window); + + INT32 sx = x; + INT32 sy = y; + if (sdl_scale_coordinates(sdl, id, &sx, &sy, FALSE, FALSE)) + SDL_WarpMouseInWindow(window, sx, sy); + } + } + break; + case SDL_USEREVENT_POINTER_SET: + sdl_Pointer_Set_Process(&windowEvent.user); + break; + case SDL_USEREVENT_QUIT: + default: + break; + } + } + } + + rc = 1; + + sdl_cleanup_sdl(sdl); + return rc; +} + +/* Called after a RDP connection was successfully established. + * Settings might have changed during negociation of client / server feature + * support. + * + * Set up local framebuffers and paing callbacks. + * If required, register pointer callbacks to change the local mouse cursor + * when hovering over the RDP window + */ +static BOOL sdl_post_connect(freerdp* instance) +{ + WINPR_ASSERT(instance); + + auto context = instance->context; + WINPR_ASSERT(context); + + auto sdl = get_context(context); + + // Retry was successful, discard dialog + { + std::lock_guard<CriticalSection> lock(sdl->critical); + if (sdl->connection_dialog) + sdl->connection_dialog->hide(); + } + + if (freerdp_settings_get_bool(context->settings, FreeRDP_AuthenticationOnly)) + { + /* Check +auth-only has a username and password. */ + if (!freerdp_settings_get_string(context->settings, FreeRDP_Password)) + { + WLog_Print(sdl->log, WLOG_INFO, "auth-only, but no password set. Please provide one."); + return FALSE; + } + + WLog_Print(sdl->log, WLOG_INFO, "Authentication only. Don't connect to X."); + return TRUE; + } + + if (!sdl_wait_create_windows(sdl)) + return FALSE; + + sdl->sdl_pixel_format = SDL_PIXELFORMAT_BGRA32; + if (!gdi_init(instance, PIXEL_FORMAT_BGRA32)) + return FALSE; + + if (!sdl_create_primary(sdl)) + return FALSE; + + if (!sdl_register_pointer(instance->context->graphics)) + return FALSE; + + WINPR_ASSERT(context->update); + + context->update->BeginPaint = sdl_begin_paint; + context->update->EndPaint = sdl_end_paint; + context->update->PlaySound = sdl_play_sound; + context->update->DesktopResize = sdl_desktop_resize; + context->update->SetKeyboardIndicators = sdlInput::keyboard_set_indicators; + context->update->SetKeyboardImeStatus = sdlInput::keyboard_set_ime_status; + + sdl->update_resizeable(FALSE); + sdl->update_fullscreen(freerdp_settings_get_bool(context->settings, FreeRDP_Fullscreen) || + freerdp_settings_get_bool(context->settings, FreeRDP_UseMultimon)); + return TRUE; +} + +/* This function is called whether a session ends by failure or success. + * Clean up everything allocated by pre_connect and post_connect. + */ +static void sdl_post_disconnect(freerdp* instance) +{ + if (!instance) + return; + + if (!instance->context) + return; + + PubSub_UnsubscribeChannelConnected(instance->context->pubSub, + sdl_OnChannelConnectedEventHandler); + PubSub_UnsubscribeChannelDisconnected(instance->context->pubSub, + sdl_OnChannelDisconnectedEventHandler); + gdi_free(instance); +} + +static void sdl_post_final_disconnect(freerdp* instance) +{ + if (!instance) + return; + + if (!instance->context) + return; + + auto context = get_context(instance->context); +} + +/* RDP main loop. + * Connects RDP, loops while running and handles event and dispatch, cleans up + * after the connection ends. */ +static DWORD WINAPI sdl_client_thread_proc(SdlContext* sdl) +{ + DWORD nCount = 0; + DWORD status = 0; + int exit_code = SDL_EXIT_SUCCESS; + char* error_msg = nullptr; + size_t error_msg_len = 0; + + HANDLE handles[MAXIMUM_WAIT_OBJECTS] = {}; + + WINPR_ASSERT(sdl); + + auto instance = sdl->context()->instance; + WINPR_ASSERT(instance); + + BOOL rc = freerdp_connect(instance); + + rdpContext* context = sdl->context(); + rdpSettings* settings = context->settings; + WINPR_ASSERT(settings); + + if (!rc) + { + UINT32 error = freerdp_get_last_error(context); + exit_code = sdl_map_error_to_exit_code(error); + } + + if (freerdp_settings_get_bool(settings, FreeRDP_AuthenticationOnly)) + { + DWORD code = freerdp_get_last_error(context); + freerdp_abort_connect_context(context); + WLog_Print(sdl->log, WLOG_ERROR, "Authentication only, %s [0x%08" PRIx32 "] %s", + freerdp_get_last_error_name(code), code, freerdp_get_last_error_string(code)); + goto terminate; + } + + if (!rc) + { + DWORD code = freerdp_error_info(instance); + if (exit_code == SDL_EXIT_SUCCESS) + exit_code = error_info_to_error(instance, &code, &error_msg, &error_msg_len); + + auto last = freerdp_get_last_error(context); + if (!error_msg) + { + winpr_asprintf(&error_msg, &error_msg_len, "%s [0x%08" PRIx32 "]\n%s", + freerdp_get_last_error_name(last), last, + freerdp_get_last_error_string(last)); + } + + if (last == FREERDP_ERROR_AUTHENTICATION_FAILED) + exit_code = SDL_EXIT_AUTH_FAILURE; + else if (code == ERRINFO_SUCCESS) + exit_code = SDL_EXIT_CONN_FAILED; + + goto terminate; + } + + while (!freerdp_shall_disconnect_context(context)) + { + /* + * win8 and server 2k12 seem to have some timing issue/race condition + * when a initial sync request is send to sync the keyboard indicators + * sending the sync event twice fixed this problem + */ + if (freerdp_focus_required(instance)) + { + auto ctx = get_context(context); + WINPR_ASSERT(ctx); + if (!ctx->input.keyboard_focus_in()) + break; + if (!ctx->input.keyboard_focus_in()) + break; + } + + nCount = freerdp_get_event_handles(context, handles, ARRAYSIZE(handles)); + + if (nCount == 0) + { + WLog_Print(sdl->log, WLOG_ERROR, "freerdp_get_event_handles failed"); + break; + } + + status = WaitForMultipleObjects(nCount, handles, FALSE, 100); + + if (status == WAIT_FAILED) + { + if (client_auto_reconnect(instance)) + continue; + else + { + /* + * Indicate an unsuccessful connection attempt if reconnect + * did not succeed and no other error was specified. + */ + if (freerdp_error_info(instance) == 0) + exit_code = SDL_EXIT_CONN_FAILED; + } + + if (freerdp_get_last_error(context) == FREERDP_ERROR_SUCCESS) + WLog_Print(sdl->log, WLOG_ERROR, "WaitForMultipleObjects failed with %" PRIu32 "", + status); + break; + } + + if (!freerdp_check_event_handles(context)) + { + if (freerdp_get_last_error(context) == FREERDP_ERROR_SUCCESS) + WLog_Print(sdl->log, WLOG_ERROR, "Failed to check FreeRDP event handles"); + + break; + } + } + + if (exit_code == SDL_EXIT_SUCCESS) + { + DWORD code = 0; + exit_code = error_info_to_error(instance, &code, &error_msg, &error_msg_len); + + if ((code == ERRINFO_LOGOFF_BY_USER) && + (freerdp_get_disconnect_ultimatum(context) == Disconnect_Ultimatum_user_requested)) + { + const char* msg = "Error info says user did not initiate but disconnect ultimatum says " + "they did; treat this as a user logoff"; + free(error_msg); + error_msg = nullptr; + error_msg_len = 0; + winpr_asprintf(&error_msg, &error_msg_len, "%s", msg); + + /* This situation might be limited to Windows XP. */ + WLog_Print(sdl->log, WLOG_INFO, "%s", msg); + exit_code = SDL_EXIT_LOGOFF; + } + } + + freerdp_disconnect(instance); + +terminate: + if (freerdp_settings_get_bool(settings, FreeRDP_AuthenticationOnly)) + WLog_Print(sdl->log, WLOG_INFO, "Authentication only, exit status %s [%" PRId32 "]", + sdl_map_to_code_tag(exit_code), exit_code); + else + { + switch (exit_code) + { + case SDL_EXIT_SUCCESS: + case SDL_EXIT_DISCONNECT: + case SDL_EXIT_LOGOFF: + case SDL_EXIT_DISCONNECT_BY_USER: + break; + default: + { + std::lock_guard<CriticalSection> lock(sdl->critical); + if (sdl->connection_dialog) + sdl->connection_dialog->showError(error_msg); + } + break; + } + } + free(error_msg); + sdl->exit_code = exit_code; + sdl_push_user_event(SDL_USEREVENT_QUIT); +#if SDL_VERSION_ATLEAST(2, 0, 16) + SDL_TLSCleanup(); +#endif + return 0; +} + +/* Optional global initializer. + * Here we just register a signal handler to print out stack traces + * if available. */ +static BOOL sdl_client_global_init(void) +{ +#if defined(_WIN32) + WSADATA wsaData = { 0 }; + const DWORD wVersionRequested = MAKEWORD(1, 1); + const int rc = WSAStartup(wVersionRequested, &wsaData); + if (rc != 0) + { + WLog_ERR(SDL_TAG, "WSAStartup failed with %s [%d]", gai_strerrorA(rc), rc); + return FALSE; + } +#endif + + if (freerdp_handle_signals() != 0) + return FALSE; + + return TRUE; +} + +/* Optional global tear down */ +static void sdl_client_global_uninit(void) +{ +#if defined(_WIN32) + WSACleanup(); +#endif +} + +static BOOL sdl_client_new(freerdp* instance, rdpContext* context) +{ + auto sdl = reinterpret_cast<sdl_rdp_context*>(context); + + if (!instance || !context) + return FALSE; + + sdl->sdl = new SdlContext(context); + if (!sdl->sdl) + return FALSE; + + instance->PreConnect = sdl_pre_connect; + instance->PostConnect = sdl_post_connect; + instance->PostDisconnect = sdl_post_disconnect; + instance->PostFinalDisconnect = sdl_post_final_disconnect; + instance->AuthenticateEx = sdl_authenticate_ex; + instance->VerifyCertificateEx = sdl_verify_certificate_ex; + instance->VerifyChangedCertificateEx = sdl_verify_changed_certificate_ex; + instance->LogonErrorInfo = sdl_logon_error_info; + instance->PresentGatewayMessage = sdl_present_gateway_message; + instance->ChooseSmartcard = sdl_choose_smartcard; + instance->RetryDialog = sdl_retry_dialog; + +#ifdef WITH_WEBVIEW + instance->GetAccessToken = sdl_webview_get_access_token; +#else + instance->GetAccessToken = client_cli_get_access_token; +#endif + /* TODO: Client display set up */ + + return TRUE; +} + +static void sdl_client_free(freerdp* instance, rdpContext* context) +{ + auto sdl = reinterpret_cast<sdl_rdp_context*>(context); + + if (!context) + return; + + delete sdl->sdl; +} + +static int sdl_client_start(rdpContext* context) +{ + auto sdl = get_context(context); + WINPR_ASSERT(sdl); + + sdl->thread = std::thread(sdl_client_thread_proc, sdl); + return 0; +} + +static int sdl_client_stop(rdpContext* context) +{ + auto sdl = get_context(context); + WINPR_ASSERT(sdl); + + /* We do not want to use freerdp_abort_connect_context here. + * It would change the exit code and we do not want that. */ + HANDLE event = freerdp_abort_event(context); + if (!SetEvent(event)) + return -1; + + sdl->thread.join(); + return 0; +} + +static int RdpClientEntry(RDP_CLIENT_ENTRY_POINTS* pEntryPoints) +{ + WINPR_ASSERT(pEntryPoints); + + ZeroMemory(pEntryPoints, sizeof(RDP_CLIENT_ENTRY_POINTS)); + pEntryPoints->Version = RDP_CLIENT_INTERFACE_VERSION; + pEntryPoints->Size = sizeof(RDP_CLIENT_ENTRY_POINTS_V1); + pEntryPoints->GlobalInit = sdl_client_global_init; + pEntryPoints->GlobalUninit = sdl_client_global_uninit; + pEntryPoints->ContextSize = sizeof(sdl_rdp_context); + pEntryPoints->ClientNew = sdl_client_new; + pEntryPoints->ClientFree = sdl_client_free; + pEntryPoints->ClientStart = sdl_client_start; + pEntryPoints->ClientStop = sdl_client_stop; + return 0; +} + +static void context_free(sdl_rdp_context* sdl) +{ + if (sdl) + freerdp_client_context_free(&sdl->common.context); +} + +static const char* category2str(int category) +{ + switch (category) + { + case SDL_LOG_CATEGORY_APPLICATION: + return "SDL_LOG_CATEGORY_APPLICATION"; + case SDL_LOG_CATEGORY_ERROR: + return "SDL_LOG_CATEGORY_ERROR"; + case SDL_LOG_CATEGORY_ASSERT: + return "SDL_LOG_CATEGORY_ASSERT"; + case SDL_LOG_CATEGORY_SYSTEM: + return "SDL_LOG_CATEGORY_SYSTEM"; + case SDL_LOG_CATEGORY_AUDIO: + return "SDL_LOG_CATEGORY_AUDIO"; + case SDL_LOG_CATEGORY_VIDEO: + return "SDL_LOG_CATEGORY_VIDEO"; + case SDL_LOG_CATEGORY_RENDER: + return "SDL_LOG_CATEGORY_RENDER"; + case SDL_LOG_CATEGORY_INPUT: + return "SDL_LOG_CATEGORY_INPUT"; + case SDL_LOG_CATEGORY_TEST: + return "SDL_LOG_CATEGORY_TEST"; + case SDL_LOG_CATEGORY_RESERVED1: + return "SDL_LOG_CATEGORY_RESERVED1"; + case SDL_LOG_CATEGORY_RESERVED2: + return "SDL_LOG_CATEGORY_RESERVED2"; + case SDL_LOG_CATEGORY_RESERVED3: + return "SDL_LOG_CATEGORY_RESERVED3"; + case SDL_LOG_CATEGORY_RESERVED4: + return "SDL_LOG_CATEGORY_RESERVED4"; + case SDL_LOG_CATEGORY_RESERVED5: + return "SDL_LOG_CATEGORY_RESERVED5"; + case SDL_LOG_CATEGORY_RESERVED6: + return "SDL_LOG_CATEGORY_RESERVED6"; + case SDL_LOG_CATEGORY_RESERVED7: + return "SDL_LOG_CATEGORY_RESERVED7"; + case SDL_LOG_CATEGORY_RESERVED8: + return "SDL_LOG_CATEGORY_RESERVED8"; + case SDL_LOG_CATEGORY_RESERVED9: + return "SDL_LOG_CATEGORY_RESERVED9"; + case SDL_LOG_CATEGORY_RESERVED10: + return "SDL_LOG_CATEGORY_RESERVED10"; + case SDL_LOG_CATEGORY_CUSTOM: + default: + return "SDL_LOG_CATEGORY_CUSTOM"; + } +} + +static SDL_LogPriority wloglevel2dl(DWORD level) +{ + switch (level) + { + case WLOG_TRACE: + return SDL_LOG_PRIORITY_VERBOSE; + case WLOG_DEBUG: + return SDL_LOG_PRIORITY_DEBUG; + case WLOG_INFO: + return SDL_LOG_PRIORITY_INFO; + case WLOG_WARN: + return SDL_LOG_PRIORITY_WARN; + case WLOG_ERROR: + return SDL_LOG_PRIORITY_ERROR; + case WLOG_FATAL: + return SDL_LOG_PRIORITY_CRITICAL; + case WLOG_OFF: + default: + return SDL_LOG_PRIORITY_VERBOSE; + } +} + +static DWORD sdlpriority2wlog(SDL_LogPriority priority) +{ + DWORD level = WLOG_OFF; + switch (priority) + { + case SDL_LOG_PRIORITY_VERBOSE: + level = WLOG_TRACE; + break; + case SDL_LOG_PRIORITY_DEBUG: + level = WLOG_DEBUG; + break; + case SDL_LOG_PRIORITY_INFO: + level = WLOG_INFO; + break; + case SDL_LOG_PRIORITY_WARN: + level = WLOG_WARN; + break; + case SDL_LOG_PRIORITY_ERROR: + level = WLOG_ERROR; + break; + case SDL_LOG_PRIORITY_CRITICAL: + level = WLOG_FATAL; + break; + default: + break; + } + + return level; +} + +static void SDLCALL winpr_LogOutputFunction(void* userdata, int category, SDL_LogPriority priority, + const char* message) +{ + auto sdl = static_cast<SdlContext*>(userdata); + WINPR_ASSERT(sdl); + + const DWORD level = sdlpriority2wlog(priority); + auto log = sdl->log; + if (!WLog_IsLevelActive(log, level)) + return; + + WLog_PrintMessage(log, WLOG_MESSAGE_TEXT, level, __LINE__, __FILE__, __func__, "[%s] %s", + category2str(category), message); +} + +static void print_config_file_help() +{ +#if defined(CJSON_FOUND) + std::cout << "CONFIGURATION FILE" << std::endl; + std::cout << std::endl; + std::cout << " The SDL client supports some user defined configuration options." << std::endl; + std::cout << " Settings are stored in JSON format" << std::endl; + std::cout << " The location is a per user file. Location for current user is " + << sdl_get_pref_file() << std::endl; + std::cout + << " The XDG_CONFIG_HOME environment variable can be used to override the base directory." + << std::endl; + std::cout << std::endl; + std::cout << " The following configuration options are supported:" << std::endl; + std::cout << std::endl; + std::cout << " SDL_KeyModMask" << std::endl; + std::cout << " Defines the key combination required for SDL client shortcuts." + << std::endl; + std::cout << " Default KMOD_RSHIFT" << std::endl; + std::cout << " An array of SDL_Keymod strings as defined at " + "https://wiki.libsdl.org/SDL2/SDL_Keymod" + << std::endl; + std::cout << std::endl; + std::cout << " SDL_Fullscreen" << std::endl; + std::cout << " Toggles client fullscreen state." << std::endl; + std::cout << " Default SDL_SCANCODE_RETURN." << std::endl; + std::cout << " A string as " + "defined at https://wiki.libsdl.org/SDL2/SDLScancodeLookup" + << std::endl; + std::cout << std::endl; + std::cout << " SDL_Resizeable" << std::endl; + std::cout << " Toggles local window resizeable state." << std::endl; + std::cout << " Default SDL_SCANCODE_R." << std::endl; + std::cout << " A string as " + "defined at https://wiki.libsdl.org/SDL2/SDLScancodeLookup" + << std::endl; + std::cout << std::endl; + std::cout << " SDL_Grab" << std::endl; + std::cout << " Toggles keyboard and mouse grab state." << std::endl; + std::cout << " Default SDL_SCANCODE_G." << std::endl; + std::cout << " A string as " + "defined at https://wiki.libsdl.org/SDL2/SDLScancodeLookup" + << std::endl; + std::cout << std::endl; + std::cout << " SDL_Disconnect" << std::endl; + std::cout << " Disconnects from the RDP session." << std::endl; + std::cout << " Default SDL_SCANCODE_D." << std::endl; + std::cout << " A string as defined at https://wiki.libsdl.org/SDL2/SDLScancodeLookup" + << std::endl; +#endif +} + +int main(int argc, char* argv[]) +{ + int rc = -1; + int status = 0; + RDP_CLIENT_ENTRY_POINTS clientEntryPoints = {}; + + freerdp_client_warn_experimental(argc, argv); + + RdpClientEntry(&clientEntryPoints); + std::unique_ptr<sdl_rdp_context, void (*)(sdl_rdp_context*)> sdl_rdp( + reinterpret_cast<sdl_rdp_context*>(freerdp_client_context_new(&clientEntryPoints)), + context_free); + + if (!sdl_rdp) + return -1; + auto sdl = sdl_rdp->sdl; + + auto settings = sdl->context()->settings; + WINPR_ASSERT(settings); + + status = freerdp_client_settings_parse_command_line(settings, argc, argv, FALSE); + if (status) + { + rc = freerdp_client_settings_command_line_status_print(settings, status, argc, argv); + print_config_file_help(); + if (freerdp_settings_get_bool(settings, FreeRDP_ListMonitors)) + sdl_list_monitors(sdl); + return rc; + } + + SDL_LogSetOutputFunction(winpr_LogOutputFunction, sdl); + auto level = WLog_GetLogLevel(sdl->log); + SDL_LogSetAllPriority(wloglevel2dl(level)); + + auto context = sdl->context(); + WINPR_ASSERT(context); + + if (!stream_dump_register_handlers(context, CONNECTION_STATE_MCS_CREATE_REQUEST, FALSE)) + return -1; + + if (freerdp_client_start(context) != 0) + return -1; + + rc = sdl_run(sdl); + + if (freerdp_client_stop(context) != 0) + return -1; + + rc = sdl->exit_code; + + return rc; +} + +BOOL SdlContext::update_fullscreen(BOOL enter) +{ + std::lock_guard<CriticalSection> lock(critical); + for (const auto& window : windows) + { + if (!sdl_push_user_event(SDL_USEREVENT_WINDOW_FULLSCREEN, &window.second, enter)) + return FALSE; + } + fullscreen = enter; + return TRUE; +} + +BOOL SdlContext::update_resizeable(BOOL enable) +{ + std::lock_guard<CriticalSection> lock(critical); + + const auto settings = context()->settings; + const BOOL dyn = freerdp_settings_get_bool(settings, FreeRDP_DynamicResolutionUpdate); + const BOOL smart = freerdp_settings_get_bool(settings, FreeRDP_SmartSizing); + BOOL use = (dyn && enable) || smart; + + for (const auto& window : windows) + { + if (!sdl_push_user_event(SDL_USEREVENT_WINDOW_RESIZEABLE, &window.second, use)) + return FALSE; + } + resizeable = use; + + return TRUE; +} + +SdlContext::SdlContext(rdpContext* context) + : _context(context), log(WLog_Get(SDL_TAG)), update_complete(true), disp(this), input(this), + primary(nullptr, SDL_FreeSurface), primary_format(nullptr, SDL_FreeFormat) +{ +} + +rdpContext* SdlContext::context() const +{ + return _context; +} + +rdpClientContext* SdlContext::common() const +{ + return reinterpret_cast<rdpClientContext*>(_context); +} diff --git a/client/SDL/sdl_freerdp.hpp b/client/SDL/sdl_freerdp.hpp new file mode 100644 index 0000000..79ed890 --- /dev/null +++ b/client/SDL/sdl_freerdp.hpp @@ -0,0 +1,88 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client + * + * Copyright 2022 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <memory> +#include <thread> +#include <map> + +#include <freerdp/freerdp.h> +#include <freerdp/client/rdpei.h> +#include <freerdp/client/rail.h> +#include <freerdp/client/cliprdr.h> +#include <freerdp/client/rdpgfx.h> + +#include <SDL.h> +#include <SDL_video.h> + +#include "sdl_types.hpp" +#include "sdl_disp.hpp" +#include "sdl_kbd.hpp" +#include "sdl_utils.hpp" +#include "sdl_window.hpp" +#include "dialogs/sdl_connection_dialog.hpp" + +using SDLSurfacePtr = std::unique_ptr<SDL_Surface, decltype(&SDL_FreeSurface)>; +using SDLPixelFormatPtr = std::unique_ptr<SDL_PixelFormat, decltype(&SDL_FreeFormat)>; + +class SdlContext +{ + public: + explicit SdlContext(rdpContext* context); + + private: + rdpContext* _context; + + public: + wLog* log; + + /* SDL */ + bool fullscreen = false; + bool resizeable = false; + bool grab_mouse = false; + bool grab_kbd = false; + + std::map<Uint32, SdlWindow> windows; + + CriticalSection critical; + std::thread thread; + WinPREvent initialize; + WinPREvent initialized; + WinPREvent update_complete; + WinPREvent windows_created; + int exit_code = -1; + + sdlDispContext disp; + sdlInput input; + + SDLSurfacePtr primary; + SDLPixelFormatPtr primary_format; + + Uint32 sdl_pixel_format = 0; + + std::unique_ptr<SDLConnectionDialog> connection_dialog; + + public: + BOOL update_resizeable(BOOL enable); + BOOL update_fullscreen(BOOL enter); + + [[nodiscard]] rdpContext* context() const; + [[nodiscard]] rdpClientContext* common() const; +}; diff --git a/client/SDL/sdl_kbd.cpp b/client/SDL/sdl_kbd.cpp new file mode 100644 index 0000000..4d62389 --- /dev/null +++ b/client/SDL/sdl_kbd.cpp @@ -0,0 +1,568 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP SDL keyboard helper + * + * Copyright 2022 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "sdl_kbd.hpp" +#include "sdl_disp.hpp" +#include "sdl_freerdp.hpp" +#include "sdl_utils.hpp" + +#include <map> + +#include <freerdp/scancode.h> + +#include <freerdp/log.h> +#define TAG CLIENT_TAG("SDL.kbd") + +typedef struct +{ + Uint32 sdl; + const char* sdl_name; + UINT32 rdp; + const char* rdp_name; +} scancode_entry_t; + +#define STR(x) #x +#define ENTRY(x, y) \ + { \ + x, STR(x), y, #y \ + } +static const scancode_entry_t map[] = { + ENTRY(SDL_SCANCODE_UNKNOWN, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_A, RDP_SCANCODE_KEY_A), + ENTRY(SDL_SCANCODE_B, RDP_SCANCODE_KEY_B), + ENTRY(SDL_SCANCODE_C, RDP_SCANCODE_KEY_C), + ENTRY(SDL_SCANCODE_D, RDP_SCANCODE_KEY_D), + ENTRY(SDL_SCANCODE_E, RDP_SCANCODE_KEY_E), + ENTRY(SDL_SCANCODE_F, RDP_SCANCODE_KEY_F), + ENTRY(SDL_SCANCODE_G, RDP_SCANCODE_KEY_G), + ENTRY(SDL_SCANCODE_H, RDP_SCANCODE_KEY_H), + ENTRY(SDL_SCANCODE_I, RDP_SCANCODE_KEY_I), + ENTRY(SDL_SCANCODE_J, RDP_SCANCODE_KEY_J), + ENTRY(SDL_SCANCODE_K, RDP_SCANCODE_KEY_K), + ENTRY(SDL_SCANCODE_L, RDP_SCANCODE_KEY_L), + ENTRY(SDL_SCANCODE_M, RDP_SCANCODE_KEY_M), + ENTRY(SDL_SCANCODE_N, RDP_SCANCODE_KEY_N), + ENTRY(SDL_SCANCODE_O, RDP_SCANCODE_KEY_O), + ENTRY(SDL_SCANCODE_P, RDP_SCANCODE_KEY_P), + ENTRY(SDL_SCANCODE_Q, RDP_SCANCODE_KEY_Q), + ENTRY(SDL_SCANCODE_R, RDP_SCANCODE_KEY_R), + ENTRY(SDL_SCANCODE_S, RDP_SCANCODE_KEY_S), + ENTRY(SDL_SCANCODE_T, RDP_SCANCODE_KEY_T), + ENTRY(SDL_SCANCODE_U, RDP_SCANCODE_KEY_U), + ENTRY(SDL_SCANCODE_V, RDP_SCANCODE_KEY_V), + ENTRY(SDL_SCANCODE_W, RDP_SCANCODE_KEY_W), + ENTRY(SDL_SCANCODE_X, RDP_SCANCODE_KEY_X), + ENTRY(SDL_SCANCODE_Y, RDP_SCANCODE_KEY_Y), + ENTRY(SDL_SCANCODE_Z, RDP_SCANCODE_KEY_Z), + ENTRY(SDL_SCANCODE_1, RDP_SCANCODE_KEY_1), + ENTRY(SDL_SCANCODE_2, RDP_SCANCODE_KEY_2), + ENTRY(SDL_SCANCODE_3, RDP_SCANCODE_KEY_3), + ENTRY(SDL_SCANCODE_4, RDP_SCANCODE_KEY_4), + ENTRY(SDL_SCANCODE_5, RDP_SCANCODE_KEY_5), + ENTRY(SDL_SCANCODE_6, RDP_SCANCODE_KEY_6), + ENTRY(SDL_SCANCODE_7, RDP_SCANCODE_KEY_7), + ENTRY(SDL_SCANCODE_8, RDP_SCANCODE_KEY_8), + ENTRY(SDL_SCANCODE_9, RDP_SCANCODE_KEY_9), + ENTRY(SDL_SCANCODE_0, RDP_SCANCODE_KEY_0), + ENTRY(SDL_SCANCODE_RETURN, RDP_SCANCODE_RETURN), + ENTRY(SDL_SCANCODE_ESCAPE, RDP_SCANCODE_ESCAPE), + ENTRY(SDL_SCANCODE_BACKSPACE, RDP_SCANCODE_BACKSPACE), + ENTRY(SDL_SCANCODE_TAB, RDP_SCANCODE_TAB), + ENTRY(SDL_SCANCODE_SPACE, RDP_SCANCODE_SPACE), + ENTRY(SDL_SCANCODE_MINUS, RDP_SCANCODE_OEM_MINUS), + ENTRY(SDL_SCANCODE_CAPSLOCK, RDP_SCANCODE_CAPSLOCK), + ENTRY(SDL_SCANCODE_F1, RDP_SCANCODE_F1), + ENTRY(SDL_SCANCODE_F2, RDP_SCANCODE_F2), + ENTRY(SDL_SCANCODE_F3, RDP_SCANCODE_F3), + ENTRY(SDL_SCANCODE_F4, RDP_SCANCODE_F4), + ENTRY(SDL_SCANCODE_F5, RDP_SCANCODE_F5), + ENTRY(SDL_SCANCODE_F6, RDP_SCANCODE_F6), + ENTRY(SDL_SCANCODE_F7, RDP_SCANCODE_F7), + ENTRY(SDL_SCANCODE_F8, RDP_SCANCODE_F8), + ENTRY(SDL_SCANCODE_F9, RDP_SCANCODE_F9), + ENTRY(SDL_SCANCODE_F10, RDP_SCANCODE_F10), + ENTRY(SDL_SCANCODE_F11, RDP_SCANCODE_F11), + ENTRY(SDL_SCANCODE_F12, RDP_SCANCODE_F12), + ENTRY(SDL_SCANCODE_F13, RDP_SCANCODE_F13), + ENTRY(SDL_SCANCODE_F14, RDP_SCANCODE_F14), + ENTRY(SDL_SCANCODE_F15, RDP_SCANCODE_F15), + ENTRY(SDL_SCANCODE_F16, RDP_SCANCODE_F16), + ENTRY(SDL_SCANCODE_F17, RDP_SCANCODE_F17), + ENTRY(SDL_SCANCODE_F18, RDP_SCANCODE_F18), + ENTRY(SDL_SCANCODE_F19, RDP_SCANCODE_F19), + ENTRY(SDL_SCANCODE_F20, RDP_SCANCODE_F20), + ENTRY(SDL_SCANCODE_F21, RDP_SCANCODE_F21), + ENTRY(SDL_SCANCODE_F22, RDP_SCANCODE_F22), + ENTRY(SDL_SCANCODE_F23, RDP_SCANCODE_F23), + ENTRY(SDL_SCANCODE_F24, RDP_SCANCODE_F24), + ENTRY(SDL_SCANCODE_NUMLOCKCLEAR, RDP_SCANCODE_NUMLOCK), + ENTRY(SDL_SCANCODE_KP_DIVIDE, RDP_SCANCODE_DIVIDE), + ENTRY(SDL_SCANCODE_KP_MULTIPLY, RDP_SCANCODE_MULTIPLY), + ENTRY(SDL_SCANCODE_KP_MINUS, RDP_SCANCODE_SUBTRACT), + ENTRY(SDL_SCANCODE_KP_PLUS, RDP_SCANCODE_ADD), + ENTRY(SDL_SCANCODE_KP_ENTER, RDP_SCANCODE_RETURN_KP), + ENTRY(SDL_SCANCODE_KP_1, RDP_SCANCODE_NUMPAD1), + ENTRY(SDL_SCANCODE_KP_2, RDP_SCANCODE_NUMPAD2), + ENTRY(SDL_SCANCODE_KP_3, RDP_SCANCODE_NUMPAD3), + ENTRY(SDL_SCANCODE_KP_4, RDP_SCANCODE_NUMPAD4), + ENTRY(SDL_SCANCODE_KP_5, RDP_SCANCODE_NUMPAD5), + ENTRY(SDL_SCANCODE_KP_6, RDP_SCANCODE_NUMPAD6), + ENTRY(SDL_SCANCODE_KP_7, RDP_SCANCODE_NUMPAD7), + ENTRY(SDL_SCANCODE_KP_8, RDP_SCANCODE_NUMPAD8), + ENTRY(SDL_SCANCODE_KP_9, RDP_SCANCODE_NUMPAD9), + ENTRY(SDL_SCANCODE_KP_0, RDP_SCANCODE_NUMPAD0), + ENTRY(SDL_SCANCODE_KP_PERIOD, RDP_SCANCODE_OEM_PERIOD), + ENTRY(SDL_SCANCODE_LCTRL, RDP_SCANCODE_LCONTROL), + ENTRY(SDL_SCANCODE_LSHIFT, RDP_SCANCODE_LSHIFT), + ENTRY(SDL_SCANCODE_LALT, RDP_SCANCODE_LMENU), + ENTRY(SDL_SCANCODE_LGUI, RDP_SCANCODE_LWIN), + ENTRY(SDL_SCANCODE_RCTRL, RDP_SCANCODE_RCONTROL), + ENTRY(SDL_SCANCODE_RSHIFT, RDP_SCANCODE_RSHIFT), + ENTRY(SDL_SCANCODE_RALT, RDP_SCANCODE_RMENU), + ENTRY(SDL_SCANCODE_RGUI, RDP_SCANCODE_RWIN), + ENTRY(SDL_SCANCODE_MODE, RDP_SCANCODE_APPS), + ENTRY(SDL_SCANCODE_MUTE, RDP_SCANCODE_VOLUME_MUTE), + ENTRY(SDL_SCANCODE_VOLUMEUP, RDP_SCANCODE_VOLUME_UP), + ENTRY(SDL_SCANCODE_VOLUMEDOWN, RDP_SCANCODE_VOLUME_DOWN), + ENTRY(SDL_SCANCODE_GRAVE, RDP_SCANCODE_OEM_3), + ENTRY(SDL_SCANCODE_COMMA, RDP_SCANCODE_OEM_COMMA), + ENTRY(SDL_SCANCODE_PERIOD, RDP_SCANCODE_OEM_PERIOD), + ENTRY(SDL_SCANCODE_SLASH, RDP_SCANCODE_OEM_2), + ENTRY(SDL_SCANCODE_BACKSLASH, RDP_SCANCODE_OEM_5), + ENTRY(SDL_SCANCODE_SCROLLLOCK, RDP_SCANCODE_SCROLLLOCK), + ENTRY(SDL_SCANCODE_INSERT, RDP_SCANCODE_INSERT), + ENTRY(SDL_SCANCODE_PRINTSCREEN, RDP_SCANCODE_PRINTSCREEN), + ENTRY(SDL_SCANCODE_HOME, RDP_SCANCODE_HOME), + ENTRY(SDL_SCANCODE_DELETE, RDP_SCANCODE_DELETE), + ENTRY(SDL_SCANCODE_RIGHT, RDP_SCANCODE_RIGHT), + ENTRY(SDL_SCANCODE_LEFT, RDP_SCANCODE_LEFT), + ENTRY(SDL_SCANCODE_DOWN, RDP_SCANCODE_DOWN), + ENTRY(SDL_SCANCODE_UP, RDP_SCANCODE_UP), + ENTRY(SDL_SCANCODE_SEMICOLON, RDP_SCANCODE_OEM_1), + ENTRY(SDL_SCANCODE_PAUSE, RDP_SCANCODE_PAUSE), + ENTRY(SDL_SCANCODE_PAGEUP, RDP_SCANCODE_PRIOR), + ENTRY(SDL_SCANCODE_END, RDP_SCANCODE_END), + ENTRY(SDL_SCANCODE_PAGEDOWN, RDP_SCANCODE_NEXT), + ENTRY(SDL_SCANCODE_AUDIONEXT, RDP_SCANCODE_MEDIA_NEXT_TRACK), + ENTRY(SDL_SCANCODE_AUDIOPREV, RDP_SCANCODE_MEDIA_PREV_TRACK), + ENTRY(SDL_SCANCODE_AUDIOSTOP, RDP_SCANCODE_MEDIA_STOP), + ENTRY(SDL_SCANCODE_AUDIOPLAY, RDP_SCANCODE_MEDIA_PLAY_PAUSE), + ENTRY(SDL_SCANCODE_AUDIOMUTE, RDP_SCANCODE_VOLUME_MUTE), + ENTRY(SDL_SCANCODE_MEDIASELECT, RDP_SCANCODE_LAUNCH_MEDIA_SELECT), + ENTRY(SDL_SCANCODE_MAIL, RDP_SCANCODE_LAUNCH_MAIL), + ENTRY(SDL_SCANCODE_APP1, RDP_SCANCODE_LAUNCH_APP1), + ENTRY(SDL_SCANCODE_APP2, RDP_SCANCODE_LAUNCH_APP2), + ENTRY(SDL_SCANCODE_SYSREQ, RDP_SCANCODE_SYSREQ), + ENTRY(SDL_SCANCODE_WWW, RDP_SCANCODE_BROWSER_HOME), + ENTRY(SDL_SCANCODE_LEFTBRACKET, RDP_SCANCODE_OEM_4), + ENTRY(SDL_SCANCODE_RIGHTBRACKET, RDP_SCANCODE_OEM_6), + ENTRY(SDL_SCANCODE_APOSTROPHE, RDP_SCANCODE_OEM_7), + ENTRY(SDL_SCANCODE_NONUSBACKSLASH, RDP_SCANCODE_OEM_102), + ENTRY(SDL_SCANCODE_SLEEP, RDP_SCANCODE_SLEEP), + ENTRY(SDL_SCANCODE_EQUALS, RDP_SCANCODE_OEM_PLUS), + ENTRY(SDL_SCANCODE_KP_COMMA, RDP_SCANCODE_DECIMAL), + ENTRY(SDL_SCANCODE_FIND, RDP_SCANCODE_BROWSER_SEARCH), + ENTRY(SDL_SCANCODE_RETURN2, RDP_SCANCODE_RETURN_KP), + ENTRY(SDL_SCANCODE_AC_SEARCH, RDP_SCANCODE_BROWSER_SEARCH), + ENTRY(SDL_SCANCODE_AC_HOME, RDP_SCANCODE_BROWSER_HOME), + ENTRY(SDL_SCANCODE_AC_BACK, RDP_SCANCODE_BROWSER_BACK), + ENTRY(SDL_SCANCODE_AC_FORWARD, RDP_SCANCODE_BROWSER_FORWARD), + ENTRY(SDL_SCANCODE_AC_STOP, RDP_SCANCODE_BROWSER_STOP), + +#if 1 // TODO: unmapped + ENTRY(SDL_SCANCODE_NONUSHASH, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_APPLICATION, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_POWER, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_EQUALS, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_EXECUTE, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_HELP, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_MENU, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_SELECT, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_STOP, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_AGAIN, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_UNDO, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_CUT, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_COPY, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_PASTE, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_EQUALSAS400, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_INTERNATIONAL1, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_INTERNATIONAL2, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_INTERNATIONAL3, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_INTERNATIONAL4, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_INTERNATIONAL5, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_INTERNATIONAL6, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_INTERNATIONAL7, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_INTERNATIONAL8, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_INTERNATIONAL9, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_LANG1, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_LANG2, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_LANG3, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_LANG4, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_LANG5, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_LANG6, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_LANG7, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_LANG8, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_LANG9, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_ALTERASE, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_CANCEL, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_CLEAR, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_PRIOR, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_SEPARATOR, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_OUT, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_OPER, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_CLEARAGAIN, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_CRSEL, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_EXSEL, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_00, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_000, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_THOUSANDSSEPARATOR, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_DECIMALSEPARATOR, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_CURRENCYUNIT, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_CURRENCYSUBUNIT, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_LEFTPAREN, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_RIGHTPAREN, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_LEFTBRACE, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_RIGHTBRACE, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_TAB, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_BACKSPACE, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_A, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_B, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_C, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_D, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_E, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_F, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_XOR, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_POWER, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_PERCENT, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_LESS, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_GREATER, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_AMPERSAND, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_DBLAMPERSAND, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_VERTICALBAR, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_DBLVERTICALBAR, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_COLON, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_HASH, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_SPACE, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_AT, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_EXCLAM, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_MEMSTORE, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_MEMRECALL, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_MEMCLEAR, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_MEMADD, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_MEMSUBTRACT, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_MEMMULTIPLY, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_MEMDIVIDE, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_PLUSMINUS, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_CLEAR, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_CLEARENTRY, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_BINARY, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_OCTAL, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_DECIMAL, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KP_HEXADECIMAL, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_CALCULATOR, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_COMPUTER, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_AC_REFRESH, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_AC_BOOKMARKS, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_BRIGHTNESSDOWN, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_BRIGHTNESSUP, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_DISPLAYSWITCH, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KBDILLUMTOGGLE, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KBDILLUMDOWN, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_KBDILLUMUP, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_EJECT, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_AUDIOREWIND, RDP_SCANCODE_UNKNOWN), + ENTRY(SDL_SCANCODE_AUDIOFASTFORWARD, RDP_SCANCODE_UNKNOWN) +#endif +}; + +static UINT32 sdl_get_kbd_flags(void) +{ + UINT32 flags = 0; + + SDL_Keymod mod = SDL_GetModState(); + if ((mod & KMOD_NUM) != 0) + flags |= KBD_SYNC_NUM_LOCK; + if ((mod & KMOD_CAPS) != 0) + flags |= KBD_SYNC_CAPS_LOCK; +#if SDL_VERSION_ATLEAST(2, 0, 18) + if ((mod & KMOD_SCROLL) != 0) + flags |= KBD_SYNC_SCROLL_LOCK; +#endif + + // TODO: KBD_SYNC_KANA_LOCK + + return flags; +} + +BOOL sdlInput::keyboard_sync_state() +{ + const UINT32 syncFlags = sdl_get_kbd_flags(); + return freerdp_input_send_synchronize_event(_sdl->context()->input, syncFlags); +} + +BOOL sdlInput::keyboard_focus_in() +{ + auto input = _sdl->context()->input; + WINPR_ASSERT(input); + + auto syncFlags = sdl_get_kbd_flags(); + freerdp_input_send_focus_in_event(input, syncFlags); + + /* finish with a mouse pointer position like mstsc.exe if required */ +#if 0 + if (xfc->remote_app) + return; + + if (XQueryPointer(xfc->display, xfc->window->handle, &w, &w, &d, &d, &x, &y, &state)) + { + if ((x >= 0) && (x < xfc->window->width) && (y >= 0) && (y < xfc->window->height)) + { + xf_event_adjust_coordinates(xfc, &x, &y); + freerdp_client_send_button_event(&xfc->common, FALSE, PTR_FLAGS_MOVE, x, y); + } + } +#endif + return TRUE; +} + +/* This function is called to update the keyboard indicator LED */ +BOOL sdlInput::keyboard_set_indicators(rdpContext* context, UINT16 led_flags) +{ + WINPR_UNUSED(context); + + int state = KMOD_NONE; + + if ((led_flags & KBD_SYNC_NUM_LOCK) != 0) + state |= KMOD_NUM; + if ((led_flags & KBD_SYNC_CAPS_LOCK) != 0) + state |= KMOD_CAPS; +#if SDL_VERSION_ATLEAST(2, 0, 18) + if ((led_flags & KBD_SYNC_SCROLL_LOCK) != 0) + state |= KMOD_SCROLL; +#endif + + // TODO: KBD_SYNC_KANA_LOCK + + SDL_SetModState(static_cast<SDL_Keymod>(state)); + + return TRUE; +} + +/* This function is called to set the IME state */ +BOOL sdlInput::keyboard_set_ime_status(rdpContext* context, UINT16 imeId, UINT32 imeState, + UINT32 imeConvMode) +{ + if (!context) + return FALSE; + + WLog_WARN(TAG, + "KeyboardSetImeStatus(unitId=%04" PRIx16 ", imeState=%08" PRIx32 + ", imeConvMode=%08" PRIx32 ") ignored", + imeId, imeState, imeConvMode); + return TRUE; +} + +uint32_t sdlInput::prefToMask() +{ + const std::map<std::string, SDL_Keymod> mapping = { + { "KMOD_LSHIFT", KMOD_LSHIFT }, + { "KMOD_RSHIFT", KMOD_RSHIFT }, + { "KMOD_LCTRL", KMOD_LCTRL }, + { "KMOD_RCTRL", KMOD_RCTRL }, + { "KMOD_LALT", KMOD_LALT }, + { "KMOD_RALT", KMOD_RALT }, + { "KMOD_LGUI", KMOD_LGUI }, + { "KMOD_RGUI", KMOD_RGUI }, + { "KMOD_NUM", KMOD_NUM }, + { "KMOD_CAPS", KMOD_CAPS }, + { "KMOD_MODE", KMOD_MODE }, +#if SDL_VERSION_ATLEAST(2, 0, 18) + { "KMOD_SCROLL", KMOD_SCROLL }, +#endif + { "KMOD_CTRL", KMOD_CTRL }, + { "KMOD_SHIFT", KMOD_SHIFT }, + { "KMOD_ALT", KMOD_ALT }, + { "KMOD_GUI", KMOD_GUI } + }; + uint32_t mod = KMOD_NONE; + for (const auto& val : sdl_get_pref_array("SDL_KeyModMask", { "KMOD_RSHIFT" })) + { + auto it = mapping.find(val); + if (it != mapping.end()) + { + mod |= it->second; + } + } + return mod; +} + +static const char* sdl_scancode_name(Uint32 scancode) +{ + for (const auto& cur : map) + { + if (cur.sdl == scancode) + return cur.sdl_name; + } + + return "SDL_SCANCODE_UNKNOWN"; +} + +static Uint32 sdl_scancode_val(const char* scancodeName) +{ + for (const auto& cur : map) + { + if (strcmp(cur.sdl_name, scancodeName) == 0) + return cur.sdl; + } + + return SDL_SCANCODE_UNKNOWN; +} + +static const char* sdl_rdp_scancode_name(UINT32 scancode) +{ + for (const auto& cur : map) + { + if (cur.rdp == scancode) + return cur.rdp_name; + } + + return "RDP_SCANCODE_UNKNOWN"; +} + +static UINT32 sdl_rdp_scancode_val(const char* scancodeName) +{ + for (const auto& cur : map) + { + if (strcmp(cur.rdp_name, scancodeName) == 0) + return cur.rdp; + } + + return RDP_SCANCODE_UNKNOWN; +} + +static UINT32 sdl_scancode_to_rdp(Uint32 scancode) +{ + UINT32 rdp = RDP_SCANCODE_UNKNOWN; + + for (const auto& cur : map) + { + if (cur.sdl == scancode) + { + rdp = cur.rdp; + break; + } + } + +#if defined(WITH_DEBUG_SDL_KBD_EVENTS) + auto code = static_cast<SDL_Scancode>(scancode); + WLog_DBG(TAG, "got %s [%s] -> [%s]", SDL_GetScancodeName(code), sdl_scancode_name(scancode), + sdl_rdp_scancode_name(rdp)); +#endif + return rdp; +} + +uint32_t sdlInput::prefKeyValue(const std::string& key, uint32_t fallback) +{ + auto item = sdl_get_pref_string(key); + if (item.empty()) + return fallback; + auto val = sdl_scancode_val(item.c_str()); + if (val == SDL_SCANCODE_UNKNOWN) + return fallback; + return val; +} + +BOOL sdlInput::keyboard_handle_event(const SDL_KeyboardEvent* ev) +{ + WINPR_ASSERT(ev); + const UINT32 rdp_scancode = sdl_scancode_to_rdp(ev->keysym.scancode); + const SDL_Keymod mods = SDL_GetModState(); + const auto mask = prefToMask(); + const auto valFullscreen = prefKeyValue("SDL_Fullscreen", SDL_SCANCODE_RETURN); + const auto valResizeable = prefKeyValue("SDL_Resizeable", SDL_SCANCODE_R); + const auto valGrab = prefKeyValue("SDL_Grab", SDL_SCANCODE_G); + const auto valDisconnect = prefKeyValue("SDL_Disconnect", SDL_SCANCODE_D); + + if ((mods & mask) == mask) + { + if (ev->type == SDL_KEYDOWN) + { + if (ev->keysym.scancode == valFullscreen) + { + _sdl->update_fullscreen(!_sdl->fullscreen); + return TRUE; + } + if (ev->keysym.scancode == valResizeable) + { + _sdl->update_resizeable(!_sdl->resizeable); + return TRUE; + } + + if (ev->keysym.scancode == valGrab) + { + keyboard_grab(ev->windowID, _sdl->grab_kbd ? SDL_FALSE : SDL_TRUE); + return TRUE; + } + if (ev->keysym.scancode == valDisconnect) + { + freerdp_abort_connect_context(_sdl->context()); + return TRUE; + } + } + } + return freerdp_input_send_keyboard_event_ex(_sdl->context()->input, ev->type == SDL_KEYDOWN, + ev->repeat, rdp_scancode); +} + +BOOL sdlInput::keyboard_grab(Uint32 windowID, SDL_bool enable) +{ + auto it = _sdl->windows.find(windowID); + if (it == _sdl->windows.end()) + return FALSE; + _sdl->grab_kbd = enable; + return it->second.grabKeyboard(enable); +} + +BOOL sdlInput::mouse_focus(Uint32 windowID) +{ + if (_lastWindowID != windowID) + { + _lastWindowID = windowID; + auto it = _sdl->windows.find(windowID); + if (it == _sdl->windows.end()) + return FALSE; + + it->second.raise(); + } + return TRUE; +} + +BOOL sdlInput::mouse_grab(Uint32 windowID, SDL_bool enable) +{ + auto it = _sdl->windows.find(windowID); + if (it == _sdl->windows.end()) + return FALSE; + _sdl->grab_mouse = enable; + return it->second.grabMouse(enable); +} + +sdlInput::sdlInput(SdlContext* sdl) : _sdl(sdl), _lastWindowID(UINT32_MAX) +{ + WINPR_ASSERT(_sdl); +} diff --git a/client/SDL/sdl_kbd.hpp b/client/SDL/sdl_kbd.hpp new file mode 100644 index 0000000..2a6c7fa --- /dev/null +++ b/client/SDL/sdl_kbd.hpp @@ -0,0 +1,56 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client keyboard helper + * + * Copyright 2022 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <string> + +#include <winpr/wtypes.h> +#include <freerdp/freerdp.h> +#include <SDL.h> + +#include "sdl_types.hpp" + +class sdlInput +{ + public: + explicit sdlInput(SdlContext* sdl); + ~sdlInput() = default; + + BOOL keyboard_sync_state(); + BOOL keyboard_focus_in(); + + BOOL keyboard_handle_event(const SDL_KeyboardEvent* ev); + + BOOL keyboard_grab(Uint32 windowID, SDL_bool enable); + BOOL mouse_focus(Uint32 windowID); + BOOL mouse_grab(Uint32 windowID, SDL_bool enable); + + public: + static BOOL keyboard_set_indicators(rdpContext* context, UINT16 led_flags); + static BOOL keyboard_set_ime_status(rdpContext* context, UINT16 imeId, UINT32 imeState, + UINT32 imeConvMode); + + static uint32_t prefToMask(); + static uint32_t prefKeyValue(const std::string& key, uint32_t fallback = SDL_SCANCODE_UNKNOWN); + + private: + SdlContext* _sdl; + Uint32 _lastWindowID; +}; diff --git a/client/SDL/sdl_monitor.cpp b/client/SDL/sdl_monitor.cpp new file mode 100644 index 0000000..e637b48 --- /dev/null +++ b/client/SDL/sdl_monitor.cpp @@ -0,0 +1,331 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Monitor Handling + * + * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * Copyright 2017 David Fort <contact@hardening-consulting.com> + * Copyright 2018 Kai Harms <kharms@rangee.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <cstdio> +#include <cstdlib> +#include <cstring> + +#include <SDL.h> + +#include <winpr/assert.h> +#include <winpr/crt.h> + +#include <freerdp/log.h> + +#define TAG CLIENT_TAG("sdl") + +#include "sdl_monitor.hpp" +#include "sdl_freerdp.hpp" + +typedef struct +{ + RECTANGLE_16 area; + RECTANGLE_16 workarea; + BOOL primary; +} MONITOR_INFO; + +typedef struct +{ + int nmonitors; + RECTANGLE_16 area; + RECTANGLE_16 workarea; + MONITOR_INFO* monitors; +} VIRTUAL_SCREEN; + +/* See MSDN Section on Multiple Display Monitors: http://msdn.microsoft.com/en-us/library/dd145071 + */ + +int sdl_list_monitors(SdlContext* sdl) +{ + SDL_Init(SDL_INIT_VIDEO); + const int nmonitors = SDL_GetNumVideoDisplays(); + + printf("listing %d monitors:\n", nmonitors); + for (int i = 0; i < nmonitors; i++) + { + SDL_Rect rect = {}; + const int brc = SDL_GetDisplayBounds(i, &rect); + const char* name = SDL_GetDisplayName(i); + + if (brc != 0) + continue; + printf(" %s [%d] [%s] %dx%d\t+%d+%d\n", (i == 0) ? "*" : " ", i, name, rect.w, rect.h, + rect.x, rect.y); + } + + SDL_Quit(); + return 0; +} + +static BOOL sdl_is_monitor_id_active(SdlContext* sdl, UINT32 id) +{ + const rdpSettings* settings = nullptr; + + WINPR_ASSERT(sdl); + + settings = sdl->context()->settings; + WINPR_ASSERT(settings); + + const UINT32 NumMonitorIds = freerdp_settings_get_uint32(settings, FreeRDP_NumMonitorIds); + if (!NumMonitorIds) + return TRUE; + + for (UINT32 index = 0; index < NumMonitorIds; index++) + { + auto cur = static_cast<const UINT32*>( + freerdp_settings_get_pointer_array(settings, FreeRDP_MonitorIds, index)); + if (cur && (*cur == id)) + return TRUE; + } + + return FALSE; +} + +static BOOL sdl_apply_max_size(SdlContext* sdl, UINT32* pMaxWidth, UINT32* pMaxHeight) +{ + WINPR_ASSERT(sdl); + WINPR_ASSERT(pMaxWidth); + WINPR_ASSERT(pMaxHeight); + + auto settings = sdl->context()->settings; + WINPR_ASSERT(settings); + + *pMaxWidth = 0; + *pMaxHeight = 0; + + for (size_t x = 0; x < freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount); x++) + { + auto monitor = static_cast<const rdpMonitor*>( + freerdp_settings_get_pointer_array(settings, FreeRDP_MonitorDefArray, x)); + + if (freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)) + { + *pMaxWidth = monitor->width; + *pMaxHeight = monitor->height; + } + else if (freerdp_settings_get_bool(settings, FreeRDP_Workarea)) + { + SDL_Rect rect = {}; + SDL_GetDisplayUsableBounds(monitor->orig_screen, &rect); + *pMaxWidth = rect.w; + *pMaxHeight = rect.h; + } + else if (freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen) > 0) + { + SDL_Rect rect = {}; + SDL_GetDisplayUsableBounds(monitor->orig_screen, &rect); + + *pMaxWidth = rect.w; + *pMaxHeight = rect.h; + + if (freerdp_settings_get_bool(settings, FreeRDP_PercentScreenUseWidth)) + *pMaxWidth = + (rect.w * freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen)) / 100; + + if (freerdp_settings_get_bool(settings, FreeRDP_PercentScreenUseHeight)) + *pMaxHeight = + (rect.h * freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen)) / 100; + } + else if (freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth) && + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight)) + { + *pMaxWidth = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth); + *pMaxHeight = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight); + } + } + return TRUE; +} + +#if SDL_VERSION_ATLEAST(2, 0, 10) +static UINT32 sdl_orientaion_to_rdp(SDL_DisplayOrientation orientation) +{ + switch (orientation) + { + case SDL_ORIENTATION_LANDSCAPE: + return ORIENTATION_LANDSCAPE; + case SDL_ORIENTATION_LANDSCAPE_FLIPPED: + return ORIENTATION_LANDSCAPE_FLIPPED; + case SDL_ORIENTATION_PORTRAIT_FLIPPED: + return ORIENTATION_PORTRAIT_FLIPPED; + case SDL_ORIENTATION_PORTRAIT: + default: + return ORIENTATION_PORTRAIT; + } +} +#endif + +static BOOL sdl_apply_display_properties(SdlContext* sdl) +{ + WINPR_ASSERT(sdl); + + rdpSettings* settings = sdl->context()->settings; + WINPR_ASSERT(settings); + + const UINT32 numIds = freerdp_settings_get_uint32(settings, FreeRDP_NumMonitorIds); + if (!freerdp_settings_set_pointer_len(settings, FreeRDP_MonitorDefArray, nullptr, numIds)) + return FALSE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_MonitorCount, numIds)) + return FALSE; + + for (UINT32 x = 0; x < numIds; x++) + { + auto id = static_cast<const UINT32*>( + freerdp_settings_get_pointer_array(settings, FreeRDP_MonitorIds, x)); + WINPR_ASSERT(id); + + float ddpi = 1.0f; + float hdpi = 1.0f; + float vdpi = 1.0f; + SDL_Rect rect = {}; + + SDL_GetDisplayBounds(*id, &rect); + SDL_GetDisplayDPI(*id, &ddpi, &hdpi, &vdpi); + + bool highDpi = hdpi > 100; + + if (highDpi) + { + // HighDPI is problematic with SDL: We can only get native resolution by creating a + // window. Work around this by checking the supported resolutions (and keep maximum) + // Also scale the DPI + const SDL_Rect scaleRect = rect; + for (int i = 0; i < SDL_GetNumDisplayModes(*id); i++) + { + SDL_DisplayMode mode = {}; + SDL_GetDisplayMode(x, i, &mode); + + if (mode.w > rect.w) + { + rect.w = mode.w; + rect.h = mode.h; + } + else if (mode.w == rect.w) + { + if (mode.h > rect.h) + { + rect.w = mode.w; + rect.h = mode.h; + } + } + } + + const float dw = 1.0f * rect.w / scaleRect.w; + const float dh = 1.0f * rect.h / scaleRect.h; + hdpi /= dw; + vdpi /= dh; + } + +#if SDL_VERSION_ATLEAST(2, 0, 10) + const SDL_DisplayOrientation orientation = SDL_GetDisplayOrientation(*id); + const UINT32 rdp_orientation = sdl_orientaion_to_rdp(orientation); +#else + const UINT32 rdp_orientation = ORIENTATION_LANDSCAPE; +#endif + + auto monitor = static_cast<rdpMonitor*>( + freerdp_settings_get_pointer_array_writable(settings, FreeRDP_MonitorDefArray, x)); + WINPR_ASSERT(monitor); + + /* windows uses 96 dpi as 'default' and the scale factors are in percent. */ + const auto factor = ddpi / 96.0f * 100.0f; + monitor->orig_screen = x; + monitor->x = rect.x; + monitor->y = rect.y; + monitor->width = rect.w; + monitor->height = rect.h; + monitor->is_primary = x == 0; + monitor->attributes.desktopScaleFactor = factor; + monitor->attributes.deviceScaleFactor = 100; + monitor->attributes.orientation = rdp_orientation; + monitor->attributes.physicalWidth = rect.w / hdpi; + monitor->attributes.physicalHeight = rect.h / vdpi; + } + return TRUE; +} + +static BOOL sdl_detect_single_window(SdlContext* sdl, UINT32* pMaxWidth, UINT32* pMaxHeight) +{ + WINPR_ASSERT(sdl); + WINPR_ASSERT(pMaxWidth); + WINPR_ASSERT(pMaxHeight); + + rdpSettings* settings = sdl->context()->settings; + WINPR_ASSERT(settings); + + if ((!freerdp_settings_get_bool(settings, FreeRDP_UseMultimon) && + !freerdp_settings_get_bool(settings, FreeRDP_SpanMonitors)) || + (freerdp_settings_get_bool(settings, FreeRDP_Workarea) && + !freerdp_settings_get_bool(settings, FreeRDP_RemoteApplicationMode))) + { + /* If no monitors were specified on the command-line then set the current monitor as active + */ + if (freerdp_settings_get_uint32(settings, FreeRDP_NumMonitorIds) == 0) + { + const size_t id = + (sdl->windows.size() > 0) ? sdl->windows.begin()->second.displayIndex() : 0; + if (!freerdp_settings_set_pointer_len(settings, FreeRDP_MonitorIds, &id, 1)) + return FALSE; + } + else + { + + /* Always sets number of monitors from command-line to just 1. + * If the monitor is invalid then we will default back to current monitor + * later as a fallback. So, there is no need to validate command-line entry here. + */ + if (!freerdp_settings_set_uint32(settings, FreeRDP_NumMonitorIds, 1)) + return FALSE; + } + + // TODO: Fill monitor struct + if (!sdl_apply_display_properties(sdl)) + return FALSE; + return sdl_apply_max_size(sdl, pMaxWidth, pMaxHeight); + } + return TRUE; +} + +BOOL sdl_detect_monitors(SdlContext* sdl, UINT32* pMaxWidth, UINT32* pMaxHeight) +{ + WINPR_ASSERT(sdl); + WINPR_ASSERT(pMaxWidth); + WINPR_ASSERT(pMaxHeight); + + rdpSettings* settings = sdl->context()->settings; + WINPR_ASSERT(settings); + + const int numDisplays = SDL_GetNumVideoDisplays(); + if (!freerdp_settings_set_pointer_len(settings, FreeRDP_MonitorIds, nullptr, numDisplays)) + return FALSE; + + for (size_t x = 0; x < numDisplays; x++) + { + if (!freerdp_settings_set_pointer_array(settings, FreeRDP_MonitorIds, x, &x)) + return FALSE; + } + + if (!sdl_apply_display_properties(sdl)) + return FALSE; + + return sdl_detect_single_window(sdl, pMaxWidth, pMaxHeight); +} diff --git a/client/SDL/sdl_monitor.hpp b/client/SDL/sdl_monitor.hpp new file mode 100644 index 0000000..64f9f56 --- /dev/null +++ b/client/SDL/sdl_monitor.hpp @@ -0,0 +1,28 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Monitor Handling + * + * Copyright 2023 Armin Novak <anovak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <freerdp/api.h> +#include <freerdp/freerdp.h> + +#include "sdl_types.hpp" + +int sdl_list_monitors(SdlContext* sdl); +BOOL sdl_detect_monitors(SdlContext* sdl, UINT32* pWidth, UINT32* pHeight); diff --git a/client/SDL/sdl_pointer.cpp b/client/SDL/sdl_pointer.cpp new file mode 100644 index 0000000..ad8a4f3 --- /dev/null +++ b/client/SDL/sdl_pointer.cpp @@ -0,0 +1,197 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Mouse Pointer + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <freerdp/gdi/gdi.h> + +#include "sdl_pointer.hpp" +#include "sdl_freerdp.hpp" +#include "sdl_touch.hpp" +#include "sdl_utils.hpp" + +#include <SDL_mouse.h> + +#define TAG CLIENT_TAG("SDL.pointer") + +typedef struct +{ + rdpPointer pointer; + SDL_Cursor* cursor; + SDL_Surface* image; + size_t size; + void* data; +} sdlPointer; + +static BOOL sdl_Pointer_New(rdpContext* context, rdpPointer* pointer) +{ + auto ptr = reinterpret_cast<sdlPointer*>(pointer); + + WINPR_ASSERT(context); + if (!ptr) + return FALSE; + + rdpGdi* gdi = context->gdi; + WINPR_ASSERT(gdi); + + ptr->size = 4ull * pointer->width * pointer->height; + ptr->data = winpr_aligned_malloc(ptr->size, 16); + + if (!ptr->data) + return FALSE; + + auto data = static_cast<BYTE*>(ptr->data); + if (!freerdp_image_copy_from_pointer_data( + data, gdi->dstFormat, 0, 0, 0, pointer->width, pointer->height, pointer->xorMaskData, + pointer->lengthXorMask, pointer->andMaskData, pointer->lengthAndMask, pointer->xorBpp, + &context->gdi->palette)) + { + winpr_aligned_free(ptr->data); + return FALSE; + } + + return TRUE; +} + +static void sdl_Pointer_Clear(sdlPointer* ptr) +{ + WINPR_ASSERT(ptr); + SDL_FreeCursor(ptr->cursor); + SDL_FreeSurface(ptr->image); + ptr->cursor = nullptr; + ptr->image = nullptr; +} + +static void sdl_Pointer_Free(rdpContext* context, rdpPointer* pointer) +{ + auto ptr = reinterpret_cast<sdlPointer*>(pointer); + WINPR_UNUSED(context); + + if (ptr) + { + sdl_Pointer_Clear(ptr); + winpr_aligned_free(ptr->data); + ptr->data = nullptr; + } +} + +static BOOL sdl_Pointer_SetDefault(rdpContext* context) +{ + WINPR_UNUSED(context); + + return sdl_push_user_event(SDL_USEREVENT_POINTER_DEFAULT); +} + +static BOOL sdl_Pointer_Set(rdpContext* context, rdpPointer* pointer) +{ + auto sdl = get_context(context); + + return sdl_push_user_event(SDL_USEREVENT_POINTER_SET, pointer, sdl); +} + +BOOL sdl_Pointer_Set_Process(SDL_UserEvent* uptr) +{ + INT32 w = 0; + INT32 h = 0; + INT32 x = 0; + INT32 y = 0; + INT32 sw = 0; + INT32 sh = 0; + + WINPR_ASSERT(uptr); + + auto sdl = static_cast<SdlContext*>(uptr->data2); + WINPR_ASSERT(sdl); + + auto context = sdl->context(); + auto ptr = static_cast<sdlPointer*>(uptr->data1); + WINPR_ASSERT(ptr); + + rdpPointer* pointer = &ptr->pointer; + + rdpGdi* gdi = context->gdi; + WINPR_ASSERT(gdi); + + x = static_cast<INT32>(pointer->xPos); + y = static_cast<INT32>(pointer->yPos); + sw = w = static_cast<INT32>(pointer->width); + sh = h = static_cast<INT32>(pointer->height); + + SDL_Window* window = SDL_GetMouseFocus(); + if (!window) + return sdl_Pointer_SetDefault(context); + + const Uint32 id = SDL_GetWindowID(window); + + if (!sdl_scale_coordinates(sdl, id, &x, &y, FALSE, FALSE) || + !sdl_scale_coordinates(sdl, id, &sw, &sh, FALSE, FALSE)) + return FALSE; + + sdl_Pointer_Clear(ptr); + + const DWORD bpp = FreeRDPGetBitsPerPixel(gdi->dstFormat); + ptr->image = + SDL_CreateRGBSurfaceWithFormat(0, sw, sh, static_cast<int>(bpp), sdl->sdl_pixel_format); + if (!ptr->image) + return FALSE; + + SDL_LockSurface(ptr->image); + auto pixels = static_cast<BYTE*>(ptr->image->pixels); + auto data = static_cast<const BYTE*>(ptr->data); + const BOOL rc = freerdp_image_scale( + pixels, gdi->dstFormat, static_cast<UINT32>(ptr->image->pitch), 0, 0, + static_cast<UINT32>(ptr->image->w), static_cast<UINT32>(ptr->image->h), data, + gdi->dstFormat, 0, 0, 0, static_cast<UINT32>(w), static_cast<UINT32>(h)); + SDL_UnlockSurface(ptr->image); + if (!rc) + return FALSE; + + ptr->cursor = SDL_CreateColorCursor(ptr->image, x, y); + if (!ptr->cursor) + return FALSE; + + SDL_SetCursor(ptr->cursor); + SDL_ShowCursor(SDL_ENABLE); + return TRUE; +} + +static BOOL sdl_Pointer_SetNull(rdpContext* context) +{ + WINPR_UNUSED(context); + + return sdl_push_user_event(SDL_USEREVENT_POINTER_NULL); +} + +static BOOL sdl_Pointer_SetPosition(rdpContext* context, UINT32 x, UINT32 y) +{ + auto sdl = get_context(context); + WINPR_ASSERT(sdl); + + return sdl_push_user_event(SDL_USEREVENT_POINTER_POSITION, x, y); +} + +BOOL sdl_register_pointer(rdpGraphics* graphics) +{ + const rdpPointer pointer = { sizeof(sdlPointer), sdl_Pointer_New, + sdl_Pointer_Free, sdl_Pointer_Set, + sdl_Pointer_SetNull, sdl_Pointer_SetDefault, + sdl_Pointer_SetPosition, 0 }; + graphics_register_pointer(graphics, &pointer); + return TRUE; +} diff --git a/client/SDL/sdl_pointer.hpp b/client/SDL/sdl_pointer.hpp new file mode 100644 index 0000000..006e962 --- /dev/null +++ b/client/SDL/sdl_pointer.hpp @@ -0,0 +1,27 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Mouse Pointer + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <SDL.h> +#include <freerdp/graphics.h> + +BOOL sdl_register_pointer(rdpGraphics* graphics); + +BOOL sdl_Pointer_Set_Process(SDL_UserEvent* uptr); diff --git a/client/SDL/sdl_touch.cpp b/client/SDL/sdl_touch.cpp new file mode 100644 index 0000000..81fcbfb --- /dev/null +++ b/client/SDL/sdl_touch.cpp @@ -0,0 +1,285 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP SDL touch/mouse input + * + * Copyright 2022 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include "sdl_touch.hpp" +#include "sdl_freerdp.hpp" + +#include <winpr/wtypes.h> +#include <winpr/assert.h> + +#include <freerdp/freerdp.h> +#include <freerdp/gdi/gdi.h> + +#include <SDL.h> + +#define TAG CLIENT_TAG("SDL.touch") + +BOOL sdl_scale_coordinates(SdlContext* sdl, Uint32 windowId, INT32* px, INT32* py, + BOOL fromLocalToRDP, BOOL applyOffset) +{ + rdpGdi* gdi = nullptr; + double sx = 1.0; + double sy = 1.0; + + if (!sdl || !px || !py || !sdl->context()->gdi) + return FALSE; + + WINPR_ASSERT(sdl->context()->gdi); + WINPR_ASSERT(sdl->context()->settings); + + gdi = sdl->context()->gdi; + + // TODO: Make this multimonitor ready! + // TODO: Need to find the primary monitor, get the scale + // TODO: Need to find the destination monitor, get the scale + // TODO: All intermediate monitors, get the scale + + int offset_x = 0; + int offset_y = 0; + for (const auto& it : sdl->windows) + { + auto& window = it.second; + const auto id = window.id(); + if (id != windowId) + { + continue; + } + + auto size = window.rect(); + + sx = size.w / static_cast<double>(gdi->width); + sy = size.h / static_cast<double>(gdi->height); + offset_x = window.offsetX(); + offset_y = window.offsetY(); + break; + } + + if (freerdp_settings_get_bool(sdl->context()->settings, FreeRDP_SmartSizing)) + { + if (!fromLocalToRDP) + { + *px = static_cast<INT32>(*px * sx); + *py = static_cast<INT32>(*py * sy); + } + else + { + *px = static_cast<INT32>(*px / sx); + *py = static_cast<INT32>(*py / sy); + } + } + else if (applyOffset) + { + *px -= offset_x; + *py -= offset_y; + } + + return TRUE; +} + +static BOOL sdl_get_touch_scaled(SdlContext* sdl, const SDL_TouchFingerEvent* ev, INT32* px, + INT32* py, BOOL local) +{ + Uint32 windowID = 0; + + WINPR_ASSERT(sdl); + WINPR_ASSERT(ev); + WINPR_ASSERT(px); + WINPR_ASSERT(py); + +#if SDL_VERSION_ATLEAST(2, 0, 12) + SDL_Window* window = SDL_GetWindowFromID(ev->windowID); +#else + SDL_Window* window = SDL_GetMouseFocus(); +#endif + + if (!window) + return FALSE; + + windowID = SDL_GetWindowID(window); + SDL_Surface* surface = SDL_GetWindowSurface(window); + if (!surface) + return FALSE; + + // TODO: Add the offset of the surface in the global coordinates + *px = static_cast<INT32>(ev->x * static_cast<float>(surface->w)); + *py = static_cast<INT32>(ev->y * static_cast<float>(surface->h)); + return sdl_scale_coordinates(sdl, windowID, px, py, local, TRUE); +} + +static BOOL send_mouse_wheel(SdlContext* sdl, UINT16 flags, INT32 avalue) +{ + WINPR_ASSERT(sdl); + if (avalue < 0) + { + flags |= PTR_FLAGS_WHEEL_NEGATIVE; + avalue = -avalue; + } + + while (avalue > 0) + { + const UINT16 cval = (avalue > 0xFF) ? 0xFF : static_cast<UINT16>(avalue); + UINT16 cflags = flags | cval; + /* Convert negative values to 9bit twos complement */ + if (flags & PTR_FLAGS_WHEEL_NEGATIVE) + cflags = (flags & 0xFF00) | (0x100 - cval); + if (!freerdp_client_send_wheel_event(sdl->common(), cflags)) + return FALSE; + + avalue -= cval; + } + return TRUE; +} + +static UINT32 sdl_scale_pressure(const float pressure) +{ + const float val = pressure * 0x400; /* [MS-RDPEI] 2.2.3.3.1.1 RDPINPUT_TOUCH_CONTACT */ + if (val < 0.0f) + return 0; + if (val > 0x400) + return 0x400; + return static_cast<UINT32>(val); +} + +BOOL sdl_handle_touch_up(SdlContext* sdl, const SDL_TouchFingerEvent* ev) +{ + WINPR_ASSERT(sdl); + WINPR_ASSERT(ev); + + INT32 x = 0; + INT32 y = 0; + if (!sdl_get_touch_scaled(sdl, ev, &x, &y, TRUE)) + return FALSE; + return freerdp_client_handle_touch(sdl->common(), FREERDP_TOUCH_UP | FREERDP_TOUCH_HAS_PRESSURE, + static_cast<INT32>(ev->fingerId), + sdl_scale_pressure(ev->pressure), x, y); +} + +BOOL sdl_handle_touch_down(SdlContext* sdl, const SDL_TouchFingerEvent* ev) +{ + WINPR_ASSERT(sdl); + WINPR_ASSERT(ev); + + INT32 x = 0; + INT32 y = 0; + if (!sdl_get_touch_scaled(sdl, ev, &x, &y, TRUE)) + return FALSE; + return freerdp_client_handle_touch( + sdl->common(), FREERDP_TOUCH_DOWN | FREERDP_TOUCH_HAS_PRESSURE, + static_cast<INT32>(ev->fingerId), sdl_scale_pressure(ev->pressure), x, y); +} + +BOOL sdl_handle_touch_motion(SdlContext* sdl, const SDL_TouchFingerEvent* ev) +{ + WINPR_ASSERT(sdl); + WINPR_ASSERT(ev); + + INT32 x = 0; + INT32 y = 0; + if (!sdl_get_touch_scaled(sdl, ev, &x, &y, TRUE)) + return FALSE; + return freerdp_client_handle_touch( + sdl->common(), FREERDP_TOUCH_MOTION | FREERDP_TOUCH_HAS_PRESSURE, + static_cast<INT32>(ev->fingerId), sdl_scale_pressure(ev->pressure), x, y); +} + +BOOL sdl_handle_mouse_motion(SdlContext* sdl, const SDL_MouseMotionEvent* ev) +{ + WINPR_ASSERT(sdl); + WINPR_ASSERT(ev); + + sdl->input.mouse_focus(ev->windowID); + const BOOL relative = freerdp_client_use_relative_mouse_events(sdl->common()); + INT32 x = relative ? ev->xrel : ev->x; + INT32 y = relative ? ev->yrel : ev->y; + sdl_scale_coordinates(sdl, ev->windowID, &x, &y, TRUE, TRUE); + return freerdp_client_send_button_event(sdl->common(), relative, PTR_FLAGS_MOVE, x, y); +} + +BOOL sdl_handle_mouse_wheel(SdlContext* sdl, const SDL_MouseWheelEvent* ev) +{ + WINPR_ASSERT(sdl); + WINPR_ASSERT(ev); + + const BOOL flipped = (ev->direction == SDL_MOUSEWHEEL_FLIPPED); + const INT32 x = ev->x * (flipped ? -1 : 1) * 0x78; + const INT32 y = ev->y * (flipped ? -1 : 1) * 0x78; + UINT16 flags = 0; + + if (y != 0) + { + flags |= PTR_FLAGS_WHEEL; + send_mouse_wheel(sdl, flags, y); + } + + if (x != 0) + { + flags |= PTR_FLAGS_HWHEEL; + send_mouse_wheel(sdl, flags, x); + } + return TRUE; +} + +BOOL sdl_handle_mouse_button(SdlContext* sdl, const SDL_MouseButtonEvent* ev) +{ + UINT16 flags = 0; + UINT16 xflags = 0; + + WINPR_ASSERT(sdl); + WINPR_ASSERT(ev); + + if (ev->state == SDL_PRESSED) + { + flags |= PTR_FLAGS_DOWN; + xflags |= PTR_XFLAGS_DOWN; + } + + switch (ev->button) + { + case 1: + flags |= PTR_FLAGS_BUTTON1; + break; + case 2: + flags |= PTR_FLAGS_BUTTON3; + break; + case 3: + flags |= PTR_FLAGS_BUTTON2; + break; + case 4: + xflags |= PTR_XFLAGS_BUTTON1; + break; + case 5: + xflags |= PTR_XFLAGS_BUTTON2; + break; + default: + break; + } + + const BOOL relative = freerdp_client_use_relative_mouse_events(sdl->common()); + INT32 x = relative ? 0 : ev->x; + INT32 y = relative ? 0 : ev->y; + sdl_scale_coordinates(sdl, ev->windowID, &x, &y, TRUE, TRUE); + if ((flags & (~PTR_FLAGS_DOWN)) != 0) + return freerdp_client_send_button_event(sdl->common(), relative, flags, x, y); + else if ((xflags & (~PTR_XFLAGS_DOWN)) != 0) + return freerdp_client_send_extended_button_event(sdl->common(), relative, xflags, x, y); + else + return FALSE; +} diff --git a/client/SDL/sdl_touch.hpp b/client/SDL/sdl_touch.hpp new file mode 100644 index 0000000..395fddb --- /dev/null +++ b/client/SDL/sdl_touch.hpp @@ -0,0 +1,36 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP SDL touch/mouse input + * + * Copyright 2022 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <winpr/wtypes.h> + +#include <SDL.h> +#include "sdl_types.hpp" + +BOOL sdl_scale_coordinates(SdlContext* sdl, Uint32 windowId, INT32* px, INT32* py, + BOOL fromLocalToRDP, BOOL applyOffset); + +BOOL sdl_handle_mouse_motion(SdlContext* sdl, const SDL_MouseMotionEvent* ev); +BOOL sdl_handle_mouse_wheel(SdlContext* sdl, const SDL_MouseWheelEvent* ev); +BOOL sdl_handle_mouse_button(SdlContext* sdl, const SDL_MouseButtonEvent* ev); + +BOOL sdl_handle_touch_down(SdlContext* sdl, const SDL_TouchFingerEvent* ev); +BOOL sdl_handle_touch_up(SdlContext* sdl, const SDL_TouchFingerEvent* ev); +BOOL sdl_handle_touch_motion(SdlContext* sdl, const SDL_TouchFingerEvent* ev); diff --git a/client/SDL/sdl_types.hpp b/client/SDL/sdl_types.hpp new file mode 100644 index 0000000..831472c --- /dev/null +++ b/client/SDL/sdl_types.hpp @@ -0,0 +1,46 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client + * + * Copyright 2022 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <freerdp/freerdp.h> + +class SdlContext; + +typedef struct +{ + rdpClientContext common; + SdlContext* sdl; +} sdl_rdp_context; + +static inline SdlContext* get_context(void* ctx) +{ + if (!ctx) + return nullptr; + auto sdl = static_cast<sdl_rdp_context*>(ctx); + return sdl->sdl; +} + +static inline SdlContext* get_context(rdpContext* ctx) +{ + if (!ctx) + return nullptr; + auto sdl = reinterpret_cast<sdl_rdp_context*>(ctx); + return sdl->sdl; +} diff --git a/client/SDL/sdl_utils.cpp b/client/SDL/sdl_utils.cpp new file mode 100644 index 0000000..c3bd2cf --- /dev/null +++ b/client/SDL/sdl_utils.cpp @@ -0,0 +1,465 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client + * + * Copyright 2022 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <fstream> +#if __has_include(<filesystem>) +#include <filesystem> +namespace fs = std::filesystem; +#elif __has_include(<experimental/filesystem>) +#include <experimental/filesystem> +namespace fs = std::experimental::filesystem; +#else +#error Could not find system header "<filesystem>" or "<experimental/filesystem>" +#endif + +#include <cassert> +#include "sdl_utils.hpp" + +#include "sdl_freerdp.hpp" + +#include <SDL.h> + +#include <winpr/path.h> +#include <freerdp/version.h> +#if defined(CJSON_FOUND) +#include <cjson/cJSON.h> +#endif + +const char* sdl_event_type_str(Uint32 type) +{ +#define STR(x) #x +#define EV_CASE_STR(x) \ + case x: \ + return STR(x) + + switch (type) + { + EV_CASE_STR(SDL_FIRSTEVENT); + EV_CASE_STR(SDL_QUIT); + EV_CASE_STR(SDL_APP_TERMINATING); + EV_CASE_STR(SDL_APP_LOWMEMORY); + EV_CASE_STR(SDL_APP_WILLENTERBACKGROUND); + EV_CASE_STR(SDL_APP_DIDENTERBACKGROUND); + EV_CASE_STR(SDL_APP_WILLENTERFOREGROUND); + EV_CASE_STR(SDL_APP_DIDENTERFOREGROUND); +#if SDL_VERSION_ATLEAST(2, 0, 10) + EV_CASE_STR(SDL_DISPLAYEVENT); +#endif + EV_CASE_STR(SDL_WINDOWEVENT); + EV_CASE_STR(SDL_SYSWMEVENT); + EV_CASE_STR(SDL_KEYDOWN); + EV_CASE_STR(SDL_KEYUP); + EV_CASE_STR(SDL_TEXTEDITING); + EV_CASE_STR(SDL_TEXTINPUT); + EV_CASE_STR(SDL_KEYMAPCHANGED); + EV_CASE_STR(SDL_MOUSEMOTION); + EV_CASE_STR(SDL_MOUSEBUTTONDOWN); + EV_CASE_STR(SDL_MOUSEBUTTONUP); + EV_CASE_STR(SDL_MOUSEWHEEL); + EV_CASE_STR(SDL_JOYAXISMOTION); + EV_CASE_STR(SDL_JOYBALLMOTION); + EV_CASE_STR(SDL_JOYHATMOTION); + EV_CASE_STR(SDL_JOYBUTTONDOWN); + EV_CASE_STR(SDL_JOYBUTTONUP); + EV_CASE_STR(SDL_JOYDEVICEADDED); + EV_CASE_STR(SDL_JOYDEVICEREMOVED); + EV_CASE_STR(SDL_CONTROLLERAXISMOTION); + EV_CASE_STR(SDL_CONTROLLERBUTTONDOWN); + EV_CASE_STR(SDL_CONTROLLERBUTTONUP); + EV_CASE_STR(SDL_CONTROLLERDEVICEADDED); + EV_CASE_STR(SDL_CONTROLLERDEVICEREMOVED); + EV_CASE_STR(SDL_CONTROLLERDEVICEREMAPPED); +#if SDL_VERSION_ATLEAST(2, 0, 14) + EV_CASE_STR(SDL_LOCALECHANGED); + EV_CASE_STR(SDL_CONTROLLERTOUCHPADDOWN); + EV_CASE_STR(SDL_CONTROLLERTOUCHPADMOTION); + EV_CASE_STR(SDL_CONTROLLERTOUCHPADUP); + EV_CASE_STR(SDL_CONTROLLERSENSORUPDATE); +#endif + EV_CASE_STR(SDL_FINGERDOWN); + EV_CASE_STR(SDL_FINGERUP); + EV_CASE_STR(SDL_FINGERMOTION); + EV_CASE_STR(SDL_DOLLARGESTURE); + EV_CASE_STR(SDL_DOLLARRECORD); + EV_CASE_STR(SDL_MULTIGESTURE); + EV_CASE_STR(SDL_CLIPBOARDUPDATE); + EV_CASE_STR(SDL_DROPFILE); + EV_CASE_STR(SDL_DROPTEXT); + EV_CASE_STR(SDL_DROPBEGIN); + EV_CASE_STR(SDL_DROPCOMPLETE); + EV_CASE_STR(SDL_AUDIODEVICEADDED); + EV_CASE_STR(SDL_AUDIODEVICEREMOVED); +#if SDL_VERSION_ATLEAST(2, 0, 9) + EV_CASE_STR(SDL_SENSORUPDATE); +#endif + EV_CASE_STR(SDL_RENDER_TARGETS_RESET); + EV_CASE_STR(SDL_RENDER_DEVICE_RESET); + EV_CASE_STR(SDL_USEREVENT); + + EV_CASE_STR(SDL_USEREVENT_CERT_DIALOG); + EV_CASE_STR(SDL_USEREVENT_CERT_RESULT); + EV_CASE_STR(SDL_USEREVENT_SHOW_DIALOG); + EV_CASE_STR(SDL_USEREVENT_SHOW_RESULT); + EV_CASE_STR(SDL_USEREVENT_AUTH_DIALOG); + EV_CASE_STR(SDL_USEREVENT_AUTH_RESULT); + EV_CASE_STR(SDL_USEREVENT_SCARD_DIALOG); + EV_CASE_STR(SDL_USEREVENT_RETRY_DIALOG); + EV_CASE_STR(SDL_USEREVENT_SCARD_RESULT); + EV_CASE_STR(SDL_USEREVENT_UPDATE); + EV_CASE_STR(SDL_USEREVENT_CREATE_WINDOWS); + EV_CASE_STR(SDL_USEREVENT_WINDOW_RESIZEABLE); + EV_CASE_STR(SDL_USEREVENT_WINDOW_FULLSCREEN); + EV_CASE_STR(SDL_USEREVENT_POINTER_NULL); + EV_CASE_STR(SDL_USEREVENT_POINTER_DEFAULT); + EV_CASE_STR(SDL_USEREVENT_POINTER_POSITION); + EV_CASE_STR(SDL_USEREVENT_POINTER_SET); + EV_CASE_STR(SDL_USEREVENT_QUIT); + + EV_CASE_STR(SDL_LASTEVENT); + default: + return "SDL_UNKNOWNEVENT"; + } +#undef EV_CASE_STR +#undef STR +} + +const char* sdl_error_string(Uint32 res) +{ + if (res == 0) + return nullptr; + + return SDL_GetError(); +} + +BOOL sdl_log_error_ex(Uint32 res, wLog* log, const char* what, const char* file, size_t line, + const char* fkt) +{ + const char* msg = sdl_error_string(res); + + WINPR_UNUSED(file); + + if (!msg) + return FALSE; + + WLog_Print(log, WLOG_ERROR, "[%s:%" PRIuz "][%s]: %s", fkt, line, what, msg); + return TRUE; +} + +BOOL sdl_push_user_event(Uint32 type, ...) +{ + SDL_Event ev = {}; + SDL_UserEvent* event = &ev.user; + + va_list ap; + va_start(ap, type); + event->type = type; + switch (type) + { + case SDL_USEREVENT_AUTH_RESULT: + { + auto arg = reinterpret_cast<SDL_UserAuthArg*>(ev.padding); + arg->user = va_arg(ap, char*); + arg->domain = va_arg(ap, char*); + arg->password = va_arg(ap, char*); + arg->result = va_arg(ap, Sint32); + } + break; + case SDL_USEREVENT_AUTH_DIALOG: + { + auto arg = reinterpret_cast<SDL_UserAuthArg*>(ev.padding); + + arg->title = va_arg(ap, char*); + arg->user = va_arg(ap, char*); + arg->domain = va_arg(ap, char*); + arg->password = va_arg(ap, char*); + arg->result = va_arg(ap, Sint32); + } + break; + case SDL_USEREVENT_SCARD_DIALOG: + { + event->data1 = va_arg(ap, char*); + event->data2 = va_arg(ap, char**); + event->code = va_arg(ap, Sint32); + } + break; + case SDL_USEREVENT_RETRY_DIALOG: + break; + case SDL_USEREVENT_SCARD_RESULT: + case SDL_USEREVENT_SHOW_RESULT: + case SDL_USEREVENT_CERT_RESULT: + event->code = va_arg(ap, Sint32); + break; + + case SDL_USEREVENT_SHOW_DIALOG: + event->data1 = va_arg(ap, char*); + event->data2 = va_arg(ap, char*); + event->code = va_arg(ap, Sint32); + break; + case SDL_USEREVENT_CERT_DIALOG: + event->data1 = va_arg(ap, char*); + event->data2 = va_arg(ap, char*); + break; + case SDL_USEREVENT_UPDATE: + event->data1 = va_arg(ap, void*); + break; + case SDL_USEREVENT_POINTER_POSITION: + event->data1 = reinterpret_cast<void*>(static_cast<uintptr_t>(va_arg(ap, UINT32))); + event->data2 = reinterpret_cast<void*>(static_cast<uintptr_t>(va_arg(ap, UINT32))); + break; + case SDL_USEREVENT_POINTER_SET: + event->data1 = va_arg(ap, void*); + event->data2 = va_arg(ap, void*); + break; + case SDL_USEREVENT_CREATE_WINDOWS: + event->data1 = reinterpret_cast<void*>(va_arg(ap, void*)); + break; + case SDL_USEREVENT_WINDOW_FULLSCREEN: + case SDL_USEREVENT_WINDOW_RESIZEABLE: + event->data1 = va_arg(ap, void*); + event->code = va_arg(ap, int); + break; + case SDL_USEREVENT_QUIT: + case SDL_USEREVENT_POINTER_NULL: + case SDL_USEREVENT_POINTER_DEFAULT: + break; + default: + va_end(ap); + return FALSE; + } + va_end(ap); + return SDL_PushEvent(&ev) == 1; +} + +CriticalSection::CriticalSection() +{ + InitializeCriticalSection(&_section); +} + +CriticalSection::~CriticalSection() +{ + DeleteCriticalSection(&_section); +} + +void CriticalSection::lock() +{ + EnterCriticalSection(&_section); +} + +void CriticalSection::unlock() +{ + LeaveCriticalSection(&_section); +} + +WinPREvent::WinPREvent(bool initial) + : _handle(CreateEventA(nullptr, TRUE, initial ? TRUE : FALSE, nullptr)) +{ +} + +WinPREvent::~WinPREvent() +{ + CloseHandle(_handle); +} + +void WinPREvent::set() +{ + SetEvent(_handle); +} + +void WinPREvent::clear() +{ + ResetEvent(_handle); +} + +bool WinPREvent::isSet() const +{ + return WaitForSingleObject(_handle, 0) == WAIT_OBJECT_0; +} + +HANDLE WinPREvent::handle() const +{ + return _handle; +} + +bool sdl_push_quit() +{ + SDL_Event ev = { 0 }; + ev.type = SDL_QUIT; + SDL_PushEvent(&ev); + return true; +} + +std::string sdl_window_event_str(Uint8 ev) +{ + switch (ev) + { + case SDL_WINDOWEVENT_NONE: + return "SDL_WINDOWEVENT_NONE"; + case SDL_WINDOWEVENT_SHOWN: + return "SDL_WINDOWEVENT_SHOWN"; + case SDL_WINDOWEVENT_HIDDEN: + return "SDL_WINDOWEVENT_HIDDEN"; + case SDL_WINDOWEVENT_EXPOSED: + return "SDL_WINDOWEVENT_EXPOSED"; + case SDL_WINDOWEVENT_MOVED: + return "SDL_WINDOWEVENT_MOVED"; + case SDL_WINDOWEVENT_RESIZED: + return "SDL_WINDOWEVENT_RESIZED"; + case SDL_WINDOWEVENT_SIZE_CHANGED: + return "SDL_WINDOWEVENT_SIZE_CHANGED"; + case SDL_WINDOWEVENT_MINIMIZED: + return "SDL_WINDOWEVENT_MINIMIZED"; + case SDL_WINDOWEVENT_MAXIMIZED: + return "SDL_WINDOWEVENT_MAXIMIZED"; + case SDL_WINDOWEVENT_RESTORED: + return "SDL_WINDOWEVENT_RESTORED"; + case SDL_WINDOWEVENT_ENTER: + return "SDL_WINDOWEVENT_ENTER"; + case SDL_WINDOWEVENT_LEAVE: + return "SDL_WINDOWEVENT_LEAVE"; + case SDL_WINDOWEVENT_FOCUS_GAINED: + return "SDL_WINDOWEVENT_FOCUS_GAINED"; + case SDL_WINDOWEVENT_FOCUS_LOST: + return "SDL_WINDOWEVENT_FOCUS_LOST"; + case SDL_WINDOWEVENT_CLOSE: + return "SDL_WINDOWEVENT_CLOSE"; +#if SDL_VERSION_ATLEAST(2, 0, 5) + case SDL_WINDOWEVENT_TAKE_FOCUS: + return "SDL_WINDOWEVENT_TAKE_FOCUS"; + case SDL_WINDOWEVENT_HIT_TEST: + return "SDL_WINDOWEVENT_HIT_TEST"; +#endif +#if SDL_VERSION_ATLEAST(2, 0, 18) + case SDL_WINDOWEVENT_ICCPROF_CHANGED: + return "SDL_WINDOWEVENT_ICCPROF_CHANGED"; + case SDL_WINDOWEVENT_DISPLAY_CHANGED: + return "SDL_WINDOWEVENT_DISPLAY_CHANGED"; +#endif + default: + return "SDL_WINDOWEVENT_UNKNOWN"; + } +} + +#if defined(CJSON_FOUND) +using cJSONPtr = std::unique_ptr<cJSON, decltype(&cJSON_Delete)>; + +static cJSONPtr get() +{ + auto config = sdl_get_pref_file(); + + std::ifstream ifs(config); + std::string content((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>())); + return { cJSON_ParseWithLength(content.c_str(), content.size()), cJSON_Delete }; +} + +static cJSON* get_item(const std::string& key) +{ + static cJSONPtr config{ nullptr, cJSON_Delete }; + if (!config) + config = get(); + if (!config) + return nullptr; + return cJSON_GetObjectItem(config.get(), key.c_str()); +} + +static std::string item_to_str(cJSON* item, const std::string& fallback = "") +{ + if (!item || !cJSON_IsString(item)) + return fallback; + auto str = cJSON_GetStringValue(item); + if (!str) + return {}; + return str; +} +#endif + +std::string sdl_get_pref_string(const std::string& key, const std::string& fallback) +{ +#if defined(CJSON_FOUND) + auto item = get_item(key); + return item_to_str(item, fallback); +#else + return fallback; +#endif +} + +bool sdl_get_pref_bool(const std::string& key, bool fallback) +{ +#if defined(CJSON_FOUND) + auto item = get_item(key); + if (!item || !cJSON_IsBool(item)) + return fallback; + return cJSON_IsTrue(item); +#else + return fallback; +#endif +} + +int64_t sdl_get_pref_int(const std::string& key, int64_t fallback) +{ +#if defined(CJSON_FOUND) + auto item = get_item(key); + if (!item || !cJSON_IsNumber(item)) + return fallback; + auto val = cJSON_GetNumberValue(item); + return static_cast<int64_t>(val); +#else + return fallback; +#endif +} + +std::vector<std::string> sdl_get_pref_array(const std::string& key, + const std::vector<std::string>& fallback) +{ +#if defined(CJSON_FOUND) + auto item = get_item(key); + if (!item || !cJSON_IsArray(item)) + return fallback; + + std::vector<std::string> values; + for (int x = 0; x < cJSON_GetArraySize(item); x++) + { + auto cur = cJSON_GetArrayItem(item, x); + values.push_back(item_to_str(cur)); + } + + return values; +#else + return fallback; +#endif +} + +std::string sdl_get_pref_dir() +{ + using CStringPtr = std::unique_ptr<char, decltype(&free)>; + CStringPtr path(GetKnownPath(KNOWN_PATH_XDG_CONFIG_HOME), free); + if (!path) + return {}; + + fs::path config{ path.get() }; + config /= FREERDP_VENDOR; + config /= FREERDP_PRODUCT; + return config.string(); +} + +std::string sdl_get_pref_file() +{ + fs::path config{ sdl_get_pref_dir() }; + config /= "sdl-freerdp.json"; + return config.string(); +} diff --git a/client/SDL/sdl_utils.hpp b/client/SDL/sdl_utils.hpp new file mode 100644 index 0000000..75cb461 --- /dev/null +++ b/client/SDL/sdl_utils.hpp @@ -0,0 +1,113 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client + * + * Copyright 2022 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <string> + +#include <winpr/synch.h> +#include <winpr/wlog.h> + +#include <SDL.h> +#include <string> +#include <vector> + +class CriticalSection +{ + public: + CriticalSection(); + ~CriticalSection(); + + void lock(); + void unlock(); + + private: + CRITICAL_SECTION _section; +}; + +class WinPREvent +{ + public: + explicit WinPREvent(bool initial = false); + ~WinPREvent(); + + void set(); + void clear(); + [[nodiscard]] bool isSet() const; + + [[nodiscard]] HANDLE handle() const; + + private: + HANDLE _handle; +}; + +enum +{ + SDL_USEREVENT_UPDATE = SDL_USEREVENT + 1, + SDL_USEREVENT_CREATE_WINDOWS, + SDL_USEREVENT_WINDOW_RESIZEABLE, + SDL_USEREVENT_WINDOW_FULLSCREEN, + SDL_USEREVENT_POINTER_NULL, + SDL_USEREVENT_POINTER_DEFAULT, + SDL_USEREVENT_POINTER_POSITION, + SDL_USEREVENT_POINTER_SET, + SDL_USEREVENT_QUIT, + SDL_USEREVENT_CERT_DIALOG, + SDL_USEREVENT_SHOW_DIALOG, + SDL_USEREVENT_AUTH_DIALOG, + SDL_USEREVENT_SCARD_DIALOG, + SDL_USEREVENT_RETRY_DIALOG, + + SDL_USEREVENT_CERT_RESULT, + SDL_USEREVENT_SHOW_RESULT, + SDL_USEREVENT_AUTH_RESULT, + SDL_USEREVENT_SCARD_RESULT +}; + +typedef struct +{ + Uint32 type; + Uint32 timestamp; + char* title; + char* user; + char* domain; + char* password; + Sint32 result; +} SDL_UserAuthArg; + +BOOL sdl_push_user_event(Uint32 type, ...); + +bool sdl_push_quit(); + +std::string sdl_window_event_str(Uint8 ev); +const char* sdl_event_type_str(Uint32 type); +const char* sdl_error_string(Uint32 res); + +#define sdl_log_error(res, log, what) sdl_log_error_ex(res, log, what, __FILE__, __LINE__, __func__) +BOOL sdl_log_error_ex(Uint32 res, wLog* log, const char* what, const char* file, size_t line, + const char* fkt); + +std::string sdl_get_pref_dir(); +std::string sdl_get_pref_file(); + +std::string sdl_get_pref_string(const std::string& key, const std::string& fallback = ""); +int64_t sdl_get_pref_int(const std::string& key, int64_t fallback = 0); +bool sdl_get_pref_bool(const std::string& key, bool fallback = false); +std::vector<std::string> sdl_get_pref_array(const std::string& key, + const std::vector<std::string>& fallback = {}); diff --git a/client/SDL/sdl_window.cpp b/client/SDL/sdl_window.cpp new file mode 100644 index 0000000..c5437bc --- /dev/null +++ b/client/SDL/sdl_window.cpp @@ -0,0 +1,203 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * Copyright 2023 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "sdl_window.hpp" +#include "sdl_utils.hpp" + +SdlWindow::SdlWindow(const std::string& title, Sint32 startupX, Sint32 startupY, Sint32 width, + Sint32 height, Uint32 flags) + : _window(SDL_CreateWindow(title.c_str(), startupX, startupY, width, height, flags)), + _offset_x(0), _offset_y(0) +{ +} + +SdlWindow::SdlWindow(SdlWindow&& other) + : _window(other._window), _offset_x(other._offset_x), _offset_y(other._offset_y) +{ + other._window = nullptr; +} + +SdlWindow::~SdlWindow() +{ + SDL_DestroyWindow(_window); +} + +Uint32 SdlWindow::id() const +{ + if (!_window) + return 0; + return SDL_GetWindowID(_window); +} + +int SdlWindow::displayIndex() const +{ + if (!_window) + return 0; + return SDL_GetWindowDisplayIndex(_window); +} + +SDL_Rect SdlWindow::rect() const +{ + SDL_Rect rect = {}; + if (_window) + { + SDL_GetWindowPosition(_window, &rect.x, &rect.y); + SDL_GetWindowSize(_window, &rect.w, &rect.h); + } + return rect; +} + +SDL_Window* SdlWindow::window() const +{ + return _window; +} + +Sint32 SdlWindow::offsetX() const +{ + return _offset_x; +} + +void SdlWindow::setOffsetX(Sint32 x) +{ + _offset_x = x; +} + +void SdlWindow::setOffsetY(Sint32 y) +{ + _offset_y = y; +} + +Sint32 SdlWindow::offsetY() const +{ + return _offset_y; +} + +bool SdlWindow::grabKeyboard(bool enable) +{ + if (!_window) + return false; +#if SDL_VERSION_ATLEAST(2, 0, 16) + SDL_SetWindowKeyboardGrab(_window, enable ? SDL_TRUE : SDL_FALSE); + return true; +#else + SDL_LogError(SDL_LOG_CATEGORY_INPUT, "Keyboard grabbing not supported by SDL2 < 2.0.16"); + return false; +#endif +} + +bool SdlWindow::grabMouse(bool enable) +{ + if (!_window) + return false; +#if SDL_VERSION_ATLEAST(2, 0, 16) + SDL_SetWindowMouseGrab(_window, enable ? SDL_TRUE : SDL_FALSE); +#else + SDL_SetWindowGrab(_window, enable ? SDL_TRUE : SDL_FALSE); +#endif + return true; +} + +void SdlWindow::setBordered(bool bordered) +{ + if (_window) + SDL_SetWindowBordered(_window, bordered ? SDL_TRUE : SDL_FALSE); +} + +void SdlWindow::raise() +{ + SDL_RaiseWindow(_window); +} + +void SdlWindow::resizeable(bool use) +{ + SDL_SetWindowResizable(_window, use ? SDL_TRUE : SDL_FALSE); +} + +void SdlWindow::fullscreen(bool enter) +{ + auto curFlags = SDL_GetWindowFlags(_window); + + if (enter) + { + if (!(curFlags & SDL_WINDOW_BORDERLESS)) + { + auto idx = SDL_GetWindowDisplayIndex(_window); + SDL_DisplayMode mode = {}; + SDL_GetCurrentDisplayMode(idx, &mode); + + SDL_RestoreWindow(_window); // Maximize so we can see the caption and + // bits + SDL_SetWindowBordered(_window, SDL_FALSE); + SDL_SetWindowPosition(_window, 0, 0); +#if SDL_VERSION_ATLEAST(2, 0, 16) + SDL_SetWindowAlwaysOnTop(_window, SDL_TRUE); +#endif + SDL_RaiseWindow(_window); + SDL_SetWindowSize(_window, mode.w, mode.h); + } + } + else + { + if (curFlags & SDL_WINDOW_BORDERLESS) + { + + SDL_SetWindowBordered(_window, SDL_TRUE); +#if SDL_VERSION_ATLEAST(2, 0, 16) + SDL_SetWindowAlwaysOnTop(_window, SDL_FALSE); +#endif + SDL_RaiseWindow(_window); + SDL_MinimizeWindow(_window); // Maximize so we can see the caption and bits + SDL_MaximizeWindow(_window); // Maximize so we can see the caption and bits + } + } +} + +bool SdlWindow::fill(Uint8 r, Uint8 g, Uint8 b, Uint8 a) +{ + auto surface = SDL_GetWindowSurface(_window); + if (!surface) + return false; + SDL_Rect rect = { 0, 0, surface->w, surface->h }; + auto color = SDL_MapRGBA(surface->format, r, g, b, a); + + SDL_FillRect(surface, &rect, color); + return true; +} + +bool SdlWindow::blit(SDL_Surface* surface, const SDL_Rect& srcRect, SDL_Rect& dstRect) +{ + auto screen = SDL_GetWindowSurface(_window); + if (!screen || !surface) + return false; + if (!SDL_SetClipRect(surface, &srcRect)) + return false; + if (!SDL_SetClipRect(screen, &dstRect)) + return false; + auto rc = SDL_BlitScaled(surface, &srcRect, screen, &dstRect); + if (rc != 0) + { + SDL_LogError(SDL_LOG_CATEGORY_RENDER, "SDL_BlitScaled: %s [%d]", sdl_error_string(rc), rc); + } + return rc == 0; +} + +void SdlWindow::updateSurface() +{ + SDL_UpdateWindowSurface(_window); +} diff --git a/client/SDL/sdl_window.hpp b/client/SDL/sdl_window.hpp new file mode 100644 index 0000000..4f84e1b --- /dev/null +++ b/client/SDL/sdl_window.hpp @@ -0,0 +1,62 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * Copyright 2023 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <string> +#include <SDL.h> + +class SdlWindow +{ + public: + SdlWindow(const std::string& title, Sint32 startupX, Sint32 startupY, Sint32 width, + Sint32 height, Uint32 flags); + SdlWindow(SdlWindow&& other); + ~SdlWindow(); + + [[nodiscard]] Uint32 id() const; + [[nodiscard]] int displayIndex() const; + [[nodiscard]] SDL_Rect rect() const; + [[nodiscard]] SDL_Window* window() const; + + [[nodiscard]] Sint32 offsetX() const; + void setOffsetX(Sint32 x); + + void setOffsetY(Sint32 y); + [[nodiscard]] Sint32 offsetY() const; + + bool grabKeyboard(bool enable); + bool grabMouse(bool enable); + void setBordered(bool bordered); + void raise(); + void resizeable(bool use); + void fullscreen(bool use); + + bool fill(Uint8 r = 0x00, Uint8 g = 0x00, Uint8 b = 0x00, Uint8 a = 0xff); + bool blit(SDL_Surface* surface, const SDL_Rect& src, SDL_Rect& dst); + void updateSurface(); + + private: + SDL_Window* _window = nullptr; + Sint32 _offset_x = 0; + Sint32 _offset_y = 0; + + private: + SdlWindow(const SdlWindow& other) = delete; +}; diff --git a/client/Sample/CMakeLists.txt b/client/Sample/CMakeLists.txt new file mode 100644 index 0000000..db4e947 --- /dev/null +++ b/client/Sample/CMakeLists.txt @@ -0,0 +1,77 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP Sample UI cmake build script +# +# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +cmake_minimum_required(VERSION 3.13) + +if(POLICY CMP0091) + cmake_policy(SET CMP0091 NEW) +endif() +if (NOT FREERDP_DEFAULT_PROJECT_VERSION) + set(FREERDP_DEFAULT_PROJECT_VERSION "1.0.0.0") +endif() + +project(sfreerdp + LANGUAGES C + VERSION ${FREERDP_DEFAULT_PROJECT_VERSION} +) + +message("project ${PROJECT_NAME} is using version ${PROJECT_VERSION}") + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS ON) + +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/) +include(CommonConfigOptions) + +include(ConfigureFreeRDP) + +set(SRCS + tf_channels.c + tf_channels.h + tf_freerdp.h + tf_freerdp.c) + +# On windows create dll version information. +# Vendor, product and year are already set in top level CMakeLists.txt +if (WIN32) + set (RC_VERSION_MAJOR ${FREERDP_VERSION_MAJOR}) + set (RC_VERSION_MINOR ${FREERDP_VERSION_MINOR}) + set (RC_VERSION_BUILD ${FREERDP_VERSION_REVISION}) + set (RC_VERSION_FILE "${PROJECT_NAME}${CMAKE_EXECUTABLE_SUFFIX}" ) + + configure_file( + ${PROJECT_SOURCE_DIR}/../../cmake/WindowsDLLVersion.rc.in + ${CMAKE_CURRENT_BINARY_DIR}/version.rc + @ONLY) + +list (APPEND SRCS ${CMAKE_CURRENT_BINARY_DIR}/version.rc) +endif() + +add_executable(${PROJECT_NAME} ${SRCS}) + +set(LIBS + freerdp-client + freerdp + winpr +) +target_link_libraries(${PROJECT_NAME} PRIVATE ${LIBS}) + +if (WITH_BINARY_VERSIONING) + set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "${PROJECT_NAME}${PROJECT_VERSION_MAJOR}") +endif() +set_property(TARGET ${PROJECT_NAME} PROPERTY FOLDER "Client/Sample") +install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT client) diff --git a/client/Sample/ModuleOptions.cmake b/client/Sample/ModuleOptions.cmake new file mode 100644 index 0000000..d4d5a9e --- /dev/null +++ b/client/Sample/ModuleOptions.cmake @@ -0,0 +1,4 @@ + +set(FREERDP_CLIENT_NAME "sfreerdp") +set(FREERDP_CLIENT_PLATFORM "Sample") +set(FREERDP_CLIENT_VENDOR "FreeRDP") diff --git a/client/Sample/tf_channels.c b/client/Sample/tf_channels.c new file mode 100644 index 0000000..d1bfdb8 --- /dev/null +++ b/client/Sample/tf_channels.c @@ -0,0 +1,79 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Sample Client Channels + * + * Copyright 2018 Armin Novak <armin.novak@thincast.com> + * Copyright 2018 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <winpr/assert.h> +#include <freerdp/gdi/gfx.h> + +#include <freerdp/client/rdpei.h> +#include <freerdp/client/rail.h> +#include <freerdp/client/cliprdr.h> +#include <freerdp/client/rdpgfx.h> +#include <freerdp/client/encomsp.h> + +#include "tf_channels.h" +#include "tf_freerdp.h" + +static UINT tf_update_surfaces(RdpgfxClientContext* context) +{ + WINPR_UNUSED(context); + return CHANNEL_RC_OK; +} + +void tf_OnChannelConnectedEventHandler(void* context, const ChannelConnectedEventArgs* e) +{ + tfContext* tf = (tfContext*)context; + + WINPR_ASSERT(tf); + WINPR_ASSERT(e); + + if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0) + { + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + CliprdrClientContext* clip = (CliprdrClientContext*)e->pInterface; + WINPR_ASSERT(clip); + clip->custom = context; + } + else + freerdp_client_OnChannelConnectedEventHandler(context, e); +} + +void tf_OnChannelDisconnectedEventHandler(void* context, const ChannelDisconnectedEventArgs* e) +{ + tfContext* tf = (tfContext*)context; + + WINPR_ASSERT(tf); + WINPR_ASSERT(e); + + if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0) + { + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + CliprdrClientContext* clip = (CliprdrClientContext*)e->pInterface; + WINPR_ASSERT(clip); + clip->custom = NULL; + } + else + freerdp_client_OnChannelDisconnectedEventHandler(context, e); +} diff --git a/client/Sample/tf_channels.h b/client/Sample/tf_channels.h new file mode 100644 index 0000000..d00a5c2 --- /dev/null +++ b/client/Sample/tf_channels.h @@ -0,0 +1,33 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Sample Client Channels + * + * Copyright 2018 Armin Novak <armin.novak@thincast.com> + * Copyright 2018 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_SAMPLE_CHANNELS_H +#define FREERDP_CLIENT_SAMPLE_CHANNELS_H + +#include <freerdp/freerdp.h> +#include <freerdp/client/channels.h> + +int tf_on_channel_connected(freerdp* instance, const char* name, void* pInterface); +int tf_on_channel_disconnected(freerdp* instance, const char* name, void* pInterface); + +void tf_OnChannelConnectedEventHandler(void* context, const ChannelConnectedEventArgs* e); +void tf_OnChannelDisconnectedEventHandler(void* context, const ChannelDisconnectedEventArgs* e); + +#endif /* FREERDP_CLIENT_SAMPLE_CHANNELS_H */ diff --git a/client/Sample/tf_freerdp.c b/client/Sample/tf_freerdp.c new file mode 100644 index 0000000..2a799fc --- /dev/null +++ b/client/Sample/tf_freerdp.c @@ -0,0 +1,411 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Test UI + * + * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * Copyright 2016,2018 Armin Novak <armin.novak@thincast.com> + * Copyright 2016,2018 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <errno.h> +#include <stdio.h> +#include <string.h> + +#include <freerdp/freerdp.h> +#include <freerdp/constants.h> +#include <freerdp/gdi/gdi.h> +#include <freerdp/streamdump.h> +#include <freerdp/utils/signal.h> + +#include <freerdp/client/file.h> +#include <freerdp/client/cmdline.h> +#include <freerdp/client/cliprdr.h> +#include <freerdp/client/channels.h> +#include <freerdp/channels/channels.h> + +#include <winpr/crt.h> +#include <winpr/assert.h> +#include <winpr/synch.h> +#include <freerdp/log.h> + +#include "tf_channels.h" +#include "tf_freerdp.h" + +#define TAG CLIENT_TAG("sample") + +/* This function is called whenever a new frame starts. + * It can be used to reset invalidated areas. */ +static BOOL tf_begin_paint(rdpContext* context) +{ + rdpGdi* gdi = NULL; + + WINPR_ASSERT(context); + + gdi = context->gdi; + WINPR_ASSERT(gdi); + WINPR_ASSERT(gdi->primary); + WINPR_ASSERT(gdi->primary->hdc); + WINPR_ASSERT(gdi->primary->hdc->hwnd); + WINPR_ASSERT(gdi->primary->hdc->hwnd->invalid); + gdi->primary->hdc->hwnd->invalid->null = TRUE; + return TRUE; +} + +/* This function is called when the library completed composing a new + * frame. Read out the changed areas and blit them to your output device. + * The image buffer will have the format specified by gdi_init + */ +static BOOL tf_end_paint(rdpContext* context) +{ + rdpGdi* gdi = NULL; + + WINPR_ASSERT(context); + + gdi = context->gdi; + WINPR_ASSERT(gdi); + WINPR_ASSERT(gdi->primary); + WINPR_ASSERT(gdi->primary->hdc); + WINPR_ASSERT(gdi->primary->hdc->hwnd); + WINPR_ASSERT(gdi->primary->hdc->hwnd->invalid); + + if (gdi->primary->hdc->hwnd->invalid->null) + return TRUE; + + return TRUE; +} + +static BOOL tf_desktop_resize(rdpContext* context) +{ + rdpGdi* gdi = NULL; + rdpSettings* settings = NULL; + + WINPR_ASSERT(context); + + settings = context->settings; + WINPR_ASSERT(settings); + + gdi = context->gdi; + return gdi_resize(gdi, freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth), + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight)); +} + +/* This function is called to output a System BEEP */ +static BOOL tf_play_sound(rdpContext* context, const PLAY_SOUND_UPDATE* play_sound) +{ + /* TODO: Implement */ + WINPR_UNUSED(context); + WINPR_UNUSED(play_sound); + return TRUE; +} + +/* This function is called to update the keyboard indocator LED */ +static BOOL tf_keyboard_set_indicators(rdpContext* context, UINT16 led_flags) +{ + /* TODO: Set local keyboard indicator LED status */ + WINPR_UNUSED(context); + WINPR_UNUSED(led_flags); + return TRUE; +} + +/* This function is called to set the IME state */ +static BOOL tf_keyboard_set_ime_status(rdpContext* context, UINT16 imeId, UINT32 imeState, + UINT32 imeConvMode) +{ + if (!context) + return FALSE; + + WLog_WARN(TAG, + "KeyboardSetImeStatus(unitId=%04" PRIx16 ", imeState=%08" PRIx32 + ", imeConvMode=%08" PRIx32 ") ignored", + imeId, imeState, imeConvMode); + return TRUE; +} + +/* Called before a connection is established. + * Set all configuration options to support and load channels here. */ +static BOOL tf_pre_connect(freerdp* instance) +{ + rdpSettings* settings = NULL; + + WINPR_ASSERT(instance); + WINPR_ASSERT(instance->context); + + settings = instance->context->settings; + WINPR_ASSERT(settings); + + /* Optional OS identifier sent to server */ + if (!freerdp_settings_set_uint32(settings, FreeRDP_OsMajorType, OSMAJORTYPE_UNIX)) + return FALSE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_OsMinorType, OSMINORTYPE_NATIVE_XSERVER)) + return FALSE; + /* OrderSupport is initialized at this point. + * Only override it if you plan to implement custom order + * callbacks or deactiveate certain features. */ + /* Register the channel listeners. + * They are required to set up / tear down channels if they are loaded. */ + PubSub_SubscribeChannelConnected(instance->context->pubSub, tf_OnChannelConnectedEventHandler); + PubSub_SubscribeChannelDisconnected(instance->context->pubSub, + tf_OnChannelDisconnectedEventHandler); + + /* TODO: Any code your client requires */ + return TRUE; +} + +/* Called after a RDP connection was successfully established. + * Settings might have changed during negociation of client / server feature + * support. + * + * Set up local framebuffers and paing callbacks. + * If required, register pointer callbacks to change the local mouse cursor + * when hovering over the RDP window + */ +static BOOL tf_post_connect(freerdp* instance) +{ + rdpContext* context = NULL; + + if (!gdi_init(instance, PIXEL_FORMAT_XRGB32)) + return FALSE; + + context = instance->context; + WINPR_ASSERT(context); + WINPR_ASSERT(context->update); + + /* With this setting we disable all graphics processing in the library. + * + * This allows low resource (client) protocol parsing. + */ + if (!freerdp_settings_set_bool(context->settings, FreeRDP_DeactivateClientDecoding, TRUE)) + return FALSE; + + context->update->BeginPaint = tf_begin_paint; + context->update->EndPaint = tf_end_paint; + context->update->PlaySound = tf_play_sound; + context->update->DesktopResize = tf_desktop_resize; + context->update->SetKeyboardIndicators = tf_keyboard_set_indicators; + context->update->SetKeyboardImeStatus = tf_keyboard_set_ime_status; + return TRUE; +} + +/* This function is called whether a session ends by failure or success. + * Clean up everything allocated by pre_connect and post_connect. + */ +static void tf_post_disconnect(freerdp* instance) +{ + tfContext* context = NULL; + + if (!instance) + return; + + if (!instance->context) + return; + + context = (tfContext*)instance->context; + PubSub_UnsubscribeChannelConnected(instance->context->pubSub, + tf_OnChannelConnectedEventHandler); + PubSub_UnsubscribeChannelDisconnected(instance->context->pubSub, + tf_OnChannelDisconnectedEventHandler); + gdi_free(instance); + /* TODO : Clean up custom stuff */ + WINPR_UNUSED(context); +} + +/* RDP main loop. + * Connects RDP, loops while running and handles event and dispatch, cleans up + * after the connection ends. */ +static DWORD WINAPI tf_client_thread_proc(LPVOID arg) +{ + freerdp* instance = (freerdp*)arg; + DWORD nCount = 0; + DWORD status = 0; + DWORD result = 0; + HANDLE handles[MAXIMUM_WAIT_OBJECTS] = { 0 }; + BOOL rc = freerdp_connect(instance); + + WINPR_ASSERT(instance->context); + WINPR_ASSERT(instance->context->settings); + if (freerdp_settings_get_bool(instance->context->settings, FreeRDP_AuthenticationOnly)) + { + result = freerdp_get_last_error(instance->context); + freerdp_abort_connect_context(instance->context); + WLog_ERR(TAG, "Authentication only, exit status 0x%08" PRIx32 "", result); + goto disconnect; + } + + if (!rc) + { + result = freerdp_get_last_error(instance->context); + WLog_ERR(TAG, "connection failure 0x%08" PRIx32, result); + return result; + } + + while (!freerdp_shall_disconnect_context(instance->context)) + { + nCount = freerdp_get_event_handles(instance->context, handles, ARRAYSIZE(handles)); + + if (nCount == 0) + { + WLog_ERR(TAG, "freerdp_get_event_handles failed"); + break; + } + + status = WaitForMultipleObjects(nCount, handles, FALSE, 100); + + if (status == WAIT_FAILED) + { + WLog_ERR(TAG, "WaitForMultipleObjects failed with %" PRIu32 "", status); + break; + } + + if (!freerdp_check_event_handles(instance->context)) + { + if (freerdp_get_last_error(instance->context) == FREERDP_ERROR_SUCCESS) + WLog_ERR(TAG, "Failed to check FreeRDP event handles"); + + break; + } + } + +disconnect: + freerdp_disconnect(instance); + return result; +} + +/* Optional global initializer. + * Here we just register a signal handler to print out stack traces + * if available. */ +static BOOL tf_client_global_init(void) +{ + if (freerdp_handle_signals() != 0) + return FALSE; + + return TRUE; +} + +/* Optional global tear down */ +static void tf_client_global_uninit(void) +{ +} + +static int tf_logon_error_info(freerdp* instance, UINT32 data, UINT32 type) +{ + tfContext* tf = NULL; + const char* str_data = freerdp_get_logon_error_info_data(data); + const char* str_type = freerdp_get_logon_error_info_type(type); + + if (!instance || !instance->context) + return -1; + + tf = (tfContext*)instance->context; + WLog_INFO(TAG, "Logon Error Info %s [%s]", str_data, str_type); + WINPR_UNUSED(tf); + + return 1; +} + +static BOOL tf_client_new(freerdp* instance, rdpContext* context) +{ + tfContext* tf = (tfContext*)context; + + if (!instance || !context) + return FALSE; + + instance->PreConnect = tf_pre_connect; + instance->PostConnect = tf_post_connect; + instance->PostDisconnect = tf_post_disconnect; + instance->LogonErrorInfo = tf_logon_error_info; + /* TODO: Client display set up */ + WINPR_UNUSED(tf); + return TRUE; +} + +static void tf_client_free(freerdp* instance, rdpContext* context) +{ + tfContext* tf = (tfContext*)instance->context; + + if (!context) + return; + + /* TODO: Client display tear down */ + WINPR_UNUSED(tf); +} + +static int tf_client_start(rdpContext* context) +{ + /* TODO: Start client related stuff */ + WINPR_UNUSED(context); + return 0; +} + +static int tf_client_stop(rdpContext* context) +{ + /* TODO: Stop client related stuff */ + WINPR_UNUSED(context); + return 0; +} + +static int RdpClientEntry(RDP_CLIENT_ENTRY_POINTS* pEntryPoints) +{ + WINPR_ASSERT(pEntryPoints); + + ZeroMemory(pEntryPoints, sizeof(RDP_CLIENT_ENTRY_POINTS)); + pEntryPoints->Version = RDP_CLIENT_INTERFACE_VERSION; + pEntryPoints->Size = sizeof(RDP_CLIENT_ENTRY_POINTS_V1); + pEntryPoints->GlobalInit = tf_client_global_init; + pEntryPoints->GlobalUninit = tf_client_global_uninit; + pEntryPoints->ContextSize = sizeof(tfContext); + pEntryPoints->ClientNew = tf_client_new; + pEntryPoints->ClientFree = tf_client_free; + pEntryPoints->ClientStart = tf_client_start; + pEntryPoints->ClientStop = tf_client_stop; + return 0; +} + +int main(int argc, char* argv[]) +{ + int rc = -1; + DWORD status = 0; + RDP_CLIENT_ENTRY_POINTS clientEntryPoints; + rdpContext* context = NULL; + RdpClientEntry(&clientEntryPoints); + context = freerdp_client_context_new(&clientEntryPoints); + + if (!context) + goto fail; + + status = freerdp_client_settings_parse_command_line(context->settings, argc, argv, FALSE); + if (status) + { + rc = freerdp_client_settings_command_line_status_print(context->settings, status, argc, + argv); + goto fail; + } + + if (!stream_dump_register_handlers(context, CONNECTION_STATE_MCS_CREATE_REQUEST, FALSE)) + goto fail; + + if (freerdp_client_start(context) != 0) + goto fail; + + rc = tf_client_thread_proc(context->instance); + + if (freerdp_client_stop(context) != 0) + rc = -1; + +fail: + freerdp_client_context_free(context); + return rc; +} diff --git a/client/Sample/tf_freerdp.h b/client/Sample/tf_freerdp.h new file mode 100644 index 0000000..c9b5295 --- /dev/null +++ b/client/Sample/tf_freerdp.h @@ -0,0 +1,37 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Sample Client + * + * Copyright 2018 Armin Novak <armin.novak@thincast.com> + * Copyright 2018 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_SAMPLE_H +#define FREERDP_CLIENT_SAMPLE_H + +#include <freerdp/freerdp.h> +#include <freerdp/client/rdpei.h> +#include <freerdp/client/rail.h> +#include <freerdp/client/cliprdr.h> +#include <freerdp/client/rdpgfx.h> + +typedef struct +{ + rdpClientContext common; + + /* Channels */ +} tfContext; + +#endif /* FREERDP_CLIENT_SAMPLE_H */ diff --git a/client/Wayland/CMakeLists.txt b/client/Wayland/CMakeLists.txt new file mode 100644 index 0000000..7076ff1 --- /dev/null +++ b/client/Wayland/CMakeLists.txt @@ -0,0 +1,62 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP Wayland Client cmake build script +# +# Copyright 2014 Manuel Bachmann <tarnyko@tarnyko.net> +# Copyright 2015 David Fort <contact@hardening-consulting.com> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set(MODULE_NAME "wlfreerdp") +set(MODULE_PREFIX "FREERDP_CLIENT_WAYLAND") + +include_directories(${WAYLAND_INCLUDE_DIR}) + +set(${MODULE_PREFIX}_SRCS + wlfreerdp.c + wlfreerdp.h + wlf_disp.c + wlf_disp.h + wlf_pointer.c + wlf_pointer.h + wlf_input.c + wlf_input.h + wlf_cliprdr.c + wlf_cliprdr.h + wlf_channels.c + wlf_channels.h + ) + +if (FREERDP_UNIFIED_BUILD) + include_directories(${PROJECT_SOURCE_DIR}/uwac/include) + include_directories(${PROJECT_BINARY_DIR}/uwac/include) +else() + find_package(uwac 0 REQUIRED) + include_directories(${UWAC_INCLUDE_DIR}) +endif() + +list (APPEND ${MODULE_PREFIX}_LIBS freerdp-client freerdp uwac) + +add_executable(${MODULE_NAME} ${${MODULE_PREFIX}_SRCS}) + +set(MANPAGE_NAME ${MODULE_NAME}) +if (WITH_BINARY_VERSIONING) + set_target_properties(${MODULE_NAME} PROPERTIES OUTPUT_NAME "${MODULE_NAME}${FREERDP_API_VERSION}") + set(MANPAGE_NAME ${MODULE_NAME}${FREERDP_API_VERSION}) +endif() +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + +install(TARGETS ${MODULE_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT client) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Client/Wayland") +configure_file(wlfreerdp.1.in ${CMAKE_CURRENT_BINARY_DIR}/${MANPAGE_NAME}.1) +install_freerdp_man(${CMAKE_CURRENT_BINARY_DIR}/${MANPAGE_NAME}.1 1) diff --git a/client/Wayland/wlf_channels.c b/client/Wayland/wlf_channels.c new file mode 100644 index 0000000..3a11407 --- /dev/null +++ b/client/Wayland/wlf_channels.c @@ -0,0 +1,79 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Client Channels + * + * Copyright 2013 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <freerdp/gdi/gfx.h> + +#include <freerdp/gdi/video.h> + +#include "wlf_channels.h" +#include "wlf_cliprdr.h" +#include "wlf_disp.h" +#include "wlfreerdp.h" + +void wlf_OnChannelConnectedEventHandler(void* context, const ChannelConnectedEventArgs* e) +{ + wlfContext* wlf = (wlfContext*)context; + + WINPR_ASSERT(wlf); + WINPR_ASSERT(e); + + if (FALSE) + { + } + else if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0) + { + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + wlf_cliprdr_init(wlf->clipboard, (CliprdrClientContext*)e->pInterface); + } + else if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) == 0) + { + wlf_disp_init(wlf->disp, (DispClientContext*)e->pInterface); + } + else + freerdp_client_OnChannelConnectedEventHandler(context, e); +} + +void wlf_OnChannelDisconnectedEventHandler(void* context, const ChannelDisconnectedEventArgs* e) +{ + wlfContext* wlf = (wlfContext*)context; + + WINPR_ASSERT(wlf); + WINPR_ASSERT(e); + + if (FALSE) + { + } + else if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0) + { + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + wlf_cliprdr_uninit(wlf->clipboard, (CliprdrClientContext*)e->pInterface); + } + else if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) == 0) + { + wlf_disp_uninit(wlf->disp, (DispClientContext*)e->pInterface); + } + else + freerdp_client_OnChannelDisconnectedEventHandler(context, e); +} diff --git a/client/Wayland/wlf_channels.h b/client/Wayland/wlf_channels.h new file mode 100644 index 0000000..e876031 --- /dev/null +++ b/client/Wayland/wlf_channels.h @@ -0,0 +1,37 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Client Channels + * + * Copyright 2013 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_WAYLAND_CHANNELS_H +#define FREERDP_CLIENT_WAYLAND_CHANNELS_H + +#include <freerdp/freerdp.h> +#include <freerdp/client/channels.h> +#include <freerdp/client/rdpei.h> +#include <freerdp/client/rail.h> +#include <freerdp/client/cliprdr.h> +#include <freerdp/client/rdpgfx.h> +#include <freerdp/client/encomsp.h> + +int wlf_on_channel_connected(freerdp* instance, const char* name, void* pInterface); +int wlf_on_channel_disconnected(freerdp* instance, const char* name, void* pInterface); + +void wlf_OnChannelConnectedEventHandler(void* context, const ChannelConnectedEventArgs* e); +void wlf_OnChannelDisconnectedEventHandler(void* context, const ChannelDisconnectedEventArgs* e); + +#endif /* FREERDP_CLIENT_WAYLAND_CHANNELS_H */ diff --git a/client/Wayland/wlf_cliprdr.c b/client/Wayland/wlf_cliprdr.c new file mode 100644 index 0000000..dc189d5 --- /dev/null +++ b/client/Wayland/wlf_cliprdr.c @@ -0,0 +1,1009 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Clipboard Redirection + * + * Copyright 2018 Armin Novak <armin.novak@thincast.com> + * Copyright 2018 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <stdlib.h> + +#include <winpr/crt.h> +#include <winpr/image.h> +#include <winpr/stream.h> +#include <winpr/clipboard.h> + +#include <freerdp/log.h> +#include <freerdp/client/cliprdr.h> +#include <freerdp/channels/channels.h> +#include <freerdp/channels/cliprdr.h> + +#include <freerdp/client/client_cliprdr_file.h> + +#include "wlf_cliprdr.h" + +#define TAG CLIENT_TAG("wayland.cliprdr") + +#define MAX_CLIPBOARD_FORMATS 255 + +#define mime_text_plain "text/plain" +#define mime_text_utf8 mime_text_plain ";charset=utf-8" + +static const char* mime_text[] = { mime_text_plain, mime_text_utf8, "UTF8_STRING", + "COMPOUND_TEXT", "TEXT", "STRING" }; + +static const char mime_png[] = "image/png"; +static const char mime_webp[] = "image/webp"; +static const char mime_jpg[] = "image/jpeg"; +static const char mime_tiff[] = "image/tiff"; +static const char mime_uri_list[] = "text/uri-list"; +static const char mime_html[] = "text/html"; + +#define BMP_MIME_LIST "image/bmp", "image/x-bmp", "image/x-MS-bmp", "image/x-win-bitmap" +static const char* mime_bitmap[] = { BMP_MIME_LIST }; +static const char* mime_image[] = { mime_png, mime_webp, mime_jpg, mime_tiff, BMP_MIME_LIST }; + +static const char mime_gnome_copied_files[] = "x-special/gnome-copied-files"; +static const char mime_mate_copied_files[] = "x-special/mate-copied-files"; + +static const char type_FileGroupDescriptorW[] = "FileGroupDescriptorW"; +static const char type_HtmlFormat[] = "HTML Format"; + +typedef struct +{ + FILE* responseFile; + UINT32 responseFormat; + char* responseMime; +} wlf_request; + +struct wlf_clipboard +{ + wlfContext* wfc; + rdpChannels* channels; + CliprdrClientContext* context; + wLog* log; + + UwacSeat* seat; + wClipboard* system; + + size_t numClientFormats; + CLIPRDR_FORMAT* clientFormats; + + size_t numServerFormats; + CLIPRDR_FORMAT* serverFormats; + + BOOL sync; + + CRITICAL_SECTION lock; + CliprdrFileContext* file; + + wQueue* request_queue; +}; + +static void wlf_request_free(void* rq) +{ + wlf_request* request = rq; + if (request) + { + free(request->responseMime); + if (request->responseFile) + fclose(request->responseFile); + } + free(request); +} + +static wlf_request* wlf_request_new(void) +{ + return calloc(1, sizeof(wlf_request)); +} + +static void* wlf_request_clone(const void* oth) +{ + const wlf_request* other = (const wlf_request*)oth; + wlf_request* copy = wlf_request_new(); + if (!copy) + return NULL; + *copy = *other; + if (other->responseMime) + { + copy->responseMime = _strdup(other->responseMime); + if (!copy->responseMime) + goto fail; + } + return copy; +fail: + wlf_request_free(copy); + return NULL; +} + +static BOOL wlf_mime_is_file(const char* mime) +{ + if (strncmp(mime_uri_list, mime, sizeof(mime_uri_list)) == 0) + return TRUE; + if (strncmp(mime_gnome_copied_files, mime, sizeof(mime_gnome_copied_files)) == 0) + return TRUE; + if (strncmp(mime_mate_copied_files, mime, sizeof(mime_mate_copied_files)) == 0) + return TRUE; + return FALSE; +} + +static BOOL wlf_mime_is_text(const char* mime) +{ + for (size_t x = 0; x < ARRAYSIZE(mime_text); x++) + { + if (strcmp(mime, mime_text[x]) == 0) + return TRUE; + } + + return FALSE; +} + +static BOOL wlf_mime_is_image(const char* mime) +{ + for (size_t x = 0; x < ARRAYSIZE(mime_image); x++) + { + if (strcmp(mime, mime_image[x]) == 0) + return TRUE; + } + + return FALSE; +} + +static BOOL wlf_mime_is_html(const char* mime) +{ + if (strcmp(mime, mime_html) == 0) + return TRUE; + + return FALSE; +} + +static void wlf_cliprdr_free_server_formats(wfClipboard* clipboard) +{ + if (clipboard && clipboard->serverFormats) + { + for (size_t j = 0; j < clipboard->numServerFormats; j++) + { + CLIPRDR_FORMAT* format = &clipboard->serverFormats[j]; + free(format->formatName); + } + + free(clipboard->serverFormats); + clipboard->serverFormats = NULL; + clipboard->numServerFormats = 0; + } + + if (clipboard) + UwacClipboardOfferDestroy(clipboard->seat); +} + +static void wlf_cliprdr_free_client_formats(wfClipboard* clipboard) +{ + if (clipboard && clipboard->numClientFormats) + { + for (size_t j = 0; j < clipboard->numClientFormats; j++) + { + CLIPRDR_FORMAT* format = &clipboard->clientFormats[j]; + free(format->formatName); + } + + free(clipboard->clientFormats); + clipboard->clientFormats = NULL; + clipboard->numClientFormats = 0; + } + + if (clipboard) + UwacClipboardOfferDestroy(clipboard->seat); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wlf_cliprdr_send_client_format_list(wfClipboard* clipboard) +{ + WINPR_ASSERT(clipboard); + + const CLIPRDR_FORMAT_LIST formatList = { .common.msgFlags = CB_RESPONSE_OK, + .numFormats = (UINT32)clipboard->numClientFormats, + .formats = clipboard->clientFormats, + .common.msgType = CB_FORMAT_LIST }; + + cliprdr_file_context_clear(clipboard->file); + + WLog_VRB(TAG, "-------------- client format list [%" PRIu32 "] ------------------", + formatList.numFormats); + for (UINT32 x = 0; x < formatList.numFormats; x++) + { + const CLIPRDR_FORMAT* format = &formatList.formats[x]; + WLog_VRB(TAG, "client announces %" PRIu32 " [%s][%s]", format->formatId, + ClipboardGetFormatIdString(format->formatId), format->formatName); + } + WINPR_ASSERT(clipboard->context); + WINPR_ASSERT(clipboard->context->ClientFormatList); + return clipboard->context->ClientFormatList(clipboard->context, &formatList); +} + +static void wfl_cliprdr_add_client_format_id(wfClipboard* clipboard, UINT32 formatId) +{ + CLIPRDR_FORMAT* format = NULL; + const char* name = ClipboardGetFormatName(clipboard->system, formatId); + + for (size_t x = 0; x < clipboard->numClientFormats; x++) + { + format = &clipboard->clientFormats[x]; + + if (format->formatId == formatId) + return; + } + + format = realloc(clipboard->clientFormats, + (clipboard->numClientFormats + 1) * sizeof(CLIPRDR_FORMAT)); + + if (!format) + return; + + clipboard->clientFormats = format; + format = &clipboard->clientFormats[clipboard->numClientFormats++]; + format->formatId = formatId; + format->formatName = NULL; + + if (name && (formatId >= CF_MAX)) + format->formatName = _strdup(name); +} + +static BOOL wlf_cliprdr_add_client_format(wfClipboard* clipboard, const char* mime) +{ + WINPR_ASSERT(mime); + ClipboardLock(clipboard->system); + if (wlf_mime_is_html(mime)) + { + UINT32 formatId = ClipboardGetFormatId(clipboard->system, type_HtmlFormat); + wfl_cliprdr_add_client_format_id(clipboard, formatId); + } + else if (wlf_mime_is_text(mime)) + { + wfl_cliprdr_add_client_format_id(clipboard, CF_TEXT); + wfl_cliprdr_add_client_format_id(clipboard, CF_OEMTEXT); + wfl_cliprdr_add_client_format_id(clipboard, CF_UNICODETEXT); + } + else if (wlf_mime_is_image(mime)) + { + for (size_t x = 0; x < ARRAYSIZE(mime_image); x++) + { + const char* mime_bmp = mime_image[x]; + UINT32 formatId = ClipboardGetFormatId(clipboard->system, mime_bmp); + if (formatId != 0) + wfl_cliprdr_add_client_format_id(clipboard, formatId); + } + wfl_cliprdr_add_client_format_id(clipboard, CF_DIB); + wfl_cliprdr_add_client_format_id(clipboard, CF_TIFF); + } + else if (wlf_mime_is_file(mime)) + { + const UINT32 fileFormatId = + ClipboardGetFormatId(clipboard->system, type_FileGroupDescriptorW); + wfl_cliprdr_add_client_format_id(clipboard, fileFormatId); + } + + ClipboardUnlock(clipboard->system); + if (wlf_cliprdr_send_client_format_list(clipboard) != CHANNEL_RC_OK) + return FALSE; + return TRUE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wlf_cliprdr_send_data_request(wfClipboard* clipboard, const wlf_request* rq) +{ + WINPR_ASSERT(rq); + + CLIPRDR_FORMAT_DATA_REQUEST request = { .requestedFormatId = rq->responseFormat }; + + if (!Queue_Enqueue(clipboard->request_queue, rq)) + return ERROR_INTERNAL_ERROR; + + WINPR_ASSERT(clipboard); + WINPR_ASSERT(clipboard->context); + WINPR_ASSERT(clipboard->context->ClientFormatDataRequest); + return clipboard->context->ClientFormatDataRequest(clipboard->context, &request); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wlf_cliprdr_send_data_response(wfClipboard* clipboard, const BYTE* data, size_t size) +{ + CLIPRDR_FORMAT_DATA_RESPONSE response = { 0 }; + + if (size > UINT32_MAX) + return ERROR_INVALID_PARAMETER; + + response.common.msgFlags = (data) ? CB_RESPONSE_OK : CB_RESPONSE_FAIL; + response.common.dataLen = (UINT32)size; + response.requestedFormatData = data; + + WINPR_ASSERT(clipboard); + WINPR_ASSERT(clipboard->context); + WINPR_ASSERT(clipboard->context->ClientFormatDataResponse); + return clipboard->context->ClientFormatDataResponse(clipboard->context, &response); +} + +BOOL wlf_cliprdr_handle_event(wfClipboard* clipboard, const UwacClipboardEvent* event) +{ + if (!clipboard || !event) + return FALSE; + + if (!clipboard->context) + return TRUE; + + switch (event->type) + { + case UWAC_EVENT_CLIPBOARD_AVAILABLE: + clipboard->seat = event->seat; + return TRUE; + + case UWAC_EVENT_CLIPBOARD_OFFER: + WLog_Print(clipboard->log, WLOG_DEBUG, "client announces mime %s", event->mime); + return wlf_cliprdr_add_client_format(clipboard, event->mime); + + case UWAC_EVENT_CLIPBOARD_SELECT: + WLog_Print(clipboard->log, WLOG_DEBUG, "client announces new data"); + wlf_cliprdr_free_client_formats(clipboard); + return TRUE; + + default: + return FALSE; + } +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wlf_cliprdr_send_client_capabilities(wfClipboard* clipboard) +{ + WINPR_ASSERT(clipboard); + + CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet = { + .capabilitySetType = CB_CAPSTYPE_GENERAL, + .capabilitySetLength = 12, + .version = CB_CAPS_VERSION_2, + .generalFlags = + CB_USE_LONG_FORMAT_NAMES | cliprdr_file_context_current_flags(clipboard->file) + }; + CLIPRDR_CAPABILITIES capabilities = { .cCapabilitiesSets = 1, + .capabilitySets = + (CLIPRDR_CAPABILITY_SET*)&(generalCapabilitySet) }; + + WINPR_ASSERT(clipboard); + WINPR_ASSERT(clipboard->context); + WINPR_ASSERT(clipboard->context->ClientCapabilities); + return clipboard->context->ClientCapabilities(clipboard->context, &capabilities); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wlf_cliprdr_send_client_format_list_response(wfClipboard* clipboard, BOOL status) +{ + const CLIPRDR_FORMAT_LIST_RESPONSE formatListResponse = { + .common.msgType = CB_FORMAT_LIST_RESPONSE, + .common.msgFlags = status ? CB_RESPONSE_OK : CB_RESPONSE_FAIL, + .common.dataLen = 0 + }; + WINPR_ASSERT(clipboard); + WINPR_ASSERT(clipboard->context); + WINPR_ASSERT(clipboard->context->ClientFormatListResponse); + return clipboard->context->ClientFormatListResponse(clipboard->context, &formatListResponse); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wlf_cliprdr_monitor_ready(CliprdrClientContext* context, + const CLIPRDR_MONITOR_READY* monitorReady) +{ + UINT ret = 0; + + WINPR_UNUSED(monitorReady); + WINPR_ASSERT(context); + WINPR_ASSERT(monitorReady); + + wfClipboard* clipboard = cliprdr_file_context_get_context(context->custom); + WINPR_ASSERT(clipboard); + + if ((ret = wlf_cliprdr_send_client_capabilities(clipboard)) != CHANNEL_RC_OK) + return ret; + + if ((ret = wlf_cliprdr_send_client_format_list(clipboard)) != CHANNEL_RC_OK) + return ret; + + clipboard->sync = TRUE; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wlf_cliprdr_server_capabilities(CliprdrClientContext* context, + const CLIPRDR_CAPABILITIES* capabilities) +{ + WINPR_ASSERT(context); + WINPR_ASSERT(capabilities); + + const BYTE* capsPtr = (const BYTE*)capabilities->capabilitySets; + WINPR_ASSERT(capsPtr); + + wfClipboard* clipboard = cliprdr_file_context_get_context(context->custom); + WINPR_ASSERT(clipboard); + + if (!cliprdr_file_context_remote_set_flags(clipboard->file, 0)) + return ERROR_INTERNAL_ERROR; + + for (UINT32 i = 0; i < capabilities->cCapabilitiesSets; i++) + { + const CLIPRDR_CAPABILITY_SET* caps = (const CLIPRDR_CAPABILITY_SET*)capsPtr; + + if (caps->capabilitySetType == CB_CAPSTYPE_GENERAL) + { + const CLIPRDR_GENERAL_CAPABILITY_SET* generalCaps = + (const CLIPRDR_GENERAL_CAPABILITY_SET*)caps; + + if (!cliprdr_file_context_remote_set_flags(clipboard->file, generalCaps->generalFlags)) + return ERROR_INTERNAL_ERROR; + } + + capsPtr += caps->capabilitySetLength; + } + + return CHANNEL_RC_OK; +} + +static UINT32 wlf_get_server_format_id(const wfClipboard* clipboard, const char* name) +{ + WINPR_ASSERT(clipboard); + WINPR_ASSERT(name); + + for (UINT32 x = 0; x < clipboard->numServerFormats; x++) + { + const CLIPRDR_FORMAT* format = &clipboard->serverFormats[x]; + if (!format->formatName) + continue; + if (strcmp(name, format->formatName) == 0) + return format->formatId; + } + return 0; +} + +static const char* wlf_get_server_format_name(const wfClipboard* clipboard, UINT32 formatId) +{ + WINPR_ASSERT(clipboard); + + for (UINT32 x = 0; x < clipboard->numServerFormats; x++) + { + const CLIPRDR_FORMAT* format = &clipboard->serverFormats[x]; + if (format->formatId == formatId) + return format->formatName; + } + return NULL; +} + +static void wlf_cliprdr_transfer_data(UwacSeat* seat, void* context, const char* mime, int fd) +{ + wfClipboard* clipboard = (wfClipboard*)context; + WINPR_UNUSED(seat); + + EnterCriticalSection(&clipboard->lock); + + wlf_request request = { 0 }; + if (wlf_mime_is_html(mime)) + { + request.responseMime = mime_html; + request.responseFormat = wlf_get_server_format_id(clipboard, type_HtmlFormat); + } + else if (wlf_mime_is_file(mime)) + { + request.responseMime = mime; + request.responseFormat = wlf_get_server_format_id(clipboard, type_FileGroupDescriptorW); + } + else if (wlf_mime_is_text(mime)) + { + request.responseMime = mime_text_plain; + request.responseFormat = CF_UNICODETEXT; + } + else if (wlf_mime_is_image(mime)) + { + request.responseMime = mime; + if (strcmp(mime, mime_tiff) == 0) + request.responseFormat = CF_TIFF; + else + request.responseFormat = CF_DIB; + } + + if (request.responseMime != NULL) + { + request.responseFile = fdopen(fd, "w"); + + if (request.responseFile) + wlf_cliprdr_send_data_request(clipboard, &request); + else + WLog_Print(clipboard->log, WLOG_ERROR, + "failed to open clipboard file descriptor for MIME %s", + request.responseMime); + } + + LeaveCriticalSection(&clipboard->lock); +} + +static void wlf_cliprdr_cancel_data(UwacSeat* seat, void* context) +{ + wfClipboard* clipboard = (wfClipboard*)context; + + WINPR_UNUSED(seat); + WINPR_ASSERT(clipboard); + cliprdr_file_context_clear(clipboard->file); +} + +/** + * Called when the clipboard changes server side. + * + * Clear the local clipboard offer and replace it with a new one + * that announces the formats we get listed here. + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wlf_cliprdr_server_format_list(CliprdrClientContext* context, + const CLIPRDR_FORMAT_LIST* formatList) +{ + BOOL html = FALSE; + BOOL text = FALSE; + BOOL image = FALSE; + BOOL file = FALSE; + + if (!context || !context->custom) + return ERROR_INVALID_PARAMETER; + + wfClipboard* clipboard = cliprdr_file_context_get_context(context->custom); + WINPR_ASSERT(clipboard); + + wlf_cliprdr_free_server_formats(clipboard); + cliprdr_file_context_clear(clipboard->file); + + if (!(clipboard->serverFormats = + (CLIPRDR_FORMAT*)calloc(formatList->numFormats, sizeof(CLIPRDR_FORMAT)))) + { + WLog_Print(clipboard->log, WLOG_ERROR, + "failed to allocate %" PRIuz " CLIPRDR_FORMAT structs", + clipboard->numServerFormats); + return CHANNEL_RC_NO_MEMORY; + } + + clipboard->numServerFormats = formatList->numFormats; + + if (!clipboard->seat) + { + WLog_Print(clipboard->log, WLOG_ERROR, + "clipboard->seat=NULL, check your client implementation"); + return ERROR_INTERNAL_ERROR; + } + + for (UINT32 i = 0; i < formatList->numFormats; i++) + { + const CLIPRDR_FORMAT* format = &formatList->formats[i]; + CLIPRDR_FORMAT* srvFormat = &clipboard->serverFormats[i]; + srvFormat->formatId = format->formatId; + + if (format->formatName) + { + srvFormat->formatName = _strdup(format->formatName); + + if (!srvFormat->formatName) + { + wlf_cliprdr_free_server_formats(clipboard); + return CHANNEL_RC_NO_MEMORY; + } + } + + if (format->formatName) + { + if (strcmp(format->formatName, type_HtmlFormat) == 0) + { + text = TRUE; + html = TRUE; + } + else if (strcmp(format->formatName, type_FileGroupDescriptorW) == 0) + { + file = TRUE; + text = TRUE; + } + } + else + { + switch (format->formatId) + { + case CF_TEXT: + case CF_OEMTEXT: + case CF_UNICODETEXT: + text = TRUE; + break; + + case CF_DIB: + image = TRUE; + break; + + default: + break; + } + } + } + + if (html) + { + UwacClipboardOfferCreate(clipboard->seat, mime_html); + } + + if (file && cliprdr_file_context_has_local_support(clipboard->file)) + { + UwacClipboardOfferCreate(clipboard->seat, mime_uri_list); + UwacClipboardOfferCreate(clipboard->seat, mime_gnome_copied_files); + UwacClipboardOfferCreate(clipboard->seat, mime_mate_copied_files); + } + + if (text) + { + for (size_t x = 0; x < ARRAYSIZE(mime_text); x++) + UwacClipboardOfferCreate(clipboard->seat, mime_text[x]); + } + + if (image) + { + for (size_t x = 0; x < ARRAYSIZE(mime_image); x++) + UwacClipboardOfferCreate(clipboard->seat, mime_image[x]); + } + + UwacClipboardOfferAnnounce(clipboard->seat, clipboard, wlf_cliprdr_transfer_data, + wlf_cliprdr_cancel_data); + return wlf_cliprdr_send_client_format_list_response(clipboard, TRUE); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wlf_cliprdr_server_format_list_response(CliprdrClientContext* context, + const CLIPRDR_FORMAT_LIST_RESPONSE* formatListResponse) +{ + WINPR_ASSERT(context); + WINPR_ASSERT(formatListResponse); + + if (formatListResponse->common.msgFlags & CB_RESPONSE_FAIL) + WLog_WARN(TAG, "format list update failed"); + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wlf_cliprdr_server_format_data_request(CliprdrClientContext* context, + const CLIPRDR_FORMAT_DATA_REQUEST* formatDataRequest) +{ + UINT rc = CHANNEL_RC_OK; + BYTE* data = NULL; + size_t size = 0; + const char* mime = NULL; + UINT32 formatId = 0; + UINT32 localFormatId = 0; + wfClipboard* clipboard = 0; + + UINT32 dsize = 0; + BYTE* ddata = NULL; + + WINPR_ASSERT(context); + WINPR_ASSERT(formatDataRequest); + + localFormatId = formatId = formatDataRequest->requestedFormatId; + clipboard = cliprdr_file_context_get_context(context->custom); + WINPR_ASSERT(clipboard); + + ClipboardLock(clipboard->system); + const UINT32 fileFormatId = ClipboardGetFormatId(clipboard->system, type_FileGroupDescriptorW); + const UINT32 htmlFormatId = ClipboardGetFormatId(clipboard->system, type_HtmlFormat); + + switch (formatId) + { + case CF_TEXT: + case CF_OEMTEXT: + case CF_UNICODETEXT: + localFormatId = ClipboardGetFormatId(clipboard->system, mime_text_plain); + mime = mime_text_utf8; + break; + + case CF_DIB: + case CF_DIBV5: + mime = mime_bitmap[0]; + break; + + case CF_TIFF: + mime = mime_tiff; + break; + + default: + if (formatId == fileFormatId) + { + localFormatId = ClipboardGetFormatId(clipboard->system, mime_uri_list); + mime = mime_uri_list; + } + else if (formatId == htmlFormatId) + { + localFormatId = ClipboardGetFormatId(clipboard->system, mime_html); + mime = mime_html; + } + else + goto fail; + break; + } + + data = UwacClipboardDataGet(clipboard->seat, mime, &size); + + if (!data) + goto fail; + + if (fileFormatId == formatId) + { + if (!cliprdr_file_context_update_client_data(clipboard->file, data, size)) + goto fail; + } + + const BOOL res = ClipboardSetData(clipboard->system, localFormatId, data, size); + free(data); + + UINT32 len = 0; + data = NULL; + if (res) + data = ClipboardGetData(clipboard->system, formatId, &len); + + if (!res || !data) + goto fail; + + if (fileFormatId == formatId) + { + const UINT32 flags = cliprdr_file_context_remote_get_flags(clipboard->file); + const UINT32 error = cliprdr_serialize_file_list_ex( + flags, (const FILEDESCRIPTORW*)data, len / sizeof(FILEDESCRIPTORW), &ddata, &dsize); + if (error) + goto fail; + } +fail: + ClipboardUnlock(clipboard->system); + rc = wlf_cliprdr_send_data_response(clipboard, ddata, dsize); + free(data); + return rc; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wlf_cliprdr_server_format_data_response(CliprdrClientContext* context, + const CLIPRDR_FORMAT_DATA_RESPONSE* formatDataResponse) +{ + UINT rc = ERROR_INTERNAL_ERROR; + + WINPR_ASSERT(context); + WINPR_ASSERT(formatDataResponse); + + const UINT32 size = formatDataResponse->common.dataLen; + const BYTE* data = formatDataResponse->requestedFormatData; + + wfClipboard* clipboard = cliprdr_file_context_get_context(context->custom); + WINPR_ASSERT(clipboard); + + wlf_request* request = Queue_Dequeue(clipboard->request_queue); + if (!request) + goto fail; + + rc = CHANNEL_RC_OK; + if (formatDataResponse->common.msgFlags & CB_RESPONSE_FAIL) + { + WLog_WARN(TAG, "clipboard data request for format %" PRIu32 " [%s], mime %s failed", + request->responseFormat, ClipboardGetFormatIdString(request->responseFormat), + request->responseMime); + goto fail; + } + rc = ERROR_INTERNAL_ERROR; + + ClipboardLock(clipboard->system); + EnterCriticalSection(&clipboard->lock); + + UINT32 srcFormatId = 0; + UINT32 dstFormatId = 0; + switch (request->responseFormat) + { + case CF_TEXT: + case CF_OEMTEXT: + case CF_UNICODETEXT: + srcFormatId = request->responseFormat; + dstFormatId = ClipboardGetFormatId(clipboard->system, request->responseMime); + break; + + case CF_DIB: + case CF_DIBV5: + srcFormatId = request->responseFormat; + dstFormatId = ClipboardGetFormatId(clipboard->system, request->responseMime); + break; + + default: + { + const char* name = wlf_get_server_format_name(clipboard, request->responseFormat); + if (name) + { + if (strcmp(type_FileGroupDescriptorW, name) == 0) + { + srcFormatId = + ClipboardGetFormatId(clipboard->system, type_FileGroupDescriptorW); + dstFormatId = ClipboardGetFormatId(clipboard->system, request->responseMime); + + if (!cliprdr_file_context_update_server_data(clipboard->file, clipboard->system, + data, size)) + goto unlock; + } + else if (strcmp(type_HtmlFormat, name) == 0) + { + srcFormatId = ClipboardGetFormatId(clipboard->system, type_HtmlFormat); + dstFormatId = ClipboardGetFormatId(clipboard->system, request->responseMime); + } + } + } + break; + } + + UINT32 len = 0; + + const BOOL sres = ClipboardSetData(clipboard->system, srcFormatId, data, size); + if (sres) + data = ClipboardGetData(clipboard->system, dstFormatId, &len); + + if (!sres || !data) + goto unlock; + + if (request->responseFile) + { + const size_t res = fwrite(data, 1, len, request->responseFile); + if (res == len) + rc = CHANNEL_RC_OK; + } + else + rc = CHANNEL_RC_OK; + +unlock: + ClipboardUnlock(clipboard->system); + LeaveCriticalSection(&clipboard->lock); +fail: + wlf_request_free(request); + return rc; +} + +wfClipboard* wlf_clipboard_new(wlfContext* wfc) +{ + rdpChannels* channels = NULL; + wfClipboard* clipboard = NULL; + + WINPR_ASSERT(wfc); + + clipboard = (wfClipboard*)calloc(1, sizeof(wfClipboard)); + + if (!clipboard) + goto fail; + + InitializeCriticalSection(&clipboard->lock); + clipboard->wfc = wfc; + channels = wfc->common.context.channels; + clipboard->log = WLog_Get(TAG); + clipboard->channels = channels; + clipboard->system = ClipboardCreate(); + if (!clipboard->system) + goto fail; + + clipboard->file = cliprdr_file_context_new(clipboard); + if (!clipboard->file) + goto fail; + + if (!cliprdr_file_context_set_locally_available(clipboard->file, TRUE)) + goto fail; + + clipboard->request_queue = Queue_New(TRUE, -1, -1); + if (!clipboard->request_queue) + goto fail; + + wObject* obj = Queue_Object(clipboard->request_queue); + WINPR_ASSERT(obj); + obj->fnObjectFree = wlf_request_free; + obj->fnObjectNew = wlf_request_clone; + + return clipboard; + +fail: + wlf_clipboard_free(clipboard); + return NULL; +} + +void wlf_clipboard_free(wfClipboard* clipboard) +{ + if (!clipboard) + return; + + cliprdr_file_context_free(clipboard->file); + + wlf_cliprdr_free_server_formats(clipboard); + wlf_cliprdr_free_client_formats(clipboard); + ClipboardDestroy(clipboard->system); + + EnterCriticalSection(&clipboard->lock); + + Queue_Free(clipboard->request_queue); + LeaveCriticalSection(&clipboard->lock); + DeleteCriticalSection(&clipboard->lock); + free(clipboard); +} + +BOOL wlf_cliprdr_init(wfClipboard* clipboard, CliprdrClientContext* cliprdr) +{ + WINPR_ASSERT(clipboard); + WINPR_ASSERT(cliprdr); + + clipboard->context = cliprdr; + cliprdr->MonitorReady = wlf_cliprdr_monitor_ready; + cliprdr->ServerCapabilities = wlf_cliprdr_server_capabilities; + cliprdr->ServerFormatList = wlf_cliprdr_server_format_list; + cliprdr->ServerFormatListResponse = wlf_cliprdr_server_format_list_response; + cliprdr->ServerFormatDataRequest = wlf_cliprdr_server_format_data_request; + cliprdr->ServerFormatDataResponse = wlf_cliprdr_server_format_data_response; + + return cliprdr_file_context_init(clipboard->file, cliprdr); +} + +BOOL wlf_cliprdr_uninit(wfClipboard* clipboard, CliprdrClientContext* cliprdr) +{ + WINPR_ASSERT(clipboard); + if (!cliprdr_file_context_uninit(clipboard->file, cliprdr)) + return FALSE; + + if (cliprdr) + cliprdr->custom = NULL; + + return TRUE; +} diff --git a/client/Wayland/wlf_cliprdr.h b/client/Wayland/wlf_cliprdr.h new file mode 100644 index 0000000..a113140 --- /dev/null +++ b/client/Wayland/wlf_cliprdr.h @@ -0,0 +1,36 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Clipboard Redirection + * + * Copyright 2018 Armin Novak <armin.novak@thincast.com> + * Copyright 2018 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_WAYLAND_CLIPRDR_H +#define FREERDP_CLIENT_WAYLAND_CLIPRDR_H + +#include "wlfreerdp.h" + +#include <freerdp/client/cliprdr.h> + +wfClipboard* wlf_clipboard_new(wlfContext* wlc); +void wlf_clipboard_free(wfClipboard* clipboard); + +BOOL wlf_cliprdr_init(wfClipboard* clipboard, CliprdrClientContext* cliprdr); +BOOL wlf_cliprdr_uninit(wfClipboard* clipboard, CliprdrClientContext* cliprdr); + +BOOL wlf_cliprdr_handle_event(wfClipboard* clipboard, const UwacClipboardEvent* event); + +#endif /* FREERDP_CLIENT_WAYLAND_CLIPRDR_H */ diff --git a/client/Wayland/wlf_disp.c b/client/Wayland/wlf_disp.c new file mode 100644 index 0000000..0d87675 --- /dev/null +++ b/client/Wayland/wlf_disp.c @@ -0,0 +1,455 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Display Control Channel + * + * Copyright 2018 Armin Novak <armin.novak@thincast.com> + * Copyright 2018 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <winpr/sysinfo.h> + +#include "wlf_disp.h" + +#define TAG CLIENT_TAG("wayland.disp") + +#define RESIZE_MIN_DELAY 200 /* minimum delay in ms between two resizes */ + +struct s_wlfDispContext +{ + wlfContext* wlc; + DispClientContext* disp; + BOOL haveXRandr; + int eventBase, errorBase; + int lastSentWidth, lastSentHeight; + UINT64 lastSentDate; + int targetWidth, targetHeight; + BOOL activated; + BOOL waitingResize; + BOOL fullscreen; + UINT16 lastSentDesktopOrientation; + UINT32 lastSentDesktopScaleFactor; + UINT32 lastSentDeviceScaleFactor; +}; + +static UINT wlf_disp_sendLayout(DispClientContext* disp, const rdpMonitor* monitors, + size_t nmonitors); + +static BOOL wlf_disp_settings_changed(wlfDispContext* wlfDisp) +{ + rdpSettings* settings = NULL; + + WINPR_ASSERT(wlfDisp); + WINPR_ASSERT(wlfDisp->wlc); + + settings = wlfDisp->wlc->common.context.settings; + WINPR_ASSERT(settings); + + if (wlfDisp->lastSentWidth != wlfDisp->targetWidth) + return TRUE; + + if (wlfDisp->lastSentHeight != wlfDisp->targetHeight) + return TRUE; + + if (wlfDisp->lastSentDesktopOrientation != + freerdp_settings_get_uint16(settings, FreeRDP_DesktopOrientation)) + return TRUE; + + if (wlfDisp->lastSentDesktopScaleFactor != + freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor)) + return TRUE; + + if (wlfDisp->lastSentDeviceScaleFactor != + freerdp_settings_get_uint32(settings, FreeRDP_DeviceScaleFactor)) + return TRUE; + + if (wlfDisp->fullscreen != wlfDisp->wlc->fullscreen) + return TRUE; + + return FALSE; +} + +static BOOL wlf_update_last_sent(wlfDispContext* wlfDisp) +{ + rdpSettings* settings = NULL; + + WINPR_ASSERT(wlfDisp); + WINPR_ASSERT(wlfDisp->wlc); + + settings = wlfDisp->wlc->common.context.settings; + WINPR_ASSERT(settings); + + wlfDisp->lastSentWidth = wlfDisp->targetWidth; + wlfDisp->lastSentHeight = wlfDisp->targetHeight; + wlfDisp->lastSentDesktopOrientation = + freerdp_settings_get_uint16(settings, FreeRDP_DesktopOrientation); + wlfDisp->lastSentDesktopScaleFactor = + freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor); + wlfDisp->lastSentDeviceScaleFactor = + freerdp_settings_get_uint32(settings, FreeRDP_DeviceScaleFactor); + wlfDisp->fullscreen = wlfDisp->wlc->fullscreen; + return TRUE; +} + +static BOOL wlf_disp_sendResize(wlfDispContext* wlfDisp) +{ + DISPLAY_CONTROL_MONITOR_LAYOUT layout; + wlfContext* wlc = NULL; + rdpSettings* settings = NULL; + + if (!wlfDisp || !wlfDisp->wlc) + return FALSE; + + wlc = wlfDisp->wlc; + settings = wlc->common.context.settings; + + if (!settings) + return FALSE; + + if (!wlfDisp->activated || !wlfDisp->disp) + return TRUE; + + if (GetTickCount64() - wlfDisp->lastSentDate < RESIZE_MIN_DELAY) + return TRUE; + + wlfDisp->lastSentDate = GetTickCount64(); + + if (!wlf_disp_settings_changed(wlfDisp)) + return TRUE; + + /* TODO: Multimonitor support for wayland + if (wlc->fullscreen && (freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount > 0)) + { + if (wlf_disp_sendLayout(wlfDisp->disp, setings->MonitorDefArray, + freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount) != + CHANNEL_RC_OK) return FALSE; + } + else + */ + { + wlfDisp->waitingResize = TRUE; + layout.Flags = DISPLAY_CONTROL_MONITOR_PRIMARY; + layout.Top = layout.Left = 0; + layout.Width = wlfDisp->targetWidth; + layout.Height = wlfDisp->targetHeight; + layout.Orientation = freerdp_settings_get_uint16(settings, FreeRDP_DesktopOrientation); + layout.DesktopScaleFactor = + freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor); + layout.DeviceScaleFactor = freerdp_settings_get_uint32(settings, FreeRDP_DeviceScaleFactor); + layout.PhysicalWidth = wlfDisp->targetWidth; + layout.PhysicalHeight = wlfDisp->targetHeight; + + if (IFCALLRESULT(CHANNEL_RC_OK, wlfDisp->disp->SendMonitorLayout, wlfDisp->disp, 1, + &layout) != CHANNEL_RC_OK) + return FALSE; + } + return wlf_update_last_sent(wlfDisp); +} + +static BOOL wlf_disp_set_window_resizable(wlfDispContext* wlfDisp) +{ +#if 0 // TODO +#endif + return TRUE; +} + +static BOOL wlf_disp_check_context(void* context, wlfContext** ppwlc, wlfDispContext** ppwlfDisp, + rdpSettings** ppSettings) +{ + wlfContext* wlc = NULL; + + if (!context) + return FALSE; + + wlc = (wlfContext*)context; + + if (!(wlc->disp)) + return FALSE; + + if (!wlc->common.context.settings) + return FALSE; + + *ppwlc = wlc; + *ppwlfDisp = wlc->disp; + *ppSettings = wlc->common.context.settings; + return TRUE; +} + +static void wlf_disp_OnActivated(void* context, const ActivatedEventArgs* e) +{ + wlfContext* wlc = NULL; + wlfDispContext* wlfDisp = NULL; + rdpSettings* settings = NULL; + + if (!wlf_disp_check_context(context, &wlc, &wlfDisp, &settings)) + return; + + wlfDisp->waitingResize = FALSE; + + if (wlfDisp->activated && !freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)) + { + wlf_disp_set_window_resizable(wlfDisp); + + if (e->firstActivation) + return; + + wlf_disp_sendResize(wlfDisp); + } +} + +static void wlf_disp_OnGraphicsReset(void* context, const GraphicsResetEventArgs* e) +{ + wlfContext* wlc = NULL; + wlfDispContext* wlfDisp = NULL; + rdpSettings* settings = NULL; + + WINPR_UNUSED(e); + if (!wlf_disp_check_context(context, &wlc, &wlfDisp, &settings)) + return; + + wlfDisp->waitingResize = FALSE; + + if (wlfDisp->activated && !freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)) + { + wlf_disp_set_window_resizable(wlfDisp); + wlf_disp_sendResize(wlfDisp); + } +} + +static void wlf_disp_OnTimer(void* context, const TimerEventArgs* e) +{ + wlfContext* wlc = NULL; + wlfDispContext* wlfDisp = NULL; + rdpSettings* settings = NULL; + + WINPR_UNUSED(e); + if (!wlf_disp_check_context(context, &wlc, &wlfDisp, &settings)) + return; + + if (!wlfDisp->activated || freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)) + return; + + wlf_disp_sendResize(wlfDisp); +} + +wlfDispContext* wlf_disp_new(wlfContext* wlc) +{ + wlfDispContext* ret = NULL; + wPubSub* pubSub = NULL; + rdpSettings* settings = NULL; + + if (!wlc || !wlc->common.context.settings || !wlc->common.context.pubSub) + return NULL; + + settings = wlc->common.context.settings; + pubSub = wlc->common.context.pubSub; + ret = calloc(1, sizeof(wlfDispContext)); + + if (!ret) + return NULL; + + ret->wlc = wlc; + ret->lastSentWidth = ret->targetWidth = + freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth); + ret->lastSentHeight = ret->targetHeight = + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight); + PubSub_SubscribeActivated(pubSub, wlf_disp_OnActivated); + PubSub_SubscribeGraphicsReset(pubSub, wlf_disp_OnGraphicsReset); + PubSub_SubscribeTimer(pubSub, wlf_disp_OnTimer); + return ret; +} + +void wlf_disp_free(wlfDispContext* disp) +{ + if (!disp) + return; + + if (disp->wlc) + { + wPubSub* pubSub = disp->wlc->common.context.pubSub; + PubSub_UnsubscribeActivated(pubSub, wlf_disp_OnActivated); + PubSub_UnsubscribeGraphicsReset(pubSub, wlf_disp_OnGraphicsReset); + PubSub_UnsubscribeTimer(pubSub, wlf_disp_OnTimer); + } + + free(disp); +} + +UINT wlf_disp_sendLayout(DispClientContext* disp, const rdpMonitor* monitors, size_t nmonitors) +{ + UINT ret = CHANNEL_RC_OK; + DISPLAY_CONTROL_MONITOR_LAYOUT* layouts = NULL; + wlfDispContext* wlfDisp = NULL; + rdpSettings* settings = NULL; + + WINPR_ASSERT(disp); + WINPR_ASSERT(monitors); + WINPR_ASSERT(nmonitors > 0); + + wlfDisp = (wlfDispContext*)disp->custom; + WINPR_ASSERT(wlfDisp); + WINPR_ASSERT(wlfDisp->wlc); + + settings = wlfDisp->wlc->common.context.settings; + WINPR_ASSERT(settings); + + layouts = calloc(nmonitors, sizeof(DISPLAY_CONTROL_MONITOR_LAYOUT)); + + if (!layouts) + return CHANNEL_RC_NO_MEMORY; + + for (size_t i = 0; i < nmonitors; i++) + { + const rdpMonitor* monitor = &monitors[i]; + DISPLAY_CONTROL_MONITOR_LAYOUT* layout = &layouts[i]; + + layout->Flags = (monitor->is_primary ? DISPLAY_CONTROL_MONITOR_PRIMARY : 0); + layout->Left = monitor->x; + layout->Top = monitor->y; + layout->Width = monitor->width; + layout->Height = monitor->height; + layout->Orientation = ORIENTATION_LANDSCAPE; + layout->PhysicalWidth = monitor->attributes.physicalWidth; + layout->PhysicalHeight = monitor->attributes.physicalHeight; + + switch (monitor->attributes.orientation) + { + case 90: + layout->Orientation = ORIENTATION_PORTRAIT; + break; + + case 180: + layout->Orientation = ORIENTATION_LANDSCAPE_FLIPPED; + break; + + case 270: + layout->Orientation = ORIENTATION_PORTRAIT_FLIPPED; + break; + + case 0: + default: + /* MS-RDPEDISP - 2.2.2.2.1: + * Orientation (4 bytes): A 32-bit unsigned integer that specifies the + * orientation of the monitor in degrees. Valid values are 0, 90, 180 + * or 270 + * + * So we default to ORIENTATION_LANDSCAPE + */ + layout->Orientation = ORIENTATION_LANDSCAPE; + break; + } + + layout->DesktopScaleFactor = + freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor); + layout->DeviceScaleFactor = + freerdp_settings_get_uint32(settings, FreeRDP_DeviceScaleFactor); + } + + ret = IFCALLRESULT(CHANNEL_RC_OK, disp->SendMonitorLayout, disp, nmonitors, layouts); + free(layouts); + return ret; +} + +BOOL wlf_disp_handle_configure(wlfDispContext* disp, int32_t width, int32_t height) +{ + if (!disp) + return FALSE; + + disp->targetWidth = width; + disp->targetHeight = height; + return wlf_disp_sendResize(disp); +} + +static UINT wlf_DisplayControlCaps(DispClientContext* disp, UINT32 maxNumMonitors, + UINT32 maxMonitorAreaFactorA, UINT32 maxMonitorAreaFactorB) +{ + /* we're called only if dynamic resolution update is activated */ + wlfDispContext* wlfDisp = NULL; + rdpSettings* settings = NULL; + + WINPR_ASSERT(disp); + + wlfDisp = (wlfDispContext*)disp->custom; + WINPR_ASSERT(wlfDisp); + WINPR_ASSERT(wlfDisp->wlc); + + settings = wlfDisp->wlc->common.context.settings; + WINPR_ASSERT(settings); + + WLog_DBG(TAG, + "DisplayControlCapsPdu: MaxNumMonitors: %" PRIu32 " MaxMonitorAreaFactorA: %" PRIu32 + " MaxMonitorAreaFactorB: %" PRIu32 "", + maxNumMonitors, maxMonitorAreaFactorA, maxMonitorAreaFactorB); + wlfDisp->activated = TRUE; + + if (freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)) + return CHANNEL_RC_OK; + + WLog_DBG(TAG, "DisplayControlCapsPdu: setting the window as resizable"); + return wlf_disp_set_window_resizable(wlfDisp) ? CHANNEL_RC_OK : CHANNEL_RC_NO_MEMORY; +} + +BOOL wlf_disp_init(wlfDispContext* wlfDisp, DispClientContext* disp) +{ + rdpSettings* settings = NULL; + + if (!wlfDisp || !wlfDisp->wlc || !disp) + return FALSE; + + settings = wlfDisp->wlc->common.context.settings; + + if (!settings) + return FALSE; + + wlfDisp->disp = disp; + disp->custom = (void*)wlfDisp; + + if (freerdp_settings_get_bool(settings, FreeRDP_DynamicResolutionUpdate)) + { + disp->DisplayControlCaps = wlf_DisplayControlCaps; + } + + return TRUE; +} + +BOOL wlf_disp_uninit(wlfDispContext* wlfDisp, DispClientContext* disp) +{ + if (!wlfDisp || !disp) + return FALSE; + + wlfDisp->disp = NULL; + return TRUE; +} + +int wlf_list_monitors(wlfContext* wlc) +{ + uint32_t nmonitors = UwacDisplayGetNbOutputs(wlc->display); + + for (uint32_t i = 0; i < nmonitors; i++) + { + const UwacOutput* monitor = UwacDisplayGetOutput(wlc->display, i); + UwacSize resolution; + UwacPosition pos; + + if (!monitor) + continue; + UwacOutputGetPosition(monitor, &pos); + UwacOutputGetResolution(monitor, &resolution); + + printf(" %s [%d] %dx%d\t+%d+%d\n", (i == 0) ? "*" : " ", i, resolution.width, + resolution.height, pos.x, pos.y); + } + + return 0; +} diff --git a/client/Wayland/wlf_disp.h b/client/Wayland/wlf_disp.h new file mode 100644 index 0000000..36fa27c --- /dev/null +++ b/client/Wayland/wlf_disp.h @@ -0,0 +1,38 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Display Control Channel + * + * Copyright 2018 Armin Novak <armin.novak@thincast.com> + * Copyright 2018 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef FREERDP_CLIENT_WAYLAND_DISP_H +#define FREERDP_CLIENT_WAYLAND_DISP_H + +#include <freerdp/types.h> +#include <freerdp/client/disp.h> + +#include "wlfreerdp.h" + +FREERDP_API BOOL wlf_disp_init(wlfDispContext* xfDisp, DispClientContext* disp); +FREERDP_API BOOL wlf_disp_uninit(wlfDispContext* xfDisp, DispClientContext* disp); + +wlfDispContext* wlf_disp_new(wlfContext* wlc); +void wlf_disp_free(wlfDispContext* disp); +BOOL wlf_disp_handle_configure(wlfDispContext* disp, int32_t width, int32_t height); +void wlf_disp_resized(wlfDispContext* disp); + +int wlf_list_monitors(wlfContext* wlc); + +#endif /* FREERDP_CLIENT_WAYLAND_DISP_H */ diff --git a/client/Wayland/wlf_input.c b/client/Wayland/wlf_input.c new file mode 100644 index 0000000..60603db --- /dev/null +++ b/client/Wayland/wlf_input.c @@ -0,0 +1,458 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Input + * + * Copyright 2014 Manuel Bachmann <tarnyko@tarnyko.net> + * Copyright 2015 David Fort <contact@hardening-consulting.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <stdlib.h> +#include <float.h> + +#include <linux/input.h> + +#include <winpr/assert.h> + +#include <freerdp/config.h> +#include <freerdp/locale/keyboard.h> +#if defined(CHANNEL_RDPEI_CLIENT) +#include <freerdp/client/rdpei.h> +#endif +#include <uwac/uwac.h> + +#include "wlfreerdp.h" +#include "wlf_input.h" + +#define TAG CLIENT_TAG("wayland.input") + +static BOOL scale_signed_coordinates(rdpContext* context, int32_t* x, int32_t* y, + BOOL fromLocalToRDP) +{ + BOOL rc = 0; + UINT32 ux = 0; + UINT32 uy = 0; + WINPR_ASSERT(context); + WINPR_ASSERT(x); + WINPR_ASSERT(y); + WINPR_ASSERT(*x >= 0); + WINPR_ASSERT(*y >= 0); + + ux = (UINT32)*x; + uy = (UINT32)*y; + rc = wlf_scale_coordinates(context, &ux, &uy, fromLocalToRDP); + WINPR_ASSERT(ux < INT32_MAX); + WINPR_ASSERT(uy < INT32_MAX); + *x = (int32_t)ux; + *y = (int32_t)uy; + return rc; +} + +BOOL wlf_handle_pointer_enter(freerdp* instance, const UwacPointerEnterLeaveEvent* ev) +{ + uint32_t x = 0; + uint32_t y = 0; + rdpClientContext* cctx = NULL; + + if (!instance || !ev) + return FALSE; + + x = ev->x; + y = ev->y; + + if (!wlf_scale_coordinates(instance->context, &x, &y, TRUE)) + return FALSE; + + WINPR_ASSERT(x <= UINT16_MAX); + WINPR_ASSERT(y <= UINT16_MAX); + cctx = (rdpClientContext*)instance->context; + return freerdp_client_send_button_event(cctx, FALSE, PTR_FLAGS_MOVE, x, y); +} + +BOOL wlf_handle_pointer_motion(freerdp* instance, const UwacPointerMotionEvent* ev) +{ + uint32_t x = 0; + uint32_t y = 0; + rdpClientContext* cctx = NULL; + + if (!instance || !ev) + return FALSE; + + cctx = (rdpClientContext*)instance->context; + WINPR_ASSERT(cctx); + + x = ev->x; + y = ev->y; + + if (!wlf_scale_coordinates(instance->context, &x, &y, TRUE)) + return FALSE; + + WINPR_ASSERT(x <= UINT16_MAX); + WINPR_ASSERT(y <= UINT16_MAX); + return freerdp_client_send_button_event(cctx, FALSE, PTR_FLAGS_MOVE, x, y); +} + +BOOL wlf_handle_pointer_buttons(freerdp* instance, const UwacPointerButtonEvent* ev) +{ + rdpClientContext* cctx = NULL; + UINT16 flags = 0; + UINT16 xflags = 0; + uint32_t x = 0; + uint32_t y = 0; + + if (!instance || !ev) + return FALSE; + + cctx = (rdpClientContext*)instance->context; + WINPR_ASSERT(cctx); + + x = ev->x; + y = ev->y; + + if (!wlf_scale_coordinates(instance->context, &x, &y, TRUE)) + return FALSE; + + if (ev->state == WL_POINTER_BUTTON_STATE_PRESSED) + { + flags |= PTR_FLAGS_DOWN; + xflags |= PTR_XFLAGS_DOWN; + } + + switch (ev->button) + { + case BTN_LEFT: + flags |= PTR_FLAGS_BUTTON1; + break; + + case BTN_RIGHT: + flags |= PTR_FLAGS_BUTTON2; + break; + + case BTN_MIDDLE: + flags |= PTR_FLAGS_BUTTON3; + break; + + case BTN_SIDE: + xflags |= PTR_XFLAGS_BUTTON1; + break; + + case BTN_EXTRA: + xflags |= PTR_XFLAGS_BUTTON2; + break; + + default: + return TRUE; + } + + WINPR_ASSERT(x <= UINT16_MAX); + WINPR_ASSERT(y <= UINT16_MAX); + + if ((flags & ~PTR_FLAGS_DOWN) != 0) + return freerdp_client_send_button_event(cctx, FALSE, flags, x, y); + + if ((xflags & ~PTR_XFLAGS_DOWN) != 0) + return freerdp_client_send_extended_button_event(cctx, FALSE, xflags, x, y); + + return FALSE; +} + +BOOL wlf_handle_pointer_axis(freerdp* instance, const UwacPointerAxisEvent* ev) +{ + wlfContext* context = NULL; + if (!instance || !instance->context || !ev) + return FALSE; + + context = (wlfContext*)instance->context; + return ArrayList_Append(context->events, ev); +} + +BOOL wlf_handle_pointer_axis_discrete(freerdp* instance, const UwacPointerAxisEvent* ev) +{ + wlfContext* context = NULL; + if (!instance || !instance->context || !ev) + return FALSE; + + context = (wlfContext*)instance->context; + return ArrayList_Append(context->events, ev); +} + +static BOOL wlf_handle_wheel(freerdp* instance, uint32_t x, uint32_t y, uint32_t axis, + int32_t value) +{ + rdpClientContext* cctx = NULL; + UINT16 flags = 0; + int32_t direction = 0; + uint32_t avalue = (uint32_t)abs(value); + + WINPR_ASSERT(instance); + + cctx = (rdpClientContext*)instance->context; + WINPR_ASSERT(cctx); + + if (!wlf_scale_coordinates(instance->context, &x, &y, TRUE)) + return FALSE; + + direction = value; + switch (axis) + { + case WL_POINTER_AXIS_VERTICAL_SCROLL: + flags |= PTR_FLAGS_WHEEL; + if (direction > 0) + flags |= PTR_FLAGS_WHEEL_NEGATIVE; + break; + + case WL_POINTER_AXIS_HORIZONTAL_SCROLL: + flags |= PTR_FLAGS_HWHEEL; + if (direction < 0) + flags |= PTR_FLAGS_WHEEL_NEGATIVE; + break; + + default: + return FALSE; + } + + /* Wheel rotation steps: + * + * positive: 0 ... 0xFF -> slow ... fast + * negative: 0 ... 0xFF -> fast ... slow + */ + + while (avalue > 0) + { + const UINT16 cval = (avalue > 0xFF) ? 0xFF : (UINT16)avalue; + UINT16 cflags = flags | cval; + /* Convert negative values to 9bit twos complement */ + if (flags & PTR_FLAGS_WHEEL_NEGATIVE) + cflags = (flags & 0xFF00) | (0x100 - cval); + if (!freerdp_client_send_wheel_event(cctx, cflags)) + return FALSE; + + avalue -= cval; + } + return TRUE; +} + +BOOL wlf_handle_pointer_frame(freerdp* instance, const UwacPointerFrameEvent* ev) +{ + BOOL success = TRUE; + BOOL handle = FALSE; + wlfContext* context = NULL; + enum wl_pointer_axis_source source = WL_POINTER_AXIS_SOURCE_CONTINUOUS; + + if (!instance || !ev || !instance->context) + return FALSE; + + context = (wlfContext*)instance->context; + + for (size_t x = 0; x < ArrayList_Count(context->events); x++) + { + UwacEvent* cev = ArrayList_GetItem(context->events, x); + if (!cev) + continue; + if (cev->type == UWAC_EVENT_POINTER_SOURCE) + { + handle = TRUE; + source = cev->mouse_source.axis_source; + } + } + + /* We need source events to determine how to interpret the data */ + if (handle) + { + for (size_t x = 0; x < ArrayList_Count(context->events); x++) + { + UwacEvent* cev = ArrayList_GetItem(context->events, x); + if (!cev) + continue; + + switch (source) + { + /* If we have a mouse wheel, just use discrete data */ + case WL_POINTER_AXIS_SOURCE_WHEEL: +#if defined(WL_POINTER_AXIS_SOURCE_WHEEL_TILT_SINCE_VERSION) + case WL_POINTER_AXIS_SOURCE_WHEEL_TILT: +#endif + if (cev->type == UWAC_EVENT_POINTER_AXIS_DISCRETE) + { + /* Get the number of steps, multiply by default step width of 120 */ + int32_t val = cev->mouse_axis.value * 0x78; + /* No wheel event received, success! */ + if (!wlf_handle_wheel(instance, cev->mouse_axis.x, cev->mouse_axis.y, + cev->mouse_axis.axis, val)) + success = FALSE; + } + break; + /* If we have a touch pad we get actual data, scale */ + case WL_POINTER_AXIS_SOURCE_FINGER: + case WL_POINTER_AXIS_SOURCE_CONTINUOUS: + if (cev->type == UWAC_EVENT_POINTER_AXIS) + { + double dval = wl_fixed_to_double(cev->mouse_axis.value); + int32_t val = (int32_t)(dval * 0x78 / 10.0); + if (!wlf_handle_wheel(instance, cev->mouse_axis.x, cev->mouse_axis.y, + cev->mouse_axis.axis, val)) + success = FALSE; + } + break; + default: + break; + } + } + } + ArrayList_Clear(context->events); + return success; +} + +BOOL wlf_handle_pointer_source(freerdp* instance, const UwacPointerSourceEvent* ev) +{ + wlfContext* context = NULL; + if (!instance || !instance->context || !ev) + return FALSE; + + context = (wlfContext*)instance->context; + return ArrayList_Append(context->events, ev); +} + +BOOL wlf_handle_key(freerdp* instance, const UwacKeyEvent* ev) +{ + rdpInput* input = NULL; + DWORD rdp_scancode = 0; + + if (!instance || !ev) + return FALSE; + + WINPR_ASSERT(instance->context); + if (freerdp_settings_get_bool(instance->context->settings, FreeRDP_GrabKeyboard) && + ev->raw_key == KEY_RIGHTCTRL) + wlf_handle_ungrab_key(instance, ev); + + input = instance->context->input; + rdp_scancode = freerdp_keyboard_get_rdp_scancode_from_x11_keycode(ev->raw_key + 8); + + if (rdp_scancode == RDP_SCANCODE_UNKNOWN) + return TRUE; + + return freerdp_input_send_keyboard_event_ex(input, ev->pressed, ev->repeated, rdp_scancode); +} + +BOOL wlf_handle_ungrab_key(freerdp* instance, const UwacKeyEvent* ev) +{ + wlfContext* context = NULL; + if (!instance || !instance->context || !ev) + return FALSE; + + context = (wlfContext*)instance->context; + + return UwacSeatInhibitShortcuts(context->seat, false) == UWAC_SUCCESS; +} + +BOOL wlf_keyboard_enter(freerdp* instance, const UwacKeyboardEnterLeaveEvent* ev) +{ + if (!instance || !ev) + return FALSE; + + ((wlfContext*)instance->context)->focusing = TRUE; + return TRUE; +} + +BOOL wlf_keyboard_modifiers(freerdp* instance, const UwacKeyboardModifiersEvent* ev) +{ + rdpInput* input = NULL; + UINT16 syncFlags = 0; + wlfContext* wlf = NULL; + + if (!instance || !ev) + return FALSE; + + wlf = (wlfContext*)instance->context; + WINPR_ASSERT(wlf); + + input = instance->context->input; + WINPR_ASSERT(input); + + syncFlags = 0; + + if (ev->modifiers & UWAC_MOD_CAPS_MASK) + syncFlags |= KBD_SYNC_CAPS_LOCK; + if (ev->modifiers & UWAC_MOD_NUM_MASK) + syncFlags |= KBD_SYNC_NUM_LOCK; + + if (!wlf->focusing) + return TRUE; + + ((wlfContext*)instance->context)->focusing = FALSE; + + return freerdp_input_send_focus_in_event(input, syncFlags) && + freerdp_client_send_button_event(&wlf->common, FALSE, PTR_FLAGS_MOVE, 0, 0); +} + +BOOL wlf_handle_touch_up(freerdp* instance, const UwacTouchUp* ev) +{ + int32_t x = 0; + int32_t y = 0; + + WINPR_ASSERT(instance); + WINPR_ASSERT(ev); + + wlfContext* wlf = (wlfContext*)instance->context; + WINPR_ASSERT(wlf); + + x = ev->x; + y = ev->y; + + if (!scale_signed_coordinates(instance->context, &x, &y, TRUE)) + return FALSE; + + return freerdp_client_handle_touch(&wlf->common, FREERDP_TOUCH_UP, ev->id, 0, x, y); +} + +BOOL wlf_handle_touch_down(freerdp* instance, const UwacTouchDown* ev) +{ + int32_t x = 0; + int32_t y = 0; + + WINPR_ASSERT(instance); + WINPR_ASSERT(ev); + + wlfContext* wlf = (wlfContext*)instance->context; + WINPR_ASSERT(wlf); + + x = ev->x; + y = ev->y; + + if (!scale_signed_coordinates(instance->context, &x, &y, TRUE)) + return FALSE; + + return freerdp_client_handle_touch(&wlf->common, FREERDP_TOUCH_DOWN, ev->id, 0, x, y); +} + +BOOL wlf_handle_touch_motion(freerdp* instance, const UwacTouchMotion* ev) +{ + int32_t x = 0; + int32_t y = 0; + + WINPR_ASSERT(instance); + WINPR_ASSERT(ev); + + wlfContext* wlf = (wlfContext*)instance->context; + WINPR_ASSERT(wlf); + + x = ev->x; + y = ev->y; + + if (!scale_signed_coordinates(instance->context, &x, &y, TRUE)) + return FALSE; + + return freerdp_client_handle_touch(&wlf->common, FREERDP_TOUCH_MOTION, 0, ev->id, x, y); +} diff --git a/client/Wayland/wlf_input.h b/client/Wayland/wlf_input.h new file mode 100644 index 0000000..0d4f5ef --- /dev/null +++ b/client/Wayland/wlf_input.h @@ -0,0 +1,45 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Input + * + * Copyright 2014 Manuel Bachmann <tarnyko@tarnyko.net> + * Copyright 2015 David Fort <contact@hardening-consulting.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_WAYLAND_INPUT_H +#define FREERDP_CLIENT_WAYLAND_INPUT_H + +#include <freerdp/freerdp.h> +#include <freerdp/gdi/gdi.h> +#include <freerdp/gdi/gfx.h> +#include <uwac/uwac.h> + +BOOL wlf_handle_pointer_enter(freerdp* instance, const UwacPointerEnterLeaveEvent* ev); +BOOL wlf_handle_pointer_motion(freerdp* instance, const UwacPointerMotionEvent* ev); +BOOL wlf_handle_pointer_buttons(freerdp* instance, const UwacPointerButtonEvent* ev); +BOOL wlf_handle_pointer_axis(freerdp* instance, const UwacPointerAxisEvent* ev); +BOOL wlf_handle_pointer_axis_discrete(freerdp* instance, const UwacPointerAxisEvent* ev); +BOOL wlf_handle_pointer_frame(freerdp* instance, const UwacPointerFrameEvent* ev); +BOOL wlf_handle_pointer_source(freerdp* instance, const UwacPointerSourceEvent* ev); +BOOL wlf_handle_touch_up(freerdp* instance, const UwacTouchUp* ev); +BOOL wlf_handle_touch_down(freerdp* instance, const UwacTouchDown* ev); +BOOL wlf_handle_touch_motion(freerdp* instance, const UwacTouchMotion* ev); + +BOOL wlf_handle_key(freerdp* instance, const UwacKeyEvent* ev); +BOOL wlf_handle_ungrab_key(freerdp* instance, const UwacKeyEvent* ev); +BOOL wlf_keyboard_enter(freerdp* instance, const UwacKeyboardEnterLeaveEvent* ev); +BOOL wlf_keyboard_modifiers(freerdp* instance, const UwacKeyboardModifiersEvent* ev); + +#endif /* FREERDP_CLIENT_WAYLAND_INPUT_H */ diff --git a/client/Wayland/wlf_pointer.c b/client/Wayland/wlf_pointer.c new file mode 100644 index 0000000..6fba40b --- /dev/null +++ b/client/Wayland/wlf_pointer.c @@ -0,0 +1,167 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Mouse Pointer + * + * Copyright 2019 Armin Novak <armin.novak@thincast.com> + * Copyright 2019 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include "wlf_pointer.h" +#include "wlfreerdp.h" + +#define TAG CLIENT_TAG("wayland.pointer") + +typedef struct +{ + rdpPointer pointer; + size_t size; + void* data; +} wlfPointer; + +static BOOL wlf_Pointer_New(rdpContext* context, rdpPointer* pointer) +{ + wlfPointer* ptr = (wlfPointer*)pointer; + + if (!ptr) + return FALSE; + + ptr->size = pointer->width * pointer->height * 4ULL; + ptr->data = winpr_aligned_malloc(ptr->size, 16); + + if (!ptr->data) + return FALSE; + + if (!freerdp_image_copy_from_pointer_data( + ptr->data, PIXEL_FORMAT_BGRA32, 0, 0, 0, pointer->width, pointer->height, + pointer->xorMaskData, pointer->lengthXorMask, pointer->andMaskData, + pointer->lengthAndMask, pointer->xorBpp, &context->gdi->palette)) + { + winpr_aligned_free(ptr->data); + return FALSE; + } + + return TRUE; +} + +static void wlf_Pointer_Free(rdpContext* context, rdpPointer* pointer) +{ + wlfPointer* ptr = (wlfPointer*)pointer; + WINPR_UNUSED(context); + + if (ptr) + winpr_aligned_free(ptr->data); +} + +static BOOL wlf_Pointer_Set(rdpContext* context, rdpPointer* pointer) +{ + wlfContext* wlf = (wlfContext*)context; + wlfPointer* ptr = (wlfPointer*)pointer; + void* data = NULL; + UINT32 w = 0; + UINT32 h = 0; + UINT32 x = 0; + UINT32 y = 0; + size_t size = 0; + UwacReturnCode rc = UWAC_ERROR_INTERNAL; + BOOL res = FALSE; + RECTANGLE_16 area; + + if (!wlf || !wlf->seat) + return FALSE; + + x = pointer->xPos; + y = pointer->yPos; + w = pointer->width; + h = pointer->height; + + if (!wlf_scale_coordinates(context, &x, &y, FALSE) || + !wlf_scale_coordinates(context, &w, &h, FALSE)) + return FALSE; + + size = w * h * 4ULL; + data = malloc(size); + + if (!data) + return FALSE; + + area.top = 0; + area.left = 0; + area.right = (UINT16)pointer->width; + area.bottom = (UINT16)pointer->height; + + if (!wlf_copy_image(ptr->data, pointer->width * 4, pointer->width, pointer->height, data, w * 4, + w, h, &area, + freerdp_settings_get_bool(context->settings, FreeRDP_SmartSizing))) + goto fail; + + rc = UwacSeatSetMouseCursor(wlf->seat, data, size, w, h, x, y); + + if (rc == UWAC_SUCCESS) + res = TRUE; + +fail: + free(data); + return res; +} + +static BOOL wlf_Pointer_SetNull(rdpContext* context) +{ + wlfContext* wlf = (wlfContext*)context; + + if (!wlf || !wlf->seat) + return FALSE; + + if (UwacSeatSetMouseCursor(wlf->seat, NULL, 0, 0, 0, 0, 0) != UWAC_SUCCESS) + return FALSE; + + return TRUE; +} + +static BOOL wlf_Pointer_SetDefault(rdpContext* context) +{ + wlfContext* wlf = (wlfContext*)context; + + if (!wlf || !wlf->seat) + return FALSE; + + if (UwacSeatSetMouseCursor(wlf->seat, NULL, 1, 0, 0, 0, 0) != UWAC_SUCCESS) + return FALSE; + + return TRUE; +} + +static BOOL wlf_Pointer_SetPosition(rdpContext* context, UINT32 x, UINT32 y) +{ + // TODO + WLog_WARN(TAG, "not implemented"); + return TRUE; +} + +BOOL wlf_register_pointer(rdpGraphics* graphics) +{ + rdpPointer pointer = { 0 }; + + pointer.size = sizeof(wlfPointer); + pointer.New = wlf_Pointer_New; + pointer.Free = wlf_Pointer_Free; + pointer.Set = wlf_Pointer_Set; + pointer.SetNull = wlf_Pointer_SetNull; + pointer.SetDefault = wlf_Pointer_SetDefault; + pointer.SetPosition = wlf_Pointer_SetPosition; + graphics_register_pointer(graphics, &pointer); + return TRUE; +} diff --git a/client/Wayland/wlf_pointer.h b/client/Wayland/wlf_pointer.h new file mode 100644 index 0000000..8ae82e1 --- /dev/null +++ b/client/Wayland/wlf_pointer.h @@ -0,0 +1,28 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Mouse Pointer + * + * Copyright 2019 Armin Novak <armin.novak@thincast.com> + * Copyright 2019 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_WAYLAND_POINTER_H +#define FREERDP_CLIENT_WAYLAND_POINTER_H + +#include <freerdp/graphics.h> + +BOOL wlf_register_pointer(rdpGraphics* graphics); + +#endif /* FREERDP_CLIENT_WAYLAND_POINTER_H */ diff --git a/client/Wayland/wlfreerdp.1.in b/client/Wayland/wlfreerdp.1.in new file mode 100644 index 0000000..ee40412 --- /dev/null +++ b/client/Wayland/wlfreerdp.1.in @@ -0,0 +1,38 @@ +.de URL +\\$2 \(laURL: \\$1 \(ra\\$3 +.. +.if \n[.g] .mso www.tmac +.TH @MANPAGE_NAME@ 1 2017-01-12 "@FREERDP_VERSION_FULL@" "FreeRDP" +.SH NAME +@MANPAGE_NAME@ \- FreeRDP wayland client +.SH SYNOPSIS +.B @MANPAGE_NAME@ +[file] +[\fIdefault_client_options\fP] +[\fB/v\fP:<server>[:port]] +[\fB/version\fP] +[\fB/help\fP] +.SH DESCRIPTION +.B @MANPAGE_NAME@ +is a wayland Remote Desktop Protocol (RDP) client which is part of the FreeRDP project. A RDP server is built-in to many editions of Windows.. Alternative servers included ogon, gnome-remote-desktop, xrdp and VRDP (VirtualBox). +.SH OPTIONS +The wayland client also supports a lot of the \fIdefault client options\fP which are not described here. For details on those see the xfreerdp(1) man page. +.IP \fB/v:\fP\fI<server>[:port]\fP +The server hostname or IP, and optionally the port, to connect to. +.IP /version +Print the version and exit. +.IP /help +Print the help and exit. +.SH EXIT STATUS +.TP +.B 0 +Successful program execution. +.TP +.B not 0 +On failure. + +.SH SEE ALSO +xfreerdp(1) wlog(7) + +.SH AUTHOR +FreeRDP <team@freerdp.com> diff --git a/client/Wayland/wlfreerdp.c b/client/Wayland/wlfreerdp.c new file mode 100644 index 0000000..037c999 --- /dev/null +++ b/client/Wayland/wlfreerdp.c @@ -0,0 +1,807 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Client + * + * Copyright 2014 Manuel Bachmann <tarnyko@tarnyko.net> + * Copyright 2016 Thincast Technologies GmbH + * Copyright 2016 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <math.h> +#include <stdio.h> +#include <errno.h> +#include <locale.h> +#include <float.h> + +#include <winpr/sysinfo.h> + +#include <freerdp/client/cmdline.h> +#include <freerdp/channels/channels.h> +#include <freerdp/gdi/gdi.h> +#include <freerdp/client.h> +#include <freerdp/utils/signal.h> +#include <freerdp/locale/keyboard.h> + +#include <linux/input.h> + +#include <uwac/uwac.h> + +#include "wlfreerdp.h" +#include "wlf_input.h" +#include "wlf_cliprdr.h" +#include "wlf_disp.h" +#include "wlf_channels.h" +#include "wlf_pointer.h" + +#define TAG CLIENT_TAG("wayland") + +static BOOL wl_update_buffer(wlfContext* context_w, INT32 ix, INT32 iy, INT32 iw, INT32 ih) +{ + BOOL res = FALSE; + rdpGdi* gdi = NULL; + char* data = NULL; + UINT32 x = 0; + UINT32 y = 0; + UINT32 w = 0; + UINT32 h = 0; + UwacSize geometry; + size_t stride = 0; + UwacReturnCode rc = UWAC_ERROR_INTERNAL; + RECTANGLE_16 area; + + if (!context_w) + return FALSE; + + if ((ix < 0) || (iy < 0) || (iw < 0) || (ih < 0)) + return FALSE; + + EnterCriticalSection(&context_w->critical); + x = (UINT32)ix; + y = (UINT32)iy; + w = (UINT32)iw; + h = (UINT32)ih; + rc = UwacWindowGetDrawingBufferGeometry(context_w->window, &geometry, &stride); + data = UwacWindowGetDrawingBuffer(context_w->window); + + if (!data || (rc != UWAC_SUCCESS)) + goto fail; + + gdi = context_w->common.context.gdi; + + if (!gdi) + goto fail; + + /* Ignore output if the surface size does not match. */ + if (((INT64)x > geometry.width) || ((INT64)y > geometry.height)) + { + res = TRUE; + goto fail; + } + + area.left = x; + area.top = y; + area.right = x + w; + area.bottom = y + h; + + if (!wlf_copy_image( + gdi->primary_buffer, gdi->stride, gdi->width, gdi->height, data, stride, geometry.width, + geometry.height, &area, + freerdp_settings_get_bool(context_w->common.context.settings, FreeRDP_SmartSizing))) + goto fail; + + if (!wlf_scale_coordinates(&context_w->common.context, &x, &y, FALSE)) + goto fail; + + if (!wlf_scale_coordinates(&context_w->common.context, &w, &h, FALSE)) + goto fail; + + if (UwacWindowAddDamage(context_w->window, x, y, w, h) != UWAC_SUCCESS) + goto fail; + + if (UwacWindowSubmitBuffer(context_w->window, false) != UWAC_SUCCESS) + goto fail; + + res = TRUE; +fail: + LeaveCriticalSection(&context_w->critical); + return res; +} + +static BOOL wl_end_paint(rdpContext* context) +{ + rdpGdi* gdi = NULL; + wlfContext* context_w = NULL; + INT32 x = 0; + INT32 y = 0; + INT32 w = 0; + INT32 h = 0; + + if (!context || !context->gdi || !context->gdi->primary) + return FALSE; + + gdi = context->gdi; + + if (gdi->primary->hdc->hwnd->invalid->null) + return TRUE; + + x = gdi->primary->hdc->hwnd->invalid->x; + y = gdi->primary->hdc->hwnd->invalid->y; + w = gdi->primary->hdc->hwnd->invalid->w; + h = gdi->primary->hdc->hwnd->invalid->h; + context_w = (wlfContext*)context; + if (!wl_update_buffer(context_w, x, y, w, h)) + { + return FALSE; + } + + gdi->primary->hdc->hwnd->invalid->null = TRUE; + gdi->primary->hdc->hwnd->ninvalid = 0; + return TRUE; +} + +static BOOL wl_refresh_display(wlfContext* context) +{ + rdpGdi* gdi = NULL; + + if (!context || !context->common.context.gdi) + return FALSE; + + gdi = context->common.context.gdi; + return wl_update_buffer(context, 0, 0, gdi->width, gdi->height); +} + +static BOOL wl_resize_display(rdpContext* context) +{ + wlfContext* wlc = (wlfContext*)context; + rdpGdi* gdi = context->gdi; + rdpSettings* settings = context->settings; + + if (!gdi_resize(gdi, freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth), + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight))) + return FALSE; + + return wl_refresh_display(wlc); +} + +static BOOL wl_pre_connect(freerdp* instance) +{ + rdpSettings* settings = NULL; + wlfContext* context = NULL; + const UwacOutput* output = NULL; + UwacSize resolution; + + if (!instance) + return FALSE; + + context = (wlfContext*)instance->context; + WINPR_ASSERT(context); + + settings = instance->context->settings; + WINPR_ASSERT(settings); + + if (!freerdp_settings_set_uint32(settings, FreeRDP_OsMajorType, OSMAJORTYPE_UNIX)) + return FALSE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_OsMinorType, OSMINORTYPE_NATIVE_WAYLAND)) + return FALSE; + PubSub_SubscribeChannelConnected(instance->context->pubSub, wlf_OnChannelConnectedEventHandler); + PubSub_SubscribeChannelDisconnected(instance->context->pubSub, + wlf_OnChannelDisconnectedEventHandler); + + if (freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)) + { + // Use the resolution of the first display output + output = UwacDisplayGetOutput(context->display, 0); + + if ((output != NULL) && (UwacOutputGetResolution(output, &resolution) == UWAC_SUCCESS)) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, + (UINT32)resolution.width)) + return FALSE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, + (UINT32)resolution.height)) + return FALSE; + } + else + { + WLog_WARN(TAG, "Failed to get output resolution! Check your display settings"); + } + } + + return TRUE; +} + +static BOOL wl_post_connect(freerdp* instance) +{ + rdpGdi* gdi = NULL; + UwacWindow* window = NULL; + wlfContext* context = NULL; + rdpSettings* settings = NULL; + char* title = "FreeRDP"; + char* app_id = "wlfreerdp"; + UINT32 w = 0; + UINT32 h = 0; + + if (!instance || !instance->context) + return FALSE; + + context = (wlfContext*)instance->context; + settings = instance->context->settings; + + const char* wtitle = freerdp_settings_get_string(settings, FreeRDP_WindowTitle); + if (wtitle) + title = wtitle; + + if (!gdi_init(instance, PIXEL_FORMAT_BGRA32)) + return FALSE; + + gdi = instance->context->gdi; + + if (!gdi || (gdi->width < 0) || (gdi->height < 0)) + return FALSE; + + if (!wlf_register_pointer(instance->context->graphics)) + return FALSE; + + w = (UINT32)gdi->width; + h = (UINT32)gdi->height; + + if (freerdp_settings_get_bool(settings, FreeRDP_SmartSizing) && !context->fullscreen) + { + const UINT32 sw = freerdp_settings_get_uint32(settings, FreeRDP_SmartSizingWidth); + if (sw > 0) + w = sw; + + const UINT32 sh = freerdp_settings_get_uint32(settings, FreeRDP_SmartSizingHeight); + if (sh > 0) + h = sh; + } + + context->window = window = UwacCreateWindowShm(context->display, w, h, WL_SHM_FORMAT_XRGB8888); + + if (!window) + return FALSE; + + UwacWindowSetFullscreenState( + window, NULL, freerdp_settings_get_bool(instance->context->settings, FreeRDP_Fullscreen)); + UwacWindowSetTitle(window, title); + UwacWindowSetAppId(window, app_id); + UwacWindowSetOpaqueRegion(context->window, 0, 0, w, h); + instance->context->update->EndPaint = wl_end_paint; + instance->context->update->DesktopResize = wl_resize_display; + UINT32 KeyboardLayout = + freerdp_settings_get_uint32(instance->context->settings, FreeRDP_KeyboardLayout); + const char* KeyboardRemappingList = + freerdp_settings_get_string(instance->context->settings, FreeRDP_KeyboardRemappingList); + + freerdp_keyboard_init_ex(KeyboardLayout, KeyboardRemappingList); + + if (!(context->disp = wlf_disp_new(context))) + return FALSE; + + context->clipboard = wlf_clipboard_new(context); + + if (!context->clipboard) + return FALSE; + + return wl_refresh_display(context); +} + +static void wl_post_disconnect(freerdp* instance) +{ + wlfContext* context = NULL; + + if (!instance) + return; + + if (!instance->context) + return; + + context = (wlfContext*)instance->context; + gdi_free(instance); + wlf_clipboard_free(context->clipboard); + wlf_disp_free(context->disp); + + if (context->window) + UwacDestroyWindow(&context->window); +} + +static BOOL handle_uwac_events(freerdp* instance, UwacDisplay* display) +{ + UwacEvent event; + wlfContext* context = NULL; + + if (UwacDisplayDispatch(display, 1) < 0) + return FALSE; + + context = (wlfContext*)instance->context; + + while (UwacHasEvent(display)) + { + if (UwacNextEvent(display, &event) != UWAC_SUCCESS) + return FALSE; + + /*printf("UWAC event type %d\n", event.type);*/ + switch (event.type) + { + case UWAC_EVENT_NEW_SEAT: + context->seat = event.seat_new.seat; + break; + + case UWAC_EVENT_REMOVED_SEAT: + context->seat = NULL; + break; + + case UWAC_EVENT_FRAME_DONE: + { + EnterCriticalSection(&context->critical); + UwacReturnCode r = UwacWindowSubmitBuffer(context->window, false); + LeaveCriticalSection(&context->critical); + if (r != UWAC_SUCCESS) + return FALSE; + } + break; + + case UWAC_EVENT_POINTER_ENTER: + if (!wlf_handle_pointer_enter(instance, &event.mouse_enter_leave)) + return FALSE; + + break; + + case UWAC_EVENT_POINTER_MOTION: + if (!wlf_handle_pointer_motion(instance, &event.mouse_motion)) + return FALSE; + + break; + + case UWAC_EVENT_POINTER_BUTTONS: + if (!wlf_handle_pointer_buttons(instance, &event.mouse_button)) + return FALSE; + + break; + + case UWAC_EVENT_POINTER_AXIS: + if (!wlf_handle_pointer_axis(instance, &event.mouse_axis)) + return FALSE; + break; + + case UWAC_EVENT_POINTER_AXIS_DISCRETE: + if (!wlf_handle_pointer_axis_discrete(instance, &event.mouse_axis)) + return FALSE; + break; + + case UWAC_EVENT_POINTER_FRAME: + if (!wlf_handle_pointer_frame(instance, &event.mouse_frame)) + return FALSE; + break; + case UWAC_EVENT_POINTER_SOURCE: + if (!wlf_handle_pointer_source(instance, &event.mouse_source)) + return FALSE; + break; + + case UWAC_EVENT_KEY: + if (!wlf_handle_key(instance, &event.key)) + return FALSE; + + break; + + case UWAC_EVENT_TOUCH_UP: + if (!wlf_handle_touch_up(instance, &event.touchUp)) + return FALSE; + + break; + + case UWAC_EVENT_TOUCH_DOWN: + if (!wlf_handle_touch_down(instance, &event.touchDown)) + return FALSE; + + break; + + case UWAC_EVENT_TOUCH_MOTION: + if (!wlf_handle_touch_motion(instance, &event.touchMotion)) + return FALSE; + + break; + + case UWAC_EVENT_KEYBOARD_ENTER: + if (freerdp_settings_get_bool(instance->context->settings, FreeRDP_GrabKeyboard)) + UwacSeatInhibitShortcuts(event.keyboard_enter_leave.seat, true); + + if (!wlf_keyboard_enter(instance, &event.keyboard_enter_leave)) + return FALSE; + + break; + + case UWAC_EVENT_KEYBOARD_MODIFIERS: + if (!wlf_keyboard_modifiers(instance, &event.keyboard_modifiers)) + return FALSE; + + break; + + case UWAC_EVENT_CONFIGURE: + if (!wlf_disp_handle_configure(context->disp, event.configure.width, + event.configure.height)) + return FALSE; + + if (!wl_refresh_display(context)) + return FALSE; + + break; + + case UWAC_EVENT_CLIPBOARD_AVAILABLE: + case UWAC_EVENT_CLIPBOARD_OFFER: + case UWAC_EVENT_CLIPBOARD_SELECT: + if (!wlf_cliprdr_handle_event(context->clipboard, &event.clipboard)) + return FALSE; + + break; + + case UWAC_EVENT_CLOSE: + context->closed = TRUE; + + break; + + default: + break; + } + } + + return TRUE; +} + +static BOOL handle_window_events(freerdp* instance) +{ + if (!instance) + return FALSE; + + return TRUE; +} + +static int wlfreerdp_run(freerdp* instance) +{ + wlfContext* context = NULL; + HANDLE handles[MAXIMUM_WAIT_OBJECTS] = { 0 }; + DWORD status = WAIT_ABANDONED; + HANDLE timer = NULL; + LARGE_INTEGER due; + + TimerEventArgs timerEvent; + EventArgsInit(&timerEvent, "xfreerdp"); + + if (!instance) + return -1; + + context = (wlfContext*)instance->context; + + if (!context) + return -1; + + if (!freerdp_connect(instance)) + { + WLog_Print(context->log, WLOG_ERROR, "Failed to connect"); + return -1; + } + + timer = CreateWaitableTimerA(NULL, FALSE, "mainloop-periodic-timer"); + + if (!timer) + { + WLog_ERR(TAG, "failed to create timer"); + goto disconnect; + } + + due.QuadPart = 0; + + if (!SetWaitableTimer(timer, &due, 20, NULL, NULL, FALSE)) + { + goto disconnect; + } + + while (!freerdp_shall_disconnect_context(instance->context)) + { + DWORD count = 0; + handles[count++] = timer; + handles[count++] = context->displayHandle; + count += freerdp_get_event_handles(instance->context, &handles[count], + ARRAYSIZE(handles) - count); + + if (count <= 2) + { + WLog_Print(context->log, WLOG_ERROR, "Failed to get FreeRDP file descriptor"); + break; + } + + status = WaitForMultipleObjects(count, handles, FALSE, INFINITE); + + if (WAIT_FAILED == status) + { + WLog_Print(context->log, WLOG_ERROR, "WaitForMultipleObjects failed"); + break; + } + + if (!handle_uwac_events(instance, context->display)) + { + WLog_Print(context->log, WLOG_ERROR, "error handling UWAC events"); + break; + } + + if (context->closed) + { + WLog_Print(context->log, WLOG_INFO, "Closed from Wayland"); + break; + } + + if (freerdp_check_event_handles(instance->context) != TRUE) + { + if (client_auto_reconnect_ex(instance, handle_window_events)) + continue; + else + { + /* + * Indicate an unsuccessful connection attempt if reconnect + * did not succeed and no other error was specified. + */ + if (freerdp_error_info(instance) == 0) + status = 42; + } + + if (freerdp_get_last_error(instance->context) == FREERDP_ERROR_SUCCESS) + WLog_Print(context->log, WLOG_ERROR, "Failed to check FreeRDP file descriptor"); + + break; + } + + if ((status != WAIT_TIMEOUT) && (status == WAIT_OBJECT_0)) + { + timerEvent.now = GetTickCount64(); + PubSub_OnTimer(context->common.context.pubSub, context, &timerEvent); + } + } + +disconnect: + if (timer) + CloseHandle(timer); + freerdp_disconnect(instance); + return status; +} + +static BOOL wlf_client_global_init(void) +{ + setlocale(LC_ALL, ""); + + if (freerdp_handle_signals() != 0) + return FALSE; + + return TRUE; +} + +static void wlf_client_global_uninit(void) +{ +} + +static int wlf_logon_error_info(freerdp* instance, UINT32 data, UINT32 type) +{ + wlfContext* wlf = NULL; + const char* str_data = freerdp_get_logon_error_info_data(data); + const char* str_type = freerdp_get_logon_error_info_type(type); + + if (!instance || !instance->context) + return -1; + + wlf = (wlfContext*)instance->context; + WLog_Print(wlf->log, WLOG_INFO, "Logon Error Info %s [%s]", str_data, str_type); + return 1; +} + +static void wlf_client_free(freerdp* instance, rdpContext* context) +{ + wlfContext* wlf = (wlfContext*)instance->context; + + if (!context) + return; + + if (wlf->display) + UwacCloseDisplay(&wlf->display); + + if (wlf->displayHandle) + CloseHandle(wlf->displayHandle); + ArrayList_Free(wlf->events); + DeleteCriticalSection(&wlf->critical); +} + +static void* uwac_event_clone(const void* val) +{ + UwacEvent* copy = NULL; + const UwacEvent* ev = (const UwacEvent*)val; + + copy = calloc(1, sizeof(UwacEvent)); + if (!copy) + return NULL; + *copy = *ev; + return copy; +} + +static BOOL wlf_client_new(freerdp* instance, rdpContext* context) +{ + wObject* obj = NULL; + UwacReturnCode status = UWAC_ERROR_INTERNAL; + wlfContext* wfl = (wlfContext*)context; + + if (!instance || !context) + return FALSE; + + instance->PreConnect = wl_pre_connect; + instance->PostConnect = wl_post_connect; + instance->PostDisconnect = wl_post_disconnect; + instance->LogonErrorInfo = wlf_logon_error_info; + wfl->log = WLog_Get(TAG); + wfl->display = UwacOpenDisplay(NULL, &status); + + if (!wfl->display || (status != UWAC_SUCCESS) || !wfl->log) + return FALSE; + + wfl->displayHandle = CreateFileDescriptorEvent(NULL, FALSE, FALSE, + UwacDisplayGetFd(wfl->display), WINPR_FD_READ); + + if (!wfl->displayHandle) + return FALSE; + + wfl->events = ArrayList_New(FALSE); + if (!wfl->events) + return FALSE; + + obj = ArrayList_Object(wfl->events); + obj->fnObjectNew = uwac_event_clone; + obj->fnObjectFree = free; + + InitializeCriticalSection(&wfl->critical); + + return TRUE; +} + +static int wfl_client_start(rdpContext* context) +{ + WINPR_UNUSED(context); + return 0; +} + +static int RdpClientEntry(RDP_CLIENT_ENTRY_POINTS* pEntryPoints) +{ + WINPR_ASSERT(pEntryPoints); + ZeroMemory(pEntryPoints, sizeof(RDP_CLIENT_ENTRY_POINTS)); + pEntryPoints->Version = RDP_CLIENT_INTERFACE_VERSION; + pEntryPoints->Size = sizeof(RDP_CLIENT_ENTRY_POINTS_V1); + pEntryPoints->GlobalInit = wlf_client_global_init; + pEntryPoints->GlobalUninit = wlf_client_global_uninit; + pEntryPoints->ContextSize = sizeof(wlfContext); + pEntryPoints->ClientNew = wlf_client_new; + pEntryPoints->ClientFree = wlf_client_free; + pEntryPoints->ClientStart = wfl_client_start; + pEntryPoints->ClientStop = freerdp_client_common_stop; + return 0; +} + +int main(int argc, char* argv[]) +{ + int rc = -1; + int status = 0; + RDP_CLIENT_ENTRY_POINTS clientEntryPoints; + rdpContext* context = NULL; + rdpSettings* settings = NULL; + wlfContext* wlc = NULL; + + freerdp_client_warn_deprecated(argc, argv); + + RdpClientEntry(&clientEntryPoints); + context = freerdp_client_context_new(&clientEntryPoints); + if (!context) + goto fail; + wlc = (wlfContext*)context; + settings = context->settings; + + status = freerdp_client_settings_parse_command_line(settings, argc, argv, FALSE); + if (status) + { + rc = freerdp_client_settings_command_line_status_print(settings, status, argc, argv); + + if (freerdp_settings_get_bool(settings, FreeRDP_ListMonitors)) + wlf_list_monitors(wlc); + + goto fail; + } + + if (freerdp_client_start(context) != 0) + goto fail; + + rc = wlfreerdp_run(context->instance); + + if (freerdp_client_stop(context) != 0) + rc = -1; + +fail: + freerdp_client_context_free(context); + return rc; +} + +BOOL wlf_copy_image(const void* src, size_t srcStride, size_t srcWidth, size_t srcHeight, void* dst, + size_t dstStride, size_t dstWidth, size_t dstHeight, const RECTANGLE_16* area, + BOOL scale) +{ + BOOL rc = FALSE; + + if (!src || !dst || !area) + return FALSE; + + if (scale) + { + return freerdp_image_scale(dst, PIXEL_FORMAT_BGRA32, dstStride, 0, 0, dstWidth, dstHeight, + src, PIXEL_FORMAT_BGRA32, srcStride, 0, 0, srcWidth, srcHeight); + } + else + { + const size_t baseSrcOffset = area->top * srcStride + area->left * 4; + const size_t baseDstOffset = area->top * dstStride + area->left * 4; + const size_t width = MIN((size_t)area->right - area->left, dstWidth - area->left); + const size_t height = MIN((size_t)area->bottom - area->top, dstHeight - area->top); + const BYTE* psrc = (const BYTE*)src; + BYTE* pdst = (BYTE*)dst; + + for (size_t i = 0; i < height; i++) + { + const size_t srcOffset = i * srcStride + baseSrcOffset; + const size_t dstOffset = i * dstStride + baseDstOffset; + memcpy(&pdst[dstOffset], &psrc[srcOffset], width * 4); + } + + rc = TRUE; + } + + return rc; +} + +BOOL wlf_scale_coordinates(rdpContext* context, UINT32* px, UINT32* py, BOOL fromLocalToRDP) +{ + wlfContext* wlf = (wlfContext*)context; + rdpGdi* gdi = NULL; + UwacSize geometry; + double sx = NAN; + double sy = NAN; + + if (!context || !px || !py || !context->gdi) + return FALSE; + + if (!freerdp_settings_get_bool(context->settings, FreeRDP_SmartSizing)) + return TRUE; + + gdi = context->gdi; + + if (UwacWindowGetDrawingBufferGeometry(wlf->window, &geometry, NULL) != UWAC_SUCCESS) + return FALSE; + + sx = geometry.width / (double)gdi->width; + sy = geometry.height / (double)gdi->height; + + if (!fromLocalToRDP) + { + *px *= sx; + *py *= sy; + } + else + { + *px /= sx; + *py /= sy; + } + + return TRUE; +} diff --git a/client/Wayland/wlfreerdp.h b/client/Wayland/wlfreerdp.h new file mode 100644 index 0000000..e1a4650 --- /dev/null +++ b/client/Wayland/wlfreerdp.h @@ -0,0 +1,59 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Wayland Client + * + * Copyright 2014 Manuel Bachmann <tarnyko@tarnyko.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_WAYLAND_FREERDP_H +#define FREERDP_CLIENT_WAYLAND_FREERDP_H + +#include <freerdp/client/rdpei.h> +#include <freerdp/gdi/gfx.h> +#include <freerdp/freerdp.h> +#include <freerdp/log.h> +#include <winpr/wtypes.h> +#include <uwac/uwac.h> + +typedef struct wlf_clipboard wfClipboard; +typedef struct s_wlfDispContext wlfDispContext; + +typedef struct +{ + rdpClientContext common; + + UwacDisplay* display; + HANDLE displayHandle; + UwacWindow* window; + UwacSeat* seat; + + BOOL fullscreen; + BOOL closed; + BOOL focusing; + + /* Channels */ + wfClipboard* clipboard; + wlfDispContext* disp; + wLog* log; + CRITICAL_SECTION critical; + wArrayList* events; +} wlfContext; + +BOOL wlf_scale_coordinates(rdpContext* context, UINT32* px, UINT32* py, BOOL fromLocalToRDP); +BOOL wlf_copy_image(const void* src, size_t srcStride, size_t srcWidth, size_t srcHeight, void* dst, + size_t dstStride, size_t dstWidth, size_t dstHeight, const RECTANGLE_16* area, + BOOL scale); + +#endif /* FREERDP_CLIENT_WAYLAND_FREERDP_H */ diff --git a/client/X11/CMakeLists.txt b/client/X11/CMakeLists.txt new file mode 100644 index 0000000..099d00d --- /dev/null +++ b/client/X11/CMakeLists.txt @@ -0,0 +1,245 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP X11 Client +# +# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com> +# Copyright 2013 Corey Clayton <can.of.tuna@gmail.com> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +cmake_minimum_required(VERSION 3.13) + +if (NOT FREERDP_DEFAULT_PROJECT_VERSION) + set(FREERDP_DEFAULT_PROJECT_VERSION "1.0.0.0") +endif() + +project(xfreerdp + LANGUAGES C + VERSION ${FREERDP_DEFAULT_PROJECT_VERSION} +) + +message("project ${PROJECT_NAME} is using version ${PROJECT_VERSION}") + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS ON) + +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/) +include(CommonConfigOptions) + +include(ConfigureFreeRDP) + +find_package(X11 REQUIRED) + +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../../resources) +include_directories(${X11_INCLUDE_DIRS}) +include_directories(${OPENSSL_INCLUDE_DIR}) + +set(SRCS + xf_utils.h + xf_utils.c + xf_gfx.c + xf_gfx.h + xf_rail.c + xf_rail.h + xf_input.c + xf_input.h + xf_event.c + xf_event.h + xf_floatbar.c + xf_floatbar.h + xf_input.c + xf_input.h + xf_channels.c + xf_channels.h + xf_cliprdr.c + xf_cliprdr.h + xf_monitor.c + xf_monitor.h + xf_disp.c + xf_disp.h + xf_graphics.c + xf_graphics.h + xf_keyboard.c + xf_keyboard.h + xf_video.c + xf_video.h + xf_window.c + xf_window.h + xf_client.c + xf_client.h) + +if (CHANNEL_TSMF_CLIENT) + list(APPEND SRCS + xf_tsmf.c + xf_tsmf.h + ) +endif() + +if(WITH_CLIENT_INTERFACE) + if(CLIENT_INTERFACE_SHARED) + add_library(${PROJECT_NAME} SHARED ${SRCS}) + if (WITH_LIBRARY_VERSIONING) + set_target_properties(${PROJECT_NAME} PROPERTIES VERSION ${FREERDP_VERSION} SOVERSION ${FREERDP_API_VERSION}) + endif() + else() + add_library(${PROJECT_NAME} ${SRCS}) + endif() + target_include_directories(${PROJECT_NAME} INTERFACE $<INSTALL_INTERFACE:include>) + +else() + list(APPEND SRCS + cli/xfreerdp.c xfreerdp.h + ) + add_executable(${PROJECT_NAME} ${SRCS}) + if (WITH_BINARY_VERSIONING) + set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "${PROJECT_NAME}${PROJECT_VERSION_MAJOR}") + endif() + include_directories(..) +endif() + +set(LIBS + ${X11_LIBRARIES} +) + +add_subdirectory(man) + +find_package(X11 REQUIRED) +if(X11_XShm_FOUND) + add_definitions(-DWITH_XSHM) + include_directories(${X11_XShm_INCLUDE_PATH}) + list(APPEND LIBS + ${X11_Xext_LIB} + ) +endif() + + +option(WITH_XINERAMA "[X11] enable xinerama" ON) +if (WITH_XINERAMA) + find_package(X11 REQUIRED) + if(X11_Xinerama_FOUND) + add_definitions(-DWITH_XINERAMA) + include_directories(${X11_Xinerama_INCLUDE_PATH}) + list(APPEND LIBS + ${X11_Xinerama_LIB} + ) + endif() +endif() + +option(WITH_XEXT "[X11] enable Xext" ON) +if (WITH_XEXT) + find_package(X11 REQUIRED) + if(X11_Xext_FOUND) + add_definitions(-DWITH_XEXT) + list(APPEND LIBS + ${X11_Xext_LIB} + ) + endif() +endif() + +option(WITH_XCURSOR "[X11] enalbe Xcursor" ON) +if (WITH_XCURSOR) + find_package(X11 REQUIRED) + if(X11_Xcursor_FOUND) + add_definitions(-DWITH_XCURSOR) + include_directories(${X11_Xcursor_INCLUDE_PATH}) + list(APPEND LIBS + ${X11_Xcursor_LIB} + ) + endif() +endif() + +option(WITH_XV "[X11] enable Xv" ON) +if (WITH_XV) + find_package(X11 REQUIRED) + if(X11_Xv_FOUND) + add_definitions(-DWITH_XV) + include_directories(${X11_Xv_INCLUDE_PATH}) + list(APPEND LIBS + ${X11_Xv_LIB} + ) + endif() +endif() + +option(WITH_XI "[X11] enalbe Xi" ON) +if (WITH_XI) + find_package(X11 REQUIRED) + if(X11_Xi_FOUND) + add_definitions(-DWITH_XI) + include_directories(${X11_Xi_INCLUDE_PATH}) + list(APPEND LIBS + ${X11_Xi_LIB} + ) + endif() +endif() + +option(WITH_XRENDER "[X11] enable XRender" ON) +if(WITH_XRENDER) + find_package(X11 REQUIRED) + if(X11_Xrender_FOUND) + add_definitions(-DWITH_XRENDER) + include_directories(${X11_Xrender_INCLUDE_PATH}) + list(APPEND LIBS + ${X11_Xrender_LIB} + ) + endif() +endif() + +option(WITH_XRANDR "[X11] enable XRandR" ON) +if (WITH_XRANDR) + find_package(X11 REQUIRED) + if(X11_Xrandr_FOUND) + add_definitions(-DWITH_XRANDR) + include_directories(${X11_Xrandr_INCLUDE_PATH}) + list(APPEND LIBS + ${X11_Xrandr_LIB} + ) + endif() +endif() + +option(WITH_XFIXES "[X11] enable Xfixes" ON) +if (WITH_XFIXES) + find_package(X11 REQUIRED) + if(X11_Xfixes_FOUND) + add_definitions(-DWITH_XFIXES) + include_directories(${X11_Xfixes_INCLUDE_PATH}) + list(APPEND LIBS + ${X11_Xfixes_LIB} + ) + endif() +endif() + +include_directories(${PROJECT_SOURCE_DIR}/resources) + +list(APPEND LIBS + freerdp-client + freerdp + m +) +if (NOT APPLE) + list(APPEND LIBS rt) +endif() +target_link_libraries(${PROJECT_NAME} PRIVATE ${LIBS}) + +if(WITH_IPP) + target_link_libraries(${PROJECT_NAME} PRIVATE ${IPP_LIBRARY_LIST}) +endif() + +option(WITH_CLIENT_INTERFACE "Build clients as a library with an interface" OFF) +if(WITH_CLIENT_INTERFACE) + install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libraries) + add_subdirectory(cli) +else() + install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT client) +endif() + +set_property(TARGET ${PROJECT_NAME} PROPERTY FOLDER "Client/X11") + diff --git a/client/X11/ModuleOptions.cmake b/client/X11/ModuleOptions.cmake new file mode 100644 index 0000000..4fef68a --- /dev/null +++ b/client/X11/ModuleOptions.cmake @@ -0,0 +1,4 @@ + +set(FREERDP_CLIENT_NAME "xfreerdp") +set(FREERDP_CLIENT_PLATFORM "X11") +set(FREERDP_CLIENT_VENDOR "FreeRDP") diff --git a/client/X11/cli/CMakeLists.txt b/client/X11/cli/CMakeLists.txt new file mode 100644 index 0000000..580337b --- /dev/null +++ b/client/X11/cli/CMakeLists.txt @@ -0,0 +1,49 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP X11 cmake build script +# +# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set(MODULE_NAME "xfreerdp-cli") +set(MODULE_PREFIX "FREERDP_CLIENT_X11") + +set(SRCS + xfreerdp.c +) + +add_executable(${MODULE_NAME} ${SRCS}) + +if (WITH_BINARY_VERSIONING) + set_target_properties(${MODULE_NAME} PROPERTIES OUTPUT_NAME "xfreerdp${PROJECT_VERSION_MAJOR}") +else() + set_target_properties(${MODULE_NAME} PROPERTIES OUTPUT_NAME "xfreerdp") +endif() +set_target_properties(${MODULE_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "..") + +list(APPEND LIBS + xfreerdp-client freerdp-client +) + +if(OPENBSD) + list(APPEND LIBS + ossaudio + ) +endif() + +target_link_libraries(${MODULE_NAME} PRIVATE ${LIBS}) + +install(TARGETS ${MODULE_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT client) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Client/X11") + diff --git a/client/X11/cli/xfreerdp.c b/client/X11/cli/xfreerdp.c new file mode 100644 index 0000000..33b2a96 --- /dev/null +++ b/client/X11/cli/xfreerdp.c @@ -0,0 +1,86 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Client + * + * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * Copyright 2012 HP Development Company, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <winpr/crt.h> +#include <winpr/synch.h> +#include <winpr/thread.h> + +#include <freerdp/streamdump.h> +#include <freerdp/freerdp.h> +#include <freerdp/client/cmdline.h> + +#include "../xf_client.h" +#include "../xfreerdp.h" + +int main(int argc, char* argv[]) +{ + int rc = 1; + int status = 0; + HANDLE thread = NULL; + xfContext* xfc = NULL; + DWORD dwExitCode = 0; + rdpContext* context = NULL; + rdpSettings* settings = NULL; + RDP_CLIENT_ENTRY_POINTS clientEntryPoints = { 0 }; + + clientEntryPoints.Size = sizeof(RDP_CLIENT_ENTRY_POINTS); + clientEntryPoints.Version = RDP_CLIENT_INTERFACE_VERSION; + + RdpClientEntry(&clientEntryPoints); + + context = freerdp_client_context_new(&clientEntryPoints); + if (!context) + return 1; + + settings = context->settings; + xfc = (xfContext*)context; + + status = freerdp_client_settings_parse_command_line(context->settings, argc, argv, FALSE); + if (status) + { + rc = freerdp_client_settings_command_line_status_print(settings, status, argc, argv); + + if (freerdp_settings_get_bool(settings, FreeRDP_ListMonitors)) + xf_list_monitors(xfc); + + goto out; + } + + if (!stream_dump_register_handlers(context, CONNECTION_STATE_MCS_CREATE_REQUEST, FALSE)) + goto out; + + if (freerdp_client_start(context) != 0) + goto out; + + thread = freerdp_client_get_thread(context); + + WaitForSingleObject(thread, INFINITE); + GetExitCodeThread(thread, &dwExitCode); + rc = xf_exit_code_from_disconnect_reason(dwExitCode); + + freerdp_client_stop(context); + +out: + freerdp_client_context_free(context); + + return rc; +} diff --git a/client/X11/man/CMakeLists.txt b/client/X11/man/CMakeLists.txt new file mode 100644 index 0000000..386f13d --- /dev/null +++ b/client/X11/man/CMakeLists.txt @@ -0,0 +1,11 @@ +set(DEPS + xfreerdp-channels.1.xml + xfreerdp-examples.1.xml + xfreerdp-envvar.1.xml + ) + +set(MANPAGE_NAME ${PROJECT_NAME}) +if (WITH_BINARY_VERSIONING) + set(MANPAGE_NAME ${PROJECT_NAME}${PROJECT_VERSION_MAJOR}) +endif() +generate_and_install_freerdp_man_from_xml(${PROJECT_NAME}.1 ${MANPAGE_NAME}.1 ${DEPS}) diff --git a/client/X11/man/xfreerdp-channels.1.xml b/client/X11/man/xfreerdp-channels.1.xml new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/client/X11/man/xfreerdp-channels.1.xml diff --git a/client/X11/man/xfreerdp-envvar.1.xml b/client/X11/man/xfreerdp-envvar.1.xml new file mode 100644 index 0000000..955adf5 --- /dev/null +++ b/client/X11/man/xfreerdp-envvar.1.xml @@ -0,0 +1,15 @@ +<refsect1> + <title>Environment variables</title> + + <variablelist> + <varlistentry> + <term>wlog environment variable</term> + <listitem> + <para>xfreerdp uses wLog as its log facility, you can refer to the + corresponding man page (wlog(7)) for more informations. Arguments passed + via the <replaceable>/log-level</replaceable> or <replaceable>/log-filters</replaceable> + have precedence over the environment variables.</para> + </listitem> + </varlistentry> + </variablelist> +</refsect1> diff --git a/client/X11/man/xfreerdp-examples.1.xml b/client/X11/man/xfreerdp-examples.1.xml new file mode 100644 index 0000000..3418143 --- /dev/null +++ b/client/X11/man/xfreerdp-examples.1.xml @@ -0,0 +1,95 @@ +<refsect1> + <title>Examples</title> + <variablelist> + <varlistentry> + <term><command>xfreerdp connection.rdp /p:Pwd123! /f</command></term> + <listitem> + <para>Connect in fullscreen mode using a stored configuration <replaceable>connection.rdp</replaceable> and the password <replaceable>Pwd123!</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>xfreerdp /u:USER /size:50%h /v:rdp.contoso.com</command></term> + <listitem> + <para>Connect to host <replaceable>rdp.contoso.com</replaceable> with user <replaceable>USER</replaceable> and a size of <replaceable>50 percent of the height</replaceable>. If width (w) is set instead of height (h) like /size:50%w. 50 percent of the width is used.</para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>xfreerdp /u:CONTOSO\\JohnDoe /p:Pwd123! /v:rdp.contoso.com</command></term> + <listitem> + <para>Connect to host <replaceable>rdp.contoso.com</replaceable> with user <replaceable>CONTOSO\\JohnDoe</replaceable> and password <replaceable>Pwd123!</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>xfreerdp /u:JohnDoe /p:Pwd123! /w:1366 /h:768 /v:192.168.1.100:4489</command></term> + <listitem> + <para>Connect to host <replaceable>192.168.1.100</replaceable> on port <replaceable>4489</replaceable> with user <replaceable>JohnDoe</replaceable>, password <replaceable>Pwd123!</replaceable>. The screen width is set to <replaceable>1366</replaceable> and the height to <replaceable>768</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>xfreerdp /u:JohnDoe /p:Pwd123! /vmconnect:C824F53E-95D2-46C6-9A18-23A5BB403532 /v:192.168.1.100</command></term> + <listitem> + <para>Establish a connection to host <replaceable>192.168.1.100</replaceable> with user <replaceable>JohnDoe</replaceable>, password <replaceable>Pwd123!</replaceable> and connect to Hyper-V console (use port 2179, disable negotiation) with VMID <replaceable>C824F53E-95D2-46C6-9A18-23A5BB403532</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>+clipboard</command></term> + <listitem> + <para>Activate clipboard redirection</para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/drive:home,/home/user</command></term> + <listitem> + <para>Activate drive redirection of <replaceable>/home/user</replaceable> as home drive</para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/smartcard:<device></command></term> + <listitem> + <para>Activate smartcard redirection for device <replaceable>device</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/printer:<device>,<driver></command></term> + <listitem> + <para>Activate printer redirection for printer <replaceable>device</replaceable> using driver <replaceable>driver</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/serial:<device></command></term> + <listitem> + <para>Activate serial port redirection for port <replaceable>device</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/parallel:<device></command></term> + <listitem> + <para>Activate parallel port redirection for port <replaceable>device</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/sound:sys:alsa</command></term> + <listitem> + <para>Activate audio output redirection using device <replaceable>sys:alsa</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/microphone:sys:alsa</command></term> + <listitem> + <para>Activate audio input redirection using device <replaceable>sys:alsa</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/multimedia:sys:alsa</command></term> + <listitem> + <para>Activate multimedia redirection using device <replaceable>sys:alsa</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/usb:id,dev:054c:0268</command></term> + <listitem> + <para>Activate USB device redirection for the device identified by <replaceable>054c:0268</replaceable></para> + </listitem> + </varlistentry> + </variablelist> +</refsect1> diff --git a/client/X11/man/xfreerdp.1.xml.in b/client/X11/man/xfreerdp.1.xml.in new file mode 100644 index 0000000..271e39d --- /dev/null +++ b/client/X11/man/xfreerdp.1.xml.in @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE refentry +PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" +"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [ + <!ENTITY syntax SYSTEM "freerdp-argument.1.xml"> + <!ENTITY channels SYSTEM "xfreerdp-channels.1.xml"> + <!ENTITY envvar SYSTEM "xfreerdp-envvar.1.xml"> + <!ENTITY examples SYSTEM "xfreerdp-examples.1.xml"> + ] +> + +<refentry> + <refentryinfo> + <date>@MAN_TODAY@</date> + <author> + <authorblurb><para>The FreeRDP Team</para></authorblurb> + </author> + </refentryinfo> + <refmeta> + <refentrytitle>@MANPAGE_NAME@</refentrytitle> + <manvolnum>1</manvolnum> + <refmiscinfo class="source">freerdp</refmiscinfo> + <refmiscinfo class="manual">@MANPAGE_NAME@</refmiscinfo> + </refmeta> + <refnamediv> + <refname><application>@MANPAGE_NAME@</application></refname> + <refpurpose>FreeRDP X11 client</refpurpose> + </refnamediv> + <refsynopsisdiv> + <refsynopsisdivinfo> + <date>@MAN_TODAY@</date> + </refsynopsisdivinfo> + <para> + <command>@MANPAGE_NAME@</command> [file] [options] [/v:server[:port]] + </para> + </refsynopsisdiv> + <refsect1> + <refsect1info> + <date>@MAN_TODAY@</date> + </refsect1info> + <title>DESCRIPTION</title> + <para> + <command>@MANPAGE_NAME@</command> is an X11 Remote Desktop Protocol (RDP) + client which is part of the FreeRDP project. An RDP server is built-in + to many editions of Windows. Alternative servers included ogon, gnome-remote-desktop, + xrdp and VRDP (VirtualBox). + </para> + </refsect1> + + &syntax; + + &channels; + + &envvar; + + &examples; + + <refsect1> + <title>LINKS</title> + <para> + <ulink url="http://www.freerdp.com/">http://www.freerdp.com/</ulink> + </para> + </refsect1> +</refentry> diff --git a/client/X11/resource/close.xbm b/client/X11/resource/close.xbm new file mode 100644 index 0000000..45c60e3 --- /dev/null +++ b/client/X11/resource/close.xbm @@ -0,0 +1,11 @@ +#define close_width 24 +#define close_height 24 +static unsigned char close_bits[] = +{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x7c, 0xfe, 0xff, 0x38, 0xfe, 0xff, 0x11, 0xff, 0xff, 0x83, 0xff, + 0xff, 0xc7, 0xff, 0xff, 0x83, 0xff, 0xff, 0x11, 0xff, 0xff, 0x38, 0xfe, + 0xff, 0x7c, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; diff --git a/client/X11/resource/lock.xbm b/client/X11/resource/lock.xbm new file mode 100644 index 0000000..12340f5 --- /dev/null +++ b/client/X11/resource/lock.xbm @@ -0,0 +1,11 @@ +#define lock_width 24 +#define lock_height 24 +static unsigned char lock_bits[] = +{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xff, + 0xff, 0x83, 0xff, 0xff, 0xc7, 0xff, 0xff, 0xc7, 0xff, 0xff, 0xc7, 0xff, + 0xff, 0x00, 0xfe, 0xff, 0x00, 0xfe, 0xff, 0xef, 0xff, 0xff, 0xef, 0xff, + 0xff, 0xef, 0xff, 0xff, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; diff --git a/client/X11/resource/minimize.xbm b/client/X11/resource/minimize.xbm new file mode 100644 index 0000000..c69d861 --- /dev/null +++ b/client/X11/resource/minimize.xbm @@ -0,0 +1,11 @@ +#define minimize_width 24 +#define minimize_height 24 +static unsigned char minimize_bits[] = +{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, + 0x3f, 0x00, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; diff --git a/client/X11/resource/restore.xbm b/client/X11/resource/restore.xbm new file mode 100644 index 0000000..e9909f5 --- /dev/null +++ b/client/X11/resource/restore.xbm @@ -0,0 +1,11 @@ +#define restore_width 24 +#define restore_height 24 +static unsigned char restore_bits[] = +{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x03, 0xff, 0xff, 0x03, 0xff, 0xff, 0x3b, 0xff, 0x7f, 0x20, 0xff, + 0x7f, 0x20, 0xff, 0x7f, 0x07, 0xff, 0x7f, 0xe7, 0xff, 0x7f, 0xe7, 0xff, + 0x7f, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; diff --git a/client/X11/resource/unlock.xbm b/client/X11/resource/unlock.xbm new file mode 100644 index 0000000..a809126 --- /dev/null +++ b/client/X11/resource/unlock.xbm @@ -0,0 +1,11 @@ +#define unlock_width 24 +#define unlock_height 24 +static unsigned char unlock_bits[] = +{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf3, 0xff, 0xff, 0xf3, 0xff, 0xff, 0x73, 0xfe, 0xff, 0x03, 0xfe, + 0x3f, 0x00, 0xfe, 0xff, 0x03, 0xfe, 0xff, 0x73, 0xfe, 0xff, 0xf3, 0xff, + 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; diff --git a/client/X11/xf_channels.c b/client/X11/xf_channels.c new file mode 100644 index 0000000..7177622 --- /dev/null +++ b/client/X11/xf_channels.c @@ -0,0 +1,132 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Client Channels + * + * Copyright 2013 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <winpr/assert.h> +#include <freerdp/gdi/video.h> +#include "xf_channels.h" + +#include "xf_client.h" +#include "xfreerdp.h" + +#include "xf_gfx.h" +#if defined(CHANNEL_TSMF_CLIENT) +#include "xf_tsmf.h" +#endif +#include "xf_rail.h" +#include "xf_cliprdr.h" +#include "xf_disp.h" +#include "xf_video.h" + +void xf_OnChannelConnectedEventHandler(void* context, const ChannelConnectedEventArgs* e) +{ + xfContext* xfc = (xfContext*)context; + rdpSettings* settings = NULL; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(e); + WINPR_ASSERT(e->name); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + if (FALSE) + { + } +#if defined(CHANNEL_TSMF_CLIENT) + else if (strcmp(e->name, TSMF_DVC_CHANNEL_NAME) == 0) + { + xf_tsmf_init(xfc, (TsmfClientContext*)e->pInterface); + } +#endif + else if (strcmp(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) + { + xf_graphics_pipeline_init(xfc, (RdpgfxClientContext*)e->pInterface); + } + else if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0) + { + xf_rail_init(xfc, (RailClientContext*)e->pInterface); + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + xf_cliprdr_init(xfc, (CliprdrClientContext*)e->pInterface); + } + else if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) == 0) + { + xf_disp_init(xfc->xfDisp, (DispClientContext*)e->pInterface); + } + else if (strcmp(e->name, VIDEO_CONTROL_DVC_CHANNEL_NAME) == 0) + { + if (freerdp_settings_get_bool(settings, FreeRDP_SoftwareGdi)) + gdi_video_control_init(xfc->common.context.gdi, (VideoClientContext*)e->pInterface); + else + xf_video_control_init(xfc, (VideoClientContext*)e->pInterface); + } + else + freerdp_client_OnChannelConnectedEventHandler(context, e); +} + +void xf_OnChannelDisconnectedEventHandler(void* context, const ChannelDisconnectedEventArgs* e) +{ + xfContext* xfc = (xfContext*)context; + rdpSettings* settings = NULL; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(e); + WINPR_ASSERT(e->name); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + if (FALSE) + { + } + else if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) == 0) + { + xf_disp_uninit(xfc->xfDisp, (DispClientContext*)e->pInterface); + } +#if defined(CHANNEL_TSMF_CLIENT) + else if (strcmp(e->name, TSMF_DVC_CHANNEL_NAME) == 0) + { + xf_tsmf_uninit(xfc, (TsmfClientContext*)e->pInterface); + } +#endif + else if (strcmp(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) + { + xf_graphics_pipeline_uninit(xfc, (RdpgfxClientContext*)e->pInterface); + } + else if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0) + { + xf_rail_uninit(xfc, (RailClientContext*)e->pInterface); + } + else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) + { + xf_cliprdr_uninit(xfc, (CliprdrClientContext*)e->pInterface); + } + else if (strcmp(e->name, VIDEO_CONTROL_DVC_CHANNEL_NAME) == 0) + { + if (freerdp_settings_get_bool(settings, FreeRDP_SoftwareGdi)) + gdi_video_control_uninit(xfc->common.context.gdi, (VideoClientContext*)e->pInterface); + else + xf_video_control_uninit(xfc, (VideoClientContext*)e->pInterface); + } + else + freerdp_client_OnChannelDisconnectedEventHandler(context, e); +} diff --git a/client/X11/xf_channels.h b/client/X11/xf_channels.h new file mode 100644 index 0000000..86b00b9 --- /dev/null +++ b/client/X11/xf_channels.h @@ -0,0 +1,37 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Client Channels + * + * Copyright 2013 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_X11_CHANNELS_H +#define FREERDP_CLIENT_X11_CHANNELS_H + +#include <freerdp/freerdp.h> +#include <freerdp/client/channels.h> +#include <freerdp/client/rdpei.h> +#include <freerdp/client/rail.h> +#include <freerdp/client/cliprdr.h> +#include <freerdp/client/rdpgfx.h> +#include <freerdp/client/encomsp.h> +#include <freerdp/client/disp.h> +#include <freerdp/client/geometry.h> +#include <freerdp/client/video.h> + +void xf_OnChannelConnectedEventHandler(void* context, const ChannelConnectedEventArgs* e); +void xf_OnChannelDisconnectedEventHandler(void* context, const ChannelDisconnectedEventArgs* e); + +#endif /* FREERDP_CLIENT_X11_CHANNELS_H */ diff --git a/client/X11/xf_client.c b/client/X11/xf_client.c new file mode 100644 index 0000000..0f51745 --- /dev/null +++ b/client/X11/xf_client.c @@ -0,0 +1,2000 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Client Interface + * + * Copyright 2013 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * Copyright 2013 Corey Clayton <can.of.tuna@gmail.com> + * Copyright 2014 Thincast Technologies GmbH + * Copyright 2014 Norbert Federa <norbert.federa@thincast.com> + * Copyright 2016 Armin Novak <armin.novak@thincast.com> + * Copyright 2016 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <math.h> +#include <winpr/assert.h> +#include <winpr/sspicli.h> + +#include <float.h> + +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <X11/Xatom.h> + +#ifdef WITH_XRENDER +#include <X11/extensions/Xrender.h> +#endif + +#ifdef WITH_XI +#include <X11/extensions/XInput.h> +#include <X11/extensions/XInput2.h> +#endif + +#ifdef WITH_XCURSOR +#include <X11/Xcursor/Xcursor.h> +#endif + +#ifdef WITH_XINERAMA +#include <X11/extensions/Xinerama.h> +#endif + +#include <X11/XKBlib.h> + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <locale.h> +#include <unistd.h> +#include <string.h> +#include <termios.h> +#include <pthread.h> +#include <sys/wait.h> +#include <sys/types.h> +#include <sys/select.h> +#include <fcntl.h> +#include <sys/stat.h> + +#include <freerdp/freerdp.h> +#include <freerdp/constants.h> +#include <freerdp/codec/nsc.h> +#include <freerdp/codec/rfx.h> +#include <freerdp/codec/color.h> +#include <freerdp/codec/bitmap.h> + +#include <freerdp/utils/signal.h> +#include <freerdp/utils/passphrase.h> +#include <freerdp/client/cliprdr.h> +#include <freerdp/client/channels.h> + +#include <freerdp/client/file.h> +#include <freerdp/client/cmdline.h> + +#include <winpr/crt.h> +#include <winpr/synch.h> +#include <winpr/file.h> +#include <winpr/print.h> +#include <winpr/sysinfo.h> + +#include "xf_rail.h" +#if defined(CHANNEL_TSMF_CLIENT) +#include "xf_tsmf.h" +#endif +#include "xf_event.h" +#include "xf_input.h" +#include "xf_cliprdr.h" +#include "xf_disp.h" +#include "xf_video.h" +#include "xf_monitor.h" +#include "xf_graphics.h" +#include "xf_keyboard.h" +#include "xf_channels.h" +#include "xfreerdp.h" +#include "xf_utils.h" + +#include <freerdp/log.h> +#define TAG CLIENT_TAG("x11") + +#define MIN_PIXEL_DIFF 0.001 + +struct xf_exit_code_map_t +{ + DWORD error; + int rc; +}; +static const struct xf_exit_code_map_t xf_exit_code_map[] = { + { FREERDP_ERROR_AUTHENTICATION_FAILED, XF_EXIT_AUTH_FAILURE }, + { FREERDP_ERROR_SECURITY_NEGO_CONNECT_FAILED, XF_EXIT_NEGO_FAILURE }, + { FREERDP_ERROR_CONNECT_LOGON_FAILURE, XF_EXIT_LOGON_FAILURE }, + { FREERDP_ERROR_CONNECT_ACCOUNT_LOCKED_OUT, XF_EXIT_ACCOUNT_LOCKED_OUT }, + { FREERDP_ERROR_PRE_CONNECT_FAILED, XF_EXIT_PRE_CONNECT_FAILED }, + { FREERDP_ERROR_CONNECT_UNDEFINED, XF_EXIT_CONNECT_UNDEFINED }, + { FREERDP_ERROR_POST_CONNECT_FAILED, XF_EXIT_POST_CONNECT_FAILED }, + { FREERDP_ERROR_DNS_ERROR, XF_EXIT_DNS_ERROR }, + { FREERDP_ERROR_DNS_NAME_NOT_FOUND, XF_EXIT_DNS_NAME_NOT_FOUND }, + { FREERDP_ERROR_CONNECT_FAILED, XF_EXIT_CONNECT_FAILED }, + { FREERDP_ERROR_MCS_CONNECT_INITIAL_ERROR, XF_EXIT_MCS_CONNECT_INITIAL_ERROR }, + { FREERDP_ERROR_TLS_CONNECT_FAILED, XF_EXIT_TLS_CONNECT_FAILED }, + { FREERDP_ERROR_INSUFFICIENT_PRIVILEGES, XF_EXIT_INSUFFICIENT_PRIVILEGES }, + { FREERDP_ERROR_CONNECT_CANCELLED, XF_EXIT_CONNECT_CANCELLED }, + { FREERDP_ERROR_CONNECT_TRANSPORT_FAILED, XF_EXIT_CONNECT_TRANSPORT_FAILED }, + { FREERDP_ERROR_CONNECT_PASSWORD_EXPIRED, XF_EXIT_CONNECT_PASSWORD_EXPIRED }, + { FREERDP_ERROR_CONNECT_PASSWORD_MUST_CHANGE, XF_EXIT_CONNECT_PASSWORD_MUST_CHANGE }, + { FREERDP_ERROR_CONNECT_KDC_UNREACHABLE, XF_EXIT_CONNECT_KDC_UNREACHABLE }, + { FREERDP_ERROR_CONNECT_ACCOUNT_DISABLED, XF_EXIT_CONNECT_ACCOUNT_DISABLED }, + { FREERDP_ERROR_CONNECT_PASSWORD_CERTAINLY_EXPIRED, + XF_EXIT_CONNECT_PASSWORD_CERTAINLY_EXPIRED }, + { FREERDP_ERROR_CONNECT_CLIENT_REVOKED, XF_EXIT_CONNECT_CLIENT_REVOKED }, + { FREERDP_ERROR_CONNECT_WRONG_PASSWORD, XF_EXIT_CONNECT_WRONG_PASSWORD }, + { FREERDP_ERROR_CONNECT_ACCESS_DENIED, XF_EXIT_CONNECT_ACCESS_DENIED }, + { FREERDP_ERROR_CONNECT_ACCOUNT_RESTRICTION, XF_EXIT_CONNECT_ACCOUNT_RESTRICTION }, + { FREERDP_ERROR_CONNECT_ACCOUNT_EXPIRED, XF_EXIT_CONNECT_ACCOUNT_EXPIRED }, + { FREERDP_ERROR_CONNECT_LOGON_TYPE_NOT_GRANTED, XF_EXIT_CONNECT_LOGON_TYPE_NOT_GRANTED }, + { FREERDP_ERROR_CONNECT_NO_OR_MISSING_CREDENTIALS, XF_EXIT_CONNECT_NO_OR_MISSING_CREDENTIALS } +}; + +static BOOL xf_setup_x11(xfContext* xfc); +static void xf_teardown_x11(xfContext* xfc); + +static int xf_map_error_to_exit_code(DWORD error) +{ + for (size_t x = 0; x < ARRAYSIZE(xf_exit_code_map); x++) + { + const struct xf_exit_code_map_t* cur = &xf_exit_code_map[x]; + if (cur->error == error) + return cur->rc; + } + + return XF_EXIT_CONN_FAILED; +} +static int (*def_error_handler)(Display*, XErrorEvent*); +static int xf_error_handler_ex(Display* d, XErrorEvent* ev); +static void xf_check_extensions(xfContext* context); +static void xf_window_free(xfContext* xfc); +static BOOL xf_get_pixmap_info(xfContext* xfc); + +#ifdef WITH_XRENDER +static void xf_draw_screen_scaled(xfContext* xfc, int x, int y, int w, int h) +{ + XTransform transform; + Picture windowPicture = 0; + Picture primaryPicture = 0; + XRenderPictureAttributes pa; + XRenderPictFormat* picFormat = NULL; + double xScalingFactor = NAN; + double yScalingFactor = NAN; + int x2 = 0; + int y2 = 0; + const char* filter = NULL; + rdpSettings* settings = NULL; + WINPR_ASSERT(xfc); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + if (xfc->scaledWidth <= 0 || xfc->scaledHeight <= 0) + { + WLog_ERR(TAG, "the current window dimensions are invalid"); + return; + } + + if (freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth) <= 0 || + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight) <= 0) + { + WLog_ERR(TAG, "the window dimensions are invalid"); + return; + } + + xScalingFactor = + freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth) / (double)xfc->scaledWidth; + yScalingFactor = + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight) / (double)xfc->scaledHeight; + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XSetForeground(xfc->display, xfc->gc, 0); + /* Black out possible space between desktop and window borders */ + { + XRectangle box1 = { 0, 0, xfc->window->width, xfc->window->height }; + XRectangle box2 = { xfc->offset_x, xfc->offset_y, xfc->scaledWidth, xfc->scaledHeight }; + Region reg1 = XCreateRegion(); + Region reg2 = XCreateRegion(); + XUnionRectWithRegion(&box1, reg1, reg1); + XUnionRectWithRegion(&box2, reg2, reg2); + + if (XSubtractRegion(reg1, reg2, reg1) && !XEmptyRegion(reg1)) + { + XSetRegion(xfc->display, xfc->gc, reg1); + XFillRectangle(xfc->display, xfc->window->handle, xfc->gc, 0, 0, xfc->window->width, + xfc->window->height); + XSetClipMask(xfc->display, xfc->gc, None); + } + + XDestroyRegion(reg1); + XDestroyRegion(reg2); + } + picFormat = XRenderFindVisualFormat(xfc->display, xfc->visual); + pa.subwindow_mode = IncludeInferiors; + primaryPicture = + XRenderCreatePicture(xfc->display, xfc->primary, picFormat, CPSubwindowMode, &pa); + windowPicture = + XRenderCreatePicture(xfc->display, xfc->window->handle, picFormat, CPSubwindowMode, &pa); + /* avoid blurry filter when scaling factor is 2x, 3x, etc + * useful when the client has high-dpi monitor */ + filter = FilterBilinear; + if (fabs(xScalingFactor - yScalingFactor) < MIN_PIXEL_DIFF) + { + const double inverseX = 1.0 / xScalingFactor; + const double inverseRoundedX = round(inverseX); + const double absInverse = fabs(inverseX - inverseRoundedX); + + if (absInverse < MIN_PIXEL_DIFF) + filter = FilterNearest; + } + XRenderSetPictureFilter(xfc->display, primaryPicture, filter, 0, 0); + transform.matrix[0][0] = XDoubleToFixed(xScalingFactor); + transform.matrix[0][1] = XDoubleToFixed(0.0); + transform.matrix[0][2] = XDoubleToFixed(0.0); + transform.matrix[1][0] = XDoubleToFixed(0.0); + transform.matrix[1][1] = XDoubleToFixed(yScalingFactor); + transform.matrix[1][2] = XDoubleToFixed(0.0); + transform.matrix[2][0] = XDoubleToFixed(0.0); + transform.matrix[2][1] = XDoubleToFixed(0.0); + transform.matrix[2][2] = XDoubleToFixed(1.0); + /* calculate and fix up scaled coordinates */ + x2 = x + w; + y2 = y + h; + x = ((int)floor(x / xScalingFactor)) - 1; + y = ((int)floor(y / yScalingFactor)) - 1; + w = ((int)ceil(x2 / xScalingFactor)) + 1 - x; + h = ((int)ceil(y2 / yScalingFactor)) + 1 - y; + XRenderSetPictureTransform(xfc->display, primaryPicture, &transform); + XRenderComposite(xfc->display, PictOpSrc, primaryPicture, 0, windowPicture, x, y, 0, 0, + xfc->offset_x + x, xfc->offset_y + y, w, h); + XRenderFreePicture(xfc->display, primaryPicture); + XRenderFreePicture(xfc->display, windowPicture); +} + +BOOL xf_picture_transform_required(xfContext* xfc) +{ + rdpSettings* settings = NULL; + + WINPR_ASSERT(xfc); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + if ((xfc->offset_x != 0) || (xfc->offset_y != 0) || + (xfc->scaledWidth != (INT64)freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth)) || + (xfc->scaledHeight != (INT64)freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight))) + { + return TRUE; + } + + return FALSE; +} +#endif /* WITH_XRENDER defined */ + +void xf_draw_screen_(xfContext* xfc, int x, int y, int w, int h, const char* fkt, const char* file, + int line) +{ + if (!xfc) + { + WLog_DBG(TAG, "called from [%s] xfc=%p", fkt, xfc); + return; + } + + if (w == 0 || h == 0) + { + WLog_WARN(TAG, "invalid width and/or height specified: w=%d h=%d", w, h); + return; + } + + if (!xfc->window) + { + WLog_WARN(TAG, "invalid xfc->window=%p", xfc->window); + return; + } + +#ifdef WITH_XRENDER + + if (xf_picture_transform_required(xfc)) + { + xf_draw_screen_scaled(xfc, x, y, w, h); + return; + } + +#endif + XCopyArea(xfc->display, xfc->primary, xfc->window->handle, xfc->gc, x, y, w, h, x, y); +} + +static BOOL xf_desktop_resize(rdpContext* context) +{ + rdpSettings* settings = NULL; + xfContext* xfc = (xfContext*)context; + + WINPR_ASSERT(xfc); + + settings = context->settings; + WINPR_ASSERT(settings); + + if (xfc->primary) + { + BOOL same = (xfc->primary == xfc->drawing) ? TRUE : FALSE; + XFreePixmap(xfc->display, xfc->primary); + + WINPR_ASSERT(xfc->depth != 0); + if (!(xfc->primary = XCreatePixmap( + xfc->display, xfc->drawable, + freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth), + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight), xfc->depth))) + return FALSE; + + if (same) + xfc->drawing = xfc->primary; + } + +#ifdef WITH_XRENDER + + if (!freerdp_settings_get_bool(settings, FreeRDP_SmartSizing)) + { + xfc->scaledWidth = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth); + xfc->scaledHeight = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight); + } + +#endif + + if (!xfc->fullscreen) + { + xf_ResizeDesktopWindow(xfc, xfc->window, + freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth), + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight)); + } + else + { +#ifdef WITH_XRENDER + + if (!freerdp_settings_get_bool(settings, FreeRDP_SmartSizing)) +#endif + { + /* Update the saved width and height values the window will be + * resized to when toggling out of fullscreen */ + xfc->savedWidth = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth); + xfc->savedHeight = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight); + } + + XSetFunction(xfc->display, xfc->gc, GXcopy); + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XSetForeground(xfc->display, xfc->gc, 0); + XFillRectangle(xfc->display, xfc->drawable, xfc->gc, 0, 0, + freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth), + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight)); + } + + return TRUE; +} + +static BOOL xf_paint(xfContext* xfc, const GDI_RGN* region) +{ + WINPR_ASSERT(xfc); + WINPR_ASSERT(region); + + if (xfc->remote_app) + { + const RECTANGLE_16 rect = { .left = region->x, + .top = region->y, + .right = region->x + region->w, + .bottom = region->y + region->h }; + xf_rail_paint(xfc, &rect); + } + else + { + XPutImage(xfc->display, xfc->primary, xfc->gc, xfc->image, region->x, region->y, region->x, + region->y, region->w, region->h); + xf_draw_screen(xfc, region->x, region->y, region->w, region->h); + } + return TRUE; +} + +static BOOL xf_end_paint(rdpContext* context) +{ + xfContext* xfc = (xfContext*)context; + rdpGdi* gdi = context->gdi; + + if (gdi->suppressOutput) + return TRUE; + + HGDI_DC hdc = gdi->primary->hdc; + + if (!xfc->complex_regions) + { + const GDI_RGN* rgn = hdc->hwnd->invalid; + if (rgn->null) + return TRUE; + xf_lock_x11(xfc); + if (!xf_paint(xfc, rgn)) + return FALSE; + xf_unlock_x11(xfc); + } + else + { + const INT32 ninvalid = hdc->hwnd->ninvalid; + const GDI_RGN* cinvalid = hdc->hwnd->cinvalid; + + if (hdc->hwnd->ninvalid < 1) + return TRUE; + + xf_lock_x11(xfc); + + for (INT32 i = 0; i < ninvalid; i++) + { + const GDI_RGN* rgn = &cinvalid[i]; + if (!xf_paint(xfc, rgn)) + return FALSE; + } + + XFlush(xfc->display); + xf_unlock_x11(xfc); + } + + hdc->hwnd->invalid->null = TRUE; + hdc->hwnd->ninvalid = 0; + return TRUE; +} + +static BOOL xf_sw_desktop_resize(rdpContext* context) +{ + rdpGdi* gdi = context->gdi; + xfContext* xfc = (xfContext*)context; + rdpSettings* settings = context->settings; + BOOL ret = FALSE; + xf_lock_x11(xfc); + + if (!gdi_resize(gdi, freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth), + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight))) + goto out; + + if (xfc->image) + { + xfc->image->data = NULL; + XDestroyImage(xfc->image); + } + + WINPR_ASSERT(xfc->depth != 0); + if (!(xfc->image = XCreateImage(xfc->display, xfc->visual, xfc->depth, ZPixmap, 0, + (char*)gdi->primary_buffer, gdi->width, gdi->height, + xfc->scanline_pad, gdi->stride))) + { + goto out; + } + + xfc->image->byte_order = LSBFirst; + xfc->image->bitmap_bit_order = LSBFirst; + ret = xf_desktop_resize(context); +out: + xf_unlock_x11(xfc); + return ret; +} + +static BOOL xf_process_x_events(freerdp* instance) +{ + BOOL status = TRUE; + int pending_status = 1; + xfContext* xfc = (xfContext*)instance->context; + + while (pending_status) + { + xf_lock_x11(xfc); + pending_status = XPending(xfc->display); + + if (pending_status) + { + XEvent xevent = { 0 }; + + XNextEvent(xfc->display, &xevent); + status = xf_event_process(instance, &xevent); + } + xf_unlock_x11(xfc); + if (!status) + break; + } + + return status; +} + +static char* xf_window_get_title(rdpSettings* settings) +{ + BOOL port = 0; + char* windowTitle = NULL; + size_t size = 0; + const char* prefix = "FreeRDP:"; + + if (!settings) + return NULL; + + const char* name = freerdp_settings_get_string(settings, FreeRDP_ServerHostname); + const char* title = freerdp_settings_get_string(settings, FreeRDP_WindowTitle); + + if (title) + return _strdup(title); + + port = (freerdp_settings_get_uint32(settings, FreeRDP_ServerPort) != 3389); + /* Just assume a window title is never longer than a filename... */ + size = strnlen(name, MAX_PATH) + 16; + windowTitle = calloc(size, sizeof(char)); + + if (!windowTitle) + return NULL; + + if (!port) + sprintf_s(windowTitle, size, "%s %s", prefix, name); + else + sprintf_s(windowTitle, size, "%s %s:%i", prefix, name, + freerdp_settings_get_uint32(settings, FreeRDP_ServerPort)); + + return windowTitle; +} + +BOOL xf_create_window(xfContext* xfc) +{ + XGCValues gcv = { 0 }; + XEvent xevent = { 0 }; + char* windowTitle = NULL; + + WINPR_ASSERT(xfc); + rdpSettings* settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + int width = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth); + int height = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight); + + const XSetWindowAttributes empty = { 0 }; + xfc->attribs = empty; + + if (xfc->remote_app) + xfc->depth = 32; + else + xfc->depth = DefaultDepthOfScreen(xfc->screen); + + XVisualInfo vinfo = { 0 }; + if (XMatchVisualInfo(xfc->display, xfc->screen_number, xfc->depth, TrueColor, &vinfo)) + { + Window root = XDefaultRootWindow(xfc->display); + xfc->visual = vinfo.visual; + xfc->attribs.colormap = xfc->colormap = + XCreateColormap(xfc->display, root, vinfo.visual, AllocNone); + } + else + { + if (xfc->remote_app) + { + WLog_WARN(TAG, "running in remote app mode, but XServer does not support transparency"); + WLog_WARN(TAG, "display of remote applications might be distorted (black frames, ...)"); + } + xfc->depth = DefaultDepthOfScreen(xfc->screen); + xfc->visual = DefaultVisual(xfc->display, xfc->screen_number); + xfc->attribs.colormap = xfc->colormap = DefaultColormap(xfc->display, xfc->screen_number); + } + + /* + * Detect if the server visual has an inverted colormap + * (BGR vs RGB, or red being the least significant byte) + */ + if (vinfo.red_mask & 0xFF) + { + xfc->invert = FALSE; + } + + if (!xfc->remote_app) + { + xfc->attribs.background_pixel = BlackPixelOfScreen(xfc->screen); + xfc->attribs.border_pixel = WhitePixelOfScreen(xfc->screen); + xfc->attribs.backing_store = xfc->primary ? NotUseful : Always; + xfc->attribs.override_redirect = False; + + xfc->attribs.bit_gravity = NorthWestGravity; + xfc->attribs.win_gravity = NorthWestGravity; + xfc->attribs_mask = CWBackPixel | CWBackingStore | CWOverrideRedirect | CWColormap | + CWBorderPixel | CWWinGravity | CWBitGravity; + +#ifdef WITH_XRENDER + xfc->offset_x = 0; + xfc->offset_y = 0; +#endif + windowTitle = xf_window_get_title(settings); + + if (!windowTitle) + return FALSE; + +#ifdef WITH_XRENDER + + if (freerdp_settings_get_bool(settings, FreeRDP_SmartSizing) && !xfc->fullscreen) + { + if (freerdp_settings_get_uint32(settings, FreeRDP_SmartSizingWidth) > 0) + width = freerdp_settings_get_uint32(settings, FreeRDP_SmartSizingWidth); + + if (freerdp_settings_get_uint32(settings, FreeRDP_SmartSizingHeight) > 0) + height = freerdp_settings_get_uint32(settings, FreeRDP_SmartSizingHeight); + + xfc->scaledWidth = width; + xfc->scaledHeight = height; + } + +#endif + xfc->window = xf_CreateDesktopWindow(xfc, windowTitle, width, height); + free(windowTitle); + + if (xfc->fullscreen) + xf_SetWindowFullscreen(xfc, xfc->window, xfc->fullscreen); + + xfc->unobscured = (xevent.xvisibility.state == VisibilityUnobscured); + XSetWMProtocols(xfc->display, xfc->window->handle, &(xfc->WM_DELETE_WINDOW), 1); + xfc->drawable = xfc->window->handle; + } + else + { + xfc->attribs.border_pixel = 0; + xfc->attribs.background_pixel = 0; + xfc->attribs.backing_store = xfc->primary ? NotUseful : Always; + xfc->attribs.override_redirect = False; + + xfc->attribs.bit_gravity = NorthWestGravity; + xfc->attribs.win_gravity = NorthWestGravity; + xfc->attribs_mask = CWBackPixel | CWBackingStore | CWOverrideRedirect | CWColormap | + CWBorderPixel | CWWinGravity | CWBitGravity; + + xfc->drawable = xf_CreateDummyWindow(xfc); + } + + if (!xfc->gc) + xfc->gc = XCreateGC(xfc->display, xfc->drawable, GCGraphicsExposures, &gcv); + + WINPR_ASSERT(xfc->depth != 0); + if (!xfc->primary) + xfc->primary = + XCreatePixmap(xfc->display, xfc->drawable, + freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth), + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight), xfc->depth); + + xfc->drawing = xfc->primary; + + if (!xfc->bitmap_mono) + xfc->bitmap_mono = XCreatePixmap(xfc->display, xfc->drawable, 8, 8, 1); + + if (!xfc->gc_mono) + xfc->gc_mono = XCreateGC(xfc->display, xfc->bitmap_mono, GCGraphicsExposures, &gcv); + + XSetFunction(xfc->display, xfc->gc, GXcopy); + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XSetForeground(xfc->display, xfc->gc, BlackPixelOfScreen(xfc->screen)); + XFillRectangle(xfc->display, xfc->primary, xfc->gc, 0, 0, + freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth), + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight)); + XFlush(xfc->display); + + return TRUE; +} + +BOOL xf_create_image(xfContext* xfc) +{ + WINPR_ASSERT(xfc); + if (!xfc->image) + { + const rdpSettings* settings = xfc->common.context.settings; + rdpGdi* cgdi = xfc->common.context.gdi; + WINPR_ASSERT(cgdi); + + WINPR_ASSERT(xfc->depth != 0); + xfc->image = XCreateImage(xfc->display, xfc->visual, xfc->depth, ZPixmap, 0, + (char*)cgdi->primary_buffer, + freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth), + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight), + xfc->scanline_pad, cgdi->stride); + xfc->image->byte_order = LSBFirst; + xfc->image->bitmap_bit_order = LSBFirst; + } + return TRUE; +} + +static void xf_window_free(xfContext* xfc) +{ + if (xfc->window) + { + xf_DestroyDesktopWindow(xfc, xfc->window); + xfc->window = NULL; + } + +#if defined(CHANNEL_TSMF_CLIENT) + if (xfc->xv_context) + { + xf_tsmf_uninit(xfc, NULL); + xfc->xv_context = NULL; + } +#endif + + if (xfc->image) + { + xfc->image->data = NULL; + XDestroyImage(xfc->image); + xfc->image = NULL; + } + + if (xfc->bitmap_mono) + { + XFreePixmap(xfc->display, xfc->bitmap_mono); + xfc->bitmap_mono = 0; + } + + if (xfc->gc_mono) + { + XFreeGC(xfc->display, xfc->gc_mono); + xfc->gc_mono = 0; + } + + if (xfc->primary) + { + XFreePixmap(xfc->display, xfc->primary); + xfc->primary = 0; + } + + if (xfc->gc) + { + XFreeGC(xfc->display, xfc->gc); + xfc->gc = 0; + } +} + +void xf_toggle_fullscreen(xfContext* xfc) +{ + WindowStateChangeEventArgs e; + rdpContext* context = (rdpContext*)xfc; + rdpSettings* settings = context->settings; + + /* + when debugging, ungrab keyboard when toggling fullscreen + to allow keyboard usage on the debugger + */ + if (xfc->debug) + xf_ungrab(xfc); + + xfc->fullscreen = (xfc->fullscreen) ? FALSE : TRUE; + xfc->decorations = + (xfc->fullscreen) ? FALSE : freerdp_settings_get_bool(settings, FreeRDP_Decorations); + xf_SetWindowFullscreen(xfc, xfc->window, xfc->fullscreen); + EventArgsInit(&e, "xfreerdp"); + e.state = xfc->fullscreen ? FREERDP_WINDOW_STATE_FULLSCREEN : 0; + PubSub_OnWindowStateChange(context->pubSub, context, &e); +} + +void xf_lock_x11_(xfContext* xfc, const char* fkt) +{ + + if (!xfc->UseXThreads) + WaitForSingleObject(xfc->mutex, INFINITE); + else + XLockDisplay(xfc->display); + + xfc->locked++; + WLog_VRB(TAG, "[%" PRIu32 "] from %s", xfc->locked, fkt); +} + +void xf_unlock_x11_(xfContext* xfc, const char* fkt) +{ + if (xfc->locked == 0) + WLog_WARN(TAG, "X11: trying to unlock although not locked!"); + + WLog_VRB(TAG, "[%" PRIu32 "] from %s", xfc->locked - 1, fkt); + if (!xfc->UseXThreads) + ReleaseMutex(xfc->mutex); + else + XUnlockDisplay(xfc->display); + xfc->locked--; +} + +static BOOL xf_get_pixmap_info(xfContext* xfc) +{ + int pf_count = 0; + XPixmapFormatValues* pfs = NULL; + + WINPR_ASSERT(xfc->display); + pfs = XListPixmapFormats(xfc->display, &pf_count); + + if (!pfs) + { + WLog_ERR(TAG, "XListPixmapFormats failed"); + return 1; + } + + WINPR_ASSERT(xfc->depth != 0); + for (int i = 0; i < pf_count; i++) + { + const XPixmapFormatValues* pf = &pfs[i]; + + if (pf->depth == xfc->depth) + { + xfc->scanline_pad = pf->scanline_pad; + break; + } + } + + XFree(pfs); + if ((xfc->visual == NULL) || (xfc->scanline_pad == 0)) + return FALSE; + + return TRUE; +} + +static int xf_error_handler(Display* d, XErrorEvent* ev) +{ + char buf[256] = { 0 }; + XGetErrorText(d, ev->error_code, buf, sizeof(buf)); + WLog_ERR(TAG, "%s", buf); + winpr_log_backtrace(TAG, WLOG_ERROR, 20); + +#if 0 + const BOOL do_abort = TRUE; + if (do_abort) + abort(); +#endif + + if (def_error_handler) + return def_error_handler(d, ev); + + return 0; +} + +static int xf_error_handler_ex(Display* d, XErrorEvent* ev) +{ + /* + * ungrab the keyboard, in case a debugger is running in + * another window. This make xf_error_handler() a potential + * debugger breakpoint. + */ + + XUngrabKeyboard(d, CurrentTime); + XUngrabPointer(d, CurrentTime); + return xf_error_handler(d, ev); +} + +static BOOL xf_play_sound(rdpContext* context, const PLAY_SOUND_UPDATE* play_sound) +{ + xfContext* xfc = (xfContext*)context; + WINPR_UNUSED(play_sound); + XkbBell(xfc->display, None, 100, 0); + return TRUE; +} + +static void xf_check_extensions(xfContext* context) +{ + int xkb_opcode = 0; + int xkb_event = 0; + int xkb_error = 0; + int xkb_major = XkbMajorVersion; + int xkb_minor = XkbMinorVersion; + + if (XkbLibraryVersion(&xkb_major, &xkb_minor) && + XkbQueryExtension(context->display, &xkb_opcode, &xkb_event, &xkb_error, &xkb_major, + &xkb_minor)) + { + context->xkbAvailable = TRUE; + } + +#ifdef WITH_XRENDER + { + int xrender_event_base = 0; + int xrender_error_base = 0; + + if (XRenderQueryExtension(context->display, &xrender_event_base, &xrender_error_base)) + { + context->xrenderAvailable = TRUE; + } + } +#endif +} + +#ifdef WITH_XI +/* Input device which does NOT have the correct mapping. We must disregard */ +/* this device when trying to find the input device which is the pointer. */ +static const char TEST_PTR_STR[] = "Virtual core XTEST pointer"; +static const size_t TEST_PTR_LEN = sizeof(TEST_PTR_STR) / sizeof(char); +#endif /* WITH_XI */ + +static void xf_get_x11_button_map(xfContext* xfc, unsigned char* x11_map) +{ +#ifdef WITH_XI + int opcode = 0; + int event = 0; + int error = 0; + XDevice* ptr_dev = NULL; + XExtensionVersion* version = NULL; + XDeviceInfo* devices1 = NULL; + XIDeviceInfo* devices2 = NULL; + int num_devices = 0; + + if (XQueryExtension(xfc->display, "XInputExtension", &opcode, &event, &error)) + { + WLog_DBG(TAG, "Searching for XInput pointer device"); + ptr_dev = NULL; + /* loop through every device, looking for a pointer */ + version = XGetExtensionVersion(xfc->display, INAME); + + if (version->major_version >= 2) + { + /* XID of pointer device using XInput version 2 */ + devices2 = XIQueryDevice(xfc->display, XIAllDevices, &num_devices); + + if (devices2) + { + for (int i = 0; i < num_devices; ++i) + { + if ((devices2[i].use == XISlavePointer) && + (strncmp(devices2[i].name, TEST_PTR_STR, TEST_PTR_LEN) != 0)) + { + ptr_dev = XOpenDevice(xfc->display, devices2[i].deviceid); + if (ptr_dev) + break; + } + } + + XIFreeDeviceInfo(devices2); + } + } + else + { + /* XID of pointer device using XInput version 1 */ + devices1 = XListInputDevices(xfc->display, &num_devices); + + if (devices1) + { + for (int i = 0; i < num_devices; ++i) + { + if ((devices1[i].use == IsXExtensionPointer) && + (strncmp(devices1[i].name, TEST_PTR_STR, TEST_PTR_LEN) != 0)) + { + ptr_dev = XOpenDevice(xfc->display, devices1[i].id); + if (ptr_dev) + break; + } + } + + XFreeDeviceList(devices1); + } + } + + XFree(version); + + /* get button mapping from input extension if there is a pointer device; */ + /* otherwise leave unchanged. */ + if (ptr_dev) + { + WLog_DBG(TAG, "Pointer device: %d", ptr_dev->device_id); + XGetDeviceButtonMapping(xfc->display, ptr_dev, x11_map, NUM_BUTTONS_MAPPED); + XCloseDevice(xfc->display, ptr_dev); + } + else + { + WLog_DBG(TAG, "No pointer device found!"); + } + } + else +#endif /* WITH_XI */ + { + WLog_DBG(TAG, "Get global pointer mapping (no XInput)"); + XGetPointerMapping(xfc->display, x11_map, NUM_BUTTONS_MAPPED); + } +} + +/* Assignment of physical (not logical) mouse buttons to wire flags. */ +/* Notice that the middle button is 2 in X11, but 3 in RDP. */ +static const button_map xf_button_flags[NUM_BUTTONS_MAPPED] = { + { Button1, PTR_FLAGS_BUTTON1 }, + { Button2, PTR_FLAGS_BUTTON3 }, + { Button3, PTR_FLAGS_BUTTON2 }, + { Button4, PTR_FLAGS_WHEEL | 0x78 }, + /* Negative value is 9bit twos complement */ + { Button5, PTR_FLAGS_WHEEL | PTR_FLAGS_WHEEL_NEGATIVE | (0x100 - 0x78) }, + { 6, PTR_FLAGS_HWHEEL | PTR_FLAGS_WHEEL_NEGATIVE | (0x100 - 0x78) }, + { 7, PTR_FLAGS_HWHEEL | 0x78 }, + { 8, PTR_XFLAGS_BUTTON1 }, + { 9, PTR_XFLAGS_BUTTON2 }, + { 97, PTR_XFLAGS_BUTTON1 }, + { 112, PTR_XFLAGS_BUTTON2 } +}; + +static UINT16 get_flags_for_button(int button) +{ + for (size_t x = 0; x < ARRAYSIZE(xf_button_flags); x++) + { + const button_map* map = &xf_button_flags[x]; + + if (map->button == button) + return map->flags; + } + + return 0; +} + +static void xf_button_map_init(xfContext* xfc) +{ + size_t pos = 0; + /* loop counter for array initialization */ + + /* logical mouse button which is used for each physical mouse */ + /* button (indexed from zero). This is the default map. */ + unsigned char x11_map[112] = { 0 }; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(xfc->common.context.settings); + + x11_map[0] = Button1; + x11_map[1] = Button2; + x11_map[2] = Button3; + x11_map[3] = Button4; + x11_map[4] = Button5; + x11_map[5] = 6; + x11_map[6] = 7; + x11_map[7] = 8; + x11_map[8] = 9; + x11_map[96] = 97; + x11_map[111] = 112; + + /* query system for actual remapping */ + if (freerdp_settings_get_bool(xfc->common.context.settings, FreeRDP_UnmapButtons)) + { + xf_get_x11_button_map(xfc, x11_map); + } + + /* iterate over all (mapped) physical buttons; for each of them */ + /* find the logical button in X11, and assign to this the */ + /* appropriate value to send over the RDP wire. */ + for (size_t physical = 0; physical < ARRAYSIZE(x11_map); ++physical) + { + const unsigned char logical = x11_map[physical]; + const UINT16 flags = get_flags_for_button(logical); + + if ((logical != 0) && (flags != 0)) + { + if (pos >= NUM_BUTTONS_MAPPED) + { + WLog_ERR(TAG, "Failed to map mouse button to RDP button, no space"); + } + else + { + button_map* map = &xfc->button_map[pos++]; + map->button = logical; + map->flags = get_flags_for_button(physical + Button1); + } + } + } +} + +/** + * Callback given to freerdp_connect() to process the pre-connect operations. + * It will fill the rdp_freerdp structure (instance) with the appropriate options to use for the + * connection. + * + * @param instance - pointer to the rdp_freerdp structure that contains the connection's parameters, + * and will be filled with the appropriate informations. + * + * @return TRUE if successful. FALSE otherwise. + * Can exit with error code XF_EXIT_PARSE_ARGUMENTS if there is an error in the parameters. + */ +static BOOL xf_pre_connect(freerdp* instance) +{ + rdpChannels* channels = NULL; + rdpSettings* settings = NULL; + rdpContext* context = NULL; + xfContext* xfc = NULL; + UINT32 maxWidth = 0; + UINT32 maxHeight = 0; + + WINPR_ASSERT(instance); + + context = instance->context; + xfc = (xfContext*)instance->context; + WINPR_ASSERT(context); + + settings = context->settings; + WINPR_ASSERT(settings); + + if (!freerdp_settings_get_bool(settings, FreeRDP_AuthenticationOnly)) + { + if (!xf_setup_x11(xfc)) + return FALSE; + } + + channels = context->channels; + WINPR_ASSERT(channels); + + if (!freerdp_settings_set_uint32(settings, FreeRDP_OsMajorType, OSMAJORTYPE_UNIX)) + return FALSE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_OsMinorType, OSMINORTYPE_NATIVE_XSERVER)) + return FALSE; + PubSub_SubscribeChannelConnected(context->pubSub, xf_OnChannelConnectedEventHandler); + PubSub_SubscribeChannelDisconnected(context->pubSub, xf_OnChannelDisconnectedEventHandler); + + if (!freerdp_settings_get_string(settings, FreeRDP_Username) && + !freerdp_settings_get_bool(settings, FreeRDP_CredentialsFromStdin) && + !freerdp_settings_get_bool(settings, FreeRDP_SmartcardLogon)) + { + char login_name[MAX_PATH] = { 0 }; + ULONG size = sizeof(login_name) - 1; + + if (GetUserNameExA(NameSamCompatible, login_name, &size)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_Username, login_name)) + return FALSE; + + WLog_INFO(TAG, "No user name set. - Using login name: %s", + freerdp_settings_get_string(settings, FreeRDP_Username)); + } + } + + if (freerdp_settings_get_bool(settings, FreeRDP_AuthenticationOnly)) + { + /* Check +auth-only has a username and password. */ + if (!freerdp_settings_get_string(settings, FreeRDP_Password)) + { + WLog_INFO(TAG, "auth-only, but no password set. Please provide one."); + return FALSE; + } + + WLog_INFO(TAG, "Authentication only. Don't connect to X."); + } + + if (!freerdp_settings_get_bool(settings, FreeRDP_AuthenticationOnly)) + { + if (!xf_keyboard_init(xfc)) + return FALSE; + } + + if (!xf_detect_monitors(xfc, &maxWidth, &maxHeight)) + return FALSE; + + if (maxWidth && maxHeight && !freerdp_settings_get_bool(settings, FreeRDP_SmartSizing)) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, maxWidth)) + return FALSE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, maxHeight)) + return FALSE; + } + +#ifdef WITH_XRENDER + + /** + * If /f is specified in combination with /smart-sizing:widthxheight then + * we run the session in the /smart-sizing dimensions scaled to full screen + */ + if (freerdp_settings_get_bool(settings, FreeRDP_Fullscreen) && + freerdp_settings_get_bool(settings, FreeRDP_SmartSizing) && + (freerdp_settings_get_uint32(settings, FreeRDP_SmartSizingWidth) > 0) && + (freerdp_settings_get_uint32(settings, FreeRDP_SmartSizingHeight) > 0)) + { + if (!freerdp_settings_set_uint32( + settings, FreeRDP_DesktopWidth, + freerdp_settings_get_uint32(settings, FreeRDP_SmartSizingWidth))) + return FALSE; + if (!freerdp_settings_set_uint32( + settings, FreeRDP_DesktopHeight, + freerdp_settings_get_uint32(settings, FreeRDP_SmartSizingHeight))) + return FALSE; + } + +#endif + xfc->fullscreen = freerdp_settings_get_bool(settings, FreeRDP_Fullscreen); + xfc->decorations = freerdp_settings_get_bool(settings, FreeRDP_Decorations); + xfc->grab_keyboard = freerdp_settings_get_bool(settings, FreeRDP_GrabKeyboard); + xfc->fullscreen_toggle = freerdp_settings_get_bool(settings, FreeRDP_ToggleFullscreen); + if (!freerdp_settings_get_bool(settings, FreeRDP_AuthenticationOnly)) + xf_button_map_init(xfc); + return TRUE; +} + +static BOOL xf_inject_keypress(rdpContext* context, const char* buffer, size_t size) +{ + WCHAR wbuffer[64] = { 0 }; + const SSIZE_T len = ConvertUtf8NToWChar(buffer, size, wbuffer, ARRAYSIZE(wbuffer)); + if (len < 0) + return FALSE; + + rdpInput* input = context->input; + WINPR_ASSERT(input); + + for (SSIZE_T x = 0; x < len; x++) + { + const WCHAR code = wbuffer[x]; + freerdp_input_send_unicode_keyboard_event(input, 0, code); + Sleep(5); + freerdp_input_send_unicode_keyboard_event(input, KBD_FLAGS_RELEASE, code); + Sleep(5); + } + return TRUE; +} + +static BOOL xf_process_pipe(rdpContext* context, const char* pipe) +{ + int fd = open(pipe, O_NONBLOCK | O_RDONLY); + if (fd < 0) + { + char ebuffer[256] = { 0 }; + WLog_ERR(TAG, "pipe '%s' open returned %s [%d]", pipe, + winpr_strerror(errno, ebuffer, sizeof(ebuffer)), errno); + return FALSE; + } + while (!freerdp_shall_disconnect_context(context)) + { + char buffer[64] = { 0 }; + ssize_t rd = read(fd, buffer, sizeof(buffer) - 1); + if (rd == 0) + { + char ebuffer[256] = { 0 }; + if ((errno == EAGAIN) || (errno == 0)) + { + Sleep(100); + continue; + } + + // EOF, abort reading. + WLog_ERR(TAG, "pipe '%s' read returned %s [%d]", pipe, + winpr_strerror(errno, ebuffer, sizeof(ebuffer)), errno); + break; + } + else if (rd < 0) + { + char ebuffer[256] = { 0 }; + WLog_ERR(TAG, "pipe '%s' read returned %s [%d]", pipe, + winpr_strerror(errno, ebuffer, sizeof(ebuffer)), errno); + break; + } + else + { + if (!xf_inject_keypress(context, buffer, rd)) + break; + } + } + close(fd); + return TRUE; +} + +static void cleanup_pipe(int signum, const char* signame, void* context) +{ + const char* pipe = context; + if (!pipe) + return; + unlink(pipe); +} + +static DWORD WINAPI xf_handle_pipe(void* arg) +{ + xfContext* xfc = arg; + WINPR_ASSERT(xfc); + + rdpContext* context = &xfc->common.context; + WINPR_ASSERT(context); + + rdpSettings* settings = context->settings; + WINPR_ASSERT(settings); + + const char* pipe = freerdp_settings_get_string(settings, FreeRDP_KeyboardPipeName); + WINPR_ASSERT(pipe); + + const int rc = mkfifo(pipe, S_IWUSR | S_IRUSR); + if (rc != 0) + { + char ebuffer[256] = { 0 }; + WLog_ERR(TAG, "Failed to create named pipe '%s': %s [%d]", pipe, + winpr_strerror(errno, ebuffer, sizeof(ebuffer)), errno); + return 0; + } + freerdp_add_signal_cleanup_handler(pipe, cleanup_pipe); + + xf_process_pipe(context, pipe); + + freerdp_del_signal_cleanup_handler(pipe, cleanup_pipe); + unlink(pipe); + return 0; +} + +/** + * Callback given to freerdp_connect() to perform post-connection operations. + * It will be called only if the connection was initialized properly, and will continue the + * initialization based on the newly created connection. + */ +static BOOL xf_post_connect(freerdp* instance) +{ + ResizeWindowEventArgs e = { 0 }; + + WINPR_ASSERT(instance); + xfContext* xfc = (xfContext*)instance->context; + rdpContext* context = instance->context; + WINPR_ASSERT(context); + + rdpSettings* settings = context->settings; + WINPR_ASSERT(settings); + + rdpUpdate* update = context->update; + WINPR_ASSERT(update); + + if (freerdp_settings_get_bool(settings, FreeRDP_RemoteApplicationMode)) + xfc->remote_app = TRUE; + + if (!xf_create_window(xfc)) + return FALSE; + + if (!xf_get_pixmap_info(xfc)) + return FALSE; + + if (!gdi_init(instance, xf_get_local_color_format(xfc, TRUE))) + return FALSE; + + if (!xf_create_image(xfc)) + return FALSE; + + if (!xf_register_pointer(context->graphics)) + return FALSE; + +#ifdef WITH_XRENDER + xfc->scaledWidth = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth); + xfc->scaledHeight = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight); + xfc->offset_x = 0; + xfc->offset_y = 0; +#endif + + if (!xfc->xrenderAvailable) + { + if (freerdp_settings_get_bool(settings, FreeRDP_SmartSizing)) + { + WLog_ERR(TAG, "XRender not available: disabling smart-sizing"); + if (!freerdp_settings_set_bool(settings, FreeRDP_SmartSizing, FALSE)) + return FALSE; + } + + if (freerdp_settings_get_bool(settings, FreeRDP_MultiTouchGestures)) + { + WLog_ERR(TAG, "XRender not available: disabling local multi-touch gestures"); + if (!freerdp_settings_set_bool(settings, FreeRDP_MultiTouchGestures, FALSE)) + return FALSE; + } + } + + update->DesktopResize = xf_sw_desktop_resize; + update->EndPaint = xf_end_paint; + update->PlaySound = xf_play_sound; + update->SetKeyboardIndicators = xf_keyboard_set_indicators; + update->SetKeyboardImeStatus = xf_keyboard_set_ime_status; + + const BOOL serverIsWindowsPlatform = + (freerdp_settings_get_uint32(settings, FreeRDP_OsMajorType) == OSMAJORTYPE_WINDOWS); + if (freerdp_settings_get_bool(settings, FreeRDP_RedirectClipboard) && + !(xfc->clipboard = xf_clipboard_new(xfc, !serverIsWindowsPlatform))) + return FALSE; + + if (!(xfc->xfDisp = xf_disp_new(xfc))) + return FALSE; + + const char* pipe = freerdp_settings_get_string(settings, FreeRDP_KeyboardPipeName); + if (pipe) + { + xfc->pipethread = CreateThread(NULL, 0, xf_handle_pipe, xfc, 0, NULL); + if (!xfc->pipethread) + return FALSE; + } + + EventArgsInit(&e, "xfreerdp"); + e.width = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth); + e.height = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight); + PubSub_OnResizeWindow(context->pubSub, xfc, &e); + return TRUE; +} + +static void xf_post_disconnect(freerdp* instance) +{ + xfContext* xfc = NULL; + rdpContext* context = NULL; + + if (!instance || !instance->context) + return; + + context = instance->context; + xfc = (xfContext*)context; + PubSub_UnsubscribeChannelConnected(instance->context->pubSub, + xf_OnChannelConnectedEventHandler); + PubSub_UnsubscribeChannelDisconnected(instance->context->pubSub, + xf_OnChannelDisconnectedEventHandler); + gdi_free(instance); + + if (xfc->pipethread) + { + WaitForSingleObject(xfc->pipethread, INFINITE); + CloseHandle(xfc->pipethread); + xfc->pipethread = NULL; + } + if (xfc->clipboard) + { + xf_clipboard_free(xfc->clipboard); + xfc->clipboard = NULL; + } + + if (xfc->xfDisp) + { + xf_disp_free(xfc->xfDisp); + xfc->xfDisp = NULL; + } + + if ((xfc->window != NULL) && (xfc->drawable == xfc->window->handle)) + xfc->drawable = 0; + else + xf_DestroyDummyWindow(xfc, xfc->drawable); + + xf_window_free(xfc); +} + +static void xf_post_final_disconnect(freerdp* instance) +{ + xfContext* xfc = NULL; + rdpContext* context = NULL; + + if (!instance || !instance->context) + return; + + context = instance->context; + xfc = (xfContext*)context; + + xf_keyboard_free(xfc); + xf_teardown_x11(xfc); +} + +static int xf_logon_error_info(freerdp* instance, UINT32 data, UINT32 type) +{ + xfContext* xfc = (xfContext*)instance->context; + const char* str_data = freerdp_get_logon_error_info_data(data); + const char* str_type = freerdp_get_logon_error_info_type(type); + WLog_INFO(TAG, "Logon Error Info %s [%s]", str_data, str_type); + if (type != LOGON_MSG_SESSION_CONTINUE) + { + xf_rail_disable_remoteapp_mode(xfc); + } + return 1; +} + +static BOOL handle_window_events(freerdp* instance) +{ + if (!xf_process_x_events(instance)) + { + WLog_DBG(TAG, "Closed from X11"); + return FALSE; + } + + return TRUE; +} + +/** Main loop for the rdp connection. + * It will be run from the thread's entry point (thread_func()). + * It initiates the connection, and will continue to run until the session ends, + * processing events as they are received. + * + * @param param - pointer to the rdp_freerdp structure that contains the session's settings + * @return A code from the enum XF_EXIT_CODE (0 if successful) + */ +static DWORD WINAPI xf_client_thread(LPVOID param) +{ + DWORD exit_code = 0; + DWORD waitStatus = 0; + HANDLE inputEvent = NULL; + HANDLE timer = NULL; + LARGE_INTEGER due = { 0 }; + TimerEventArgs timerEvent = { 0 }; + + EventArgsInit(&timerEvent, "xfreerdp"); + freerdp* instance = (freerdp*)param; + WINPR_ASSERT(instance); + + const BOOL status = freerdp_connect(instance); + rdpContext* context = instance->context; + WINPR_ASSERT(context); + xfContext* xfc = (xfContext*)instance->context; + WINPR_ASSERT(xfc); + + rdpSettings* settings = context->settings; + WINPR_ASSERT(settings); + + if (!status) + { + UINT32 error = freerdp_get_last_error(instance->context); + exit_code = xf_map_error_to_exit_code(error); + } + else + exit_code = XF_EXIT_SUCCESS; + + if (!status) + goto end; + + /* --authonly ? */ + if (freerdp_settings_get_bool(settings, FreeRDP_AuthenticationOnly)) + { + WLog_ERR(TAG, "Authentication only, exit status %" PRId32 "", !status); + goto disconnect; + } + + if (!status) + { + WLog_ERR(TAG, "Freerdp connect error exit status %" PRId32 "", !status); + exit_code = freerdp_error_info(instance); + + if (freerdp_get_last_error(instance->context) == FREERDP_ERROR_AUTHENTICATION_FAILED) + exit_code = XF_EXIT_AUTH_FAILURE; + else if (exit_code == ERRINFO_SUCCESS) + exit_code = XF_EXIT_CONN_FAILED; + + goto disconnect; + } + + timer = CreateWaitableTimerA(NULL, FALSE, "mainloop-periodic-timer"); + + if (!timer) + { + WLog_ERR(TAG, "failed to create timer"); + goto disconnect; + } + + due.QuadPart = 0; + + if (!SetWaitableTimer(timer, &due, 20, NULL, NULL, FALSE)) + { + goto disconnect; + } + inputEvent = xfc->x11event; + + while (!freerdp_shall_disconnect_context(instance->context)) + { + HANDLE handles[MAXIMUM_WAIT_OBJECTS] = { 0 }; + DWORD nCount = 0; + handles[nCount++] = timer; + handles[nCount++] = inputEvent; + + /* + * win8 and server 2k12 seem to have some timing issue/race condition + * when a initial sync request is send to sync the keyboard indicators + * sending the sync event twice fixed this problem + */ + if (freerdp_focus_required(instance)) + { + xf_keyboard_focus_in(xfc); + xf_keyboard_focus_in(xfc); + } + + { + DWORD tmp = + freerdp_get_event_handles(context, &handles[nCount], ARRAYSIZE(handles) - nCount); + + if (tmp == 0) + { + WLog_ERR(TAG, "freerdp_get_event_handles failed"); + break; + } + + nCount += tmp; + } + + if (xfc->window) + xf_floatbar_hide_and_show(xfc->window->floatbar); + + waitStatus = WaitForMultipleObjects(nCount, handles, FALSE, INFINITE); + + if (waitStatus == WAIT_FAILED) + break; + + { + if (!freerdp_check_event_handles(context)) + { + if (client_auto_reconnect_ex(instance, handle_window_events)) + continue; + else + { + /* + * Indicate an unsuccessful connection attempt if reconnect + * did not succeed and no other error was specified. + */ + const UINT32 error = freerdp_get_last_error(instance->context); + + if (freerdp_error_info(instance) == 0) + exit_code = xf_map_error_to_exit_code(error); + } + + if (freerdp_get_last_error(context) == FREERDP_ERROR_SUCCESS) + WLog_ERR(TAG, "Failed to check FreeRDP file descriptor"); + + break; + } + } + + if (!handle_window_events(instance)) + break; + + if ((waitStatus != WAIT_TIMEOUT) && (waitStatus == WAIT_OBJECT_0)) + { + timerEvent.now = GetTickCount64(); + PubSub_OnTimer(context->pubSub, context, &timerEvent); + } + } + + if (!exit_code) + { + exit_code = freerdp_error_info(instance); + + if (exit_code == XF_EXIT_DISCONNECT && + freerdp_get_disconnect_ultimatum(context) == Disconnect_Ultimatum_user_requested) + { + /* This situation might be limited to Windows XP. */ + WLog_INFO(TAG, "Error info says user did not initiate but disconnect ultimatum says " + "they did; treat this as a user logoff"); + exit_code = XF_EXIT_LOGOFF; + } + } + +disconnect: + + if (timer) + CloseHandle(timer); + + freerdp_disconnect(instance); +end: + ExitThread(exit_code); + return exit_code; +} + +DWORD xf_exit_code_from_disconnect_reason(DWORD reason) +{ + if (reason == 0 || + (reason >= XF_EXIT_PARSE_ARGUMENTS && reason <= XF_EXIT_CONNECT_NO_OR_MISSING_CREDENTIALS)) + return reason; + /* License error set */ + else if (reason >= 0x100 && reason <= 0x10A) + reason -= 0x100 + XF_EXIT_LICENSE_INTERNAL; + /* RDP protocol error set */ + else if (reason >= 0x10c9 && reason <= 0x1193) + reason = XF_EXIT_RDP; + /* There's no need to test protocol-independent codes: they match */ + else if (!(reason <= 0xC)) + reason = XF_EXIT_UNKNOWN; + + return reason; +} + +static void xf_TerminateEventHandler(void* context, const TerminateEventArgs* e) +{ + rdpContext* ctx = (rdpContext*)context; + WINPR_UNUSED(e); + freerdp_abort_connect_context(ctx); +} + +#ifdef WITH_XRENDER +static void xf_ZoomingChangeEventHandler(void* context, const ZoomingChangeEventArgs* e) +{ + int w = 0; + int h = 0; + rdpSettings* settings = NULL; + xfContext* xfc = (xfContext*)context; + + WINPR_ASSERT(xfc); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + w = xfc->scaledWidth + e->dx; + h = xfc->scaledHeight + e->dy; + + if (e->dx == 0 && e->dy == 0) + return; + + if (w < 10) + w = 10; + + if (h < 10) + h = 10; + + if (w == xfc->scaledWidth && h == xfc->scaledHeight) + return; + + xfc->scaledWidth = w; + xfc->scaledHeight = h; + xf_draw_screen(xfc, 0, 0, freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth), + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight)); +} + +static void xf_PanningChangeEventHandler(void* context, const PanningChangeEventArgs* e) +{ + xfContext* xfc = (xfContext*)context; + rdpSettings* settings = NULL; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(e); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + if (e->dx == 0 && e->dy == 0) + return; + + xfc->offset_x += e->dx; + xfc->offset_y += e->dy; + xf_draw_screen(xfc, 0, 0, freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth), + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight)); +} +#endif + +/** + * Client Interface + */ + +static BOOL xfreerdp_client_global_init(void) +{ + setlocale(LC_ALL, ""); + + if (freerdp_handle_signals() != 0) + return FALSE; + + return TRUE; +} + +static void xfreerdp_client_global_uninit(void) +{ +} + +static int xfreerdp_client_start(rdpContext* context) +{ + xfContext* xfc = (xfContext*)context; + rdpSettings* settings = context->settings; + + if (!freerdp_settings_get_string(settings, FreeRDP_ServerHostname)) + { + WLog_ERR(TAG, "error: server hostname was not specified with /v:<server>[:port]"); + return -1; + } + + if (!(xfc->common.thread = CreateThread(NULL, 0, xf_client_thread, context->instance, 0, NULL))) + { + WLog_ERR(TAG, "failed to create client thread"); + return -1; + } + + return 0; +} + +static Atom get_supported_atom(xfContext* xfc, const char* atomName) +{ + const Atom atom = XInternAtom(xfc->display, atomName, False); + + for (unsigned long i = 0; i < xfc->supportedAtomCount; i++) + { + if (xfc->supportedAtoms[i] == atom) + return atom; + } + + return None; +} + +void xf_teardown_x11(xfContext* xfc) +{ + WINPR_ASSERT(xfc); + + if (xfc->display) + { + XCloseDisplay(xfc->display); + xfc->display = NULL; + } + + if (xfc->x11event) + { + CloseHandle(xfc->x11event); + xfc->x11event = NULL; + } + + if (xfc->mutex) + { + CloseHandle(xfc->mutex); + xfc->mutex = NULL; + } + + if (xfc->vscreen.monitors) + { + free(xfc->vscreen.monitors); + xfc->vscreen.monitors = NULL; + } + xfc->vscreen.nmonitors = 0; + + free(xfc->supportedAtoms); + xfc->supportedAtoms = NULL; + xfc->supportedAtomCount = 0; +} + +BOOL xf_setup_x11(xfContext* xfc) +{ + + WINPR_ASSERT(xfc); + xfc->UseXThreads = TRUE; + +#if !defined(NDEBUG) + /* uncomment below if debugging to prevent keyboard grap */ + xfc->debug = TRUE; +#endif + + if (xfc->UseXThreads) + { + if (!XInitThreads()) + { + WLog_WARN(TAG, "XInitThreads() failure"); + xfc->UseXThreads = FALSE; + } + } + + xfc->display = XOpenDisplay(NULL); + + if (!xfc->display) + { + WLog_ERR(TAG, "failed to open display: %s", XDisplayName(NULL)); + WLog_ERR(TAG, "Please check that the $DISPLAY environment variable is properly set."); + goto fail; + } + if (xfc->debug) + { + WLog_INFO(TAG, "Enabling X11 debug mode."); + XSynchronize(xfc->display, TRUE); + } + def_error_handler = XSetErrorHandler(xf_error_handler_ex); + + xfc->mutex = CreateMutex(NULL, FALSE, NULL); + + if (!xfc->mutex) + { + WLog_ERR(TAG, "Could not create mutex!"); + goto fail; + } + + xfc->xfds = ConnectionNumber(xfc->display); + xfc->screen_number = DefaultScreen(xfc->display); + xfc->screen = ScreenOfDisplay(xfc->display, xfc->screen_number); + xfc->big_endian = (ImageByteOrder(xfc->display) == MSBFirst); + xfc->invert = TRUE; + xfc->complex_regions = TRUE; + xfc->_NET_SUPPORTED = XInternAtom(xfc->display, "_NET_SUPPORTED", True); + xfc->_NET_SUPPORTING_WM_CHECK = XInternAtom(xfc->display, "_NET_SUPPORTING_WM_CHECK", True); + + if ((xfc->_NET_SUPPORTED != None) && (xfc->_NET_SUPPORTING_WM_CHECK != None)) + { + Atom actual_type = 0; + int actual_format = 0; + unsigned long nitems = 0; + unsigned long after = 0; + unsigned char* data = NULL; + int status = LogTagAndXGetWindowProperty( + TAG, xfc->display, RootWindowOfScreen(xfc->screen), xfc->_NET_SUPPORTED, 0, 1024, False, + XA_ATOM, &actual_type, &actual_format, &nitems, &after, &data); + + if ((status == Success) && (actual_type == XA_ATOM) && (actual_format == 32)) + { + xfc->supportedAtomCount = nitems; + xfc->supportedAtoms = calloc(xfc->supportedAtomCount, sizeof(Atom)); + WINPR_ASSERT(xfc->supportedAtoms); + memcpy(xfc->supportedAtoms, data, nitems * sizeof(Atom)); + } + + if (data) + XFree(data); + } + + xfc->_XWAYLAND_MAY_GRAB_KEYBOARD = + XInternAtom(xfc->display, "_XWAYLAND_MAY_GRAB_KEYBOARD", False); + xfc->_NET_WM_ICON = XInternAtom(xfc->display, "_NET_WM_ICON", False); + xfc->_MOTIF_WM_HINTS = XInternAtom(xfc->display, "_MOTIF_WM_HINTS", False); + xfc->_NET_CURRENT_DESKTOP = XInternAtom(xfc->display, "_NET_CURRENT_DESKTOP", False); + xfc->_NET_WORKAREA = XInternAtom(xfc->display, "_NET_WORKAREA", False); + xfc->_NET_WM_STATE = get_supported_atom(xfc, "_NET_WM_STATE"); + xfc->_NET_WM_STATE_FULLSCREEN = get_supported_atom(xfc, "_NET_WM_STATE_FULLSCREEN"); + xfc->_NET_WM_STATE_MAXIMIZED_HORZ = + XInternAtom(xfc->display, "_NET_WM_STATE_MAXIMIZED_HORZ", False); + xfc->_NET_WM_STATE_MAXIMIZED_VERT = + XInternAtom(xfc->display, "_NET_WM_STATE_MAXIMIZED_VERT", False); + xfc->_NET_WM_FULLSCREEN_MONITORS = get_supported_atom(xfc, "_NET_WM_FULLSCREEN_MONITORS"); + xfc->_NET_WM_NAME = XInternAtom(xfc->display, "_NET_WM_NAME", False); + xfc->_NET_WM_PID = XInternAtom(xfc->display, "_NET_WM_PID", False); + xfc->_NET_WM_WINDOW_TYPE = XInternAtom(xfc->display, "_NET_WM_WINDOW_TYPE", False); + xfc->_NET_WM_WINDOW_TYPE_NORMAL = + XInternAtom(xfc->display, "_NET_WM_WINDOW_TYPE_NORMAL", False); + xfc->_NET_WM_WINDOW_TYPE_DIALOG = + XInternAtom(xfc->display, "_NET_WM_WINDOW_TYPE_DIALOG", False); + xfc->_NET_WM_WINDOW_TYPE_POPUP = XInternAtom(xfc->display, "_NET_WM_WINDOW_TYPE_POPUP", False); + xfc->_NET_WM_WINDOW_TYPE_POPUP_MENU = + XInternAtom(xfc->display, "_NET_WM_WINDOW_TYPE_POPUP_MENU", False); + xfc->_NET_WM_WINDOW_TYPE_UTILITY = + XInternAtom(xfc->display, "_NET_WM_WINDOW_TYPE_UTILITY", False); + xfc->_NET_WM_WINDOW_TYPE_DROPDOWN_MENU = + XInternAtom(xfc->display, "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU", False); + xfc->_NET_WM_STATE_SKIP_TASKBAR = + XInternAtom(xfc->display, "_NET_WM_STATE_SKIP_TASKBAR", False); + xfc->_NET_WM_STATE_SKIP_PAGER = XInternAtom(xfc->display, "_NET_WM_STATE_SKIP_PAGER", False); + xfc->_NET_WM_MOVERESIZE = XInternAtom(xfc->display, "_NET_WM_MOVERESIZE", False); + xfc->_NET_MOVERESIZE_WINDOW = XInternAtom(xfc->display, "_NET_MOVERESIZE_WINDOW", False); + xfc->UTF8_STRING = XInternAtom(xfc->display, "UTF8_STRING", FALSE); + xfc->WM_PROTOCOLS = XInternAtom(xfc->display, "WM_PROTOCOLS", False); + xfc->WM_DELETE_WINDOW = XInternAtom(xfc->display, "WM_DELETE_WINDOW", False); + xfc->WM_STATE = XInternAtom(xfc->display, "WM_STATE", False); + xfc->x11event = CreateFileDescriptorEvent(NULL, FALSE, FALSE, xfc->xfds, WINPR_FD_READ); + + if (!xfc->x11event) + { + WLog_ERR(TAG, "Could not create xfds event"); + goto fail; + } + + xf_check_extensions(xfc); + + xfc->vscreen.monitors = calloc(16, sizeof(MONITOR_INFO)); + + if (!xfc->vscreen.monitors) + goto fail; + return TRUE; + +fail: + xf_teardown_x11(xfc); + return FALSE; +} + +static BOOL xfreerdp_client_new(freerdp* instance, rdpContext* context) +{ + xfContext* xfc = (xfContext*)instance->context; + WINPR_ASSERT(context); + WINPR_ASSERT(xfc); + WINPR_ASSERT(!xfc->display); + WINPR_ASSERT(!xfc->mutex); + WINPR_ASSERT(!xfc->x11event); + instance->PreConnect = xf_pre_connect; + instance->PostConnect = xf_post_connect; + instance->PostDisconnect = xf_post_disconnect; + instance->PostFinalDisconnect = xf_post_final_disconnect; + instance->LogonErrorInfo = xf_logon_error_info; + instance->GetAccessToken = client_cli_get_access_token; + PubSub_SubscribeTerminate(context->pubSub, xf_TerminateEventHandler); +#ifdef WITH_XRENDER + PubSub_SubscribeZoomingChange(context->pubSub, xf_ZoomingChangeEventHandler); + PubSub_SubscribePanningChange(context->pubSub, xf_PanningChangeEventHandler); +#endif + + return TRUE; +} + +static void xfreerdp_client_free(freerdp* instance, rdpContext* context) +{ + if (!context) + return; + + PubSub_UnsubscribeTerminate(context->pubSub, xf_TerminateEventHandler); +#ifdef WITH_XRENDER + PubSub_UnsubscribeZoomingChange(context->pubSub, xf_ZoomingChangeEventHandler); + PubSub_UnsubscribePanningChange(context->pubSub, xf_PanningChangeEventHandler); +#endif +} + +int RdpClientEntry(RDP_CLIENT_ENTRY_POINTS* pEntryPoints) +{ + pEntryPoints->Version = 1; + pEntryPoints->Size = sizeof(RDP_CLIENT_ENTRY_POINTS_V1); + pEntryPoints->GlobalInit = xfreerdp_client_global_init; + pEntryPoints->GlobalUninit = xfreerdp_client_global_uninit; + pEntryPoints->ContextSize = sizeof(xfContext); + pEntryPoints->ClientNew = xfreerdp_client_new; + pEntryPoints->ClientFree = xfreerdp_client_free; + pEntryPoints->ClientStart = xfreerdp_client_start; + pEntryPoints->ClientStop = freerdp_client_common_stop; + return 0; +} diff --git a/client/X11/xf_client.h b/client/X11/xf_client.h new file mode 100644 index 0000000..c9bc21b --- /dev/null +++ b/client/X11/xf_client.h @@ -0,0 +1,53 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Client Interface + * + * Copyright 2013 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_X11_CLIENT_H +#define FREERDP_CLIENT_X11_CLIENT_H + +#include <winpr/crt.h> +#include <winpr/synch.h> +#include <winpr/thread.h> +#include <winpr/collections.h> + +#include <freerdp/api.h> +#include <freerdp/freerdp.h> +#include <freerdp/client.h> + +#include <freerdp/gdi/gdi.h> +#include <freerdp/gdi/dc.h> +#include <freerdp/gdi/region.h> + +#include <freerdp/channels/channels.h> + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** + * Client Interface + */ + + FREERDP_API int RdpClientEntry(RDP_CLIENT_ENTRY_POINTS* pEntryPoints); + +#ifdef __cplusplus +} +#endif + +#endif /* FREERDP_CLIENT_X11_CLIENT_H */ diff --git a/client/X11/xf_cliprdr.c b/client/X11/xf_cliprdr.c new file mode 100644 index 0000000..a68dae9 --- /dev/null +++ b/client/X11/xf_cliprdr.c @@ -0,0 +1,2517 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Clipboard Redirection + * + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <stdlib.h> +#include <errno.h> + +#include <X11/Xlib.h> +#include <X11/Xatom.h> + +#ifdef WITH_XFIXES +#include <X11/extensions/Xfixes.h> +#endif + +#include <winpr/crt.h> +#include <winpr/assert.h> +#include <winpr/image.h> +#include <winpr/stream.h> +#include <winpr/clipboard.h> +#include <winpr/path.h> + +#include <freerdp/utils/signal.h> +#include <freerdp/log.h> +#include <freerdp/client/cliprdr.h> +#include <freerdp/channels/channels.h> +#include <freerdp/channels/cliprdr.h> + +#include <freerdp/client/client_cliprdr_file.h> + +#include "xf_cliprdr.h" +#include "xf_event.h" +#include "xf_utils.h" + +#define TAG CLIENT_TAG("x11.cliprdr") + +#define MAX_CLIPBOARD_FORMATS 255 +#define WIN32_FILETIME_TO_UNIX_EPOCH_USEC UINT64_C(116444736000000000) + +#ifdef WITH_DEBUG_CLIPRDR +#define DEBUG_CLIPRDR(...) WLog_DBG(TAG, __VA_ARGS__) +#else +#define DEBUG_CLIPRDR(...) \ + do \ + { \ + } while (0) +#endif + +typedef struct +{ + Atom atom; + UINT32 formatToRequest; + UINT32 localFormat; + char* formatName; +} xfCliprdrFormat; + +typedef struct +{ + BYTE* data; + UINT32 data_length; +} xfCachedData; + +struct xf_clipboard +{ + xfContext* xfc; + rdpChannels* channels; + CliprdrClientContext* context; + + wClipboard* system; + + Window root_window; + Atom clipboard_atom; + Atom property_atom; + + Atom timestamp_property_atom; + Time selection_ownership_timestamp; + + Atom raw_transfer_atom; + Atom raw_format_list_atom; + + UINT32 numClientFormats; + xfCliprdrFormat clientFormats[20]; + + UINT32 numServerFormats; + CLIPRDR_FORMAT* serverFormats; + + size_t numTargets; + Atom targets[20]; + + int requestedFormatId; + + wHashTable* cachedData; + wHashTable* cachedRawData; + + BOOL data_raw_format; + + const xfCliprdrFormat* requestedFormat; + + XSelectionEvent* respond; + + Window owner; + BOOL sync; + + /* INCR mechanism */ + Atom incr_atom; + BOOL incr_starts; + BYTE* incr_data; + int incr_data_length; + + /* XFixes extension */ + int xfixes_event_base; + int xfixes_error_base; + BOOL xfixes_supported; + + /* last sent data */ + CLIPRDR_FORMAT* lastSentFormats; + UINT32 lastSentNumFormats; + CliprdrFileContext* file; +}; + +static const char mime_text_plain[] = "text/plain"; +static const char mime_uri_list[] = "text/uri-list"; +static const char mime_html[] = "text/html"; +static const char* mime_bitmap[] = { "image/bmp", "image/x-bmp", "image/x-MS-bmp", + "image/x-win-bitmap" }; +static const char mime_webp[] = "image/webp"; +static const char mime_png[] = "image/png"; +static const char mime_jpeg[] = "image/jpeg"; +static const char mime_tiff[] = "image/tiff"; +static const char* mime_images[] = { mime_webp, mime_png, mime_jpeg, mime_tiff }; + +static const char mime_gnome_copied_files[] = "x-special/gnome-copied-files"; +static const char mime_mate_copied_files[] = "x-special/mate-copied-files"; + +static const char type_FileGroupDescriptorW[] = "FileGroupDescriptorW"; +static const char type_HtmlFormat[] = "HTML Format"; + +static void xf_cliprdr_clear_cached_data(xfClipboard* clipboard); +static UINT xf_cliprdr_send_client_format_list(xfClipboard* clipboard, BOOL force); +static void xf_cliprdr_set_selection_owner(xfContext* xfc, xfClipboard* clipboard, Time timestamp); + +static void xf_cached_data_free(void* ptr) +{ + xfCachedData* cached_data = ptr; + if (!cached_data) + return; + + free(cached_data->data); + free(cached_data); +} + +static xfCachedData* xf_cached_data_new(BYTE* data, UINT32 data_length) +{ + xfCachedData* cached_data = NULL; + + cached_data = calloc(1, sizeof(xfCachedData)); + if (!cached_data) + return NULL; + + cached_data->data = data; + cached_data->data_length = data_length; + + return cached_data; +} + +static xfCachedData* xf_cached_data_new_copy(BYTE* data, UINT32 data_length) +{ + BYTE* copy = NULL; + if (data_length > 0) + { + copy = malloc(data_length); + if (!copy) + return NULL; + memcpy(copy, data, data_length); + } + + xfCachedData* cache = xf_cached_data_new(copy, data_length); + if (!cache) + free(copy); + return cache; +} + +static void xf_clipboard_free_server_formats(xfClipboard* clipboard) +{ + WINPR_ASSERT(clipboard); + if (clipboard->serverFormats) + { + for (size_t i = 0; i < clipboard->numServerFormats; i++) + { + CLIPRDR_FORMAT* format = &clipboard->serverFormats[i]; + free(format->formatName); + } + + free(clipboard->serverFormats); + clipboard->serverFormats = NULL; + } +} + +static void xf_cliprdr_check_owner(xfClipboard* clipboard) +{ + Window owner = 0; + xfContext* xfc = NULL; + + WINPR_ASSERT(clipboard); + + xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + + if (clipboard->sync) + { + owner = XGetSelectionOwner(xfc->display, clipboard->clipboard_atom); + + if (clipboard->owner != owner) + { + clipboard->owner = owner; + xf_cliprdr_send_client_format_list(clipboard, FALSE); + } + } +} + +static BOOL xf_cliprdr_is_self_owned(xfClipboard* clipboard) +{ + xfContext* xfc = NULL; + + WINPR_ASSERT(clipboard); + + xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + return XGetSelectionOwner(xfc->display, clipboard->clipboard_atom) == xfc->drawable; +} + +static void xf_cliprdr_set_raw_transfer_enabled(xfClipboard* clipboard, BOOL enabled) +{ + UINT32 data = enabled; + xfContext* xfc = NULL; + + WINPR_ASSERT(clipboard); + + xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + LogTagAndXChangeProperty(TAG, xfc->display, xfc->drawable, clipboard->raw_transfer_atom, + XA_INTEGER, 32, PropModeReplace, (BYTE*)&data, 1); +} + +static BOOL xf_cliprdr_is_raw_transfer_available(xfClipboard* clipboard) +{ + Atom type = 0; + int format = 0; + int result = 0; + unsigned long length = 0; + unsigned long bytes_left = 0; + UINT32* data = NULL; + UINT32 is_enabled = 0; + Window owner = None; + xfContext* xfc = NULL; + + WINPR_ASSERT(clipboard); + + xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + + owner = XGetSelectionOwner(xfc->display, clipboard->clipboard_atom); + + if (owner != None) + { + result = LogTagAndXGetWindowProperty(TAG, xfc->display, owner, clipboard->raw_transfer_atom, + 0, 4, 0, XA_INTEGER, &type, &format, &length, + &bytes_left, (BYTE**)&data); + } + + if (data) + { + is_enabled = *data; + XFree(data); + } + + if ((owner == None) || (owner == xfc->drawable)) + return FALSE; + + if (result != Success) + return FALSE; + + return is_enabled ? TRUE : FALSE; +} + +static BOOL xf_cliprdr_formats_equal(const CLIPRDR_FORMAT* server, const xfCliprdrFormat* client) +{ + WINPR_ASSERT(server); + WINPR_ASSERT(client); + + if (server->formatName && client->formatName) + { + /* The server may be using short format names while we store them in full form. */ + return (0 == strncmp(server->formatName, client->formatName, strlen(server->formatName))); + } + + if (!server->formatName && !client->formatName) + { + return (server->formatId == client->formatToRequest); + } + + return FALSE; +} + +static const xfCliprdrFormat* xf_cliprdr_get_client_format_by_id(xfClipboard* clipboard, + UINT32 formatId) +{ + WINPR_ASSERT(clipboard); + + for (size_t index = 0; index < clipboard->numClientFormats; index++) + { + const xfCliprdrFormat* format = &(clipboard->clientFormats[index]); + + if (format->formatToRequest == formatId) + return format; + } + + return NULL; +} + +static const xfCliprdrFormat* xf_cliprdr_get_client_format_by_atom(xfClipboard* clipboard, + Atom atom) +{ + WINPR_ASSERT(clipboard); + + for (UINT32 i = 0; i < clipboard->numClientFormats; i++) + { + const xfCliprdrFormat* format = &(clipboard->clientFormats[i]); + + if (format->atom == atom) + return format; + } + + return NULL; +} + +static const CLIPRDR_FORMAT* xf_cliprdr_get_server_format_by_atom(xfClipboard* clipboard, Atom atom) +{ + WINPR_ASSERT(clipboard); + + for (size_t i = 0; i < clipboard->numClientFormats; i++) + { + const xfCliprdrFormat* client_format = &(clipboard->clientFormats[i]); + + if (client_format->atom == atom) + { + for (size_t j = 0; j < clipboard->numServerFormats; j++) + { + const CLIPRDR_FORMAT* server_format = &(clipboard->serverFormats[j]); + + if (xf_cliprdr_formats_equal(server_format, client_format)) + return server_format; + } + } + } + + return NULL; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_cliprdr_send_data_request(xfClipboard* clipboard, UINT32 formatId, + const xfCliprdrFormat* cformat) +{ + CLIPRDR_FORMAT_DATA_REQUEST request = { 0 }; + request.requestedFormatId = formatId; + + DEBUG_CLIPRDR("requesting format 0x%08" PRIx32 " [%s] {local 0x%08" PRIx32 "} [%s]", formatId, + ClipboardGetFormatIdString(formatId), cformat->localFormat, cformat->formatName); + + WINPR_ASSERT(clipboard); + WINPR_ASSERT(clipboard->context); + WINPR_ASSERT(clipboard->context->ClientFormatDataRequest); + return clipboard->context->ClientFormatDataRequest(clipboard->context, &request); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_cliprdr_send_data_response(xfClipboard* clipboard, const xfCliprdrFormat* format, + const BYTE* data, size_t size) +{ + CLIPRDR_FORMAT_DATA_RESPONSE response = { 0 }; + + WINPR_ASSERT(clipboard); + + /* No request currently pending, do not send a response. */ + if (clipboard->requestedFormatId < 0) + return CHANNEL_RC_OK; + + if (size == 0) + { + if (format) + DEBUG_CLIPRDR("send CB_RESPONSE_FAIL response {format 0x%08" PRIx32 + " [%s] {local 0x%08" PRIx32 "} [%s]", + format->formatToRequest, + ClipboardGetFormatIdString(format->formatToRequest), format->localFormat, + format->formatName); + else + DEBUG_CLIPRDR("send CB_RESPONSE_FAIL response"); + } + else + { + WINPR_ASSERT(format); + DEBUG_CLIPRDR("send response format 0x%08" PRIx32 " [%s] {local 0x%08" PRIx32 "} [%s]", + format->formatToRequest, ClipboardGetFormatIdString(format->formatToRequest), + format->localFormat, format->formatName); + } + /* Request handled, reset to invalid */ + clipboard->requestedFormatId = -1; + + response.common.msgFlags = (data) ? CB_RESPONSE_OK : CB_RESPONSE_FAIL; + response.common.dataLen = size; + response.requestedFormatData = data; + + WINPR_ASSERT(clipboard->context); + WINPR_ASSERT(clipboard->context->ClientFormatDataResponse); + return clipboard->context->ClientFormatDataResponse(clipboard->context, &response); +} + +static wStream* xf_cliprdr_serialize_server_format_list(xfClipboard* clipboard) +{ + UINT32 formatCount = 0; + wStream* s = NULL; + + WINPR_ASSERT(clipboard); + + /* Typical MS Word format list is about 80 bytes long. */ + if (!(s = Stream_New(NULL, 128))) + { + WLog_ERR(TAG, "failed to allocate serialized format list"); + goto error; + } + + /* If present, the last format is always synthetic CF_RAW. Do not include it. */ + formatCount = (clipboard->numServerFormats > 0) ? clipboard->numServerFormats - 1 : 0; + Stream_Write_UINT32(s, formatCount); + + for (UINT32 i = 0; i < formatCount; i++) + { + CLIPRDR_FORMAT* format = &clipboard->serverFormats[i]; + size_t name_length = format->formatName ? strlen(format->formatName) : 0; + + DEBUG_CLIPRDR("server announced 0x%08" PRIx32 " [%s][%s]", format->formatId, + ClipboardGetFormatIdString(format->formatId), format->formatName); + if (!Stream_EnsureRemainingCapacity(s, sizeof(UINT32) + name_length + 1)) + { + WLog_ERR(TAG, "failed to expand serialized format list"); + goto error; + } + + Stream_Write_UINT32(s, format->formatId); + + if (format->formatName) + Stream_Write(s, format->formatName, name_length); + + Stream_Write_UINT8(s, '\0'); + } + + Stream_SealLength(s); + return s; +error: + Stream_Free(s, TRUE); + return NULL; +} + +static CLIPRDR_FORMAT* xf_cliprdr_parse_server_format_list(BYTE* data, size_t length, + UINT32* numFormats) +{ + wStream* s = NULL; + CLIPRDR_FORMAT* formats = NULL; + + WINPR_ASSERT(data || (length == 0)); + WINPR_ASSERT(numFormats); + + if (!(s = Stream_New(data, length))) + { + WLog_ERR(TAG, "failed to allocate stream for parsing serialized format list"); + goto error; + } + + if (!Stream_CheckAndLogRequiredLength(TAG, s, sizeof(UINT32))) + goto error; + + Stream_Read_UINT32(s, *numFormats); + + if (*numFormats > MAX_CLIPBOARD_FORMATS) + { + WLog_ERR(TAG, "unexpectedly large number of formats: %" PRIu32 "", *numFormats); + goto error; + } + + if (!(formats = (CLIPRDR_FORMAT*)calloc(*numFormats, sizeof(CLIPRDR_FORMAT)))) + { + WLog_ERR(TAG, "failed to allocate format list"); + goto error; + } + + for (UINT32 i = 0; i < *numFormats; i++) + { + const char* formatName = NULL; + size_t formatNameLength = 0; + + if (!Stream_CheckAndLogRequiredLength(TAG, s, sizeof(UINT32))) + goto error; + + Stream_Read_UINT32(s, formats[i].formatId); + formatName = (const char*)Stream_Pointer(s); + formatNameLength = strnlen(formatName, Stream_GetRemainingLength(s)); + + if (formatNameLength == Stream_GetRemainingLength(s)) + { + WLog_ERR(TAG, "missing terminating null byte, %" PRIuz " bytes left to read", + formatNameLength); + goto error; + } + + formats[i].formatName = strndup(formatName, formatNameLength); + Stream_Seek(s, formatNameLength + 1); + } + + Stream_Free(s, FALSE); + return formats; +error: + Stream_Free(s, FALSE); + free(formats); + *numFormats = 0; + return NULL; +} + +static void xf_cliprdr_free_formats(CLIPRDR_FORMAT* formats, UINT32 numFormats) +{ + WINPR_ASSERT(formats || (numFormats == 0)); + + for (UINT32 i = 0; i < numFormats; i++) + { + free(formats[i].formatName); + } + + free(formats); +} + +static CLIPRDR_FORMAT* xf_cliprdr_get_raw_server_formats(xfClipboard* clipboard, UINT32* numFormats) +{ + Atom type = None; + int format = 0; + unsigned long length = 0; + unsigned long remaining = 0; + BYTE* data = NULL; + CLIPRDR_FORMAT* formats = NULL; + xfContext* xfc = NULL; + + WINPR_ASSERT(clipboard); + WINPR_ASSERT(numFormats); + + xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + + *numFormats = 0; + LogTagAndXGetWindowProperty( + TAG, xfc->display, clipboard->owner, clipboard->raw_format_list_atom, 0, 4096, False, + clipboard->raw_format_list_atom, &type, &format, &length, &remaining, &data); + + if (data && length > 0 && format == 8 && type == clipboard->raw_format_list_atom) + { + formats = xf_cliprdr_parse_server_format_list(data, length, numFormats); + } + else + { + WLog_ERR(TAG, + "failed to retrieve raw format list: data=%p, length=%lu, format=%d, type=%lu " + "(expected=%lu)", + (void*)data, length, format, (unsigned long)type, + (unsigned long)clipboard->raw_format_list_atom); + } + + if (data) + XFree(data); + + return formats; +} + +static CLIPRDR_FORMAT* xf_cliprdr_get_formats_from_targets(xfClipboard* clipboard, + UINT32* numFormats) +{ + Atom atom = None; + BYTE* data = NULL; + int format_property = 0; + unsigned long length = 0; + unsigned long bytes_left = 0; + CLIPRDR_FORMAT* formats = NULL; + + WINPR_ASSERT(clipboard); + WINPR_ASSERT(numFormats); + + xfContext* xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + + *numFormats = 0; + LogTagAndXGetWindowProperty(TAG, xfc->display, xfc->drawable, clipboard->property_atom, 0, 200, + 0, XA_ATOM, &atom, &format_property, &length, &bytes_left, &data); + + if (length > 0) + { + if (!data) + { + WLog_ERR(TAG, "XGetWindowProperty set length = %lu but data is NULL", length); + goto out; + } + + if (!(formats = (CLIPRDR_FORMAT*)calloc(length, sizeof(CLIPRDR_FORMAT)))) + { + WLog_ERR(TAG, "failed to allocate %lu CLIPRDR_FORMAT structs", length); + goto out; + } + } + + for (unsigned long i = 0; i < length; i++) + { + Atom tatom = ((Atom*)data)[i]; + const xfCliprdrFormat* format = xf_cliprdr_get_client_format_by_atom(clipboard, tatom); + + if (format) + { + formats[*numFormats].formatId = format->formatToRequest; + formats[*numFormats].formatName = _strdup(format->formatName); + *numFormats += 1; + } + } + +out: + + if (data) + XFree(data); + + return formats; +} + +static CLIPRDR_FORMAT* xf_cliprdr_get_client_formats(xfClipboard* clipboard, UINT32* numFormats) +{ + CLIPRDR_FORMAT* formats = NULL; + + WINPR_ASSERT(clipboard); + WINPR_ASSERT(numFormats); + + *numFormats = 0; + + if (xf_cliprdr_is_raw_transfer_available(clipboard)) + { + formats = xf_cliprdr_get_raw_server_formats(clipboard, numFormats); + } + + if (*numFormats == 0) + { + xf_cliprdr_free_formats(formats, *numFormats); + formats = xf_cliprdr_get_formats_from_targets(clipboard, numFormats); + } + + return formats; +} + +static void xf_cliprdr_provide_server_format_list(xfClipboard* clipboard) +{ + wStream* formats = NULL; + xfContext* xfc = NULL; + + WINPR_ASSERT(clipboard); + + xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + + formats = xf_cliprdr_serialize_server_format_list(clipboard); + + if (formats) + { + LogTagAndXChangeProperty(TAG, xfc->display, xfc->drawable, clipboard->raw_format_list_atom, + clipboard->raw_format_list_atom, 8, PropModeReplace, + Stream_Buffer(formats), Stream_Length(formats)); + } + else + { + LogTagAndXDeleteProperty(TAG, xfc->display, xfc->drawable, clipboard->raw_format_list_atom); + } + + Stream_Free(formats, TRUE); +} + +static BOOL xf_clipboard_format_equal(const CLIPRDR_FORMAT* a, const CLIPRDR_FORMAT* b) +{ + WINPR_ASSERT(a); + WINPR_ASSERT(b); + + if (a->formatId != b->formatId) + return FALSE; + if (!a->formatName && !b->formatName) + return TRUE; + if (!a->formatName || !b->formatName) + return FALSE; + return strcmp(a->formatName, b->formatName) == 0; +} + +static BOOL xf_clipboard_changed(xfClipboard* clipboard, const CLIPRDR_FORMAT* formats, + UINT32 numFormats) +{ + WINPR_ASSERT(clipboard); + WINPR_ASSERT(formats || (numFormats == 0)); + + if (clipboard->lastSentNumFormats != numFormats) + return TRUE; + + for (UINT32 x = 0; x < numFormats; x++) + { + const CLIPRDR_FORMAT* cur = &clipboard->lastSentFormats[x]; + BOOL contained = FALSE; + for (UINT32 y = 0; y < numFormats; y++) + { + if (xf_clipboard_format_equal(cur, &formats[y])) + { + contained = TRUE; + break; + } + } + if (!contained) + return TRUE; + } + + return FALSE; +} + +static void xf_clipboard_formats_free(xfClipboard* clipboard) +{ + WINPR_ASSERT(clipboard); + + xf_cliprdr_free_formats(clipboard->lastSentFormats, clipboard->lastSentNumFormats); + clipboard->lastSentFormats = NULL; + clipboard->lastSentNumFormats = 0; +} + +static BOOL xf_clipboard_copy_formats(xfClipboard* clipboard, const CLIPRDR_FORMAT* formats, + UINT32 numFormats) +{ + WINPR_ASSERT(clipboard); + WINPR_ASSERT(formats || (numFormats == 0)); + + xf_clipboard_formats_free(clipboard); + clipboard->lastSentFormats = calloc(numFormats, sizeof(CLIPRDR_FORMAT)); + if (!clipboard->lastSentFormats) + return FALSE; + clipboard->lastSentNumFormats = numFormats; + for (UINT32 x = 0; x < numFormats; x++) + { + CLIPRDR_FORMAT* lcur = &clipboard->lastSentFormats[x]; + const CLIPRDR_FORMAT* cur = &formats[x]; + *lcur = *cur; + if (cur->formatName) + lcur->formatName = _strdup(cur->formatName); + } + return FALSE; +} + +static UINT xf_cliprdr_send_format_list(xfClipboard* clipboard, const CLIPRDR_FORMAT* formats, + UINT32 numFormats, BOOL force) +{ + union + { + const CLIPRDR_FORMAT* cpv; + CLIPRDR_FORMAT* pv; + } cnv = { .cpv = formats }; + const CLIPRDR_FORMAT_LIST formatList = { .common.msgFlags = CB_RESPONSE_OK, + .numFormats = numFormats, + .formats = cnv.pv, + .common.msgType = CB_FORMAT_LIST }; + UINT ret = 0; + + WINPR_ASSERT(clipboard); + WINPR_ASSERT(formats || (numFormats == 0)); + + if (!force && !xf_clipboard_changed(clipboard, formats, numFormats)) + return CHANNEL_RC_OK; + +#if defined(WITH_DEBUG_CLIPRDR) + for (UINT32 x = 0; x < numFormats; x++) + { + const CLIPRDR_FORMAT* format = &formats[x]; + DEBUG_CLIPRDR("announcing format 0x%08" PRIx32 " [%s] [%s]", format->formatId, + ClipboardGetFormatIdString(format->formatId), format->formatName); + } +#endif + + xf_clipboard_copy_formats(clipboard, formats, numFormats); + /* Ensure all pending requests are answered. */ + xf_cliprdr_send_data_response(clipboard, NULL, NULL, 0); + + xf_cliprdr_clear_cached_data(clipboard); + + ret = cliprdr_file_context_notify_new_client_format_list(clipboard->file); + if (ret) + return ret; + + WINPR_ASSERT(clipboard->context); + WINPR_ASSERT(clipboard->context->ClientFormatList); + return clipboard->context->ClientFormatList(clipboard->context, &formatList); +} + +static void xf_cliprdr_get_requested_targets(xfClipboard* clipboard) +{ + UINT32 numFormats = 0; + CLIPRDR_FORMAT* formats = xf_cliprdr_get_client_formats(clipboard, &numFormats); + xf_cliprdr_send_format_list(clipboard, formats, numFormats, FALSE); + xf_cliprdr_free_formats(formats, numFormats); +} + +static void xf_cliprdr_process_requested_data(xfClipboard* clipboard, BOOL hasData, BYTE* data, + int size) +{ + BOOL bSuccess = 0; + UINT32 SrcSize = 0; + UINT32 DstSize = 0; + UINT32 srcFormatId = 0; + BYTE* pDstData = NULL; + const xfCliprdrFormat* format = NULL; + + WINPR_ASSERT(clipboard); + + if (clipboard->incr_starts && hasData) + return; + + format = xf_cliprdr_get_client_format_by_id(clipboard, clipboard->requestedFormatId); + + if (!hasData || !data || !format) + { + xf_cliprdr_send_data_response(clipboard, format, NULL, 0); + return; + } + + srcFormatId = 0; + + switch (format->formatToRequest) + { + case CF_RAW: + srcFormatId = CF_RAW; + break; + + case CF_TEXT: + case CF_OEMTEXT: + case CF_UNICODETEXT: + size = strlen((char*)data) + 1; + srcFormatId = format->localFormat; + break; + + default: + srcFormatId = format->localFormat; + break; + } + + if (srcFormatId == 0) + { + xf_cliprdr_send_data_response(clipboard, format, NULL, 0); + return; + } + + ClipboardLock(clipboard->system); + SrcSize = (UINT32)size; + bSuccess = ClipboardSetData(clipboard->system, srcFormatId, data, SrcSize); + + if (bSuccess) + { + DstSize = 0; + pDstData = (BYTE*)ClipboardGetData(clipboard->system, format->formatToRequest, &DstSize); + } + ClipboardUnlock(clipboard->system); + + if (!pDstData) + { + xf_cliprdr_send_data_response(clipboard, format, NULL, 0); + return; + } + + /* + * File lists require a bit of postprocessing to convert them from WinPR's FILDESCRIPTOR + * format to CLIPRDR_FILELIST expected by the server. + * + * We check for "FileGroupDescriptorW" format being registered (i.e., nonzero) in order + * to not process CF_RAW as a file list in case WinPR does not support file transfers. + */ + ClipboardLock(clipboard->system); + if (format->formatToRequest && + (format->formatToRequest == + ClipboardGetFormatId(clipboard->system, type_FileGroupDescriptorW))) + { + UINT error = NO_ERROR; + FILEDESCRIPTORW* file_array = (FILEDESCRIPTORW*)pDstData; + UINT32 file_count = DstSize / sizeof(FILEDESCRIPTORW); + pDstData = NULL; + DstSize = 0; + + const UINT32 flags = cliprdr_file_context_remote_get_flags(clipboard->file); + error = cliprdr_serialize_file_list_ex(flags, file_array, file_count, &pDstData, &DstSize); + + if (error) + WLog_ERR(TAG, "failed to serialize CLIPRDR_FILELIST: 0x%08X", error); + + UINT32 formatId = ClipboardGetFormatId(clipboard->system, mime_uri_list); + UINT32 url_size = 0; + ClipboardLock(clipboard->system); + char* url = ClipboardGetData(clipboard->system, formatId, &url_size); + ClipboardUnlock(clipboard->system); + cliprdr_file_context_update_client_data(clipboard->file, url, url_size); + free(url); + + free(file_array); + } + ClipboardUnlock(clipboard->system); + + xf_cliprdr_send_data_response(clipboard, format, pDstData, DstSize); + free(pDstData); +} + +static BOOL xf_cliprdr_get_requested_data(xfClipboard* clipboard, Atom target) +{ + Atom type = 0; + BYTE* data = NULL; + BOOL has_data = FALSE; + int format_property = 0; + unsigned long dummy = 0; + unsigned long length = 0; + unsigned long bytes_left = 0; + const xfCliprdrFormat* format = NULL; + xfContext* xfc = NULL; + + WINPR_ASSERT(clipboard); + + xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + + format = xf_cliprdr_get_client_format_by_id(clipboard, clipboard->requestedFormatId); + + if (!format || (format->atom != target)) + { + xf_cliprdr_send_data_response(clipboard, format, NULL, 0); + return FALSE; + } + + LogTagAndXGetWindowProperty(TAG, xfc->display, xfc->drawable, clipboard->property_atom, 0, 0, 0, + target, &type, &format_property, &length, &bytes_left, &data); + + if (data) + { + XFree(data); + data = NULL; + } + + if (bytes_left <= 0 && !clipboard->incr_starts) + { + } + else if (type == clipboard->incr_atom) + { + clipboard->incr_starts = TRUE; + + if (clipboard->incr_data) + { + free(clipboard->incr_data); + clipboard->incr_data = NULL; + } + + clipboard->incr_data_length = 0; + has_data = TRUE; /* data will be followed in PropertyNotify event */ + XSelectInput(xfc->display, xfc->drawable, PropertyChangeMask); + } + else + { + if (bytes_left <= 0) + { + /* INCR finish */ + data = clipboard->incr_data; + clipboard->incr_data = NULL; + bytes_left = clipboard->incr_data_length; + clipboard->incr_data_length = 0; + clipboard->incr_starts = 0; + has_data = TRUE; + } + else if (LogTagAndXGetWindowProperty( + TAG, xfc->display, xfc->drawable, clipboard->property_atom, 0, bytes_left, 0, + target, &type, &format_property, &length, &dummy, &data) == Success) + { + if (clipboard->incr_starts) + { + BYTE* new_data = NULL; + bytes_left = length * format_property / 8; + new_data = + (BYTE*)realloc(clipboard->incr_data, clipboard->incr_data_length + bytes_left); + + if (new_data) + { + + clipboard->incr_data = new_data; + CopyMemory(clipboard->incr_data + clipboard->incr_data_length, data, + bytes_left); + clipboard->incr_data_length += bytes_left; + XFree(data); + data = NULL; + } + } + + has_data = TRUE; + } + else + { + } + } + + LogTagAndXDeleteProperty(TAG, xfc->display, xfc->drawable, clipboard->property_atom); + xf_cliprdr_process_requested_data(clipboard, has_data, data, bytes_left); + + if (data) + XFree(data); + + return TRUE; +} + +static void xf_cliprdr_append_target(xfClipboard* clipboard, Atom target) +{ + WINPR_ASSERT(clipboard); + + if (clipboard->numTargets >= ARRAYSIZE(clipboard->targets)) + return; + + for (size_t i = 0; i < clipboard->numTargets; i++) + { + if (clipboard->targets[i] == target) + return; + } + + clipboard->targets[clipboard->numTargets++] = target; +} + +static void xf_cliprdr_provide_targets(xfClipboard* clipboard, const XSelectionEvent* respond) +{ + xfContext* xfc = NULL; + + WINPR_ASSERT(clipboard); + + xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + + if (respond->property != None) + { + LogTagAndXChangeProperty(TAG, xfc->display, respond->requestor, respond->property, XA_ATOM, + 32, PropModeReplace, (BYTE*)clipboard->targets, + clipboard->numTargets); + } +} + +static void xf_cliprdr_provide_timestamp(xfClipboard* clipboard, const XSelectionEvent* respond) +{ + xfContext* xfc = NULL; + + WINPR_ASSERT(clipboard); + + xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + + if (respond->property != None) + { + LogTagAndXChangeProperty(TAG, xfc->display, respond->requestor, respond->property, + XA_INTEGER, 32, PropModeReplace, + (BYTE*)&clipboard->selection_ownership_timestamp, 1); + } +} + +static void xf_cliprdr_provide_data(xfClipboard* clipboard, const XSelectionEvent* respond, + const BYTE* data, UINT32 size) +{ + xfContext* xfc = NULL; + + WINPR_ASSERT(clipboard); + + xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + + if (respond->property != None) + { + LogTagAndXChangeProperty(TAG, xfc->display, respond->requestor, respond->property, + respond->target, 8, PropModeReplace, data, size); + } +} + +static void log_selection_event(xfContext* xfc, const XEvent* event) +{ + const DWORD level = WLOG_TRACE; + static wLog* _log_cached_ptr = NULL; + if (!_log_cached_ptr) + _log_cached_ptr = WLog_Get(TAG); + if (WLog_IsLevelActive(_log_cached_ptr, level)) + { + + switch (event->type) + { + case SelectionClear: + { + const XSelectionClearEvent* xevent = &event->xselectionclear; + char* selection = Safe_XGetAtomName(xfc->display, xevent->selection); + WLog_Print(_log_cached_ptr, level, "got event %s [selection %s]", + x11_event_string(event->type), selection); + XFree(selection); + } + break; + case SelectionNotify: + { + const XSelectionEvent* xevent = &event->xselection; + char* selection = Safe_XGetAtomName(xfc->display, xevent->selection); + char* target = Safe_XGetAtomName(xfc->display, xevent->target); + char* property = Safe_XGetAtomName(xfc->display, xevent->property); + WLog_Print(_log_cached_ptr, level, + "got event %s [selection %s, target %s, property %s]", + x11_event_string(event->type), selection, target, property); + XFree(selection); + XFree(target); + XFree(property); + } + break; + case SelectionRequest: + { + const XSelectionRequestEvent* xevent = &event->xselectionrequest; + char* selection = Safe_XGetAtomName(xfc->display, xevent->selection); + char* target = Safe_XGetAtomName(xfc->display, xevent->target); + char* property = Safe_XGetAtomName(xfc->display, xevent->property); + WLog_Print(_log_cached_ptr, level, + "got event %s [selection %s, target %s, property %s]", + x11_event_string(event->type), selection, target, property); + XFree(selection); + XFree(target); + XFree(property); + } + break; + case PropertyNotify: + { + const XPropertyEvent* xevent = &event->xproperty; + char* atom = Safe_XGetAtomName(xfc->display, xevent->atom); + WLog_Print(_log_cached_ptr, level, "got event %s [atom %s]", + x11_event_string(event->type), atom); + XFree(atom); + } + break; + default: + break; + } + } +} + +static BOOL xf_cliprdr_process_selection_notify(xfClipboard* clipboard, + const XSelectionEvent* xevent) +{ + WINPR_ASSERT(clipboard); + WINPR_ASSERT(xevent); + + if (xevent->target == clipboard->targets[1]) + { + if (xevent->property == None) + { + xf_cliprdr_send_client_format_list(clipboard, FALSE); + } + else + { + xf_cliprdr_get_requested_targets(clipboard); + } + + return TRUE; + } + else + { + return xf_cliprdr_get_requested_data(clipboard, xevent->target); + } +} + +void xf_cliprdr_clear_cached_data(xfClipboard* clipboard) +{ + WINPR_ASSERT(clipboard); + + ClipboardLock(clipboard->system); + ClipboardEmpty(clipboard->system); + cliprdr_file_context_clear(clipboard->file); + + HashTable_Clear(clipboard->cachedData); + HashTable_Clear(clipboard->cachedRawData); + + cliprdr_file_context_clear(clipboard->file); + ClipboardUnlock(clipboard->system); +} + +static UINT32 get_dst_format_id_for_local_request(xfClipboard* clipboard, + const xfCliprdrFormat* format) +{ + UINT32 dstFormatId = 0; + + WINPR_ASSERT(format); + + if (!format->formatName) + return format->localFormat; + + ClipboardLock(clipboard->system); + if (strcmp(format->formatName, type_HtmlFormat) == 0) + dstFormatId = ClipboardGetFormatId(clipboard->system, mime_html); + ClipboardUnlock(clipboard->system); + + if (strcmp(format->formatName, type_FileGroupDescriptorW) == 0) + dstFormatId = format->localFormat; + + return dstFormatId; +} + +static void get_src_format_info_for_local_request(xfClipboard* clipboard, + const xfCliprdrFormat* format, + UINT32* srcFormatId, BOOL* nullTerminated) +{ + *srcFormatId = 0; + *nullTerminated = FALSE; + + if (format->formatName) + { + ClipboardLock(clipboard->system); + if (strcmp(format->formatName, type_HtmlFormat) == 0) + { + *srcFormatId = ClipboardGetFormatId(clipboard->system, type_HtmlFormat); + *nullTerminated = TRUE; + } + else if (strcmp(format->formatName, type_FileGroupDescriptorW) == 0) + { + *srcFormatId = ClipboardGetFormatId(clipboard->system, type_FileGroupDescriptorW); + *nullTerminated = TRUE; + } + ClipboardUnlock(clipboard->system); + } + else + { + *srcFormatId = format->formatToRequest; + switch (format->formatToRequest) + { + case CF_TEXT: + case CF_OEMTEXT: + case CF_UNICODETEXT: + *nullTerminated = TRUE; + break; + case CF_DIB: + *srcFormatId = CF_DIB; + break; + case CF_TIFF: + *srcFormatId = CF_TIFF; + break; + default: + break; + } + } +} + +static xfCachedData* convert_data_from_existing_raw_data(xfClipboard* clipboard, + xfCachedData* cached_raw_data, + UINT32 srcFormatId, BOOL nullTerminated, + UINT32 dstFormatId) +{ + xfCachedData* cached_data = NULL; + BOOL success = 0; + BYTE* dst_data = NULL; + UINT32 dst_size = 0; + + WINPR_ASSERT(clipboard); + WINPR_ASSERT(cached_raw_data); + WINPR_ASSERT(cached_raw_data->data); + + ClipboardLock(clipboard->system); + success = ClipboardSetData(clipboard->system, srcFormatId, cached_raw_data->data, + cached_raw_data->data_length); + if (!success) + { + WLog_WARN(TAG, "Failed to set clipboard data (formatId: %u, data: %p, data_length: %u)", + srcFormatId, cached_raw_data->data, cached_raw_data->data_length); + ClipboardUnlock(clipboard->system); + return NULL; + } + + dst_data = ClipboardGetData(clipboard->system, dstFormatId, &dst_size); + if (!dst_data) + { + WLog_WARN(TAG, "Failed to get converted clipboard data"); + ClipboardUnlock(clipboard->system); + return NULL; + } + ClipboardUnlock(clipboard->system); + + if (nullTerminated) + { + BYTE* nullTerminator = memchr(dst_data, '\0', dst_size); + if (nullTerminator) + dst_size = nullTerminator - dst_data; + } + + cached_data = xf_cached_data_new(dst_data, dst_size); + if (!cached_data) + { + WLog_WARN(TAG, "Failed to allocate cache entry"); + free(dst_data); + return NULL; + } + + if (!HashTable_Insert(clipboard->cachedData, (void*)(UINT_PTR)dstFormatId, cached_data)) + { + WLog_WARN(TAG, "Failed to cache clipboard data"); + xf_cached_data_free(cached_data); + return NULL; + } + + return cached_data; +} + +static BOOL xf_cliprdr_process_selection_request(xfClipboard* clipboard, + const XSelectionRequestEvent* xevent) +{ + int fmt = 0; + Atom type = 0; + UINT32 formatId = 0; + XSelectionEvent* respond = NULL; + BYTE* data = NULL; + BOOL delayRespond = 0; + BOOL rawTransfer = 0; + unsigned long length = 0; + unsigned long bytes_left = 0; + xfContext* xfc = NULL; + + WINPR_ASSERT(clipboard); + WINPR_ASSERT(xevent); + + xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + + if (xevent->owner != xfc->drawable) + return FALSE; + + delayRespond = FALSE; + + if (!(respond = (XSelectionEvent*)calloc(1, sizeof(XSelectionEvent)))) + { + WLog_ERR(TAG, "failed to allocate XEvent data"); + return FALSE; + } + + respond->property = None; + respond->type = SelectionNotify; + respond->display = xevent->display; + respond->requestor = xevent->requestor; + respond->selection = xevent->selection; + respond->target = xevent->target; + respond->time = xevent->time; + + if (xevent->target == clipboard->targets[0]) /* TIMESTAMP */ + { + /* Someone else requests the selection's timestamp */ + respond->property = xevent->property; + xf_cliprdr_provide_timestamp(clipboard, respond); + } + else if (xevent->target == clipboard->targets[1]) /* TARGETS */ + { + /* Someone else requests our available formats */ + respond->property = xevent->property; + xf_cliprdr_provide_targets(clipboard, respond); + } + else + { + const CLIPRDR_FORMAT* format = + xf_cliprdr_get_server_format_by_atom(clipboard, xevent->target); + const xfCliprdrFormat* cformat = + xf_cliprdr_get_client_format_by_atom(clipboard, xevent->target); + + if (format && (xevent->requestor != xfc->drawable)) + { + formatId = format->formatId; + rawTransfer = FALSE; + xfCachedData* cached_data = NULL; + UINT32 dstFormatId = 0; + + if (formatId == CF_RAW) + { + if (LogTagAndXGetWindowProperty( + TAG, xfc->display, xevent->requestor, clipboard->property_atom, 0, 4, 0, + XA_INTEGER, &type, &fmt, &length, &bytes_left, &data) != Success) + { + } + + if (data) + { + rawTransfer = TRUE; + CopyMemory(&formatId, data, 4); + XFree(data); + } + } + + dstFormatId = get_dst_format_id_for_local_request(clipboard, cformat); + DEBUG_CLIPRDR("formatId: %u, dstFormatId: %u", formatId, dstFormatId); + + if (!rawTransfer) + cached_data = + HashTable_GetItemValue(clipboard->cachedData, (void*)(UINT_PTR)dstFormatId); + else + cached_data = + HashTable_GetItemValue(clipboard->cachedRawData, (void*)(UINT_PTR)formatId); + + DEBUG_CLIPRDR("hasCachedData: %u, rawTransfer: %u", cached_data ? 1 : 0, rawTransfer); + + if (!cached_data && !rawTransfer) + { + UINT32 srcFormatId = 0; + BOOL nullTerminated = FALSE; + xfCachedData* cached_raw_data = NULL; + + get_src_format_info_for_local_request(clipboard, cformat, &srcFormatId, + &nullTerminated); + cached_raw_data = + HashTable_GetItemValue(clipboard->cachedRawData, (void*)(UINT_PTR)srcFormatId); + + DEBUG_CLIPRDR("hasCachedRawData: %u, rawDataLength: %u", cached_raw_data ? 1 : 0, + cached_raw_data ? cached_raw_data->data_length : 0); + + if (cached_raw_data && cached_raw_data->data_length != 0) + cached_data = convert_data_from_existing_raw_data( + clipboard, cached_raw_data, srcFormatId, nullTerminated, dstFormatId); + } + + DEBUG_CLIPRDR("hasCachedData: %u", cached_data ? 1 : 0); + + if (cached_data) + { + /* Cached clipboard data available. Send it now */ + respond->property = xevent->property; + + // NOLINTNEXTLINE(clang-analyzer-unix.Malloc) + xf_cliprdr_provide_data(clipboard, respond, cached_data->data, + cached_data->data_length); + } + else if (clipboard->respond) + { + /* duplicate request */ + } + else + { + WINPR_ASSERT(cformat); + + /** + * Send clipboard data request to the server. + * Response will be postponed after receiving the data + */ + respond->property = xevent->property; + clipboard->respond = respond; + clipboard->requestedFormat = cformat; + clipboard->data_raw_format = rawTransfer; + delayRespond = TRUE; + xf_cliprdr_send_data_request(clipboard, formatId, cformat); + } + } + } + + if (!delayRespond) + { + union + { + XEvent* ev; + XSelectionEvent* sev; + } conv; + + conv.sev = respond; + XSendEvent(xfc->display, xevent->requestor, 0, 0, conv.ev); + XFlush(xfc->display); + free(respond); + } + + return TRUE; +} + +static BOOL xf_cliprdr_process_selection_clear(xfClipboard* clipboard, + const XSelectionClearEvent* xevent) +{ + xfContext* xfc = NULL; + + WINPR_ASSERT(clipboard); + WINPR_ASSERT(xevent); + + xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + + WINPR_UNUSED(xevent); + + if (xf_cliprdr_is_self_owned(clipboard)) + return FALSE; + + LogTagAndXDeleteProperty(TAG, xfc->display, clipboard->root_window, clipboard->property_atom); + return TRUE; +} + +static BOOL xf_cliprdr_process_property_notify(xfClipboard* clipboard, const XPropertyEvent* xevent) +{ + const xfCliprdrFormat* format = NULL; + xfContext* xfc = NULL; + + if (!clipboard) + return TRUE; + + xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + WINPR_ASSERT(xevent); + + if (xevent->atom == clipboard->timestamp_property_atom) + { + /* This is the response to the property change we did + * in xf_cliprdr_prepare_to_set_selection_owner. Now + * we can set ourselves as the selection owner. (See + * comments in those functions below.) */ + xf_cliprdr_set_selection_owner(xfc, clipboard, xevent->time); + return TRUE; + } + + if (xevent->atom != clipboard->property_atom) + return FALSE; /* Not cliprdr-related */ + + if (xevent->window == clipboard->root_window) + { + xf_cliprdr_send_client_format_list(clipboard, FALSE); + } + else if ((xevent->window == xfc->drawable) && (xevent->state == PropertyNewValue) && + clipboard->incr_starts) + { + format = xf_cliprdr_get_client_format_by_id(clipboard, clipboard->requestedFormatId); + + if (format) + xf_cliprdr_get_requested_data(clipboard, format->atom); + } + + return TRUE; +} + +void xf_cliprdr_handle_xevent(xfContext* xfc, const XEvent* event) +{ + xfClipboard* clipboard = NULL; + + if (!xfc || !event) + return; + + clipboard = xfc->clipboard; + + if (!clipboard) + return; + +#ifdef WITH_XFIXES + + if (clipboard->xfixes_supported && + event->type == XFixesSelectionNotify + clipboard->xfixes_event_base) + { + const XFixesSelectionNotifyEvent* se = (const XFixesSelectionNotifyEvent*)event; + + if (se->subtype == XFixesSetSelectionOwnerNotify) + { + if (se->selection != clipboard->clipboard_atom) + return; + + if (XGetSelectionOwner(xfc->display, se->selection) == xfc->drawable) + return; + + clipboard->owner = None; + xf_cliprdr_check_owner(clipboard); + } + + return; + } + +#endif + + switch (event->type) + { + case SelectionNotify: + log_selection_event(xfc, event); + xf_cliprdr_process_selection_notify(clipboard, &event->xselection); + break; + + case SelectionRequest: + log_selection_event(xfc, event); + xf_cliprdr_process_selection_request(clipboard, &event->xselectionrequest); + break; + + case SelectionClear: + log_selection_event(xfc, event); + xf_cliprdr_process_selection_clear(clipboard, &event->xselectionclear); + break; + + case PropertyNotify: + log_selection_event(xfc, event); + xf_cliprdr_process_property_notify(clipboard, &event->xproperty); + break; + + case FocusIn: + if (!clipboard->xfixes_supported) + { + xf_cliprdr_check_owner(clipboard); + } + + break; + default: + break; + } +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_cliprdr_send_client_capabilities(xfClipboard* clipboard) +{ + CLIPRDR_CAPABILITIES capabilities = { 0 }; + CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet = { 0 }; + + WINPR_ASSERT(clipboard); + + capabilities.cCapabilitiesSets = 1; + capabilities.capabilitySets = (CLIPRDR_CAPABILITY_SET*)&(generalCapabilitySet); + generalCapabilitySet.capabilitySetType = CB_CAPSTYPE_GENERAL; + generalCapabilitySet.capabilitySetLength = 12; + generalCapabilitySet.version = CB_CAPS_VERSION_2; + generalCapabilitySet.generalFlags = CB_USE_LONG_FORMAT_NAMES; + + WINPR_ASSERT(clipboard); + generalCapabilitySet.generalFlags |= cliprdr_file_context_current_flags(clipboard->file); + + WINPR_ASSERT(clipboard->context); + WINPR_ASSERT(clipboard->context->ClientCapabilities); + return clipboard->context->ClientCapabilities(clipboard->context, &capabilities); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_cliprdr_send_client_format_list(xfClipboard* clipboard, BOOL force) +{ + WINPR_ASSERT(clipboard); + + xfContext* xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + + UINT32 numFormats = 0; + CLIPRDR_FORMAT* formats = xf_cliprdr_get_client_formats(clipboard, &numFormats); + + const UINT ret = xf_cliprdr_send_format_list(clipboard, formats, numFormats, force); + + if (clipboard->owner && clipboard->owner != xfc->drawable) + { + /* Request the owner for TARGETS, and wait for SelectionNotify event */ + XConvertSelection(xfc->display, clipboard->clipboard_atom, clipboard->targets[1], + clipboard->property_atom, xfc->drawable, CurrentTime); + } + + xf_cliprdr_free_formats(formats, numFormats); + + return ret; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_cliprdr_send_client_format_list_response(xfClipboard* clipboard, BOOL status) +{ + CLIPRDR_FORMAT_LIST_RESPONSE formatListResponse = { 0 }; + + formatListResponse.common.msgType = CB_FORMAT_LIST_RESPONSE; + formatListResponse.common.msgFlags = status ? CB_RESPONSE_OK : CB_RESPONSE_FAIL; + formatListResponse.common.dataLen = 0; + + WINPR_ASSERT(clipboard); + WINPR_ASSERT(clipboard->context); + WINPR_ASSERT(clipboard->context->ClientFormatListResponse); + return clipboard->context->ClientFormatListResponse(clipboard->context, &formatListResponse); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_cliprdr_monitor_ready(CliprdrClientContext* context, + const CLIPRDR_MONITOR_READY* monitorReady) +{ + UINT ret = 0; + xfClipboard* clipboard = NULL; + + WINPR_ASSERT(context); + WINPR_ASSERT(monitorReady); + + clipboard = cliprdr_file_context_get_context(context->custom); + WINPR_ASSERT(clipboard); + + WINPR_UNUSED(monitorReady); + + if ((ret = xf_cliprdr_send_client_capabilities(clipboard)) != CHANNEL_RC_OK) + return ret; + + xf_clipboard_formats_free(clipboard); + + if ((ret = xf_cliprdr_send_client_format_list(clipboard, TRUE)) != CHANNEL_RC_OK) + return ret; + + clipboard->sync = TRUE; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_cliprdr_server_capabilities(CliprdrClientContext* context, + const CLIPRDR_CAPABILITIES* capabilities) +{ + const CLIPRDR_GENERAL_CAPABILITY_SET* generalCaps = NULL; + const BYTE* capsPtr = NULL; + xfClipboard* clipboard = NULL; + + WINPR_ASSERT(context); + WINPR_ASSERT(capabilities); + + clipboard = cliprdr_file_context_get_context(context->custom); + WINPR_ASSERT(clipboard); + + capsPtr = (const BYTE*)capabilities->capabilitySets; + WINPR_ASSERT(capsPtr); + + cliprdr_file_context_remote_set_flags(clipboard->file, 0); + + for (UINT32 i = 0; i < capabilities->cCapabilitiesSets; i++) + { + const CLIPRDR_CAPABILITY_SET* caps = (const CLIPRDR_CAPABILITY_SET*)capsPtr; + + if (caps->capabilitySetType == CB_CAPSTYPE_GENERAL) + { + generalCaps = (const CLIPRDR_GENERAL_CAPABILITY_SET*)caps; + + cliprdr_file_context_remote_set_flags(clipboard->file, generalCaps->generalFlags); + } + + capsPtr += caps->capabilitySetLength; + } + + return CHANNEL_RC_OK; +} + +static void xf_cliprdr_prepare_to_set_selection_owner(xfContext* xfc, xfClipboard* clipboard) +{ + WINPR_ASSERT(xfc); + WINPR_ASSERT(clipboard); + /* + * When you're writing to the selection in response to a + * normal X event like a mouse click or keyboard action, you + * get the selection timestamp by copying the time field out + * of that X event. Here, we're doing it on our own + * initiative, so we have to _request_ the X server time. + * + * There isn't a GetServerTime request in the X protocol, so I + * work around it by setting a property on our own window, and + * waiting for a PropertyNotify event to come back telling me + * it's been done - which will have a timestamp we can use. + */ + + /* We have to set the property to some value, but it doesn't + * matter what. Set it to its own name, which we have here + * anyway! */ + Atom value = clipboard->timestamp_property_atom; + + LogTagAndXChangeProperty(TAG, xfc->display, xfc->drawable, clipboard->timestamp_property_atom, + XA_ATOM, 32, PropModeReplace, (BYTE*)&value, 1); + XFlush(xfc->display); +} + +static void xf_cliprdr_set_selection_owner(xfContext* xfc, xfClipboard* clipboard, Time timestamp) +{ + WINPR_ASSERT(xfc); + WINPR_ASSERT(clipboard); + /* + * Actually set ourselves up as the selection owner, now that + * we have a timestamp to use. + */ + + clipboard->selection_ownership_timestamp = timestamp; + XSetSelectionOwner(xfc->display, clipboard->clipboard_atom, xfc->drawable, timestamp); + XFlush(xfc->display); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_cliprdr_server_format_list(CliprdrClientContext* context, + const CLIPRDR_FORMAT_LIST* formatList) +{ + xfContext* xfc = NULL; + UINT ret = 0; + xfClipboard* clipboard = NULL; + + WINPR_ASSERT(context); + WINPR_ASSERT(formatList); + + clipboard = cliprdr_file_context_get_context(context->custom); + WINPR_ASSERT(clipboard); + + xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + + /* Clear the active SelectionRequest, as it is now invalid */ + free(clipboard->respond); + clipboard->respond = NULL; + + xf_clipboard_formats_free(clipboard); + xf_cliprdr_clear_cached_data(clipboard); + clipboard->requestedFormat = NULL; + + xf_clipboard_free_server_formats(clipboard); + + clipboard->numServerFormats = formatList->numFormats + 1; /* +1 for CF_RAW */ + + if (!(clipboard->serverFormats = + (CLIPRDR_FORMAT*)calloc(clipboard->numServerFormats, sizeof(CLIPRDR_FORMAT)))) + { + WLog_ERR(TAG, "failed to allocate %d CLIPRDR_FORMAT structs", clipboard->numServerFormats); + return CHANNEL_RC_NO_MEMORY; + } + + for (size_t i = 0; i < formatList->numFormats; i++) + { + const CLIPRDR_FORMAT* format = &formatList->formats[i]; + CLIPRDR_FORMAT* srvFormat = &clipboard->serverFormats[i]; + + srvFormat->formatId = format->formatId; + + if (format->formatName) + { + srvFormat->formatName = _strdup(format->formatName); + + if (!srvFormat->formatName) + { + for (UINT32 k = 0; k < i; k++) + free(clipboard->serverFormats[k].formatName); + + clipboard->numServerFormats = 0; + free(clipboard->serverFormats); + clipboard->serverFormats = NULL; + return CHANNEL_RC_NO_MEMORY; + } + } + } + + ret = cliprdr_file_context_notify_new_server_format_list(clipboard->file); + if (ret) + return ret; + + /* CF_RAW is always implicitly supported by the server */ + { + CLIPRDR_FORMAT* format = &clipboard->serverFormats[formatList->numFormats]; + format->formatId = CF_RAW; + format->formatName = NULL; + } + xf_cliprdr_provide_server_format_list(clipboard); + clipboard->numTargets = 2; + + for (size_t i = 0; i < formatList->numFormats; i++) + { + const CLIPRDR_FORMAT* format = &formatList->formats[i]; + + for (size_t j = 0; j < clipboard->numClientFormats; j++) + { + const xfCliprdrFormat* clientFormat = &clipboard->clientFormats[j]; + if (xf_cliprdr_formats_equal(format, clientFormat)) + { + if ((clientFormat->formatName != NULL) && + (strcmp(type_FileGroupDescriptorW, clientFormat->formatName) == 0)) + { + if (!cliprdr_file_context_has_local_support(clipboard->file)) + continue; + } + xf_cliprdr_append_target(clipboard, clientFormat->atom); + } + } + } + + ret = xf_cliprdr_send_client_format_list_response(clipboard, TRUE); + if (xfc->remote_app) + xf_cliprdr_set_selection_owner(xfc, clipboard, CurrentTime); + else + xf_cliprdr_prepare_to_set_selection_owner(xfc, clipboard); + return ret; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +xf_cliprdr_server_format_list_response(CliprdrClientContext* context, + const CLIPRDR_FORMAT_LIST_RESPONSE* formatListResponse) +{ + WINPR_ASSERT(context); + WINPR_ASSERT(formatListResponse); + // xfClipboard* clipboard = (xfClipboard*) context->custom; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +xf_cliprdr_server_format_data_request(CliprdrClientContext* context, + const CLIPRDR_FORMAT_DATA_REQUEST* formatDataRequest) +{ + BOOL rawTransfer = 0; + const xfCliprdrFormat* format = NULL; + UINT32 formatId = 0; + xfContext* xfc = NULL; + xfClipboard* clipboard = NULL; + + WINPR_ASSERT(context); + WINPR_ASSERT(formatDataRequest); + + clipboard = cliprdr_file_context_get_context(context->custom); + WINPR_ASSERT(clipboard); + + xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + + formatId = formatDataRequest->requestedFormatId; + + rawTransfer = xf_cliprdr_is_raw_transfer_available(clipboard); + + if (rawTransfer) + { + format = xf_cliprdr_get_client_format_by_id(clipboard, CF_RAW); + LogTagAndXChangeProperty(TAG, xfc->display, xfc->drawable, clipboard->property_atom, + XA_INTEGER, 32, PropModeReplace, (BYTE*)&formatId, 1); + } + else + format = xf_cliprdr_get_client_format_by_id(clipboard, formatId); + + clipboard->requestedFormatId = rawTransfer ? CF_RAW : formatId; + if (!format) + return xf_cliprdr_send_data_response(clipboard, format, NULL, 0); + + DEBUG_CLIPRDR("requested format 0x%08" PRIx32 " [%s] {local 0x%08" PRIx32 "} [%s]", + format->formatToRequest, ClipboardGetFormatIdString(format->formatToRequest), + format->localFormat, format->formatName); + XConvertSelection(xfc->display, clipboard->clipboard_atom, format->atom, + clipboard->property_atom, xfc->drawable, CurrentTime); + XFlush(xfc->display); + /* After this point, we expect a SelectionNotify event from the clipboard owner. */ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +xf_cliprdr_server_format_data_response(CliprdrClientContext* context, + const CLIPRDR_FORMAT_DATA_RESPONSE* formatDataResponse) +{ + BOOL bSuccess = 0; + BYTE* pDstData = NULL; + UINT32 DstSize = 0; + UINT32 SrcSize = 0; + UINT32 srcFormatId = 0; + UINT32 dstFormatId = 0; + BOOL nullTerminated = FALSE; + UINT32 size = 0; + const BYTE* data = NULL; + xfContext* xfc = NULL; + xfClipboard* clipboard = NULL; + xfCachedData* cached_data = NULL; + + WINPR_ASSERT(context); + WINPR_ASSERT(formatDataResponse); + + clipboard = cliprdr_file_context_get_context(context->custom); + WINPR_ASSERT(clipboard); + + xfc = clipboard->xfc; + WINPR_ASSERT(xfc); + + size = formatDataResponse->common.dataLen; + data = formatDataResponse->requestedFormatData; + + if (formatDataResponse->common.msgFlags == CB_RESPONSE_FAIL) + { + WLog_WARN(TAG, "Format Data Response PDU msgFlags is CB_RESPONSE_FAIL"); + free(clipboard->respond); + clipboard->respond = NULL; + return CHANNEL_RC_OK; + } + + if (!clipboard->respond) + return CHANNEL_RC_OK; + + pDstData = NULL; + DstSize = 0; + srcFormatId = 0; + dstFormatId = 0; + + const xfCliprdrFormat* format = clipboard->requestedFormat; + if (clipboard->data_raw_format) + { + srcFormatId = CF_RAW; + dstFormatId = CF_RAW; + } + else if (!format) + return ERROR_INTERNAL_ERROR; + else if (format->formatName) + { + ClipboardLock(clipboard->system); + if (strcmp(format->formatName, type_HtmlFormat) == 0) + { + srcFormatId = ClipboardGetFormatId(clipboard->system, type_HtmlFormat); + dstFormatId = ClipboardGetFormatId(clipboard->system, mime_html); + nullTerminated = TRUE; + } + + if (strcmp(format->formatName, type_FileGroupDescriptorW) == 0) + { + if (!cliprdr_file_context_update_server_data(clipboard->file, clipboard->system, data, + size)) + WLog_WARN(TAG, "failed to update file descriptors"); + + srcFormatId = ClipboardGetFormatId(clipboard->system, type_FileGroupDescriptorW); + const xfCliprdrFormat* dstTargetFormat = + xf_cliprdr_get_client_format_by_atom(clipboard, clipboard->respond->target); + if (!dstTargetFormat) + { + dstFormatId = ClipboardGetFormatId(clipboard->system, mime_uri_list); + } + else + { + dstFormatId = dstTargetFormat->localFormat; + } + + nullTerminated = TRUE; + } + ClipboardUnlock(clipboard->system); + } + else + { + srcFormatId = format->formatToRequest; + dstFormatId = format->localFormat; + switch (format->formatToRequest) + { + case CF_TEXT: + nullTerminated = TRUE; + break; + + case CF_OEMTEXT: + nullTerminated = TRUE; + break; + + case CF_UNICODETEXT: + nullTerminated = TRUE; + break; + + case CF_DIB: + srcFormatId = CF_DIB; + break; + + case CF_TIFF: + srcFormatId = CF_TIFF; + break; + + default: + break; + } + } + + DEBUG_CLIPRDR("requested format 0x%08" PRIx32 " [%s] {local 0x%08" PRIx32 "} [%s]", + format->formatToRequest, ClipboardGetFormatIdString(format->formatToRequest), + format->localFormat, format->formatName); + SrcSize = (UINT32)size; + + DEBUG_CLIPRDR("srcFormatId: %u, dstFormatId: %u", srcFormatId, dstFormatId); + + ClipboardLock(clipboard->system); + bSuccess = ClipboardSetData(clipboard->system, srcFormatId, data, SrcSize); + + BOOL willQuit = FALSE; + if (bSuccess) + { + if (SrcSize == 0) + { + WLog_DBG(TAG, "skipping, empty data detected!"); + free(clipboard->respond); + clipboard->respond = NULL; + willQuit = TRUE; + } + else + { + pDstData = (BYTE*)ClipboardGetData(clipboard->system, dstFormatId, &DstSize); + + if (!pDstData) + { + WLog_WARN(TAG, "failed to get clipboard data in format %s [source format %s]", + ClipboardGetFormatName(clipboard->system, dstFormatId), + ClipboardGetFormatName(clipboard->system, srcFormatId)); + } + + if (nullTerminated && pDstData) + { + BYTE* nullTerminator = memchr(pDstData, '\0', DstSize); + if (nullTerminator) + DstSize = nullTerminator - pDstData; + } + } + } + ClipboardUnlock(clipboard->system); + if (willQuit) + return CHANNEL_RC_OK; + + /* Cache converted and original data to avoid doing a possibly costly + * conversion again on subsequent requests */ + if (pDstData) + { + cached_data = xf_cached_data_new(pDstData, DstSize); + if (!cached_data) + { + WLog_WARN(TAG, "Failed to allocate cache entry"); + free(pDstData); + return CHANNEL_RC_OK; + } + if (!HashTable_Insert(clipboard->cachedData, (void*)(UINT_PTR)dstFormatId, cached_data)) + { + WLog_WARN(TAG, "Failed to cache clipboard data"); + xf_cached_data_free(cached_data); + return CHANNEL_RC_OK; + } + } + + /* We have to copy the original data again, as pSrcData is now owned + * by clipboard->system. Memory allocation failure is not fatal here + * as this is only a cached value. */ + { + // clipboard->cachedData owns cached_data + // NOLINTNEXTLINE(clang-analyzer-unix.Malloc + xfCachedData* cached_raw_data = xf_cached_data_new_copy(data, size); + if (!cached_raw_data) + WLog_WARN(TAG, "Failed to allocate cache entry"); + else + { + if (!HashTable_Insert(clipboard->cachedRawData, (void*)(UINT_PTR)srcFormatId, + cached_raw_data)) + { + WLog_WARN(TAG, "Failed to cache clipboard data"); + xf_cached_data_free(cached_raw_data); + } + } + } + + // clipboard->cachedRawData owns cached_raw_data + // NOLINTNEXTLINE(clang-analyzer-unix.Malloc) + xf_cliprdr_provide_data(clipboard, clipboard->respond, pDstData, DstSize); + { + union + { + XEvent* ev; + XSelectionEvent* sev; + } conv; + + conv.sev = clipboard->respond; + + XSendEvent(xfc->display, clipboard->respond->requestor, 0, 0, conv.ev); + XFlush(xfc->display); + } + free(clipboard->respond); + clipboard->respond = NULL; + return CHANNEL_RC_OK; +} + +static BOOL xf_cliprdr_is_valid_unix_filename(LPCWSTR filename) +{ + if (!filename) + return FALSE; + + if (filename[0] == L'\0') + return FALSE; + + /* Reserved characters */ + for (const WCHAR* c = filename; *c; ++c) + { + if (*c == L'/') + return FALSE; + } + + return TRUE; +} + +xfClipboard* xf_clipboard_new(xfContext* xfc, BOOL relieveFilenameRestriction) +{ + int n = 0; + rdpChannels* channels = NULL; + xfClipboard* clipboard = NULL; + const char* selectionAtom = NULL; + xfCliprdrFormat* clientFormat = NULL; + wObject* obj = NULL; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(xfc->common.context.settings); + + if (!(clipboard = (xfClipboard*)calloc(1, sizeof(xfClipboard)))) + { + WLog_ERR(TAG, "failed to allocate xfClipboard data"); + return NULL; + } + + clipboard->file = cliprdr_file_context_new(clipboard); + if (!clipboard->file) + goto fail; + + xfc->clipboard = clipboard; + clipboard->xfc = xfc; + channels = xfc->common.context.channels; + clipboard->channels = channels; + clipboard->system = ClipboardCreate(); + clipboard->requestedFormatId = -1; + clipboard->root_window = DefaultRootWindow(xfc->display); + + selectionAtom = + freerdp_settings_get_string(xfc->common.context.settings, FreeRDP_ClipboardUseSelection); + if (!selectionAtom) + selectionAtom = "CLIPBOARD"; + + clipboard->clipboard_atom = XInternAtom(xfc->display, selectionAtom, FALSE); + + if (clipboard->clipboard_atom == None) + { + WLog_ERR(TAG, "unable to get %s atom", selectionAtom); + goto fail; + } + + clipboard->timestamp_property_atom = + XInternAtom(xfc->display, "_FREERDP_TIMESTAMP_PROPERTY", FALSE); + clipboard->property_atom = XInternAtom(xfc->display, "_FREERDP_CLIPRDR", FALSE); + clipboard->raw_transfer_atom = XInternAtom(xfc->display, "_FREERDP_CLIPRDR_RAW", FALSE); + clipboard->raw_format_list_atom = XInternAtom(xfc->display, "_FREERDP_CLIPRDR_FORMATS", FALSE); + xf_cliprdr_set_raw_transfer_enabled(clipboard, TRUE); + XSelectInput(xfc->display, clipboard->root_window, PropertyChangeMask); +#ifdef WITH_XFIXES + + if (XFixesQueryExtension(xfc->display, &clipboard->xfixes_event_base, + &clipboard->xfixes_error_base)) + { + int xfmajor = 0; + int xfminor = 0; + + if (XFixesQueryVersion(xfc->display, &xfmajor, &xfminor)) + { + XFixesSelectSelectionInput(xfc->display, clipboard->root_window, + clipboard->clipboard_atom, + XFixesSetSelectionOwnerNotifyMask); + clipboard->xfixes_supported = TRUE; + } + else + { + WLog_ERR(TAG, "Error querying X Fixes extension version"); + } + } + else + { + WLog_ERR(TAG, "Error loading X Fixes extension"); + } + +#else + WLog_ERR( + TAG, + "Warning: Using clipboard redirection without XFIXES extension is strongly discouraged!"); +#endif + clientFormat = &clipboard->clientFormats[n++]; + clientFormat->atom = XInternAtom(xfc->display, "_FREERDP_RAW", False); + clientFormat->localFormat = clientFormat->formatToRequest = CF_RAW; + + clientFormat = &clipboard->clientFormats[n++]; + clientFormat->atom = XInternAtom(xfc->display, "UTF8_STRING", False); + clientFormat->formatToRequest = CF_UNICODETEXT; + clientFormat->localFormat = ClipboardGetFormatId(xfc->clipboard->system, mime_text_plain); + + clientFormat = &clipboard->clientFormats[n++]; + clientFormat->atom = XA_STRING; + clientFormat->formatToRequest = CF_TEXT; + clientFormat->localFormat = ClipboardGetFormatId(xfc->clipboard->system, mime_text_plain); + + clientFormat = &clipboard->clientFormats[n++]; + clientFormat->atom = XInternAtom(xfc->display, mime_tiff, False); + clientFormat->formatToRequest = clientFormat->localFormat = CF_TIFF; + + for (size_t x = 0; x < ARRAYSIZE(mime_bitmap); x++) + { + const char* mime_bmp = mime_bitmap[x]; + const DWORD format = ClipboardGetFormatId(xfc->clipboard->system, mime_bmp); + if (format == 0) + { + WLog_DBG(TAG, "skipping local bitmap format %s [NOT SUPPORTED]", mime_bmp); + continue; + } + + WLog_DBG(TAG, "register local bitmap format %s [0x%08" PRIx32 "]", mime_bmp, format); + clientFormat = &clipboard->clientFormats[n++]; + clientFormat->localFormat = format; + clientFormat->atom = XInternAtom(xfc->display, mime_bmp, False); + clientFormat->formatToRequest = CF_DIB; + } + + for (size_t x = 0; x < ARRAYSIZE(mime_images); x++) + { + const char* mime_bmp = mime_images[x]; + const DWORD format = ClipboardGetFormatId(xfc->clipboard->system, mime_bmp); + if (format == 0) + { + WLog_DBG(TAG, "skipping local bitmap format %s [NOT SUPPORTED]", mime_bmp); + continue; + } + + WLog_DBG(TAG, "register local bitmap format %s [0x%08" PRIx32 "]", mime_bmp, format); + clientFormat = &clipboard->clientFormats[n++]; + clientFormat->localFormat = format; + clientFormat->atom = XInternAtom(xfc->display, mime_bmp, False); + clientFormat->formatToRequest = CF_DIB; + } + + clientFormat = &clipboard->clientFormats[n++]; + clientFormat->atom = XInternAtom(xfc->display, mime_html, False); + clientFormat->formatToRequest = ClipboardGetFormatId(xfc->clipboard->system, type_HtmlFormat); + clientFormat->localFormat = ClipboardGetFormatId(xfc->clipboard->system, mime_html); + clientFormat->formatName = _strdup(type_HtmlFormat); + + if (!clientFormat->formatName) + goto fail; + + clientFormat = &clipboard->clientFormats[n++]; + + /* + * Existence of registered format IDs for file formats does not guarantee that they are + * in fact supported by wClipboard (as further initialization may have failed after format + * registration). However, they are definitely not supported if there are no registered + * formats. In this case we should not list file formats in TARGETS. + */ + const UINT32 fgid = ClipboardGetFormatId(clipboard->system, type_FileGroupDescriptorW); + const UINT32 uid = ClipboardGetFormatId(clipboard->system, mime_uri_list); + if (uid) + { + cliprdr_file_context_set_locally_available(clipboard->file, TRUE); + clientFormat->atom = XInternAtom(xfc->display, mime_uri_list, False); + clientFormat->localFormat = uid; + clientFormat->formatToRequest = fgid; + clientFormat->formatName = _strdup(type_FileGroupDescriptorW); + + if (!clientFormat->formatName) + goto fail; + + clientFormat = &clipboard->clientFormats[n++]; + } + + const UINT32 gid = ClipboardGetFormatId(clipboard->system, mime_gnome_copied_files); + if (gid != 0) + { + cliprdr_file_context_set_locally_available(clipboard->file, TRUE); + clientFormat->atom = XInternAtom(xfc->display, mime_gnome_copied_files, False); + clientFormat->localFormat = gid; + clientFormat->formatToRequest = fgid; + clientFormat->formatName = _strdup(type_FileGroupDescriptorW); + + if (!clientFormat->formatName) + goto fail; + + clientFormat = &clipboard->clientFormats[n++]; + } + + const UINT32 mid = ClipboardGetFormatId(clipboard->system, mime_mate_copied_files); + if (mid != 0) + { + cliprdr_file_context_set_locally_available(clipboard->file, TRUE); + clientFormat->atom = XInternAtom(xfc->display, mime_mate_copied_files, False); + clientFormat->localFormat = mid; + clientFormat->formatToRequest = fgid; + clientFormat->formatName = _strdup(type_FileGroupDescriptorW); + + if (!clientFormat->formatName) + goto fail; + } + + clipboard->numClientFormats = n; + clipboard->targets[0] = XInternAtom(xfc->display, "TIMESTAMP", FALSE); + clipboard->targets[1] = XInternAtom(xfc->display, "TARGETS", FALSE); + clipboard->numTargets = 2; + clipboard->incr_atom = XInternAtom(xfc->display, "INCR", FALSE); + + if (relieveFilenameRestriction) + { + WLog_DBG(TAG, "Relieving CLIPRDR filename restriction"); + ClipboardGetDelegate(clipboard->system)->IsFileNameComponentValid = + xf_cliprdr_is_valid_unix_filename; + } + + clipboard->cachedData = HashTable_New(TRUE); + if (!clipboard->cachedData) + goto fail; + + obj = HashTable_ValueObject(clipboard->cachedData); + obj->fnObjectFree = xf_cached_data_free; + + clipboard->cachedRawData = HashTable_New(TRUE); + if (!clipboard->cachedRawData) + goto fail; + + obj = HashTable_ValueObject(clipboard->cachedRawData); + obj->fnObjectFree = xf_cached_data_free; + + return clipboard; + +fail: + WINPR_PRAGMA_DIAG_PUSH + WINPR_PRAGMA_DIAG_IGNORED_MISMATCHED_DEALLOC + xf_clipboard_free(clipboard); + WINPR_PRAGMA_DIAG_POP + return NULL; +} + +void xf_clipboard_free(xfClipboard* clipboard) +{ + if (!clipboard) + return; + + xf_clipboard_free_server_formats(clipboard); + + if (clipboard->numClientFormats) + { + for (UINT32 i = 0; i < clipboard->numClientFormats; i++) + { + xfCliprdrFormat* format = &clipboard->clientFormats[i]; + free(format->formatName); + } + } + + cliprdr_file_context_free(clipboard->file); + + ClipboardDestroy(clipboard->system); + xf_clipboard_formats_free(clipboard); + HashTable_Free(clipboard->cachedRawData); + HashTable_Free(clipboard->cachedData); + free(clipboard->respond); + free(clipboard->incr_data); + free(clipboard); +} + +void xf_cliprdr_init(xfContext* xfc, CliprdrClientContext* cliprdr) +{ + WINPR_ASSERT(xfc); + WINPR_ASSERT(cliprdr); + + xfc->cliprdr = cliprdr; + xfc->clipboard->context = cliprdr; + + cliprdr->MonitorReady = xf_cliprdr_monitor_ready; + cliprdr->ServerCapabilities = xf_cliprdr_server_capabilities; + cliprdr->ServerFormatList = xf_cliprdr_server_format_list; + cliprdr->ServerFormatListResponse = xf_cliprdr_server_format_list_response; + cliprdr->ServerFormatDataRequest = xf_cliprdr_server_format_data_request; + cliprdr->ServerFormatDataResponse = xf_cliprdr_server_format_data_response; + + cliprdr_file_context_init(xfc->clipboard->file, cliprdr); +} + +void xf_cliprdr_uninit(xfContext* xfc, CliprdrClientContext* cliprdr) +{ + WINPR_ASSERT(xfc); + WINPR_ASSERT(cliprdr); + + xfc->cliprdr = NULL; + + if (xfc->clipboard) + { + cliprdr_file_context_uninit(xfc->clipboard->file, cliprdr); + xfc->clipboard->context = NULL; + } +} diff --git a/client/X11/xf_cliprdr.h b/client/X11/xf_cliprdr.h new file mode 100644 index 0000000..33d75c8 --- /dev/null +++ b/client/X11/xf_cliprdr.h @@ -0,0 +1,38 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Clipboard Redirection + * + * Copyright 2010-2011 Vic Lee + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_X11_CLIPRDR_H +#define FREERDP_CLIENT_X11_CLIPRDR_H + +#include "xf_client.h" +#include "xfreerdp.h" + +#include <freerdp/client/cliprdr.h> + +void xf_clipboard_free(xfClipboard* clipboard); + +WINPR_ATTR_MALLOC(xf_clipboard_free, 1) +xfClipboard* xf_clipboard_new(xfContext* xfc, BOOL relieveFilenameRestriction); + +void xf_cliprdr_init(xfContext* xfc, CliprdrClientContext* cliprdr); +void xf_cliprdr_uninit(xfContext* xfc, CliprdrClientContext* cliprdr); + +void xf_cliprdr_handle_xevent(xfContext* xfc, const XEvent* event); + +#endif /* FREERDP_CLIENT_X11_CLIPRDR_H */ diff --git a/client/X11/xf_disp.c b/client/X11/xf_disp.c new file mode 100644 index 0000000..f7be118 --- /dev/null +++ b/client/X11/xf_disp.c @@ -0,0 +1,550 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Display Control channel + * + * Copyright 2017 David Fort <contact@hardening-consulting.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <winpr/assert.h> +#include <winpr/sysinfo.h> +#include <X11/Xutil.h> + +#ifdef WITH_XRANDR +#include <X11/extensions/Xrandr.h> +#include <X11/extensions/randr.h> + +#if (RANDR_MAJOR * 100 + RANDR_MINOR) >= 105 +#define USABLE_XRANDR +#endif + +#endif + +#include "xf_disp.h" +#include "xf_monitor.h" + +#include <freerdp/log.h> +#define TAG CLIENT_TAG("x11disp") +#define RESIZE_MIN_DELAY 200 /* minimum delay in ms between two resizes */ + +struct s_xfDispContext +{ + xfContext* xfc; + DispClientContext* disp; + BOOL haveXRandr; + int eventBase; + int errorBase; + UINT32 lastSentWidth; + UINT32 lastSentHeight; + BYTE reserved[4]; + UINT64 lastSentDate; + UINT32 targetWidth; + UINT32 targetHeight; + BOOL activated; + BOOL fullscreen; + UINT16 lastSentDesktopOrientation; + BYTE reserved2[2]; + UINT32 lastSentDesktopScaleFactor; + UINT32 lastSentDeviceScaleFactor; + BYTE reserved3[4]; +}; + +static UINT xf_disp_sendLayout(DispClientContext* disp, const rdpMonitor* monitors, + UINT32 nmonitors); + +static BOOL xf_disp_settings_changed(xfDispContext* xfDisp) +{ + rdpSettings* settings = NULL; + + WINPR_ASSERT(xfDisp); + WINPR_ASSERT(xfDisp->xfc); + + settings = xfDisp->xfc->common.context.settings; + WINPR_ASSERT(settings); + + if (xfDisp->lastSentWidth != xfDisp->targetWidth) + return TRUE; + + if (xfDisp->lastSentHeight != xfDisp->targetHeight) + return TRUE; + + if (xfDisp->lastSentDesktopOrientation != + freerdp_settings_get_uint16(settings, FreeRDP_DesktopOrientation)) + return TRUE; + + if (xfDisp->lastSentDesktopScaleFactor != + freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor)) + return TRUE; + + if (xfDisp->lastSentDeviceScaleFactor != + freerdp_settings_get_uint32(settings, FreeRDP_DeviceScaleFactor)) + return TRUE; + + if (xfDisp->fullscreen != xfDisp->xfc->fullscreen) + return TRUE; + + return FALSE; +} + +static BOOL xf_update_last_sent(xfDispContext* xfDisp) +{ + rdpSettings* settings = NULL; + + WINPR_ASSERT(xfDisp); + WINPR_ASSERT(xfDisp->xfc); + + settings = xfDisp->xfc->common.context.settings; + WINPR_ASSERT(settings); + + xfDisp->lastSentWidth = xfDisp->targetWidth; + xfDisp->lastSentHeight = xfDisp->targetHeight; + xfDisp->lastSentDesktopOrientation = + freerdp_settings_get_uint16(settings, FreeRDP_DesktopOrientation); + xfDisp->lastSentDesktopScaleFactor = + freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor); + xfDisp->lastSentDeviceScaleFactor = + freerdp_settings_get_uint32(settings, FreeRDP_DeviceScaleFactor); + xfDisp->fullscreen = xfDisp->xfc->fullscreen; + return TRUE; +} + +static BOOL xf_disp_sendResize(xfDispContext* xfDisp) +{ + DISPLAY_CONTROL_MONITOR_LAYOUT layout = { 0 }; + xfContext* xfc = NULL; + rdpSettings* settings = NULL; + + if (!xfDisp || !xfDisp->xfc) + return FALSE; + + xfc = xfDisp->xfc; + settings = xfc->common.context.settings; + + if (!settings) + return FALSE; + + if (!xfDisp->activated || !xfDisp->disp) + return TRUE; + + if (GetTickCount64() - xfDisp->lastSentDate < RESIZE_MIN_DELAY) + return TRUE; + + if (!xf_disp_settings_changed(xfDisp)) + return TRUE; + + xfDisp->lastSentDate = GetTickCount64(); + + const UINT32 mcount = freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount); + if (xfc->fullscreen && (mcount > 0)) + { + const rdpMonitor* monitors = + freerdp_settings_get_pointer(settings, FreeRDP_MonitorDefArray); + if (xf_disp_sendLayout(xfDisp->disp, monitors, mcount) != CHANNEL_RC_OK) + return FALSE; + } + else + { + layout.Flags = DISPLAY_CONTROL_MONITOR_PRIMARY; + layout.Top = layout.Left = 0; + layout.Width = xfDisp->targetWidth; + layout.Height = xfDisp->targetHeight; + layout.Orientation = freerdp_settings_get_uint16(settings, FreeRDP_DesktopOrientation); + layout.DesktopScaleFactor = + freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor); + layout.DeviceScaleFactor = freerdp_settings_get_uint32(settings, FreeRDP_DeviceScaleFactor); + layout.PhysicalWidth = xfDisp->targetWidth / 75.0 * 25.4; + layout.PhysicalHeight = xfDisp->targetHeight / 75.0 * 25.4; + + if (IFCALLRESULT(CHANNEL_RC_OK, xfDisp->disp->SendMonitorLayout, xfDisp->disp, 1, + &layout) != CHANNEL_RC_OK) + return FALSE; + } + + return xf_update_last_sent(xfDisp); +} + +static BOOL xf_disp_queueResize(xfDispContext* xfDisp, UINT32 width, UINT32 height) +{ + if ((xfDisp->targetWidth == (INT64)width) && (xfDisp->targetHeight == (INT64)height)) + return TRUE; + xfDisp->targetWidth = width; + xfDisp->targetHeight = height; + xfDisp->lastSentDate = GetTickCount64(); + return xf_disp_sendResize(xfDisp); +} + +static BOOL xf_disp_set_window_resizable(xfDispContext* xfDisp) +{ + XSizeHints* size_hints = NULL; + + if (!(size_hints = XAllocSizeHints())) + return FALSE; + + size_hints->flags = PMinSize | PMaxSize | PWinGravity; + size_hints->win_gravity = NorthWestGravity; + size_hints->min_width = size_hints->min_height = 320; + size_hints->max_width = size_hints->max_height = 8192; + + if (xfDisp->xfc->window) + XSetWMNormalHints(xfDisp->xfc->display, xfDisp->xfc->window->handle, size_hints); + + XFree(size_hints); + return TRUE; +} + +static BOOL xf_disp_check_context(void* context, xfContext** ppXfc, xfDispContext** ppXfDisp, + rdpSettings** ppSettings) +{ + xfContext* xfc = NULL; + + if (!context) + return FALSE; + + xfc = (xfContext*)context; + + if (!(xfc->xfDisp)) + return FALSE; + + if (!xfc->common.context.settings) + return FALSE; + + *ppXfc = xfc; + *ppXfDisp = xfc->xfDisp; + *ppSettings = xfc->common.context.settings; + return TRUE; +} + +static void xf_disp_OnActivated(void* context, const ActivatedEventArgs* e) +{ + xfContext* xfc = NULL; + xfDispContext* xfDisp = NULL; + rdpSettings* settings = NULL; + + if (!xf_disp_check_context(context, &xfc, &xfDisp, &settings)) + return; + + if (xfDisp->activated && !xfc->fullscreen) + { + xf_disp_set_window_resizable(xfDisp); + + if (e->firstActivation) + return; + + xf_disp_sendResize(xfDisp); + } +} + +static void xf_disp_OnGraphicsReset(void* context, const GraphicsResetEventArgs* e) +{ + xfContext* xfc = NULL; + xfDispContext* xfDisp = NULL; + rdpSettings* settings = NULL; + + WINPR_UNUSED(e); + + if (!xf_disp_check_context(context, &xfc, &xfDisp, &settings)) + return; + + if (xfDisp->activated && !freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)) + { + xf_disp_set_window_resizable(xfDisp); + xf_disp_sendResize(xfDisp); + } +} + +static void xf_disp_OnTimer(void* context, const TimerEventArgs* e) +{ + xfContext* xfc = NULL; + xfDispContext* xfDisp = NULL; + rdpSettings* settings = NULL; + + WINPR_UNUSED(e); + + if (!xf_disp_check_context(context, &xfc, &xfDisp, &settings)) + return; + + if (!xfDisp->activated || xfc->fullscreen) + return; + + xf_disp_sendResize(xfDisp); +} + +static void xf_disp_OnWindowStateChange(void* context, const WindowStateChangeEventArgs* e) +{ + xfContext* xfc = NULL; + xfDispContext* xfDisp = NULL; + rdpSettings* settings = NULL; + + WINPR_UNUSED(e); + + if (!xf_disp_check_context(context, &xfc, &xfDisp, &settings)) + return; + + if (!xfDisp->activated || !xfc->fullscreen) + return; + + xf_disp_sendResize(xfDisp); +} + +xfDispContext* xf_disp_new(xfContext* xfc) +{ + xfDispContext* ret = NULL; + const rdpSettings* settings = NULL; + wPubSub* pubSub = NULL; + + WINPR_ASSERT(xfc); + + pubSub = xfc->common.context.pubSub; + WINPR_ASSERT(pubSub); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + ret = calloc(1, sizeof(xfDispContext)); + + if (!ret) + return NULL; + + ret->xfc = xfc; +#ifdef USABLE_XRANDR + + if (XRRQueryExtension(xfc->display, &ret->eventBase, &ret->errorBase)) + { + ret->haveXRandr = TRUE; + } + +#endif + ret->lastSentWidth = ret->targetWidth = + freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth); + ret->lastSentHeight = ret->targetHeight = + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight); + PubSub_SubscribeActivated(pubSub, xf_disp_OnActivated); + PubSub_SubscribeGraphicsReset(pubSub, xf_disp_OnGraphicsReset); + PubSub_SubscribeTimer(pubSub, xf_disp_OnTimer); + PubSub_SubscribeWindowStateChange(pubSub, xf_disp_OnWindowStateChange); + return ret; +} + +void xf_disp_free(xfDispContext* disp) +{ + if (!disp) + return; + + if (disp->xfc) + { + wPubSub* pubSub = disp->xfc->common.context.pubSub; + PubSub_UnsubscribeActivated(pubSub, xf_disp_OnActivated); + PubSub_UnsubscribeGraphicsReset(pubSub, xf_disp_OnGraphicsReset); + PubSub_UnsubscribeTimer(pubSub, xf_disp_OnTimer); + PubSub_UnsubscribeWindowStateChange(pubSub, xf_disp_OnWindowStateChange); + } + + free(disp); +} + +UINT xf_disp_sendLayout(DispClientContext* disp, const rdpMonitor* monitors, UINT32 nmonitors) +{ + UINT ret = CHANNEL_RC_OK; + xfDispContext* xfDisp = NULL; + rdpSettings* settings = NULL; + DISPLAY_CONTROL_MONITOR_LAYOUT* layouts = NULL; + + WINPR_ASSERT(disp); + WINPR_ASSERT(monitors); + WINPR_ASSERT(nmonitors > 0); + + xfDisp = (xfDispContext*)disp->custom; + WINPR_ASSERT(xfDisp); + WINPR_ASSERT(xfDisp->xfc); + + settings = xfDisp->xfc->common.context.settings; + WINPR_ASSERT(settings); + + layouts = calloc(nmonitors, sizeof(DISPLAY_CONTROL_MONITOR_LAYOUT)); + + if (!layouts) + return CHANNEL_RC_NO_MEMORY; + + for (UINT32 i = 0; i < nmonitors; i++) + { + const rdpMonitor* monitor = &monitors[i]; + DISPLAY_CONTROL_MONITOR_LAYOUT* layout = &layouts[i]; + + layout->Flags = (monitor->is_primary ? DISPLAY_CONTROL_MONITOR_PRIMARY : 0); + layout->Left = monitor->x; + layout->Top = monitor->y; + layout->Width = monitor->width; + layout->Height = monitor->height; + layout->Orientation = ORIENTATION_LANDSCAPE; + layout->PhysicalWidth = monitor->attributes.physicalWidth; + layout->PhysicalHeight = monitor->attributes.physicalHeight; + + switch (monitor->attributes.orientation) + { + case 90: + layout->Orientation = ORIENTATION_PORTRAIT; + break; + + case 180: + layout->Orientation = ORIENTATION_LANDSCAPE_FLIPPED; + break; + + case 270: + layout->Orientation = ORIENTATION_PORTRAIT_FLIPPED; + break; + + case 0: + default: + /* MS-RDPEDISP - 2.2.2.2.1: + * Orientation (4 bytes): A 32-bit unsigned integer that specifies the + * orientation of the monitor in degrees. Valid values are 0, 90, 180 + * or 270 + * + * So we default to ORIENTATION_LANDSCAPE + */ + layout->Orientation = ORIENTATION_LANDSCAPE; + break; + } + + layout->DesktopScaleFactor = + freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor); + layout->DeviceScaleFactor = + freerdp_settings_get_uint32(settings, FreeRDP_DeviceScaleFactor); + } + + ret = IFCALLRESULT(CHANNEL_RC_OK, disp->SendMonitorLayout, disp, nmonitors, layouts); + free(layouts); + return ret; +} + +BOOL xf_disp_handle_xevent(xfContext* xfc, const XEvent* event) +{ + xfDispContext* xfDisp = NULL; + rdpSettings* settings = NULL; + UINT32 maxWidth = 0; + UINT32 maxHeight = 0; + + if (!xfc || !event) + return FALSE; + + xfDisp = xfc->xfDisp; + + if (!xfDisp) + return FALSE; + + settings = xfc->common.context.settings; + + if (!settings) + return FALSE; + + if (!xfDisp->haveXRandr || !xfDisp->disp) + return TRUE; + +#ifdef USABLE_XRANDR + + if (event->type != xfDisp->eventBase + RRScreenChangeNotify) + return TRUE; + +#endif + xf_detect_monitors(xfc, &maxWidth, &maxHeight); + const rdpMonitor* monitors = freerdp_settings_get_pointer(settings, FreeRDP_MonitorDefArray); + const UINT32 mcount = freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount); + return xf_disp_sendLayout(xfDisp->disp, monitors, mcount) == CHANNEL_RC_OK; +} + +BOOL xf_disp_handle_configureNotify(xfContext* xfc, int width, int height) +{ + xfDispContext* xfDisp = NULL; + + if (!xfc) + return FALSE; + + xfDisp = xfc->xfDisp; + + if (!xfDisp) + return FALSE; + + return xf_disp_queueResize(xfDisp, width, height); +} + +static UINT xf_DisplayControlCaps(DispClientContext* disp, UINT32 maxNumMonitors, + UINT32 maxMonitorAreaFactorA, UINT32 maxMonitorAreaFactorB) +{ + /* we're called only if dynamic resolution update is activated */ + xfDispContext* xfDisp = NULL; + rdpSettings* settings = NULL; + + WINPR_ASSERT(disp); + + xfDisp = (xfDispContext*)disp->custom; + WINPR_ASSERT(xfDisp); + WINPR_ASSERT(xfDisp->xfc); + + settings = xfDisp->xfc->common.context.settings; + WINPR_ASSERT(settings); + + WLog_DBG(TAG, + "DisplayControlCapsPdu: MaxNumMonitors: %" PRIu32 " MaxMonitorAreaFactorA: %" PRIu32 + " MaxMonitorAreaFactorB: %" PRIu32 "", + maxNumMonitors, maxMonitorAreaFactorA, maxMonitorAreaFactorB); + xfDisp->activated = TRUE; + + if (freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)) + return CHANNEL_RC_OK; + + WLog_DBG(TAG, "DisplayControlCapsPdu: setting the window as resizable"); + return xf_disp_set_window_resizable(xfDisp) ? CHANNEL_RC_OK : CHANNEL_RC_NO_MEMORY; +} + +BOOL xf_disp_init(xfDispContext* xfDisp, DispClientContext* disp) +{ + rdpSettings* settings = NULL; + + if (!xfDisp || !xfDisp->xfc || !disp) + return FALSE; + + settings = xfDisp->xfc->common.context.settings; + + if (!settings) + return FALSE; + + xfDisp->disp = disp; + disp->custom = (void*)xfDisp; + + if (freerdp_settings_get_bool(settings, FreeRDP_DynamicResolutionUpdate)) + { + disp->DisplayControlCaps = xf_DisplayControlCaps; +#ifdef USABLE_XRANDR + + if (freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)) + { + /* ask X11 to notify us of screen changes */ + XRRSelectInput(xfDisp->xfc->display, DefaultRootWindow(xfDisp->xfc->display), + RRScreenChangeNotifyMask); + } + +#endif + } + + return TRUE; +} + +BOOL xf_disp_uninit(xfDispContext* xfDisp, DispClientContext* disp) +{ + if (!xfDisp || !disp) + return FALSE; + + xfDisp->disp = NULL; + return TRUE; +} diff --git a/client/X11/xf_disp.h b/client/X11/xf_disp.h new file mode 100644 index 0000000..c3c8792 --- /dev/null +++ b/client/X11/xf_disp.h @@ -0,0 +1,40 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Display Control channel + * + * Copyright 2017 David Fort <contact@hardening-consulting.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef FREERDP_CLIENT_X11_DISP_H +#define FREERDP_CLIENT_X11_DISP_H + +#include <freerdp/types.h> +#include <freerdp/client/disp.h> + +#include "xf_client.h" +#include "xfreerdp.h" + +FREERDP_API BOOL xf_disp_init(xfDispContext* xfDisp, DispClientContext* disp); +FREERDP_API BOOL xf_disp_uninit(xfDispContext* xfDisp, DispClientContext* disp); + +void xf_disp_free(xfDispContext* disp); + +WINPR_ATTR_MALLOC(xf_disp_free, 1) +xfDispContext* xf_disp_new(xfContext* xfc); + +BOOL xf_disp_handle_xevent(xfContext* xfc, const XEvent* event); +BOOL xf_disp_handle_configureNotify(xfContext* xfc, int width, int height); +void xf_disp_resized(xfDispContext* disp); + +#endif /* FREERDP_CLIENT_X11_DISP_H */ diff --git a/client/X11/xf_event.c b/client/X11/xf_event.c new file mode 100644 index 0000000..6bc4c4d --- /dev/null +++ b/client/X11/xf_event.c @@ -0,0 +1,1314 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Event Handling + * + * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * Copyright 2023 HP Development Company, L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <X11/Xlib.h> +#include <X11/Xutil.h> + +#include <winpr/assert.h> + +#include <freerdp/log.h> +#include <freerdp/locale/keyboard.h> + +#include "xf_rail.h" +#include "xf_window.h" +#include "xf_cliprdr.h" +#include "xf_disp.h" +#include "xf_input.h" +#include "xf_gfx.h" +#include "xf_graphics.h" + +#include "xf_event.h" + +#define TAG CLIENT_TAG("x11") + +#define CLAMP_COORDINATES(x, y) \ + if (x < 0) \ + x = 0; \ + if (y < 0) \ + y = 0 + +const char* x11_event_string(int event) +{ + switch (event) + { + case KeyPress: + return "KeyPress"; + + case KeyRelease: + return "KeyRelease"; + + case ButtonPress: + return "ButtonPress"; + + case ButtonRelease: + return "ButtonRelease"; + + case MotionNotify: + return "MotionNotify"; + + case EnterNotify: + return "EnterNotify"; + + case LeaveNotify: + return "LeaveNotify"; + + case FocusIn: + return "FocusIn"; + + case FocusOut: + return "FocusOut"; + + case KeymapNotify: + return "KeymapNotify"; + + case Expose: + return "Expose"; + + case GraphicsExpose: + return "GraphicsExpose"; + + case NoExpose: + return "NoExpose"; + + case VisibilityNotify: + return "VisibilityNotify"; + + case CreateNotify: + return "CreateNotify"; + + case DestroyNotify: + return "DestroyNotify"; + + case UnmapNotify: + return "UnmapNotify"; + + case MapNotify: + return "MapNotify"; + + case MapRequest: + return "MapRequest"; + + case ReparentNotify: + return "ReparentNotify"; + + case ConfigureNotify: + return "ConfigureNotify"; + + case ConfigureRequest: + return "ConfigureRequest"; + + case GravityNotify: + return "GravityNotify"; + + case ResizeRequest: + return "ResizeRequest"; + + case CirculateNotify: + return "CirculateNotify"; + + case CirculateRequest: + return "CirculateRequest"; + + case PropertyNotify: + return "PropertyNotify"; + + case SelectionClear: + return "SelectionClear"; + + case SelectionRequest: + return "SelectionRequest"; + + case SelectionNotify: + return "SelectionNotify"; + + case ColormapNotify: + return "ColormapNotify"; + + case ClientMessage: + return "ClientMessage"; + + case MappingNotify: + return "MappingNotify"; + + case GenericEvent: + return "GenericEvent"; + + default: + return "UNKNOWN"; + } +} + +#ifdef WITH_DEBUG_X11 +#define DEBUG_X11(...) WLog_DBG(TAG, __VA_ARGS__) +#else +#define DEBUG_X11(...) \ + do \ + { \ + } while (0) +#endif + +BOOL xf_event_action_script_init(xfContext* xfc) +{ + wObject* obj = NULL; + FILE* actionScript = NULL; + char buffer[1024] = { 0 }; + char command[1024] = { 0 }; + const rdpSettings* settings = NULL; + const char* ActionScript = NULL; + + WINPR_ASSERT(xfc); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + xfc->xevents = ArrayList_New(TRUE); + + if (!xfc->xevents) + return FALSE; + + obj = ArrayList_Object(xfc->xevents); + WINPR_ASSERT(obj); + obj->fnObjectNew = winpr_ObjectStringClone; + obj->fnObjectFree = winpr_ObjectStringFree; + ActionScript = freerdp_settings_get_string(settings, FreeRDP_ActionScript); + sprintf_s(command, sizeof(command), "%s xevent", ActionScript); + actionScript = popen(command, "r"); + + if (!actionScript) + return FALSE; + + while (fgets(buffer, sizeof(buffer), actionScript)) + { + char* context = NULL; + strtok_s(buffer, "\n", &context); + + if (!buffer || !ArrayList_Append(xfc->xevents, buffer)) + { + pclose(actionScript); + ArrayList_Free(xfc->xevents); + xfc->xevents = NULL; + return FALSE; + } + } + + pclose(actionScript); + return TRUE; +} + +void xf_event_action_script_free(xfContext* xfc) +{ + if (xfc->xevents) + { + ArrayList_Free(xfc->xevents); + xfc->xevents = NULL; + } +} + +static BOOL xf_event_execute_action_script(xfContext* xfc, const XEvent* event) +{ + int count = 0; + char* name = NULL; + FILE* actionScript = NULL; + BOOL match = FALSE; + const char* xeventName = NULL; + const char* ActionScript = NULL; + char buffer[1024] = { 0 }; + char command[1024] = { 0 }; + + if (!xfc->actionScriptExists || !xfc->xevents || !xfc->window) + return FALSE; + + if (event->type > LASTEvent) + return FALSE; + + xeventName = x11_event_string(event->type); + count = ArrayList_Count(xfc->xevents); + + for (int index = 0; index < count; index++) + { + name = (char*)ArrayList_GetItem(xfc->xevents, index); + + if (_stricmp(name, xeventName) == 0) + { + match = TRUE; + break; + } + } + + if (!match) + return FALSE; + + ActionScript = freerdp_settings_get_string(xfc->common.context.settings, FreeRDP_ActionScript); + sprintf_s(command, sizeof(command), "%s xevent %s %lu", ActionScript, xeventName, + (unsigned long)xfc->window->handle); + actionScript = popen(command, "r"); + + if (!actionScript) + return FALSE; + + while (fgets(buffer, sizeof(buffer), actionScript)) + { + char* context = NULL; + strtok_s(buffer, "\n", &context); + } + + pclose(actionScript); + return TRUE; +} + +void xf_adjust_coordinates_to_screen(xfContext* xfc, UINT32* x, UINT32* y) +{ + rdpSettings* settings = NULL; + INT64 tx = 0; + INT64 ty = 0; + + if (!xfc || !xfc->common.context.settings || !y || !x) + return; + + settings = xfc->common.context.settings; + tx = *x; + ty = *y; + if (!xfc->remote_app) + { +#ifdef WITH_XRENDER + + if (xf_picture_transform_required(xfc)) + { + double xScalingFactor = xfc->scaledWidth / (double)freerdp_settings_get_uint32( + settings, FreeRDP_DesktopWidth); + double yScalingFactor = xfc->scaledHeight / (double)freerdp_settings_get_uint32( + settings, FreeRDP_DesktopHeight); + tx = ((tx + xfc->offset_x) * xScalingFactor); + ty = ((ty + xfc->offset_y) * yScalingFactor); + } + +#endif + } + + CLAMP_COORDINATES(tx, ty); + *x = tx; + *y = ty; +} + +void xf_event_adjust_coordinates(xfContext* xfc, int* x, int* y) +{ + if (!xfc || !xfc->common.context.settings || !y || !x) + return; + + if (!xfc->remote_app) + { +#ifdef WITH_XRENDER + rdpSettings* settings = xfc->common.context.settings; + if (xf_picture_transform_required(xfc)) + { + double xScalingFactor = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth) / + (double)xfc->scaledWidth; + double yScalingFactor = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight) / + (double)xfc->scaledHeight; + *x = (int)((*x - xfc->offset_x) * xScalingFactor); + *y = (int)((*y - xfc->offset_y) * yScalingFactor); + } + +#endif + } + + CLAMP_COORDINATES(*x, *y); +} + +static BOOL xf_event_Expose(xfContext* xfc, const XExposeEvent* event, BOOL app) +{ + int x = 0; + int y = 0; + int w = 0; + int h = 0; + rdpSettings* settings = NULL; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(event); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + if (!app && (freerdp_settings_get_bool(settings, FreeRDP_SmartSizing) || + freerdp_settings_get_bool(settings, FreeRDP_MultiTouchGestures))) + { + x = 0; + y = 0; + w = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth); + h = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight); + } + else + { + x = event->x; + y = event->y; + w = event->width; + h = event->height; + } + + if (!app) + { + if (xfc->common.context.gdi->gfx) + { + xf_OutputExpose(xfc, x, y, w, h); + return TRUE; + } + xf_draw_screen(xfc, x, y, w, h); + } + else + { + xfAppWindow* appWindow = xf_AppWindowFromX11Window(xfc, event->window); + if (appWindow) + { + xf_UpdateWindowArea(xfc, appWindow, x, y, w, h); + } + } + + return TRUE; +} + +static BOOL xf_event_VisibilityNotify(xfContext* xfc, const XVisibilityEvent* event, BOOL app) +{ + WINPR_UNUSED(app); + xfc->unobscured = event->state == VisibilityUnobscured; + return TRUE; +} + +BOOL xf_generic_MotionNotify(xfContext* xfc, int x, int y, int state, Window window, BOOL app) +{ + Window childWindow = None; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(xfc->common.context.settings); + + rdpInput* input = xfc->common.context.input; + WINPR_ASSERT(input); + + if (!freerdp_settings_get_bool(xfc->common.context.settings, FreeRDP_MouseMotion)) + { + if ((state & (Button1Mask | Button2Mask | Button3Mask)) == 0) + return TRUE; + } + + if (app) + { + /* make sure window exists */ + if (!xf_AppWindowFromX11Window(xfc, window)) + return TRUE; + + /* Translate to desktop coordinates */ + XTranslateCoordinates(xfc->display, window, RootWindowOfScreen(xfc->screen), x, y, &x, &y, + &childWindow); + } + + xf_event_adjust_coordinates(xfc, &x, &y); + freerdp_client_send_button_event(&xfc->common, FALSE, PTR_FLAGS_MOVE, x, y); + + if (xfc->fullscreen && !app) + { + XSetInputFocus(xfc->display, xfc->window->handle, RevertToPointerRoot, CurrentTime); + } + + return TRUE; +} + +BOOL xf_generic_RawMotionNotify(xfContext* xfc, int x, int y, Window window, BOOL app) +{ + WINPR_ASSERT(xfc); + + if (app) + { + WLog_ERR(TAG, "Relative mouse input is not supported with remoate app mode!"); + return FALSE; + } + + return freerdp_client_send_button_event(&xfc->common, TRUE, PTR_FLAGS_MOVE, x, y); +} + +static BOOL xf_event_MotionNotify(xfContext* xfc, const XMotionEvent* event, BOOL app) +{ + WINPR_ASSERT(xfc); + + if (xfc->xi_event) + return TRUE; + + if (xfc->window) + xf_floatbar_set_root_y(xfc->window->floatbar, event->y); + + return xf_generic_MotionNotify(xfc, event->x, event->y, event->state, event->window, app); +} + +BOOL xf_generic_ButtonEvent(xfContext* xfc, int x, int y, int button, Window window, BOOL app, + BOOL down) +{ + UINT16 flags = 0; + Window childWindow = None; + + WINPR_ASSERT(xfc); + + for (size_t i = 0; i < ARRAYSIZE(xfc->button_map); i++) + { + const button_map* cur = &xfc->button_map[i]; + + if (cur->button == button) + { + flags = cur->flags; + break; + } + } + + if (flags != 0) + { + if (flags & (PTR_FLAGS_WHEEL | PTR_FLAGS_HWHEEL)) + { + if (down) + freerdp_client_send_wheel_event(&xfc->common, flags); + } + else + { + BOOL extended = FALSE; + + if (flags & (PTR_XFLAGS_BUTTON1 | PTR_XFLAGS_BUTTON2)) + { + extended = TRUE; + + if (down) + flags |= PTR_XFLAGS_DOWN; + } + else if (flags & (PTR_FLAGS_BUTTON1 | PTR_FLAGS_BUTTON2 | PTR_FLAGS_BUTTON3)) + { + if (down) + flags |= PTR_FLAGS_DOWN; + } + + if (app) + { + /* make sure window exists */ + if (!xf_AppWindowFromX11Window(xfc, window)) + return TRUE; + + /* Translate to desktop coordinates */ + XTranslateCoordinates(xfc->display, window, RootWindowOfScreen(xfc->screen), x, y, + &x, &y, &childWindow); + } + + xf_event_adjust_coordinates(xfc, &x, &y); + + if (extended) + freerdp_client_send_extended_button_event(&xfc->common, FALSE, flags, x, y); + else + freerdp_client_send_button_event(&xfc->common, FALSE, flags, x, y); + } + } + + return TRUE; +} + +static BOOL xf_grab_mouse(xfContext* xfc) +{ + WINPR_ASSERT(xfc); + + if (!xfc->window) + return FALSE; + + if (freerdp_settings_get_bool(xfc->common.context.settings, FreeRDP_GrabMouse)) + { + XGrabPointer(xfc->display, xfc->window->handle, False, + ButtonPressMask | ButtonReleaseMask | PointerMotionMask | FocusChangeMask | + EnterWindowMask | LeaveWindowMask, + GrabModeAsync, GrabModeAsync, xfc->window->handle, None, CurrentTime); + xfc->common.mouse_grabbed = TRUE; + } + return TRUE; +} + +static BOOL xf_grab_kbd(xfContext* xfc) +{ + WINPR_ASSERT(xfc); + + if (!xfc->window) + return FALSE; + + XGrabKeyboard(xfc->display, xfc->window->handle, TRUE, GrabModeAsync, GrabModeAsync, + CurrentTime); + return TRUE; +} + +static BOOL xf_event_ButtonPress(xfContext* xfc, const XButtonEvent* event, BOOL app) +{ + xf_grab_mouse(xfc); + + if (xfc->xi_event) + return TRUE; + return xf_generic_ButtonEvent(xfc, event->x, event->y, event->button, event->window, app, TRUE); +} + +static BOOL xf_event_ButtonRelease(xfContext* xfc, const XButtonEvent* event, BOOL app) +{ + xf_grab_mouse(xfc); + + if (xfc->xi_event) + return TRUE; + return xf_generic_ButtonEvent(xfc, event->x, event->y, event->button, event->window, app, + FALSE); +} + +static BOOL xf_event_KeyPress(xfContext* xfc, const XKeyEvent* event, BOOL app) +{ + KeySym keysym = 0; + char str[256] = { 0 }; + union + { + const XKeyEvent* cev; + XKeyEvent* ev; + } cnv; + cnv.cev = event; + WINPR_UNUSED(app); + XLookupString(cnv.ev, str, sizeof(str), &keysym, NULL); + xf_keyboard_key_press(xfc, event, keysym); + return TRUE; +} + +static BOOL xf_event_KeyRelease(xfContext* xfc, const XKeyEvent* event, BOOL app) +{ + KeySym keysym = 0; + char str[256] = { 0 }; + union + { + const XKeyEvent* cev; + XKeyEvent* ev; + } cnv; + cnv.cev = event; + + WINPR_UNUSED(app); + XLookupString(cnv.ev, str, sizeof(str), &keysym, NULL); + xf_keyboard_key_release(xfc, event, keysym); + return TRUE; +} + +/* Release a key, but ignore the event in case of autorepeat. + */ +static BOOL xf_event_KeyReleaseOrIgnore(xfContext* xfc, const XKeyEvent* event, BOOL app) +{ + WINPR_ASSERT(xfc); + WINPR_ASSERT(event); + + if ((event->type == KeyRelease) && XEventsQueued(xfc->display, QueuedAfterReading)) + { + XEvent nev = { 0 }; + XPeekEvent(xfc->display, &nev); + + if ((nev.type == KeyPress) && (nev.xkey.time == event->time) && + (nev.xkey.keycode == event->keycode)) + { + /* Key wasn’t actually released */ + return TRUE; + } + } + + return xf_event_KeyRelease(xfc, event, app); +} + +static BOOL xf_event_FocusIn(xfContext* xfc, const XFocusInEvent* event, BOOL app) +{ + if (event->mode == NotifyGrab) + return TRUE; + + xfc->focused = TRUE; + + if (xfc->mouse_active && !app) + { + xf_grab_mouse(xfc); + if (!xf_grab_kbd(xfc)) + return FALSE; + } + + /* Release all keys, should already be done at FocusOut but might be missed + * if the WM decided to use an alternate event order */ + if (!app) + xf_keyboard_release_all_keypress(xfc); + + xf_pointer_update_scale(xfc); + + if (app) + { + xfAppWindow* appWindow = xf_AppWindowFromX11Window(xfc, event->window); + + /* Update the server with any window changes that occurred while the window was not focused. + */ + if (appWindow) + xf_rail_adjust_position(xfc, appWindow); + } + + xf_keyboard_focus_in(xfc); + return TRUE; +} + +static BOOL xf_event_FocusOut(xfContext* xfc, const XFocusOutEvent* event, BOOL app) +{ + if (event->mode == NotifyUngrab) + return TRUE; + + xfc->focused = FALSE; + + if (event->mode == NotifyWhileGrabbed) + XUngrabKeyboard(xfc->display, CurrentTime); + + xf_keyboard_release_all_keypress(xfc); + + return TRUE; +} + +static BOOL xf_event_MappingNotify(xfContext* xfc, const XMappingEvent* event, BOOL app) +{ + WINPR_UNUSED(app); + + if (event->request == MappingModifier) + return xf_keyboard_update_modifier_map(xfc); + + return TRUE; +} + +static BOOL xf_event_ClientMessage(xfContext* xfc, const XClientMessageEvent* event, BOOL app) +{ + if ((event->message_type == xfc->WM_PROTOCOLS) && + ((Atom)event->data.l[0] == xfc->WM_DELETE_WINDOW)) + { + if (app) + { + xfAppWindow* appWindow = xf_AppWindowFromX11Window(xfc, event->window); + + if (appWindow) + xf_rail_send_client_system_command(xfc, appWindow->windowId, SC_CLOSE); + + return TRUE; + } + else + { + DEBUG_X11("Main window closed"); + return FALSE; + } + } + + return TRUE; +} + +static BOOL xf_event_EnterNotify(xfContext* xfc, const XEnterWindowEvent* event, BOOL app) +{ + if (!app) + { + if (!xfc->window) + return FALSE; + + xfc->mouse_active = TRUE; + + if (xfc->fullscreen) + XSetInputFocus(xfc->display, xfc->window->handle, RevertToPointerRoot, CurrentTime); + + if (xfc->focused) + xf_grab_kbd(xfc); + } + else + { + xfAppWindow* appWindow = xf_AppWindowFromX11Window(xfc, event->window); + + /* keep track of which window has focus so that we can apply pointer updates */ + xfc->appWindow = appWindow; + } + + return TRUE; +} + +static BOOL xf_event_LeaveNotify(xfContext* xfc, const XLeaveWindowEvent* event, BOOL app) +{ + if (event->mode == NotifyGrab || event->mode == NotifyUngrab) + return TRUE; + if (!app) + { + xfc->mouse_active = FALSE; + XUngrabKeyboard(xfc->display, CurrentTime); + } + else + { + xfAppWindow* appWindow = xf_AppWindowFromX11Window(xfc, event->window); + + /* keep track of which window has focus so that we can apply pointer updates */ + if (xfc->appWindow == appWindow) + xfc->appWindow = NULL; + } + return TRUE; +} + +static BOOL xf_event_ConfigureNotify(xfContext* xfc, const XConfigureEvent* event, BOOL app) +{ + Window childWindow = None; + xfAppWindow* appWindow = NULL; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(event); + + const rdpSettings* settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + WLog_DBG(TAG, "x=%" PRId32 ", y=%" PRId32 ", w=%" PRId32 ", h=%" PRId32, event->x, event->y, + event->width, event->height); + + if (!app) + { + if (!xfc->window) + return FALSE; + + if (xfc->window->left != event->x) + xfc->window->left = event->x; + + if (xfc->window->top != event->y) + xfc->window->top = event->y; + + if (xfc->window->width != event->width || xfc->window->height != event->height) + { + xfc->window->width = event->width; + xfc->window->height = event->height; +#ifdef WITH_XRENDER + xfc->offset_x = 0; + xfc->offset_y = 0; + + if (freerdp_settings_get_bool(settings, FreeRDP_SmartSizing) || + freerdp_settings_get_bool(settings, FreeRDP_MultiTouchGestures)) + { + xfc->scaledWidth = xfc->window->width; + xfc->scaledHeight = xfc->window->height; + xf_draw_screen(xfc, 0, 0, + freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth), + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight)); + } + else + { + xfc->scaledWidth = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth); + xfc->scaledHeight = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight); + } + +#endif + } + + if (freerdp_settings_get_bool(settings, FreeRDP_DynamicResolutionUpdate)) + { + int alignedWidth = 0; + int alignedHeight = 0; + alignedWidth = (xfc->window->width / 2) * 2; + alignedHeight = (xfc->window->height / 2) * 2; + /* ask the server to resize using the display channel */ + xf_disp_handle_configureNotify(xfc, alignedWidth, alignedHeight); + } + } + else + { + appWindow = xf_AppWindowFromX11Window(xfc, event->window); + + if (appWindow) + { + /* + * ConfigureNotify coordinates are expressed relative to the window parent. + * Translate these to root window coordinates. + */ + XTranslateCoordinates(xfc->display, appWindow->handle, RootWindowOfScreen(xfc->screen), + 0, 0, &appWindow->x, &appWindow->y, &childWindow); + appWindow->width = event->width; + appWindow->height = event->height; + + xf_AppWindowResize(xfc, appWindow); + + /* + * Additional checks for not in a local move and not ignoring configure to send + * position update to server, also should the window not be focused then do not + * send to server yet (i.e. resizing using window decoration). + * The server will be updated when the window gets refocused. + */ + if (appWindow->decorations) + { + /* moving resizing using window decoration */ + xf_rail_adjust_position(xfc, appWindow); + } + else + { + if ((!event->send_event || appWindow->local_move.state == LMS_NOT_ACTIVE) && + !appWindow->rail_ignore_configure && xfc->focused) + xf_rail_adjust_position(xfc, appWindow); + } + } + } + return xf_pointer_update_scale(xfc); +} + +static BOOL xf_event_MapNotify(xfContext* xfc, const XMapEvent* event, BOOL app) +{ + WINPR_ASSERT(xfc); + if (!app) + gdi_send_suppress_output(xfc->common.context.gdi, FALSE); + else + { + xfAppWindow* appWindow = xf_AppWindowFromX11Window(xfc, event->window); + + if (appWindow) + { + /* local restore event */ + /* This is now handled as part of the PropertyNotify + * Doing this here would inhibit the ability to restore a maximized window + * that is minimized back to the maximized state + */ + // xf_rail_send_client_system_command(xfc, appWindow->windowId, SC_RESTORE); + appWindow->is_mapped = TRUE; + } + } + + return TRUE; +} + +static BOOL xf_event_UnmapNotify(xfContext* xfc, const XUnmapEvent* event, BOOL app) +{ + WINPR_ASSERT(xfc); + WINPR_ASSERT(event); + + if (!app) + xf_keyboard_release_all_keypress(xfc); + + if (!app) + gdi_send_suppress_output(xfc->common.context.gdi, TRUE); + else + { + xfAppWindow* appWindow = xf_AppWindowFromX11Window(xfc, event->window); + + if (appWindow) + appWindow->is_mapped = FALSE; + } + + return TRUE; +} + +static BOOL xf_event_PropertyNotify(xfContext* xfc, const XPropertyEvent* event, BOOL app) +{ + WINPR_ASSERT(xfc); + WINPR_ASSERT(event); + + /* + * This section handles sending the appropriate commands to the rail server + * when the window has been minimized, maximized, restored locally + * ie. not using the buttons on the rail window itself + */ + if ((((Atom)event->atom == xfc->_NET_WM_STATE) && (event->state != PropertyDelete)) || + (((Atom)event->atom == xfc->WM_STATE) && (event->state != PropertyDelete))) + { + BOOL status = FALSE; + BOOL minimized = FALSE; + BOOL minimizedChanged = FALSE; + unsigned long nitems = 0; + unsigned long bytes = 0; + unsigned char* prop = NULL; + xfAppWindow* appWindow = NULL; + + if (app) + { + appWindow = xf_AppWindowFromX11Window(xfc, event->window); + + if (!appWindow) + return TRUE; + } + + if ((Atom)event->atom == xfc->_NET_WM_STATE) + { + status = xf_GetWindowProperty(xfc, event->window, xfc->_NET_WM_STATE, 12, &nitems, + &bytes, &prop); + + if (status) + { + if (appWindow) + { + appWindow->maxVert = FALSE; + appWindow->maxHorz = FALSE; + } + for (unsigned long i = 0; i < nitems; i++) + { + if ((Atom)((UINT16**)prop)[i] == + XInternAtom(xfc->display, "_NET_WM_STATE_MAXIMIZED_VERT", False)) + { + if (appWindow) + appWindow->maxVert = TRUE; + } + + if ((Atom)((UINT16**)prop)[i] == + XInternAtom(xfc->display, "_NET_WM_STATE_MAXIMIZED_HORZ", False)) + { + if (appWindow) + appWindow->maxHorz = TRUE; + } + } + + XFree(prop); + } + } + + if ((Atom)event->atom == xfc->WM_STATE) + { + status = + xf_GetWindowProperty(xfc, event->window, xfc->WM_STATE, 1, &nitems, &bytes, &prop); + + if (status) + { + /* If the window is in the iconic state */ + if (((UINT32)*prop == 3)) + { + minimized = TRUE; + if (appWindow) + appWindow->minimized = TRUE; + } + else + { + minimized = FALSE; + if (appWindow) + appWindow->minimized = FALSE; + } + + minimizedChanged = TRUE; + XFree(prop); + } + } + + if (app) + { + WINPR_ASSERT(appWindow); + if (appWindow->maxVert && appWindow->maxHorz && !appWindow->minimized) + { + if (appWindow->rail_state != WINDOW_SHOW_MAXIMIZED) + { + appWindow->rail_state = WINDOW_SHOW_MAXIMIZED; + xf_rail_send_client_system_command(xfc, appWindow->windowId, SC_MAXIMIZE); + } + } + else if (appWindow->minimized) + { + if (appWindow->rail_state != WINDOW_SHOW_MINIMIZED) + { + appWindow->rail_state = WINDOW_SHOW_MINIMIZED; + xf_rail_send_client_system_command(xfc, appWindow->windowId, SC_MINIMIZE); + } + } + else + { + if (appWindow->rail_state != WINDOW_SHOW && appWindow->rail_state != WINDOW_HIDE) + { + appWindow->rail_state = WINDOW_SHOW; + xf_rail_send_client_system_command(xfc, appWindow->windowId, SC_RESTORE); + } + } + } + else if (minimizedChanged) + gdi_send_suppress_output(xfc->common.context.gdi, minimized); + } + + return TRUE; +} + +static BOOL xf_event_suppress_events(xfContext* xfc, xfAppWindow* appWindow, const XEvent* event) +{ + if (!xfc->remote_app) + return FALSE; + + switch (appWindow->local_move.state) + { + case LMS_NOT_ACTIVE: + + /* No local move in progress, nothing to do */ + + /* Prevent Configure from happening during indeterminant state of Horz or Vert Max only + */ + if ((event->type == ConfigureNotify) && appWindow->rail_ignore_configure) + { + appWindow->rail_ignore_configure = FALSE; + return TRUE; + } + + break; + + case LMS_STARTING: + + /* Local move initiated by RDP server, but we have not yet seen any updates from the X + * server */ + switch (event->type) + { + case ConfigureNotify: + /* Starting to see move events from the X server. Local move is now in progress. + */ + appWindow->local_move.state = LMS_ACTIVE; + /* Allow these events to be processed during move to keep our state up to date. + */ + break; + + case ButtonPress: + case ButtonRelease: + case KeyPress: + case KeyRelease: + case UnmapNotify: + /* + * A button release event means the X window server did not grab the + * mouse before the user released it. In this case we must cancel the + * local move. The event will be processed below as normal, below. + */ + break; + + case VisibilityNotify: + case PropertyNotify: + case Expose: + /* Allow these events to pass */ + break; + + default: + /* Eat any other events */ + return TRUE; + } + + break; + + case LMS_ACTIVE: + + /* Local move is in progress */ + switch (event->type) + { + case ConfigureNotify: + case VisibilityNotify: + case PropertyNotify: + case Expose: + case GravityNotify: + /* Keep us up to date on position */ + break; + + default: + /* Any other event terminates move */ + xf_rail_end_local_move(xfc, appWindow); + break; + } + + break; + + case LMS_TERMINATING: + /* Already sent RDP end move to server. Allow events to pass. */ + break; + } + + return FALSE; +} + +BOOL xf_event_process(freerdp* instance, const XEvent* event) +{ + BOOL status = TRUE; + + WINPR_ASSERT(instance); + WINPR_ASSERT(event); + + xfContext* xfc = (xfContext*)instance->context; + WINPR_ASSERT(xfc); + + rdpSettings* settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + if (xfc->remote_app) + { + xfAppWindow* appWindow = xf_AppWindowFromX11Window(xfc, event->xany.window); + + if (appWindow) + { + /* Update "current" window for cursor change orders */ + xfc->appWindow = appWindow; + + if (xf_event_suppress_events(xfc, appWindow, event)) + return TRUE; + } + } + + if (xfc->window) + { + xfFloatbar* floatbar = xfc->window->floatbar; + if (xf_floatbar_check_event(floatbar, event)) + { + xf_floatbar_event_process(floatbar, event); + return TRUE; + } + + if (xf_floatbar_is_locked(floatbar)) + return TRUE; + } + + xf_event_execute_action_script(xfc, event); + + if (event->type != MotionNotify) + { + DEBUG_X11("%s Event(%d): wnd=0x%08lX", x11_event_string(event->type), event->type, + (unsigned long)event->xany.window); + } + + switch (event->type) + { + case Expose: + status = xf_event_Expose(xfc, &event->xexpose, xfc->remote_app); + break; + + case VisibilityNotify: + status = xf_event_VisibilityNotify(xfc, &event->xvisibility, xfc->remote_app); + break; + + case MotionNotify: + status = xf_event_MotionNotify(xfc, &event->xmotion, xfc->remote_app); + break; + + case ButtonPress: + status = xf_event_ButtonPress(xfc, &event->xbutton, xfc->remote_app); + break; + + case ButtonRelease: + status = xf_event_ButtonRelease(xfc, &event->xbutton, xfc->remote_app); + break; + + case KeyPress: + status = xf_event_KeyPress(xfc, &event->xkey, xfc->remote_app); + break; + + case KeyRelease: + status = xf_event_KeyReleaseOrIgnore(xfc, &event->xkey, xfc->remote_app); + break; + + case FocusIn: + status = xf_event_FocusIn(xfc, &event->xfocus, xfc->remote_app); + break; + + case FocusOut: + status = xf_event_FocusOut(xfc, &event->xfocus, xfc->remote_app); + break; + + case EnterNotify: + status = xf_event_EnterNotify(xfc, &event->xcrossing, xfc->remote_app); + break; + + case LeaveNotify: + status = xf_event_LeaveNotify(xfc, &event->xcrossing, xfc->remote_app); + break; + + case NoExpose: + break; + + case GraphicsExpose: + break; + + case ConfigureNotify: + status = xf_event_ConfigureNotify(xfc, &event->xconfigure, xfc->remote_app); + break; + + case MapNotify: + status = xf_event_MapNotify(xfc, &event->xmap, xfc->remote_app); + break; + + case UnmapNotify: + status = xf_event_UnmapNotify(xfc, &event->xunmap, xfc->remote_app); + break; + + case ReparentNotify: + break; + + case MappingNotify: + status = xf_event_MappingNotify(xfc, &event->xmapping, xfc->remote_app); + break; + + case ClientMessage: + status = xf_event_ClientMessage(xfc, &event->xclient, xfc->remote_app); + break; + + case PropertyNotify: + status = xf_event_PropertyNotify(xfc, &event->xproperty, xfc->remote_app); + break; + + default: + if (freerdp_settings_get_bool(settings, FreeRDP_SupportDisplayControl)) + xf_disp_handle_xevent(xfc, event); + + break; + } + + xfWindow* window = xfc->window; + xfFloatbar* floatbar = NULL; + if (window) + floatbar = window->floatbar; + + xf_cliprdr_handle_xevent(xfc, event); + if (!xf_floatbar_check_event(floatbar, event) && !xf_floatbar_is_locked(floatbar)) + xf_input_handle_event(xfc, event); + + XSync(xfc->display, FALSE); + return status; +} + +BOOL xf_generic_RawButtonEvent(xfContext* xfc, int button, BOOL app, BOOL down) +{ + UINT16 flags = 0; + + if (app) + return FALSE; + + for (size_t i = 0; i < ARRAYSIZE(xfc->button_map); i++) + { + const button_map* cur = &xfc->button_map[i]; + + if (cur->button == button) + { + flags = cur->flags; + break; + } + } + + if (flags != 0) + { + if (flags & (PTR_FLAGS_WHEEL | PTR_FLAGS_HWHEEL)) + { + if (down) + freerdp_client_send_wheel_event(&xfc->common, flags); + } + else + { + BOOL extended = FALSE; + + if (flags & (PTR_XFLAGS_BUTTON1 | PTR_XFLAGS_BUTTON2)) + { + extended = TRUE; + + if (down) + flags |= PTR_XFLAGS_DOWN; + } + else if (flags & (PTR_FLAGS_BUTTON1 | PTR_FLAGS_BUTTON2 | PTR_FLAGS_BUTTON3)) + { + if (down) + flags |= PTR_FLAGS_DOWN; + } + + if (extended) + freerdp_client_send_extended_button_event(&xfc->common, TRUE, flags, 0, 0); + else + freerdp_client_send_button_event(&xfc->common, TRUE, flags, 0, 0); + } + } + + return TRUE; +} diff --git a/client/X11/xf_event.h b/client/X11/xf_event.h new file mode 100644 index 0000000..2f4ab07 --- /dev/null +++ b/client/X11/xf_event.h @@ -0,0 +1,46 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Event Handling + * + * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_X11_EVENT_H +#define FREERDP_CLIENT_X11_EVENT_H + +#include "xf_keyboard.h" + +#include "xf_client.h" +#include "xfreerdp.h" + +const char* x11_event_string(int event); + +BOOL xf_event_action_script_init(xfContext* xfc); +void xf_event_action_script_free(xfContext* xfc); + +BOOL xf_event_process(freerdp* instance, const XEvent* event); +void xf_event_SendClientEvent(xfContext* xfc, xfWindow* window, Atom atom, unsigned int numArgs, + ...); + +void xf_event_adjust_coordinates(xfContext* xfc, int* x, int* y); +void xf_adjust_coordinates_to_screen(xfContext* xfc, UINT32* x, UINT32* y); + +BOOL xf_generic_MotionNotify(xfContext* xfc, int x, int y, int state, Window window, BOOL app); +BOOL xf_generic_RawMotionNotify(xfContext* xfc, int x, int y, Window window, BOOL app); +BOOL xf_generic_ButtonEvent(xfContext* xfc, int x, int y, int button, Window window, BOOL app, + BOOL down); +BOOL xf_generic_RawButtonEvent(xfContext* xfc, int button, BOOL app, BOOL down); + +#endif /* FREERDP_CLIENT_X11_EVENT_H */ diff --git a/client/X11/xf_floatbar.c b/client/X11/xf_floatbar.c new file mode 100644 index 0000000..e4e290a --- /dev/null +++ b/client/X11/xf_floatbar.c @@ -0,0 +1,933 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Windows + * + * Licensed under the Apache License, Version 2.0 (the "License");n + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <X11/Xatom.h> +#include <X11/extensions/shape.h> +#include <X11/cursorfont.h> + +#include <winpr/assert.h> + +#include "xf_floatbar.h" +#include "resource/close.xbm" +#include "resource/lock.xbm" +#include "resource/unlock.xbm" +#include "resource/minimize.xbm" +#include "resource/restore.xbm" + +#include <freerdp/log.h> +#define TAG CLIENT_TAG("x11") + +#define FLOATBAR_HEIGHT 26 +#define FLOATBAR_DEFAULT_WIDTH 576 +#define FLOATBAR_MIN_WIDTH 200 +#define FLOATBAR_BORDER 24 +#define FLOATBAR_BUTTON_WIDTH 24 +#define FLOATBAR_COLOR_BACKGROUND "RGB:31/6c/a9" +#define FLOATBAR_COLOR_BORDER "RGB:75/9a/c8" +#define FLOATBAR_COLOR_FOREGROUND "RGB:FF/FF/FF" + +#ifdef WITH_DEBUG_X11 +#define DEBUG_X11(...) WLog_DBG(TAG, __VA_ARGS__) +#else +#define DEBUG_X11(...) \ + do \ + { \ + } while (0) +#endif + +#define XF_FLOATBAR_MODE_NONE 0 +#define XF_FLOATBAR_MODE_DRAGGING 1 +#define XF_FLOATBAR_MODE_RESIZE_LEFT 2 +#define XF_FLOATBAR_MODE_RESIZE_RIGHT 3 + +#define XF_FLOATBAR_BUTTON_CLOSE 1 +#define XF_FLOATBAR_BUTTON_RESTORE 2 +#define XF_FLOATBAR_BUTTON_MINIMIZE 3 +#define XF_FLOATBAR_BUTTON_LOCKED 4 + +typedef BOOL (*OnClick)(xfFloatbar*); + +typedef struct +{ + int x; + int y; + int type; + bool focus; + bool clicked; + OnClick onclick; + Window handle; +} xfFloatbarButton; + +struct xf_floatbar +{ + int x; + int y; + int width; + int height; + int mode; + int last_motion_x_root; + int last_motion_y_root; + BOOL locked; + xfFloatbarButton* buttons[4]; + Window handle; + BOOL hasCursor; + xfContext* xfc; + DWORD flags; + BOOL created; + Window root_window; + char* title; + XFontSet fontSet; +}; + +static xfFloatbarButton* xf_floatbar_new_button(xfFloatbar* floatbar, int type); + +static BOOL xf_floatbar_button_onclick_close(xfFloatbar* floatbar) +{ + if (!floatbar) + return FALSE; + + return freerdp_abort_connect_context(&floatbar->xfc->common.context); +} + +static BOOL xf_floatbar_button_onclick_minimize(xfFloatbar* floatbar) +{ + xfContext* xfc = NULL; + + if (!floatbar || !floatbar->xfc) + return FALSE; + + xfc = floatbar->xfc; + xf_SetWindowMinimized(xfc, xfc->window); + return TRUE; +} + +static BOOL xf_floatbar_button_onclick_restore(xfFloatbar* floatbar) +{ + if (!floatbar) + return FALSE; + + xf_toggle_fullscreen(floatbar->xfc); + return TRUE; +} + +static BOOL xf_floatbar_button_onclick_locked(xfFloatbar* floatbar) +{ + if (!floatbar) + return FALSE; + + floatbar->locked = (floatbar->locked) ? FALSE : TRUE; + return xf_floatbar_hide_and_show(floatbar); +} + +BOOL xf_floatbar_set_root_y(xfFloatbar* floatbar, int y) +{ + if (!floatbar) + return FALSE; + + floatbar->last_motion_y_root = y; + return TRUE; +} + +BOOL xf_floatbar_hide_and_show(xfFloatbar* floatbar) +{ + xfContext* xfc = NULL; + + if (!floatbar || !floatbar->xfc) + return FALSE; + + if (!floatbar->created) + return TRUE; + + xfc = floatbar->xfc; + WINPR_ASSERT(xfc); + WINPR_ASSERT(xfc->display); + + if (!floatbar->locked) + { + if ((floatbar->mode == XF_FLOATBAR_MODE_NONE) && (floatbar->last_motion_y_root > 10) && + (floatbar->y > (FLOATBAR_HEIGHT * -1))) + { + floatbar->y = floatbar->y - 1; + XMoveWindow(xfc->display, floatbar->handle, floatbar->x, floatbar->y); + } + else if (floatbar->y < 0 && (floatbar->last_motion_y_root < 10)) + { + floatbar->y = floatbar->y + 1; + XMoveWindow(xfc->display, floatbar->handle, floatbar->x, floatbar->y); + } + } + + return TRUE; +} + +static BOOL create_floatbar(xfFloatbar* floatbar) +{ + xfContext* xfc = NULL; + Status status = 0; + XWindowAttributes attr = { 0 }; + + WINPR_ASSERT(floatbar); + if (floatbar->created) + return TRUE; + + xfc = floatbar->xfc; + WINPR_ASSERT(xfc); + WINPR_ASSERT(xfc->display); + + status = XGetWindowAttributes(xfc->display, floatbar->root_window, &attr); + if (status == 0) + { + WLog_WARN(TAG, "XGetWindowAttributes failed"); + return FALSE; + } + floatbar->x = attr.x + attr.width / 2 - FLOATBAR_DEFAULT_WIDTH / 2; + floatbar->y = 0; + + if (((floatbar->flags & 0x0004) == 0) && !floatbar->locked) + floatbar->y = -FLOATBAR_HEIGHT + 1; + + floatbar->handle = + XCreateWindow(xfc->display, floatbar->root_window, floatbar->x, 0, FLOATBAR_DEFAULT_WIDTH, + FLOATBAR_HEIGHT, 0, CopyFromParent, InputOutput, CopyFromParent, 0, NULL); + floatbar->width = FLOATBAR_DEFAULT_WIDTH; + floatbar->height = FLOATBAR_HEIGHT; + floatbar->mode = XF_FLOATBAR_MODE_NONE; + floatbar->buttons[0] = xf_floatbar_new_button(floatbar, XF_FLOATBAR_BUTTON_CLOSE); + floatbar->buttons[1] = xf_floatbar_new_button(floatbar, XF_FLOATBAR_BUTTON_RESTORE); + floatbar->buttons[2] = xf_floatbar_new_button(floatbar, XF_FLOATBAR_BUTTON_MINIMIZE); + floatbar->buttons[3] = xf_floatbar_new_button(floatbar, XF_FLOATBAR_BUTTON_LOCKED); + XSelectInput(xfc->display, floatbar->handle, + ExposureMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | + FocusChangeMask | LeaveWindowMask | EnterWindowMask | StructureNotifyMask | + PropertyChangeMask); + floatbar->created = TRUE; + return TRUE; +} + +BOOL xf_floatbar_toggle_fullscreen(xfFloatbar* floatbar, bool fullscreen) +{ + int size = 0; + bool visible = False; + xfContext* xfc = NULL; + + if (!floatbar || !floatbar->xfc) + return FALSE; + + xfc = floatbar->xfc; + WINPR_ASSERT(xfc->display); + + /* Only visible if enabled */ + if (floatbar->flags & 0x0001) + { + /* Visible if fullscreen and flag visible in fullscreen mode */ + visible |= ((floatbar->flags & 0x0010) != 0) && fullscreen; + /* Visible if window and flag visible in window mode */ + visible |= ((floatbar->flags & 0x0020) != 0) && !fullscreen; + } + + if (visible) + { + if (!create_floatbar(floatbar)) + return FALSE; + + XMapWindow(xfc->display, floatbar->handle); + size = ARRAYSIZE(floatbar->buttons); + + for (int i = 0; i < size; i++) + { + xfFloatbarButton* button = floatbar->buttons[i]; + XMapWindow(xfc->display, button->handle); + } + + /* If default is hidden (and not sticky) don't show on fullscreen state changes */ + if (((floatbar->flags & 0x0004) == 0) && !floatbar->locked) + floatbar->y = -FLOATBAR_HEIGHT + 1; + + xf_floatbar_hide_and_show(floatbar); + } + else if (floatbar->created) + { + XUnmapSubwindows(xfc->display, floatbar->handle); + XUnmapWindow(xfc->display, floatbar->handle); + } + + return TRUE; +} + +xfFloatbarButton* xf_floatbar_new_button(xfFloatbar* floatbar, int type) +{ + xfFloatbarButton* button = NULL; + + WINPR_ASSERT(floatbar); + WINPR_ASSERT(floatbar->xfc); + WINPR_ASSERT(floatbar->xfc->display); + WINPR_ASSERT(floatbar->handle); + + button = (xfFloatbarButton*)calloc(1, sizeof(xfFloatbarButton)); + button->type = type; + + switch (type) + { + case XF_FLOATBAR_BUTTON_CLOSE: + button->x = floatbar->width - FLOATBAR_BORDER - FLOATBAR_BUTTON_WIDTH * type; + button->onclick = xf_floatbar_button_onclick_close; + break; + + case XF_FLOATBAR_BUTTON_RESTORE: + button->x = floatbar->width - FLOATBAR_BORDER - FLOATBAR_BUTTON_WIDTH * type; + button->onclick = xf_floatbar_button_onclick_restore; + break; + + case XF_FLOATBAR_BUTTON_MINIMIZE: + button->x = floatbar->width - FLOATBAR_BORDER - FLOATBAR_BUTTON_WIDTH * type; + button->onclick = xf_floatbar_button_onclick_minimize; + break; + + case XF_FLOATBAR_BUTTON_LOCKED: + button->x = FLOATBAR_BORDER; + button->onclick = xf_floatbar_button_onclick_locked; + break; + + default: + break; + } + + button->y = 0; + button->focus = FALSE; + button->handle = XCreateWindow(floatbar->xfc->display, floatbar->handle, button->x, 0, + FLOATBAR_BUTTON_WIDTH, FLOATBAR_BUTTON_WIDTH, 0, CopyFromParent, + InputOutput, CopyFromParent, 0, NULL); + XSelectInput(floatbar->xfc->display, button->handle, + ExposureMask | ButtonPressMask | ButtonReleaseMask | FocusChangeMask | + LeaveWindowMask | EnterWindowMask | StructureNotifyMask); + return button; +} + +xfFloatbar* xf_floatbar_new(xfContext* xfc, Window window, const char* name, DWORD flags) +{ + WINPR_ASSERT(xfc); + WINPR_ASSERT(xfc->display); + WINPR_ASSERT(name); + + /* Floatbar not enabled */ + if ((flags & 0x0001) == 0) + return NULL; + + if (!xfc) + return NULL; + + /* Force disable with remote app */ + if (xfc->remote_app) + return NULL; + + xfFloatbar* floatbar = (xfFloatbar*)calloc(1, sizeof(xfFloatbar)); + + if (!floatbar) + return NULL; + + floatbar->title = _strdup(name); + + if (!floatbar->title) + goto fail; + + floatbar->root_window = window; + floatbar->flags = flags; + floatbar->xfc = xfc; + floatbar->locked = flags & 0x0002; + xf_floatbar_toggle_fullscreen(floatbar, FALSE); + char** missingList = NULL; + int missingCount = 0; + char* defString = NULL; + floatbar->fontSet = XCreateFontSet(floatbar->xfc->display, "-*-*-*-*-*-*-*-*-*-*-*-*-*-*", + &missingList, &missingCount, &defString); + if (floatbar->fontSet == NULL) + { + WLog_ERR(TAG, "Failed to create fontset"); + } + XFreeStringList(missingList); + return floatbar; +fail: + WINPR_PRAGMA_DIAG_PUSH + WINPR_PRAGMA_DIAG_IGNORED_MISMATCHED_DEALLOC + xf_floatbar_free(floatbar); + WINPR_PRAGMA_DIAG_POP + return NULL; +} + +static unsigned long xf_floatbar_get_color(xfFloatbar* floatbar, char* rgb_value) +{ + XColor color; + + WINPR_ASSERT(floatbar); + WINPR_ASSERT(floatbar->xfc); + + Display* display = floatbar->xfc->display; + WINPR_ASSERT(display); + + Colormap cmap = DefaultColormap(display, XDefaultScreen(display)); + XParseColor(display, cmap, rgb_value, &color); + XAllocColor(display, cmap, &color); + return color.pixel; +} + +static void xf_floatbar_event_expose(xfFloatbar* floatbar) +{ + GC gc = NULL; + GC shape_gc = NULL; + Pixmap pmap = 0; + XPoint shape[5] = { 0 }; + XPoint border[5] = { 0 }; + int len = 0; + + WINPR_ASSERT(floatbar); + WINPR_ASSERT(floatbar->xfc); + + Display* display = floatbar->xfc->display; + WINPR_ASSERT(display); + + /* create the pixmap that we'll use for shaping the window */ + pmap = XCreatePixmap(display, floatbar->handle, floatbar->width, floatbar->height, 1); + gc = XCreateGC(display, floatbar->handle, 0, 0); + shape_gc = XCreateGC(display, pmap, 0, 0); + /* points for drawing the floatbar */ + shape[0].x = 0; + shape[0].y = 0; + shape[1].x = floatbar->width; + shape[1].y = 0; + shape[2].x = shape[1].x - FLOATBAR_BORDER; + shape[2].y = FLOATBAR_HEIGHT; + shape[3].x = shape[0].x + FLOATBAR_BORDER; + shape[3].y = FLOATBAR_HEIGHT; + shape[4].x = shape[0].x; + shape[4].y = shape[0].y; + /* points for drawing the border of the floatbar */ + border[0].x = shape[0].x; + border[0].y = shape[0].y - 1; + border[1].x = shape[1].x - 1; + border[1].y = shape[1].y - 1; + border[2].x = shape[2].x; + border[2].y = shape[2].y - 1; + border[3].x = shape[3].x - 1; + border[3].y = shape[3].y - 1; + border[4].x = border[0].x; + border[4].y = border[0].y; + /* Fill all pixels with 0 */ + XSetForeground(display, shape_gc, 0); + XFillRectangle(display, pmap, shape_gc, 0, 0, floatbar->width, floatbar->height); + /* Fill all pixels which should be shown with 1 */ + XSetForeground(display, shape_gc, 1); + XFillPolygon(display, pmap, shape_gc, shape, 5, 0, CoordModeOrigin); + XShapeCombineMask(display, floatbar->handle, ShapeBounding, 0, 0, pmap, ShapeSet); + /* draw the float bar */ + XSetForeground(display, gc, xf_floatbar_get_color(floatbar, FLOATBAR_COLOR_BACKGROUND)); + XFillPolygon(display, floatbar->handle, gc, shape, 4, 0, CoordModeOrigin); + /* draw an border for the floatbar */ + XSetForeground(display, gc, xf_floatbar_get_color(floatbar, FLOATBAR_COLOR_BORDER)); + XDrawLines(display, floatbar->handle, gc, border, 5, CoordModeOrigin); + /* draw the host name connected to (limit to maximum file name) */ + len = strnlen(floatbar->title, MAX_PATH); + XSetForeground(display, gc, xf_floatbar_get_color(floatbar, FLOATBAR_COLOR_FOREGROUND)); + if (floatbar->fontSet != NULL) + { + XmbDrawString(display, floatbar->handle, floatbar->fontSet, gc, + floatbar->width / 2 - len * 2, 15, floatbar->title, len); + } + else + { + XDrawString(display, floatbar->handle, gc, floatbar->width / 2 - len * 2, 15, + floatbar->title, len); + } + XFreeGC(display, gc); + XFreeGC(display, shape_gc); +} + +static xfFloatbarButton* xf_floatbar_get_button(xfFloatbar* floatbar, Window window) +{ + WINPR_ASSERT(floatbar); + const size_t size = ARRAYSIZE(floatbar->buttons); + + for (size_t i = 0; i < size; i++) + { + xfFloatbarButton* button = floatbar->buttons[i]; + if (button->handle == window) + { + return button; + } + } + + return NULL; +} + +static void xf_floatbar_button_update_positon(xfFloatbar* floatbar) +{ + xfFloatbarButton* button = NULL; + WINPR_ASSERT(floatbar); + xfContext* xfc = floatbar->xfc; + const size_t size = ARRAYSIZE(floatbar->buttons); + + for (size_t i = 0; i < size; i++) + { + button = floatbar->buttons[i]; + + switch (button->type) + { + case XF_FLOATBAR_BUTTON_CLOSE: + button->x = + floatbar->width - FLOATBAR_BORDER - FLOATBAR_BUTTON_WIDTH * button->type; + break; + + case XF_FLOATBAR_BUTTON_RESTORE: + button->x = + floatbar->width - FLOATBAR_BORDER - FLOATBAR_BUTTON_WIDTH * button->type; + break; + + case XF_FLOATBAR_BUTTON_MINIMIZE: + button->x = + floatbar->width - FLOATBAR_BORDER - FLOATBAR_BUTTON_WIDTH * button->type; + break; + + default: + break; + } + + WINPR_ASSERT(xfc); + WINPR_ASSERT(xfc->display); + XMoveWindow(xfc->display, button->handle, button->x, button->y); + xf_floatbar_event_expose(floatbar); + } +} + +static void xf_floatbar_button_event_expose(xfFloatbar* floatbar, Window window) +{ + xfFloatbarButton* button = xf_floatbar_get_button(floatbar, window); + static unsigned char* bits; + GC gc = NULL; + Pixmap pattern = 0; + xfContext* xfc = floatbar->xfc; + + if (!button) + return; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(xfc->display); + WINPR_ASSERT(xfc->window); + + gc = XCreateGC(xfc->display, button->handle, 0, 0); + floatbar = xfc->window->floatbar; + WINPR_ASSERT(floatbar); + + switch (button->type) + { + case XF_FLOATBAR_BUTTON_CLOSE: + bits = close_bits; + break; + + case XF_FLOATBAR_BUTTON_RESTORE: + bits = restore_bits; + break; + + case XF_FLOATBAR_BUTTON_MINIMIZE: + bits = minimize_bits; + break; + + case XF_FLOATBAR_BUTTON_LOCKED: + if (floatbar->locked) + bits = lock_bits; + else + bits = unlock_bits; + + break; + + default: + break; + } + + pattern = XCreateBitmapFromData(xfc->display, button->handle, (const char*)bits, + FLOATBAR_BUTTON_WIDTH, FLOATBAR_BUTTON_WIDTH); + + if (!(button->focus)) + XSetForeground(xfc->display, gc, + xf_floatbar_get_color(floatbar, FLOATBAR_COLOR_BACKGROUND)); + else + XSetForeground(xfc->display, gc, xf_floatbar_get_color(floatbar, FLOATBAR_COLOR_BORDER)); + + XSetBackground(xfc->display, gc, xf_floatbar_get_color(floatbar, FLOATBAR_COLOR_FOREGROUND)); + XCopyPlane(xfc->display, pattern, button->handle, gc, 0, 0, FLOATBAR_BUTTON_WIDTH, + FLOATBAR_BUTTON_WIDTH, 0, 0, 1); + XFreePixmap(xfc->display, pattern); + XFreeGC(xfc->display, gc); +} + +static void xf_floatbar_button_event_buttonpress(xfFloatbar* floatbar, const XButtonEvent* event) +{ + WINPR_ASSERT(event); + xfFloatbarButton* button = xf_floatbar_get_button(floatbar, event->window); + + if (button) + button->clicked = TRUE; +} + +static void xf_floatbar_button_event_buttonrelease(xfFloatbar* floatbar, const XButtonEvent* event) +{ + xfFloatbarButton* button = NULL; + + WINPR_ASSERT(floatbar); + WINPR_ASSERT(event); + + button = xf_floatbar_get_button(floatbar, event->window); + + if (button) + { + if (button->clicked) + button->onclick(floatbar); + button->clicked = FALSE; + } +} + +static void xf_floatbar_event_buttonpress(xfFloatbar* floatbar, const XButtonEvent* event) +{ + WINPR_ASSERT(floatbar); + WINPR_ASSERT(event); + + switch (event->button) + { + case Button1: + if (event->x <= FLOATBAR_BORDER) + floatbar->mode = XF_FLOATBAR_MODE_RESIZE_LEFT; + else if (event->x >= (floatbar->width - FLOATBAR_BORDER)) + floatbar->mode = XF_FLOATBAR_MODE_RESIZE_RIGHT; + else + floatbar->mode = XF_FLOATBAR_MODE_DRAGGING; + + break; + + default: + break; + } +} + +static void xf_floatbar_event_buttonrelease(xfFloatbar* floatbar, const XButtonEvent* event) +{ + WINPR_ASSERT(floatbar); + WINPR_ASSERT(event); + + switch (event->button) + { + case Button1: + floatbar->mode = XF_FLOATBAR_MODE_NONE; + break; + + default: + break; + } +} + +static void xf_floatbar_resize(xfFloatbar* floatbar, const XMotionEvent* event) +{ + int x = 0; + int width = 0; + int movement = 0; + + WINPR_ASSERT(floatbar); + WINPR_ASSERT(event); + + xfContext* xfc = floatbar->xfc; + WINPR_ASSERT(xfc); + WINPR_ASSERT(xfc->display); + + /* calculate movement which happened on the root window */ + movement = event->x_root - floatbar->last_motion_x_root; + + /* set x and width depending if movement happens on the left or right */ + if (floatbar->mode == XF_FLOATBAR_MODE_RESIZE_LEFT) + { + x = floatbar->x + movement; + width = floatbar->width + movement * -1; + } + else + { + x = floatbar->x; + width = floatbar->width + movement; + } + + /* only resize and move window if still above minimum width */ + if (FLOATBAR_MIN_WIDTH < width) + { + XMoveResizeWindow(xfc->display, floatbar->handle, x, 0, width, floatbar->height); + floatbar->x = x; + floatbar->width = width; + } +} + +static void xf_floatbar_dragging(xfFloatbar* floatbar, const XMotionEvent* event) +{ + int x = 0; + int movement = 0; + + WINPR_ASSERT(floatbar); + WINPR_ASSERT(event); + xfContext* xfc = floatbar->xfc; + WINPR_ASSERT(xfc); + WINPR_ASSERT(xfc->window); + WINPR_ASSERT(xfc->display); + + /* calculate movement and new x position */ + movement = event->x_root - floatbar->last_motion_x_root; + x = floatbar->x + movement; + + /* do nothing if floatbar would be moved out of the window */ + if (x < 0 || (x + floatbar->width) > xfc->window->width) + return; + + /* move window to new x position */ + XMoveWindow(xfc->display, floatbar->handle, x, 0); + /* update struct values for the next event */ + floatbar->last_motion_x_root = floatbar->last_motion_x_root + movement; + floatbar->x = x; +} + +static void xf_floatbar_event_motionnotify(xfFloatbar* floatbar, const XMotionEvent* event) +{ + int mode = 0; + Cursor cursor = 0; + + WINPR_ASSERT(floatbar); + WINPR_ASSERT(event); + + xfContext* xfc = floatbar->xfc; + WINPR_ASSERT(xfc); + WINPR_ASSERT(xfc->display); + + mode = floatbar->mode; + cursor = XCreateFontCursor(xfc->display, XC_arrow); + + if ((event->state & Button1Mask) && (mode > XF_FLOATBAR_MODE_DRAGGING)) + { + xf_floatbar_resize(floatbar, event); + } + else if ((event->state & Button1Mask) && (mode == XF_FLOATBAR_MODE_DRAGGING)) + { + xf_floatbar_dragging(floatbar, event); + } + else + { + if (event->x <= FLOATBAR_BORDER || event->x >= floatbar->width - FLOATBAR_BORDER) + cursor = XCreateFontCursor(xfc->display, XC_sb_h_double_arrow); + } + + XDefineCursor(xfc->display, xfc->window->handle, cursor); + XFreeCursor(xfc->display, cursor); + floatbar->last_motion_x_root = event->x_root; +} + +static void xf_floatbar_button_event_focusin(xfFloatbar* floatbar, const XAnyEvent* event) +{ + xfFloatbarButton* button = NULL; + + WINPR_ASSERT(floatbar); + WINPR_ASSERT(event); + + button = xf_floatbar_get_button(floatbar, event->window); + + if (button) + { + button->focus = TRUE; + xf_floatbar_button_event_expose(floatbar, event->window); + } +} + +static void xf_floatbar_button_event_focusout(xfFloatbar* floatbar, const XAnyEvent* event) +{ + xfFloatbarButton* button = NULL; + + WINPR_ASSERT(floatbar); + WINPR_ASSERT(event); + + button = xf_floatbar_get_button(floatbar, event->window); + + if (button) + { + button->focus = FALSE; + xf_floatbar_button_event_expose(floatbar, event->window); + } +} + +static void xf_floatbar_event_focusout(xfFloatbar* floatbar) +{ + WINPR_ASSERT(floatbar); + xfContext* xfc = floatbar->xfc; + WINPR_ASSERT(xfc); + + if (xfc->pointer) + { + WINPR_ASSERT(xfc->window); + WINPR_ASSERT(xfc->pointer); + XDefineCursor(xfc->display, xfc->window->handle, xfc->pointer->cursor); + } +} + +BOOL xf_floatbar_check_event(xfFloatbar* floatbar, const XEvent* event) +{ + if (!floatbar || !floatbar->xfc || !event) + return FALSE; + + if (!floatbar->created) + return FALSE; + + if (event->xany.window == floatbar->handle) + return TRUE; + + size_t size = ARRAYSIZE(floatbar->buttons); + + for (size_t i = 0; i < size; i++) + { + const xfFloatbarButton* button = floatbar->buttons[i]; + + if (event->xany.window == button->handle) + return TRUE; + } + + return FALSE; +} + +BOOL xf_floatbar_event_process(xfFloatbar* floatbar, const XEvent* event) +{ + if (!floatbar || !floatbar->xfc || !event) + return FALSE; + + if (!floatbar->created) + return FALSE; + + switch (event->type) + { + case Expose: + if (event->xexpose.window == floatbar->handle) + xf_floatbar_event_expose(floatbar); + else + xf_floatbar_button_event_expose(floatbar, event->xexpose.window); + + break; + + case MotionNotify: + xf_floatbar_event_motionnotify(floatbar, &event->xmotion); + break; + + case ButtonPress: + if (event->xany.window == floatbar->handle) + xf_floatbar_event_buttonpress(floatbar, &event->xbutton); + else + xf_floatbar_button_event_buttonpress(floatbar, &event->xbutton); + + break; + + case ButtonRelease: + if (event->xany.window == floatbar->handle) + xf_floatbar_event_buttonrelease(floatbar, &event->xbutton); + else + xf_floatbar_button_event_buttonrelease(floatbar, &event->xbutton); + + break; + + case EnterNotify: + case FocusIn: + if (event->xany.window != floatbar->handle) + xf_floatbar_button_event_focusin(floatbar, &event->xany); + + break; + + case LeaveNotify: + case FocusOut: + if (event->xany.window == floatbar->handle) + xf_floatbar_event_focusout(floatbar); + else + xf_floatbar_button_event_focusout(floatbar, &event->xany); + + break; + + case ConfigureNotify: + if (event->xany.window == floatbar->handle) + xf_floatbar_button_update_positon(floatbar); + + break; + + case PropertyNotify: + if (event->xany.window == floatbar->handle) + xf_floatbar_button_update_positon(floatbar); + + break; + + default: + break; + } + + return floatbar->handle == event->xany.window; +} + +static void xf_floatbar_button_free(xfContext* xfc, xfFloatbarButton* button) +{ + if (!button) + return; + + if (button->handle) + { + WINPR_ASSERT(xfc); + WINPR_ASSERT(xfc->display); + XUnmapWindow(xfc->display, button->handle); + XDestroyWindow(xfc->display, button->handle); + } + + free(button); +} + +void xf_floatbar_free(xfFloatbar* floatbar) +{ + size_t size = 0; + xfContext* xfc = NULL; + + if (!floatbar) + return; + + free(floatbar->title); + xfc = floatbar->xfc; + WINPR_ASSERT(xfc); + + size = ARRAYSIZE(floatbar->buttons); + + for (size_t i = 0; i < size; i++) + { + xf_floatbar_button_free(xfc, floatbar->buttons[i]); + floatbar->buttons[i] = NULL; + } + + if (floatbar->handle) + { + WINPR_ASSERT(xfc->display); + XUnmapWindow(xfc->display, floatbar->handle); + XDestroyWindow(xfc->display, floatbar->handle); + } + + free(floatbar); +} + +BOOL xf_floatbar_is_locked(xfFloatbar* floatbar) +{ + if (!floatbar) + return FALSE; + return floatbar->mode != XF_FLOATBAR_MODE_NONE; +} diff --git a/client/X11/xf_floatbar.h b/client/X11/xf_floatbar.h new file mode 100644 index 0000000..1ac7c91 --- /dev/null +++ b/client/X11/xf_floatbar.h @@ -0,0 +1,37 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Windows + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_X11_FLOATBAR_H +#define FREERDP_CLIENT_X11_FLOATBAR_H + +typedef struct xf_floatbar xfFloatbar; + +#include "xfreerdp.h" + +void xf_floatbar_free(xfFloatbar* floatbar); + +WINPR_ATTR_MALLOC(xf_floatbar_free, 1) +xfFloatbar* xf_floatbar_new(xfContext* xfc, Window window, const char* title, DWORD flags); + +BOOL xf_floatbar_is_locked(xfFloatbar* floatbar); +BOOL xf_floatbar_event_process(xfFloatbar* floatbar, const XEvent* event); +BOOL xf_floatbar_check_event(xfFloatbar* floatbar, const XEvent* event); +BOOL xf_floatbar_toggle_fullscreen(xfFloatbar* floatbar, bool visible); +BOOL xf_floatbar_hide_and_show(xfFloatbar* floatbar); +BOOL xf_floatbar_set_root_y(xfFloatbar* floatbar, int y); + +#endif /* FREERDP_CLIENT_X11_FLOATBAR_H */ diff --git a/client/X11/xf_gfx.c b/client/X11/xf_gfx.c new file mode 100644 index 0000000..757b424 --- /dev/null +++ b/client/X11/xf_gfx.c @@ -0,0 +1,476 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Graphics Pipeline + * + * Copyright 2014 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * Copyright 2016 Armin Novak <armin.novak@thincast.com> + * Copyright 2016 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <math.h> +#include <winpr/assert.h> +#include <freerdp/log.h> +#include "xf_gfx.h" +#include "xf_rail.h" + +#include <X11/Xutil.h> + +#define TAG CLIENT_TAG("x11") + +static UINT xf_OutputUpdate(xfContext* xfc, xfGfxSurface* surface) +{ + UINT rc = ERROR_INTERNAL_ERROR; + UINT32 surfaceX = 0; + UINT32 surfaceY = 0; + RECTANGLE_16 surfaceRect; + rdpGdi* gdi = NULL; + const rdpSettings* settings = NULL; + UINT32 nbRects = 0; + double sx = NAN; + double sy = NAN; + const RECTANGLE_16* rects = NULL; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(surface); + + gdi = xfc->common.context.gdi; + WINPR_ASSERT(gdi); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + surfaceX = surface->gdi.outputOriginX; + surfaceY = surface->gdi.outputOriginY; + surfaceRect.left = 0; + surfaceRect.top = 0; + surfaceRect.right = surface->gdi.mappedWidth; + surfaceRect.bottom = surface->gdi.mappedHeight; + XSetClipMask(xfc->display, xfc->gc, None); + XSetFunction(xfc->display, xfc->gc, GXcopy); + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + region16_intersect_rect(&(surface->gdi.invalidRegion), &(surface->gdi.invalidRegion), + &surfaceRect); + sx = surface->gdi.outputTargetWidth / (double)surface->gdi.mappedWidth; + sy = surface->gdi.outputTargetHeight / (double)surface->gdi.mappedHeight; + + if (!(rects = region16_rects(&surface->gdi.invalidRegion, &nbRects))) + return CHANNEL_RC_OK; + + for (UINT32 x = 0; x < nbRects; x++) + { + const RECTANGLE_16* rect = &rects[x]; + const UINT32 nXSrc = rect->left; + const UINT32 nYSrc = rect->top; + const UINT32 swidth = rect->right - nXSrc; + const UINT32 sheight = rect->bottom - nYSrc; + const UINT32 nXDst = surfaceX + nXSrc * sx; + const UINT32 nYDst = surfaceY + nYSrc * sy; + const UINT32 dwidth = swidth * sx; + const UINT32 dheight = sheight * sy; + + if (surface->stage) + { + if (!freerdp_image_scale(surface->stage, gdi->dstFormat, surface->stageScanline, nXSrc, + nYSrc, dwidth, dheight, surface->gdi.data, surface->gdi.format, + surface->gdi.scanline, nXSrc, nYSrc, swidth, sheight)) + goto fail; + } + + if (xfc->remote_app) + { + XPutImage(xfc->display, xfc->primary, xfc->gc, surface->image, nXSrc, nYSrc, nXDst, + nYDst, dwidth, dheight); + xf_lock_x11(xfc); + xf_rail_paint_surface(xfc, surface->gdi.windowId, rect); + xf_unlock_x11(xfc); + } + else +#ifdef WITH_XRENDER + if (freerdp_settings_get_bool(settings, FreeRDP_SmartSizing) || + freerdp_settings_get_bool(settings, FreeRDP_MultiTouchGestures)) + { + XPutImage(xfc->display, xfc->primary, xfc->gc, surface->image, nXSrc, nYSrc, nXDst, + nYDst, dwidth, dheight); + xf_draw_screen(xfc, nXDst, nYDst, dwidth, dheight); + } + else +#endif + { + XPutImage(xfc->display, xfc->drawable, xfc->gc, surface->image, nXSrc, nYSrc, nXDst, + nYDst, dwidth, dheight); + } + } + + rc = CHANNEL_RC_OK; +fail: + region16_clear(&surface->gdi.invalidRegion); + XSetClipMask(xfc->display, xfc->gc, None); + XSync(xfc->display, False); + return rc; +} + +static UINT xf_WindowUpdate(RdpgfxClientContext* context, xfGfxSurface* surface) +{ + WINPR_ASSERT(context); + WINPR_ASSERT(surface); + return IFCALLRESULT(CHANNEL_RC_OK, context->UpdateWindowFromSurface, context, &surface->gdi); +} + +static UINT xf_UpdateSurfaces(RdpgfxClientContext* context) +{ + UINT16 count = 0; + UINT status = CHANNEL_RC_OK; + UINT16* pSurfaceIds = NULL; + rdpGdi* gdi = (rdpGdi*)context->custom; + xfContext* xfc = NULL; + + if (!gdi) + return status; + + if (gdi->suppressOutput) + return CHANNEL_RC_OK; + + xfc = (xfContext*)gdi->context; + EnterCriticalSection(&context->mux); + context->GetSurfaceIds(context, &pSurfaceIds, &count); + + for (UINT32 index = 0; index < count; index++) + { + xfGfxSurface* surface = (xfGfxSurface*)context->GetSurfaceData(context, pSurfaceIds[index]); + + if (!surface) + continue; + + /* If UpdateSurfaceArea callback is available, the output has already been updated. */ + if (context->UpdateSurfaceArea) + { + if (surface->gdi.handleInUpdateSurfaceArea) + continue; + } + + if (surface->gdi.outputMapped) + status = xf_OutputUpdate(xfc, surface); + else if (surface->gdi.windowMapped) + status = xf_WindowUpdate(context, surface); + + if (status != CHANNEL_RC_OK) + break; + } + + free(pSurfaceIds); + LeaveCriticalSection(&context->mux); + return status; +} + +UINT xf_OutputExpose(xfContext* xfc, UINT32 x, UINT32 y, UINT32 width, UINT32 height) +{ + UINT16 count = 0; + UINT status = ERROR_INTERNAL_ERROR; + RECTANGLE_16 invalidRect = { 0 }; + RECTANGLE_16 intersection = { 0 }; + UINT16* pSurfaceIds = NULL; + RdpgfxClientContext* context = NULL; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(xfc->common.context.gdi); + + context = xfc->common.context.gdi->gfx; + WINPR_ASSERT(context); + + invalidRect.left = x; + invalidRect.top = y; + invalidRect.right = x + width; + invalidRect.bottom = y + height; + status = context->GetSurfaceIds(context, &pSurfaceIds, &count); + + if (status != CHANNEL_RC_OK) + goto fail; + + if (!TryEnterCriticalSection(&context->mux)) + { + free(pSurfaceIds); + return CHANNEL_RC_OK; + } + for (UINT32 index = 0; index < count; index++) + { + RECTANGLE_16 surfaceRect = { 0 }; + xfGfxSurface* surface = (xfGfxSurface*)context->GetSurfaceData(context, pSurfaceIds[index]); + + if (!surface || (!surface->gdi.outputMapped && !surface->gdi.windowMapped)) + continue; + + surfaceRect.left = surface->gdi.outputOriginX; + surfaceRect.top = surface->gdi.outputOriginY; + surfaceRect.right = surface->gdi.outputOriginX + surface->gdi.outputTargetWidth; + surfaceRect.bottom = surface->gdi.outputOriginY + surface->gdi.outputTargetHeight; + + if (rectangles_intersection(&invalidRect, &surfaceRect, &intersection)) + { + /* Invalid rects are specified relative to surface origin */ + intersection.left -= surfaceRect.left; + intersection.top -= surfaceRect.top; + intersection.right -= surfaceRect.left; + intersection.bottom -= surfaceRect.top; + region16_union_rect(&surface->gdi.invalidRegion, &surface->gdi.invalidRegion, + &intersection); + } + } + + free(pSurfaceIds); + LeaveCriticalSection(&context->mux); + IFCALLRET(context->UpdateSurfaces, status, context); + + if (status != CHANNEL_RC_OK) + goto fail; + +fail: + return status; +} + +static UINT32 x11_pad_scanline(UINT32 scanline, UINT32 inPad) +{ + /* Ensure X11 alignment is met */ + if (inPad > 0) + { + const UINT32 align = inPad / 8; + const UINT32 pad = align - scanline % align; + + if (align != pad) + scanline += pad; + } + + /* 16 byte alingment is required for ASM optimized code */ + if (scanline % 16) + scanline += 16 - scanline % 16; + + return scanline; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_CreateSurface(RdpgfxClientContext* context, + const RDPGFX_CREATE_SURFACE_PDU* createSurface) +{ + UINT ret = CHANNEL_RC_NO_MEMORY; + size_t size = 0; + xfGfxSurface* surface = NULL; + rdpGdi* gdi = (rdpGdi*)context->custom; + xfContext* xfc = (xfContext*)gdi->context; + surface = (xfGfxSurface*)calloc(1, sizeof(xfGfxSurface)); + + if (!surface) + return CHANNEL_RC_NO_MEMORY; + + surface->gdi.codecs = context->codecs; + + if (!surface->gdi.codecs) + { + WLog_ERR(TAG, "global GDI codecs aren't set"); + goto out_free; + } + + surface->gdi.surfaceId = createSurface->surfaceId; + surface->gdi.width = x11_pad_scanline(createSurface->width, 0); + surface->gdi.height = x11_pad_scanline(createSurface->height, 0); + surface->gdi.mappedWidth = createSurface->width; + surface->gdi.mappedHeight = createSurface->height; + surface->gdi.outputTargetWidth = createSurface->width; + surface->gdi.outputTargetHeight = createSurface->height; + + switch (createSurface->pixelFormat) + { + case GFX_PIXEL_FORMAT_ARGB_8888: + surface->gdi.format = PIXEL_FORMAT_BGRA32; + break; + + case GFX_PIXEL_FORMAT_XRGB_8888: + surface->gdi.format = PIXEL_FORMAT_BGRX32; + break; + + default: + WLog_ERR(TAG, "unknown pixelFormat 0x%" PRIx32 "", createSurface->pixelFormat); + ret = ERROR_INTERNAL_ERROR; + goto out_free; + } + + surface->gdi.scanline = surface->gdi.width * FreeRDPGetBytesPerPixel(surface->gdi.format); + surface->gdi.scanline = x11_pad_scanline(surface->gdi.scanline, xfc->scanline_pad); + size = 1ull * surface->gdi.scanline * surface->gdi.height; + surface->gdi.data = (BYTE*)winpr_aligned_malloc(size, 16); + + if (!surface->gdi.data) + { + WLog_ERR(TAG, "unable to allocate GDI data"); + goto out_free; + } + + ZeroMemory(surface->gdi.data, size); + + if (FreeRDPAreColorFormatsEqualNoAlpha(gdi->dstFormat, surface->gdi.format)) + { + WINPR_ASSERT(xfc->depth != 0); + surface->image = + XCreateImage(xfc->display, xfc->visual, xfc->depth, ZPixmap, 0, + (char*)surface->gdi.data, surface->gdi.mappedWidth, + surface->gdi.mappedHeight, xfc->scanline_pad, surface->gdi.scanline); + } + else + { + UINT32 width = surface->gdi.width; + UINT32 bytes = FreeRDPGetBytesPerPixel(gdi->dstFormat); + surface->stageScanline = width * bytes; + surface->stageScanline = x11_pad_scanline(surface->stageScanline, xfc->scanline_pad); + size = 1ull * surface->stageScanline * surface->gdi.height; + surface->stage = (BYTE*)winpr_aligned_malloc(size, 16); + + if (!surface->stage) + { + WLog_ERR(TAG, "unable to allocate stage buffer"); + goto out_free_gdidata; + } + + ZeroMemory(surface->stage, size); + WINPR_ASSERT(xfc->depth != 0); + surface->image = + XCreateImage(xfc->display, xfc->visual, xfc->depth, ZPixmap, 0, (char*)surface->stage, + surface->gdi.mappedWidth, surface->gdi.mappedHeight, xfc->scanline_pad, + surface->stageScanline); + } + + if (!surface->image) + { + WLog_ERR(TAG, "an error occurred when creating the XImage"); + goto error_surface_image; + } + + surface->image->byte_order = LSBFirst; + surface->image->bitmap_bit_order = LSBFirst; + + region16_init(&surface->gdi.invalidRegion); + + if (context->SetSurfaceData(context, surface->gdi.surfaceId, (void*)surface) != CHANNEL_RC_OK) + { + WLog_ERR(TAG, "an error occurred during SetSurfaceData"); + goto error_set_surface_data; + } + + return CHANNEL_RC_OK; +error_set_surface_data: + surface->image->data = NULL; + XDestroyImage(surface->image); +error_surface_image: + winpr_aligned_free(surface->stage); +out_free_gdidata: + winpr_aligned_free(surface->gdi.data); +out_free: + free(surface); + return ret; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_DeleteSurface(RdpgfxClientContext* context, + const RDPGFX_DELETE_SURFACE_PDU* deleteSurface) +{ + rdpCodecs* codecs = NULL; + xfGfxSurface* surface = NULL; + UINT status = 0; + EnterCriticalSection(&context->mux); + surface = (xfGfxSurface*)context->GetSurfaceData(context, deleteSurface->surfaceId); + + if (surface) + { + if (surface->gdi.windowMapped) + IFCALL(context->UnmapWindowForSurface, context, surface->gdi.windowId); + +#ifdef WITH_GFX_H264 + h264_context_free(surface->gdi.h264); +#endif + surface->image->data = NULL; + XDestroyImage(surface->image); + winpr_aligned_free(surface->gdi.data); + winpr_aligned_free(surface->stage); + region16_uninit(&surface->gdi.invalidRegion); + codecs = surface->gdi.codecs; + free(surface); + } + + status = context->SetSurfaceData(context, deleteSurface->surfaceId, NULL); + + if (codecs && codecs->progressive) + progressive_delete_surface_context(codecs->progressive, deleteSurface->surfaceId); + + LeaveCriticalSection(&context->mux); + return status; +} + +static UINT xf_UpdateWindowFromSurface(RdpgfxClientContext* context, gdiGfxSurface* surface) +{ + WINPR_ASSERT(context); + WINPR_ASSERT(surface); + + rdpGdi* gdi = (rdpGdi*)context->custom; + WINPR_ASSERT(gdi); + + xfContext* xfc = (xfContext*)gdi->context; + WINPR_ASSERT(gdi->context); + + if (freerdp_settings_get_bool(gdi->context->settings, FreeRDP_RemoteApplicationMode)) + return xf_AppUpdateWindowFromSurface(xfc, surface); + + WLog_WARN(TAG, "function not implemented"); + return CHANNEL_RC_OK; +} + +void xf_graphics_pipeline_init(xfContext* xfc, RdpgfxClientContext* gfx) +{ + rdpGdi* gdi = NULL; + const rdpSettings* settings = NULL; + WINPR_ASSERT(xfc); + WINPR_ASSERT(gfx); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + gdi = xfc->common.context.gdi; + + gdi_graphics_pipeline_init(gdi, gfx); + + if (!freerdp_settings_get_bool(settings, FreeRDP_SoftwareGdi)) + { + gfx->UpdateSurfaces = xf_UpdateSurfaces; + gfx->CreateSurface = xf_CreateSurface; + gfx->DeleteSurface = xf_DeleteSurface; + } + gfx->UpdateWindowFromSurface = xf_UpdateWindowFromSurface; +} + +void xf_graphics_pipeline_uninit(xfContext* xfc, RdpgfxClientContext* gfx) +{ + rdpGdi* gdi = NULL; + + WINPR_ASSERT(xfc); + + gdi = xfc->common.context.gdi; + gdi_graphics_pipeline_uninit(gdi, gfx); +} diff --git a/client/X11/xf_gfx.h b/client/X11/xf_gfx.h new file mode 100644 index 0000000..934e85a --- /dev/null +++ b/client/X11/xf_gfx.h @@ -0,0 +1,45 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Graphics Pipeline + * + * Copyright 2014 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * Copyright 2016 Thincast Technologies GmbH + * Copyright 2016 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_X11_GFX_H +#define FREERDP_CLIENT_X11_GFX_H + +#include "xf_client.h" +#include "xfreerdp.h" + +#include <freerdp/gdi/gfx.h> + +struct xf_gfx_surface +{ + gdiGfxSurface gdi; + BYTE* stage; + UINT32 stageScanline; + XImage* image; +}; +typedef struct xf_gfx_surface xfGfxSurface; + +UINT xf_OutputExpose(xfContext* xfc, UINT32 x, UINT32 y, UINT32 width, UINT32 height); + +void xf_graphics_pipeline_init(xfContext* xfc, RdpgfxClientContext* gfx); + +void xf_graphics_pipeline_uninit(xfContext* xfc, RdpgfxClientContext* gfx); + +#endif /* FREERDP_CLIENT_X11_GFX_H */ diff --git a/client/X11/xf_graphics.c b/client/X11/xf_graphics.c new file mode 100644 index 0000000..10b0eb5 --- /dev/null +++ b/client/X11/xf_graphics.c @@ -0,0 +1,532 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Graphical Objects + * + * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * Copyright 2016 Thincast Technologies GmbH + * Copyright 2016 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <X11/Xlib.h> +#include <X11/Xutil.h> + +#ifdef WITH_XCURSOR +#include <X11/Xcursor/Xcursor.h> +#endif + +#include <float.h> +#include <math.h> + +#include <winpr/crt.h> +#include <winpr/assert.h> + +#include <freerdp/codec/bitmap.h> +#include <freerdp/codec/rfx.h> + +#include "xf_graphics.h" +#include "xf_event.h" + +#include <freerdp/log.h> +#define TAG CLIENT_TAG("x11") + +static BOOL xf_Pointer_Set(rdpContext* context, rdpPointer* pointer); + +BOOL xf_decode_color(xfContext* xfc, const UINT32 srcColor, XColor* color) +{ + UINT32 SrcFormat = 0; + BYTE r = 0; + BYTE g = 0; + BYTE b = 0; + BYTE a = 0; + + if (!xfc || !color) + return FALSE; + + rdpGdi* gdi = xfc->common.context.gdi; + + if (!gdi) + return FALSE; + + rdpSettings* settings = xfc->common.context.settings; + + if (!settings) + return FALSE; + + switch (freerdp_settings_get_uint32(settings, FreeRDP_ColorDepth)) + { + case 32: + case 24: + SrcFormat = PIXEL_FORMAT_BGR24; + break; + + case 16: + SrcFormat = PIXEL_FORMAT_RGB16; + break; + + case 15: + SrcFormat = PIXEL_FORMAT_RGB15; + break; + + case 8: + SrcFormat = PIXEL_FORMAT_RGB8; + break; + + default: + return FALSE; + } + + FreeRDPSplitColor(srcColor, SrcFormat, &r, &g, &b, &a, &gdi->palette); + color->blue = (unsigned short)(b << 8); + color->green = (unsigned short)(g << 8); + color->red = (unsigned short)(r << 8); + color->flags = DoRed | DoGreen | DoBlue; + + if (XAllocColor(xfc->display, xfc->colormap, color) == 0) + return FALSE; + + return TRUE; +} + +static BOOL xf_Pointer_GetCursorForCurrentScale(rdpContext* context, rdpPointer* pointer, + Cursor* cursor) +{ +#if defined(WITH_XCURSOR) && defined(WITH_XRENDER) + xfContext* xfc = (xfContext*)context; + xfPointer* xpointer = (xfPointer*)pointer; + XcursorImage ci = { 0 }; + int cursorIndex = -1; + + if (!context || !pointer || !context->gdi) + return FALSE; + + rdpSettings* settings = xfc->common.context.settings; + + if (!settings) + return FALSE; + + const double xscale = (freerdp_settings_get_bool(settings, FreeRDP_SmartSizing) + ? xfc->scaledWidth / (double)freerdp_settings_get_uint32( + settings, FreeRDP_DesktopWidth) + : 1); + const double yscale = (freerdp_settings_get_bool(settings, FreeRDP_SmartSizing) + ? xfc->scaledHeight / (double)freerdp_settings_get_uint32( + settings, FreeRDP_DesktopHeight) + : 1); + const UINT32 xTargetSize = MAX(1, pointer->width * xscale); + const UINT32 yTargetSize = MAX(1, pointer->height * yscale); + + WLog_DBG(TAG, "scaled: %" PRIu32 "x%" PRIu32 ", desktop: %" PRIu32 "x%" PRIu32, + xfc->scaledWidth, xfc->scaledHeight, + freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth), + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight)); + for (UINT32 i = 0; i < xpointer->nCursors; i++) + { + if ((xpointer->cursorWidths[i] == xTargetSize) && + (xpointer->cursorHeights[i] == yTargetSize)) + { + cursorIndex = i; + } + } + + if (cursorIndex == -1) + { + UINT32 CursorFormat = 0; + xf_lock_x11(xfc); + + if (!xfc->invert) + CursorFormat = (!xfc->big_endian) ? PIXEL_FORMAT_RGBA32 : PIXEL_FORMAT_ABGR32; + else + CursorFormat = (!xfc->big_endian) ? PIXEL_FORMAT_BGRA32 : PIXEL_FORMAT_ARGB32; + + if (xpointer->nCursors == xpointer->mCursors) + { + void* tmp2 = NULL; + xpointer->mCursors = (xpointer->mCursors == 0 ? 1 : xpointer->mCursors * 2); + + tmp2 = realloc(xpointer->cursorWidths, sizeof(UINT32) * xpointer->mCursors); + if (!tmp2) + { + xf_unlock_x11(xfc); + return FALSE; + } + xpointer->cursorWidths = tmp2; + + tmp2 = realloc(xpointer->cursorHeights, sizeof(UINT32) * xpointer->mCursors); + if (!tmp2) + { + xf_unlock_x11(xfc); + return FALSE; + } + xpointer->cursorHeights = (UINT32*)tmp2; + + tmp2 = realloc(xpointer->cursors, sizeof(Cursor) * xpointer->mCursors); + if (!tmp2) + { + xf_unlock_x11(xfc); + return FALSE; + } + xpointer->cursors = (Cursor*)tmp2; + } + + ci.version = XCURSOR_IMAGE_VERSION; + ci.size = sizeof(ci); + ci.width = xTargetSize; + ci.height = yTargetSize; + ci.xhot = pointer->xPos * xscale; + ci.yhot = pointer->yPos * yscale; + const size_t size = 1ull * ci.height * ci.width * FreeRDPGetBytesPerPixel(CursorFormat); + + void* tmp = winpr_aligned_malloc(size, 16); + if (!tmp) + { + xf_unlock_x11(xfc); + return FALSE; + } + ci.pixels = (XcursorPixel*)tmp; + + const double xs = fabs(fabs(xscale) - 1.0); + const double ys = fabs(fabs(yscale) - 1.0); + + WLog_DBG(TAG, + "cursorIndex %" PRId32 " scaling pointer %" PRIu32 "x%" PRIu32 " --> %" PRIu32 + "x%" PRIu32 " [%lfx%lf]", + cursorIndex, pointer->width, pointer->height, ci.width, ci.height, xscale, yscale); + if ((xs > DBL_EPSILON) || (ys > DBL_EPSILON)) + { + if (!freerdp_image_scale((BYTE*)ci.pixels, CursorFormat, 0, 0, 0, ci.width, ci.height, + (BYTE*)xpointer->cursorPixels, CursorFormat, 0, 0, 0, + pointer->width, pointer->height)) + { + winpr_aligned_free(tmp); + xf_unlock_x11(xfc); + return FALSE; + } + } + else + { + ci.pixels = xpointer->cursorPixels; + } + + cursorIndex = xpointer->nCursors; + xpointer->cursorWidths[cursorIndex] = ci.width; + xpointer->cursorHeights[cursorIndex] = ci.height; + xpointer->cursors[cursorIndex] = XcursorImageLoadCursor(xfc->display, &ci); + xpointer->nCursors += 1; + + winpr_aligned_free(tmp); + + xf_unlock_x11(xfc); + } + else + { + WLog_DBG(TAG, "using cached cursor %" PRId32, cursorIndex); + } + + cursor[0] = xpointer->cursors[cursorIndex]; +#endif + return TRUE; +} + +/* Pointer Class */ +static Window xf_Pointer_get_window(xfContext* xfc) +{ + if (!xfc) + { + WLog_WARN(TAG, "xf_Pointer: Invalid context"); + return 0; + } + if (xfc->remote_app) + { + if (!xfc->appWindow) + { + WLog_WARN(TAG, "xf_Pointer: Invalid appWindow"); + return 0; + } + return xfc->appWindow->handle; + } + else + { + if (!xfc->window) + { + WLog_WARN(TAG, "xf_Pointer: Invalid window"); + return 0; + } + return xfc->window->handle; + } +} + +BOOL xf_pointer_update_scale(xfContext* xfc) +{ + xfPointer* pointer = NULL; + WINPR_ASSERT(xfc); + + pointer = xfc->pointer; + if (!pointer) + return TRUE; + + return xf_Pointer_Set(&xfc->common.context, &xfc->pointer->pointer); +} + +static BOOL xf_Pointer_New(rdpContext* context, rdpPointer* pointer) +{ + BOOL rc = FALSE; + +#ifdef WITH_XCURSOR + UINT32 CursorFormat = 0; + size_t size = 0; + xfContext* xfc = (xfContext*)context; + xfPointer* xpointer = (xfPointer*)pointer; + + if (!context || !pointer || !context->gdi) + goto fail; + + if (!xfc->invert) + CursorFormat = (!xfc->big_endian) ? PIXEL_FORMAT_RGBA32 : PIXEL_FORMAT_ABGR32; + else + CursorFormat = (!xfc->big_endian) ? PIXEL_FORMAT_BGRA32 : PIXEL_FORMAT_ARGB32; + + xpointer->nCursors = 0; + xpointer->mCursors = 0; + + size = 1ull * pointer->height * pointer->width * FreeRDPGetBytesPerPixel(CursorFormat); + + if (!(xpointer->cursorPixels = (XcursorPixel*)winpr_aligned_malloc(size, 16))) + goto fail; + + if (!freerdp_image_copy_from_pointer_data( + (BYTE*)xpointer->cursorPixels, CursorFormat, 0, 0, 0, pointer->width, pointer->height, + pointer->xorMaskData, pointer->lengthXorMask, pointer->andMaskData, + pointer->lengthAndMask, pointer->xorBpp, &context->gdi->palette)) + { + winpr_aligned_free(xpointer->cursorPixels); + goto fail; + } + +#endif + + rc = TRUE; + +fail: + WLog_DBG(TAG, "%p", rc ? pointer : NULL); + return rc; +} + +static void xf_Pointer_Free(rdpContext* context, rdpPointer* pointer) +{ + WLog_DBG(TAG, "%p", pointer); + +#ifdef WITH_XCURSOR + xfContext* xfc = (xfContext*)context; + xfPointer* xpointer = (xfPointer*)pointer; + + xf_lock_x11(xfc); + + winpr_aligned_free(xpointer->cursorPixels); + free(xpointer->cursorWidths); + free(xpointer->cursorHeights); + + for (UINT32 i = 0; i < xpointer->nCursors; i++) + { + XFreeCursor(xfc->display, xpointer->cursors[i]); + } + + free(xpointer->cursors); + xpointer->nCursors = 0; + xpointer->mCursors = 0; + + xf_unlock_x11(xfc); +#endif +} + +static BOOL xf_Pointer_Set(rdpContext* context, rdpPointer* pointer) +{ + WLog_DBG(TAG, "%p", pointer); +#ifdef WITH_XCURSOR + xfContext* xfc = (xfContext*)context; + Window handle = xf_Pointer_get_window(xfc); + + WINPR_ASSERT(xfc); + WINPR_ASSERT(pointer); + + xfc->pointer = (xfPointer*)pointer; + + /* in RemoteApp mode, window can be null if none has had focus */ + + if (handle) + { + if (!xf_Pointer_GetCursorForCurrentScale(context, pointer, &(xfc->pointer->cursor))) + return FALSE; + xf_lock_x11(xfc); + XDefineCursor(xfc->display, handle, xfc->pointer->cursor); + xf_unlock_x11(xfc); + } + else + { + WLog_WARN(TAG, "handle=%ld", handle); + } +#endif + return TRUE; +} + +static BOOL xf_Pointer_SetNull(rdpContext* context) +{ + WLog_DBG(TAG, "called"); +#ifdef WITH_XCURSOR + xfContext* xfc = (xfContext*)context; + static Cursor nullcursor = None; + Window handle = xf_Pointer_get_window(xfc); + xf_lock_x11(xfc); + + if (nullcursor == None) + { + XcursorImage ci = { 0 }; + XcursorPixel xp = 0; + + ci.version = XCURSOR_IMAGE_VERSION; + ci.size = sizeof(ci); + ci.width = ci.height = 1; + ci.xhot = ci.yhot = 0; + ci.pixels = &xp; + nullcursor = XcursorImageLoadCursor(xfc->display, &ci); + } + + xfc->pointer = NULL; + + if ((handle) && (nullcursor != None)) + XDefineCursor(xfc->display, handle, nullcursor); + + xf_unlock_x11(xfc); +#endif + return TRUE; +} + +static BOOL xf_Pointer_SetDefault(rdpContext* context) +{ + WLog_DBG(TAG, "called"); +#ifdef WITH_XCURSOR + xfContext* xfc = (xfContext*)context; + Window handle = xf_Pointer_get_window(xfc); + xf_lock_x11(xfc); + xfc->pointer = NULL; + + if (handle) + XUndefineCursor(xfc->display, handle); + + xf_unlock_x11(xfc); +#endif + return TRUE; +} + +static BOOL xf_Pointer_SetPosition(rdpContext* context, UINT32 x, UINT32 y) +{ + xfContext* xfc = (xfContext*)context; + XWindowAttributes current = { 0 }; + XSetWindowAttributes tmp = { 0 }; + BOOL ret = FALSE; + Status rc = 0; + Window handle = xf_Pointer_get_window(xfc); + + if (!handle) + { + WLog_WARN(TAG, "focus %d, handle%lu", xfc->focused, handle); + return TRUE; + } + + WLog_DBG(TAG, "%" PRIu32 "x%" PRIu32, x, y); + if (!xfc->focused) + return TRUE; + + xf_adjust_coordinates_to_screen(xfc, &x, &y); + + xf_lock_x11(xfc); + + rc = XGetWindowAttributes(xfc->display, handle, ¤t); + if (rc == 0) + { + WLog_WARN(TAG, "XGetWindowAttributes==%d", rc); + goto out; + } + + tmp.event_mask = (current.your_event_mask & ~(PointerMotionMask)); + + rc = XChangeWindowAttributes(xfc->display, handle, CWEventMask, &tmp); + if (rc == 0) + { + WLog_WARN(TAG, "XChangeWindowAttributes==%d", rc); + goto out; + } + + rc = XWarpPointer(xfc->display, handle, handle, 0, 0, 0, 0, x, y); + if (rc == 0) + WLog_WARN(TAG, "XWarpPointer==%d", rc); + tmp.event_mask = current.your_event_mask; + rc = XChangeWindowAttributes(xfc->display, handle, CWEventMask, &tmp); + if (rc == 0) + WLog_WARN(TAG, "2.try XChangeWindowAttributes==%d", rc); + ret = TRUE; +out: + xf_unlock_x11(xfc); + return ret; +} + +/* Graphics Module */ +BOOL xf_register_pointer(rdpGraphics* graphics) +{ + rdpPointer pointer = { 0 }; + + pointer.size = sizeof(xfPointer); + pointer.New = xf_Pointer_New; + pointer.Free = xf_Pointer_Free; + pointer.Set = xf_Pointer_Set; + pointer.SetNull = xf_Pointer_SetNull; + pointer.SetDefault = xf_Pointer_SetDefault; + pointer.SetPosition = xf_Pointer_SetPosition; + graphics_register_pointer(graphics, &pointer); + return TRUE; +} + +UINT32 xf_get_local_color_format(xfContext* xfc, BOOL aligned) +{ + UINT32 DstFormat = 0; + BOOL invert = FALSE; + + if (!xfc) + return 0; + + invert = xfc->invert; + + WINPR_ASSERT(xfc->depth != 0); + if (xfc->depth == 32) + DstFormat = (!invert) ? PIXEL_FORMAT_RGBA32 : PIXEL_FORMAT_BGRA32; + else if (xfc->depth == 30) + DstFormat = (!invert) ? PIXEL_FORMAT_RGBX32_DEPTH30 : PIXEL_FORMAT_BGRX32_DEPTH30; + else if (xfc->depth == 24) + { + if (aligned) + DstFormat = (!invert) ? PIXEL_FORMAT_RGBX32 : PIXEL_FORMAT_BGRX32; + else + DstFormat = (!invert) ? PIXEL_FORMAT_RGB24 : PIXEL_FORMAT_BGR24; + } + else if (xfc->depth == 16) + DstFormat = PIXEL_FORMAT_RGB16; + else if (xfc->depth == 15) + DstFormat = PIXEL_FORMAT_RGB15; + else + DstFormat = (!invert) ? PIXEL_FORMAT_RGBX32 : PIXEL_FORMAT_BGRX32; + + return DstFormat; +} diff --git a/client/X11/xf_graphics.h b/client/X11/xf_graphics.h new file mode 100644 index 0000000..a194f43 --- /dev/null +++ b/client/X11/xf_graphics.h @@ -0,0 +1,33 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Graphical Objects + * + * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_X11_GRAPHICS_H +#define FREERDP_CLIENT_X11_GRAPHICS_H + +#include "xf_client.h" +#include "xfreerdp.h" + +BOOL xf_register_pointer(rdpGraphics* graphics); + +BOOL xf_decode_color(xfContext* xfc, const UINT32 srcColor, XColor* color); +UINT32 xf_get_local_color_format(xfContext* xfc, BOOL aligned); + +BOOL xf_pointer_update_scale(xfContext* xfc); + +#endif /* FREERDP_CLIENT_X11_GRAPHICS_H */ diff --git a/client/X11/xf_input.c b/client/X11/xf_input.c new file mode 100644 index 0000000..f1cdc83 --- /dev/null +++ b/client/X11/xf_input.c @@ -0,0 +1,946 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Input + * + * Copyright 2013 Corey Clayton <can.of.tuna@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <X11/Xlib.h> +#include <X11/Xutil.h> + +#ifdef WITH_XCURSOR +#include <X11/Xcursor/Xcursor.h> +#endif + +#ifdef WITH_XI +#include <X11/extensions/XInput2.h> +#endif + +#include <math.h> +#include <float.h> +#include <limits.h> + +#include "xf_event.h" +#include "xf_input.h" + +#include <winpr/assert.h> +#include <winpr/wtypes.h> +#include <freerdp/log.h> +#define TAG CLIENT_TAG("x11") + +#ifdef WITH_XI + +#define PAN_THRESHOLD 50 +#define ZOOM_THRESHOLD 10 + +#define MIN_FINGER_DIST 5 + +static int xf_input_event(xfContext* xfc, const XEvent* xevent, XIDeviceEvent* event, int evtype); + +#ifdef DEBUG_XINPUT +static const char* xf_input_get_class_string(int class) +{ + if (class == XIKeyClass) + return "XIKeyClass"; + else if (class == XIButtonClass) + return "XIButtonClass"; + else if (class == XIValuatorClass) + return "XIValuatorClass"; + else if (class == XIScrollClass) + return "XIScrollClass"; + else if (class == XITouchClass) + return "XITouchClass"; + + return "XIUnknownClass"; +} +#endif + +static BOOL register_input_events(xfContext* xfc, Window window) +{ +#define MAX_NR_MASKS 64 + int ndevices = 0; + int nmasks = 0; + XIEventMask evmasks[MAX_NR_MASKS] = { 0 }; + BYTE masks[MAX_NR_MASKS][XIMaskLen(XI_LASTEVENT)] = { 0 }; + + WINPR_ASSERT(xfc); + + rdpSettings* settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + XIDeviceInfo* info = XIQueryDevice(xfc->display, XIAllDevices, &ndevices); + + for (int i = 0; i < MIN(ndevices, MAX_NR_MASKS); i++) + { + BOOL used = FALSE; + XIDeviceInfo* dev = &info[i]; + + evmasks[nmasks].mask = masks[nmasks]; + evmasks[nmasks].mask_len = sizeof(masks[0]); + evmasks[nmasks].deviceid = dev->deviceid; + + /* Ignore virtual core pointer */ + if (strcmp(dev->name, "Virtual core pointer") == 0) + continue; + + for (int j = 0; j < dev->num_classes; j++) + { + const XIAnyClassInfo* class = dev->classes[j]; + + switch (class->type) + { + case XITouchClass: + if (freerdp_settings_get_bool(settings, FreeRDP_MultiTouchInput)) + { + const XITouchClassInfo* t = (const XITouchClassInfo*)class; + if (t->mode == XIDirectTouch) + { + WLog_DBG( + TAG, + "%s %s touch device (id: %d, mode: %d), supporting %d touches.", + dev->name, (t->mode == XIDirectTouch) ? "direct" : "dependent", + dev->deviceid, t->mode, t->num_touches); + XISetMask(masks[nmasks], XI_TouchBegin); + XISetMask(masks[nmasks], XI_TouchUpdate); + XISetMask(masks[nmasks], XI_TouchEnd); + } + } + break; + case XIButtonClass: + { + const XIButtonClassInfo* t = (const XIButtonClassInfo*)class; + WLog_DBG(TAG, "%s button device (id: %d, mode: %d)", dev->name, dev->deviceid, + t->num_buttons); + XISetMask(masks[nmasks], XI_ButtonPress); + XISetMask(masks[nmasks], XI_ButtonRelease); + XISetMask(masks[nmasks], XI_Motion); + used = TRUE; + break; + } + case XIValuatorClass: + { + const XIValuatorClassInfo* t = (const XIValuatorClassInfo*)class; + char* name = t->label ? XGetAtomName(xfc->display, t->label) : NULL; + + WLog_DBG(TAG, "%s device (id: %d) valuator %d label %s range %f - %f", + dev->name, dev->deviceid, t->number, name ? name : "None", t->min, + t->max); + free(name); + + if (t->number == 2) + { + double max_pressure = t->max; + + char devName[200] = { 0 }; + strncpy(devName, dev->name, ARRAYSIZE(devName) - 1); + CharLowerBuffA(devName, ARRAYSIZE(devName)); + + if (strstr(devName, "eraser") != NULL) + { + if (freerdp_client_handle_pen(&xfc->common, + FREERDP_PEN_REGISTER | + FREERDP_PEN_IS_INVERTED | + FREERDP_PEN_HAS_PRESSURE, + dev->deviceid, max_pressure)) + WLog_DBG(TAG, "registered eraser"); + } + else if (strstr(devName, "stylus") != NULL || + strstr(devName, "pen") != NULL) + { + if (freerdp_client_handle_pen( + &xfc->common, FREERDP_PEN_REGISTER | FREERDP_PEN_HAS_PRESSURE, + dev->deviceid, max_pressure)) + WLog_DBG(TAG, "registered pen"); + } + } + break; + } + default: + break; + } + } + if (used) + nmasks++; + } + + XIFreeDeviceInfo(info); + + if (nmasks > 0) + { + Status xstatus = XISelectEvents(xfc->display, window, evmasks, nmasks); + if (xstatus != 0) + WLog_WARN(TAG, "XISelectEvents returned %d", xstatus); + } + + return TRUE; +} + +static BOOL register_raw_events(xfContext* xfc, Window window) +{ + XIEventMask mask; + unsigned char mask_bytes[XIMaskLen(XI_LASTEVENT)] = { 0 }; + rdpSettings* settings = NULL; + + WINPR_ASSERT(xfc); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + if (freerdp_client_use_relative_mouse_events(&xfc->common)) + { + XISetMask(mask_bytes, XI_RawMotion); + XISetMask(mask_bytes, XI_RawButtonPress); + XISetMask(mask_bytes, XI_RawButtonRelease); + + mask.deviceid = XIAllMasterDevices; + mask.mask_len = sizeof(mask_bytes); + mask.mask = mask_bytes; + + XISelectEvents(xfc->display, window, &mask, 1); + } + + return TRUE; +} + +static BOOL register_device_events(xfContext* xfc, Window window) +{ + XIEventMask mask; + unsigned char mask_bytes[XIMaskLen(XI_LASTEVENT)] = { 0 }; + rdpSettings* settings = NULL; + + WINPR_ASSERT(xfc); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + XISetMask(mask_bytes, XI_DeviceChanged); + XISetMask(mask_bytes, XI_HierarchyChanged); + + mask.deviceid = XIAllDevices; + mask.mask_len = sizeof(mask_bytes); + mask.mask = mask_bytes; + + XISelectEvents(xfc->display, window, &mask, 1); + + return TRUE; +} + +int xf_input_init(xfContext* xfc, Window window) +{ + int major = XI_2_Major; + int minor = XI_2_Minor; + int opcode = 0; + int event = 0; + int error = 0; + + WINPR_ASSERT(xfc); + + rdpSettings* settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + xfc->firstDist = -1.0; + xfc->z_vector = 0; + xfc->px_vector = 0; + xfc->py_vector = 0; + xfc->active_contacts = 0; + + if (!XQueryExtension(xfc->display, "XInputExtension", &opcode, &event, &error)) + { + WLog_WARN(TAG, "XInput extension not available."); + return -1; + } + + xfc->XInputOpcode = opcode; + XIQueryVersion(xfc->display, &major, &minor); + + if ((major < XI_2_Major) || ((major == XI_2_Major) && (minor < 2))) + { + WLog_WARN(TAG, "Server does not support XI 2.2"); + return -1; + } + else + { + int scr = DefaultScreen(xfc->display); + Window root = RootWindow(xfc->display, scr); + + if (!register_raw_events(xfc, root)) + return -1; + if (!register_input_events(xfc, window)) + return -1; + if (!register_device_events(xfc, window)) + return -1; + } + + return 0; +} + +static BOOL xf_input_is_duplicate(xfContext* xfc, const XGenericEventCookie* cookie) +{ + const XIDeviceEvent* event = NULL; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(cookie); + + event = cookie->data; + WINPR_ASSERT(event); + + if ((xfc->lastEvent.time == event->time) && (xfc->lastEvType == cookie->evtype) && + (xfc->lastEvent.detail == event->detail) && + (fabs(xfc->lastEvent.event_x - event->event_x) < DBL_EPSILON) && + (fabs(xfc->lastEvent.event_y - event->event_y) < DBL_EPSILON)) + { + return TRUE; + } + + return FALSE; +} + +static void xf_input_save_last_event(xfContext* xfc, const XGenericEventCookie* cookie) +{ + const XIDeviceEvent* event = NULL; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(cookie); + + event = cookie->data; + WINPR_ASSERT(event); + + xfc->lastEvType = cookie->evtype; + xfc->lastEvent.time = event->time; + xfc->lastEvent.detail = event->detail; + xfc->lastEvent.event_x = event->event_x; + xfc->lastEvent.event_y = event->event_y; +} + +static void xf_input_detect_pan(xfContext* xfc) +{ + double dx[2]; + double dy[2]; + double px = NAN; + double py = NAN; + double dist_x = NAN; + double dist_y = NAN; + rdpContext* ctx = NULL; + + WINPR_ASSERT(xfc); + ctx = &xfc->common.context; + WINPR_ASSERT(ctx); + + if (xfc->active_contacts != 2) + { + return; + } + + dx[0] = xfc->contacts[0].pos_x - xfc->contacts[0].last_x; + dx[1] = xfc->contacts[1].pos_x - xfc->contacts[1].last_x; + dy[0] = xfc->contacts[0].pos_y - xfc->contacts[0].last_y; + dy[1] = xfc->contacts[1].pos_y - xfc->contacts[1].last_y; + px = fabs(dx[0]) < fabs(dx[1]) ? dx[0] : dx[1]; + py = fabs(dy[0]) < fabs(dy[1]) ? dy[0] : dy[1]; + xfc->px_vector += px; + xfc->py_vector += py; + dist_x = fabs(xfc->contacts[0].pos_x - xfc->contacts[1].pos_x); + dist_y = fabs(xfc->contacts[0].pos_y - xfc->contacts[1].pos_y); + + if (dist_y > MIN_FINGER_DIST) + { + if (xfc->px_vector > PAN_THRESHOLD) + { + { + PanningChangeEventArgs e; + EventArgsInit(&e, "xfreerdp"); + e.dx = 5; + e.dy = 0; + PubSub_OnPanningChange(ctx->pubSub, xfc, &e); + } + xfc->px_vector = 0; + xfc->py_vector = 0; + xfc->z_vector = 0; + } + else if (xfc->px_vector < -PAN_THRESHOLD) + { + { + PanningChangeEventArgs e; + EventArgsInit(&e, "xfreerdp"); + e.dx = -5; + e.dy = 0; + PubSub_OnPanningChange(ctx->pubSub, xfc, &e); + } + xfc->px_vector = 0; + xfc->py_vector = 0; + xfc->z_vector = 0; + } + } + + if (dist_x > MIN_FINGER_DIST) + { + if (xfc->py_vector > PAN_THRESHOLD) + { + { + PanningChangeEventArgs e; + EventArgsInit(&e, "xfreerdp"); + e.dx = 0; + e.dy = 5; + PubSub_OnPanningChange(ctx->pubSub, xfc, &e); + } + xfc->py_vector = 0; + xfc->px_vector = 0; + xfc->z_vector = 0; + } + else if (xfc->py_vector < -PAN_THRESHOLD) + { + { + PanningChangeEventArgs e; + EventArgsInit(&e, "xfreerdp"); + e.dx = 0; + e.dy = -5; + PubSub_OnPanningChange(ctx->pubSub, xfc, &e); + } + xfc->py_vector = 0; + xfc->px_vector = 0; + xfc->z_vector = 0; + } + } +} + +static void xf_input_detect_pinch(xfContext* xfc) +{ + double dist = NAN; + double delta = NAN; + ZoomingChangeEventArgs e; + rdpContext* ctx = NULL; + + WINPR_ASSERT(xfc); + ctx = &xfc->common.context; + WINPR_ASSERT(ctx); + + if (xfc->active_contacts != 2) + { + xfc->firstDist = -1.0; + return; + } + + /* first calculate the distance */ + dist = sqrt(pow(xfc->contacts[1].pos_x - xfc->contacts[0].last_x, 2.0) + + pow(xfc->contacts[1].pos_y - xfc->contacts[0].last_y, 2.0)); + + /* if this is the first 2pt touch */ + if (xfc->firstDist <= 0) + { + xfc->firstDist = dist; + xfc->lastDist = xfc->firstDist; + xfc->z_vector = 0; + xfc->px_vector = 0; + xfc->py_vector = 0; + } + else + { + delta = xfc->lastDist - dist; + + if (delta > 1.0) + delta = 1.0; + + if (delta < -1.0) + delta = -1.0; + + /* compare the current distance to the first one */ + xfc->z_vector += delta; + xfc->lastDist = dist; + + if (xfc->z_vector > ZOOM_THRESHOLD) + { + EventArgsInit(&e, "xfreerdp"); + e.dx = e.dy = -10; + PubSub_OnZoomingChange(ctx->pubSub, xfc, &e); + xfc->z_vector = 0; + xfc->px_vector = 0; + xfc->py_vector = 0; + } + + if (xfc->z_vector < -ZOOM_THRESHOLD) + { + EventArgsInit(&e, "xfreerdp"); + e.dx = e.dy = 10; + PubSub_OnZoomingChange(ctx->pubSub, xfc, &e); + xfc->z_vector = 0; + xfc->px_vector = 0; + xfc->py_vector = 0; + } + } +} + +static void xf_input_touch_begin(xfContext* xfc, const XIDeviceEvent* event) +{ + WINPR_UNUSED(xfc); + for (int i = 0; i < MAX_CONTACTS; i++) + { + if (xfc->contacts[i].id == 0) + { + xfc->contacts[i].id = event->detail; + xfc->contacts[i].count = 1; + xfc->contacts[i].pos_x = event->event_x; + xfc->contacts[i].pos_y = event->event_y; + xfc->active_contacts++; + break; + } + } +} + +static void xf_input_touch_update(xfContext* xfc, const XIDeviceEvent* event) +{ + WINPR_ASSERT(xfc); + WINPR_ASSERT(event); + + for (int i = 0; i < MAX_CONTACTS; i++) + { + if (xfc->contacts[i].id == event->detail) + { + xfc->contacts[i].count++; + xfc->contacts[i].last_x = xfc->contacts[i].pos_x; + xfc->contacts[i].last_y = xfc->contacts[i].pos_y; + xfc->contacts[i].pos_x = event->event_x; + xfc->contacts[i].pos_y = event->event_y; + xf_input_detect_pinch(xfc); + xf_input_detect_pan(xfc); + break; + } + } +} + +static void xf_input_touch_end(xfContext* xfc, const XIDeviceEvent* event) +{ + WINPR_UNUSED(xfc); + for (int i = 0; i < MAX_CONTACTS; i++) + { + if (xfc->contacts[i].id == event->detail) + { + xfc->contacts[i].id = 0; + xfc->contacts[i].count = 0; + xfc->active_contacts--; + break; + } + } +} + +static int xf_input_handle_event_local(xfContext* xfc, const XEvent* event) +{ + union + { + const XGenericEventCookie* cc; + XGenericEventCookie* vc; + } cookie; + cookie.cc = &event->xcookie; + XGetEventData(xfc->display, cookie.vc); + + if ((cookie.cc->type == GenericEvent) && (cookie.cc->extension == xfc->XInputOpcode)) + { + switch (cookie.cc->evtype) + { + case XI_TouchBegin: + if (xf_input_is_duplicate(xfc, cookie.cc) == FALSE) + xf_input_touch_begin(xfc, cookie.cc->data); + + xf_input_save_last_event(xfc, cookie.cc); + break; + + case XI_TouchUpdate: + if (xf_input_is_duplicate(xfc, cookie.cc) == FALSE) + xf_input_touch_update(xfc, cookie.cc->data); + + xf_input_save_last_event(xfc, cookie.cc); + break; + + case XI_TouchEnd: + if (xf_input_is_duplicate(xfc, cookie.cc) == FALSE) + xf_input_touch_end(xfc, cookie.cc->data); + + xf_input_save_last_event(xfc, cookie.cc); + break; + + default: + xf_input_event(xfc, event, cookie.cc->data, cookie.cc->evtype); + break; + } + } + + XFreeEventData(xfc->display, cookie.vc); + return 0; +} + +#ifdef WITH_DEBUG_X11 +static char* xf_input_touch_state_string(DWORD flags) +{ + if (flags & RDPINPUT_CONTACT_FLAG_DOWN) + return "RDPINPUT_CONTACT_FLAG_DOWN"; + else if (flags & RDPINPUT_CONTACT_FLAG_UPDATE) + return "RDPINPUT_CONTACT_FLAG_UPDATE"; + else if (flags & RDPINPUT_CONTACT_FLAG_UP) + return "RDPINPUT_CONTACT_FLAG_UP"; + else if (flags & RDPINPUT_CONTACT_FLAG_INRANGE) + return "RDPINPUT_CONTACT_FLAG_INRANGE"; + else if (flags & RDPINPUT_CONTACT_FLAG_INCONTACT) + return "RDPINPUT_CONTACT_FLAG_INCONTACT"; + else if (flags & RDPINPUT_CONTACT_FLAG_CANCELED) + return "RDPINPUT_CONTACT_FLAG_CANCELED"; + else + return "RDPINPUT_CONTACT_FLAG_UNKNOWN"; +} +#endif + +static void xf_input_hide_cursor(xfContext* xfc) +{ +#ifdef WITH_XCURSOR + + if (!xfc->cursorHidden) + { + XcursorImage ci = { 0 }; + XcursorPixel xp = 0; + static Cursor nullcursor = None; + xf_lock_x11(xfc); + ci.version = XCURSOR_IMAGE_VERSION; + ci.size = sizeof(ci); + ci.width = ci.height = 1; + ci.xhot = ci.yhot = 0; + ci.pixels = &xp; + nullcursor = XcursorImageLoadCursor(xfc->display, &ci); + + if ((xfc->window) && (nullcursor != None)) + XDefineCursor(xfc->display, xfc->window->handle, nullcursor); + + xfc->cursorHidden = TRUE; + xf_unlock_x11(xfc); + } + +#endif +} + +static void xf_input_show_cursor(xfContext* xfc) +{ +#ifdef WITH_XCURSOR + xf_lock_x11(xfc); + + if (xfc->cursorHidden) + { + if (xfc->window) + { + if (!xfc->pointer) + XUndefineCursor(xfc->display, xfc->window->handle); + else + XDefineCursor(xfc->display, xfc->window->handle, xfc->pointer->cursor); + } + + xfc->cursorHidden = FALSE; + } + + xf_unlock_x11(xfc); +#endif +} + +static int xf_input_touch_remote(xfContext* xfc, XIDeviceEvent* event, int evtype) +{ + int x = 0; + int y = 0; + int touchId = 0; + RdpeiClientContext* rdpei = xfc->common.rdpei; + + if (!rdpei) + return 0; + + xf_input_hide_cursor(xfc); + touchId = event->detail; + x = (int)event->event_x; + y = (int)event->event_y; + xf_event_adjust_coordinates(xfc, &x, &y); + + switch (evtype) + { + case XI_TouchBegin: + freerdp_client_handle_touch(&xfc->common, FREERDP_TOUCH_DOWN, touchId, 0, x, y); + break; + case XI_TouchUpdate: + freerdp_client_handle_touch(&xfc->common, FREERDP_TOUCH_MOTION, touchId, 0, x, y); + break; + case XI_TouchEnd: + freerdp_client_handle_touch(&xfc->common, FREERDP_TOUCH_UP, touchId, 0, x, y); + break; + default: + break; + } + + return 0; +} + +static BOOL xf_input_pen_remote(xfContext* xfc, XIDeviceEvent* event, int evtype, int deviceid) +{ + int x = 0; + int y = 0; + RdpeiClientContext* rdpei = xfc->common.rdpei; + + if (!rdpei) + return FALSE; + + xf_input_hide_cursor(xfc); + x = (int)event->event_x; + y = (int)event->event_y; + xf_event_adjust_coordinates(xfc, &x, &y); + + double pressure = 0.0; + double* val = event->valuators.values; + for (int i = 0; i < MIN(event->valuators.mask_len * 8, 3); i++) + { + if (XIMaskIsSet(event->valuators.mask, i)) + { + double value = *val++; + if (i == 2) + pressure = value; + } + } + + UINT32 flags = FREERDP_PEN_HAS_PRESSURE; + if ((evtype == XI_ButtonPress) || (evtype == XI_ButtonRelease)) + { + WLog_DBG(TAG, "pen button %d", event->detail); + switch (event->detail) + { + case 1: + break; + case 3: + flags |= FREERDP_PEN_BARREL_PRESSED; + break; + default: + return FALSE; + } + } + + switch (evtype) + { + case XI_ButtonPress: + flags |= FREERDP_PEN_PRESS; + if (!freerdp_client_handle_pen(&xfc->common, flags, deviceid, x, y, pressure)) + return FALSE; + break; + case XI_Motion: + flags |= FREERDP_PEN_MOTION; + if (!freerdp_client_handle_pen(&xfc->common, flags, deviceid, x, y, pressure)) + return FALSE; + break; + case XI_ButtonRelease: + flags |= FREERDP_PEN_RELEASE; + if (!freerdp_client_handle_pen(&xfc->common, flags, deviceid, x, y, pressure)) + return FALSE; + break; + default: + break; + } + return TRUE; +} + +static int xf_input_pens_unhover(xfContext* xfc) +{ + WINPR_ASSERT(xfc); + + freerdp_client_pen_cancel_all(&xfc->common); + return 0; +} + +int xf_input_event(xfContext* xfc, const XEvent* xevent, XIDeviceEvent* event, int evtype) +{ + const rdpSettings* settings = NULL; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(xevent); + WINPR_ASSERT(event); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + xfWindow* window = xfc->window; + if (window) + { + if (xf_floatbar_is_locked(window->floatbar)) + return 0; + } + + xf_input_show_cursor(xfc); + + switch (evtype) + { + case XI_ButtonPress: + xfc->xi_event = TRUE; + xf_generic_ButtonEvent(xfc, (int)event->event_x, (int)event->event_y, event->detail, + event->event, xfc->remote_app, TRUE); + break; + + case XI_ButtonRelease: + xfc->xi_event = TRUE; + xf_generic_ButtonEvent(xfc, (int)event->event_x, (int)event->event_y, event->detail, + event->event, xfc->remote_app, FALSE); + break; + + case XI_Motion: + xfc->xi_event = TRUE; + xf_generic_MotionNotify(xfc, (int)event->event_x, (int)event->event_y, event->detail, + event->event, xfc->remote_app); + break; + case XI_RawButtonPress: + case XI_RawButtonRelease: + xfc->xi_rawevent = + xfc->common.mouse_grabbed && freerdp_client_use_relative_mouse_events(&xfc->common); + + if (xfc->xi_rawevent) + { + const XIRawEvent* ev = (const XIRawEvent*)event; + xf_generic_RawButtonEvent(xfc, ev->detail, xfc->remote_app, + evtype == XI_RawButtonPress); + } + break; + case XI_RawMotion: + xfc->xi_rawevent = + xfc->common.mouse_grabbed && freerdp_client_use_relative_mouse_events(&xfc->common); + + if (xfc->xi_rawevent) + { + const XIRawEvent* ev = (const XIRawEvent*)event; + double x = 0.0; + double y = 0.0; + if (XIMaskIsSet(ev->valuators.mask, 0)) + x = ev->raw_values[0]; + if (XIMaskIsSet(ev->valuators.mask, 1)) + y = ev->raw_values[1]; + + xf_generic_RawMotionNotify(xfc, (int)x, (int)y, event->event, xfc->remote_app); + } + break; + case XI_DeviceChanged: + { + const XIDeviceChangedEvent* ev = (const XIDeviceChangedEvent*)event; + if (ev->reason != XIDeviceChange) + break; + + /* + * TODO: + * 1. Register only changed devices. + * 2. Both `XIDeviceChangedEvent` and `XIHierarchyEvent` have no target + * `Window` which is used to register xinput events. So assume + * `xfc->window` created by `xf_CreateDesktopWindow` is the same + * `Window` we registered. + */ + if (xfc->window) + register_input_events(xfc, xfc->window->handle); + } + break; + case XI_HierarchyChanged: + if (xfc->window) + register_input_events(xfc, xfc->window->handle); + break; + + default: + WLog_WARN(TAG, "Unhandled event %d: Event was registered but is not handled!", evtype); + break; + } + + return 0; +} + +static int xf_input_handle_event_remote(xfContext* xfc, const XEvent* event) +{ + union + { + const XGenericEventCookie* cc; + XGenericEventCookie* vc; + } cookie; + cookie.cc = &event->xcookie; + XGetEventData(xfc->display, cookie.vc); + + if ((cookie.cc->type == GenericEvent) && (cookie.cc->extension == xfc->XInputOpcode)) + { + switch (cookie.cc->evtype) + { + case XI_TouchBegin: + xf_input_pens_unhover(xfc); + /* fallthrough */ + WINPR_FALLTHROUGH + case XI_TouchUpdate: + case XI_TouchEnd: + xf_input_touch_remote(xfc, cookie.cc->data, cookie.cc->evtype); + break; + case XI_ButtonPress: + case XI_Motion: + case XI_ButtonRelease: + { + WLog_DBG(TAG, "checking for pen"); + XIDeviceEvent* deviceEvent = (XIDeviceEvent*)cookie.cc->data; + int deviceid = deviceEvent->deviceid; + + if (freerdp_client_is_pen(&xfc->common, deviceid)) + { + if (!xf_input_pen_remote(xfc, cookie.cc->data, cookie.cc->evtype, deviceid)) + { + // XXX: don't show cursor + xf_input_event(xfc, event, cookie.cc->data, cookie.cc->evtype); + } + break; + } + } + /* fallthrough */ + WINPR_FALLTHROUGH + default: + xf_input_pens_unhover(xfc); + xf_input_event(xfc, event, cookie.cc->data, cookie.cc->evtype); + break; + } + } + + XFreeEventData(xfc->display, cookie.vc); + return 0; +} + +#else + +int xf_input_init(xfContext* xfc, Window window) +{ + return 0; +} + +#endif + +int xf_input_handle_event(xfContext* xfc, const XEvent* event) +{ +#ifdef WITH_XI + const rdpSettings* settings = NULL; + WINPR_ASSERT(xfc); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + if (freerdp_settings_get_bool(settings, FreeRDP_MultiTouchInput)) + { + return xf_input_handle_event_remote(xfc, event); + } + else if (freerdp_settings_get_bool(settings, FreeRDP_MultiTouchGestures)) + { + return xf_input_handle_event_local(xfc, event); + } + else + { + return xf_input_handle_event_local(xfc, event); + } + +#else + return 0; +#endif +} diff --git a/client/X11/xf_input.h b/client/X11/xf_input.h new file mode 100644 index 0000000..a961512 --- /dev/null +++ b/client/X11/xf_input.h @@ -0,0 +1,33 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Input + * + * Copyright 2013 Corey Clayton <can.of.tuna@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_X11_INPUT_H +#define FREERDP_CLIENT_X11_INPUT_H + +#include "xf_client.h" +#include "xfreerdp.h" + +#ifdef WITH_XI +#include <X11/extensions/XInput2.h> +#endif + +int xf_input_init(xfContext* xfc, Window window); +int xf_input_handle_event(xfContext* xfc, const XEvent* event); + +#endif /* FREERDP_CLIENT_X11_INPUT_H */ diff --git a/client/X11/xf_keyboard.c b/client/X11/xf_keyboard.c new file mode 100644 index 0000000..9b575c2 --- /dev/null +++ b/client/X11/xf_keyboard.c @@ -0,0 +1,746 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Keyboard Handling + * + * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <winpr/crt.h> +#include <winpr/path.h> +#include <winpr/assert.h> +#include <winpr/collections.h> + +#include <freerdp/utils/string.h> + +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <X11/keysym.h> +#include <X11/XKBlib.h> + +#include <freerdp/locale/keyboard.h> + +#include "xf_event.h" + +#include "xf_keyboard.h" + +#include <freerdp/log.h> +#define TAG CLIENT_TAG("x11") + +static void xf_keyboard_modifier_map_free(xfContext* xfc) +{ + WINPR_ASSERT(xfc); + if (xfc->modifierMap) + { + XFreeModifiermap(xfc->modifierMap); + xfc->modifierMap = NULL; + } +} + +BOOL xf_keyboard_update_modifier_map(xfContext* xfc) +{ + WINPR_ASSERT(xfc); + xf_keyboard_modifier_map_free(xfc); + xfc->modifierMap = XGetModifierMapping(xfc->display); + return xfc->modifierMap != NULL; +} + +static void xf_keyboard_send_key(xfContext* xfc, BOOL down, BOOL repeat, const XKeyEvent* ev); + +static BOOL xf_sync_kbd_state(xfContext* xfc) +{ + const UINT32 syncFlags = xf_keyboard_get_toggle_keys_state(xfc); + + WINPR_ASSERT(xfc); + return freerdp_input_send_synchronize_event(xfc->common.context.input, syncFlags); +} + +static void xf_keyboard_clear(xfContext* xfc) +{ + WINPR_ASSERT(xfc); + ZeroMemory(xfc->KeyboardState, sizeof(xfc->KeyboardState)); +} + +static BOOL xf_keyboard_action_script_init(xfContext* xfc) +{ + wObject* obj = NULL; + FILE* keyScript = NULL; + char buffer[1024] = { 0 }; + char command[1024] = { 0 }; + const rdpSettings* settings = NULL; + const char* ActionScript = NULL; + WINPR_ASSERT(xfc); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + ActionScript = freerdp_settings_get_string(settings, FreeRDP_ActionScript); + xfc->actionScriptExists = winpr_PathFileExists(ActionScript); + + if (!xfc->actionScriptExists) + return FALSE; + + xfc->keyCombinations = ArrayList_New(TRUE); + + if (!xfc->keyCombinations) + return FALSE; + + obj = ArrayList_Object(xfc->keyCombinations); + WINPR_ASSERT(obj); + obj->fnObjectNew = winpr_ObjectStringClone; + obj->fnObjectFree = winpr_ObjectStringFree; + sprintf_s(command, sizeof(command), "%s key", ActionScript); + keyScript = popen(command, "r"); + + if (!keyScript) + { + xfc->actionScriptExists = FALSE; + return FALSE; + } + + while (fgets(buffer, sizeof(buffer), keyScript) != NULL) + { + char* context = NULL; + strtok_s(buffer, "\n", &context); + + if (!buffer || !ArrayList_Append(xfc->keyCombinations, buffer)) + { + ArrayList_Free(xfc->keyCombinations); + xfc->actionScriptExists = FALSE; + pclose(keyScript); + return FALSE; + } + } + + pclose(keyScript); + return xf_event_action_script_init(xfc); +} + +static void xf_keyboard_action_script_free(xfContext* xfc) +{ + xf_event_action_script_free(xfc); + + if (xfc->keyCombinations) + { + ArrayList_Free(xfc->keyCombinations); + xfc->keyCombinations = NULL; + xfc->actionScriptExists = FALSE; + } +} + +BOOL xf_keyboard_init(xfContext* xfc) +{ + rdpSettings* settings = NULL; + + WINPR_ASSERT(xfc); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + xf_keyboard_clear(xfc); + xfc->KeyboardLayout = freerdp_settings_get_uint32(settings, FreeRDP_KeyboardLayout); + xfc->KeyboardLayout = freerdp_keyboard_init_ex( + xfc->KeyboardLayout, freerdp_settings_get_string(settings, FreeRDP_KeyboardRemappingList)); + if (!freerdp_settings_set_uint32(settings, FreeRDP_KeyboardLayout, xfc->KeyboardLayout)) + return FALSE; + + if (!xf_keyboard_update_modifier_map(xfc)) + return FALSE; + + xf_keyboard_action_script_init(xfc); + return TRUE; +} + +void xf_keyboard_free(xfContext* xfc) +{ + xf_keyboard_modifier_map_free(xfc); + xf_keyboard_action_script_free(xfc); +} + +void xf_keyboard_key_press(xfContext* xfc, const XKeyEvent* event, KeySym keysym) +{ + BOOL last = 0; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(event); + WINPR_ASSERT(event->keycode <= ARRAYSIZE(xfc->KeyboardState)); + + last = xfc->KeyboardState[event->keycode]; + xfc->KeyboardState[event->keycode] = TRUE; + + if (xf_keyboard_handle_special_keys(xfc, keysym)) + return; + + xf_keyboard_send_key(xfc, TRUE, last, event); +} + +void xf_keyboard_key_release(xfContext* xfc, const XKeyEvent* event, KeySym keysym) +{ + WINPR_ASSERT(xfc); + WINPR_ASSERT(event); + WINPR_ASSERT(event->keycode <= ARRAYSIZE(xfc->KeyboardState)); + + BOOL last = xfc->KeyboardState[event->keycode]; + xfc->KeyboardState[event->keycode] = FALSE; + xf_keyboard_handle_special_keys_release(xfc, keysym); + xf_keyboard_send_key(xfc, FALSE, last, event); +} + +void xf_keyboard_release_all_keypress(xfContext* xfc) +{ + WINPR_ASSERT(xfc); + + for (size_t keycode = 0; keycode < ARRAYSIZE(xfc->KeyboardState); keycode++) + { + if (xfc->KeyboardState[keycode]) + { + const DWORD rdp_scancode = freerdp_keyboard_get_rdp_scancode_from_x11_keycode(keycode); + + // release tab before releasing the windows key. + // this stops the start menu from opening on unfocus event. + if (rdp_scancode == RDP_SCANCODE_LWIN) + freerdp_input_send_keyboard_event_ex(xfc->common.context.input, FALSE, FALSE, + RDP_SCANCODE_TAB); + + freerdp_input_send_keyboard_event_ex(xfc->common.context.input, FALSE, FALSE, + rdp_scancode); + xfc->KeyboardState[keycode] = FALSE; + } + } + xf_sync_kbd_state(xfc); +} + +BOOL xf_keyboard_key_pressed(xfContext* xfc, KeySym keysym) +{ + KeyCode keycode = XKeysymToKeycode(xfc->display, keysym); + WINPR_ASSERT(keycode <= ARRAYSIZE(xfc->KeyboardState)); + return xfc->KeyboardState[keycode]; +} + +void xf_keyboard_send_key(xfContext* xfc, BOOL down, BOOL repeat, const XKeyEvent* event) +{ + DWORD rdp_scancode = 0; + rdpInput* input = NULL; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(event); + + input = xfc->common.context.input; + WINPR_ASSERT(input); + + rdp_scancode = freerdp_keyboard_get_rdp_scancode_from_x11_keycode(event->keycode); + if (rdp_scancode == RDP_SCANCODE_PAUSE && !xf_keyboard_key_pressed(xfc, XK_Control_L) && + !xf_keyboard_key_pressed(xfc, XK_Control_R)) + { + /* Pause without Ctrl has to be sent as a series of keycodes + * in a single input PDU. Pause only happens on "press"; + * no code is sent on "release". + */ + if (down) + { + freerdp_input_send_keyboard_pause_event(input); + } + } + else + { + if (freerdp_settings_get_bool(xfc->common.context.settings, FreeRDP_UnicodeInput)) + { + wchar_t buffer[32] = { 0 }; + int xwc = -1; + + switch (rdp_scancode) + { + case RDP_SCANCODE_RETURN: + break; + default: + { + XIM xim = XOpenIM(xfc->display, 0, 0, 0); + XIC xic = + XCreateIC(xim, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, NULL); + + KeySym ignore = { 0 }; + Status return_status = 0; + XKeyEvent ev = *event; + ev.type = KeyPress; + xwc = XwcLookupString(xic, &ev, buffer, ARRAYSIZE(buffer), &ignore, + &return_status); + } + break; + } + + if (xwc < 1) + { + if (rdp_scancode == RDP_SCANCODE_UNKNOWN) + WLog_ERR(TAG, "Unknown key with X keycode 0x%02" PRIx8 "", event->keycode); + else + freerdp_input_send_keyboard_event_ex(input, down, repeat, rdp_scancode); + } + else + freerdp_input_send_unicode_keyboard_event(input, down ? 0 : KBD_FLAGS_RELEASE, + buffer[0]); + } + else if (rdp_scancode == RDP_SCANCODE_UNKNOWN) + WLog_ERR(TAG, "Unknown key with X keycode 0x%02" PRIx8 "", event->keycode); + else + freerdp_input_send_keyboard_event_ex(input, down, repeat, rdp_scancode); + + if ((rdp_scancode == RDP_SCANCODE_CAPSLOCK) && (down == FALSE)) + { + xf_sync_kbd_state(xfc); + } + } +} + +int xf_keyboard_read_keyboard_state(xfContext* xfc) +{ + int dummy = 0; + Window wdummy = 0; + UINT32 state = 0; + + if (!xfc->remote_app && xfc->window) + { + XQueryPointer(xfc->display, xfc->window->handle, &wdummy, &wdummy, &dummy, &dummy, &dummy, + &dummy, &state); + } + else + { + XQueryPointer(xfc->display, DefaultRootWindow(xfc->display), &wdummy, &wdummy, &dummy, + &dummy, &dummy, &dummy, &state); + } + + return state; +} + +static int xf_keyboard_get_keymask(xfContext* xfc, int keysym) +{ + int keysymMask = 0; + KeyCode keycode = XKeysymToKeycode(xfc->display, keysym); + + if (keycode == NoSymbol) + return 0; + + WINPR_ASSERT(xfc->modifierMap); + for (int modifierpos = 0; modifierpos < 8; modifierpos++) + { + int offset = xfc->modifierMap->max_keypermod * modifierpos; + + for (int key = 0; key < xfc->modifierMap->max_keypermod; key++) + { + if (xfc->modifierMap->modifiermap[offset + key] == keycode) + { + keysymMask |= 1 << modifierpos; + } + } + } + + return keysymMask; +} + +BOOL xf_keyboard_get_key_state(xfContext* xfc, int state, int keysym) +{ + int keysymMask = xf_keyboard_get_keymask(xfc, keysym); + + if (!keysymMask) + return FALSE; + + return (state & keysymMask) ? TRUE : FALSE; +} + +static BOOL xf_keyboard_set_key_state(xfContext* xfc, BOOL on, int keysym) +{ + if (!xfc->xkbAvailable) + return FALSE; + + const int keysymMask = xf_keyboard_get_keymask(xfc, keysym); + + if (!keysymMask) + { + return FALSE; + } + + return XkbLockModifiers(xfc->display, XkbUseCoreKbd, keysymMask, on ? keysymMask : 0); +} + +UINT32 xf_keyboard_get_toggle_keys_state(xfContext* xfc) +{ + UINT32 toggleKeysState = 0; + const int state = xf_keyboard_read_keyboard_state(xfc); + + if (xf_keyboard_get_key_state(xfc, state, XK_Scroll_Lock)) + toggleKeysState |= KBD_SYNC_SCROLL_LOCK; + + if (xf_keyboard_get_key_state(xfc, state, XK_Num_Lock)) + toggleKeysState |= KBD_SYNC_NUM_LOCK; + + if (xf_keyboard_get_key_state(xfc, state, XK_Caps_Lock)) + toggleKeysState |= KBD_SYNC_CAPS_LOCK; + + if (xf_keyboard_get_key_state(xfc, state, XK_Kana_Lock)) + toggleKeysState |= KBD_SYNC_KANA_LOCK; + + return toggleKeysState; +} + +static void xk_keyboard_update_modifier_keys(xfContext* xfc) +{ + const int keysyms[] = { XK_Shift_L, XK_Shift_R, XK_Alt_L, XK_Alt_R, + XK_Control_L, XK_Control_R, XK_Super_L, XK_Super_R }; + + xf_keyboard_clear(xfc); + + const int state = xf_keyboard_read_keyboard_state(xfc); + + for (size_t i = 0; i < ARRAYSIZE(keysyms); i++) + { + if (xf_keyboard_get_key_state(xfc, state, keysyms[i])) + { + const KeyCode keycode = XKeysymToKeycode(xfc->display, keysyms[i]); + WINPR_ASSERT(keycode <= ARRAYSIZE(xfc->KeyboardState)); + xfc->KeyboardState[keycode] = TRUE; + } + } +} + +void xf_keyboard_focus_in(xfContext* xfc) +{ + UINT32 state = 0; + Window w = None; + int d = 0; + int x = 0; + int y = 0; + + WINPR_ASSERT(xfc); + if (!xfc->display || !xfc->window) + return; + + rdpInput* input = xfc->common.context.input; + WINPR_ASSERT(input); + + const UINT32 syncFlags = xf_keyboard_get_toggle_keys_state(xfc); + freerdp_input_send_focus_in_event(input, syncFlags); + xk_keyboard_update_modifier_keys(xfc); + + /* finish with a mouse pointer position like mstsc.exe if required */ + + if (xfc->remote_app || !xfc->window) + return; + + if (XQueryPointer(xfc->display, xfc->window->handle, &w, &w, &d, &d, &x, &y, &state)) + { + if ((x >= 0) && (x < xfc->window->width) && (y >= 0) && (y < xfc->window->height)) + { + xf_event_adjust_coordinates(xfc, &x, &y); + freerdp_client_send_button_event(&xfc->common, FALSE, PTR_FLAGS_MOVE, x, y); + } + } +} + +static int xf_keyboard_execute_action_script(xfContext* xfc, XF_MODIFIER_KEYS* mod, KeySym keysym) +{ + int status = 1; + BOOL match = FALSE; + char buffer[1024] = { 0 }; + char command[2048] = { 0 }; + char combination[1024] = { 0 }; + + if (!xfc->actionScriptExists) + return 1; + + if ((keysym == XK_Shift_L) || (keysym == XK_Shift_R) || (keysym == XK_Alt_L) || + (keysym == XK_Alt_R) || (keysym == XK_Control_L) || (keysym == XK_Control_R)) + { + return 1; + } + + const char* keyStr = XKeysymToString(keysym); + + if (keyStr == 0) + { + return 1; + } + + if (mod->Shift) + winpr_str_append("Shift", combination, sizeof(combination), "+"); + + if (mod->Ctrl) + winpr_str_append("Ctrl", combination, sizeof(combination), "+"); + + if (mod->Alt) + winpr_str_append("Alt", combination, sizeof(combination), "+"); + + if (mod->Super) + winpr_str_append("Super", combination, sizeof(combination), "+"); + + winpr_str_append(keyStr, combination, sizeof(combination), NULL); + + const size_t count = ArrayList_Count(xfc->keyCombinations); + + for (size_t index = 0; index < count; index++) + { + const char* keyCombination = (const char*)ArrayList_GetItem(xfc->keyCombinations, index); + + if (_stricmp(keyCombination, combination) == 0) + { + match = TRUE; + break; + } + } + + if (!match) + return 1; + + const char* ActionScript = + freerdp_settings_get_string(xfc->common.context.settings, FreeRDP_ActionScript); + sprintf_s(command, sizeof(command), "%s key %s", ActionScript, combination); + FILE* keyScript = popen(command, "r"); + + if (!keyScript) + return -1; + + while (fgets(buffer, sizeof(buffer), keyScript) != NULL) + { + char* context = NULL; + strtok_s(buffer, "\n", &context); + + if (strcmp(buffer, "key-local") == 0) + status = 0; + } + + if (pclose(keyScript) == -1) + status = -1; + + return status; +} + +static int xk_keyboard_get_modifier_keys(xfContext* xfc, XF_MODIFIER_KEYS* mod) +{ + mod->LeftShift = xf_keyboard_key_pressed(xfc, XK_Shift_L); + mod->RightShift = xf_keyboard_key_pressed(xfc, XK_Shift_R); + mod->Shift = mod->LeftShift || mod->RightShift; + mod->LeftAlt = xf_keyboard_key_pressed(xfc, XK_Alt_L); + mod->RightAlt = xf_keyboard_key_pressed(xfc, XK_Alt_R); + mod->Alt = mod->LeftAlt || mod->RightAlt; + mod->LeftCtrl = xf_keyboard_key_pressed(xfc, XK_Control_L); + mod->RightCtrl = xf_keyboard_key_pressed(xfc, XK_Control_R); + mod->Ctrl = mod->LeftCtrl || mod->RightCtrl; + mod->LeftSuper = xf_keyboard_key_pressed(xfc, XK_Super_L); + mod->RightSuper = xf_keyboard_key_pressed(xfc, XK_Super_R); + mod->Super = mod->LeftSuper || mod->RightSuper; + return 0; +} + +BOOL xf_keyboard_handle_special_keys(xfContext* xfc, KeySym keysym) +{ + XF_MODIFIER_KEYS mod = { 0 }; + xk_keyboard_get_modifier_keys(xfc, &mod); + + // remember state of RightCtrl to ungrab keyboard if next action is release of RightCtrl + // do not return anything such that the key could be used by client if ungrab is not the goal + if (keysym == XK_Control_R) + { + if (mod.RightCtrl && xfc->firstPressRightCtrl) + { + // Right Ctrl is pressed, getting ready to ungrab + xfc->ungrabKeyboardWithRightCtrl = TRUE; + xfc->firstPressRightCtrl = FALSE; + } + } + else + { + // some other key has been pressed, abort ungrabbing + if (xfc->ungrabKeyboardWithRightCtrl) + xfc->ungrabKeyboardWithRightCtrl = FALSE; + } + + if (!xf_keyboard_execute_action_script(xfc, &mod, keysym)) + { + return TRUE; + } + + if (!xfc->remote_app && xfc->fullscreen_toggle) + { + if (keysym == XK_Return) + { + if (mod.Ctrl && mod.Alt) + { + /* Ctrl-Alt-Enter: toggle full screen */ + xf_toggle_fullscreen(xfc); + return TRUE; + } + } + } + + if ((keysym == XK_c) || (keysym == XK_C)) + { + if (mod.Ctrl && mod.Alt) + { + /* Ctrl-Alt-C: toggle control */ + if (freerdp_client_encomsp_toggle_control(xfc->common.encomsp)) + return TRUE; + } + } + +#if 0 /* set to 1 to enable multi touch gesture simulation via keyboard */ +#ifdef WITH_XRENDER + + if (!xfc->remote_app && freerdp_settings_get_bool(xfc->common.context.settings, FreeRDP_MultiTouchGestures)) + { + rdpContext* ctx = &xfc->common.context; + + if (mod.Ctrl && mod.Alt) + { + int pdx = 0; + int pdy = 0; + int zdx = 0; + int zdy = 0; + + switch (keysym) + { + case XK_0: /* Ctrl-Alt-0: Reset scaling and panning */{ + const UINT32 sessionWidth = freerdp_settings_get_uint32(xfc->common.context.settings, FreeRDP_DesktopWidth); + const UINT32 sessionHeight = freerdp_settings_get_uint32(xfc->common.context.settings, FreeRDP_DesktopHeight); + + xfc->scaledWidth = sessionWidth; + xfc->scaledHeight = sessionHeight; + xfc->offset_x = 0; + xfc->offset_y = 0; + + if (!xfc->fullscreen && (sessionWidth != xfc->window->width || + sessionHeight != xfc->window->height)) + { + xf_ResizeDesktopWindow(xfc, xfc->window, sessionWidth, sessionHeight); + } + + xf_draw_screen(xfc, 0, 0, sessionWidth, sessionHeight); + return TRUE; +} + + case XK_1: /* Ctrl-Alt-1: Zoom in */ + zdx = zdy = 10; + break; + + case XK_2: /* Ctrl-Alt-2: Zoom out */ + zdx = zdy = -10; + break; + + case XK_3: /* Ctrl-Alt-3: Pan left */ + pdx = -10; + break; + + case XK_4: /* Ctrl-Alt-4: Pan right */ + pdx = 10; + break; + + case XK_5: /* Ctrl-Alt-5: Pan up */ + pdy = -10; + break; + + case XK_6: /* Ctrl-Alt-6: Pan up */ + pdy = 10; + break; + } + + if (pdx != 0 || pdy != 0) + { + PanningChangeEventArgs e; + EventArgsInit(&e, "xfreerdp"); + e.dx = pdx; + e.dy = pdy; + PubSub_OnPanningChange(ctx->pubSub, xfc, &e); + return TRUE; + } + + if (zdx != 0 || zdy != 0) + { + ZoomingChangeEventArgs e; + EventArgsInit(&e, "xfreerdp"); + e.dx = zdx; + e.dy = zdy; + PubSub_OnZoomingChange(ctx->pubSub, xfc, &e); + return TRUE; + } + } + } + +#endif /* WITH_XRENDER defined */ +#endif /* pinch/zoom/pan simulation */ + return FALSE; +} + +void xf_keyboard_handle_special_keys_release(xfContext* xfc, KeySym keysym) +{ + if (keysym != XK_Control_R) + return; + + xfc->firstPressRightCtrl = TRUE; + + if (!xfc->ungrabKeyboardWithRightCtrl) + return; + + // all requirements for ungrab are fulfilled, ungrabbing now + XF_MODIFIER_KEYS mod = { 0 }; + xk_keyboard_get_modifier_keys(xfc, &mod); + + if (!mod.RightCtrl) + { + if (!xfc->fullscreen) + { + freerdp_client_encomsp_toggle_control(xfc->common.encomsp); + } + + xfc->mouse_active = FALSE; + xf_ungrab(xfc); + } + + // ungrabbed + xfc->ungrabKeyboardWithRightCtrl = FALSE; +} + +BOOL xf_keyboard_set_indicators(rdpContext* context, UINT16 led_flags) +{ + xfContext* xfc = (xfContext*)context; + xf_keyboard_set_key_state(xfc, led_flags & KBD_SYNC_SCROLL_LOCK, XK_Scroll_Lock); + xf_keyboard_set_key_state(xfc, led_flags & KBD_SYNC_NUM_LOCK, XK_Num_Lock); + xf_keyboard_set_key_state(xfc, led_flags & KBD_SYNC_CAPS_LOCK, XK_Caps_Lock); + xf_keyboard_set_key_state(xfc, led_flags & KBD_SYNC_KANA_LOCK, XK_Kana_Lock); + return TRUE; +} + +BOOL xf_keyboard_set_ime_status(rdpContext* context, UINT16 imeId, UINT32 imeState, + UINT32 imeConvMode) +{ + if (!context) + return FALSE; + + WLog_WARN(TAG, + "KeyboardSetImeStatus(unitId=%04" PRIx16 ", imeState=%08" PRIx32 + ", imeConvMode=%08" PRIx32 ") ignored", + imeId, imeState, imeConvMode); + return TRUE; +} + +BOOL xf_ungrab(xfContext* xfc) +{ + WINPR_ASSERT(xfc); + XUngrabKeyboard(xfc->display, CurrentTime); + XUngrabPointer(xfc->display, CurrentTime); + xfc->common.mouse_grabbed = FALSE; + return TRUE; +} diff --git a/client/X11/xf_keyboard.h b/client/X11/xf_keyboard.h new file mode 100644 index 0000000..a9374b8 --- /dev/null +++ b/client/X11/xf_keyboard.h @@ -0,0 +1,65 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Keyboard Handling + * + * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_X11_XF_KEYBOARD_H +#define FREERDP_CLIENT_X11_XF_KEYBOARD_H + +#include <freerdp/locale/keyboard.h> + +#include "xf_client.h" +#include "xfreerdp.h" + +typedef struct +{ + BOOL Shift; + BOOL LeftShift; + BOOL RightShift; + BOOL Alt; + BOOL LeftAlt; + BOOL RightAlt; + BOOL Ctrl; + BOOL LeftCtrl; + BOOL RightCtrl; + BOOL Super; + BOOL LeftSuper; + BOOL RightSuper; +} XF_MODIFIER_KEYS; + +BOOL xf_keyboard_init(xfContext* xfc); +void xf_keyboard_free(xfContext* xfc); + +void xf_keyboard_key_press(xfContext* xfc, const XKeyEvent* event, KeySym keysym); +void xf_keyboard_key_release(xfContext* xfc, const XKeyEvent* event, KeySym keysym); + +void xf_keyboard_release_all_keypress(xfContext* xfc); +BOOL xf_keyboard_key_pressed(xfContext* xfc, KeySym keysym); + +int xf_keyboard_read_keyboard_state(xfContext* xfc); +BOOL xf_keyboard_get_key_state(xfContext* xfc, int state, int keysym); +UINT32 xf_keyboard_get_toggle_keys_state(xfContext* xfc); +void xf_keyboard_focus_in(xfContext* xfc); +BOOL xf_keyboard_handle_special_keys(xfContext* xfc, KeySym keysym); +void xf_keyboard_handle_special_keys_release(xfContext* xfc, KeySym keysym); +BOOL xf_keyboard_set_indicators(rdpContext* context, UINT16 led_flags); +BOOL xf_keyboard_set_ime_status(rdpContext* context, UINT16 imeId, UINT32 imeState, + UINT32 imeConvMode); + +BOOL xf_ungrab(xfContext* xfc); + +#endif /* FREERDP_CLIENT_X11_XF_KEYBOARD_H */ diff --git a/client/X11/xf_monitor.c b/client/X11/xf_monitor.c new file mode 100644 index 0000000..d872a4c --- /dev/null +++ b/client/X11/xf_monitor.c @@ -0,0 +1,654 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Monitor Handling + * + * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * Copyright 2017 David Fort <contact@hardening-consulting.com> + * Copyright 2018 Kai Harms <kharms@rangee.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <X11/Xlib.h> +#include <X11/Xutil.h> + +#include <winpr/assert.h> +#include <winpr/crt.h> + +#include <freerdp/log.h> + +#define TAG CLIENT_TAG("x11") + +#ifdef WITH_XINERAMA +#include <X11/extensions/Xinerama.h> +#endif + +#ifdef WITH_XRANDR +#include <X11/extensions/Xrandr.h> +#include <X11/extensions/randr.h> + +#if (RANDR_MAJOR * 100 + RANDR_MINOR) >= 105 +#define USABLE_XRANDR +#endif + +#endif + +#include "xf_monitor.h" + +/* See MSDN Section on Multiple Display Monitors: http://msdn.microsoft.com/en-us/library/dd145071 + */ + +int xf_list_monitors(xfContext* xfc) +{ + Display* display = NULL; + int major = 0; + int minor = 0; + int nmonitors = 0; + display = XOpenDisplay(NULL); + + if (!display) + { + WLog_ERR(TAG, "failed to open X display"); + return -1; + } + +#if defined(USABLE_XRANDR) + + if (XRRQueryExtension(display, &major, &minor) && + (XRRQueryVersion(display, &major, &minor) == True) && (major * 100 + minor >= 105)) + { + XRRMonitorInfo* monitors = + XRRGetMonitors(display, DefaultRootWindow(display), 1, &nmonitors); + + for (int i = 0; i < nmonitors; i++) + { + printf(" %s [%d] %dx%d\t+%d+%d\n", monitors[i].primary ? "*" : " ", i, + monitors[i].width, monitors[i].height, monitors[i].x, monitors[i].y); + } + + XRRFreeMonitors(monitors); + } + else +#endif +#ifdef WITH_XINERAMA + if (XineramaQueryExtension(display, &major, &minor)) + { + if (XineramaIsActive(display)) + { + XineramaScreenInfo* screen = XineramaQueryScreens(display, &nmonitors); + + for (int i = 0; i < nmonitors; i++) + { + printf(" %s [%d] %hdx%hd\t+%hd+%hd\n", (i == 0) ? "*" : " ", i, + screen[i].width, screen[i].height, screen[i].x_org, screen[i].y_org); + } + + XFree(screen); + } + } + else +#else + { + Screen* screen = ScreenOfDisplay(display, DefaultScreen(display)); + printf(" * [0] %dx%d\t+0+0\n", WidthOfScreen(screen), HeightOfScreen(screen)); + } + +#endif + XCloseDisplay(display); + return 0; +} + +static BOOL xf_is_monitor_id_active(xfContext* xfc, UINT32 id) +{ + const rdpSettings* settings = NULL; + + WINPR_ASSERT(xfc); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + const UINT32 NumMonitorIds = freerdp_settings_get_uint32(settings, FreeRDP_NumMonitorIds); + if (NumMonitorIds == 0) + return TRUE; + + for (UINT32 index = 0; index < NumMonitorIds; index++) + { + const UINT32* cur = freerdp_settings_get_pointer_array(settings, FreeRDP_MonitorIds, index); + if (cur && (*cur == id)) + return TRUE; + } + + return FALSE; +} + +BOOL xf_detect_monitors(xfContext* xfc, UINT32* pMaxWidth, UINT32* pMaxHeight) +{ + BOOL rc = FALSE; + int nmonitors = 0; + UINT32 monitor_index = 0; + BOOL primaryMonitorFound = FALSE; + VIRTUAL_SCREEN* vscreen = NULL; + rdpSettings* settings = NULL; + int mouse_x = 0; + int mouse_y = 0; + int _dummy_i = 0; + Window _dummy_w = 0; + int current_monitor = 0; + Screen* screen = NULL; +#if defined WITH_XINERAMA || defined WITH_XRANDR + int major = 0; + int minor = 0; +#endif +#if defined(USABLE_XRANDR) + XRRMonitorInfo* rrmonitors = NULL; + BOOL useXRandr = FALSE; +#endif + + if (!xfc || !pMaxWidth || !pMaxHeight || !xfc->common.context.settings) + return FALSE; + + settings = xfc->common.context.settings; + vscreen = &xfc->vscreen; + *pMaxWidth = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth); + *pMaxHeight = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight); + + if (freerdp_settings_get_uint64(settings, FreeRDP_ParentWindowId) > 0) + { + xfc->workArea.x = 0; + xfc->workArea.y = 0; + xfc->workArea.width = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth); + xfc->workArea.height = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight); + return TRUE; + } + + /* get mouse location */ + if (!XQueryPointer(xfc->display, DefaultRootWindow(xfc->display), &_dummy_w, &_dummy_w, + &mouse_x, &mouse_y, &_dummy_i, &_dummy_i, (void*)&_dummy_i)) + mouse_x = mouse_y = 0; + +#if defined(USABLE_XRANDR) + + if (XRRQueryExtension(xfc->display, &major, &minor) && + (XRRQueryVersion(xfc->display, &major, &minor) == True) && (major * 100 + minor >= 105)) + { + rrmonitors = + XRRGetMonitors(xfc->display, DefaultRootWindow(xfc->display), 1, &vscreen->nmonitors); + + if (vscreen->nmonitors > 16) + vscreen->nmonitors = 0; + + if (vscreen->nmonitors) + { + for (int i = 0; i < vscreen->nmonitors; i++) + { + MONITOR_INFO* cur_vscreen = &vscreen->monitors[i]; + const XRRMonitorInfo* cur_monitor = &rrmonitors[i]; + cur_vscreen->area.left = cur_monitor->x; + cur_vscreen->area.top = cur_monitor->y; + cur_vscreen->area.right = cur_monitor->x + cur_monitor->width - 1; + cur_vscreen->area.bottom = cur_monitor->y + cur_monitor->height - 1; + cur_vscreen->primary = cur_monitor->primary > 0; + } + } + + useXRandr = TRUE; + } + else +#endif +#ifdef WITH_XINERAMA + if (XineramaQueryExtension(xfc->display, &major, &minor) && XineramaIsActive(xfc->display)) + { + XineramaScreenInfo* screenInfo = XineramaQueryScreens(xfc->display, &vscreen->nmonitors); + + if (vscreen->nmonitors > 16) + vscreen->nmonitors = 0; + + if (vscreen->nmonitors) + { + for (int i = 0; i < vscreen->nmonitors; i++) + { + MONITOR_INFO* monitor = &vscreen->monitors[i]; + monitor->area.left = screenInfo[i].x_org; + monitor->area.top = screenInfo[i].y_org; + monitor->area.right = screenInfo[i].x_org + screenInfo[i].width - 1; + monitor->area.bottom = screenInfo[i].y_org + screenInfo[i].height - 1; + } + } + + XFree(screenInfo); + } + +#endif + xfc->fullscreenMonitors.top = xfc->fullscreenMonitors.bottom = xfc->fullscreenMonitors.left = + xfc->fullscreenMonitors.right = 0; + + /* Determine which monitor that the mouse cursor is on */ + if (vscreen->monitors) + { + for (int i = 0; i < vscreen->nmonitors; i++) + { + const MONITOR_INFO* monitor = &vscreen->monitors[i]; + + if ((mouse_x >= monitor->area.left) && (mouse_x <= monitor->area.right) && + (mouse_y >= monitor->area.top) && (mouse_y <= monitor->area.bottom)) + { + current_monitor = i; + break; + } + } + } + + /* + Even for a single monitor, we need to calculate the virtual screen to support + window managers that do not implement all X window state hints. + + If the user did not request multiple monitor or is using workarea + without remote app, we force the number of monitors be 1 so later + the rest of the client don't end up using more monitors than the user desires. + */ + if ((!freerdp_settings_get_bool(settings, FreeRDP_UseMultimon) && + !freerdp_settings_get_bool(settings, FreeRDP_SpanMonitors)) || + (freerdp_settings_get_bool(settings, FreeRDP_Workarea) && + !freerdp_settings_get_bool(settings, FreeRDP_RemoteApplicationMode))) + { + /* If no monitors were specified on the command-line then set the current monitor as active + */ + if (freerdp_settings_get_uint32(settings, FreeRDP_NumMonitorIds) == 0) + { + UINT32 id = current_monitor; + if (!freerdp_settings_set_pointer_len(settings, FreeRDP_MonitorIds, &id, 1)) + goto fail; + } + + /* Always sets number of monitors from command-line to just 1. + * If the monitor is invalid then we will default back to current monitor + * later as a fallback. So, there is no need to validate command-line entry here. + */ + if (!freerdp_settings_set_uint32(settings, FreeRDP_NumMonitorIds, 1)) + goto fail; + } + + /* WORKAROUND: With Remote Application Mode - using NET_WM_WORKAREA + * causes issues with the ability to fully size the window vertically + * (the bottom of the window area is never updated). So, we just set + * the workArea to match the full Screen width/height. + */ + if (freerdp_settings_get_bool(settings, FreeRDP_RemoteApplicationMode) || !xf_GetWorkArea(xfc)) + { + /* + if only 1 monitor is enabled, use monitor area + this is required in case of a screen composed of more than one monitor + but user did not enable multimonitor + */ + if ((freerdp_settings_get_uint32(settings, FreeRDP_NumMonitorIds) == 1) && + (vscreen->nmonitors > current_monitor)) + { + MONITOR_INFO* monitor = vscreen->monitors + current_monitor; + + if (!monitor) + goto fail; + + xfc->workArea.x = monitor->area.left; + xfc->workArea.y = monitor->area.top; + xfc->workArea.width = monitor->area.right - monitor->area.left + 1; + xfc->workArea.height = monitor->area.bottom - monitor->area.top + 1; + } + else + { + xfc->workArea.x = 0; + xfc->workArea.y = 0; + xfc->workArea.width = WidthOfScreen(xfc->screen); + xfc->workArea.height = HeightOfScreen(xfc->screen); + } + } + + if (freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)) + { + *pMaxWidth = WidthOfScreen(xfc->screen); + *pMaxHeight = HeightOfScreen(xfc->screen); + } + else if (freerdp_settings_get_bool(settings, FreeRDP_Workarea)) + { + *pMaxWidth = xfc->workArea.width; + *pMaxHeight = xfc->workArea.height; + } + else if (freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen)) + { + /* If we have specific monitor information then limit the PercentScreen value + * to only affect the current monitor vs. the entire desktop + */ + if (vscreen->nmonitors > 0) + { + if (!vscreen->monitors) + goto fail; + + *pMaxWidth = vscreen->monitors[current_monitor].area.right - + vscreen->monitors[current_monitor].area.left + 1; + *pMaxHeight = vscreen->monitors[current_monitor].area.bottom - + vscreen->monitors[current_monitor].area.top + 1; + + if (freerdp_settings_get_bool(settings, FreeRDP_PercentScreenUseWidth)) + *pMaxWidth = ((vscreen->monitors[current_monitor].area.right - + vscreen->monitors[current_monitor].area.left + 1) * + freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen)) / + 100; + + if (freerdp_settings_get_bool(settings, FreeRDP_PercentScreenUseHeight)) + *pMaxHeight = ((vscreen->monitors[current_monitor].area.bottom - + vscreen->monitors[current_monitor].area.top + 1) * + freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen)) / + 100; + } + else + { + *pMaxWidth = xfc->workArea.width; + *pMaxHeight = xfc->workArea.height; + + if (freerdp_settings_get_bool(settings, FreeRDP_PercentScreenUseWidth)) + *pMaxWidth = (xfc->workArea.width * + freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen)) / + 100; + + if (freerdp_settings_get_bool(settings, FreeRDP_PercentScreenUseHeight)) + *pMaxHeight = (xfc->workArea.height * + freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen)) / + 100; + } + } + else if (freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth) && + freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight)) + { + *pMaxWidth = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth); + *pMaxHeight = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight); + } + + /* Create array of all active monitors by taking into account monitors requested on the + * command-line */ + { + UINT32 nr = 0; + + { + const UINT32* ids = freerdp_settings_get_pointer(settings, FreeRDP_MonitorIds); + if (ids) + nr = *ids; + } + for (UINT32 i = 0; i < vscreen->nmonitors; i++) + { + MONITOR_ATTRIBUTES* attrs = NULL; + + if (!xf_is_monitor_id_active(xfc, (UINT32)i)) + continue; + + if (!vscreen->monitors) + goto fail; + + rdpMonitor* monitor = freerdp_settings_get_pointer_array_writable( + settings, FreeRDP_MonitorDefArray, nmonitors); + monitor->x = (vscreen->monitors[i].area.left * + (freerdp_settings_get_bool(settings, FreeRDP_PercentScreenUseWidth) + ? freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen) + : 100)) / + 100; + monitor->y = (vscreen->monitors[i].area.top * + (freerdp_settings_get_bool(settings, FreeRDP_PercentScreenUseHeight) + ? freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen) + : 100)) / + 100; + monitor->width = + ((vscreen->monitors[i].area.right - vscreen->monitors[i].area.left + 1) * + (freerdp_settings_get_bool(settings, FreeRDP_PercentScreenUseWidth) + ? freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen) + : 100)) / + 100; + monitor->height = + ((vscreen->monitors[i].area.bottom - vscreen->monitors[i].area.top + 1) * + (freerdp_settings_get_bool(settings, FreeRDP_PercentScreenUseWidth) + ? freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen) + : 100)) / + 100; + monitor->orig_screen = i; +#ifdef USABLE_XRANDR + + if (useXRandr && rrmonitors) + { + Rotation rot = 0; + Rotation ret = 0; + attrs = &monitor->attributes; + attrs->physicalWidth = rrmonitors[i].mwidth; + attrs->physicalHeight = rrmonitors[i].mheight; + ret = XRRRotations(xfc->display, i, &rot); + attrs->orientation = ret; + } + +#endif + + if ((UINT32)i == nr) + { + monitor->is_primary = TRUE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftX, monitor->x)) + goto fail; + if (!freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftY, monitor->y)) + goto fail; + primaryMonitorFound = TRUE; + } + + nmonitors++; + } + } + + /* If no monitor is active(bogus command-line monitor specification) - then lets try to fallback + * to go fullscreen on the current monitor only */ + if (nmonitors == 0 && vscreen->nmonitors > 0) + { + INT32 width = 0; + INT32 height = 0; + if (!vscreen->monitors) + goto fail; + + width = vscreen->monitors[current_monitor].area.right - + vscreen->monitors[current_monitor].area.left + 1L; + height = vscreen->monitors[current_monitor].area.bottom - + vscreen->monitors[current_monitor].area.top + 1L; + + rdpMonitor* monitor = + freerdp_settings_get_pointer_array_writable(settings, FreeRDP_MonitorDefArray, 0); + if (!monitor) + goto fail; + + monitor->x = vscreen->monitors[current_monitor].area.left; + monitor->y = vscreen->monitors[current_monitor].area.top; + monitor->width = MIN(width, (INT64)(*pMaxWidth)); + monitor->height = MIN(height, (INT64)(*pMaxHeight)); + monitor->orig_screen = current_monitor; + nmonitors = 1; + } + + if (!freerdp_settings_set_uint32(settings, FreeRDP_MonitorCount, nmonitors)) + goto fail; + + /* If we have specific monitor information */ + if (freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount) > 0) + { + const rdpMonitor* cmonitor = + freerdp_settings_get_pointer_array(settings, FreeRDP_MonitorDefArray, 0); + if (!cmonitor) + goto fail; + + /* Initialize bounding rectangle for all monitors */ + int vX = cmonitor->x; + int vY = cmonitor->y; + int vR = vX + cmonitor->width; + int vB = vY + cmonitor->height; + xfc->fullscreenMonitors.top = xfc->fullscreenMonitors.bottom = + xfc->fullscreenMonitors.left = xfc->fullscreenMonitors.right = cmonitor->orig_screen; + + /* Calculate bounding rectangle around all monitors to be used AND + * also set the Xinerama indices which define left/top/right/bottom monitors. + */ + for (UINT32 i = 0; i < freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount); i++) + { + rdpMonitor* monitor = + freerdp_settings_get_pointer_array_writable(settings, FreeRDP_MonitorDefArray, i); + + /* does the same as gdk_rectangle_union */ + int destX = MIN(vX, monitor->x); + int destY = MIN(vY, monitor->y); + int destR = MAX(vR, monitor->x + monitor->width); + int destB = MAX(vB, monitor->y + monitor->height); + + if (vX != destX) + xfc->fullscreenMonitors.left = monitor->orig_screen; + + if (vY != destY) + xfc->fullscreenMonitors.top = monitor->orig_screen; + + if (vR != destR) + xfc->fullscreenMonitors.right = monitor->orig_screen; + + if (vB != destB) + xfc->fullscreenMonitors.bottom = monitor->orig_screen; + + vX = destX / ((freerdp_settings_get_bool(settings, FreeRDP_PercentScreenUseWidth) + ? freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen) + : 100) / + 100.); + vY = destY / ((freerdp_settings_get_bool(settings, FreeRDP_PercentScreenUseHeight) + ? freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen) + : 100) / + 100.); + vR = destR / ((freerdp_settings_get_bool(settings, FreeRDP_PercentScreenUseWidth) + ? freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen) + : 100) / + 100.); + vB = destB / ((freerdp_settings_get_bool(settings, FreeRDP_PercentScreenUseHeight) + ? freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen) + : 100) / + 100.); + } + + vscreen->area.left = 0; + vscreen->area.right = vR - vX - 1; + vscreen->area.top = 0; + vscreen->area.bottom = vB - vY - 1; + + if (freerdp_settings_get_bool(settings, FreeRDP_Workarea)) + { + vscreen->area.top = xfc->workArea.y; + vscreen->area.bottom = xfc->workArea.height + xfc->workArea.y - 1; + } + + if (!primaryMonitorFound) + { + /* If we have a command line setting we should use it */ + if (freerdp_settings_get_uint32(settings, FreeRDP_NumMonitorIds) > 0) + { + /* The first monitor is the first in the setting which should be used */ + UINT32* ids = + freerdp_settings_get_pointer_array_writable(settings, FreeRDP_MonitorIds, 0); + if (ids) + monitor_index = *ids; + } + else + { + /* This is the same as when we would trust the Xinerama results.. + and set the monitor index to zero. + The monitor listed with /list:monitor on index zero is always the primary + */ + screen = DefaultScreenOfDisplay(xfc->display); + monitor_index = XScreenNumberOfScreen(screen); + } + + UINT32 j = monitor_index; + rdpMonitor* pmonitor = + freerdp_settings_get_pointer_array_writable(settings, FreeRDP_MonitorDefArray, j); + + /* If the "default" monitor is not 0,0 use it */ + if ((pmonitor->x != 0) || (pmonitor->y != 0)) + { + pmonitor->is_primary = TRUE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftX, pmonitor->x)) + goto fail; + if (!freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftY, pmonitor->y)) + goto fail; + } + else + { + /* Lets try to see if there is a monitor with a 0,0 coordinate and use it as a + * fallback*/ + for (UINT32 i = 0; i < freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount); + i++) + { + rdpMonitor* monitor = freerdp_settings_get_pointer_array_writable( + settings, FreeRDP_MonitorDefArray, i); + if (!primaryMonitorFound && monitor->x == 0 && monitor->y == 0) + { + monitor->is_primary = TRUE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftX, + monitor->x)) + goto fail; + if (!freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftY, + monitor->y)) + goto fail; + primaryMonitorFound = TRUE; + } + } + } + } + + /* Subtract monitor shift from monitor variables for server-side use. + * We maintain monitor shift value as Window requires the primary monitor to have a + * coordinate of 0,0 In some X configurations, no monitor may have a coordinate of 0,0. This + * can also be happen if the user requests specific monitors from the command-line as well. + * So, we make sure to translate our primary monitor's upper-left corner to 0,0 on the + * server. + */ + for (UINT32 i = 0; i < freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount); i++) + { + rdpMonitor* monitor = + freerdp_settings_get_pointer_array_writable(settings, FreeRDP_MonitorDefArray, i); + monitor->x = + monitor->x - freerdp_settings_get_uint32(settings, FreeRDP_MonitorLocalShiftX); + monitor->y = + monitor->y - freerdp_settings_get_uint32(settings, FreeRDP_MonitorLocalShiftY); + } + + /* Set the desktop width and height according to the bounding rectangle around the active + * monitors */ + *pMaxWidth = MIN(*pMaxWidth, (UINT32)vscreen->area.right - vscreen->area.left + 1); + *pMaxHeight = MIN(*pMaxHeight, (UINT32)vscreen->area.bottom - vscreen->area.top + 1); + } + + /* some 2008 server freeze at logon if we announce support for monitor layout PDU with + * #monitors < 2. So let's announce it only if we have more than 1 monitor. + */ + if (freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount) > 1) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportMonitorLayoutPdu, TRUE)) + goto fail; + } + + rc = TRUE; +fail: +#ifdef USABLE_XRANDR + + if (rrmonitors) + XRRFreeMonitors(rrmonitors); + +#endif + return rc; +} diff --git a/client/X11/xf_monitor.h b/client/X11/xf_monitor.h new file mode 100644 index 0000000..c27c88f --- /dev/null +++ b/client/X11/xf_monitor.h @@ -0,0 +1,48 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Monitor Handling + * + * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_X11_MONITOR_H +#define FREERDP_CLIENT_X11_MONITOR_H + +#include <freerdp/api.h> +#include <freerdp/freerdp.h> + +typedef struct +{ + RECTANGLE_16 area; + RECTANGLE_16 workarea; + BOOL primary; +} MONITOR_INFO; + +typedef struct +{ + int nmonitors; + RECTANGLE_16 area; + RECTANGLE_16 workarea; + MONITOR_INFO* monitors; +} VIRTUAL_SCREEN; + +#include "xf_client.h" +#include "xfreerdp.h" + +FREERDP_API int xf_list_monitors(xfContext* xfc); +FREERDP_API BOOL xf_detect_monitors(xfContext* xfc, UINT32* pWidth, UINT32* pHeight); +FREERDP_API void xf_monitors_free(xfContext* xfc); + +#endif /* FREERDP_CLIENT_X11_MONITOR_H */ diff --git a/client/X11/xf_rail.c b/client/X11/xf_rail.c new file mode 100644 index 0000000..7402bb2 --- /dev/null +++ b/client/X11/xf_rail.c @@ -0,0 +1,1184 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 RAIL + * + * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <X11/Xlib.h> +#include <X11/Xatom.h> +#include <X11/Xutil.h> + +#include <winpr/assert.h> +#include <winpr/wlog.h> +#include <winpr/print.h> + +#include <freerdp/client/rail.h> + +#include "xf_window.h" +#include "xf_rail.h" +#include "xf_utils.h" + +#include <freerdp/log.h> +#define TAG CLIENT_TAG("x11") + +static const char* error_code_names[] = { "RAIL_EXEC_S_OK", + "RAIL_EXEC_E_HOOK_NOT_LOADED", + "RAIL_EXEC_E_DECODE_FAILED", + "RAIL_EXEC_E_NOT_IN_ALLOWLIST", + "RAIL_EXEC_E_FILE_NOT_FOUND", + "RAIL_EXEC_E_FAIL", + "RAIL_EXEC_E_SESSION_LOCKED" }; + +#ifdef WITH_DEBUG_RAIL +static const char* movetype_names[] = { + "(invalid)", "RAIL_WMSZ_LEFT", "RAIL_WMSZ_RIGHT", + "RAIL_WMSZ_TOP", "RAIL_WMSZ_TOPLEFT", "RAIL_WMSZ_TOPRIGHT", + "RAIL_WMSZ_BOTTOM", "RAIL_WMSZ_BOTTOMLEFT", "RAIL_WMSZ_BOTTOMRIGHT", + "RAIL_WMSZ_MOVE", "RAIL_WMSZ_KEYMOVE", "RAIL_WMSZ_KEYSIZE" +}; +#endif + +struct xf_rail_icon +{ + long* data; + int length; +}; +typedef struct xf_rail_icon xfRailIcon; + +struct xf_rail_icon_cache +{ + xfRailIcon* entries; + UINT32 numCaches; + UINT32 numCacheEntries; + xfRailIcon scratch; +}; + +typedef struct +{ + xfContext* xfc; + const RECTANGLE_16* rect; +} rail_paint_fn_arg_t; + +void xf_rail_enable_remoteapp_mode(xfContext* xfc) +{ + if (!xfc->remote_app) + { + xfc->remote_app = TRUE; + xfc->drawable = xf_CreateDummyWindow(xfc); + xf_DestroyDesktopWindow(xfc, xfc->window); + xfc->window = NULL; + } +} + +void xf_rail_disable_remoteapp_mode(xfContext* xfc) +{ + if (xfc->remote_app) + { + xfc->remote_app = FALSE; + xf_DestroyDummyWindow(xfc, xfc->drawable); + xf_create_window(xfc); + xf_create_image(xfc); + } +} + +void xf_rail_send_activate(xfContext* xfc, Window xwindow, BOOL enabled) +{ + RAIL_ACTIVATE_ORDER activate; + xfAppWindow* appWindow = xf_AppWindowFromX11Window(xfc, xwindow); + + if (!appWindow) + return; + + if (enabled) + xf_SetWindowStyle(xfc, appWindow, appWindow->dwStyle, appWindow->dwExStyle); + else + xf_SetWindowStyle(xfc, appWindow, 0, 0); + + activate.windowId = appWindow->windowId; + activate.enabled = enabled; + xfc->rail->ClientActivate(xfc->rail, &activate); +} + +void xf_rail_send_client_system_command(xfContext* xfc, UINT32 windowId, UINT16 command) +{ + RAIL_SYSCOMMAND_ORDER syscommand; + syscommand.windowId = windowId; + syscommand.command = command; + xfc->rail->ClientSystemCommand(xfc->rail, &syscommand); +} + +/** + * The position of the X window can become out of sync with the RDP window + * if the X window is moved locally by the window manager. In this event + * send an update to the RDP server informing it of the new window position + * and size. + */ +void xf_rail_adjust_position(xfContext* xfc, xfAppWindow* appWindow) +{ + RAIL_WINDOW_MOVE_ORDER windowMove; + + if (!appWindow->is_mapped || appWindow->local_move.state != LMS_NOT_ACTIVE) + return; + + /* If current window position disagrees with RDP window position, send update to RDP server */ + if (appWindow->x != appWindow->windowOffsetX || appWindow->y != appWindow->windowOffsetY || + appWindow->width != (INT64)appWindow->windowWidth || + appWindow->height != (INT64)appWindow->windowHeight) + { + windowMove.windowId = appWindow->windowId; + /* + * Calculate new size/position for the rail window(new values for + * windowOffsetX/windowOffsetY/windowWidth/windowHeight) on the server + */ + windowMove.left = appWindow->x - appWindow->resizeMarginLeft; + windowMove.top = appWindow->y - appWindow->resizeMarginTop; + windowMove.right = appWindow->x + appWindow->width + appWindow->resizeMarginRight; + windowMove.bottom = appWindow->y + appWindow->height + appWindow->resizeMarginBottom; + xfc->rail->ClientWindowMove(xfc->rail, &windowMove); + } +} + +void xf_rail_end_local_move(xfContext* xfc, xfAppWindow* appWindow) +{ + int x = 0; + int y = 0; + int child_x = 0; + int child_y = 0; + unsigned int mask = 0; + Window root_window = 0; + Window child_window = 0; + rdpInput* input = NULL; + + WINPR_ASSERT(xfc); + + input = xfc->common.context.input; + WINPR_ASSERT(input); + + if ((appWindow->local_move.direction == _NET_WM_MOVERESIZE_MOVE_KEYBOARD) || + (appWindow->local_move.direction == _NET_WM_MOVERESIZE_SIZE_KEYBOARD)) + { + RAIL_WINDOW_MOVE_ORDER windowMove; + + /* + * For keyboard moves send and explicit update to RDP server + */ + windowMove.windowId = appWindow->windowId; + /* + * Calculate new size/position for the rail window(new values for + * windowOffsetX/windowOffsetY/windowWidth/windowHeight) on the server + * + */ + windowMove.left = appWindow->x - appWindow->resizeMarginLeft; + windowMove.top = appWindow->y - appWindow->resizeMarginTop; + windowMove.right = + appWindow->x + appWindow->width + + appWindow + ->resizeMarginRight; /* In the update to RDP the position is one past the window */ + windowMove.bottom = appWindow->y + appWindow->height + appWindow->resizeMarginBottom; + xfc->rail->ClientWindowMove(xfc->rail, &windowMove); + } + + /* + * Simulate button up at new position to end the local move (per RDP spec) + */ + XQueryPointer(xfc->display, appWindow->handle, &root_window, &child_window, &x, &y, &child_x, + &child_y, &mask); + + /* only send the mouse coordinates if not a keyboard move or size */ + if ((appWindow->local_move.direction != _NET_WM_MOVERESIZE_MOVE_KEYBOARD) && + (appWindow->local_move.direction != _NET_WM_MOVERESIZE_SIZE_KEYBOARD)) + { + freerdp_client_send_button_event(&xfc->common, FALSE, PTR_FLAGS_BUTTON1, x, y); + } + + /* + * Proactively update the RAIL window dimensions. There is a race condition where + * we can start to receive GDI orders for the new window dimensions before we + * receive the RAIL ORDER for the new window size. This avoids that race condition. + */ + appWindow->windowOffsetX = appWindow->x; + appWindow->windowOffsetY = appWindow->y; + appWindow->windowWidth = appWindow->width; + appWindow->windowHeight = appWindow->height; + appWindow->local_move.state = LMS_TERMINATING; +} + +BOOL xf_rail_paint_surface(xfContext* xfc, UINT64 windowId, const RECTANGLE_16* rect) +{ + xfAppWindow* appWindow = xf_rail_get_window(xfc, windowId); + + WINPR_ASSERT(rect); + + if (!appWindow) + return FALSE; + + const RECTANGLE_16 windowRect = { .left = MAX(appWindow->x, 0), + .top = MAX(appWindow->y, 0), + .right = MAX(appWindow->x + appWindow->width, 0), + .bottom = MAX(appWindow->y + appWindow->height, 0) }; + + REGION16 windowInvalidRegion = { 0 }; + region16_init(&windowInvalidRegion); + region16_union_rect(&windowInvalidRegion, &windowInvalidRegion, &windowRect); + region16_intersect_rect(&windowInvalidRegion, &windowInvalidRegion, rect); + + if (!region16_is_empty(&windowInvalidRegion)) + { + const RECTANGLE_16* extents = region16_extents(&windowInvalidRegion); + const RECTANGLE_16 updateRect = { .left = extents->left - appWindow->x, + .top = extents->top - appWindow->y, + .right = extents->right - appWindow->x, + .bottom = extents->bottom - appWindow->y }; + + xf_UpdateWindowArea(xfc, appWindow, updateRect.left, updateRect.top, + updateRect.right - updateRect.left, updateRect.bottom - updateRect.top); + } + region16_uninit(&windowInvalidRegion); + return TRUE; +} + +static BOOL rail_paint_fn(const void* pvkey, void* value, void* pvarg) +{ + rail_paint_fn_arg_t* arg = pvarg; + WINPR_ASSERT(pvkey); + WINPR_ASSERT(arg); + + const UINT64 key = *(const UINT64*)pvkey; + return xf_rail_paint_surface(arg->xfc, key, arg->rect); +} + +BOOL xf_rail_paint(xfContext* xfc, const RECTANGLE_16* rect) +{ + rail_paint_fn_arg_t arg = { .xfc = xfc, .rect = rect }; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(rect); + + if (!xfc->railWindows) + return TRUE; + + return HashTable_Foreach(xfc->railWindows, rail_paint_fn, &arg); +} + +/* RemoteApp Core Protocol Extension */ + +static BOOL xf_rail_window_common(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const WINDOW_STATE_ORDER* windowState) +{ + xfAppWindow* appWindow = NULL; + xfContext* xfc = (xfContext*)context; + UINT32 fieldFlags = orderInfo->fieldFlags; + BOOL position_or_size_updated = FALSE; + appWindow = xf_rail_get_window(xfc, orderInfo->windowId); + + if (fieldFlags & WINDOW_ORDER_STATE_NEW) + { + if (!appWindow) + appWindow = xf_rail_add_window(xfc, orderInfo->windowId, windowState->windowOffsetX, + windowState->windowOffsetY, windowState->windowWidth, + windowState->windowHeight, 0xFFFFFFFF); + + if (!appWindow) + return FALSE; + + appWindow->dwStyle = windowState->style; + appWindow->dwExStyle = windowState->extendedStyle; + + /* Ensure window always gets a window title */ + if (fieldFlags & WINDOW_ORDER_FIELD_TITLE) + { + union + { + WCHAR* wc; + BYTE* b; + } cnv; + char* title = NULL; + + cnv.b = windowState->titleInfo.string; + if (windowState->titleInfo.length == 0) + { + if (!(title = _strdup(""))) + { + WLog_ERR(TAG, "failed to duplicate empty window title string"); + /* error handled below */ + } + } + else if (!(title = ConvertWCharNToUtf8Alloc( + cnv.wc, windowState->titleInfo.length / sizeof(WCHAR), NULL))) + { + WLog_ERR(TAG, "failed to convert window title"); + /* error handled below */ + } + + appWindow->title = title; + } + else + { + if (!(appWindow->title = _strdup("RdpRailWindow"))) + WLog_ERR(TAG, "failed to duplicate default window title string"); + } + + if (!appWindow->title) + { + free(appWindow); + return FALSE; + } + + xf_AppWindowInit(xfc, appWindow); + } + + if (!appWindow) + return FALSE; + + /* Keep track of any position/size update so that we can force a refresh of the window */ + if ((fieldFlags & WINDOW_ORDER_FIELD_WND_OFFSET) || + (fieldFlags & WINDOW_ORDER_FIELD_WND_SIZE) || + (fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_OFFSET) || + (fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_SIZE) || + (fieldFlags & WINDOW_ORDER_FIELD_WND_CLIENT_DELTA) || + (fieldFlags & WINDOW_ORDER_FIELD_VIS_OFFSET) || + (fieldFlags & WINDOW_ORDER_FIELD_VISIBILITY)) + { + position_or_size_updated = TRUE; + } + + /* Update Parameters */ + + if (fieldFlags & WINDOW_ORDER_FIELD_WND_OFFSET) + { + appWindow->windowOffsetX = windowState->windowOffsetX; + appWindow->windowOffsetY = windowState->windowOffsetY; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_WND_SIZE) + { + appWindow->windowWidth = windowState->windowWidth; + appWindow->windowHeight = windowState->windowHeight; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_RESIZE_MARGIN_X) + { + appWindow->resizeMarginLeft = windowState->resizeMarginLeft; + appWindow->resizeMarginRight = windowState->resizeMarginRight; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_RESIZE_MARGIN_Y) + { + appWindow->resizeMarginTop = windowState->resizeMarginTop; + appWindow->resizeMarginBottom = windowState->resizeMarginBottom; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_OWNER) + { + appWindow->ownerWindowId = windowState->ownerWindowId; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_STYLE) + { + appWindow->dwStyle = windowState->style; + appWindow->dwExStyle = windowState->extendedStyle; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_SHOW) + { + appWindow->showState = windowState->showState; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_TITLE) + { + char* title = NULL; + union + { + WCHAR* wc; + BYTE* b; + } cnv; + + cnv.b = windowState->titleInfo.string; + if (windowState->titleInfo.length == 0) + { + if (!(title = _strdup(""))) + { + WLog_ERR(TAG, "failed to duplicate empty window title string"); + return FALSE; + } + } + else if (!(title = ConvertWCharNToUtf8Alloc( + cnv.wc, windowState->titleInfo.length / sizeof(WCHAR), NULL))) + { + WLog_ERR(TAG, "failed to convert window title"); + return FALSE; + } + + free(appWindow->title); + appWindow->title = title; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_OFFSET) + { + appWindow->clientOffsetX = windowState->clientOffsetX; + appWindow->clientOffsetY = windowState->clientOffsetY; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_SIZE) + { + appWindow->clientAreaWidth = windowState->clientAreaWidth; + appWindow->clientAreaHeight = windowState->clientAreaHeight; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_WND_CLIENT_DELTA) + { + appWindow->windowClientDeltaX = windowState->windowClientDeltaX; + appWindow->windowClientDeltaY = windowState->windowClientDeltaY; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_WND_RECTS) + { + if (appWindow->windowRects) + { + free(appWindow->windowRects); + appWindow->windowRects = NULL; + } + + appWindow->numWindowRects = windowState->numWindowRects; + + if (appWindow->numWindowRects) + { + appWindow->windowRects = + (RECTANGLE_16*)calloc(appWindow->numWindowRects, sizeof(RECTANGLE_16)); + + if (!appWindow->windowRects) + return FALSE; + + CopyMemory(appWindow->windowRects, windowState->windowRects, + appWindow->numWindowRects * sizeof(RECTANGLE_16)); + } + } + + if (fieldFlags & WINDOW_ORDER_FIELD_VIS_OFFSET) + { + appWindow->visibleOffsetX = windowState->visibleOffsetX; + appWindow->visibleOffsetY = windowState->visibleOffsetY; + } + + if (fieldFlags & WINDOW_ORDER_FIELD_VISIBILITY) + { + if (appWindow->visibilityRects) + { + free(appWindow->visibilityRects); + appWindow->visibilityRects = NULL; + } + + appWindow->numVisibilityRects = windowState->numVisibilityRects; + + if (appWindow->numVisibilityRects) + { + appWindow->visibilityRects = + (RECTANGLE_16*)calloc(appWindow->numVisibilityRects, sizeof(RECTANGLE_16)); + + if (!appWindow->visibilityRects) + return FALSE; + + CopyMemory(appWindow->visibilityRects, windowState->visibilityRects, + appWindow->numVisibilityRects * sizeof(RECTANGLE_16)); + } + } + + /* Update Window */ + + if (fieldFlags & WINDOW_ORDER_FIELD_STYLE) + { + } + + if (fieldFlags & WINDOW_ORDER_FIELD_SHOW) + { + xf_ShowWindow(xfc, appWindow, appWindow->showState); + } + + if (fieldFlags & WINDOW_ORDER_FIELD_TITLE) + { + if (appWindow->title) + xf_SetWindowText(xfc, appWindow, appWindow->title); + } + + if (position_or_size_updated) + { + UINT32 visibilityRectsOffsetX = + (appWindow->visibleOffsetX - + (appWindow->clientOffsetX - appWindow->windowClientDeltaX)); + UINT32 visibilityRectsOffsetY = + (appWindow->visibleOffsetY - + (appWindow->clientOffsetY - appWindow->windowClientDeltaY)); + + /* + * The rail server like to set the window to a small size when it is minimized even though + * it is hidden in some cases this can cause the window not to restore back to its original + * size. Therefore we don't update our local window when that rail window state is minimized + */ + if (appWindow->rail_state != WINDOW_SHOW_MINIMIZED) + { + /* Redraw window area if already in the correct position */ + if (appWindow->x == (INT64)appWindow->windowOffsetX && + appWindow->y == (INT64)appWindow->windowOffsetY && + appWindow->width == (INT64)appWindow->windowWidth && + appWindow->height == (INT64)appWindow->windowHeight) + { + xf_UpdateWindowArea(xfc, appWindow, 0, 0, appWindow->windowWidth, + appWindow->windowHeight); + } + else + { + xf_MoveWindow(xfc, appWindow, appWindow->windowOffsetX, appWindow->windowOffsetY, + appWindow->windowWidth, appWindow->windowHeight); + } + + xf_SetWindowVisibilityRects(xfc, appWindow, visibilityRectsOffsetX, + visibilityRectsOffsetY, appWindow->visibilityRects, + appWindow->numVisibilityRects); + } + } + + /* We should only be using the visibility rects for shaping the window */ + /*if (fieldFlags & WINDOW_ORDER_FIELD_WND_RECTS) + { + xf_SetWindowRects(xfc, appWindow, appWindow->windowRects, appWindow->numWindowRects); + }*/ + return TRUE; +} + +static BOOL xf_rail_window_delete(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo) +{ + xfContext* xfc = (xfContext*)context; + return xf_rail_del_window(xfc, orderInfo->windowId); +} + +static xfRailIconCache* RailIconCache_New(rdpSettings* settings) +{ + xfRailIconCache* cache = NULL; + cache = calloc(1, sizeof(xfRailIconCache)); + + if (!cache) + return NULL; + + cache->numCaches = freerdp_settings_get_uint32(settings, FreeRDP_RemoteAppNumIconCaches); + cache->numCacheEntries = + freerdp_settings_get_uint32(settings, FreeRDP_RemoteAppNumIconCacheEntries); + cache->entries = calloc(1ull * cache->numCaches * cache->numCacheEntries, sizeof(xfRailIcon)); + + if (!cache->entries) + { + WLog_ERR(TAG, "failed to allocate icon cache %" PRIu32 " x %" PRIu32 " entries", + cache->numCaches, cache->numCacheEntries); + free(cache); + return NULL; + } + + return cache; +} + +static void RailIconCache_Free(xfRailIconCache* cache) +{ + if (cache) + { + for (UINT32 i = 0; i < cache->numCaches * cache->numCacheEntries; i++) + { + free(cache->entries[i].data); + } + + free(cache->scratch.data); + free(cache->entries); + free(cache); + } +} + +static xfRailIcon* RailIconCache_Lookup(xfRailIconCache* cache, UINT8 cacheId, UINT16 cacheEntry) +{ + /* + * MS-RDPERP 2.2.1.2.3 Icon Info (TS_ICON_INFO) + * + * CacheId (1 byte): + * If the value is 0xFFFF, the icon SHOULD NOT be cached. + * + * Yes, the spec says "0xFFFF" in the 2018-03-16 revision, + * but the actual protocol field is 1-byte wide. + */ + if (cacheId == 0xFF) + return &cache->scratch; + + if (cacheId >= cache->numCaches) + return NULL; + + if (cacheEntry >= cache->numCacheEntries) + return NULL; + + return &cache->entries[cache->numCacheEntries * cacheId + cacheEntry]; +} + +/* + * _NET_WM_ICON format is defined as "array of CARDINAL" values which for + * Xlib must be represented with an array of C's "long" values. Note that + * "long" != "INT32" on 64-bit systems. Therefore we can't simply cast + * the bitmap data as (unsigned char*), we have to copy all the pixels. + * + * The first two values are width and height followed by actual color data + * in ARGB format (e.g., 0xFFFF0000L is opaque red), pixels are in normal, + * left-to-right top-down order. + */ +static BOOL convert_rail_icon(const ICON_INFO* iconInfo, xfRailIcon* railIcon) +{ + BYTE* argbPixels = NULL; + BYTE* nextPixel = NULL; + long* pixels = NULL; + int nelements = 0; + argbPixels = calloc(1ull * iconInfo->width * iconInfo->height, 4); + + if (!argbPixels) + goto error; + + if (!freerdp_image_copy_from_icon_data( + argbPixels, PIXEL_FORMAT_ARGB32, 0, 0, 0, iconInfo->width, iconInfo->height, + iconInfo->bitsColor, iconInfo->cbBitsColor, iconInfo->bitsMask, iconInfo->cbBitsMask, + iconInfo->colorTable, iconInfo->cbColorTable, iconInfo->bpp)) + goto error; + + nelements = 2 + iconInfo->width * iconInfo->height; + pixels = realloc(railIcon->data, nelements * sizeof(long)); + + if (!pixels) + goto error; + + railIcon->data = pixels; + railIcon->length = nelements; + pixels[0] = iconInfo->width; + pixels[1] = iconInfo->height; + nextPixel = argbPixels; + + for (int i = 2; i < nelements; i++) + { + pixels[i] = FreeRDPReadColor(nextPixel, PIXEL_FORMAT_BGRA32); + nextPixel += 4; + } + + free(argbPixels); + return TRUE; +error: + free(argbPixels); + return FALSE; +} + +static void xf_rail_set_window_icon(xfContext* xfc, xfAppWindow* railWindow, xfRailIcon* icon, + BOOL replace) +{ + WINPR_ASSERT(xfc); + + LogTagAndXChangeProperty(TAG, xfc->display, railWindow->handle, xfc->_NET_WM_ICON, XA_CARDINAL, + 32, replace ? PropModeReplace : PropModeAppend, + (unsigned char*)icon->data, icon->length); + XFlush(xfc->display); +} + +static BOOL xf_rail_window_icon(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const WINDOW_ICON_ORDER* windowIcon) +{ + xfContext* xfc = (xfContext*)context; + xfAppWindow* railWindow = NULL; + xfRailIcon* icon = NULL; + BOOL replaceIcon = 0; + railWindow = xf_rail_get_window(xfc, orderInfo->windowId); + + if (!railWindow) + return TRUE; + + icon = RailIconCache_Lookup(xfc->railIconCache, windowIcon->iconInfo->cacheId, + windowIcon->iconInfo->cacheEntry); + + if (!icon) + { + WLog_WARN(TAG, "failed to get icon from cache %02X:%04X", windowIcon->iconInfo->cacheId, + windowIcon->iconInfo->cacheEntry); + return FALSE; + } + + if (!convert_rail_icon(windowIcon->iconInfo, icon)) + { + WLog_WARN(TAG, "failed to convert icon for window %08X", orderInfo->windowId); + return FALSE; + } + + replaceIcon = !!(orderInfo->fieldFlags & WINDOW_ORDER_STATE_NEW); + xf_rail_set_window_icon(xfc, railWindow, icon, replaceIcon); + return TRUE; +} + +static BOOL xf_rail_window_cached_icon(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const WINDOW_CACHED_ICON_ORDER* windowCachedIcon) +{ + xfContext* xfc = (xfContext*)context; + xfAppWindow* railWindow = NULL; + xfRailIcon* icon = NULL; + BOOL replaceIcon = 0; + railWindow = xf_rail_get_window(xfc, orderInfo->windowId); + + if (!railWindow) + return TRUE; + + icon = RailIconCache_Lookup(xfc->railIconCache, windowCachedIcon->cachedIcon.cacheId, + windowCachedIcon->cachedIcon.cacheEntry); + + if (!icon) + { + WLog_WARN(TAG, "failed to get icon from cache %02X:%04X", + windowCachedIcon->cachedIcon.cacheId, windowCachedIcon->cachedIcon.cacheEntry); + return FALSE; + } + + replaceIcon = !!(orderInfo->fieldFlags & WINDOW_ORDER_STATE_NEW); + xf_rail_set_window_icon(xfc, railWindow, icon, replaceIcon); + return TRUE; +} + +static BOOL xf_rail_notify_icon_common(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const NOTIFY_ICON_STATE_ORDER* notifyIconState) +{ + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_NOTIFY_VERSION) + { + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_NOTIFY_TIP) + { + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_NOTIFY_INFO_TIP) + { + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_NOTIFY_STATE) + { + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_ICON) + { + } + + if (orderInfo->fieldFlags & WINDOW_ORDER_CACHED_ICON) + { + } + + return TRUE; +} + +static BOOL xf_rail_notify_icon_create(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const NOTIFY_ICON_STATE_ORDER* notifyIconState) +{ + return xf_rail_notify_icon_common(context, orderInfo, notifyIconState); +} + +static BOOL xf_rail_notify_icon_update(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const NOTIFY_ICON_STATE_ORDER* notifyIconState) +{ + return xf_rail_notify_icon_common(context, orderInfo, notifyIconState); +} + +static BOOL xf_rail_notify_icon_delete(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo) +{ + return TRUE; +} + +static BOOL xf_rail_monitored_desktop(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const MONITORED_DESKTOP_ORDER* monitoredDesktop) +{ + return TRUE; +} + +static BOOL xf_rail_non_monitored_desktop(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo) +{ + xfContext* xfc = (xfContext*)context; + xf_rail_disable_remoteapp_mode(xfc); + return TRUE; +} + +static void xf_rail_register_update_callbacks(rdpUpdate* update) +{ + rdpWindowUpdate* window = update->window; + window->WindowCreate = xf_rail_window_common; + window->WindowUpdate = xf_rail_window_common; + window->WindowDelete = xf_rail_window_delete; + window->WindowIcon = xf_rail_window_icon; + window->WindowCachedIcon = xf_rail_window_cached_icon; + window->NotifyIconCreate = xf_rail_notify_icon_create; + window->NotifyIconUpdate = xf_rail_notify_icon_update; + window->NotifyIconDelete = xf_rail_notify_icon_delete; + window->MonitoredDesktop = xf_rail_monitored_desktop; + window->NonMonitoredDesktop = xf_rail_non_monitored_desktop; +} + +/* RemoteApp Virtual Channel Extension */ + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_rail_server_execute_result(RailClientContext* context, + const RAIL_EXEC_RESULT_ORDER* execResult) +{ + xfContext* xfc = NULL; + + WINPR_ASSERT(context); + WINPR_ASSERT(execResult); + + xfc = (xfContext*)context->custom; + WINPR_ASSERT(xfc); + + if (execResult->execResult != RAIL_EXEC_S_OK) + { + WLog_ERR(TAG, "RAIL exec error: execResult=%s NtError=0x%X\n", + error_code_names[execResult->execResult], execResult->rawResult); + freerdp_abort_connect_context(&xfc->common.context); + } + else + { + xf_rail_enable_remoteapp_mode(xfc); + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_rail_server_system_param(RailClientContext* context, + const RAIL_SYSPARAM_ORDER* sysparam) +{ + // TODO: Actually apply param + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_rail_server_handshake(RailClientContext* context, + const RAIL_HANDSHAKE_ORDER* handshake) +{ + return client_rail_server_start_cmd(context); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_rail_server_handshake_ex(RailClientContext* context, + const RAIL_HANDSHAKE_EX_ORDER* handshakeEx) +{ + return client_rail_server_start_cmd(context); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_rail_server_local_move_size(RailClientContext* context, + const RAIL_LOCALMOVESIZE_ORDER* localMoveSize) +{ + int x = 0; + int y = 0; + int direction = 0; + Window child_window = 0; + xfContext* xfc = (xfContext*)context->custom; + xfAppWindow* appWindow = xf_rail_get_window(xfc, localMoveSize->windowId); + + if (!appWindow) + return ERROR_INTERNAL_ERROR; + + switch (localMoveSize->moveSizeType) + { + case RAIL_WMSZ_LEFT: + direction = _NET_WM_MOVERESIZE_SIZE_LEFT; + x = localMoveSize->posX; + y = localMoveSize->posY; + break; + + case RAIL_WMSZ_RIGHT: + direction = _NET_WM_MOVERESIZE_SIZE_RIGHT; + x = localMoveSize->posX; + y = localMoveSize->posY; + break; + + case RAIL_WMSZ_TOP: + direction = _NET_WM_MOVERESIZE_SIZE_TOP; + x = localMoveSize->posX; + y = localMoveSize->posY; + break; + + case RAIL_WMSZ_TOPLEFT: + direction = _NET_WM_MOVERESIZE_SIZE_TOPLEFT; + x = localMoveSize->posX; + y = localMoveSize->posY; + break; + + case RAIL_WMSZ_TOPRIGHT: + direction = _NET_WM_MOVERESIZE_SIZE_TOPRIGHT; + x = localMoveSize->posX; + y = localMoveSize->posY; + break; + + case RAIL_WMSZ_BOTTOM: + direction = _NET_WM_MOVERESIZE_SIZE_BOTTOM; + x = localMoveSize->posX; + y = localMoveSize->posY; + break; + + case RAIL_WMSZ_BOTTOMLEFT: + direction = _NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT; + x = localMoveSize->posX; + y = localMoveSize->posY; + break; + + case RAIL_WMSZ_BOTTOMRIGHT: + direction = _NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT; + x = localMoveSize->posX; + y = localMoveSize->posY; + break; + + case RAIL_WMSZ_MOVE: + direction = _NET_WM_MOVERESIZE_MOVE; + XTranslateCoordinates(xfc->display, appWindow->handle, RootWindowOfScreen(xfc->screen), + localMoveSize->posX, localMoveSize->posY, &x, &y, &child_window); + break; + + case RAIL_WMSZ_KEYMOVE: + direction = _NET_WM_MOVERESIZE_MOVE_KEYBOARD; + x = localMoveSize->posX; + y = localMoveSize->posY; + /* FIXME: local keyboard moves not working */ + return CHANNEL_RC_OK; + + case RAIL_WMSZ_KEYSIZE: + direction = _NET_WM_MOVERESIZE_SIZE_KEYBOARD; + x = localMoveSize->posX; + y = localMoveSize->posY; + /* FIXME: local keyboard moves not working */ + return CHANNEL_RC_OK; + } + + if (localMoveSize->isMoveSizeStart) + xf_StartLocalMoveSize(xfc, appWindow, direction, x, y); + else + xf_EndLocalMoveSize(xfc, appWindow); + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_rail_server_min_max_info(RailClientContext* context, + const RAIL_MINMAXINFO_ORDER* minMaxInfo) +{ + xfContext* xfc = (xfContext*)context->custom; + xfAppWindow* appWindow = xf_rail_get_window(xfc, minMaxInfo->windowId); + + if (appWindow) + { + xf_SetWindowMinMaxInfo(xfc, appWindow, minMaxInfo->maxWidth, minMaxInfo->maxHeight, + minMaxInfo->maxPosX, minMaxInfo->maxPosY, minMaxInfo->minTrackWidth, + minMaxInfo->minTrackHeight, minMaxInfo->maxTrackWidth, + minMaxInfo->maxTrackHeight); + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_rail_server_language_bar_info(RailClientContext* context, + const RAIL_LANGBAR_INFO_ORDER* langBarInfo) +{ + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT xf_rail_server_get_appid_response(RailClientContext* context, + const RAIL_GET_APPID_RESP_ORDER* getAppIdResp) +{ + return CHANNEL_RC_OK; +} + +static BOOL rail_window_key_equals(const void* key1, const void* key2) +{ + const UINT64* k1 = (const UINT64*)key1; + const UINT64* k2 = (const UINT64*)key2; + + if (!k1 || !k2) + return FALSE; + + return *k1 == *k2; +} + +static UINT32 rail_window_key_hash(const void* key) +{ + const UINT64* k1 = (const UINT64*)key; + return (UINT32)*k1; +} + +static void rail_window_free(void* value) +{ + xfAppWindow* appWindow = (xfAppWindow*)value; + + if (!appWindow) + return; + + xf_DestroyWindow(appWindow->xfc, appWindow); +} + +int xf_rail_init(xfContext* xfc, RailClientContext* rail) +{ + rdpContext* context = (rdpContext*)xfc; + + if (!xfc || !rail) + return 0; + + xfc->rail = rail; + xf_rail_register_update_callbacks(context->update); + rail->custom = (void*)xfc; + rail->ServerExecuteResult = xf_rail_server_execute_result; + rail->ServerSystemParam = xf_rail_server_system_param; + rail->ServerHandshake = xf_rail_server_handshake; + rail->ServerHandshakeEx = xf_rail_server_handshake_ex; + rail->ServerLocalMoveSize = xf_rail_server_local_move_size; + rail->ServerMinMaxInfo = xf_rail_server_min_max_info; + rail->ServerLanguageBarInfo = xf_rail_server_language_bar_info; + rail->ServerGetAppIdResponse = xf_rail_server_get_appid_response; + xfc->railWindows = HashTable_New(TRUE); + + if (!xfc->railWindows) + return 0; + + if (!HashTable_SetHashFunction(xfc->railWindows, rail_window_key_hash)) + goto fail; + { + wObject* obj = HashTable_KeyObject(xfc->railWindows); + obj->fnObjectEquals = rail_window_key_equals; + } + { + wObject* obj = HashTable_ValueObject(xfc->railWindows); + obj->fnObjectFree = rail_window_free; + } + xfc->railIconCache = RailIconCache_New(xfc->common.context.settings); + + if (!xfc->railIconCache) + { + } + + return 1; +fail: + HashTable_Free(xfc->railWindows); + return 0; +} + +int xf_rail_uninit(xfContext* xfc, RailClientContext* rail) +{ + WINPR_UNUSED(rail); + + if (xfc->rail) + { + xfc->rail->custom = NULL; + xfc->rail = NULL; + } + + if (xfc->railWindows) + { + HashTable_Free(xfc->railWindows); + xfc->railWindows = NULL; + } + + if (xfc->railIconCache) + { + RailIconCache_Free(xfc->railIconCache); + xfc->railIconCache = NULL; + } + + return 1; +} + +xfAppWindow* xf_rail_add_window(xfContext* xfc, UINT64 id, UINT32 x, UINT32 y, UINT32 width, + UINT32 height, UINT32 surfaceId) +{ + xfAppWindow* appWindow = NULL; + + if (!xfc) + return NULL; + + appWindow = (xfAppWindow*)calloc(1, sizeof(xfAppWindow)); + + if (!appWindow) + return NULL; + + appWindow->xfc = xfc; + appWindow->windowId = id; + appWindow->surfaceId = surfaceId; + appWindow->x = x; + appWindow->y = y; + appWindow->width = width; + appWindow->height = height; + + if (!xf_AppWindowCreate(xfc, appWindow)) + goto fail; + if (!HashTable_Insert(xfc->railWindows, &appWindow->windowId, (void*)appWindow)) + goto fail; + return appWindow; +fail: + rail_window_free(appWindow); + return NULL; +} + +BOOL xf_rail_del_window(xfContext* xfc, UINT64 id) +{ + if (!xfc) + return FALSE; + + if (!xfc->railWindows) + return FALSE; + + return HashTable_Remove(xfc->railWindows, &id); +} + +xfAppWindow* xf_rail_get_window(xfContext* xfc, UINT64 id) +{ + if (!xfc) + return NULL; + + if (!xfc->railWindows) + return FALSE; + + return HashTable_GetItemValue(xfc->railWindows, &id); +} diff --git a/client/X11/xf_rail.h b/client/X11/xf_rail.h new file mode 100644 index 0000000..32b5f44 --- /dev/null +++ b/client/X11/xf_rail.h @@ -0,0 +1,47 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 RAIL + * + * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_X11_RAIL_H +#define FREERDP_CLIENT_X11_RAIL_H + +#include "xf_client.h" +#include "xfreerdp.h" + +#include <freerdp/client/rail.h> + +BOOL xf_rail_paint(xfContext* xfc, const RECTANGLE_16* rect); +BOOL xf_rail_paint_surface(xfContext* xfc, UINT64 windowId, const RECTANGLE_16* rect); + +void xf_rail_send_client_system_command(xfContext* xfc, UINT32 windowId, UINT16 command); +void xf_rail_send_activate(xfContext* xfc, Window xwindow, BOOL enabled); +void xf_rail_adjust_position(xfContext* xfc, xfAppWindow* appWindow); +void xf_rail_end_local_move(xfContext* xfc, xfAppWindow* appWindow); +void xf_rail_enable_remoteapp_mode(xfContext* xfc); +void xf_rail_disable_remoteapp_mode(xfContext* xfc); + +xfAppWindow* xf_rail_add_window(xfContext* xfc, UINT64 id, UINT32 x, UINT32 y, UINT32 width, + UINT32 height, UINT32 surfaceId); +xfAppWindow* xf_rail_get_window(xfContext* xfc, UINT64 id); + +BOOL xf_rail_del_window(xfContext* xfc, UINT64 id); + +int xf_rail_init(xfContext* xfc, RailClientContext* rail); +int xf_rail_uninit(xfContext* xfc, RailClientContext* rail); + +#endif /* FREERDP_CLIENT_X11_RAIL_H */ diff --git a/client/X11/xf_tsmf.c b/client/X11/xf_tsmf.c new file mode 100644 index 0000000..1c5e5b3 --- /dev/null +++ b/client/X11/xf_tsmf.c @@ -0,0 +1,471 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Video Redirection + * + * Copyright 2010-2011 Vic Lee + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <winpr/crt.h> + +#include <sys/ipc.h> +#include <sys/shm.h> + +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <X11/Xatom.h> +#include <X11/extensions/XShm.h> + +#include <freerdp/log.h> +#include <freerdp/client/tsmf.h> + +#include "xf_tsmf.h" + +#ifdef WITH_XV + +#include <X11/extensions/Xv.h> +#include <X11/extensions/Xvlib.h> + +static long xv_port = 0; + +struct xf_xv_context +{ + long xv_port; + Atom xv_colorkey_atom; + int xv_image_size; + int xv_shmid; + char* xv_shmaddr; + UINT32* xv_pixfmts; +}; +typedef struct xf_xv_context xfXvContext; + +#define TAG CLIENT_TAG("x11") + +static BOOL xf_tsmf_is_format_supported(xfXvContext* xv, UINT32 pixfmt) +{ + if (!xv->xv_pixfmts) + return FALSE; + + for (int i = 0; xv->xv_pixfmts[i]; i++) + { + if (xv->xv_pixfmts[i] == pixfmt) + return TRUE; + } + + return FALSE; +} + +static int xf_tsmf_xv_video_frame_event(TsmfClientContext* tsmf, TSMF_VIDEO_FRAME_EVENT* event) +{ + int x = 0; + int y = 0; + UINT32 width = 0; + UINT32 height = 0; + BYTE* data1 = NULL; + BYTE* data2 = NULL; + UINT32 pixfmt = 0; + UINT32 xvpixfmt = 0; + XvImage* image = NULL; + int colorkey = 0; + int numRects = 0; + xfContext* xfc = NULL; + xfXvContext* xv = NULL; + XRectangle* xrects = NULL; + XShmSegmentInfo shminfo; + BOOL converti420yv12 = FALSE; + + if (!tsmf) + return -1; + + xfc = (xfContext*)tsmf->custom; + + if (!xfc) + return -1; + + xv = (xfXvContext*)xfc->xv_context; + + if (!xv) + return -1; + + if (xv->xv_port == 0) + return -1001; + + /* In case the player is minimized */ + if (event->x < -2048 || event->y < -2048 || event->numVisibleRects == 0) + { + return -1002; + } + + xrects = NULL; + numRects = event->numVisibleRects; + + if (numRects > 0) + { + xrects = (XRectangle*)calloc(numRects, sizeof(XRectangle)); + + if (!xrects) + return -1; + + for (int i = 0; i < numRects; i++) + { + x = event->x + event->visibleRects[i].left; + y = event->y + event->visibleRects[i].top; + width = event->visibleRects[i].right - event->visibleRects[i].left; + height = event->visibleRects[i].bottom - event->visibleRects[i].top; + + xrects[i].x = x; + xrects[i].y = y; + xrects[i].width = width; + xrects[i].height = height; + } + } + + if (xv->xv_colorkey_atom != None) + { + XvGetPortAttribute(xfc->display, xv->xv_port, xv->xv_colorkey_atom, &colorkey); + XSetFunction(xfc->display, xfc->gc, GXcopy); + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + XSetForeground(xfc->display, xfc->gc, colorkey); + + if (event->numVisibleRects < 1) + { + XSetClipMask(xfc->display, xfc->gc, None); + } + else + { + XFillRectangles(xfc->display, xfc->window->handle, xfc->gc, xrects, numRects); + } + } + else + { + XSetFunction(xfc->display, xfc->gc, GXcopy); + XSetFillStyle(xfc->display, xfc->gc, FillSolid); + + if (event->numVisibleRects < 1) + { + XSetClipMask(xfc->display, xfc->gc, None); + } + else + { + XSetClipRectangles(xfc->display, xfc->gc, 0, 0, xrects, numRects, YXBanded); + } + } + + pixfmt = event->framePixFmt; + + if (xf_tsmf_is_format_supported(xv, pixfmt)) + { + xvpixfmt = pixfmt; + } + else if (pixfmt == RDP_PIXFMT_I420 && xf_tsmf_is_format_supported(xv, RDP_PIXFMT_YV12)) + { + xvpixfmt = RDP_PIXFMT_YV12; + converti420yv12 = TRUE; + } + else if (pixfmt == RDP_PIXFMT_YV12 && xf_tsmf_is_format_supported(xv, RDP_PIXFMT_I420)) + { + xvpixfmt = RDP_PIXFMT_I420; + converti420yv12 = TRUE; + } + else + { + WLog_DBG(TAG, "pixel format 0x%" PRIX32 " not supported by hardware.", pixfmt); + free(xrects); + return -1003; + } + + image = XvShmCreateImage(xfc->display, xv->xv_port, xvpixfmt, 0, event->frameWidth, + event->frameHeight, &shminfo); + + if (xv->xv_image_size != image->data_size) + { + if (xv->xv_image_size > 0) + { + shmdt(xv->xv_shmaddr); + shmctl(xv->xv_shmid, IPC_RMID, NULL); + } + + xv->xv_image_size = image->data_size; + xv->xv_shmid = shmget(IPC_PRIVATE, image->data_size, IPC_CREAT | 0777); + xv->xv_shmaddr = shmat(xv->xv_shmid, 0, 0); + } + + shminfo.shmid = xv->xv_shmid; + shminfo.shmaddr = image->data = xv->xv_shmaddr; + shminfo.readOnly = FALSE; + + if (!XShmAttach(xfc->display, &shminfo)) + { + XFree(image); + free(xrects); + WLog_DBG(TAG, "XShmAttach failed."); + return -1004; + } + + /* The video driver may align each line to a different size + and we need to convert our original image data. */ + switch (pixfmt) + { + case RDP_PIXFMT_I420: + case RDP_PIXFMT_YV12: + /* Y */ + if (image->pitches[0] == event->frameWidth) + { + CopyMemory(image->data + image->offsets[0], event->frameData, + event->frameWidth * event->frameHeight); + } + else + { + for (int i = 0; i < event->frameHeight; i++) + { + CopyMemory(image->data + image->offsets[0] + i * image->pitches[0], + event->frameData + i * event->frameWidth, event->frameWidth); + } + } + /* UV */ + /* Conversion between I420 and YV12 is to simply swap U and V */ + if (!converti420yv12) + { + data1 = event->frameData + event->frameWidth * event->frameHeight; + data2 = event->frameData + event->frameWidth * event->frameHeight + + event->frameWidth * event->frameHeight / 4; + } + else + { + data2 = event->frameData + event->frameWidth * event->frameHeight; + data1 = event->frameData + event->frameWidth * event->frameHeight + + event->frameWidth * event->frameHeight / 4; + image->id = pixfmt == RDP_PIXFMT_I420 ? RDP_PIXFMT_YV12 : RDP_PIXFMT_I420; + } + + if (image->pitches[1] * 2 == event->frameWidth) + { + CopyMemory(image->data + image->offsets[1], data1, + event->frameWidth * event->frameHeight / 4); + CopyMemory(image->data + image->offsets[2], data2, + event->frameWidth * event->frameHeight / 4); + } + else + { + for (int i = 0; i < event->frameHeight / 2; i++) + { + CopyMemory(image->data + image->offsets[1] + i * image->pitches[1], + data1 + i * event->frameWidth / 2, event->frameWidth / 2); + CopyMemory(image->data + image->offsets[2] + i * image->pitches[2], + data2 + i * event->frameWidth / 2, event->frameWidth / 2); + } + } + break; + + default: + if (image->data_size < 0) + { + free(xrects); + return -2000; + } + else + { + const size_t size = ((UINT32)image->data_size <= event->frameSize) + ? (UINT32)image->data_size + : event->frameSize; + CopyMemory(image->data, event->frameData, size); + } + break; + } + + XvShmPutImage(xfc->display, xv->xv_port, xfc->window->handle, xfc->gc, image, 0, 0, + image->width, image->height, event->x, event->y, event->width, event->height, + FALSE); + + if (xv->xv_colorkey_atom == None) + XSetClipMask(xfc->display, xfc->gc, None); + + XSync(xfc->display, FALSE); + + XShmDetach(xfc->display, &shminfo); + XFree(image); + + free(xrects); + + return 1; +} + +static int xf_tsmf_xv_init(xfContext* xfc, TsmfClientContext* tsmf) +{ + int ret = 0; + unsigned int version = 0; + unsigned int release = 0; + unsigned int event_base = 0; + unsigned int error_base = 0; + unsigned int request_base = 0; + unsigned int num_adaptors = 0; + xfXvContext* xv = NULL; + XvAdaptorInfo* ai = NULL; + XvAttribute* attr = NULL; + XvImageFormatValues* fo = NULL; + + if (xfc->xv_context) + return 1; /* context already created */ + + xv = (xfXvContext*)calloc(1, sizeof(xfXvContext)); + + if (!xv) + return -1; + + xfc->xv_context = xv; + + xv->xv_colorkey_atom = None; + xv->xv_image_size = 0; + xv->xv_port = xv_port; + + if (!XShmQueryExtension(xfc->display)) + { + WLog_DBG(TAG, "no xshm available."); + return -1; + } + + ret = + XvQueryExtension(xfc->display, &version, &release, &request_base, &event_base, &error_base); + + if (ret != Success) + { + WLog_DBG(TAG, "XvQueryExtension failed %d.", ret); + return -1; + } + + WLog_DBG(TAG, "version %u release %u", version, release); + + ret = XvQueryAdaptors(xfc->display, DefaultRootWindow(xfc->display), &num_adaptors, &ai); + + if (ret != Success) + { + WLog_DBG(TAG, "XvQueryAdaptors failed %d.", ret); + return -1; + } + + for (unsigned int i = 0; i < num_adaptors; i++) + { + WLog_DBG(TAG, "adapter port %lu-%lu (%s)", ai[i].base_id, + ai[i].base_id + ai[i].num_ports - 1, ai[i].name); + + if (xv->xv_port == 0 && i == num_adaptors - 1) + xv->xv_port = ai[i].base_id; + } + + if (num_adaptors > 0) + XvFreeAdaptorInfo(ai); + + if (xv->xv_port == 0) + { + WLog_DBG(TAG, "no adapter selected, video frames will not be processed."); + return -1; + } + WLog_DBG(TAG, "selected %ld", xv->xv_port); + + attr = XvQueryPortAttributes(xfc->display, xv->xv_port, &ret); + + unsigned int i = 0; + for (; i < (unsigned int)ret; i++) + { + if (strcmp(attr[i].name, "XV_COLORKEY") == 0) + { + xv->xv_colorkey_atom = XInternAtom(xfc->display, "XV_COLORKEY", FALSE); + XvSetPortAttribute(xfc->display, xv->xv_port, xv->xv_colorkey_atom, + attr[i].min_value + 1); + break; + } + } + XFree(attr); + + WLog_DBG(TAG, "xf_tsmf_init: pixel format "); + + fo = XvListImageFormats(xfc->display, xv->xv_port, &ret); + + if (ret > 0) + { + xv->xv_pixfmts = (UINT32*)calloc((ret + 1), sizeof(UINT32)); + + for (unsigned int i = 0; i < (unsigned int)ret; i++) + { + xv->xv_pixfmts[i] = fo[i].id; + WLog_DBG(TAG, "%c%c%c%c ", ((char*)(xv->xv_pixfmts + i))[0], + ((char*)(xv->xv_pixfmts + i))[1], ((char*)(xv->xv_pixfmts + i))[2], + ((char*)(xv->xv_pixfmts + i))[3]); + } + xv->xv_pixfmts[i] = 0; + } + XFree(fo); + + if (tsmf) + { + xfc->tsmf = tsmf; + tsmf->custom = (void*)xfc; + + tsmf->FrameEvent = xf_tsmf_xv_video_frame_event; + } + + return 1; +} + +static int xf_tsmf_xv_uninit(xfContext* xfc, TsmfClientContext* tsmf) +{ + xfXvContext* xv = (xfXvContext*)xfc->xv_context; + + WINPR_UNUSED(tsmf); + if (xv) + { + if (xv->xv_image_size > 0) + { + shmdt(xv->xv_shmaddr); + shmctl(xv->xv_shmid, IPC_RMID, NULL); + } + if (xv->xv_pixfmts) + { + free(xv->xv_pixfmts); + xv->xv_pixfmts = NULL; + } + free(xv); + xfc->xv_context = NULL; + } + + if (xfc->tsmf) + { + xfc->tsmf->custom = NULL; + xfc->tsmf = NULL; + } + + return 1; +} + +#endif + +int xf_tsmf_init(xfContext* xfc, TsmfClientContext* tsmf) +{ +#ifdef WITH_XV + return xf_tsmf_xv_init(xfc, tsmf); +#else + return 1; +#endif +} + +int xf_tsmf_uninit(xfContext* xfc, TsmfClientContext* tsmf) +{ +#ifdef WITH_XV + return xf_tsmf_xv_uninit(xfc, tsmf); +#else + return 1; +#endif +} diff --git a/client/X11/xf_tsmf.h b/client/X11/xf_tsmf.h new file mode 100644 index 0000000..63a973a --- /dev/null +++ b/client/X11/xf_tsmf.h @@ -0,0 +1,29 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Video Redirection + * + * Copyright 2010-2011 Vic Lee + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_X11_TSMF_H +#define FREERDP_CLIENT_X11_TSMF_H + +#include "xf_client.h" +#include "xfreerdp.h" + +int xf_tsmf_init(xfContext* xfc, TsmfClientContext* tsmf); +int xf_tsmf_uninit(xfContext* xfc, TsmfClientContext* tsmf); + +#endif /* FREERDP_CLIENT_X11_TSMF_H */ diff --git a/client/X11/xf_utils.c b/client/X11/xf_utils.c new file mode 100644 index 0000000..4cce7c1 --- /dev/null +++ b/client/X11/xf_utils.c @@ -0,0 +1,123 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 helper utilities + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * Copyringht 2023 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <winpr/assert.h> + +#include "xf_utils.h" + +static const DWORD log_level = WLOG_TRACE; + +static void write_log(wLog* log, DWORD level, const char* fname, const char* fkt, size_t line, ...) +{ + va_list ap; + va_start(ap, line); + WLog_PrintMessageVA(log, WLOG_MESSAGE_TEXT, level, line, fname, fkt, ap); + va_end(ap); +} + +char* Safe_XGetAtomName(Display* display, Atom atom) +{ + if (atom == None) + return strdup("Atom_None"); + return XGetAtomName(display, atom); +} + +int LogTagAndXChangeProperty_ex(const char* tag, const char* file, const char* fkt, size_t line, + Display* display, Window w, Atom property, Atom type, int format, + int mode, const unsigned char* data, int nelements) +{ + wLog* log = WLog_Get(tag); + return LogDynAndXChangeProperty_ex(log, file, fkt, line, display, w, property, type, format, + mode, data, nelements); +} + +int LogDynAndXChangeProperty_ex(wLog* log, const char* file, const char* fkt, size_t line, + Display* display, Window w, Atom property, Atom type, int format, + int mode, const unsigned char* data, int nelements) +{ + if (WLog_IsLevelActive(log, log_level)) + { + char* propstr = Safe_XGetAtomName(display, property); + char* typestr = Safe_XGetAtomName(display, type); + write_log(log, log_level, file, fkt, line, + "XChangeProperty(%p, %d, %s [%d], %s [%d], %d, %d, %p, %d)", display, w, propstr, + property, typestr, type, format, mode, data, nelements); + XFree(propstr); + XFree(typestr); + } + return XChangeProperty(display, w, property, type, format, mode, data, nelements); +} + +int LogTagAndXDeleteProperty_ex(const char* tag, const char* file, const char* fkt, size_t line, + Display* display, Window w, Atom property) +{ + wLog* log = WLog_Get(tag); + return LogDynAndXDeleteProperty_ex(log, file, fkt, line, display, w, property); +} + +int LogDynAndXDeleteProperty_ex(wLog* log, const char* file, const char* fkt, size_t line, + Display* display, Window w, Atom property) +{ + if (WLog_IsLevelActive(log, log_level)) + { + char* propstr = Safe_XGetAtomName(display, property); + write_log(log, log_level, file, fkt, line, "XDeleteProperty(%p, %d, %s [%d])", display, w, + propstr, property); + XFree(propstr); + } + return XDeleteProperty(display, w, property); +} + +int LogTagAndXGetWindowProperty_ex(const char* tag, const char* file, const char* fkt, size_t line, + Display* display, Window w, Atom property, long long_offset, + long long_length, int delete, Atom req_type, + Atom* actual_type_return, int* actual_format_return, + unsigned long* nitems_return, unsigned long* bytes_after_return, + unsigned char** prop_return) +{ + wLog* log = WLog_Get(tag); + return LogDynAndXGetWindowProperty_ex( + log, file, fkt, line, display, w, property, long_offset, long_length, delete, req_type, + actual_type_return, actual_format_return, nitems_return, bytes_after_return, prop_return); +} + +int LogDynAndXGetWindowProperty_ex(wLog* log, const char* file, const char* fkt, size_t line, + Display* display, Window w, Atom property, long long_offset, + long long_length, int delete, Atom req_type, + Atom* actual_type_return, int* actual_format_return, + unsigned long* nitems_return, unsigned long* bytes_after_return, + unsigned char** prop_return) +{ + if (WLog_IsLevelActive(log, log_level)) + { + char* propstr = Safe_XGetAtomName(display, property); + char* req_type_str = Safe_XGetAtomName(display, req_type); + write_log(log, log_level, file, fkt, line, + "XGetWindowProperty(%p, %d, %s [%d], %ld, %ld, %d, %s [%d], %p, %p, %p, %p, %p)", + display, w, propstr, property, long_offset, long_length, delete, req_type_str, + req_type, actual_type_return, actual_format_return, nitems_return, + bytes_after_return, prop_return); + XFree(propstr); + XFree(req_type_str); + } + return XGetWindowProperty(display, w, property, long_offset, long_length, delete, req_type, + actual_type_return, actual_format_return, nitems_return, + bytes_after_return, prop_return); +} diff --git a/client/X11/xf_utils.h b/client/X11/xf_utils.h new file mode 100644 index 0000000..f946554 --- /dev/null +++ b/client/X11/xf_utils.h @@ -0,0 +1,78 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 helper utilities + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * Copyringht 2023 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <winpr/wlog.h> + +#include <X11/Xlib.h> + +char* Safe_XGetAtomName(Display* display, Atom atom); + +#define LogTagAndXGetWindowProperty(tag, display, w, property, long_offset, long_length, delete, \ + req_type, actual_type_return, actual_format_return, \ + nitems_return, bytes_after_return, prop_return) \ + LogTagAndXGetWindowProperty_ex((tag), __FILE__, __func__, __LINE__, (display), (w), \ + (property), (long_offset), (long_length), (delete), (req_type), \ + (actual_type_return), (actual_format_return), (nitems_return), \ + (bytes_after_return), (prop_return)) +int LogTagAndXGetWindowProperty_ex(const char* tag, const char* file, const char* fkt, size_t line, + Display* display, Window w, Atom property, long long_offset, + long long_length, Bool delete, Atom req_type, + Atom* actual_type_return, int* actual_format_return, + unsigned long* nitems_return, unsigned long* bytes_after_return, + unsigned char** prop_return); + +#define LogDynAndXGetWindowProperty(log, display, w, property, long_offset, long_length, delete, \ + req_type, actual_type_return, actual_format_return, \ + nitems_return, bytes_after_return, prop_return) \ + LogDynAndXGetWindowProperty_ex((log), __FILE__, __func__, __LINE__, (display), (w), \ + (property), (long_offset), (long_length), (delete), (req_type), \ + (actual_type_return), (actual_format_return), (nitems_return), \ + (bytes_after_return), (prop_return)) +int LogDynAndXGetWindowProperty_ex(wLog* log, const char* file, const char* fkt, size_t line, + Display* display, Window w, Atom property, long long_offset, + long long_length, Bool delete, Atom req_type, + Atom* actual_type_return, int* actual_format_return, + unsigned long* nitems_return, unsigned long* bytes_after_return, + unsigned char** prop_return); + +#define LogTagAndXChangeProperty(tag, display, w, property, type, format, mode, data, nelements) \ + LogTagAndXChangeProperty_ex((tag), __FILE__, __func__, __LINE__, (display), (w), (property), \ + (type), (format), (mode), (data), (nelements)) +int LogTagAndXChangeProperty_ex(const char* tag, const char* file, const char* fkt, size_t line, + Display* display, Window w, Atom property, Atom type, int format, + int mode, _Xconst unsigned char* data, int nelements); + +#define LogDynAndXChangeProperty(log, display, w, property, type, format, mode, data, nelements) \ + LogDynAndXChangeProperty_ex((log), __FILE__, __func__, __LINE__, (display), (w), (property), \ + (type), (format), (mode), (data), (nelements)) +int LogDynAndXChangeProperty_ex(wLog* log, const char* file, const char* fkt, size_t line, + Display* display, Window w, Atom property, Atom type, int format, + int mode, _Xconst unsigned char* data, int nelements); + +#define LogTagAndXDeleteProperty(tag, display, w, property) \ + LogTagAndXDeleteProperty_ex((tag), __FILE__, __func__, __LINE__, (display), (w), (property)) +int LogTagAndXDeleteProperty_ex(const char* tag, const char* file, const char* fkt, size_t line, + Display* display, Window w, Atom property); + +#define LogDynAndXDeleteProperty(log, display, w, property) \ + LogDynAndXDeleteProperty_ex((log), __FILE__, __func__, __LINE__, (display), (w), (property)) +int LogDynAndXDeleteProperty_ex(wLog* log, const char* file, const char* fkt, size_t line, + Display* display, Window w, Atom property); diff --git a/client/X11/xf_video.c b/client/X11/xf_video.c new file mode 100644 index 0000000..461f33d --- /dev/null +++ b/client/X11/xf_video.c @@ -0,0 +1,132 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Optimized Remoting Virtual Channel Extension for X11 + * + * Copyright 2017 David Fort <contact@hardening-consulting.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <winpr/assert.h> +#include <freerdp/client/geometry.h> +#include <freerdp/client/video.h> +#include <freerdp/gdi/video.h> + +#include "xf_video.h" + +#include <freerdp/log.h> +#define TAG CLIENT_TAG("video") + +typedef struct +{ + VideoSurface base; + XImage* image; +} xfVideoSurface; + +static VideoSurface* xfVideoCreateSurface(VideoClientContext* video, UINT32 x, UINT32 y, + UINT32 width, UINT32 height) +{ + xfContext* xfc = NULL; + xfVideoSurface* ret = NULL; + + WINPR_ASSERT(video); + ret = (xfVideoSurface*)VideoClient_CreateCommonContext(sizeof(xfContext), x, y, width, height); + if (!ret) + return NULL; + + xfc = (xfContext*)video->custom; + WINPR_ASSERT(xfc); + + ret->image = XCreateImage(xfc->display, xfc->visual, xfc->depth, ZPixmap, 0, + (char*)ret->base.data, width, height, 8, ret->base.scanline); + + if (!ret->image) + { + WLog_ERR(TAG, "unable to create surface image"); + VideoClient_DestroyCommonContext(&ret->base); + return NULL; + } + + return &ret->base; +} + +static BOOL xfVideoShowSurface(VideoClientContext* video, const VideoSurface* surface, + UINT32 destinationWidth, UINT32 destinationHeight) +{ + const xfVideoSurface* xfSurface = (const xfVideoSurface*)surface; + xfContext* xfc = NULL; + const rdpSettings* settings = NULL; + + WINPR_ASSERT(video); + WINPR_ASSERT(xfSurface); + + xfc = video->custom; + WINPR_ASSERT(xfc); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + +#ifdef WITH_XRENDER + + if (freerdp_settings_get_bool(settings, FreeRDP_SmartSizing) || + freerdp_settings_get_bool(settings, FreeRDP_MultiTouchGestures)) + { + XPutImage(xfc->display, xfc->primary, xfc->gc, xfSurface->image, 0, 0, surface->x, + surface->y, surface->w, surface->h); + xf_draw_screen(xfc, surface->x, surface->y, surface->w, surface->h); + } + else +#endif + { + XPutImage(xfc->display, xfc->drawable, xfc->gc, xfSurface->image, 0, 0, surface->x, + surface->y, surface->w, surface->h); + } + + return TRUE; +} + +static BOOL xfVideoDeleteSurface(VideoClientContext* video, VideoSurface* surface) +{ + xfVideoSurface* xfSurface = (xfVideoSurface*)surface; + + WINPR_UNUSED(video); + + if (xfSurface) + XFree(xfSurface->image); + + VideoClient_DestroyCommonContext(surface); + return TRUE; +} + +void xf_video_control_init(xfContext* xfc, VideoClientContext* video) +{ + WINPR_ASSERT(xfc); + WINPR_ASSERT(video); + + gdi_video_control_init(xfc->common.context.gdi, video); + + /* X11 needs to be able to handle 32bpp colors directly. */ + if (xfc->depth >= 24) + { + video->custom = xfc; + video->createSurface = xfVideoCreateSurface; + video->showSurface = xfVideoShowSurface; + video->deleteSurface = xfVideoDeleteSurface; + } +} + +void xf_video_control_uninit(xfContext* xfc, VideoClientContext* video) +{ + WINPR_ASSERT(xfc); + gdi_video_control_uninit(xfc->common.context.gdi, video); +} diff --git a/client/X11/xf_video.h b/client/X11/xf_video.h new file mode 100644 index 0000000..385b4ea --- /dev/null +++ b/client/X11/xf_video.h @@ -0,0 +1,35 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Video Optimized Remoting Virtual Channel Extension for X11 + * + * Copyright 2017 David Fort <contact@hardening-consulting.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef CLIENT_X11_XF_VIDEO_H_ +#define CLIENT_X11_XF_VIDEO_H_ + +#include "xfreerdp.h" + +#include <freerdp/channels/geometry.h> +#include <freerdp/channels/video.h> + +void xf_video_control_init(xfContext* xfc, VideoClientContext* video); +void xf_video_control_uninit(xfContext* xfc, VideoClientContext* video); + +void xf_video_free(xfVideoContext* context); + +WINPR_ATTR_MALLOC(xf_video_free, 1) +xfVideoContext* xf_video_new(xfContext* xfc); + +#endif /* CLIENT_X11_XF_VIDEO_H_ */ diff --git a/client/X11/xf_window.c b/client/X11/xf_window.c new file mode 100644 index 0000000..a5e68c7 --- /dev/null +++ b/client/X11/xf_window.c @@ -0,0 +1,1323 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Windows + * + * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * Copyright 2012 HP Development Company, LLC + * Copyright 2016 Thincast Technologies GmbH + * Copyright 2016 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <stdarg.h> +#include <unistd.h> +#include <sys/types.h> + +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <X11/Xatom.h> + +#include <sys/mman.h> +#include <sys/stat.h> +#include <fcntl.h> + +#include <winpr/assert.h> +#include <winpr/thread.h> +#include <winpr/crt.h> +#include <winpr/string.h> + +#include <freerdp/rail.h> +#include <freerdp/log.h> + +#ifdef WITH_XEXT +#include <X11/extensions/shape.h> +#endif + +#ifdef WITH_XI +#include <X11/extensions/XInput2.h> +#endif + +#include "xf_gfx.h" +#include "xf_rail.h" +#include "xf_input.h" +#include "xf_keyboard.h" +#include "xf_utils.h" + +#define TAG CLIENT_TAG("x11") + +#ifdef WITH_DEBUG_X11 +#define DEBUG_X11(...) WLog_DBG(TAG, __VA_ARGS__) +#else +#define DEBUG_X11(...) \ + do \ + { \ + } while (0) +#endif + +#include <FreeRDP_Icon_256px.h> +#define xf_icon_prop FreeRDP_Icon_256px_prop + +#include "xf_window.h" + +/* Extended Window Manager Hints: http://standards.freedesktop.org/wm-spec/wm-spec-1.3.html */ + +/* bit definitions for MwmHints.flags */ +#define MWM_HINTS_FUNCTIONS (1L << 0) +#define MWM_HINTS_DECORATIONS (1L << 1) +#define MWM_HINTS_INPUT_MODE (1L << 2) +#define MWM_HINTS_STATUS (1L << 3) + +/* bit definitions for MwmHints.functions */ +#define MWM_FUNC_ALL (1L << 0) +#define MWM_FUNC_RESIZE (1L << 1) +#define MWM_FUNC_MOVE (1L << 2) +#define MWM_FUNC_MINIMIZE (1L << 3) +#define MWM_FUNC_MAXIMIZE (1L << 4) +#define MWM_FUNC_CLOSE (1L << 5) + +/* bit definitions for MwmHints.decorations */ +#define MWM_DECOR_ALL (1L << 0) +#define MWM_DECOR_BORDER (1L << 1) +#define MWM_DECOR_RESIZEH (1L << 2) +#define MWM_DECOR_TITLE (1L << 3) +#define MWM_DECOR_MENU (1L << 4) +#define MWM_DECOR_MINIMIZE (1L << 5) +#define MWM_DECOR_MAXIMIZE (1L << 6) + +#define PROP_MOTIF_WM_HINTS_ELEMENTS 5 + +typedef struct +{ + unsigned long flags; + unsigned long functions; + unsigned long decorations; + long inputMode; + unsigned long status; +} PropMotifWmHints; + +static void xf_SetWindowTitleText(xfContext* xfc, Window window, const char* name) +{ + WINPR_ASSERT(xfc); + WINPR_ASSERT(name); + + const size_t i = strnlen(name, MAX_PATH); + XStoreName(xfc->display, window, name); + Atom wm_Name = xfc->_NET_WM_NAME; + Atom utf8Str = xfc->UTF8_STRING; + LogTagAndXChangeProperty(TAG, xfc->display, window, wm_Name, utf8Str, 8, PropModeReplace, + (const unsigned char*)name, (int)i); +} + +/** + * Post an event from the client to the X server + */ +void xf_SendClientEvent(xfContext* xfc, Window window, Atom atom, unsigned int numArgs, ...) +{ + XEvent xevent = { 0 }; + va_list argp; + va_start(argp, numArgs); + + xevent.xclient.type = ClientMessage; + xevent.xclient.serial = 0; + xevent.xclient.send_event = False; + xevent.xclient.display = xfc->display; + xevent.xclient.window = window; + xevent.xclient.message_type = atom; + xevent.xclient.format = 32; + + for (size_t i = 0; i < numArgs; i++) + { + xevent.xclient.data.l[i] = va_arg(argp, int); + } + + DEBUG_X11("Send ClientMessage Event: wnd=0x%04lX", (unsigned long)xevent.xclient.window); + XSendEvent(xfc->display, RootWindowOfScreen(xfc->screen), False, + SubstructureRedirectMask | SubstructureNotifyMask, &xevent); + XSync(xfc->display, False); + va_end(argp); +} + +void xf_SetWindowMinimized(xfContext* xfc, xfWindow* window) +{ + XIconifyWindow(xfc->display, window->handle, xfc->screen_number); +} + +void xf_SetWindowFullscreen(xfContext* xfc, xfWindow* window, BOOL fullscreen) +{ + const rdpSettings* settings = NULL; + int startX = 0; + int startY = 0; + UINT32 width = window->width; + UINT32 height = window->height; + + WINPR_ASSERT(xfc); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + /* xfc->decorations is set by caller depending on settings and whether it is fullscreen or not + */ + window->decorations = xfc->decorations; + /* show/hide decorations (e.g. title bar) as guided by xfc->decorations */ + xf_SetWindowDecorations(xfc, window->handle, window->decorations); + DEBUG_X11(TAG, "X window decoration set to %d", (int)window->decorations); + xf_floatbar_toggle_fullscreen(xfc->window->floatbar, fullscreen); + + if (fullscreen) + { + xfc->savedWidth = xfc->window->width; + xfc->savedHeight = xfc->window->height; + xfc->savedPosX = xfc->window->left; + xfc->savedPosY = xfc->window->top; + + startX = (freerdp_settings_get_uint32(settings, FreeRDP_DesktopPosX) != UINT32_MAX) + ? freerdp_settings_get_uint32(settings, FreeRDP_DesktopPosX) + : 0; + startY = (freerdp_settings_get_uint32(settings, FreeRDP_DesktopPosY) != UINT32_MAX) + ? freerdp_settings_get_uint32(settings, FreeRDP_DesktopPosY) + : 0; + } + else + { + width = xfc->savedWidth; + height = xfc->savedHeight; + startX = xfc->savedPosX; + startY = xfc->savedPosY; + } + + /* Determine the x,y starting location for the fullscreen window */ + if (fullscreen) + { + const rdpMonitor* firstMonitor = + freerdp_settings_get_pointer_array(settings, FreeRDP_MonitorDefArray, 0); + /* Initialize startX and startY with reasonable values */ + startX = firstMonitor->x; + startY = firstMonitor->y; + + /* Search all monitors to find the lowest startX and startY values */ + for (size_t i = 0; i < freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount); i++) + { + const rdpMonitor* monitor = + freerdp_settings_get_pointer_array(settings, FreeRDP_MonitorDefArray, i); + startX = MIN(startX, monitor->x); + startY = MIN(startY, monitor->y); + } + + /* Lastly apply any monitor shift(translation from remote to local coordinate system) + * to startX and startY values + */ + startX += freerdp_settings_get_uint32(settings, FreeRDP_MonitorLocalShiftX); + startY += freerdp_settings_get_uint32(settings, FreeRDP_MonitorLocalShiftY); + } + + /* + It is safe to proceed with simply toogling _NET_WM_STATE_FULLSCREEN window state on the + following conditions: + - The window manager supports multiple monitor full screen + - The user requested to use a single monitor to render the remote desktop + */ + if (xfc->_NET_WM_FULLSCREEN_MONITORS != None || + freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount) == 1) + { + xf_ResizeDesktopWindow(xfc, window, width, height); + + if (fullscreen) + { + /* enter full screen: move the window before adding NET_WM_STATE_FULLSCREEN */ + XMoveWindow(xfc->display, window->handle, startX, startY); + } + + /* Set the fullscreen state */ + xf_SendClientEvent(xfc, window->handle, xfc->_NET_WM_STATE, 4, + fullscreen ? _NET_WM_STATE_ADD : _NET_WM_STATE_REMOVE, + xfc->_NET_WM_STATE_FULLSCREEN, 0, 0); + + if (!fullscreen) + { + /* leave full screen: move the window after removing NET_WM_STATE_FULLSCREEN + * Resize the window again, the previous call to xf_SendClientEvent might have + * changed the window size (borders, ...) + */ + xf_ResizeDesktopWindow(xfc, window, width, height); + XMoveWindow(xfc->display, window->handle, startX, startY); + } + + /* Set monitor bounds */ + if (freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount) > 1) + { + xf_SendClientEvent(xfc, window->handle, xfc->_NET_WM_FULLSCREEN_MONITORS, 5, + xfc->fullscreenMonitors.top, xfc->fullscreenMonitors.bottom, + xfc->fullscreenMonitors.left, xfc->fullscreenMonitors.right, 1); + } + } + else + { + if (fullscreen) + { + xf_SetWindowDecorations(xfc, window->handle, FALSE); + + if (xfc->fullscreenMonitors.top) + { + xf_SendClientEvent(xfc, window->handle, xfc->_NET_WM_STATE, 4, _NET_WM_STATE_ADD, + xfc->fullscreenMonitors.top, 0, 0); + } + else + { + XSetWindowAttributes xswa; + xswa.override_redirect = True; + XChangeWindowAttributes(xfc->display, window->handle, CWOverrideRedirect, &xswa); + XRaiseWindow(xfc->display, window->handle); + xswa.override_redirect = False; + XChangeWindowAttributes(xfc->display, window->handle, CWOverrideRedirect, &xswa); + } + + /* if window is in maximized state, save and remove */ + if (xfc->_NET_WM_STATE_MAXIMIZED_VERT != None) + { + BYTE state = 0; + unsigned long nitems = 0; + unsigned long bytes = 0; + BYTE* prop = NULL; + + if (xf_GetWindowProperty(xfc, window->handle, xfc->_NET_WM_STATE, 255, &nitems, + &bytes, &prop)) + { + state = 0; + + while (nitems-- > 0) + { + if (((Atom*)prop)[nitems] == xfc->_NET_WM_STATE_MAXIMIZED_VERT) + state |= 0x01; + + if (((Atom*)prop)[nitems] == xfc->_NET_WM_STATE_MAXIMIZED_HORZ) + state |= 0x02; + } + + if (state) + { + xf_SendClientEvent(xfc, window->handle, xfc->_NET_WM_STATE, 4, + _NET_WM_STATE_REMOVE, xfc->_NET_WM_STATE_MAXIMIZED_VERT, + 0, 0); + xf_SendClientEvent(xfc, window->handle, xfc->_NET_WM_STATE, 4, + _NET_WM_STATE_REMOVE, xfc->_NET_WM_STATE_MAXIMIZED_HORZ, + 0, 0); + xfc->savedMaximizedState = state; + } + + XFree(prop); + } + } + + width = xfc->vscreen.area.right - xfc->vscreen.area.left + 1; + height = xfc->vscreen.area.bottom - xfc->vscreen.area.top + 1; + DEBUG_X11("X window move and resize %dx%d@%dx%d", startX, startY, width, height); + xf_ResizeDesktopWindow(xfc, window, width, height); + XMoveWindow(xfc->display, window->handle, startX, startY); + } + else + { + xf_SetWindowDecorations(xfc, window->handle, window->decorations); + xf_ResizeDesktopWindow(xfc, window, width, height); + XMoveWindow(xfc->display, window->handle, startX, startY); + + if (xfc->fullscreenMonitors.top) + { + xf_SendClientEvent(xfc, window->handle, xfc->_NET_WM_STATE, 4, _NET_WM_STATE_REMOVE, + xfc->fullscreenMonitors.top, 0, 0); + } + + /* restore maximized state, if the window was maximized before setting fullscreen */ + if (xfc->savedMaximizedState & 0x01) + { + xf_SendClientEvent(xfc, window->handle, xfc->_NET_WM_STATE, 4, _NET_WM_STATE_ADD, + xfc->_NET_WM_STATE_MAXIMIZED_VERT, 0, 0); + } + + if (xfc->savedMaximizedState & 0x02) + { + xf_SendClientEvent(xfc, window->handle, xfc->_NET_WM_STATE, 4, _NET_WM_STATE_ADD, + xfc->_NET_WM_STATE_MAXIMIZED_HORZ, 0, 0); + } + + xfc->savedMaximizedState = 0; + } + } +} + +/* http://tronche.com/gui/x/xlib/window-information/XGetWindowProperty.html */ + +BOOL xf_GetWindowProperty(xfContext* xfc, Window window, Atom property, int length, + unsigned long* nitems, unsigned long* bytes, BYTE** prop) +{ + int status = 0; + Atom actual_type = None; + int actual_format = 0; + + if (property == None) + return FALSE; + + status = LogTagAndXGetWindowProperty(TAG, xfc->display, window, property, 0, length, False, + AnyPropertyType, &actual_type, &actual_format, nitems, + bytes, prop); + + if (status != Success) + return FALSE; + + if (actual_type == None) + { + WLog_DBG(TAG, "Property %lu does not exist", (unsigned long)property); + return FALSE; + } + + return TRUE; +} + +BOOL xf_GetCurrentDesktop(xfContext* xfc) +{ + BOOL status = 0; + unsigned long nitems = 0; + unsigned long bytes = 0; + unsigned char* prop = NULL; + status = xf_GetWindowProperty(xfc, DefaultRootWindow(xfc->display), xfc->_NET_CURRENT_DESKTOP, + 1, &nitems, &bytes, &prop); + + if (!status) + return FALSE; + + xfc->current_desktop = (int)*prop; + free(prop); + return TRUE; +} + +BOOL xf_GetWorkArea(xfContext* xfc) +{ + long* plong = NULL; + BOOL status = 0; + unsigned long nitems = 0; + unsigned long bytes = 0; + unsigned char* prop = NULL; + status = xf_GetCurrentDesktop(xfc); + + if (!status) + return FALSE; + + status = xf_GetWindowProperty(xfc, DefaultRootWindow(xfc->display), xfc->_NET_WORKAREA, 32 * 4, + &nitems, &bytes, &prop); + + if (!status) + return FALSE; + + if ((xfc->current_desktop * 4 + 3) >= (INT64)nitems) + { + free(prop); + return FALSE; + } + + plong = (long*)prop; + xfc->workArea.x = plong[xfc->current_desktop * 4 + 0]; + xfc->workArea.y = plong[xfc->current_desktop * 4 + 1]; + xfc->workArea.width = plong[xfc->current_desktop * 4 + 2]; + xfc->workArea.height = plong[xfc->current_desktop * 4 + 3]; + free(prop); + return TRUE; +} + +void xf_SetWindowDecorations(xfContext* xfc, Window window, BOOL show) +{ + PropMotifWmHints hints = { .decorations = (show) ? MWM_DECOR_ALL : 0, + .functions = MWM_FUNC_ALL, + .flags = MWM_HINTS_DECORATIONS | MWM_HINTS_FUNCTIONS, + .inputMode = 0, + .status = 0 }; + WINPR_ASSERT(xfc); + LogTagAndXChangeProperty(TAG, xfc->display, window, xfc->_MOTIF_WM_HINTS, xfc->_MOTIF_WM_HINTS, + 32, PropModeReplace, (BYTE*)&hints, PROP_MOTIF_WM_HINTS_ELEMENTS); +} + +void xf_SetWindowUnlisted(xfContext* xfc, Window window) +{ + WINPR_ASSERT(xfc); + Atom window_state[] = { xfc->_NET_WM_STATE_SKIP_PAGER, xfc->_NET_WM_STATE_SKIP_TASKBAR }; + LogTagAndXChangeProperty(TAG, xfc->display, window, xfc->_NET_WM_STATE, XA_ATOM, 32, + PropModeReplace, (BYTE*)window_state, 2); +} + +static void xf_SetWindowPID(xfContext* xfc, Window window, pid_t pid) +{ + Atom am_wm_pid = 0; + + WINPR_ASSERT(xfc); + if (!pid) + pid = getpid(); + + am_wm_pid = xfc->_NET_WM_PID; + LogTagAndXChangeProperty(TAG, xfc->display, window, am_wm_pid, XA_CARDINAL, 32, PropModeReplace, + (BYTE*)&pid, 1); +} + +static const char* get_shm_id(void) +{ + static char shm_id[64]; + sprintf_s(shm_id, sizeof(shm_id), "/com.freerdp.xfreerdp.tsmf_%016X", GetCurrentProcessId()); + return shm_id; +} + +Window xf_CreateDummyWindow(xfContext* xfc) +{ + return XCreateWindow(xfc->display, RootWindowOfScreen(xfc->screen), xfc->workArea.x, + xfc->workArea.y, 1, 1, 0, xfc->depth, InputOutput, xfc->visual, + xfc->attribs_mask, &xfc->attribs); +} + +void xf_DestroyDummyWindow(xfContext* xfc, Window window) +{ + if (window) + XDestroyWindow(xfc->display, window); +} + +xfWindow* xf_CreateDesktopWindow(xfContext* xfc, char* name, int width, int height) +{ + XEvent xevent = { 0 }; + int input_mask = 0; + XClassHint* classHints = NULL; + xfWindow* window = (xfWindow*)calloc(1, sizeof(xfWindow)); + + if (!window) + return NULL; + + rdpSettings* settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + Window parentWindow = (Window)freerdp_settings_get_uint64(settings, FreeRDP_ParentWindowId); + window->width = width; + window->height = height; + window->decorations = xfc->decorations; + window->is_mapped = FALSE; + window->is_transient = FALSE; + + WINPR_ASSERT(xfc->depth != 0); + window->handle = + XCreateWindow(xfc->display, RootWindowOfScreen(xfc->screen), xfc->workArea.x, + xfc->workArea.y, xfc->workArea.width, xfc->workArea.height, 0, xfc->depth, + InputOutput, xfc->visual, xfc->attribs_mask, &xfc->attribs); + window->shmid = shm_open(get_shm_id(), (O_CREAT | O_RDWR), (S_IREAD | S_IWRITE)); + + if (window->shmid < 0) + { + DEBUG_X11("xf_CreateDesktopWindow: failed to get access to shared memory - shmget()\n"); + } + else + { + int rc = ftruncate(window->shmid, sizeof(window->handle)); + if (rc != 0) + { +#ifdef WITH_DEBUG_X11 + char ebuffer[256] = { 0 }; + DEBUG_X11("ftruncate failed with %s [%d]", winpr_strerror(rc, ebuffer, sizeof(ebuffer)), + rc); +#endif + } + else + { + void* mem = mmap(0, sizeof(window->handle), PROT_READ | PROT_WRITE, MAP_SHARED, + window->shmid, 0); + + if (mem == MAP_FAILED) + { + DEBUG_X11( + "xf_CreateDesktopWindow: failed to assign pointer to the memory address - " + "shmat()\n"); + } + else + { + window->xfwin = mem; + *window->xfwin = window->handle; + } + } + } + + classHints = XAllocClassHint(); + + if (classHints) + { + classHints->res_name = "xfreerdp"; + + char* res_class = NULL; + const char* WmClass = freerdp_settings_get_string(settings, FreeRDP_WmClass); + if (WmClass) + res_class = _strdup(WmClass); + else + res_class = _strdup("xfreerdp"); + + XSetClassHint(xfc->display, window->handle, classHints); + XFree(classHints); + free(res_class); + } + + xf_ResizeDesktopWindow(xfc, window, width, height); + xf_SetWindowDecorations(xfc, window->handle, window->decorations); + xf_SetWindowPID(xfc, window->handle, 0); + input_mask = KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | + VisibilityChangeMask | FocusChangeMask | StructureNotifyMask | PointerMotionMask | + ExposureMask | PropertyChangeMask; + + if (xfc->grab_keyboard) + input_mask |= EnterWindowMask | LeaveWindowMask; + + LogTagAndXChangeProperty(TAG, xfc->display, window->handle, xfc->_NET_WM_ICON, XA_CARDINAL, 32, + PropModeReplace, (BYTE*)xf_icon_prop, ARRAYSIZE(xf_icon_prop)); + + if (parentWindow) + XReparentWindow(xfc->display, window->handle, parentWindow, 0, 0); + + XSelectInput(xfc->display, window->handle, input_mask); + XClearWindow(xfc->display, window->handle); + xf_SetWindowTitleText(xfc, window->handle, name); + XMapWindow(xfc->display, window->handle); + xf_input_init(xfc, window->handle); + + /* + * NOTE: This must be done here to handle reparenting the window, + * so that we don't miss the event and hang waiting for the next one + */ + do + { + XMaskEvent(xfc->display, VisibilityChangeMask, &xevent); + } while (xevent.type != VisibilityNotify); + + /* + * The XCreateWindow call will start the window in the upper-left corner of our current + * monitor instead of the upper-left monitor for remote app mode (which uses all monitors). + * This extra call after the window is mapped will position the login window correctly + */ + if (freerdp_settings_get_bool(settings, FreeRDP_RemoteApplicationMode)) + { + XMoveWindow(xfc->display, window->handle, 0, 0); + } + else if ((freerdp_settings_get_uint32(settings, FreeRDP_DesktopPosX) != UINT32_MAX) && + (freerdp_settings_get_uint32(settings, FreeRDP_DesktopPosY) != UINT32_MAX)) + { + XMoveWindow(xfc->display, window->handle, + freerdp_settings_get_uint32(settings, FreeRDP_DesktopPosX), + freerdp_settings_get_uint32(settings, FreeRDP_DesktopPosY)); + } + + window->floatbar = xf_floatbar_new(xfc, window->handle, name, + freerdp_settings_get_uint32(settings, FreeRDP_Floatbar)); + + if (xfc->_XWAYLAND_MAY_GRAB_KEYBOARD) + xf_SendClientEvent(xfc, window->handle, xfc->_XWAYLAND_MAY_GRAB_KEYBOARD, 1, 1); + + return window; +} + +void xf_ResizeDesktopWindow(xfContext* xfc, xfWindow* window, int width, int height) +{ + XSizeHints* size_hints = NULL; + rdpSettings* settings = NULL; + + if (!xfc || !window) + return; + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + if (!(size_hints = XAllocSizeHints())) + return; + + size_hints->flags = PMinSize | PMaxSize | PWinGravity; + size_hints->win_gravity = NorthWestGravity; + size_hints->min_width = size_hints->min_height = 1; + size_hints->max_width = size_hints->max_height = 16384; + XResizeWindow(xfc->display, window->handle, width, height); +#ifdef WITH_XRENDER + + if (!freerdp_settings_get_bool(settings, FreeRDP_SmartSizing) && + !freerdp_settings_get_bool(settings, FreeRDP_DynamicResolutionUpdate)) +#endif + { + if (!xfc->fullscreen) + { + /* min == max is an hint for the WM to indicate that the window should + * not be resizable */ + size_hints->min_width = size_hints->max_width = width; + size_hints->min_height = size_hints->max_height = height; + } + } + + XSetWMNormalHints(xfc->display, window->handle, size_hints); + XFree(size_hints); +} + +void xf_DestroyDesktopWindow(xfContext* xfc, xfWindow* window) +{ + if (!window) + return; + + if (xfc->window == window) + xfc->window = NULL; + + xf_floatbar_free(window->floatbar); + + if (window->gc) + XFreeGC(xfc->display, window->gc); + + if (window->handle) + { + XUnmapWindow(xfc->display, window->handle); + XDestroyWindow(xfc->display, window->handle); + } + + if (window->xfwin) + munmap(0, sizeof(*window->xfwin)); + + if (window->shmid >= 0) + close(window->shmid); + + shm_unlink(get_shm_id()); + window->xfwin = (Window*)-1; + window->shmid = -1; + free(window); +} + +void xf_SetWindowStyle(xfContext* xfc, xfAppWindow* appWindow, UINT32 style, UINT32 ex_style) +{ + Atom window_type = 0; + BOOL redirect = FALSE; + + if ((ex_style & WS_EX_NOACTIVATE) || (ex_style & WS_EX_TOOLWINDOW)) + { + redirect = TRUE; + appWindow->is_transient = TRUE; + xf_SetWindowUnlisted(xfc, appWindow->handle); + window_type = xfc->_NET_WM_WINDOW_TYPE_DROPDOWN_MENU; + } + /* + * TOPMOST window that is not a tool window is treated like a regular window (i.e. task + * manager). Want to do this here, since the window may have type WS_POPUP + */ + else if (ex_style & WS_EX_TOPMOST) + { + window_type = xfc->_NET_WM_WINDOW_TYPE_NORMAL; + } + else if (style & WS_POPUP) + { + /* this includes dialogs, popups, etc, that need to be full-fledged windows */ + appWindow->is_transient = TRUE; + window_type = xfc->_NET_WM_WINDOW_TYPE_DIALOG; + xf_SetWindowUnlisted(xfc, appWindow->handle); + } + else + { + window_type = xfc->_NET_WM_WINDOW_TYPE_NORMAL; + } + + { + /* + * Tooltips and menu items should be unmanaged windows + * (called "override redirect" in X windows parlance) + * If they are managed, there are issues with window focus that + * cause the windows to behave improperly. For example, a mouse + * press will dismiss a drop-down menu because the RDP server + * sees that as a focus out event from the window owning the + * dropdown. + */ + XSetWindowAttributes attrs; + attrs.override_redirect = redirect ? True : False; + XChangeWindowAttributes(xfc->display, appWindow->handle, CWOverrideRedirect, &attrs); + } + + LogTagAndXChangeProperty(TAG, xfc->display, appWindow->handle, xfc->_NET_WM_WINDOW_TYPE, + XA_ATOM, 32, PropModeReplace, (BYTE*)&window_type, 1); +} + +void xf_SetWindowText(xfContext* xfc, xfAppWindow* appWindow, const char* name) +{ + xf_SetWindowTitleText(xfc, appWindow->handle, name); +} + +static void xf_FixWindowCoordinates(xfContext* xfc, int* x, int* y, int* width, int* height) +{ + int vscreen_width = 0; + int vscreen_height = 0; + vscreen_width = xfc->vscreen.area.right - xfc->vscreen.area.left + 1; + vscreen_height = xfc->vscreen.area.bottom - xfc->vscreen.area.top + 1; + + if (*x < xfc->vscreen.area.left) + { + *width += *x; + *x = xfc->vscreen.area.left; + } + + if (*y < xfc->vscreen.area.top) + { + *height += *y; + *y = xfc->vscreen.area.top; + } + + if (*width > vscreen_width) + { + *width = vscreen_width; + } + + if (*height > vscreen_height) + { + *height = vscreen_height; + } + + if (*width < 1) + { + *width = 1; + } + + if (*height < 1) + { + *height = 1; + } +} + +int xf_AppWindowInit(xfContext* xfc, xfAppWindow* appWindow) +{ + if (!xfc || !appWindow) + return -1; + + xf_SetWindowDecorations(xfc, appWindow->handle, appWindow->decorations); + xf_SetWindowStyle(xfc, appWindow, appWindow->dwStyle, appWindow->dwExStyle); + xf_SetWindowPID(xfc, appWindow->handle, 0); + xf_ShowWindow(xfc, appWindow, WINDOW_SHOW); + XClearWindow(xfc->display, appWindow->handle); + XMapWindow(xfc->display, appWindow->handle); + /* Move doesn't seem to work until window is mapped. */ + xf_MoveWindow(xfc, appWindow, appWindow->x, appWindow->y, appWindow->width, appWindow->height); + xf_SetWindowText(xfc, appWindow, appWindow->title); + return 1; +} + +BOOL xf_AppWindowCreate(xfContext* xfc, xfAppWindow* appWindow) +{ + XGCValues gcv = { 0 }; + int input_mask = 0; + XWMHints* InputModeHint = NULL; + XClassHint* class_hints = NULL; + const rdpSettings* settings = NULL; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(appWindow); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + xf_FixWindowCoordinates(xfc, &appWindow->x, &appWindow->y, &appWindow->width, + &appWindow->height); + appWindow->shmid = -1; + appWindow->decorations = FALSE; + appWindow->fullscreen = FALSE; + appWindow->local_move.state = LMS_NOT_ACTIVE; + appWindow->is_mapped = FALSE; + appWindow->is_transient = FALSE; + appWindow->rail_state = 0; + appWindow->maxVert = FALSE; + appWindow->maxHorz = FALSE; + appWindow->minimized = FALSE; + appWindow->rail_ignore_configure = FALSE; + + WINPR_ASSERT(xfc->depth != 0); + appWindow->handle = + XCreateWindow(xfc->display, RootWindowOfScreen(xfc->screen), appWindow->x, appWindow->y, + appWindow->width, appWindow->height, 0, xfc->depth, InputOutput, xfc->visual, + xfc->attribs_mask, &xfc->attribs); + + if (!appWindow->handle) + return FALSE; + + appWindow->gc = XCreateGC(xfc->display, appWindow->handle, GCGraphicsExposures, &gcv); + + if (!xf_AppWindowResize(xfc, appWindow)) + return FALSE; + + class_hints = XAllocClassHint(); + + if (class_hints) + { + char* strclass = NULL; + + const char* WmClass = freerdp_settings_get_string(settings, FreeRDP_WmClass); + if (WmClass) + strclass = _strdup(WmClass); + else + { + size_t size = 0; + winpr_asprintf(&strclass, &size, "RAIL:%08" PRIX64 "", appWindow->windowId); + } + class_hints->res_class = strclass; + class_hints->res_name = "RAIL"; + XSetClassHint(xfc->display, appWindow->handle, class_hints); + XFree(class_hints); + free(strclass); + } + + /* Set the input mode hint for the WM */ + InputModeHint = XAllocWMHints(); + InputModeHint->flags = (1L << 0); + InputModeHint->input = True; + XSetWMHints(xfc->display, appWindow->handle, InputModeHint); + XFree(InputModeHint); + XSetWMProtocols(xfc->display, appWindow->handle, &(xfc->WM_DELETE_WINDOW), 1); + input_mask = KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | + EnterWindowMask | LeaveWindowMask | PointerMotionMask | Button1MotionMask | + Button2MotionMask | Button3MotionMask | Button4MotionMask | Button5MotionMask | + ButtonMotionMask | KeymapStateMask | ExposureMask | VisibilityChangeMask | + StructureNotifyMask | SubstructureNotifyMask | SubstructureRedirectMask | + FocusChangeMask | PropertyChangeMask | ColormapChangeMask | OwnerGrabButtonMask; + XSelectInput(xfc->display, appWindow->handle, input_mask); + + if (xfc->_XWAYLAND_MAY_GRAB_KEYBOARD) + xf_SendClientEvent(xfc, appWindow->handle, xfc->_XWAYLAND_MAY_GRAB_KEYBOARD, 1, 1); + + return TRUE; +} + +void xf_SetWindowMinMaxInfo(xfContext* xfc, xfAppWindow* appWindow, int maxWidth, int maxHeight, + int maxPosX, int maxPosY, int minTrackWidth, int minTrackHeight, + int maxTrackWidth, int maxTrackHeight) +{ + XSizeHints* size_hints = NULL; + size_hints = XAllocSizeHints(); + + if (size_hints) + { + size_hints->flags = PMinSize | PMaxSize | PResizeInc; + size_hints->min_width = minTrackWidth; + size_hints->min_height = minTrackHeight; + size_hints->max_width = maxTrackWidth; + size_hints->max_height = maxTrackHeight; + /* to speedup window drawing we need to select optimal value for sizing step. */ + size_hints->width_inc = size_hints->height_inc = 1; + XSetWMNormalHints(xfc->display, appWindow->handle, size_hints); + XFree(size_hints); + } +} + +void xf_StartLocalMoveSize(xfContext* xfc, xfAppWindow* appWindow, int direction, int x, int y) +{ + if (appWindow->local_move.state != LMS_NOT_ACTIVE) + return; + + /* + * Save original mouse location relative to root. This will be needed + * to end local move to RDP server and/or X server + */ + appWindow->local_move.root_x = x; + appWindow->local_move.root_y = y; + appWindow->local_move.state = LMS_STARTING; + appWindow->local_move.direction = direction; + + xf_ungrab(xfc); + + xf_SendClientEvent( + xfc, appWindow->handle, + xfc->_NET_WM_MOVERESIZE, /* request X window manager to initiate a local move */ + 5, /* 5 arguments to follow */ + x, /* x relative to root window */ + y, /* y relative to root window */ + direction, /* extended ICCM direction flag */ + 1, /* simulated mouse button 1 */ + 1); /* 1 == application request per extended ICCM */ +} + +void xf_EndLocalMoveSize(xfContext* xfc, xfAppWindow* appWindow) +{ + if (appWindow->local_move.state == LMS_NOT_ACTIVE) + return; + + if (appWindow->local_move.state == LMS_STARTING) + { + /* + * The move never was property started. This can happen due to race + * conditions between the mouse button up and the communications to the + * RDP server for local moves. We must cancel the X window manager move. + * Per ICCM, the X client can ask to cancel an active move. + */ + xf_SendClientEvent( + xfc, appWindow->handle, + xfc->_NET_WM_MOVERESIZE, /* request X window manager to abort a local move */ + 5, /* 5 arguments to follow */ + appWindow->local_move.root_x, /* x relative to root window */ + appWindow->local_move.root_y, /* y relative to root window */ + _NET_WM_MOVERESIZE_CANCEL, /* extended ICCM direction flag */ + 1, /* simulated mouse button 1 */ + 1); /* 1 == application request per extended ICCM */ + } + + appWindow->local_move.state = LMS_NOT_ACTIVE; +} + +void xf_MoveWindow(xfContext* xfc, xfAppWindow* appWindow, int x, int y, int width, int height) +{ + BOOL resize = FALSE; + + if ((width * height) < 1) + return; + + if ((appWindow->width != width) || (appWindow->height != height)) + resize = TRUE; + + if (appWindow->local_move.state == LMS_STARTING || appWindow->local_move.state == LMS_ACTIVE) + return; + + appWindow->x = x; + appWindow->y = y; + appWindow->width = width; + appWindow->height = height; + + if (resize) + XMoveResizeWindow(xfc->display, appWindow->handle, x, y, width, height); + else + XMoveWindow(xfc->display, appWindow->handle, x, y); + + xf_UpdateWindowArea(xfc, appWindow, 0, 0, width, height); +} + +void xf_ShowWindow(xfContext* xfc, xfAppWindow* appWindow, BYTE state) +{ + WINPR_ASSERT(xfc); + WINPR_ASSERT(appWindow); + + switch (state) + { + case WINDOW_HIDE: + XWithdrawWindow(xfc->display, appWindow->handle, xfc->screen_number); + break; + + case WINDOW_SHOW_MINIMIZED: + appWindow->minimized = TRUE; + XIconifyWindow(xfc->display, appWindow->handle, xfc->screen_number); + break; + + case WINDOW_SHOW_MAXIMIZED: + /* Set the window as maximized */ + appWindow->maxHorz = TRUE; + appWindow->maxVert = TRUE; + xf_SendClientEvent(xfc, appWindow->handle, xfc->_NET_WM_STATE, 4, _NET_WM_STATE_ADD, + xfc->_NET_WM_STATE_MAXIMIZED_VERT, xfc->_NET_WM_STATE_MAXIMIZED_HORZ, + 0); + + /* + * This is a workaround for the case where the window is maximized locally before the + * rail server is told to maximize the window, this appears to be a race condition where + * the local window with incomplete data and once the window is actually maximized on + * the server + * - an update of the new areas may not happen. So, we simply to do a full update of the + * entire window once the rail server notifies us that the window is now maximized. + */ + if (appWindow->rail_state == WINDOW_SHOW_MAXIMIZED) + { + xf_UpdateWindowArea(xfc, appWindow, 0, 0, appWindow->windowWidth, + appWindow->windowHeight); + } + + break; + + case WINDOW_SHOW: + /* Ensure the window is not maximized */ + xf_SendClientEvent(xfc, appWindow->handle, xfc->_NET_WM_STATE, 4, _NET_WM_STATE_REMOVE, + xfc->_NET_WM_STATE_MAXIMIZED_VERT, xfc->_NET_WM_STATE_MAXIMIZED_HORZ, + 0); + + /* + * Ignore configure requests until both the Maximized properties have been processed + * to prevent condition where WM overrides size of request due to one or both of these + * properties still being set - which causes a position adjustment to be sent back to + * the server thus causing the window to not return to its original size + */ + if (appWindow->rail_state == WINDOW_SHOW_MAXIMIZED) + appWindow->rail_ignore_configure = TRUE; + + if (appWindow->is_transient) + xf_SetWindowUnlisted(xfc, appWindow->handle); + + XMapWindow(xfc->display, appWindow->handle); + break; + } + + /* Save the current rail state of this window */ + appWindow->rail_state = state; + XFlush(xfc->display); +} + +void xf_SetWindowRects(xfContext* xfc, xfAppWindow* appWindow, RECTANGLE_16* rects, int nrects) +{ + XRectangle* xrects = NULL; + + if (nrects < 1) + return; + +#ifdef WITH_XEXT + xrects = (XRectangle*)calloc(nrects, sizeof(XRectangle)); + + for (int i = 0; i < nrects; i++) + { + xrects[i].x = rects[i].left; + xrects[i].y = rects[i].top; + xrects[i].width = rects[i].right - rects[i].left; + xrects[i].height = rects[i].bottom - rects[i].top; + } + + XShapeCombineRectangles(xfc->display, appWindow->handle, ShapeBounding, 0, 0, xrects, nrects, + ShapeSet, 0); + free(xrects); +#endif +} + +void xf_SetWindowVisibilityRects(xfContext* xfc, xfAppWindow* appWindow, UINT32 rectsOffsetX, + UINT32 rectsOffsetY, RECTANGLE_16* rects, int nrects) +{ + XRectangle* xrects = NULL; + + if (nrects < 1) + return; + +#ifdef WITH_XEXT + xrects = (XRectangle*)calloc(nrects, sizeof(XRectangle)); + + for (int i = 0; i < nrects; i++) + { + xrects[i].x = rects[i].left; + xrects[i].y = rects[i].top; + xrects[i].width = rects[i].right - rects[i].left; + xrects[i].height = rects[i].bottom - rects[i].top; + } + + XShapeCombineRectangles(xfc->display, appWindow->handle, ShapeBounding, rectsOffsetX, + rectsOffsetY, xrects, nrects, ShapeSet, 0); + free(xrects); +#endif +} + +void xf_UpdateWindowArea(xfContext* xfc, xfAppWindow* appWindow, int x, int y, int width, + int height) +{ + int ax = 0; + int ay = 0; + const rdpSettings* settings = NULL; + + WINPR_ASSERT(xfc); + + settings = xfc->common.context.settings; + WINPR_ASSERT(settings); + + if (appWindow == NULL) + return; + + if (appWindow->surfaceId < UINT16_MAX) + return; + + ax = x + appWindow->windowOffsetX; + ay = y + appWindow->windowOffsetY; + + if (ax + width > appWindow->windowOffsetX + appWindow->width) + width = (appWindow->windowOffsetX + appWindow->width - 1) - ax; + + if (ay + height > appWindow->windowOffsetY + appWindow->height) + height = (appWindow->windowOffsetY + appWindow->height - 1) - ay; + + xf_lock_x11(xfc); + + if (freerdp_settings_get_bool(settings, FreeRDP_SoftwareGdi)) + { + XPutImage(xfc->display, appWindow->pixmap, appWindow->gc, xfc->image, ax, ay, x, y, width, + height); + } + + XCopyArea(xfc->display, appWindow->pixmap, appWindow->handle, appWindow->gc, x, y, width, + height, x, y); + XFlush(xfc->display); + xf_unlock_x11(xfc); +} + +static void xf_AppWindowDestroyImage(xfAppWindow* appWindow) +{ + WINPR_ASSERT(appWindow); + if (appWindow->image) + { + appWindow->image->data = NULL; + XDestroyImage(appWindow->image); + appWindow->image = NULL; + } +} + +void xf_DestroyWindow(xfContext* xfc, xfAppWindow* appWindow) +{ + if (!appWindow) + return; + + if (xfc->appWindow == appWindow) + xfc->appWindow = NULL; + + if (appWindow->gc) + XFreeGC(xfc->display, appWindow->gc); + + if (appWindow->pixmap) + XFreePixmap(xfc->display, appWindow->pixmap); + + xf_AppWindowDestroyImage(appWindow); + + if (appWindow->handle) + { + XUnmapWindow(xfc->display, appWindow->handle); + XDestroyWindow(xfc->display, appWindow->handle); + } + + if (appWindow->xfwin) + munmap(0, sizeof(*appWindow->xfwin)); + + if (appWindow->shmid >= 0) + close(appWindow->shmid); + + shm_unlink(get_shm_id()); + appWindow->xfwin = (Window*)-1; + appWindow->shmid = -1; + free(appWindow->title); + free(appWindow->windowRects); + free(appWindow->visibilityRects); + free(appWindow); +} + +xfAppWindow* xf_AppWindowFromX11Window(xfContext* xfc, Window wnd) +{ + ULONG_PTR* pKeys = NULL; + + WINPR_ASSERT(xfc); + if (!xfc->railWindows) + return NULL; + + size_t count = HashTable_GetKeys(xfc->railWindows, &pKeys); + + for (size_t index = 0; index < count; index++) + { + xfAppWindow* appWindow = xf_rail_get_window(xfc, *(UINT64*)pKeys[index]); + + if (!appWindow) + { + free(pKeys); + return NULL; + } + + if (appWindow->handle == wnd) + { + free(pKeys); + return appWindow; + } + } + + free(pKeys); + return NULL; +} + +UINT xf_AppUpdateWindowFromSurface(xfContext* xfc, gdiGfxSurface* surface) +{ + XImage* image = NULL; + UINT rc = ERROR_INTERNAL_ERROR; + + WINPR_ASSERT(xfc); + WINPR_ASSERT(surface); + + xfAppWindow* appWindow = xf_rail_get_window(xfc, surface->windowId); + if (!appWindow) + { + WLog_VRB(TAG, "Failed to find a window for id=0x%08" PRIx64, surface->windowId); + return CHANNEL_RC_OK; + } + + const BOOL swGdi = freerdp_settings_get_bool(xfc->common.context.settings, FreeRDP_SoftwareGdi); + UINT32 nrects = 0; + const RECTANGLE_16* rects = region16_rects(&surface->invalidRegion, &nrects); + + xf_lock_x11(xfc); + if (swGdi) + { + if (appWindow->surfaceId != surface->surfaceId) + { + xf_AppWindowDestroyImage(appWindow); + appWindow->surfaceId = surface->surfaceId; + } + if (appWindow->width != (INT64)surface->width) + xf_AppWindowDestroyImage(appWindow); + if (appWindow->height != (INT64)surface->height) + xf_AppWindowDestroyImage(appWindow); + + if (!appWindow->image) + { + WINPR_ASSERT(xfc->depth != 0); + appWindow->image = XCreateImage(xfc->display, xfc->visual, xfc->depth, ZPixmap, 0, + (char*)surface->data, surface->width, surface->height, + xfc->scanline_pad, surface->scanline); + if (!appWindow->image) + { + WLog_WARN(TAG, + "Failed create a XImage[%" PRIu32 "x%" PRIu32 ", scanline=%" PRIu32 + ", bpp=%" PRIu32 "] for window id=0x%08" PRIx64, + surface->width, surface->height, surface->scanline, xfc->depth, + surface->windowId); + goto fail; + } + appWindow->image->byte_order = LSBFirst; + appWindow->image->bitmap_bit_order = LSBFirst; + } + + image = appWindow->image; + } + else + { + xfGfxSurface* xfSurface = (xfGfxSurface*)surface; + image = xfSurface->image; + } + + for (UINT32 x = 0; x < nrects; x++) + { + const RECTANGLE_16* rect = &rects[x]; + const UINT32 width = rect->right - rect->left; + const UINT32 height = rect->bottom - rect->top; + + XPutImage(xfc->display, appWindow->pixmap, appWindow->gc, image, rect->left, rect->top, + rect->left, rect->top, width, height); + + XCopyArea(xfc->display, appWindow->pixmap, appWindow->handle, appWindow->gc, rect->left, + rect->top, width, height, rect->left, rect->top); + } + + rc = CHANNEL_RC_OK; +fail: + XFlush(xfc->display); + xf_unlock_x11(xfc); + return rc; +} + +BOOL xf_AppWindowResize(xfContext* xfc, xfAppWindow* appWindow) +{ + WINPR_ASSERT(xfc); + WINPR_ASSERT(appWindow); + + if (appWindow->pixmap != 0) + XFreePixmap(xfc->display, appWindow->pixmap); + + WINPR_ASSERT(xfc->depth != 0); + appWindow->pixmap = + XCreatePixmap(xfc->display, xfc->drawable, appWindow->width, appWindow->height, xfc->depth); + xf_AppWindowDestroyImage(appWindow); + + return appWindow->pixmap != 0; +} diff --git a/client/X11/xf_window.h b/client/X11/xf_window.h new file mode 100644 index 0000000..9f30280 --- /dev/null +++ b/client/X11/xf_window.h @@ -0,0 +1,207 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Windows + * + * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_X11_WINDOW_H +#define FREERDP_CLIENT_X11_WINDOW_H + +#include <X11/Xlib.h> + +#include <winpr/platform.h> +#include <freerdp/freerdp.h> +#include <freerdp/gdi/gfx.h> + +typedef struct xf_app_window xfAppWindow; + +typedef struct xf_localmove xfLocalMove; +typedef struct xf_window xfWindow; + +#include "xf_client.h" +#include "xf_floatbar.h" +#include "xfreerdp.h" + +// Extended ICCM flags http://standards.freedesktop.org/wm-spec/wm-spec-latest.html +WINPR_PRAGMA_DIAG_PUSH +WINPR_PRAGMA_DIAG_IGNORED_RESERVED_ID_MACRO + +#define _NET_WM_MOVERESIZE_SIZE_TOPLEFT 0 +#define _NET_WM_MOVERESIZE_SIZE_TOP 1 +#define _NET_WM_MOVERESIZE_SIZE_TOPRIGHT 2 +#define _NET_WM_MOVERESIZE_SIZE_RIGHT 3 +#define _NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT 4 +#define _NET_WM_MOVERESIZE_SIZE_BOTTOM 5 +#define _NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT 6 +#define _NET_WM_MOVERESIZE_SIZE_LEFT 7 +#define _NET_WM_MOVERESIZE_MOVE 8 /* movement only */ +#define _NET_WM_MOVERESIZE_SIZE_KEYBOARD 9 /* size via keyboard */ +#define _NET_WM_MOVERESIZE_MOVE_KEYBOARD 10 /* move via keyboard */ +#define _NET_WM_MOVERESIZE_CANCEL 11 /* cancel operation */ + +#define _NET_WM_STATE_REMOVE 0 /* remove/unset property */ +#define _NET_WM_STATE_ADD 1 /* add/set property */ +#define _NET_WM_STATE_TOGGLE 2 /* toggle property */ + +WINPR_PRAGMA_DIAG_POP + +enum xf_localmove_state +{ + LMS_NOT_ACTIVE, + LMS_STARTING, + LMS_ACTIVE, + LMS_TERMINATING +}; + +struct xf_localmove +{ + int root_x; + int root_y; + int window_x; + int window_y; + enum xf_localmove_state state; + int direction; +}; + +struct xf_window +{ + GC gc; + int left; + int top; + int right; + int bottom; + int width; + int height; + int shmid; + Window handle; + Window* xfwin; + xfFloatbar* floatbar; + BOOL decorations; + BOOL is_mapped; + BOOL is_transient; +}; + +struct xf_app_window +{ + xfContext* xfc; + + int x; + int y; + int width; + int height; + char* title; + + UINT32 surfaceId; + UINT64 windowId; + UINT32 ownerWindowId; + + UINT32 dwStyle; + UINT32 dwExStyle; + UINT32 showState; + + INT32 clientOffsetX; + INT32 clientOffsetY; + UINT32 clientAreaWidth; + UINT32 clientAreaHeight; + + INT32 windowOffsetX; + INT32 windowOffsetY; + INT32 windowClientDeltaX; + INT32 windowClientDeltaY; + UINT32 windowWidth; + UINT32 windowHeight; + UINT32 numWindowRects; + RECTANGLE_16* windowRects; + + INT32 visibleOffsetX; + INT32 visibleOffsetY; + UINT32 numVisibilityRects; + RECTANGLE_16* visibilityRects; + + UINT32 localWindowOffsetCorrX; + UINT32 localWindowOffsetCorrY; + + UINT32 resizeMarginLeft; + UINT32 resizeMarginTop; + UINT32 resizeMarginRight; + UINT32 resizeMarginBottom; + + GC gc; + int shmid; + Window handle; + Window* xfwin; + BOOL fullscreen; + BOOL decorations; + BOOL is_mapped; + BOOL is_transient; + xfLocalMove local_move; + BYTE rail_state; + BOOL maxVert; + BOOL maxHorz; + BOOL minimized; + BOOL rail_ignore_configure; + + Pixmap pixmap; + XImage* image; +}; + +void xf_ewmhints_init(xfContext* xfc); + +BOOL xf_GetCurrentDesktop(xfContext* xfc); +BOOL xf_GetWorkArea(xfContext* xfc); + +void xf_SetWindowFullscreen(xfContext* xfc, xfWindow* window, BOOL fullscreen); +void xf_SetWindowMinimized(xfContext* xfc, xfWindow* window); +void xf_SetWindowDecorations(xfContext* xfc, Window window, BOOL show); +void xf_SetWindowUnlisted(xfContext* xfc, Window window); + +xfWindow* xf_CreateDesktopWindow(xfContext* xfc, char* name, int width, int height); +void xf_ResizeDesktopWindow(xfContext* xfc, xfWindow* window, int width, int height); +void xf_DestroyDesktopWindow(xfContext* xfc, xfWindow* window); + +Window xf_CreateDummyWindow(xfContext* xfc); +void xf_DestroyDummyWindow(xfContext* xfc, Window window); + +BOOL xf_GetWindowProperty(xfContext* xfc, Window window, Atom property, int length, + unsigned long* nitems, unsigned long* bytes, BYTE** prop); +void xf_SendClientEvent(xfContext* xfc, Window window, Atom atom, unsigned int numArgs, ...); + +BOOL xf_AppWindowCreate(xfContext* xfc, xfAppWindow* appWindow); +int xf_AppWindowInit(xfContext* xfc, xfAppWindow* appWindow); + +BOOL xf_AppWindowResize(xfContext* xfc, xfAppWindow* appWindow); + +void xf_SetWindowText(xfContext* xfc, xfAppWindow* appWindow, const char* name); +void xf_MoveWindow(xfContext* xfc, xfAppWindow* appWindow, int x, int y, int width, int height); +void xf_ShowWindow(xfContext* xfc, xfAppWindow* appWindow, BYTE state); +// void xf_SetWindowIcon(xfContext* xfc, xfAppWindow* appWindow, rdpIcon* icon); +void xf_SetWindowRects(xfContext* xfc, xfAppWindow* appWindow, RECTANGLE_16* rects, int nrects); +void xf_SetWindowVisibilityRects(xfContext* xfc, xfAppWindow* appWindow, UINT32 rectsOffsetX, + UINT32 rectsOffsetY, RECTANGLE_16* rects, int nrects); +void xf_SetWindowStyle(xfContext* xfc, xfAppWindow* appWindow, UINT32 style, UINT32 ex_style); +void xf_UpdateWindowArea(xfContext* xfc, xfAppWindow* appWindow, int x, int y, int width, + int height); +UINT xf_AppUpdateWindowFromSurface(xfContext* xfc, gdiGfxSurface* surface); + +void xf_DestroyWindow(xfContext* xfc, xfAppWindow* appWindow); +void xf_SetWindowMinMaxInfo(xfContext* xfc, xfAppWindow* appWindow, int maxWidth, int maxHeight, + int maxPosX, int maxPosY, int minTrackWidth, int minTrackHeight, + int maxTrackWidth, int maxTrackHeight); +void xf_StartLocalMoveSize(xfContext* xfc, xfAppWindow* appWindow, int direction, int x, int y); +void xf_EndLocalMoveSize(xfContext* xfc, xfAppWindow* appWindow); +xfAppWindow* xf_AppWindowFromX11Window(xfContext* xfc, Window wnd); + +#endif /* FREERDP_CLIENT_X11_WINDOW_H */ diff --git a/client/X11/xfreerdp.h b/client/X11/xfreerdp.h new file mode 100644 index 0000000..314c63d --- /dev/null +++ b/client/X11/xfreerdp.h @@ -0,0 +1,389 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Client + * + * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * Copyright 2016 Thincast Technologies GmbH + * Copyright 2016 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FREERDP_CLIENT_X11_FREERDP_H +#define FREERDP_CLIENT_X11_FREERDP_H + +#include <freerdp/config.h> + +typedef struct xf_context xfContext; + +#ifdef WITH_XCURSOR +#include <X11/Xcursor/Xcursor.h> +#endif + +#ifdef WITH_XI +#include <X11/extensions/XInput2.h> +#endif + +#include <freerdp/api.h> + +#include "xf_window.h" +#include "xf_monitor.h" +#include "xf_channels.h" + +#if defined(CHANNEL_TSMF_CLIENT) +#include <freerdp/client/tsmf.h> +#endif + +#include <freerdp/gdi/gdi.h> +#include <freerdp/codec/rfx.h> +#include <freerdp/codec/nsc.h> +#include <freerdp/codec/clear.h> +#include <freerdp/codec/color.h> +#include <freerdp/codec/bitmap.h> +#include <freerdp/codec/h264.h> +#include <freerdp/codec/progressive.h> +#include <freerdp/codec/region.h> + +#if !defined(XcursorUInt) +typedef unsigned int XcursorUInt; +#endif + +#if !defined(XcursorPixel) +typedef XcursorUInt XcursorPixel; +#endif + +struct xf_FullscreenMonitors +{ + UINT32 top; + UINT32 bottom; + UINT32 left; + UINT32 right; +}; +typedef struct xf_FullscreenMonitors xfFullscreenMonitors; + +struct xf_WorkArea +{ + UINT32 x; + UINT32 y; + UINT32 width; + UINT32 height; +}; +typedef struct xf_WorkArea xfWorkArea; + +struct xf_pointer +{ + rdpPointer pointer; + XcursorPixel* cursorPixels; + UINT32 nCursors; + UINT32 mCursors; + UINT32* cursorWidths; + UINT32* cursorHeights; + Cursor* cursors; + Cursor cursor; +}; +typedef struct xf_pointer xfPointer; + +struct xf_bitmap +{ + rdpBitmap bitmap; + Pixmap pixmap; + XImage* image; +}; +typedef struct xf_bitmap xfBitmap; + +struct xf_glyph +{ + rdpGlyph glyph; + Pixmap pixmap; +}; +typedef struct xf_glyph xfGlyph; + +typedef struct xf_clipboard xfClipboard; +typedef struct s_xfDispContext xfDispContext; +typedef struct s_xfVideoContext xfVideoContext; +typedef struct xf_rail_icon_cache xfRailIconCache; + +/* Number of buttons that are mapped from X11 to RDP button events. */ +#define NUM_BUTTONS_MAPPED 11 + +typedef struct +{ + int button; + UINT16 flags; +} button_map; + +#if defined(WITH_XI) +#define MAX_CONTACTS 20 + +typedef struct touch_contact +{ + int id; + int count; + double pos_x; + double pos_y; + double last_x; + double last_y; + +} touchContact; + +#endif + +struct xf_context +{ + rdpClientContext common; + + GC gc; + int xfds; + int depth; + + GC gc_mono; + BOOL invert; + Screen* screen; + XImage* image; + Pixmap primary; + Pixmap drawing; + Visual* visual; + Display* display; + Drawable drawable; + Pixmap bitmap_mono; + Colormap colormap; + int screen_number; + int scanline_pad; + BOOL big_endian; + BOOL fullscreen; + BOOL decorations; + BOOL grab_keyboard; + BOOL unobscured; + BOOL debug; + HANDLE x11event; + xfWindow* window; + xfAppWindow* appWindow; + xfPointer* pointer; + xfWorkArea workArea; + xfFullscreenMonitors fullscreenMonitors; + int current_desktop; + BOOL remote_app; + HANDLE mutex; + BOOL UseXThreads; + BOOL cursorHidden; + + UINT32 bitmap_size; + BYTE* bitmap_buffer; + + BOOL frame_begin; + + int XInputOpcode; + + int savedWidth; + int savedHeight; + int savedPosX; + int savedPosY; + +#ifdef WITH_XRENDER + int scaledWidth; + int scaledHeight; + int offset_x; + int offset_y; +#endif + + BOOL focused; + BOOL mouse_active; + BOOL fullscreen_toggle; + UINT32 KeyboardLayout; + BOOL KeyboardState[256]; + XModifierKeymap* modifierMap; + wArrayList* keyCombinations; + wArrayList* xevents; + BOOL actionScriptExists; + + int attribs_mask; + XSetWindowAttributes attribs; + BOOL complex_regions; + VIRTUAL_SCREEN vscreen; +#if defined(CHANNEL_TSMF_CLIENT) + void* xv_context; +#endif + + Atom* supportedAtoms; + unsigned long supportedAtomCount; + + Atom UTF8_STRING; + + Atom _XWAYLAND_MAY_GRAB_KEYBOARD; + + Atom _NET_WM_ICON; + Atom _MOTIF_WM_HINTS; + Atom _NET_CURRENT_DESKTOP; + Atom _NET_WORKAREA; + + Atom _NET_SUPPORTED; + Atom _NET_SUPPORTING_WM_CHECK; + + Atom _NET_WM_STATE; + Atom _NET_WM_STATE_FULLSCREEN; + Atom _NET_WM_STATE_MAXIMIZED_HORZ; + Atom _NET_WM_STATE_MAXIMIZED_VERT; + Atom _NET_WM_STATE_SKIP_TASKBAR; + Atom _NET_WM_STATE_SKIP_PAGER; + + Atom _NET_WM_FULLSCREEN_MONITORS; + + Atom _NET_WM_NAME; + Atom _NET_WM_PID; + + Atom _NET_WM_WINDOW_TYPE; + Atom _NET_WM_WINDOW_TYPE_NORMAL; + Atom _NET_WM_WINDOW_TYPE_DIALOG; + Atom _NET_WM_WINDOW_TYPE_UTILITY; + Atom _NET_WM_WINDOW_TYPE_POPUP; + Atom _NET_WM_WINDOW_TYPE_POPUP_MENU; + Atom _NET_WM_WINDOW_TYPE_DROPDOWN_MENU; + + Atom _NET_WM_MOVERESIZE; + Atom _NET_MOVERESIZE_WINDOW; + + Atom WM_STATE; + Atom WM_PROTOCOLS; + Atom WM_DELETE_WINDOW; + + /* Channels */ +#if defined(CHANNEL_TSMF_CLIENT) + TsmfClientContext* tsmf; +#endif + + xfClipboard* clipboard; + CliprdrClientContext* cliprdr; + xfVideoContext* xfVideo; + xfDispContext* xfDisp; + + RailClientContext* rail; + wHashTable* railWindows; + xfRailIconCache* railIconCache; + + BOOL xkbAvailable; + BOOL xrenderAvailable; + + /* value to be sent over wire for each logical client mouse button */ + button_map button_map[NUM_BUTTONS_MAPPED]; + BYTE savedMaximizedState; + UINT32 locked; + BOOL firstPressRightCtrl; + BOOL ungrabKeyboardWithRightCtrl; + +#if defined(WITH_XI) + touchContact contacts[MAX_CONTACTS]; + int active_contacts; + int lastEvType; + XIDeviceEvent lastEvent; + double firstDist; + double lastDist; + double z_vector; + double px_vector; + double py_vector; +#endif + BOOL xi_rawevent; + BOOL xi_event; + HANDLE pipethread; +}; + +BOOL xf_create_window(xfContext* xfc); +BOOL xf_create_image(xfContext* xfc); +void xf_toggle_fullscreen(xfContext* xfc); + +enum XF_EXIT_CODE +{ + /* section 0-15: protocol-independent codes */ + XF_EXIT_SUCCESS = 0, + XF_EXIT_DISCONNECT = 1, + XF_EXIT_LOGOFF = 2, + XF_EXIT_IDLE_TIMEOUT = 3, + XF_EXIT_LOGON_TIMEOUT = 4, + XF_EXIT_CONN_REPLACED = 5, + XF_EXIT_OUT_OF_MEMORY = 6, + XF_EXIT_CONN_DENIED = 7, + XF_EXIT_CONN_DENIED_FIPS = 8, + XF_EXIT_USER_PRIVILEGES = 9, + XF_EXIT_FRESH_CREDENTIALS_REQUIRED = 10, + XF_EXIT_DISCONNECT_BY_USER = 11, + + /* section 16-31: license error set */ + XF_EXIT_LICENSE_INTERNAL = 16, + XF_EXIT_LICENSE_NO_LICENSE_SERVER = 17, + XF_EXIT_LICENSE_NO_LICENSE = 18, + XF_EXIT_LICENSE_BAD_CLIENT_MSG = 19, + XF_EXIT_LICENSE_HWID_DOESNT_MATCH = 20, + XF_EXIT_LICENSE_BAD_CLIENT = 21, + XF_EXIT_LICENSE_CANT_FINISH_PROTOCOL = 22, + XF_EXIT_LICENSE_CLIENT_ENDED_PROTOCOL = 23, + XF_EXIT_LICENSE_BAD_CLIENT_ENCRYPTION = 24, + XF_EXIT_LICENSE_CANT_UPGRADE = 25, + XF_EXIT_LICENSE_NO_REMOTE_CONNECTIONS = 26, + + /* section 32-127: RDP protocol error set */ + XF_EXIT_RDP = 32, + + /* section 128-254: xfreerdp specific exit codes */ + XF_EXIT_PARSE_ARGUMENTS = 128, + XF_EXIT_MEMORY = 129, + XF_EXIT_PROTOCOL = 130, + XF_EXIT_CONN_FAILED = 131, + XF_EXIT_AUTH_FAILURE = 132, + XF_EXIT_NEGO_FAILURE = 133, + XF_EXIT_LOGON_FAILURE = 134, + XF_EXIT_ACCOUNT_LOCKED_OUT = 135, + XF_EXIT_PRE_CONNECT_FAILED = 136, + XF_EXIT_CONNECT_UNDEFINED = 137, + XF_EXIT_POST_CONNECT_FAILED = 138, + XF_EXIT_DNS_ERROR = 139, + XF_EXIT_DNS_NAME_NOT_FOUND = 140, + XF_EXIT_CONNECT_FAILED = 141, + XF_EXIT_MCS_CONNECT_INITIAL_ERROR = 142, + XF_EXIT_TLS_CONNECT_FAILED = 143, + XF_EXIT_INSUFFICIENT_PRIVILEGES = 144, + XF_EXIT_CONNECT_CANCELLED = 145, + + XF_EXIT_CONNECT_TRANSPORT_FAILED = 147, + XF_EXIT_CONNECT_PASSWORD_EXPIRED = 148, + XF_EXIT_CONNECT_PASSWORD_MUST_CHANGE = 149, + XF_EXIT_CONNECT_KDC_UNREACHABLE = 150, + XF_EXIT_CONNECT_ACCOUNT_DISABLED = 151, + XF_EXIT_CONNECT_PASSWORD_CERTAINLY_EXPIRED = 152, + XF_EXIT_CONNECT_CLIENT_REVOKED = 153, + XF_EXIT_CONNECT_WRONG_PASSWORD = 154, + XF_EXIT_CONNECT_ACCESS_DENIED = 155, + XF_EXIT_CONNECT_ACCOUNT_RESTRICTION = 156, + XF_EXIT_CONNECT_ACCOUNT_EXPIRED = 157, + XF_EXIT_CONNECT_LOGON_TYPE_NOT_GRANTED = 158, + XF_EXIT_CONNECT_NO_OR_MISSING_CREDENTIALS = 159, + + XF_EXIT_UNKNOWN = 255, +}; + +#define xf_lock_x11(xfc) xf_lock_x11_(xfc, __func__) +#define xf_unlock_x11(xfc) xf_unlock_x11_(xfc, __func__) + +void xf_lock_x11_(xfContext* xfc, const char* fkt); +void xf_unlock_x11_(xfContext* xfc, const char* fkt); + +BOOL xf_picture_transform_required(xfContext* xfc); + +#define xf_draw_screen(_xfc, _x, _y, _w, _h) \ + xf_draw_screen_((_xfc), (_x), (_y), (_w), (_h), __func__, __FILE__, __LINE__) +void xf_draw_screen_(xfContext* xfc, int x, int y, int w, int h, const char* fkt, const char* file, + int line); + +BOOL xf_keyboard_update_modifier_map(xfContext* xfc); + +DWORD xf_exit_code_from_disconnect_reason(DWORD reason); + +#endif /* FREERDP_CLIENT_X11_FREERDP_H */ diff --git a/client/common/CMakeLists.txt b/client/common/CMakeLists.txt new file mode 100644 index 0000000..6040bf3 --- /dev/null +++ b/client/common/CMakeLists.txt @@ -0,0 +1,110 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP Client Common +# +# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set(MODULE_NAME "freerdp-client") +set(MODULE_PREFIX "FREERDP_CLIENT") + +# Policy CMP0022: INTERFACE_LINK_LIBRARIES defines the link +# interface. Run "cmake --help-policy CMP0022" for policy details. Use the +# cmake_policy command to set the policy and suppress this warning. +if(POLICY CMP0022) + cmake_policy(SET CMP0022 NEW) +endif() + +set(SRCS + client.c + cmdline.c + cmdline.h + file.c + client_cliprdr_file.c + geometry.c + smartcard_cli.c) + +foreach(FREERDP_CHANNELS_CLIENT_SRC ${FREERDP_CHANNELS_CLIENT_SRCS}) + get_filename_component(NINC ${FREERDP_CHANNELS_CLIENT_SRC} PATH) + include_directories(${NINC}) + list(APPEND SRCS "${FREERDP_CHANNELS_CLIENT_SRC}") +endforeach() + +if (NOT APPLE AND NOT WIN32 AND NOT ANDROID) + set(OPT_FUSE_DEFAULT ON) +else() + set(OPT_FUSE_DEFAULT OFF) +endif() + +option(WITH_FUSE "Build clipboard with FUSE file copy support" ${OPT_FUSE_DEFAULT}) +if(WITH_FUSE) + find_package(PkgConfig REQUIRED) + + pkg_check_modules(FUSE3 REQUIRED fuse3) + include_directories(${FUSE3_INCLUDE_DIRS}) + add_definitions(-DWITH_FUSE) + list(APPEND LIBS ${FUSE3_LIBRARIES}) + + add_definitions(-D_FILE_OFFSET_BITS=64) +endif() + +# On windows create dll version information. +# Vendor, product and year are already set in top level CMakeLists.txt +if (WIN32 AND BUILD_SHARED_LIBS) + set (RC_VERSION_MAJOR ${FREERDP_VERSION_MAJOR}) + set (RC_VERSION_MINOR ${FREERDP_VERSION_MINOR}) + set (RC_VERSION_BUILD ${FREERDP_VERSION_REVISION}) + set (RC_VERSION_FILE "${CMAKE_SHARED_LIBRARY_PREFIX}${MODULE_NAME}${FREERDP_API_VERSION}${CMAKE_SHARED_LIBRARY_SUFFIX}" ) + + configure_file( + ${PROJECT_SOURCE_DIR}/cmake/WindowsDLLVersion.rc.in + ${CMAKE_CURRENT_BINARY_DIR}/version.rc + @ONLY) + +list (APPEND SRCS ${CMAKE_CURRENT_BINARY_DIR}/version.rc) +endif() + +include_directories(${OPENSSL_INCLUDE_DIR}) + +add_library(${MODULE_NAME} ${SRCS}) + +set_target_properties(${MODULE_NAME} PROPERTIES OUTPUT_NAME ${MODULE_NAME}${FREERDP_API_VERSION}) +if (WITH_LIBRARY_VERSIONING) + set_target_properties(${MODULE_NAME} PROPERTIES VERSION ${FREERDP_VERSION} SOVERSION ${FREERDP_API_VERSION}) +endif() + +list(APPEND LIBS freerdp winpr) + +target_include_directories(${MODULE_NAME} INTERFACE $<INSTALL_INTERFACE:include>) +target_link_libraries(${MODULE_NAME} PRIVATE ${FREERDP_CHANNELS_CLIENT_LIBS}) +target_link_libraries(${MODULE_NAME} PUBLIC ${LIBS}) + +install(TARGETS ${MODULE_NAME} COMPONENT libraries EXPORT FreeRDP-ClientTargets + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + +if (WITH_DEBUG_SYMBOLS AND MSVC AND BUILD_SHARED_LIBS) + get_target_property(OUTPUT_FILENAME ${MODULE_NAME} OUTPUT_NAME) + install(FILES ${CMAKE_PDB_BINARY_DIR}/${OUTPUT_FILENAME}.pdb DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT symbols) +endif() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Client/Common") + +if(BUILD_TESTING) + add_subdirectory(test) +endif() + +if (WITH_MANPAGES) + add_subdirectory(man) +endif() diff --git a/client/common/client.c b/client/common/client.c new file mode 100644 index 0000000..9d6ec03 --- /dev/null +++ b/client/common/client.c @@ -0,0 +1,2156 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Client Common + * + * Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <string.h> +#include <errno.h> +#include <limits.h> +#include <float.h> + +#include <freerdp/client.h> + +#include <freerdp/freerdp.h> +#include <freerdp/addin.h> +#include <freerdp/assistance.h> +#include <freerdp/client/file.h> +#include <freerdp/utils/passphrase.h> +#include <freerdp/client/cmdline.h> +#include <freerdp/client/channels.h> +#include <freerdp/utils/smartcardlogon.h> + +#if defined(CHANNEL_AINPUT_CLIENT) +#include <freerdp/client/ainput.h> +#include <freerdp/channels/ainput.h> +#endif + +#if defined(CHANNEL_VIDEO_CLIENT) +#include <freerdp/client/video.h> +#include <freerdp/channels/video.h> +#endif + +#if defined(CHANNEL_RDPGFX_CLIENT) +#include <freerdp/client/rdpgfx.h> +#include <freerdp/channels/rdpgfx.h> +#include <freerdp/gdi/gfx.h> +#endif + +#if defined(CHANNEL_GEOMETRY_CLIENT) +#include <freerdp/client/geometry.h> +#include <freerdp/channels/geometry.h> +#endif + +#if defined(CHANNEL_GEOMETRY_CLIENT) || defined(CHANNEL_VIDEO_CLIENT) +#include <freerdp/gdi/video.h> +#endif + +#ifdef WITH_AAD +#include <freerdp/utils/http.h> +#include <freerdp/utils/aad.h> +#endif + +#include <freerdp/log.h> +#define TAG CLIENT_TAG("common") + +static void set_default_callbacks(freerdp* instance) +{ + WINPR_ASSERT(instance); + instance->AuthenticateEx = client_cli_authenticate_ex; + instance->ChooseSmartcard = client_cli_choose_smartcard; + instance->VerifyCertificateEx = client_cli_verify_certificate_ex; + instance->VerifyChangedCertificateEx = client_cli_verify_changed_certificate_ex; + instance->PresentGatewayMessage = client_cli_present_gateway_message; + instance->LogonErrorInfo = client_cli_logon_error_info; + instance->GetAccessToken = client_cli_get_access_token; + instance->RetryDialog = client_common_retry_dialog; +} + +static BOOL freerdp_client_common_new(freerdp* instance, rdpContext* context) +{ + RDP_CLIENT_ENTRY_POINTS* pEntryPoints = NULL; + + WINPR_ASSERT(instance); + WINPR_ASSERT(context); + + instance->LoadChannels = freerdp_client_load_channels; + set_default_callbacks(instance); + + pEntryPoints = instance->pClientEntryPoints; + WINPR_ASSERT(pEntryPoints); + return IFCALLRESULT(TRUE, pEntryPoints->ClientNew, instance, context); +} + +static void freerdp_client_common_free(freerdp* instance, rdpContext* context) +{ + RDP_CLIENT_ENTRY_POINTS* pEntryPoints = NULL; + + WINPR_ASSERT(instance); + WINPR_ASSERT(context); + + pEntryPoints = instance->pClientEntryPoints; + WINPR_ASSERT(pEntryPoints); + IFCALL(pEntryPoints->ClientFree, instance, context); +} + +/* Common API */ + +rdpContext* freerdp_client_context_new(const RDP_CLIENT_ENTRY_POINTS* pEntryPoints) +{ + freerdp* instance = NULL; + rdpContext* context = NULL; + + if (!pEntryPoints) + return NULL; + + IFCALL(pEntryPoints->GlobalInit); + instance = freerdp_new(); + + if (!instance) + return NULL; + + instance->ContextSize = pEntryPoints->ContextSize; + instance->ContextNew = freerdp_client_common_new; + instance->ContextFree = freerdp_client_common_free; + instance->pClientEntryPoints = (RDP_CLIENT_ENTRY_POINTS*)malloc(pEntryPoints->Size); + + if (!instance->pClientEntryPoints) + goto out_fail; + + CopyMemory(instance->pClientEntryPoints, pEntryPoints, pEntryPoints->Size); + + if (!freerdp_context_new_ex(instance, pEntryPoints->settings)) + goto out_fail2; + + context = instance->context; + context->instance = instance; + +#if defined(WITH_CHANNELS) + if (freerdp_register_addin_provider(freerdp_channels_load_static_addin_entry, 0) != + CHANNEL_RC_OK) + goto out_fail2; +#endif + + return context; +out_fail2: + free(instance->pClientEntryPoints); +out_fail: + freerdp_free(instance); + return NULL; +} + +void freerdp_client_context_free(rdpContext* context) +{ + freerdp* instance = NULL; + + if (!context) + return; + + instance = context->instance; + + if (instance) + { + RDP_CLIENT_ENTRY_POINTS* pEntryPoints = instance->pClientEntryPoints; + freerdp_context_free(instance); + + if (pEntryPoints) + IFCALL(pEntryPoints->GlobalUninit); + + free(instance->pClientEntryPoints); + freerdp_free(instance); + } +} + +int freerdp_client_start(rdpContext* context) +{ + RDP_CLIENT_ENTRY_POINTS* pEntryPoints = NULL; + + if (!context || !context->instance || !context->instance->pClientEntryPoints) + return ERROR_BAD_ARGUMENTS; + + if (freerdp_settings_get_bool(context->settings, FreeRDP_UseCommonStdioCallbacks)) + set_default_callbacks(context->instance); + + pEntryPoints = context->instance->pClientEntryPoints; + return IFCALLRESULT(CHANNEL_RC_OK, pEntryPoints->ClientStart, context); +} + +int freerdp_client_stop(rdpContext* context) +{ + RDP_CLIENT_ENTRY_POINTS* pEntryPoints = NULL; + + if (!context || !context->instance || !context->instance->pClientEntryPoints) + return ERROR_BAD_ARGUMENTS; + + pEntryPoints = context->instance->pClientEntryPoints; + return IFCALLRESULT(CHANNEL_RC_OK, pEntryPoints->ClientStop, context); +} + +freerdp* freerdp_client_get_instance(rdpContext* context) +{ + if (!context || !context->instance) + return NULL; + + return context->instance; +} + +HANDLE freerdp_client_get_thread(rdpContext* context) +{ + if (!context) + return NULL; + + return ((rdpClientContext*)context)->thread; +} + +static BOOL freerdp_client_settings_post_process(rdpSettings* settings) +{ + /* Moved GatewayUseSameCredentials logic outside of cmdline.c, so + * that the rdp file also triggers this functionality */ + if (freerdp_settings_get_bool(settings, FreeRDP_GatewayEnabled)) + { + if (freerdp_settings_get_bool(settings, FreeRDP_GatewayUseSameCredentials)) + { + const char* Username = freerdp_settings_get_string(settings, FreeRDP_Username); + const char* Domain = freerdp_settings_get_string(settings, FreeRDP_Domain); + if (Username) + { + if (!freerdp_settings_set_string(settings, FreeRDP_GatewayUsername, Username)) + goto out_error; + } + + if (Domain) + { + if (!freerdp_settings_set_string(settings, FreeRDP_GatewayDomain, Domain)) + goto out_error; + } + + if (freerdp_settings_get_string(settings, FreeRDP_Password)) + { + if (!freerdp_settings_set_string( + settings, FreeRDP_GatewayPassword, + freerdp_settings_get_string(settings, FreeRDP_Password))) + goto out_error; + } + } + } + + /* Moved logic for Multimon and Span monitors to force fullscreen, so + * that the rdp file also triggers this functionality */ + if (freerdp_settings_get_bool(settings, FreeRDP_SpanMonitors)) + { + freerdp_settings_set_bool(settings, FreeRDP_UseMultimon, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_Fullscreen, TRUE); + } + else if (freerdp_settings_get_bool(settings, FreeRDP_UseMultimon)) + { + freerdp_settings_set_bool(settings, FreeRDP_Fullscreen, TRUE); + } + + /* deal with the smartcard / smartcard logon stuff */ + if (freerdp_settings_get_bool(settings, FreeRDP_SmartcardLogon)) + { + freerdp_settings_set_bool(settings, FreeRDP_TlsSecurity, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_RedirectSmartCards, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_PasswordIsSmartcardPin, TRUE); + } + + return TRUE; +out_error: + return FALSE; +} + +int freerdp_client_settings_parse_command_line(rdpSettings* settings, int argc, char** argv, + BOOL allowUnknown) +{ + int status = 0; + + if (argc < 1) + return 0; + + if (!argv) + return -1; + + status = + freerdp_client_settings_parse_command_line_arguments(settings, argc, argv, allowUnknown); + + if (status < 0) + return status; + + /* This function will call logic that is applicable to the settings + * from command line parsing AND the rdp file parsing */ + if (!freerdp_client_settings_post_process(settings)) + status = -1; + + WLog_DBG(TAG, "This is %s %s", freerdp_get_version_string(), freerdp_get_build_config()); + return status; +} + +int freerdp_client_settings_parse_connection_file(rdpSettings* settings, const char* filename) +{ + rdpFile* file = NULL; + int ret = -1; + file = freerdp_client_rdp_file_new(); + + if (!file) + return -1; + + if (!freerdp_client_parse_rdp_file(file, filename)) + goto out; + + if (!freerdp_client_populate_settings_from_rdp_file(file, settings)) + goto out; + + ret = 0; +out: + freerdp_client_rdp_file_free(file); + return ret; +} + +int freerdp_client_settings_parse_connection_file_buffer(rdpSettings* settings, const BYTE* buffer, + size_t size) +{ + rdpFile* file = NULL; + int status = -1; + file = freerdp_client_rdp_file_new(); + + if (!file) + return -1; + + if (freerdp_client_parse_rdp_file_buffer(file, buffer, size) && + freerdp_client_populate_settings_from_rdp_file(file, settings)) + { + status = 0; + } + + freerdp_client_rdp_file_free(file); + return status; +} + +int freerdp_client_settings_write_connection_file(const rdpSettings* settings, const char* filename, + BOOL unicode) +{ + rdpFile* file = NULL; + int ret = -1; + file = freerdp_client_rdp_file_new(); + + if (!file) + return -1; + + if (!freerdp_client_populate_rdp_file_from_settings(file, settings)) + goto out; + + if (!freerdp_client_write_rdp_file(file, filename, unicode)) + goto out; + + ret = 0; +out: + freerdp_client_rdp_file_free(file); + return ret; +} + +int freerdp_client_settings_parse_assistance_file(rdpSettings* settings, int argc, char* argv[]) +{ + int status = 0; + int ret = -1; + char* filename = NULL; + char* password = NULL; + rdpAssistanceFile* file = NULL; + + if (!settings || !argv || (argc < 2)) + return -1; + + filename = argv[1]; + + for (int x = 2; x < argc; x++) + { + const char* key = strstr(argv[x], "assistance:"); + + if (key) + password = strchr(key, ':') + 1; + } + + file = freerdp_assistance_file_new(); + + if (!file) + return -1; + + status = freerdp_assistance_parse_file(file, filename, password); + + if (status < 0) + goto out; + + if (!freerdp_assistance_populate_settings_from_assistance_file(file, settings)) + goto out; + + ret = 0; +out: + freerdp_assistance_file_free(file); + return ret; +} + +/** Callback set in the rdp_freerdp structure, and used to get the user's password, + * if required to establish the connection. + * This function is actually called in credssp_ntlmssp_client_init() + * @see rdp_server_accept_nego() and rdp_check_fds() + * @param instance - pointer to the rdp_freerdp structure that contains the connection settings + * @param username - unused + * @param password - on return: pointer to a character string that will be filled by the password + * entered by the user. Note that this character string will be allocated inside the function, and + * needs to be deallocated by the caller using free(), even in case this function fails. + * @param domain - unused + * @return TRUE if a password was successfully entered. See freerdp_passphrase_read() for more + * details. + */ +static BOOL client_cli_authenticate_raw(freerdp* instance, rdp_auth_reason reason, char** username, + char** password, char** domain) +{ + static const size_t password_size = 512; + const char* auth[] = { "Username: ", "Domain: ", "Password: " }; + const char* authPin[] = { "Username: ", "Domain: ", "Smartcard-Pin: " }; + const char* gw[] = { "GatewayUsername: ", "GatewayDomain: ", "GatewayPassword: " }; + const char** prompt = NULL; + BOOL pinOnly = FALSE; + + WINPR_ASSERT(instance); + WINPR_ASSERT(instance->context); + WINPR_ASSERT(instance->context->settings); + + switch (reason) + { + case AUTH_SMARTCARD_PIN: + prompt = authPin; + pinOnly = TRUE; + break; + case AUTH_TLS: + case AUTH_RDP: + case AUTH_NLA: + prompt = auth; + break; + case GW_AUTH_HTTP: + case GW_AUTH_RDG: + case GW_AUTH_RPC: + prompt = gw; + break; + default: + return FALSE; + } + + if (!username || !password || !domain) + return FALSE; + + if (!*username && !pinOnly) + { + size_t username_size = 0; + printf("%s", prompt[0]); + fflush(stdout); + + if (freerdp_interruptible_get_line(instance->context, username, &username_size, stdin) < 0) + { + char ebuffer[256] = { 0 }; + WLog_ERR(TAG, "freerdp_interruptible_get_line returned %s [%d]", + winpr_strerror(errno, ebuffer, sizeof(ebuffer)), errno); + goto fail; + } + + if (*username) + { + *username = StrSep(username, "\r"); + *username = StrSep(username, "\n"); + } + } + + if (!*domain && !pinOnly) + { + size_t domain_size = 0; + printf("%s", prompt[1]); + fflush(stdout); + + if (freerdp_interruptible_get_line(instance->context, domain, &domain_size, stdin) < 0) + { + char ebuffer[256] = { 0 }; + WLog_ERR(TAG, "freerdp_interruptible_get_line returned %s [%d]", + winpr_strerror(errno, ebuffer, sizeof(ebuffer)), errno); + goto fail; + } + + if (*domain) + { + *domain = StrSep(domain, "\r"); + *domain = StrSep(domain, "\n"); + } + } + + if (!*password) + { + *password = calloc(password_size, sizeof(char)); + + if (!*password) + goto fail; + + const BOOL fromStdin = + freerdp_settings_get_bool(instance->context->settings, FreeRDP_CredentialsFromStdin); + if (freerdp_passphrase_read(instance->context, prompt[2], *password, password_size, + fromStdin) == NULL) + goto fail; + } + + return TRUE; +fail: + free(*username); + free(*domain); + free(*password); + *username = NULL; + *domain = NULL; + *password = NULL; + return FALSE; +} + +BOOL client_cli_authenticate_ex(freerdp* instance, char** username, char** password, char** domain, + rdp_auth_reason reason) +{ + WINPR_ASSERT(instance); + WINPR_ASSERT(username); + WINPR_ASSERT(password); + WINPR_ASSERT(domain); + + switch (reason) + { + case AUTH_NLA: + break; + + case AUTH_TLS: + case AUTH_RDP: + case AUTH_SMARTCARD_PIN: /* in this case password is pin code */ + if ((*username) && (*password)) + return TRUE; + break; + case GW_AUTH_HTTP: + case GW_AUTH_RDG: + case GW_AUTH_RPC: + break; + default: + return FALSE; + } + + return client_cli_authenticate_raw(instance, reason, username, password, domain); +} + +BOOL client_cli_choose_smartcard(freerdp* instance, SmartcardCertInfo** cert_list, DWORD count, + DWORD* choice, BOOL gateway) +{ + unsigned long answer = 0; + char* p = NULL; + + printf("Multiple smartcards are available for use:\n"); + for (DWORD i = 0; i < count; i++) + { + const SmartcardCertInfo* cert = cert_list[i]; + char* reader = ConvertWCharToUtf8Alloc(cert->reader, NULL); + char* container_name = ConvertWCharToUtf8Alloc(cert->containerName, NULL); + + printf("[%" PRIu32 + "] %s\n\tReader: %s\n\tUser: %s@%s\n\tSubject: %s\n\tIssuer: %s\n\tUPN: %s\n", + i, container_name, reader, cert->userHint, cert->domainHint, cert->subject, + cert->issuer, cert->upn); + + free(reader); + free(container_name); + } + + while (1) + { + char input[10] = { 0 }; + + printf("\nChoose a smartcard to use for %s (0 - %" PRIu32 "): ", + gateway ? "gateway authentication" : "logon", count - 1); + fflush(stdout); + if (!fgets(input, 10, stdin)) + { + WLog_ERR(TAG, "could not read from stdin"); + return FALSE; + } + + answer = strtoul(input, &p, 10); + if ((*p == '\n' && p != input) && answer < count) + { + *choice = answer; + return TRUE; + } + } +} + +#if defined(WITH_FREERDP_DEPRECATED) +BOOL client_cli_authenticate(freerdp* instance, char** username, char** password, char** domain) +{ + if (freerdp_settings_get_bool(instance->settings, FreeRDP_SmartcardLogon)) + { + WLog_INFO(TAG, "Authentication via smartcard"); + return TRUE; + } + + return client_cli_authenticate_raw(instance, FALSE, username, password, domain); +} + +BOOL client_cli_gw_authenticate(freerdp* instance, char** username, char** password, char** domain) +{ + return client_cli_authenticate_raw(instance, TRUE, username, password, domain); +} +#endif + +static DWORD client_cli_accept_certificate(freerdp* instance) +{ + int answer = 0; + + WINPR_ASSERT(instance); + WINPR_ASSERT(instance->context); + + const rdpSettings* settings = instance->context->settings; + WINPR_ASSERT(settings); + + const BOOL fromStdin = + freerdp_settings_get_bool(instance->context->settings, FreeRDP_CredentialsFromStdin); + if (fromStdin) + return 0; + + while (1) + { + printf("Do you trust the above certificate? (Y/T/N) "); + fflush(stdout); + answer = freerdp_interruptible_getc(instance->context, stdin); + + if ((answer == EOF) || feof(stdin)) + { + printf("\nError: Could not read answer from stdin."); + + if (fromStdin) + printf(" - Run without parameter \"--from-stdin\" to set trust."); + + printf("\n"); + return 0; + } + + switch (answer) + { + case 'y': + case 'Y': + answer = freerdp_interruptible_getc(instance->context, stdin); + if (answer == EOF) + return 0; + return 1; + + case 't': + case 'T': + answer = freerdp_interruptible_getc(instance->context, stdin); + if (answer == EOF) + return 0; + return 2; + + case 'n': + case 'N': + answer = freerdp_interruptible_getc(instance->context, stdin); + if (answer == EOF) + return 0; + return 0; + + default: + break; + } + + printf("\n"); + } +} + +/** Callback set in the rdp_freerdp structure, and used to make a certificate validation + * when the connection requires it. + * This function will actually be called by tls_verify_certificate(). + * @see rdp_client_connect() and freerdp_tls_connect() + * @deprecated Use client_cli_verify_certificate_ex + * @param instance - pointer to the rdp_freerdp structure that contains the connection settings + * @param common_name + * @param subject + * @param issuer + * @param fingerprint + * @param host_mismatch Indicates the certificate host does not match. + * @return 1 if the certificate is trusted, 2 if temporary trusted, 0 otherwise. + */ +#if defined(WITH_FREERDP_DEPRECATED) +DWORD client_cli_verify_certificate(freerdp* instance, const char* common_name, const char* subject, + const char* issuer, const char* fingerprint, BOOL host_mismatch) +{ + WINPR_UNUSED(common_name); + WINPR_UNUSED(host_mismatch); + + printf("WARNING: This callback is deprecated, migrate to client_cli_verify_certificate_ex\n"); + printf("Certificate details:\n"); + printf("\tSubject: %s\n", subject); + printf("\tIssuer: %s\n", issuer); + printf("\tThumbprint: %s\n", fingerprint); + printf("The above X.509 certificate could not be verified, possibly because you do not have\n" + "the CA certificate in your certificate store, or the certificate has expired.\n" + "Please look at the OpenSSL documentation on how to add a private CA to the store.\n"); + return client_cli_accept_certificate(instance); +} +#endif + +/** Callback set in the rdp_freerdp structure, and used to make a certificate validation + * when the connection requires it. + * This function will actually be called by tls_verify_certificate(). + * @see rdp_client_connect() and freerdp_tls_connect() + * @param instance pointer to the rdp_freerdp structure that contains the connection settings + * @param host The host currently connecting to + * @param port The port currently connecting to + * @param common_name The common name of the certificate, should match host or an alias of it + * @param subject The subject of the certificate + * @param issuer The certificate issuer name + * @param fingerprint The fingerprint of the certificate + * @param flags See VERIFY_CERT_FLAG_* for possible values. + * + * @return 1 if the certificate is trusted, 2 if temporary trusted, 0 otherwise. + */ +DWORD client_cli_verify_certificate_ex(freerdp* instance, const char* host, UINT16 port, + const char* common_name, const char* subject, + const char* issuer, const char* fingerprint, DWORD flags) +{ + const char* type = "RDP-Server"; + + WINPR_ASSERT(instance); + WINPR_ASSERT(instance->context); + WINPR_ASSERT(instance->context->settings); + + if (flags & VERIFY_CERT_FLAG_GATEWAY) + type = "RDP-Gateway"; + + if (flags & VERIFY_CERT_FLAG_REDIRECT) + type = "RDP-Redirect"; + + printf("Certificate details for %s:%" PRIu16 " (%s):\n", host, port, type); + printf("\tCommon Name: %s\n", common_name); + printf("\tSubject: %s\n", subject); + printf("\tIssuer: %s\n", issuer); + /* Newer versions of FreeRDP allow exposing the whole PEM by setting + * FreeRDP_CertificateCallbackPreferPEM to TRUE + */ + if (flags & VERIFY_CERT_FLAG_FP_IS_PEM) + { + printf("\t----------- Certificate --------------\n"); + printf("%s\n", fingerprint); + printf("\t--------------------------------------\n"); + } + else + printf("\tThumbprint: %s\n", fingerprint); + + printf("The above X.509 certificate could not be verified, possibly because you do not have\n" + "the CA certificate in your certificate store, or the certificate has expired.\n" + "Please look at the OpenSSL documentation on how to add a private CA to the store.\n"); + return client_cli_accept_certificate(instance); +} + +/** Callback set in the rdp_freerdp structure, and used to make a certificate validation + * when a stored certificate does not match the remote counterpart. + * This function will actually be called by tls_verify_certificate(). + * @see rdp_client_connect() and freerdp_tls_connect() + * @deprecated Use client_cli_verify_changed_certificate_ex + * @param instance - pointer to the rdp_freerdp structure that contains the connection settings + * @param common_name + * @param subject + * @param issuer + * @param fingerprint + * @param old_subject + * @param old_issuer + * @param old_fingerprint + * @return 1 if the certificate is trusted, 2 if temporary trusted, 0 otherwise. + */ +#if defined(WITH_FREERDP_DEPRECATED) +DWORD client_cli_verify_changed_certificate(freerdp* instance, const char* common_name, + const char* subject, const char* issuer, + const char* fingerprint, const char* old_subject, + const char* old_issuer, const char* old_fingerprint) +{ + WINPR_UNUSED(common_name); + + printf("WARNING: This callback is deprecated, migrate to " + "client_cli_verify_changed_certificate_ex\n"); + printf("!!! Certificate has changed !!!\n"); + printf("\n"); + printf("New Certificate details:\n"); + printf("\tSubject: %s\n", subject); + printf("\tIssuer: %s\n", issuer); + printf("\tThumbprint: %s\n", fingerprint); + printf("\n"); + printf("Old Certificate details:\n"); + printf("\tSubject: %s\n", old_subject); + printf("\tIssuer: %s\n", old_issuer); + printf("\tThumbprint: %s\n", old_fingerprint); + printf("\n"); + printf("The above X.509 certificate does not match the certificate used for previous " + "connections.\n" + "This may indicate that the certificate has been tampered with.\n" + "Please contact the administrator of the RDP server and clarify.\n"); + return client_cli_accept_certificate(instance); +} +#endif + +/** Callback set in the rdp_freerdp structure, and used to make a certificate validation + * when a stored certificate does not match the remote counterpart. + * This function will actually be called by tls_verify_certificate(). + * @see rdp_client_connect() and freerdp_tls_connect() + * @param instance pointer to the rdp_freerdp structure that contains the connection + * settings + * @param host The host currently connecting to + * @param port The port currently connecting to + * @param common_name The common name of the certificate, should match host or an alias of it + * @param subject The subject of the certificate + * @param issuer The certificate issuer name + * @param fingerprint The fingerprint of the certificate + * @param old_subject The subject of the previous certificate + * @param old_issuer The previous certificate issuer name + * @param old_fingerprint The fingerprint of the previous certificate + * @param flags See VERIFY_CERT_FLAG_* for possible values. + * + * @return 1 if the certificate is trusted, 2 if temporary trusted, 0 otherwise. + */ +DWORD client_cli_verify_changed_certificate_ex(freerdp* instance, const char* host, UINT16 port, + const char* common_name, const char* subject, + const char* issuer, const char* fingerprint, + const char* old_subject, const char* old_issuer, + const char* old_fingerprint, DWORD flags) +{ + const char* type = "RDP-Server"; + + WINPR_ASSERT(instance); + WINPR_ASSERT(instance->context); + WINPR_ASSERT(instance->context->settings); + + if (flags & VERIFY_CERT_FLAG_GATEWAY) + type = "RDP-Gateway"; + + if (flags & VERIFY_CERT_FLAG_REDIRECT) + type = "RDP-Redirect"; + + printf("!!!Certificate for %s:%" PRIu16 " (%s) has changed!!!\n", host, port, type); + printf("\n"); + printf("New Certificate details:\n"); + printf("\tCommon Name: %s\n", common_name); + printf("\tSubject: %s\n", subject); + printf("\tIssuer: %s\n", issuer); + /* Newer versions of FreeRDP allow exposing the whole PEM by setting + * FreeRDP_CertificateCallbackPreferPEM to TRUE + */ + if (flags & VERIFY_CERT_FLAG_FP_IS_PEM) + { + printf("\t----------- Certificate --------------\n"); + printf("%s\n", fingerprint); + printf("\t--------------------------------------\n"); + } + else + printf("\tThumbprint: %s\n", fingerprint); + printf("\n"); + printf("Old Certificate details:\n"); + printf("\tSubject: %s\n", old_subject); + printf("\tIssuer: %s\n", old_issuer); + /* Newer versions of FreeRDP allow exposing the whole PEM by setting + * FreeRDP_CertificateCallbackPreferPEM to TRUE + */ + if (flags & VERIFY_CERT_FLAG_FP_IS_PEM) + { + printf("\t----------- Certificate --------------\n"); + printf("%s\n", old_fingerprint); + printf("\t--------------------------------------\n"); + } + else + printf("\tThumbprint: %s\n", old_fingerprint); + printf("\n"); + if (flags & VERIFY_CERT_FLAG_MATCH_LEGACY_SHA1) + { + printf("\tA matching entry with legacy SHA1 was found in local known_hosts2 store.\n"); + printf("\tIf you just upgraded from a FreeRDP version before 2.0 this is expected.\n"); + printf("\tThe hashing algorithm has been upgraded from SHA1 to SHA256.\n"); + printf("\tAll manually accepted certificates must be reconfirmed!\n"); + printf("\n"); + } + printf("The above X.509 certificate does not match the certificate used for previous " + "connections.\n" + "This may indicate that the certificate has been tampered with.\n" + "Please contact the administrator of the RDP server and clarify.\n"); + return client_cli_accept_certificate(instance); +} + +BOOL client_cli_present_gateway_message(freerdp* instance, UINT32 type, BOOL isDisplayMandatory, + BOOL isConsentMandatory, size_t length, + const WCHAR* message) +{ + int answer = 0; + const char* msgType = (type == GATEWAY_MESSAGE_CONSENT) ? "Consent message" : "Service message"; + + WINPR_ASSERT(instance); + WINPR_ASSERT(instance->context); + WINPR_ASSERT(instance->context->settings); + + if (!isDisplayMandatory && !isConsentMandatory) + return TRUE; + + printf("%s:\n", msgType); +#if defined(WIN32) + printf("%.*S\n", (int)length, message); +#else + { + LPSTR msg = ConvertWCharNToUtf8Alloc(message, length / sizeof(WCHAR), NULL); + if (!msg) + { + printf("Failed to convert message!\n"); + return FALSE; + } + printf("%s\n", msg); + free(msg); + } +#endif + + while (isConsentMandatory) + { + printf("I understand and agree to the terms of this policy (Y/N) \n"); + fflush(stdout); + answer = freerdp_interruptible_getc(instance->context, stdin); + + if ((answer == EOF) || feof(stdin)) + { + printf("\nError: Could not read answer from stdin.\n"); + return FALSE; + } + + switch (answer) + { + case 'y': + case 'Y': + answer = freerdp_interruptible_getc(instance->context, stdin); + if (answer == EOF) + return FALSE; + return TRUE; + + case 'n': + case 'N': + freerdp_interruptible_getc(instance->context, stdin); + return FALSE; + + default: + break; + } + + printf("\n"); + } + + return TRUE; +} + +static char* extract_authorization_code(char* url) +{ + WINPR_ASSERT(url); + + for (char* p = strchr(url, '?'); p++ != NULL; p = strchr(p, '&')) + { + if (strncmp(p, "code=", 5) != 0) + continue; + + char* end = NULL; + p += 5; + + end = strchr(p, '&'); + if (end) + *end = 0; + else + end = strchr(p, '\0'); + + return p; + } + + return NULL; +} + +static BOOL client_cli_get_rdsaad_access_token(freerdp* instance, const char* scope, + const char* req_cnf, char** token) +{ + size_t size = 0; + char* url = NULL; + char* token_request = NULL; + const char* client_id = "a85cf173-4192-42f8-81fa-777a763e6e2c"; + const char* redirect_uri = + "https%3A%2F%2Flogin.microsoftonline.com%2Fcommon%2Foauth2%2Fnativeclient"; + + WINPR_ASSERT(instance); + WINPR_ASSERT(scope); + WINPR_ASSERT(req_cnf); + WINPR_ASSERT(token); + + *token = NULL; + + printf("Browse to: https://login.microsoftonline.com/common/oauth2/v2.0/" + "authorize?client_id=%s&response_type=" + "code&scope=%s&redirect_uri=%s" + "\n", + client_id, scope, redirect_uri); + printf("Paste redirect URL here: \n"); + + if (freerdp_interruptible_get_line(instance->context, &url, &size, stdin) < 0) + return FALSE; + + BOOL rc = FALSE; + char* code = extract_authorization_code(url); + if (!code) + goto cleanup; + + if (winpr_asprintf(&token_request, &size, + "grant_type=authorization_code&code=%s&client_id=%s&scope=%s&redirect_uri=%" + "s&req_cnf=%s", + code, client_id, scope, redirect_uri, req_cnf) <= 0) + goto cleanup; + + rc = client_common_get_access_token(instance, token_request, token); + +cleanup: + free(token_request); + free(url); + return rc && (*token != NULL); +} + +static BOOL client_cli_get_avd_access_token(freerdp* instance, char** token) +{ + size_t size = 0; + char* url = NULL; + char* token_request = NULL; + const char* client_id = "a85cf173-4192-42f8-81fa-777a763e6e2c"; + const char* redirect_uri = + "https%3A%2F%2Flogin.microsoftonline.com%2Fcommon%2Foauth2%2Fnativeclient"; + const char* scope = "https%3A%2F%2Fwww.wvd.microsoft.com%2F.default"; + + WINPR_ASSERT(instance); + WINPR_ASSERT(token); + + *token = NULL; + + printf("Browse to: https://login.microsoftonline.com/common/oauth2/v2.0/" + "authorize?client_id=%s&response_type=" + "code&scope=%s&redirect_uri=%s" + "\n", + client_id, scope, redirect_uri); + printf("Paste redirect URL here: \n"); + + if (freerdp_interruptible_get_line(instance->context, &url, &size, stdin) < 0) + return FALSE; + + BOOL rc = FALSE; + char* code = extract_authorization_code(url); + if (!code) + goto cleanup; + + if (winpr_asprintf( + &token_request, &size, + "grant_type=authorization_code&code=%s&client_id=%s&scope=%s&redirect_uri=%s", code, + client_id, scope, redirect_uri) <= 0) + goto cleanup; + + rc = client_common_get_access_token(instance, token_request, token); + +cleanup: + free(token_request); + free(url); + return rc && (*token != NULL); +} + +BOOL client_cli_get_access_token(freerdp* instance, AccessTokenType tokenType, char** token, + size_t count, ...) +{ + WINPR_ASSERT(instance); + WINPR_ASSERT(token); + switch (tokenType) + { + case ACCESS_TOKEN_TYPE_AAD: + { + if (count < 2) + { + WLog_ERR(TAG, + "ACCESS_TOKEN_TYPE_AAD expected 2 additional arguments, but got %" PRIuz + ", aborting", + count); + return FALSE; + } + else if (count > 2) + WLog_WARN(TAG, + "ACCESS_TOKEN_TYPE_AAD expected 2 additional arguments, but got %" PRIuz + ", ignoring", + count); + va_list ap; + va_start(ap, count); + const char* scope = va_arg(ap, const char*); + const char* req_cnf = va_arg(ap, const char*); + const BOOL rc = client_cli_get_rdsaad_access_token(instance, scope, req_cnf, token); + va_end(ap); + return rc; + } + case ACCESS_TOKEN_TYPE_AVD: + if (count != 0) + WLog_WARN(TAG, + "ACCESS_TOKEN_TYPE_AVD expected 0 additional arguments, but got %" PRIuz + ", ignoring", + count); + return client_cli_get_avd_access_token(instance, token); + default: + WLog_ERR(TAG, "Unexpected value for AccessTokenType [%" PRIuz "], aborting", tokenType); + return FALSE; + } +} + +BOOL client_common_get_access_token(freerdp* instance, const char* request, char** token) +{ +#ifdef WITH_AAD + WINPR_ASSERT(request); + WINPR_ASSERT(token); + + BOOL ret = FALSE; + long resp_code = 0; + BYTE* response = NULL; + size_t response_length = 0; + + wLog* log = WLog_Get(TAG); + + if (!freerdp_http_request("https://login.microsoftonline.com/common/oauth2/v2.0/token", request, + &resp_code, &response, &response_length)) + { + WLog_ERR(TAG, "access token request failed"); + return FALSE; + } + + if (resp_code != HTTP_STATUS_OK) + { + char buffer[64] = { 0 }; + + WLog_Print(log, WLOG_ERROR, + "Server unwilling to provide access token; returned status code %s", + freerdp_http_status_string_format(resp_code, buffer, sizeof(buffer))); + if (response_length > 0) + WLog_Print(log, WLOG_ERROR, "[status message] %s", response); + goto cleanup; + } + + *token = freerdp_utils_aad_get_access_token(log, (const char*)response, response_length); + if (*token) + ret = TRUE; + +cleanup: + free(response); + return ret; +#else + return FALSE; +#endif +} + +SSIZE_T client_common_retry_dialog(freerdp* instance, const char* what, size_t current, + void* userarg) +{ + WINPR_UNUSED(instance); + WINPR_ASSERT(instance->context); + WINPR_UNUSED(userarg); + WINPR_ASSERT(instance); + WINPR_ASSERT(what); + + if ((strcmp(what, "arm-transport") != 0) && (strcmp(what, "connection") != 0)) + { + WLog_ERR(TAG, "Unknown module %s, aborting", what); + return -1; + } + + if (current == 0) + { + if (strcmp(what, "arm-transport") == 0) + WLog_INFO(TAG, "[%s] Starting your VM. It may take up to 5 minutes", what); + } + + const rdpSettings* settings = instance->context->settings; + const BOOL enabled = freerdp_settings_get_bool(settings, FreeRDP_AutoReconnectionEnabled); + if (!enabled) + { + WLog_WARN(TAG, "Automatic reconnection disabled, terminating. Try to connect again later"); + return -1; + } + + const size_t max = freerdp_settings_get_uint32(settings, FreeRDP_AutoReconnectMaxRetries); + const size_t delay = freerdp_settings_get_uint32(settings, FreeRDP_TcpConnectTimeout); + if (current >= max) + { + WLog_ERR(TAG, + "[%s] retries exceeded. Your VM failed to start. Try again later or contact your " + "tech support for help if this keeps happening.", + what); + return -1; + } + + WLog_INFO(TAG, "[%s] retry %" PRIuz "/%" PRIuz ", delaying %" PRIuz "ms before next attempt", + what, current, max, delay); + return delay; +} + +BOOL client_auto_reconnect(freerdp* instance) +{ + return client_auto_reconnect_ex(instance, NULL); +} + +BOOL client_auto_reconnect_ex(freerdp* instance, BOOL (*window_events)(freerdp* instance)) +{ + BOOL retry = TRUE; + UINT32 error = 0; + UINT32 numRetries = 0; + rdpSettings* settings = NULL; + + if (!instance) + return FALSE; + + WINPR_ASSERT(instance->context); + + settings = instance->context->settings; + WINPR_ASSERT(settings); + + const UINT32 maxRetries = + freerdp_settings_get_uint32(settings, FreeRDP_AutoReconnectMaxRetries); + + /* Only auto reconnect on network disconnects. */ + error = freerdp_error_info(instance); + switch (error) + { + case ERRINFO_GRAPHICS_SUBSYSTEM_FAILED: + /* A network disconnect was detected */ + WLog_WARN(TAG, "Disconnected by server hitting a bug or resource limit [%s]", + freerdp_get_error_info_string(error)); + break; + case ERRINFO_SUCCESS: + /* A network disconnect was detected */ + WLog_INFO(TAG, "Network disconnect!"); + break; + default: + return FALSE; + } + + if (!freerdp_settings_get_bool(settings, FreeRDP_AutoReconnectionEnabled)) + { + /* No auto-reconnect - just quit */ + return FALSE; + } + + switch (freerdp_get_last_error(instance->context)) + { + case FREERDP_ERROR_CONNECT_CANCELLED: + WLog_WARN(TAG, "Connection aborted by user"); + return FALSE; + default: + break; + } + + /* Perform an auto-reconnect. */ + while (retry) + { + /* Quit retrying if max retries has been exceeded */ + if ((maxRetries > 0) && (numRetries++ >= maxRetries)) + { + return FALSE; + } + + /* Attempt the next reconnect */ + WLog_INFO(TAG, "Attempting reconnect (%" PRIu32 " of %" PRIu32 ")", numRetries, maxRetries); + + IFCALL(instance->RetryDialog, instance, "connection", numRetries, NULL); + + if (freerdp_reconnect(instance)) + return TRUE; + + switch (freerdp_get_last_error(instance->context)) + { + case FREERDP_ERROR_CONNECT_CANCELLED: + WLog_WARN(TAG, "Autoreconnect aborted by user"); + return FALSE; + default: + break; + } + for (UINT32 x = 0; x < 50; x++) + { + if (!IFCALLRESULT(TRUE, window_events, instance)) + return FALSE; + + Sleep(10); + } + } + + WLog_ERR(TAG, "Maximum reconnect retries exceeded"); + return FALSE; +} + +int freerdp_client_common_stop(rdpContext* context) +{ + rdpClientContext* cctx = (rdpClientContext*)context; + WINPR_ASSERT(cctx); + + freerdp_abort_connect_context(&cctx->context); + + if (cctx->thread) + { + WaitForSingleObject(cctx->thread, INFINITE); + CloseHandle(cctx->thread); + cctx->thread = NULL; + } + + return 0; +} + +#if defined(CHANNEL_ENCOMSP_CLIENT) +BOOL freerdp_client_encomsp_toggle_control(EncomspClientContext* encomsp) +{ + rdpClientContext* cctx = NULL; + BOOL state = 0; + + if (!encomsp) + return FALSE; + + cctx = (rdpClientContext*)encomsp->custom; + + state = cctx->controlToggle; + cctx->controlToggle = !cctx->controlToggle; + return freerdp_client_encomsp_set_control(encomsp, state); +} + +BOOL freerdp_client_encomsp_set_control(EncomspClientContext* encomsp, BOOL control) +{ + ENCOMSP_CHANGE_PARTICIPANT_CONTROL_LEVEL_PDU pdu = { 0 }; + + if (!encomsp) + return FALSE; + + pdu.ParticipantId = encomsp->participantId; + pdu.Flags = ENCOMSP_REQUEST_VIEW; + + if (control) + pdu.Flags |= ENCOMSP_REQUEST_INTERACT; + + encomsp->ChangeParticipantControlLevel(encomsp, &pdu); + + return TRUE; +} + +static UINT +client_encomsp_participant_created(EncomspClientContext* context, + const ENCOMSP_PARTICIPANT_CREATED_PDU* participantCreated) +{ + rdpClientContext* cctx = NULL; + rdpSettings* settings = NULL; + BOOL request = 0; + + if (!context || !context->custom || !participantCreated) + return ERROR_INVALID_PARAMETER; + + cctx = (rdpClientContext*)context->custom; + WINPR_ASSERT(cctx); + + settings = cctx->context.settings; + WINPR_ASSERT(settings); + + if (participantCreated->Flags & ENCOMSP_IS_PARTICIPANT) + context->participantId = participantCreated->ParticipantId; + + request = freerdp_settings_get_bool(settings, FreeRDP_RemoteAssistanceRequestControl); + if (request && (participantCreated->Flags & ENCOMSP_MAY_VIEW) && + !(participantCreated->Flags & ENCOMSP_MAY_INTERACT)) + { + if (!freerdp_client_encomsp_set_control(context, TRUE)) + return ERROR_INTERNAL_ERROR; + + /* if auto-request-control setting is enabled then only request control once upon connect, + * otherwise it will auto request control again every time server turns off control which + * is a bit annoying */ + freerdp_settings_set_bool(settings, FreeRDP_RemoteAssistanceRequestControl, FALSE); + } + + return CHANNEL_RC_OK; +} + +static void client_encomsp_init(rdpClientContext* cctx, EncomspClientContext* encomsp) +{ + cctx->encomsp = encomsp; + encomsp->custom = (void*)cctx; + encomsp->ParticipantCreated = client_encomsp_participant_created; +} + +static void client_encomsp_uninit(rdpClientContext* cctx, EncomspClientContext* encomsp) +{ + if (encomsp) + { + encomsp->custom = NULL; + encomsp->ParticipantCreated = NULL; + } + + if (cctx) + cctx->encomsp = NULL; +} +#endif + +void freerdp_client_OnChannelConnectedEventHandler(void* context, + const ChannelConnectedEventArgs* e) +{ + rdpClientContext* cctx = (rdpClientContext*)context; + + WINPR_ASSERT(cctx); + WINPR_ASSERT(e); + + if (0) + { + } +#if defined(CHANNEL_AINPUT_CLIENT) + else if (strcmp(e->name, AINPUT_DVC_CHANNEL_NAME) == 0) + cctx->ainput = (AInputClientContext*)e->pInterface; +#endif +#if defined(CHANNEL_RDPEI_CLIENT) + else if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) == 0) + { + cctx->rdpei = (RdpeiClientContext*)e->pInterface; + } +#endif +#if defined(CHANNEL_RDPGFX_CLIENT) + else if (strcmp(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) + { + gdi_graphics_pipeline_init(cctx->context.gdi, (RdpgfxClientContext*)e->pInterface); + } +#endif +#if defined(CHANNEL_GEOMETRY_CLIENT) + else if (strcmp(e->name, GEOMETRY_DVC_CHANNEL_NAME) == 0) + { + gdi_video_geometry_init(cctx->context.gdi, (GeometryClientContext*)e->pInterface); + } +#endif +#if defined(CHANNEL_VIDEO_CLIENT) + else if (strcmp(e->name, VIDEO_CONTROL_DVC_CHANNEL_NAME) == 0) + { + gdi_video_control_init(cctx->context.gdi, (VideoClientContext*)e->pInterface); + } + else if (strcmp(e->name, VIDEO_DATA_DVC_CHANNEL_NAME) == 0) + { + gdi_video_data_init(cctx->context.gdi, (VideoClientContext*)e->pInterface); + } +#endif +#if defined(CHANNEL_ENCOMSP_CLIENT) + else if (strcmp(e->name, ENCOMSP_SVC_CHANNEL_NAME) == 0) + { + client_encomsp_init(cctx, (EncomspClientContext*)e->pInterface); + } +#endif +} + +void freerdp_client_OnChannelDisconnectedEventHandler(void* context, + const ChannelDisconnectedEventArgs* e) +{ + rdpClientContext* cctx = (rdpClientContext*)context; + + WINPR_ASSERT(cctx); + WINPR_ASSERT(e); + + if (0) + { + } +#if defined(CHANNEL_AINPUT_CLIENT) + else if (strcmp(e->name, AINPUT_DVC_CHANNEL_NAME) == 0) + cctx->ainput = NULL; +#endif +#if defined(CHANNEL_RDPEI_CLIENT) + else if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) == 0) + { + cctx->rdpei = NULL; + } +#endif +#if defined(CHANNEL_RDPGFX_CLIENT) + else if (strcmp(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) + { + gdi_graphics_pipeline_uninit(cctx->context.gdi, (RdpgfxClientContext*)e->pInterface); + } +#endif +#if defined(CHANNEL_GEOMETRY_CLIENT) + else if (strcmp(e->name, GEOMETRY_DVC_CHANNEL_NAME) == 0) + { + gdi_video_geometry_uninit(cctx->context.gdi, (GeometryClientContext*)e->pInterface); + } +#endif +#if defined(CHANNEL_VIDEO_CLIENT) + else if (strcmp(e->name, VIDEO_CONTROL_DVC_CHANNEL_NAME) == 0) + { + gdi_video_control_uninit(cctx->context.gdi, (VideoClientContext*)e->pInterface); + } + else if (strcmp(e->name, VIDEO_DATA_DVC_CHANNEL_NAME) == 0) + { + gdi_video_data_uninit(cctx->context.gdi, (VideoClientContext*)e->pInterface); + } +#endif +#if defined(CHANNEL_ENCOMSP_CLIENT) + else if (strcmp(e->name, ENCOMSP_SVC_CHANNEL_NAME) == 0) + { + client_encomsp_uninit(cctx, (EncomspClientContext*)e->pInterface); + } +#endif +} + +BOOL freerdp_client_send_wheel_event(rdpClientContext* cctx, UINT16 mflags) +{ + BOOL handled = FALSE; + + WINPR_ASSERT(cctx); + +#if defined(CHANNEL_AINPUT_CLIENT) + if (cctx->ainput) + { + UINT rc = 0; + UINT64 flags = 0; + INT32 x = 0; + INT32 y = 0; + INT32 value = mflags & 0xFF; + + if (mflags & PTR_FLAGS_WHEEL_NEGATIVE) + value = -1 * (0x100 - value); + + /* We have discrete steps, scale this so we can also support high + * resolution wheels. */ + value *= 0x10000; + + if (mflags & PTR_FLAGS_WHEEL) + { + flags |= AINPUT_FLAGS_WHEEL; + y = value; + } + + if (mflags & PTR_FLAGS_HWHEEL) + { + flags |= AINPUT_FLAGS_WHEEL; + x = value; + } + + WINPR_ASSERT(cctx->ainput->AInputSendInputEvent); + rc = cctx->ainput->AInputSendInputEvent(cctx->ainput, flags, x, y); + if (rc == CHANNEL_RC_OK) + handled = TRUE; + } +#endif + + if (!handled) + freerdp_input_send_mouse_event(cctx->context.input, mflags, 0, 0); + + return TRUE; +} + +#if defined(CHANNEL_AINPUT_CLIENT) +static INLINE BOOL ainput_send_diff_event(rdpClientContext* cctx, UINT64 flags, INT32 x, INT32 y) +{ + UINT rc = 0; + + WINPR_ASSERT(cctx); + WINPR_ASSERT(cctx->ainput); + WINPR_ASSERT(cctx->ainput->AInputSendInputEvent); + + rc = cctx->ainput->AInputSendInputEvent(cctx->ainput, flags, x, y); + + return rc == CHANNEL_RC_OK; +} +#endif + +BOOL freerdp_client_send_button_event(rdpClientContext* cctx, BOOL relative, UINT16 mflags, INT32 x, + INT32 y) +{ + BOOL handled = FALSE; + + WINPR_ASSERT(cctx); + + const BOOL relativeInput = freerdp_client_use_relative_mouse_events(cctx); + if (relative && relativeInput) + { + return freerdp_input_send_rel_mouse_event(cctx->context.input, mflags, x, y); + } + +#if defined(CHANNEL_AINPUT_CLIENT) + if (cctx->ainput) + { + UINT64 flags = 0; + + if (cctx->mouse_grabbed && relativeInput) + flags |= AINPUT_FLAGS_HAVE_REL; + + if (relative) + flags |= AINPUT_FLAGS_REL; + + if (mflags & PTR_FLAGS_DOWN) + flags |= AINPUT_FLAGS_DOWN; + if (mflags & PTR_FLAGS_BUTTON1) + flags |= AINPUT_FLAGS_BUTTON1; + if (mflags & PTR_FLAGS_BUTTON2) + flags |= AINPUT_FLAGS_BUTTON2; + if (mflags & PTR_FLAGS_BUTTON3) + flags |= AINPUT_FLAGS_BUTTON3; + if (mflags & PTR_FLAGS_MOVE) + flags |= AINPUT_FLAGS_MOVE; + handled = ainput_send_diff_event(cctx, flags, x, y); + } +#endif + + if (!handled) + { + if (relative) + { + cctx->lastX += x; + cctx->lastY += y; + WLog_WARN(TAG, "Relative mouse input channel not available, sending absolute!"); + } + else + { + cctx->lastX = x; + cctx->lastY = y; + } + freerdp_input_send_mouse_event(cctx->context.input, mflags, (UINT16)cctx->lastX, + (UINT16)cctx->lastY); + } + return TRUE; +} + +BOOL freerdp_client_send_extended_button_event(rdpClientContext* cctx, BOOL relative, UINT16 mflags, + INT32 x, INT32 y) +{ + BOOL handled = FALSE; + WINPR_ASSERT(cctx); + + if (relative && freerdp_client_use_relative_mouse_events(cctx)) + { + return freerdp_input_send_rel_mouse_event(cctx->context.input, mflags, x, y); + } + +#if defined(CHANNEL_AINPUT_CLIENT) + if (cctx->ainput) + { + UINT64 flags = 0; + + if (relative) + flags |= AINPUT_FLAGS_REL; + if (mflags & PTR_XFLAGS_DOWN) + flags |= AINPUT_FLAGS_DOWN; + if (mflags & PTR_XFLAGS_BUTTON1) + flags |= AINPUT_XFLAGS_BUTTON1; + if (mflags & PTR_XFLAGS_BUTTON2) + flags |= AINPUT_XFLAGS_BUTTON2; + + handled = ainput_send_diff_event(cctx, flags, x, y); + } +#endif + + if (!handled) + { + if (relative) + { + cctx->lastX += x; + cctx->lastY += y; + WLog_WARN(TAG, "Relative mouse input channel not available, sending absolute!"); + } + else + { + cctx->lastX = x; + cctx->lastY = y; + } + freerdp_input_send_extended_mouse_event(cctx->context.input, mflags, (UINT16)cctx->lastX, + (UINT16)cctx->lastY); + } + + return TRUE; +} + +static BOOL freerdp_handle_touch_up(rdpClientContext* cctx, const FreeRDP_TouchContact* contact) +{ + WINPR_ASSERT(cctx); + WINPR_ASSERT(contact); + +#if defined(CHANNEL_RDPEI_CLIENT) + RdpeiClientContext* rdpei = cctx->rdpei; + + if (!rdpei) + { + UINT16 flags = 0; + flags |= PTR_FLAGS_BUTTON1; + + WINPR_ASSERT(contact->x <= UINT16_MAX); + WINPR_ASSERT(contact->y <= UINT16_MAX); + return freerdp_client_send_button_event(cctx, FALSE, flags, contact->x, contact->y); + } + else + { + int contactId = 0; + + if (rdpei->TouchRawEvent) + { + const UINT32 flags = RDPINPUT_CONTACT_FLAG_UP; + const UINT32 contactFlags = ((contact->flags & FREERDP_TOUCH_HAS_PRESSURE) != 0) + ? CONTACT_DATA_PRESSURE_PRESENT + : 0; + // Ensure contact position is unchanged from "engaged" to "out of range" state + rdpei->TouchRawEvent(rdpei, contact->id, contact->x, contact->y, &contactId, + RDPINPUT_CONTACT_FLAG_UPDATE | RDPINPUT_CONTACT_FLAG_INRANGE | + RDPINPUT_CONTACT_FLAG_INCONTACT, + contactFlags, contact->pressure); + rdpei->TouchRawEvent(rdpei, contact->id, contact->x, contact->y, &contactId, flags, + contactFlags, contact->pressure); + } + else + { + WINPR_ASSERT(rdpei->TouchEnd); + rdpei->TouchEnd(rdpei, contact->id, contact->x, contact->y, &contactId); + } + } +#else + WLog_WARN(TAG, "Touch event detected but RDPEI support not compiled in. Recompile with " + "-DWITH_CHANNELS=ON"); +#endif + + return TRUE; +} + +static BOOL freerdp_handle_touch_down(rdpClientContext* cctx, const FreeRDP_TouchContact* contact) +{ + WINPR_ASSERT(cctx); + WINPR_ASSERT(contact); + +#if defined(CHANNEL_RDPEI_CLIENT) + RdpeiClientContext* rdpei = cctx->rdpei; + + // Emulate mouse click if touch is not possible, like in login screen + if (!rdpei) + { + UINT16 flags = 0; + flags |= PTR_FLAGS_DOWN; + flags |= PTR_FLAGS_MOVE; + flags |= PTR_FLAGS_BUTTON1; + + WINPR_ASSERT(contact->x <= UINT16_MAX); + WINPR_ASSERT(contact->y <= UINT16_MAX); + return freerdp_client_send_button_event(cctx, FALSE, flags, contact->x, contact->y); + } + else + { + int contactId = 0; + + if (rdpei->TouchRawEvent) + { + const UINT32 flags = RDPINPUT_CONTACT_FLAG_DOWN | RDPINPUT_CONTACT_FLAG_INRANGE | + RDPINPUT_CONTACT_FLAG_INCONTACT; + const UINT32 contactFlags = ((contact->flags & FREERDP_TOUCH_HAS_PRESSURE) != 0) + ? CONTACT_DATA_PRESSURE_PRESENT + : 0; + rdpei->TouchRawEvent(rdpei, contact->id, contact->x, contact->y, &contactId, flags, + contactFlags, contact->pressure); + } + else + { + WINPR_ASSERT(rdpei->TouchBegin); + rdpei->TouchBegin(rdpei, contact->id, contact->x, contact->y, &contactId); + } + } +#else + WLog_WARN(TAG, "Touch event detected but RDPEI support not compiled in. Recompile with " + "-DWITH_CHANNELS=ON"); +#endif + + return TRUE; +} + +static BOOL freerdp_handle_touch_motion(rdpClientContext* cctx, const FreeRDP_TouchContact* contact) +{ + WINPR_ASSERT(cctx); + WINPR_ASSERT(contact); + +#if defined(CHANNEL_RDPEI_CLIENT) + RdpeiClientContext* rdpei = cctx->rdpei; + + if (!rdpei) + { + UINT16 flags = 0; + flags |= PTR_FLAGS_MOVE; + + WINPR_ASSERT(contact->x <= UINT16_MAX); + WINPR_ASSERT(contact->y <= UINT16_MAX); + return freerdp_client_send_button_event(cctx, FALSE, flags, contact->x, contact->y); + } + else + { + int contactId = 0; + + if (rdpei->TouchRawEvent) + { + const UINT32 flags = RDPINPUT_CONTACT_FLAG_UPDATE | RDPINPUT_CONTACT_FLAG_INRANGE | + RDPINPUT_CONTACT_FLAG_INCONTACT; + const UINT32 contactFlags = ((contact->flags & FREERDP_TOUCH_HAS_PRESSURE) != 0) + ? CONTACT_DATA_PRESSURE_PRESENT + : 0; + rdpei->TouchRawEvent(rdpei, contact->id, contact->x, contact->y, &contactId, flags, + contactFlags, contact->pressure); + } + else + { + WINPR_ASSERT(rdpei->TouchUpdate); + rdpei->TouchUpdate(rdpei, contact->id, contact->x, contact->y, &contactId); + } + } +#else + WLog_WARN(TAG, "Touch event detected but RDPEI support not compiled in. Recompile with " + "-DWITH_CHANNELS=ON"); +#endif + + return TRUE; +} + +static BOOL freerdp_client_touch_update(rdpClientContext* cctx, UINT32 flags, INT32 touchId, + UINT32 pressure, INT32 x, INT32 y, + FreeRDP_TouchContact* pcontact) +{ + WINPR_ASSERT(cctx); + WINPR_ASSERT(pcontact); + + for (size_t i = 0; i < ARRAYSIZE(cctx->contacts); i++) + { + FreeRDP_TouchContact* contact = &cctx->contacts[i]; + + const BOOL newcontact = ((contact->id == 0) && ((flags & FREERDP_TOUCH_DOWN) != 0)); + if (newcontact || (contact->id == touchId)) + { + contact->id = touchId; + contact->flags = flags; + contact->pressure = pressure; + contact->x = x; + contact->y = y; + + *pcontact = *contact; + + const BOOL resetcontact = (flags & FREERDP_TOUCH_UP) != 0; + if (resetcontact) + { + FreeRDP_TouchContact empty = { 0 }; + *contact = empty; + } + return TRUE; + } + } + + return FALSE; +} + +BOOL freerdp_client_handle_touch(rdpClientContext* cctx, UINT32 flags, INT32 finger, + UINT32 pressure, INT32 x, INT32 y) +{ + const UINT32 mask = FREERDP_TOUCH_DOWN | FREERDP_TOUCH_UP | FREERDP_TOUCH_MOTION; + WINPR_ASSERT(cctx); + + FreeRDP_TouchContact contact = { 0 }; + + if (!freerdp_client_touch_update(cctx, flags, finger, pressure, x, y, &contact)) + return FALSE; + + switch (flags & mask) + { + case FREERDP_TOUCH_DOWN: + return freerdp_handle_touch_down(cctx, &contact); + case FREERDP_TOUCH_UP: + return freerdp_handle_touch_up(cctx, &contact); + case FREERDP_TOUCH_MOTION: + return freerdp_handle_touch_motion(cctx, &contact); + default: + WLog_WARN(TAG, "Unhandled FreeRDPTouchEventType %d, ignoring", flags); + return FALSE; + } +} + +BOOL freerdp_client_load_channels(freerdp* instance) +{ + WINPR_ASSERT(instance); + WINPR_ASSERT(instance->context); + + if (!freerdp_client_load_addins(instance->context->channels, instance->context->settings)) + { + WLog_ERR(TAG, "Failed to load addins [%08" PRIx32 "]", GetLastError()); + return FALSE; + } + return TRUE; +} + +int client_cli_logon_error_info(freerdp* instance, UINT32 data, UINT32 type) +{ + const char* str_data = freerdp_get_logon_error_info_data(data); + const char* str_type = freerdp_get_logon_error_info_type(type); + + if (!instance || !instance->context) + return -1; + + WLog_INFO(TAG, "Logon Error Info %s [%s]", str_data, str_type); + return 1; +} + +static FreeRDP_PenDevice* freerdp_client_get_pen(rdpClientContext* cctx, INT32 deviceid, + size_t* pos) +{ + WINPR_ASSERT(cctx); + + for (size_t i = 0; i < ARRAYSIZE(cctx->pens); i++) + { + FreeRDP_PenDevice* pen = &cctx->pens[i]; + if (deviceid == pen->deviceid) + { + if (pos) + *pos = i; + return pen; + } + } + return NULL; +} + +static BOOL freerdp_client_register_pen(rdpClientContext* cctx, UINT32 flags, INT32 deviceid, + double pressure) +{ + static const INT32 null_deviceid = 0; + + WINPR_ASSERT(cctx); + WINPR_ASSERT((flags & FREERDP_PEN_REGISTER) != 0); + if (freerdp_client_is_pen(cctx, deviceid)) + { + WLog_WARN(TAG, "trying to double register pen device %" PRId32, deviceid); + return FALSE; + } + + size_t pos = 0; + FreeRDP_PenDevice* pen = freerdp_client_get_pen(cctx, null_deviceid, &pos); + if (pen) + { + const FreeRDP_PenDevice empty = { 0 }; + *pen = empty; + + pen->deviceid = deviceid; + pen->max_pressure = pressure; + pen->flags = flags; + + WLog_DBG(TAG, "registered pen at index %" PRIuz, pos); + return TRUE; + } + + WLog_WARN(TAG, "No free slots for an additiona pen device, skipping"); + return TRUE; +} + +BOOL freerdp_client_handle_pen(rdpClientContext* cctx, UINT32 flags, INT32 deviceid, ...) +{ + if ((flags & FREERDP_PEN_REGISTER) != 0) + { + va_list args; + + va_start(args, deviceid); + double pressure = va_arg(args, double); + va_end(args); + return freerdp_client_register_pen(cctx, flags, deviceid, pressure); + } + size_t pos = 0; + FreeRDP_PenDevice* pen = freerdp_client_get_pen(cctx, deviceid, &pos); + if (!pen) + { + WLog_WARN(TAG, "unregistered pen device %" PRId32 " event 0x%08" PRIx32, deviceid, flags); + return FALSE; + } + + UINT32 fieldFlags = RDPINPUT_PEN_CONTACT_PENFLAGS_PRESENT; + UINT32 penFlags = + ((pen->flags & FREERDP_PEN_IS_INVERTED) != 0) ? RDPINPUT_PEN_FLAG_INVERTED : 0; + + RdpeiClientContext* rdpei = cctx->rdpei; + WINPR_ASSERT(rdpei); + + UINT32 normalizedpressure = 1024; + INT32 x = 0; + INT32 y = 0; + UINT16 rotation = 0; + INT16 tiltX = 0; + INT16 tiltY = 0; + va_list args; + va_start(args, deviceid); + + x = va_arg(args, INT32); + y = va_arg(args, INT32); + if ((flags & FREERDP_PEN_HAS_PRESSURE) != 0) + { + const double pressure = va_arg(args, double); + normalizedpressure = (pressure * 1024) / pen->max_pressure; + WLog_DBG(TAG, "pen pressure %lf -> %" PRIu32, pressure, normalizedpressure); + fieldFlags |= RDPINPUT_PEN_CONTACT_PRESSURE_PRESENT; + } + if ((flags & FREERDP_PEN_HAS_ROTATION) != 0) + { + rotation = va_arg(args, unsigned); + fieldFlags |= RDPINPUT_PEN_CONTACT_ROTATION_PRESENT; + } + if ((flags & FREERDP_PEN_HAS_TILTX) != 0) + { + tiltX = va_arg(args, int); + fieldFlags |= RDPINPUT_PEN_CONTACT_TILTX_PRESENT; + } + if ((flags & FREERDP_PEN_HAS_TILTY) != 0) + { + tiltX = va_arg(args, int); + fieldFlags |= RDPINPUT_PEN_CONTACT_TILTY_PRESENT; + } + va_end(args); + + if ((flags & FREERDP_PEN_PRESS) != 0) + { + // Ensure that only one button is pressed + if (pen->pressed) + flags = FREERDP_PEN_MOTION | + (flags & (UINT32) ~(FREERDP_PEN_PRESS | FREERDP_PEN_BARREL_PRESSED)); + else if ((flags & FREERDP_PEN_BARREL_PRESSED) != 0) + pen->flags |= FREERDP_PEN_BARREL_PRESSED; + } + else if ((flags & FREERDP_PEN_RELEASE) != 0) + { + if (!pen->pressed || + ((flags & FREERDP_PEN_BARREL_PRESSED) ^ (pen->flags & FREERDP_PEN_BARREL_PRESSED))) + flags = FREERDP_PEN_MOTION | + (flags & (UINT32) ~(FREERDP_PEN_RELEASE | FREERDP_PEN_BARREL_PRESSED)); + else + pen->flags &= (UINT32)~FREERDP_PEN_BARREL_PRESSED; + } + + flags |= pen->flags; + if ((flags & FREERDP_PEN_ERASER_PRESSED) != 0) + penFlags |= RDPINPUT_PEN_FLAG_ERASER_PRESSED; + if ((flags & FREERDP_PEN_BARREL_PRESSED) != 0) + penFlags |= RDPINPUT_PEN_FLAG_BARREL_PRESSED; + + pen->last_x = x; + pen->last_y = y; + if ((flags & FREERDP_PEN_PRESS) != 0) + { + WLog_DBG(TAG, "Pen press %" PRId32, deviceid); + pen->hovering = FALSE; + pen->pressed = TRUE; + + WINPR_ASSERT(rdpei->PenBegin); + const UINT rc = rdpei->PenBegin(rdpei, deviceid, fieldFlags, x, y, penFlags, + normalizedpressure, rotation, tiltX, tiltY); + return rc == CHANNEL_RC_OK; + } + else if ((flags & FREERDP_PEN_MOTION) != 0) + { + UINT rc = ERROR_INTERNAL_ERROR; + if (pen->pressed) + { + WLog_DBG(TAG, "Pen update %" PRId32, deviceid); + + // TODO: what if no rotation is supported but tilt is? + WINPR_ASSERT(rdpei->PenUpdate); + rc = rdpei->PenUpdate(rdpei, deviceid, fieldFlags, x, y, penFlags, normalizedpressure, + rotation, tiltX, tiltY); + } + else if (pen->hovering) + { + WLog_DBG(TAG, "Pen hover update %" PRId32, deviceid); + + WINPR_ASSERT(rdpei->PenHoverUpdate); + rc = rdpei->PenHoverUpdate(rdpei, deviceid, RDPINPUT_PEN_CONTACT_PENFLAGS_PRESENT, x, y, + penFlags, normalizedpressure, rotation, tiltX, tiltY); + } + else + { + WLog_DBG(TAG, "Pen hover begin %" PRId32, deviceid); + pen->hovering = TRUE; + + WINPR_ASSERT(rdpei->PenHoverBegin); + rc = rdpei->PenHoverBegin(rdpei, deviceid, RDPINPUT_PEN_CONTACT_PENFLAGS_PRESENT, x, y, + penFlags, normalizedpressure, rotation, tiltX, tiltY); + } + return rc == CHANNEL_RC_OK; + } + else if ((flags & FREERDP_PEN_RELEASE) != 0) + { + WLog_DBG(TAG, "Pen release %" PRId32, deviceid); + pen->pressed = FALSE; + pen->hovering = TRUE; + + WINPR_ASSERT(rdpei->PenUpdate); + const UINT rc = rdpei->PenUpdate(rdpei, deviceid, fieldFlags, x, y, penFlags, + normalizedpressure, rotation, tiltX, tiltY); + if (rc != CHANNEL_RC_OK) + return FALSE; + WINPR_ASSERT(rdpei->PenEnd); + const UINT re = rdpei->PenEnd(rdpei, deviceid, RDPINPUT_PEN_CONTACT_PENFLAGS_PRESENT, x, y, + penFlags, normalizedpressure, rotation, tiltX, tiltY); + return re == CHANNEL_RC_OK; + } + + WLog_WARN(TAG, "Invalid pen %" PRId32 " flags 0x%08" PRIx32, deviceid, flags); + return FALSE; +} + +BOOL freerdp_client_pen_cancel_all(rdpClientContext* cctx) +{ + WINPR_ASSERT(cctx); + + RdpeiClientContext* rdpei = cctx->rdpei; + + if (!rdpei) + return FALSE; + + for (size_t i = 0; i < ARRAYSIZE(cctx->pens); i++) + { + FreeRDP_PenDevice* pen = &cctx->pens[i]; + if (pen->hovering) + { + WLog_DBG(TAG, "unhover pen %" PRId32, pen->deviceid); + pen->hovering = FALSE; + rdpei->PenHoverCancel(rdpei, pen->deviceid, 0, pen->last_x, pen->last_y); + } + } + return TRUE; +} + +BOOL freerdp_client_is_pen(rdpClientContext* cctx, INT32 deviceid) +{ + WINPR_ASSERT(cctx); + + if (deviceid == 0) + return FALSE; + + for (size_t x = 0; x < ARRAYSIZE(cctx->pens); x++) + { + const FreeRDP_PenDevice* pen = &cctx->pens[x]; + if (pen->deviceid == deviceid) + return TRUE; + } + + return FALSE; +} + +BOOL freerdp_client_use_relative_mouse_events(rdpClientContext* ccontext) +{ + WINPR_ASSERT(ccontext); + + const rdpSettings* settings = ccontext->context.settings; + const BOOL useRelative = freerdp_settings_get_bool(settings, FreeRDP_MouseUseRelativeMove); + const BOOL haveRelative = freerdp_settings_get_bool(settings, FreeRDP_HasRelativeMouseEvent); + BOOL ainput = false; +#if defined(CHANNEL_AINPUT_SERVER) + ainput = ccontext->ainput != NULL; +#endif + + return useRelative && (haveRelative || ainput); +} diff --git a/client/common/client_cliprdr_file.c b/client/common/client_cliprdr_file.c new file mode 100644 index 0000000..9b3ee22 --- /dev/null +++ b/client/common/client_cliprdr_file.c @@ -0,0 +1,2556 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * X11 Clipboard Redirection + * + * Copyright 2010-2011 Vic Lee + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com> + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * Copyright 2023 Pascal Nowack <Pascal.Nowack@gmx.de> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <stdlib.h> +#include <errno.h> + +#ifdef WITH_FUSE +#define FUSE_USE_VERSION 30 +#include <fuse_lowlevel.h> +#endif + +#if defined(WITH_FUSE) +#include <sys/mount.h> +#include <sys/stat.h> +#include <errno.h> +#include <time.h> +#endif + +#include <winpr/crt.h> +#include <winpr/assert.h> +#include <winpr/image.h> +#include <winpr/stream.h> +#include <winpr/clipboard.h> +#include <winpr/path.h> + +#include <freerdp/utils/signal.h> +#include <freerdp/log.h> +#include <freerdp/client/cliprdr.h> +#include <freerdp/channels/channels.h> +#include <freerdp/channels/cliprdr.h> + +#include <freerdp/client/client_cliprdr_file.h> + +#define MAX_CLIP_DATA_DIR_LEN 10 +#define MAX_CLIPBOARD_FORMATS 255 +#define NO_CLIP_DATA_ID (UINT64_C(1) << 32) +#define WIN32_FILETIME_TO_UNIX_EPOCH UINT64_C(11644473600) + +#ifdef WITH_DEBUG_CLIPRDR +#define DEBUG_CLIPRDR(log, ...) WLog_Print(log, WLOG_DEBUG, __VA_ARGS__) +#else +#define DEBUG_CLIPRDR(log, ...) \ + do \ + { \ + } while (0) +#endif + +#if defined(WITH_FUSE) +typedef enum eFuseLowlevelOperationType +{ + FUSE_LL_OPERATION_NONE, + FUSE_LL_OPERATION_LOOKUP, + FUSE_LL_OPERATION_GETATTR, + FUSE_LL_OPERATION_READ, +} FuseLowlevelOperationType; + +typedef struct sCliprdrFuseFile CliprdrFuseFile; + +struct sCliprdrFuseFile +{ + CliprdrFuseFile* parent; + wArrayList* children; + + char* filename; + char* filename_with_root; + UINT32 list_idx; + fuse_ino_t ino; + + BOOL is_directory; + BOOL is_readonly; + + BOOL has_size; + UINT64 size; + + BOOL has_last_write_time; + UINT64 last_write_time_unix; + + BOOL has_clip_data_id; + UINT32 clip_data_id; +}; + +typedef struct +{ + CliprdrFileContext* file_context; + + CliprdrFuseFile* clip_data_dir; + + BOOL has_clip_data_id; + UINT32 clip_data_id; +} CliprdrFuseClipDataEntry; + +typedef struct +{ + CliprdrFileContext* file_context; + + wArrayList* fuse_files; + + BOOL all_files; + BOOL has_clip_data_id; + UINT32 clip_data_id; +} FuseFileClearContext; + +typedef struct +{ + FuseLowlevelOperationType operation_type; + CliprdrFuseFile* fuse_file; + fuse_req_t fuse_req; + UINT32 stream_id; +} CliprdrFuseRequest; + +typedef struct +{ + CliprdrFuseFile* parent; + char* parent_path; +} CliprdrFuseFindParentContext; +#endif + +typedef struct +{ + char* name; + FILE* fp; + INT64 size; + CliprdrFileContext* context; +} CliprdrLocalFile; + +typedef struct +{ + UINT32 lockId; + BOOL locked; + size_t count; + CliprdrLocalFile* files; + CliprdrFileContext* context; +} CliprdrLocalStream; + +struct cliprdr_file_context +{ +#if defined(WITH_FUSE) + /* FUSE related**/ + HANDLE fuse_start_sync; + HANDLE fuse_stop_sync; + HANDLE fuse_thread; + struct fuse_session* fuse_sess; +#if FUSE_USE_VERSION < 30 + struct fuse_chan* ch; +#endif + + wHashTable* inode_table; + wHashTable* clip_data_table; + wHashTable* request_table; + + CliprdrFuseFile* root_dir; + CliprdrFuseClipDataEntry* clip_data_entry_without_id; + UINT32 current_clip_data_id; + + fuse_ino_t next_ino; + UINT32 next_clip_data_id; + UINT32 next_stream_id; +#endif + + /* File clipping */ + BOOL file_formats_registered; + UINT32 file_capability_flags; + + UINT32 local_lock_id; + + wHashTable* local_streams; + wLog* log; + void* clipboard; + CliprdrClientContext* context; + char* path; + char* exposed_path; + BYTE server_data_hash[WINPR_SHA256_DIGEST_LENGTH]; + BYTE client_data_hash[WINPR_SHA256_DIGEST_LENGTH]; +}; + +#if defined(WITH_FUSE) +static void fuse_file_free(void* data) +{ + CliprdrFuseFile* fuse_file = data; + + if (!fuse_file) + return; + + ArrayList_Free(fuse_file->children); + free(fuse_file->filename_with_root); + + free(fuse_file); +} + +static CliprdrFuseFile* fuse_file_new(void) +{ + CliprdrFuseFile* fuse_file = NULL; + + fuse_file = calloc(1, sizeof(CliprdrFuseFile)); + if (!fuse_file) + return NULL; + + fuse_file->children = ArrayList_New(FALSE); + if (!fuse_file->children) + { + free(fuse_file); + return NULL; + } + + return fuse_file; +} + +static void clip_data_entry_free(void* data) +{ + CliprdrFuseClipDataEntry* clip_data_entry = data; + + if (!clip_data_entry) + return; + + if (clip_data_entry->has_clip_data_id) + { + CliprdrFileContext* file_context = clip_data_entry->file_context; + CLIPRDR_UNLOCK_CLIPBOARD_DATA unlock_clipboard_data = { 0 }; + + WINPR_ASSERT(file_context); + + unlock_clipboard_data.common.msgType = CB_UNLOCK_CLIPDATA; + unlock_clipboard_data.clipDataId = clip_data_entry->clip_data_id; + + file_context->context->ClientUnlockClipboardData(file_context->context, + &unlock_clipboard_data); + clip_data_entry->has_clip_data_id = FALSE; + + WLog_Print(file_context->log, WLOG_DEBUG, "Destroyed ClipDataEntry with id %u", + clip_data_entry->clip_data_id); + } + + free(clip_data_entry); +} + +static BOOL does_server_support_clipdata_locking(CliprdrFileContext* file_context) +{ + WINPR_ASSERT(file_context); + + if (cliprdr_file_context_remote_get_flags(file_context) & CB_CAN_LOCK_CLIPDATA) + return TRUE; + + return FALSE; +} + +static UINT32 get_next_free_clip_data_id(CliprdrFileContext* file_context) +{ + UINT32 clip_data_id = 0; + + WINPR_ASSERT(file_context); + + HashTable_Lock(file_context->inode_table); + clip_data_id = file_context->next_clip_data_id; + while (clip_data_id == 0 || + HashTable_GetItemValue(file_context->clip_data_table, (void*)(UINT_PTR)clip_data_id)) + ++clip_data_id; + + file_context->next_clip_data_id = clip_data_id + 1; + HashTable_Unlock(file_context->inode_table); + + return clip_data_id; +} + +static CliprdrFuseClipDataEntry* clip_data_entry_new(CliprdrFileContext* file_context, + BOOL needs_clip_data_id) +{ + CliprdrFuseClipDataEntry* clip_data_entry = NULL; + CLIPRDR_LOCK_CLIPBOARD_DATA lock_clipboard_data = { 0 }; + + WINPR_ASSERT(file_context); + + clip_data_entry = calloc(1, sizeof(CliprdrFuseClipDataEntry)); + if (!clip_data_entry) + return NULL; + + clip_data_entry->file_context = file_context; + clip_data_entry->clip_data_id = get_next_free_clip_data_id(file_context); + + if (!needs_clip_data_id) + return clip_data_entry; + + lock_clipboard_data.common.msgType = CB_LOCK_CLIPDATA; + lock_clipboard_data.clipDataId = clip_data_entry->clip_data_id; + + if (file_context->context->ClientLockClipboardData(file_context->context, &lock_clipboard_data)) + { + HashTable_Lock(file_context->inode_table); + clip_data_entry_free(clip_data_entry); + HashTable_Unlock(file_context->inode_table); + return NULL; + } + clip_data_entry->has_clip_data_id = TRUE; + + WLog_Print(file_context->log, WLOG_DEBUG, "Created ClipDataEntry with id %u", + clip_data_entry->clip_data_id); + + return clip_data_entry; +} + +static BOOL should_remove_fuse_file(CliprdrFuseFile* fuse_file, BOOL all_files, + BOOL has_clip_data_id, BOOL clip_data_id) +{ + if (all_files) + return TRUE; + + if (fuse_file->ino == FUSE_ROOT_ID) + return FALSE; + if (!fuse_file->has_clip_data_id && !has_clip_data_id) + return TRUE; + if (fuse_file->has_clip_data_id && has_clip_data_id && fuse_file->clip_data_id == clip_data_id) + return TRUE; + + return FALSE; +} + +static BOOL maybe_clear_fuse_request(const void* key, void* value, void* arg) +{ + CliprdrFuseRequest* fuse_request = value; + FuseFileClearContext* clear_context = arg; + CliprdrFileContext* file_context = clear_context->file_context; + CliprdrFuseFile* fuse_file = fuse_request->fuse_file; + + WINPR_ASSERT(file_context); + + if (!should_remove_fuse_file(fuse_file, clear_context->all_files, + clear_context->has_clip_data_id, clear_context->clip_data_id)) + return TRUE; + + DEBUG_CLIPRDR(file_context->log, "Clearing FileContentsRequest for file \"%s\"", + fuse_file->filename_with_root); + + fuse_reply_err(fuse_request->fuse_req, EIO); + HashTable_Remove(file_context->request_table, key); + free(fuse_request); + + return TRUE; +} + +static BOOL maybe_steal_inode(const void* key, void* value, void* arg) +{ + CliprdrFuseFile* fuse_file = value; + FuseFileClearContext* clear_context = arg; + CliprdrFileContext* file_context = clear_context->file_context; + + WINPR_ASSERT(file_context); + + if (should_remove_fuse_file(fuse_file, clear_context->all_files, + clear_context->has_clip_data_id, clear_context->clip_data_id)) + { + if (!ArrayList_Append(clear_context->fuse_files, fuse_file)) + WLog_Print(file_context->log, WLOG_ERROR, + "Failed to append FUSE file to list for deletion"); + + HashTable_Remove(file_context->inode_table, key); + } + + return TRUE; +} + +static BOOL notify_delete_child(void* data, size_t index, va_list ap) +{ + CliprdrFuseFile* child = data; + + WINPR_ASSERT(child); + + CliprdrFileContext* file_context = va_arg(ap, CliprdrFileContext*); + CliprdrFuseFile* parent = va_arg(ap, CliprdrFuseFile*); + + WINPR_ASSERT(file_context); + WINPR_ASSERT(parent); + + WINPR_ASSERT(file_context->fuse_sess); + fuse_lowlevel_notify_delete(file_context->fuse_sess, parent->ino, child->ino, child->filename, + strlen(child->filename)); + + return TRUE; +} + +static BOOL invalidate_inode(void* data, size_t index, va_list ap) +{ + CliprdrFuseFile* fuse_file = data; + + WINPR_ASSERT(fuse_file); + + CliprdrFileContext* file_context = va_arg(ap, CliprdrFileContext*); + WINPR_ASSERT(file_context); + WINPR_ASSERT(file_context->fuse_sess); + + ArrayList_ForEach(fuse_file->children, notify_delete_child, file_context, fuse_file); + + DEBUG_CLIPRDR(file_context->log, "Invalidating inode %lu for file \"%s\"", fuse_file->ino, + fuse_file->filename); + fuse_lowlevel_notify_inval_inode(file_context->fuse_sess, fuse_file->ino, 0, 0); + WLog_Print(file_context->log, WLOG_DEBUG, "Inode %lu invalidated", fuse_file->ino); + + return TRUE; +} + +static void clear_selection(CliprdrFileContext* file_context, BOOL all_selections, + CliprdrFuseClipDataEntry* clip_data_entry) +{ + FuseFileClearContext clear_context = { 0 }; + CliprdrFuseFile* root_dir = NULL; + CliprdrFuseFile* clip_data_dir = NULL; + + WINPR_ASSERT(file_context); + + root_dir = file_context->root_dir; + WINPR_ASSERT(root_dir); + + clear_context.file_context = file_context; + clear_context.fuse_files = ArrayList_New(FALSE); + WINPR_ASSERT(clear_context.fuse_files); + + wObject* aobj = ArrayList_Object(clear_context.fuse_files); + WINPR_ASSERT(aobj); + aobj->fnObjectFree = fuse_file_free; + + if (clip_data_entry) + { + clip_data_dir = clip_data_entry->clip_data_dir; + clip_data_entry->clip_data_dir = NULL; + + WINPR_ASSERT(clip_data_dir); + + ArrayList_Remove(root_dir->children, clip_data_dir); + + clear_context.has_clip_data_id = clip_data_dir->has_clip_data_id; + clear_context.clip_data_id = clip_data_dir->clip_data_id; + } + clear_context.all_files = all_selections; + + if (clip_data_entry && clip_data_entry->has_clip_data_id) + WLog_Print(file_context->log, WLOG_DEBUG, "Clearing selection for clipDataId %u", + clip_data_entry->clip_data_id); + else + WLog_Print(file_context->log, WLOG_DEBUG, "Clearing selection%s", + all_selections ? "s" : ""); + + HashTable_Foreach(file_context->request_table, maybe_clear_fuse_request, &clear_context); + HashTable_Foreach(file_context->inode_table, maybe_steal_inode, &clear_context); + HashTable_Unlock(file_context->inode_table); + + if (file_context->fuse_sess) + { + /* + * fuse_lowlevel_notify_inval_inode() is a blocking operation. If we receive a + * FUSE request (e.g. read()), then FUSE would block in read(), since the + * mutex of the inode_table would still be locked, if we wouldn't unlock it + * here. + * So, to avoid a deadlock here, unlock the mutex and reply all incoming + * operations with -ENOENT until the invalidation process is complete. + */ + ArrayList_ForEach(clear_context.fuse_files, invalidate_inode, file_context); + if (clip_data_dir) + { + fuse_lowlevel_notify_delete(file_context->fuse_sess, file_context->root_dir->ino, + clip_data_dir->ino, clip_data_dir->filename, + strlen(clip_data_dir->filename)); + } + } + ArrayList_Free(clear_context.fuse_files); + + HashTable_Lock(file_context->inode_table); + if (clip_data_entry && clip_data_entry->has_clip_data_id) + WLog_Print(file_context->log, WLOG_DEBUG, "Selection cleared for clipDataId %u", + clip_data_entry->clip_data_id); + else + WLog_Print(file_context->log, WLOG_DEBUG, "Selection%s cleared", all_selections ? "s" : ""); +} + +static void clear_entry_selection(CliprdrFuseClipDataEntry* clip_data_entry) +{ + WINPR_ASSERT(clip_data_entry); + + if (!clip_data_entry->clip_data_dir) + return; + + clear_selection(clip_data_entry->file_context, FALSE, clip_data_entry); +} + +static void clear_no_cdi_entry(CliprdrFileContext* file_context) +{ + WINPR_ASSERT(file_context); + + if (!file_context->clip_data_entry_without_id) + return; + + WINPR_ASSERT(file_context->inode_table); + + HashTable_Lock(file_context->inode_table); + clear_entry_selection(file_context->clip_data_entry_without_id); + + clip_data_entry_free(file_context->clip_data_entry_without_id); + file_context->clip_data_entry_without_id = NULL; + HashTable_Unlock(file_context->inode_table); +} + +static BOOL clear_clip_data_entries(const void* key, void* value, void* arg) +{ + clear_entry_selection(value); + + return TRUE; +} + +static void clear_cdi_entries(CliprdrFileContext* file_context) +{ + WINPR_ASSERT(file_context); + + HashTable_Lock(file_context->inode_table); + HashTable_Foreach(file_context->clip_data_table, clear_clip_data_entries, NULL); + + HashTable_Clear(file_context->clip_data_table); + HashTable_Unlock(file_context->inode_table); +} + +static UINT prepare_clip_data_entry_with_id(CliprdrFileContext* file_context) +{ + CliprdrFuseClipDataEntry* clip_data_entry = NULL; + + WINPR_ASSERT(file_context); + + clip_data_entry = clip_data_entry_new(file_context, TRUE); + if (!clip_data_entry) + { + WLog_Print(file_context->log, WLOG_ERROR, "Failed to create clipDataEntry"); + return ERROR_INTERNAL_ERROR; + } + + HashTable_Lock(file_context->inode_table); + if (!HashTable_Insert(file_context->clip_data_table, + (void*)(UINT_PTR)clip_data_entry->clip_data_id, clip_data_entry)) + { + WLog_Print(file_context->log, WLOG_ERROR, "Failed to insert clipDataEntry"); + clip_data_entry_free(clip_data_entry); + return ERROR_INTERNAL_ERROR; + } + HashTable_Unlock(file_context->inode_table); + + // HashTable_Insert owns clip_data_entry + // NOLINTNEXTLINE(clang-analyzer-unix.Malloc) + file_context->current_clip_data_id = clip_data_entry->clip_data_id; + + return CHANNEL_RC_OK; +} + +static UINT prepare_clip_data_entry_without_id(CliprdrFileContext* file_context) +{ + WINPR_ASSERT(file_context); + WINPR_ASSERT(!file_context->clip_data_entry_without_id); + + file_context->clip_data_entry_without_id = clip_data_entry_new(file_context, FALSE); + if (!file_context->clip_data_entry_without_id) + { + WLog_Print(file_context->log, WLOG_ERROR, "Failed to create clipDataEntry"); + return ERROR_INTERNAL_ERROR; + } + + return CHANNEL_RC_OK; +} +#endif + +UINT cliprdr_file_context_notify_new_server_format_list(CliprdrFileContext* file_context) +{ + WINPR_ASSERT(file_context); + WINPR_ASSERT(file_context->context); + +#if defined(WITH_FUSE) + clear_no_cdi_entry(file_context); + /* TODO: assign timeouts to old locks instead */ + clear_cdi_entries(file_context); + + if (does_server_support_clipdata_locking(file_context)) + return prepare_clip_data_entry_with_id(file_context); + else + return prepare_clip_data_entry_without_id(file_context); +#else + return CHANNEL_RC_OK; +#endif +} + +UINT cliprdr_file_context_notify_new_client_format_list(CliprdrFileContext* file_context) +{ + WINPR_ASSERT(file_context); + WINPR_ASSERT(file_context->context); + +#if defined(WITH_FUSE) + clear_no_cdi_entry(file_context); + /* TODO: assign timeouts to old locks instead */ + clear_cdi_entries(file_context); +#endif + + return CHANNEL_RC_OK; +} + +static CliprdrLocalStream* cliprdr_local_stream_new(CliprdrFileContext* context, UINT32 streamID, + const char* data, size_t size); +static void cliprdr_file_session_terminate(CliprdrFileContext* file, BOOL stop_thread); +static BOOL local_stream_discard(const void* key, void* value, void* arg); + +static void writelog(wLog* log, DWORD level, const char* fname, const char* fkt, size_t line, ...) +{ + if (!WLog_IsLevelActive(log, level)) + return; + + va_list ap; + va_start(ap, line); + WLog_PrintMessageVA(log, WLOG_MESSAGE_TEXT, level, line, fname, fkt, ap); + va_end(ap); +} + +#if defined(WITH_FUSE) +static void cliprdr_file_fuse_lookup(fuse_req_t req, fuse_ino_t parent, const char* name); +static void cliprdr_file_fuse_getattr(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi); +static void cliprdr_file_fuse_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, + struct fuse_file_info* fi); +static void cliprdr_file_fuse_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, + struct fuse_file_info* fi); +static void cliprdr_file_fuse_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi); +static void cliprdr_file_fuse_opendir(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi); + +static const struct fuse_lowlevel_ops cliprdr_file_fuse_oper = { + .lookup = cliprdr_file_fuse_lookup, + .getattr = cliprdr_file_fuse_getattr, + .readdir = cliprdr_file_fuse_readdir, + .open = cliprdr_file_fuse_open, + .read = cliprdr_file_fuse_read, + .opendir = cliprdr_file_fuse_opendir, +}; + +static CliprdrFuseFile* get_fuse_file_by_ino(CliprdrFileContext* file_context, fuse_ino_t fuse_ino) +{ + WINPR_ASSERT(file_context); + + return HashTable_GetItemValue(file_context->inode_table, (void*)(UINT_PTR)fuse_ino); +} + +static CliprdrFuseFile* get_fuse_file_by_name_from_parent(CliprdrFileContext* file_context, + CliprdrFuseFile* parent, const char* name) +{ + WINPR_ASSERT(file_context); + WINPR_ASSERT(parent); + + for (size_t i = 0; i < ArrayList_Count(parent->children); ++i) + { + CliprdrFuseFile* child = ArrayList_GetItem(parent->children, i); + + WINPR_ASSERT(child); + + if (strcmp(name, child->filename) == 0) + return child; + } + + DEBUG_CLIPRDR(file_context->log, "Requested file \"%s\" in directory \"%s\" does not exist", + name, parent->filename); + + return NULL; +} + +static CliprdrFuseRequest* cliprdr_fuse_request_new(CliprdrFileContext* file_context, + CliprdrFuseFile* fuse_file, fuse_req_t fuse_req, + FuseLowlevelOperationType operation_type) +{ + CliprdrFuseRequest* fuse_request = NULL; + UINT32 stream_id = file_context->next_stream_id; + + WINPR_ASSERT(file_context); + WINPR_ASSERT(fuse_file); + + fuse_request = calloc(1, sizeof(CliprdrFuseRequest)); + if (!fuse_request) + { + WLog_Print(file_context->log, WLOG_ERROR, "Failed to allocate FUSE request for file \"%s\"", + fuse_file->filename_with_root); + return NULL; + } + + fuse_request->fuse_file = fuse_file; + fuse_request->fuse_req = fuse_req; + fuse_request->operation_type = operation_type; + + while (stream_id == 0 || + HashTable_GetItemValue(file_context->request_table, (void*)(UINT_PTR)stream_id)) + ++stream_id; + fuse_request->stream_id = stream_id; + + file_context->next_stream_id = stream_id + 1; + + if (!HashTable_Insert(file_context->request_table, (void*)(UINT_PTR)fuse_request->stream_id, + fuse_request)) + { + WLog_Print(file_context->log, WLOG_ERROR, "Failed to track FUSE request for file \"%s\"", + fuse_file->filename_with_root); + free(fuse_request); + return NULL; + } + + return fuse_request; +} + +static BOOL request_file_size_async(CliprdrFileContext* file_context, CliprdrFuseFile* fuse_file, + fuse_req_t fuse_req, FuseLowlevelOperationType operation_type) +{ + CliprdrFuseRequest* fuse_request = NULL; + CLIPRDR_FILE_CONTENTS_REQUEST file_contents_request = { 0 }; + + WINPR_ASSERT(file_context); + WINPR_ASSERT(fuse_file); + + fuse_request = cliprdr_fuse_request_new(file_context, fuse_file, fuse_req, operation_type); + if (!fuse_request) + return FALSE; + + file_contents_request.common.msgType = CB_FILECONTENTS_REQUEST; + file_contents_request.streamId = fuse_request->stream_id; + file_contents_request.listIndex = fuse_file->list_idx; + file_contents_request.dwFlags = FILECONTENTS_SIZE; + file_contents_request.cbRequested = 0x8; + file_contents_request.haveClipDataId = fuse_file->has_clip_data_id; + file_contents_request.clipDataId = fuse_file->clip_data_id; + + if (file_context->context->ClientFileContentsRequest(file_context->context, + &file_contents_request)) + { + WLog_Print(file_context->log, WLOG_ERROR, + "Failed to send FileContentsRequest for file \"%s\"", + fuse_file->filename_with_root); + HashTable_Remove(file_context->request_table, (void*)(UINT_PTR)fuse_request->stream_id); + free(fuse_request); + return FALSE; + } + DEBUG_CLIPRDR(file_context->log, "Requested file size for file \"%s\" with stream id %u", + fuse_file->filename, fuse_request->stream_id); + + return TRUE; +} + +static void write_file_attributes(CliprdrFuseFile* fuse_file, struct stat* attr) +{ + memset(attr, 0, sizeof(struct stat)); + + if (!fuse_file) + return; + + attr->st_ino = fuse_file->ino; + if (fuse_file->is_directory) + { + attr->st_mode = S_IFDIR | (fuse_file->is_readonly ? 0555 : 0755); + attr->st_nlink = 2; + } + else + { + attr->st_mode = S_IFREG | (fuse_file->is_readonly ? 0444 : 0644); + attr->st_nlink = 1; + attr->st_size = fuse_file->size; + } + attr->st_uid = getuid(); + attr->st_gid = getgid(); + attr->st_atime = attr->st_mtime = attr->st_ctime = + (fuse_file->has_last_write_time ? fuse_file->last_write_time_unix : time(NULL)); +} + +static void cliprdr_file_fuse_lookup(fuse_req_t fuse_req, fuse_ino_t parent_ino, const char* name) +{ + CliprdrFileContext* file_context = fuse_req_userdata(fuse_req); + CliprdrFuseFile* parent = NULL; + CliprdrFuseFile* fuse_file = NULL; + struct fuse_entry_param entry = { 0 }; + + WINPR_ASSERT(file_context); + + HashTable_Lock(file_context->inode_table); + if (!(parent = get_fuse_file_by_ino(file_context, parent_ino)) || !parent->is_directory) + { + HashTable_Unlock(file_context->inode_table); + fuse_reply_err(fuse_req, ENOENT); + return; + } + if (!(fuse_file = get_fuse_file_by_name_from_parent(file_context, parent, name))) + { + HashTable_Unlock(file_context->inode_table); + fuse_reply_err(fuse_req, ENOENT); + return; + } + + DEBUG_CLIPRDR(file_context->log, "lookup() has been called for \"%s\"", name); + DEBUG_CLIPRDR(file_context->log, "Parent is \"%s\", child is \"%s\"", + parent->filename_with_root, fuse_file->filename_with_root); + + if (!fuse_file->is_directory && !fuse_file->has_size) + { + BOOL result = 0; + + result = + request_file_size_async(file_context, fuse_file, fuse_req, FUSE_LL_OPERATION_LOOKUP); + HashTable_Unlock(file_context->inode_table); + + if (!result) + fuse_reply_err(fuse_req, EIO); + + return; + } + + entry.ino = fuse_file->ino; + write_file_attributes(fuse_file, &entry.attr); + entry.attr_timeout = 1.0; + entry.entry_timeout = 1.0; + HashTable_Unlock(file_context->inode_table); + + fuse_reply_entry(fuse_req, &entry); +} + +static void cliprdr_file_fuse_getattr(fuse_req_t fuse_req, fuse_ino_t fuse_ino, + struct fuse_file_info* file_info) +{ + CliprdrFileContext* file_context = fuse_req_userdata(fuse_req); + CliprdrFuseFile* fuse_file = NULL; + struct stat attr = { 0 }; + + WINPR_ASSERT(file_context); + + HashTable_Lock(file_context->inode_table); + if (!(fuse_file = get_fuse_file_by_ino(file_context, fuse_ino))) + { + HashTable_Unlock(file_context->inode_table); + fuse_reply_err(fuse_req, ENOENT); + return; + } + + DEBUG_CLIPRDR(file_context->log, "getattr() has been called for file \"%s\"", + fuse_file->filename_with_root); + + if (!fuse_file->is_directory && !fuse_file->has_size) + { + BOOL result = 0; + + result = + request_file_size_async(file_context, fuse_file, fuse_req, FUSE_LL_OPERATION_GETATTR); + HashTable_Unlock(file_context->inode_table); + + if (!result) + fuse_reply_err(fuse_req, EIO); + + return; + } + + write_file_attributes(fuse_file, &attr); + HashTable_Unlock(file_context->inode_table); + + fuse_reply_attr(fuse_req, &attr, 1.0); +} + +static void cliprdr_file_fuse_open(fuse_req_t fuse_req, fuse_ino_t fuse_ino, + struct fuse_file_info* file_info) +{ + CliprdrFileContext* file_context = fuse_req_userdata(fuse_req); + CliprdrFuseFile* fuse_file = NULL; + + WINPR_ASSERT(file_context); + + HashTable_Lock(file_context->inode_table); + if (!(fuse_file = get_fuse_file_by_ino(file_context, fuse_ino))) + { + HashTable_Unlock(file_context->inode_table); + fuse_reply_err(fuse_req, ENOENT); + return; + } + if (fuse_file->is_directory) + { + HashTable_Unlock(file_context->inode_table); + fuse_reply_err(fuse_req, EISDIR); + return; + } + HashTable_Unlock(file_context->inode_table); + + if ((file_info->flags & O_ACCMODE) != O_RDONLY) + { + fuse_reply_err(fuse_req, EACCES); + return; + } + + /* Important for KDE to get file correctly */ + file_info->direct_io = 1; + + fuse_reply_open(fuse_req, file_info); +} + +static BOOL request_file_range_async(CliprdrFileContext* file_context, CliprdrFuseFile* fuse_file, + fuse_req_t fuse_req, off_t offset, size_t requested_size) +{ + CliprdrFuseRequest* fuse_request = NULL; + CLIPRDR_FILE_CONTENTS_REQUEST file_contents_request = { 0 }; + + WINPR_ASSERT(file_context); + WINPR_ASSERT(fuse_file); + + fuse_request = + cliprdr_fuse_request_new(file_context, fuse_file, fuse_req, FUSE_LL_OPERATION_READ); + if (!fuse_request) + return FALSE; + + file_contents_request.common.msgType = CB_FILECONTENTS_REQUEST; + file_contents_request.streamId = fuse_request->stream_id; + file_contents_request.listIndex = fuse_file->list_idx; + file_contents_request.dwFlags = FILECONTENTS_RANGE; + file_contents_request.nPositionLow = offset & 0xFFFFFFFF; + file_contents_request.nPositionHigh = offset >> 32 & 0xFFFFFFFF; + file_contents_request.cbRequested = requested_size; + file_contents_request.haveClipDataId = fuse_file->has_clip_data_id; + file_contents_request.clipDataId = fuse_file->clip_data_id; + + if (file_context->context->ClientFileContentsRequest(file_context->context, + &file_contents_request)) + { + WLog_Print(file_context->log, WLOG_ERROR, + "Failed to send FileContentsRequest for file \"%s\"", + fuse_file->filename_with_root); + HashTable_Remove(file_context->request_table, (void*)(UINT_PTR)fuse_request->stream_id); + return FALSE; + } + + // file_context->request_table owns fuse_request + // NOLINTBEGIN(clang-analyzer-unix.Malloc) + DEBUG_CLIPRDR( + file_context->log, + "Requested file range (%zu Bytes at offset %lu) for file \"%s\" with stream id %u", + requested_size, offset, fuse_file->filename, fuse_request->stream_id); + + return TRUE; + // NOLINTEND(clang-analyzer-unix.Malloc) +} + +static void cliprdr_file_fuse_read(fuse_req_t fuse_req, fuse_ino_t fuse_ino, size_t size, + off_t offset, struct fuse_file_info* file_info) +{ + CliprdrFileContext* file_context = fuse_req_userdata(fuse_req); + CliprdrFuseFile* fuse_file = NULL; + BOOL result = 0; + + WINPR_ASSERT(file_context); + + HashTable_Lock(file_context->inode_table); + if (!(fuse_file = get_fuse_file_by_ino(file_context, fuse_ino))) + { + HashTable_Unlock(file_context->inode_table); + fuse_reply_err(fuse_req, ENOENT); + return; + } + if (fuse_file->is_directory) + { + HashTable_Unlock(file_context->inode_table); + fuse_reply_err(fuse_req, EISDIR); + return; + } + if (!fuse_file->has_size || offset > fuse_file->size) + { + HashTable_Unlock(file_context->inode_table); + fuse_reply_err(fuse_req, EINVAL); + return; + } + + size = MIN(size, 8 * 1024 * 1024); + + result = request_file_range_async(file_context, fuse_file, fuse_req, offset, size); + HashTable_Unlock(file_context->inode_table); + + if (!result) + fuse_reply_err(fuse_req, EIO); +} + +static void cliprdr_file_fuse_opendir(fuse_req_t fuse_req, fuse_ino_t fuse_ino, + struct fuse_file_info* file_info) +{ + CliprdrFileContext* file_context = fuse_req_userdata(fuse_req); + CliprdrFuseFile* fuse_file = NULL; + + WINPR_ASSERT(file_context); + + HashTable_Lock(file_context->inode_table); + if (!(fuse_file = get_fuse_file_by_ino(file_context, fuse_ino))) + { + HashTable_Unlock(file_context->inode_table); + fuse_reply_err(fuse_req, ENOENT); + return; + } + if (!fuse_file->is_directory) + { + HashTable_Unlock(file_context->inode_table); + fuse_reply_err(fuse_req, ENOTDIR); + return; + } + HashTable_Unlock(file_context->inode_table); + + if ((file_info->flags & O_ACCMODE) != O_RDONLY) + { + fuse_reply_err(fuse_req, EACCES); + return; + } + + fuse_reply_open(fuse_req, file_info); +} + +static void cliprdr_file_fuse_readdir(fuse_req_t fuse_req, fuse_ino_t fuse_ino, size_t max_size, + off_t offset, struct fuse_file_info* file_info) +{ + CliprdrFileContext* file_context = fuse_req_userdata(fuse_req); + CliprdrFuseFile* fuse_file = NULL; + CliprdrFuseFile* child = NULL; + struct stat attr = { 0 }; + size_t written_size = 0; + size_t entry_size = 0; + char* filename = NULL; + char* buf = NULL; + + WINPR_ASSERT(file_context); + + HashTable_Lock(file_context->inode_table); + if (!(fuse_file = get_fuse_file_by_ino(file_context, fuse_ino))) + { + HashTable_Unlock(file_context->inode_table); + fuse_reply_err(fuse_req, ENOENT); + return; + } + if (!fuse_file->is_directory) + { + HashTable_Unlock(file_context->inode_table); + fuse_reply_err(fuse_req, ENOTDIR); + return; + } + + DEBUG_CLIPRDR(file_context->log, "Reading directory \"%s\" at offset %lu", + fuse_file->filename_with_root, offset); + + if (offset >= ArrayList_Count(fuse_file->children)) + { + HashTable_Unlock(file_context->inode_table); + fuse_reply_buf(fuse_req, NULL, 0); + return; + } + + buf = calloc(max_size, sizeof(char)); + if (!buf) + { + HashTable_Unlock(file_context->inode_table); + fuse_reply_err(fuse_req, ENOMEM); + return; + } + written_size = 0; + + for (off_t i = offset; i < 2; ++i) + { + if (i == 0) + { + write_file_attributes(fuse_file, &attr); + filename = "."; + } + else if (i == 1) + { + write_file_attributes(fuse_file->parent, &attr); + attr.st_ino = fuse_file->parent ? attr.st_ino : FUSE_ROOT_ID; + attr.st_mode = fuse_file->parent ? attr.st_mode : 0555; + filename = ".."; + } + else + { + WINPR_ASSERT(FALSE); + } + + /** + * buf needs to be large enough to hold the entry. If it's not, then the + * entry is not filled in but the size of the entry is still returned. + */ + entry_size = fuse_add_direntry(fuse_req, buf + written_size, max_size - written_size, + filename, &attr, i + 1); + if (entry_size > max_size - written_size) + break; + + written_size += entry_size; + } + + for (size_t j = 0, i = 2; j < ArrayList_Count(fuse_file->children); ++j, ++i) + { + if (i < offset) + continue; + + child = ArrayList_GetItem(fuse_file->children, j); + + write_file_attributes(child, &attr); + entry_size = fuse_add_direntry(fuse_req, buf + written_size, max_size - written_size, + child->filename, &attr, i + 1); + if (entry_size > max_size - written_size) + break; + + written_size += entry_size; + } + HashTable_Unlock(file_context->inode_table); + + fuse_reply_buf(fuse_req, buf, written_size); + free(buf); +} + +static void fuse_abort(int sig, const char* signame, void* context) +{ + CliprdrFileContext* file = (CliprdrFileContext*)context; + + if (file) + { + WLog_Print(file->log, WLOG_INFO, "signal %s [%d] aborting session", signame, sig); + cliprdr_file_session_terminate(file, FALSE); + } +} + +static DWORD WINAPI cliprdr_file_fuse_thread(LPVOID arg) +{ + CliprdrFileContext* file = (CliprdrFileContext*)arg; + + WINPR_ASSERT(file); + + DEBUG_CLIPRDR(file->log, "Starting fuse with mountpoint '%s'", file->path); + + struct fuse_args args = FUSE_ARGS_INIT(0, NULL); + fuse_opt_add_arg(&args, file->path); + file->fuse_sess = fuse_session_new(&args, &cliprdr_file_fuse_oper, + sizeof(cliprdr_file_fuse_oper), (void*)file); + SetEvent(file->fuse_start_sync); + + if (file->fuse_sess != NULL) + { + freerdp_add_signal_cleanup_handler(file, fuse_abort); + if (0 == fuse_session_mount(file->fuse_sess, file->path)) + { + fuse_session_loop(file->fuse_sess); + fuse_session_unmount(file->fuse_sess); + } + freerdp_del_signal_cleanup_handler(file, fuse_abort); + + WLog_Print(file->log, WLOG_DEBUG, "Waiting for FUSE stop sync"); + if (WaitForSingleObject(file->fuse_stop_sync, INFINITE) == WAIT_FAILED) + WLog_Print(file->log, WLOG_ERROR, "Failed to wait for stop sync"); + fuse_session_destroy(file->fuse_sess); + } + fuse_opt_free_args(&args); + + DEBUG_CLIPRDR(file->log, "Quitting fuse with mountpoint '%s'", file->path); + + ExitThread(0); + return 0; +} + +static UINT cliprdr_file_context_server_file_contents_response( + CliprdrClientContext* cliprdr_context, + const CLIPRDR_FILE_CONTENTS_RESPONSE* file_contents_response) +{ + CliprdrFileContext* file_context = NULL; + CliprdrFuseRequest* fuse_request = NULL; + struct fuse_entry_param entry = { 0 }; + + WINPR_ASSERT(cliprdr_context); + WINPR_ASSERT(file_contents_response); + + file_context = cliprdr_context->custom; + WINPR_ASSERT(file_context); + + HashTable_Lock(file_context->inode_table); + fuse_request = HashTable_GetItemValue(file_context->request_table, + (void*)(UINT_PTR)file_contents_response->streamId); + if (!fuse_request) + { + HashTable_Unlock(file_context->inode_table); + return CHANNEL_RC_OK; + } + HashTable_Remove(file_context->request_table, + (void*)(UINT_PTR)file_contents_response->streamId); + + if (!(file_contents_response->common.msgFlags & CB_RESPONSE_OK)) + { + WLog_Print(file_context->log, WLOG_WARN, + "FileContentsRequests for file \"%s\" was unsuccessful", + fuse_request->fuse_file->filename); + HashTable_Unlock(file_context->inode_table); + + fuse_reply_err(fuse_request->fuse_req, EIO); + free(fuse_request); + return CHANNEL_RC_OK; + } + + if ((fuse_request->operation_type == FUSE_LL_OPERATION_LOOKUP || + fuse_request->operation_type == FUSE_LL_OPERATION_GETATTR) && + file_contents_response->cbRequested != sizeof(UINT64)) + { + WLog_Print(file_context->log, WLOG_WARN, + "Received invalid file size for file \"%s\" from the client", + fuse_request->fuse_file->filename); + HashTable_Unlock(file_context->inode_table); + + fuse_reply_err(fuse_request->fuse_req, EIO); + free(fuse_request); + return CHANNEL_RC_OK; + } + + if (fuse_request->operation_type == FUSE_LL_OPERATION_LOOKUP || + fuse_request->operation_type == FUSE_LL_OPERATION_GETATTR) + { + DEBUG_CLIPRDR(file_context->log, "Received file size for file \"%s\" with stream id %u", + fuse_request->fuse_file->filename, file_contents_response->streamId); + + fuse_request->fuse_file->size = *((const UINT64*)file_contents_response->requestedData); + fuse_request->fuse_file->has_size = TRUE; + + entry.ino = fuse_request->fuse_file->ino; + write_file_attributes(fuse_request->fuse_file, &entry.attr); + entry.attr_timeout = 1.0; + entry.entry_timeout = 1.0; + } + else if (fuse_request->operation_type == FUSE_LL_OPERATION_READ) + { + DEBUG_CLIPRDR(file_context->log, "Received file range for file \"%s\" with stream id %u", + fuse_request->fuse_file->filename, file_contents_response->streamId); + } + HashTable_Unlock(file_context->inode_table); + + switch (fuse_request->operation_type) + { + case FUSE_LL_OPERATION_NONE: + break; + case FUSE_LL_OPERATION_LOOKUP: + fuse_reply_entry(fuse_request->fuse_req, &entry); + break; + case FUSE_LL_OPERATION_GETATTR: + fuse_reply_attr(fuse_request->fuse_req, &entry.attr, entry.attr_timeout); + break; + case FUSE_LL_OPERATION_READ: + fuse_reply_buf(fuse_request->fuse_req, + (const char*)file_contents_response->requestedData, + file_contents_response->cbRequested); + break; + } + + free(fuse_request); + + return CHANNEL_RC_OK; +} +#endif + +static UINT cliprdr_file_context_send_file_contents_failure( + CliprdrFileContext* file, const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response = { 0 }; + + WINPR_ASSERT(file); + WINPR_ASSERT(fileContentsRequest); + + const UINT64 offset = (((UINT64)fileContentsRequest->nPositionHigh) << 32) | + ((UINT64)fileContentsRequest->nPositionLow); + writelog(file->log, WLOG_WARN, __FILE__, __func__, __LINE__, + "server file contents request [lockID %" PRIu32 ", streamID %" PRIu32 + ", index %" PRIu32 "] offset %" PRIu64 ", size %" PRIu32 " failed", + fileContentsRequest->clipDataId, fileContentsRequest->streamId, + fileContentsRequest->listIndex, offset, fileContentsRequest->cbRequested); + + response.common.msgFlags = CB_RESPONSE_FAIL; + response.streamId = fileContentsRequest->streamId; + + WINPR_ASSERT(file->context); + WINPR_ASSERT(file->context->ClientFileContentsResponse); + return file->context->ClientFileContentsResponse(file->context, &response); +} + +static UINT +cliprdr_file_context_send_contents_response(CliprdrFileContext* file, + const CLIPRDR_FILE_CONTENTS_REQUEST* request, + const void* data, size_t size) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response = { .streamId = request->streamId, + .requestedData = data, + .cbRequested = size, + .common.msgFlags = CB_RESPONSE_OK }; + + WINPR_ASSERT(request); + WINPR_ASSERT(file); + + WLog_Print(file->log, WLOG_DEBUG, "send contents response streamID=%" PRIu32 ", size=%" PRIu32, + response.streamId, response.cbRequested); + WINPR_ASSERT(file->context); + WINPR_ASSERT(file->context->ClientFileContentsResponse); + return file->context->ClientFileContentsResponse(file->context, &response); +} + +static BOOL dump_streams(const void* key, void* value, void* arg) +{ + const UINT32* ukey = key; + CliprdrLocalStream* cur = value; + + writelog(cur->context->log, WLOG_WARN, __FILE__, __func__, __LINE__, + "[key %" PRIu32 "] lockID %" PRIu32 ", count %" PRIuz ", locked %d", *ukey, + cur->lockId, cur->count, cur->locked); + for (size_t x = 0; x < cur->count; x++) + { + const CliprdrLocalFile* file = &cur->files[x]; + writelog(cur->context->log, WLOG_WARN, __FILE__, __func__, __LINE__, "file [%" PRIuz "] ", + x, file->name, file->size); + } + return TRUE; +} + +static CliprdrLocalFile* file_info_for_request(CliprdrFileContext* file, UINT32 lockId, + UINT32 listIndex) +{ + WINPR_ASSERT(file); + + CliprdrLocalStream* cur = HashTable_GetItemValue(file->local_streams, &lockId); + if (cur) + { + if (listIndex < cur->count) + { + CliprdrLocalFile* f = &cur->files[listIndex]; + return f; + } + else + { + writelog(file->log, WLOG_WARN, __FILE__, __func__, __LINE__, + "invalid entry index for lockID %" PRIu32 ", index %" PRIu32 " [count %" PRIu32 + "] [locked %d]", + lockId, listIndex, cur->count, cur->locked); + } + } + else + { + writelog(file->log, WLOG_WARN, __FILE__, __func__, __LINE__, + "missing entry for lockID %" PRIu32 ", index %" PRIu32, lockId, listIndex); + HashTable_Foreach(file->local_streams, dump_streams, file); + } + + return NULL; +} + +static CliprdrLocalFile* file_for_request(CliprdrFileContext* file, UINT32 lockId, UINT32 listIndex) +{ + CliprdrLocalFile* f = file_info_for_request(file, lockId, listIndex); + if (f) + { + if (!f->fp) + { + const char* name = f->name; + f->fp = winpr_fopen(name, "rb"); + } + if (!f->fp) + { + char ebuffer[256] = { 0 }; + writelog(file->log, WLOG_WARN, __FILE__, __func__, __LINE__, + "[lockID %" PRIu32 ", index %" PRIu32 + "] failed to open file '%s' [size %" PRId64 "] %s [%d]", + lockId, listIndex, f->name, f->size, + winpr_strerror(errno, ebuffer, sizeof(ebuffer)), errno); + return NULL; + } + } + + return f; +} + +static void cliprdr_local_file_try_close(CliprdrLocalFile* file, UINT res, UINT64 offset, + UINT64 size) +{ + WINPR_ASSERT(file); + + if (res != 0) + { + WINPR_ASSERT(file->context); + WLog_Print(file->context->log, WLOG_DEBUG, "closing file %s after error %" PRIu32, + file->name, res); + } + else if (((file->size > 0) && (offset + size >= (UINT64)file->size))) + { + WINPR_ASSERT(file->context); + WLog_Print(file->context->log, WLOG_DEBUG, "closing file %s after read", file->name); + } + else + { + // TODO: we need to keep track of open files to avoid running out of file descriptors + // TODO: for the time being just close again. + } + if (file->fp) + fclose(file->fp); + file->fp = NULL; +} + +static UINT cliprdr_file_context_server_file_size_request( + CliprdrFileContext* file, const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest) +{ + WINPR_ASSERT(fileContentsRequest); + + if (fileContentsRequest->cbRequested != sizeof(UINT64)) + { + WLog_Print(file->log, WLOG_WARN, "unexpected FILECONTENTS_SIZE request: %" PRIu32 " bytes", + fileContentsRequest->cbRequested); + } + + HashTable_Lock(file->local_streams); + + UINT res = CHANNEL_RC_OK; + CliprdrLocalFile* rfile = + file_for_request(file, fileContentsRequest->clipDataId, fileContentsRequest->listIndex); + if (!rfile) + res = cliprdr_file_context_send_file_contents_failure(file, fileContentsRequest); + else + { + if (_fseeki64(rfile->fp, 0, SEEK_END) < 0) + res = cliprdr_file_context_send_file_contents_failure(file, fileContentsRequest); + else + { + const INT64 size = _ftelli64(rfile->fp); + rfile->size = size; + cliprdr_local_file_try_close(rfile, res, 0, 0); + + res = cliprdr_file_context_send_contents_response(file, fileContentsRequest, &size, + sizeof(size)); + } + } + + HashTable_Unlock(file->local_streams); + return res; +} + +static UINT cliprdr_file_context_server_file_range_request( + CliprdrFileContext* file, const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest) +{ + BYTE* data = NULL; + + WINPR_ASSERT(fileContentsRequest); + + HashTable_Lock(file->local_streams); + const UINT64 offset = (((UINT64)fileContentsRequest->nPositionHigh) << 32) | + ((UINT64)fileContentsRequest->nPositionLow); + + CliprdrLocalFile* rfile = + file_for_request(file, fileContentsRequest->clipDataId, fileContentsRequest->listIndex); + if (!rfile) + goto fail; + + if (_fseeki64(rfile->fp, offset, SEEK_SET) < 0) + goto fail; + + data = malloc(fileContentsRequest->cbRequested); + if (!data) + goto fail; + + const size_t r = fread(data, 1, fileContentsRequest->cbRequested, rfile->fp); + const UINT rc = cliprdr_file_context_send_contents_response(file, fileContentsRequest, data, r); + free(data); + + cliprdr_local_file_try_close(rfile, rc, offset, fileContentsRequest->cbRequested); + HashTable_Unlock(file->local_streams); + return rc; +fail: + if (rfile) + cliprdr_local_file_try_close(rfile, ERROR_INTERNAL_ERROR, offset, + fileContentsRequest->cbRequested); + free(data); + HashTable_Unlock(file->local_streams); + return cliprdr_file_context_send_file_contents_failure(file, fileContentsRequest); +} + +static void cliprdr_local_stream_free(void* obj); + +static UINT change_lock(CliprdrFileContext* file, UINT32 lockId, BOOL lock) +{ + UINT rc = CHANNEL_RC_OK; + + WINPR_ASSERT(file); + + HashTable_Lock(file->local_streams); + CliprdrLocalStream* stream = HashTable_GetItemValue(file->local_streams, &lockId); + if (lock && !stream) + { + stream = cliprdr_local_stream_new(file, lockId, NULL, 0); + if (!HashTable_Insert(file->local_streams, &lockId, stream)) + { + rc = ERROR_INTERNAL_ERROR; + cliprdr_local_stream_free(stream); + stream = NULL; + } + file->local_lock_id = lockId; + } + if (stream) + { + stream->locked = lock; + stream->lockId = lockId; + } + + if (!lock) + { + if (!HashTable_Foreach(file->local_streams, local_stream_discard, file)) + rc = ERROR_INTERNAL_ERROR; + } + HashTable_Unlock(file->local_streams); + return rc; +} + +static UINT cliprdr_file_context_lock(CliprdrClientContext* context, + const CLIPRDR_LOCK_CLIPBOARD_DATA* lockClipboardData) +{ + WINPR_ASSERT(context); + WINPR_ASSERT(lockClipboardData); + CliprdrFileContext* file = (context->custom); + return change_lock(file, lockClipboardData->clipDataId, TRUE); +} + +static UINT cliprdr_file_context_unlock(CliprdrClientContext* context, + const CLIPRDR_UNLOCK_CLIPBOARD_DATA* unlockClipboardData) +{ + WINPR_ASSERT(context); + WINPR_ASSERT(unlockClipboardData); + CliprdrFileContext* file = (context->custom); + return change_lock(file, unlockClipboardData->clipDataId, FALSE); +} + +static UINT cliprdr_file_context_server_file_contents_request( + CliprdrClientContext* context, const CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest) +{ + UINT error = NO_ERROR; + + WINPR_ASSERT(context); + WINPR_ASSERT(fileContentsRequest); + + CliprdrFileContext* file = (context->custom); + WINPR_ASSERT(file); + + /* + * MS-RDPECLIP 2.2.5.3 File Contents Request PDU (CLIPRDR_FILECONTENTS_REQUEST): + * The FILECONTENTS_SIZE and FILECONTENTS_RANGE flags MUST NOT be set at the same time. + */ + if ((fileContentsRequest->dwFlags & (FILECONTENTS_SIZE | FILECONTENTS_RANGE)) == + (FILECONTENTS_SIZE | FILECONTENTS_RANGE)) + { + WLog_Print(file->log, WLOG_ERROR, "invalid CLIPRDR_FILECONTENTS_REQUEST.dwFlags"); + return cliprdr_file_context_send_file_contents_failure(file, fileContentsRequest); + } + + if (fileContentsRequest->dwFlags & FILECONTENTS_SIZE) + error = cliprdr_file_context_server_file_size_request(file, fileContentsRequest); + + if (fileContentsRequest->dwFlags & FILECONTENTS_RANGE) + error = cliprdr_file_context_server_file_range_request(file, fileContentsRequest); + + if (error) + { + WLog_Print(file->log, WLOG_ERROR, "failed to handle CLIPRDR_FILECONTENTS_REQUEST: 0x%08X", + error); + return cliprdr_file_context_send_file_contents_failure(file, fileContentsRequest); + } + + return CHANNEL_RC_OK; +} + +BOOL cliprdr_file_context_init(CliprdrFileContext* file, CliprdrClientContext* cliprdr) +{ + WINPR_ASSERT(file); + WINPR_ASSERT(cliprdr); + + cliprdr->custom = file; + file->context = cliprdr; + + cliprdr->ServerLockClipboardData = cliprdr_file_context_lock; + cliprdr->ServerUnlockClipboardData = cliprdr_file_context_unlock; + cliprdr->ServerFileContentsRequest = cliprdr_file_context_server_file_contents_request; +#if defined(WITH_FUSE) + cliprdr->ServerFileContentsResponse = cliprdr_file_context_server_file_contents_response; +#endif + + return TRUE; +} + +#if defined(WITH_FUSE) +static void clear_all_selections(CliprdrFileContext* file_context) +{ + WINPR_ASSERT(file_context); + WINPR_ASSERT(file_context->inode_table); + + HashTable_Lock(file_context->inode_table); + clear_selection(file_context, TRUE, NULL); + + HashTable_Clear(file_context->clip_data_table); + HashTable_Unlock(file_context->inode_table); +} +#endif + +BOOL cliprdr_file_context_uninit(CliprdrFileContext* file, CliprdrClientContext* cliprdr) +{ + WINPR_ASSERT(file); + WINPR_ASSERT(cliprdr); + + // Clear all data before the channel is closed + // the cleanup handlers are dependent on a working channel. +#if defined(WITH_FUSE) + if (file->inode_table) + { + clear_no_cdi_entry(file); + clear_all_selections(file); + } +#endif + + HashTable_Clear(file->local_streams); + + file->context = NULL; +#if defined(WITH_FUSE) + cliprdr->ServerFileContentsResponse = NULL; +#endif + + return TRUE; +} + +static BOOL cliprdr_file_content_changed_and_update(void* ihash, size_t hsize, const void* data, + size_t size) +{ + + BYTE hash[WINPR_SHA256_DIGEST_LENGTH] = { 0 }; + + if (hsize < sizeof(hash)) + return FALSE; + + if (!winpr_Digest(WINPR_MD_SHA256, data, size, hash, sizeof(hash))) + return FALSE; + + const BOOL changed = memcmp(hash, ihash, sizeof(hash)) != 0; + if (changed) + memcpy(ihash, hash, sizeof(hash)); + return changed; +} + +static BOOL cliprdr_file_server_content_changed_and_update(CliprdrFileContext* file, + const void* data, size_t size) +{ + WINPR_ASSERT(file); + return cliprdr_file_content_changed_and_update(file->server_data_hash, + sizeof(file->server_data_hash), data, size); +} + +static BOOL cliprdr_file_client_content_changed_and_update(CliprdrFileContext* file, + const void* data, size_t size) +{ + WINPR_ASSERT(file); + return cliprdr_file_content_changed_and_update(file->client_data_hash, + sizeof(file->client_data_hash), data, size); +} + +#if defined(WITH_FUSE) +static fuse_ino_t get_next_free_inode(CliprdrFileContext* file_context) +{ + fuse_ino_t ino = 0; + + WINPR_ASSERT(file_context); + + ino = file_context->next_ino; + while (ino == 0 || ino == FUSE_ROOT_ID || + HashTable_GetItemValue(file_context->inode_table, (void*)(UINT_PTR)ino)) + ++ino; + + file_context->next_ino = ino + 1; + + return ino; +} + +static CliprdrFuseFile* clip_data_dir_new(CliprdrFileContext* file_context, BOOL has_clip_data_id, + UINT32 clip_data_id) +{ + CliprdrFuseFile* root_dir = NULL; + CliprdrFuseFile* clip_data_dir = NULL; + size_t path_length = 0; + + WINPR_ASSERT(file_context); + + clip_data_dir = fuse_file_new(); + if (!clip_data_dir) + return NULL; + + path_length = 1 + MAX_CLIP_DATA_DIR_LEN + 1; + + clip_data_dir->filename_with_root = calloc(path_length, sizeof(char)); + if (!clip_data_dir->filename_with_root) + { + WLog_Print(file_context->log, WLOG_ERROR, "Failed to allocate filename"); + fuse_file_free(clip_data_dir); + return NULL; + } + + if (has_clip_data_id) + _snprintf(clip_data_dir->filename_with_root, path_length, "/%u", (unsigned)clip_data_id); + else + _snprintf(clip_data_dir->filename_with_root, path_length, "/%" PRIu64, NO_CLIP_DATA_ID); + + clip_data_dir->filename = strrchr(clip_data_dir->filename_with_root, '/') + 1; + + clip_data_dir->ino = get_next_free_inode(file_context); + clip_data_dir->is_directory = TRUE; + clip_data_dir->is_readonly = TRUE; + clip_data_dir->has_clip_data_id = has_clip_data_id; + clip_data_dir->clip_data_id = clip_data_id; + + root_dir = file_context->root_dir; + if (!ArrayList_Append(root_dir->children, clip_data_dir)) + { + WLog_Print(file_context->log, WLOG_ERROR, "Failed to append FUSE file"); + fuse_file_free(clip_data_dir); + return NULL; + } + clip_data_dir->parent = root_dir; + + if (!HashTable_Insert(file_context->inode_table, (void*)(UINT_PTR)clip_data_dir->ino, + clip_data_dir)) + { + WLog_Print(file_context->log, WLOG_ERROR, "Failed to insert inode into inode table"); + ArrayList_Remove(root_dir->children, clip_data_dir); + fuse_file_free(clip_data_dir); + return NULL; + } + + return clip_data_dir; +} + +static char* get_parent_path(const char* filepath) +{ + char* base = NULL; + size_t parent_path_length = 0; + char* parent_path = NULL; + + base = strrchr(filepath, '/'); + WINPR_ASSERT(base); + + while (base > filepath && *base == '/') + --base; + + parent_path_length = 1 + base - filepath; + parent_path = calloc(parent_path_length + 1, sizeof(char)); + if (!parent_path) + return NULL; + + memcpy(parent_path, filepath, parent_path_length); + + return parent_path; +} + +static BOOL is_fuse_file_not_parent(const void* key, void* value, void* arg) +{ + CliprdrFuseFile* fuse_file = value; + CliprdrFuseFindParentContext* find_context = arg; + + if (!fuse_file->is_directory) + return TRUE; + + if (strcmp(find_context->parent_path, fuse_file->filename_with_root) == 0) + { + find_context->parent = fuse_file; + return FALSE; + } + + return TRUE; +} + +static CliprdrFuseFile* get_parent_directory(CliprdrFileContext* file_context, const char* path) +{ + CliprdrFuseFindParentContext find_context = { 0 }; + + WINPR_ASSERT(file_context); + WINPR_ASSERT(path); + + find_context.parent_path = get_parent_path(path); + if (!find_context.parent_path) + return NULL; + + WINPR_ASSERT(!find_context.parent); + + if (HashTable_Foreach(file_context->inode_table, is_fuse_file_not_parent, &find_context)) + { + free(find_context.parent_path); + return NULL; + } + WINPR_ASSERT(find_context.parent); + + free(find_context.parent_path); + + return find_context.parent; +} + +static BOOL set_selection_for_clip_data_entry(CliprdrFileContext* file_context, + CliprdrFuseClipDataEntry* clip_data_entry, + FILEDESCRIPTORW* files, UINT32 n_files) +{ + CliprdrFuseFile* clip_data_dir = NULL; + + WINPR_ASSERT(file_context); + WINPR_ASSERT(clip_data_entry); + WINPR_ASSERT(files); + + clip_data_dir = clip_data_entry->clip_data_dir; + WINPR_ASSERT(clip_data_dir); + + if (clip_data_entry->has_clip_data_id) + WLog_Print(file_context->log, WLOG_DEBUG, "Setting selection for clipDataId %u", + clip_data_entry->clip_data_id); + else + WLog_Print(file_context->log, WLOG_DEBUG, "Setting selection"); + + // NOLINTBEGIN(clang-analyzer-unix.Malloc) HashTable_Insert owns fuse_file + for (UINT32 i = 0; i < n_files; ++i) + { + FILEDESCRIPTORW* file = &files[i]; + CliprdrFuseFile* fuse_file = NULL; + char* filename = NULL; + size_t path_length = 0; + + fuse_file = fuse_file_new(); + if (!fuse_file) + { + WLog_Print(file_context->log, WLOG_ERROR, "Failed to create FUSE file"); + clear_entry_selection(clip_data_entry); + return FALSE; + } + + filename = ConvertWCharToUtf8Alloc(file->cFileName, NULL); + if (!filename) + { + WLog_Print(file_context->log, WLOG_ERROR, "Failed to convert filename"); + fuse_file_free(fuse_file); + clear_entry_selection(clip_data_entry); + return FALSE; + } + + for (size_t j = 0; filename[j]; ++j) + { + if (filename[j] == '\\') + filename[j] = '/'; + } + + path_length = strlen(clip_data_dir->filename_with_root) + 1 + strlen(filename) + 1; + fuse_file->filename_with_root = calloc(path_length, sizeof(char)); + if (!fuse_file->filename_with_root) + { + WLog_Print(file_context->log, WLOG_ERROR, "Failed to allocate filename"); + free(filename); + fuse_file_free(fuse_file); + clear_entry_selection(clip_data_entry); + return FALSE; + } + + _snprintf(fuse_file->filename_with_root, path_length, "%s/%s", + clip_data_dir->filename_with_root, filename); + free(filename); + + fuse_file->filename = strrchr(fuse_file->filename_with_root, '/') + 1; + + fuse_file->parent = get_parent_directory(file_context, fuse_file->filename_with_root); + if (!fuse_file->parent) + { + WLog_Print(file_context->log, WLOG_ERROR, "Found no parent for FUSE file"); + fuse_file_free(fuse_file); + clear_entry_selection(clip_data_entry); + return FALSE; + } + + if (!ArrayList_Append(fuse_file->parent->children, fuse_file)) + { + WLog_Print(file_context->log, WLOG_ERROR, "Failed to append FUSE file"); + fuse_file_free(fuse_file); + clear_entry_selection(clip_data_entry); + return FALSE; + } + + fuse_file->list_idx = i; + fuse_file->ino = get_next_free_inode(file_context); + fuse_file->has_clip_data_id = clip_data_entry->has_clip_data_id; + fuse_file->clip_data_id = clip_data_entry->clip_data_id; + if (file->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + fuse_file->is_directory = TRUE; + if (file->dwFileAttributes & FILE_ATTRIBUTE_READONLY) + fuse_file->is_readonly = TRUE; + if (file->dwFlags & FD_FILESIZE) + { + fuse_file->size = ((UINT64)file->nFileSizeHigh << 32) + file->nFileSizeLow; + fuse_file->has_size = TRUE; + } + if (file->dwFlags & FD_WRITESTIME) + { + UINT64 filetime = 0; + + filetime = file->ftLastWriteTime.dwHighDateTime; + filetime <<= 32; + filetime += file->ftLastWriteTime.dwLowDateTime; + + fuse_file->last_write_time_unix = + filetime / (10 * 1000 * 1000) - WIN32_FILETIME_TO_UNIX_EPOCH; + fuse_file->has_last_write_time = TRUE; + } + + if (!HashTable_Insert(file_context->inode_table, (void*)(UINT_PTR)fuse_file->ino, + fuse_file)) + { + WLog_Print(file_context->log, WLOG_ERROR, "Failed to insert inode into inode table"); + fuse_file_free(fuse_file); + clear_entry_selection(clip_data_entry); + return FALSE; + } + } + // NOLINTEND(clang-analyzer-unix.Malloc) HashTable_Insert owns fuse_file + + if (clip_data_entry->has_clip_data_id) + WLog_Print(file_context->log, WLOG_DEBUG, "Selection set for clipDataId %u", + clip_data_entry->clip_data_id); + else + WLog_Print(file_context->log, WLOG_DEBUG, "Selection set"); + + return TRUE; +} + +static BOOL update_exposed_path(CliprdrFileContext* file_context, wClipboard* clip, + CliprdrFuseClipDataEntry* clip_data_entry) +{ + wClipboardDelegate* delegate = NULL; + CliprdrFuseFile* clip_data_dir = NULL; + + WINPR_ASSERT(file_context); + WINPR_ASSERT(clip); + WINPR_ASSERT(clip_data_entry); + + delegate = ClipboardGetDelegate(clip); + WINPR_ASSERT(delegate); + + clip_data_dir = clip_data_entry->clip_data_dir; + WINPR_ASSERT(clip_data_dir); + + free(file_context->exposed_path); + file_context->exposed_path = GetCombinedPath(file_context->path, clip_data_dir->filename); + if (file_context->exposed_path) + WLog_Print(file_context->log, WLOG_DEBUG, "Updated exposed path to \"%s\"", + file_context->exposed_path); + + delegate->basePath = file_context->exposed_path; + + return delegate->basePath != NULL; +} +#endif + +BOOL cliprdr_file_context_update_server_data(CliprdrFileContext* file_context, wClipboard* clip, + const void* data, size_t size) +{ +#if defined(WITH_FUSE) + CliprdrFuseClipDataEntry* clip_data_entry = NULL; + FILEDESCRIPTORW* files = NULL; + UINT32 n_files = 0; + + WINPR_ASSERT(file_context); + WINPR_ASSERT(clip); + + if (cliprdr_parse_file_list(data, size, &files, &n_files)) + { + WLog_Print(file_context->log, WLOG_ERROR, "Failed to parse file list"); + return FALSE; + } + + HashTable_Lock(file_context->inode_table); + if (does_server_support_clipdata_locking(file_context)) + clip_data_entry = HashTable_GetItemValue( + file_context->clip_data_table, (void*)(UINT_PTR)file_context->current_clip_data_id); + else + clip_data_entry = file_context->clip_data_entry_without_id; + + WINPR_ASSERT(clip_data_entry); + + clear_entry_selection(clip_data_entry); + WINPR_ASSERT(!clip_data_entry->clip_data_dir); + + clip_data_entry->clip_data_dir = + clip_data_dir_new(file_context, does_server_support_clipdata_locking(file_context), + file_context->current_clip_data_id); + if (!clip_data_entry->clip_data_dir) + { + HashTable_Unlock(file_context->inode_table); + free(files); + return FALSE; + } + + if (!update_exposed_path(file_context, clip, clip_data_entry)) + { + HashTable_Unlock(file_context->inode_table); + free(files); + return FALSE; + } + + if (!set_selection_for_clip_data_entry(file_context, clip_data_entry, files, n_files)) + { + HashTable_Unlock(file_context->inode_table); + free(files); + return FALSE; + } + HashTable_Unlock(file_context->inode_table); + + return TRUE; +#else + return FALSE; +#endif +} + +void* cliprdr_file_context_get_context(CliprdrFileContext* file) +{ + WINPR_ASSERT(file); + return file->clipboard; +} + +void cliprdr_file_session_terminate(CliprdrFileContext* file, BOOL stop_thread) +{ + if (!file) + return; + +#if defined(WITH_FUSE) + WINPR_ASSERT(file->fuse_stop_sync); + + WLog_Print(file->log, WLOG_DEBUG, "Setting FUSE exit flag"); + if (file->fuse_sess) + fuse_session_exit(file->fuse_sess); + + if (stop_thread) + { + WLog_Print(file->log, WLOG_DEBUG, "Setting FUSE stop event"); + SetEvent(file->fuse_stop_sync); + } +#endif + /* not elegant but works for umounting FUSE + fuse_chan must receive an oper buf to unblock fuse_session_receive_buf function. + */ +#if defined(WITH_FUSE) + WLog_Print(file->log, WLOG_DEBUG, "Forcing FUSE to check exit flag"); +#endif + winpr_PathFileExists(file->path); +} + +void cliprdr_file_context_free(CliprdrFileContext* file) +{ + if (!file) + return; + +#if defined(WITH_FUSE) + if (file->inode_table) + { + clear_no_cdi_entry(file); + clear_all_selections(file); + } + + if (file->fuse_thread) + { + WINPR_ASSERT(file->fuse_stop_sync); + + WLog_Print(file->log, WLOG_DEBUG, "Stopping FUSE thread"); + cliprdr_file_session_terminate(file, TRUE); + + WLog_Print(file->log, WLOG_DEBUG, "Waiting on FUSE thread"); + WaitForSingleObject(file->fuse_thread, INFINITE); + CloseHandle(file->fuse_thread); + } + if (file->fuse_stop_sync) + CloseHandle(file->fuse_stop_sync); + if (file->fuse_start_sync) + CloseHandle(file->fuse_start_sync); + + HashTable_Free(file->request_table); + HashTable_Free(file->clip_data_table); + HashTable_Free(file->inode_table); +#endif + HashTable_Free(file->local_streams); + winpr_RemoveDirectory(file->path); + free(file->path); + free(file->exposed_path); + free(file); +} + +static BOOL create_base_path(CliprdrFileContext* file) +{ + WINPR_ASSERT(file); + + char base[64] = { 0 }; + _snprintf(base, sizeof(base), "com.freerdp.client.cliprdr.%" PRIu32, GetCurrentProcessId()); + + file->path = GetKnownSubPath(KNOWN_PATH_TEMP, base); + if (!file->path) + return FALSE; + + if (!winpr_PathFileExists(file->path) && !winpr_PathMakePath(file->path, 0)) + { + WLog_Print(file->log, WLOG_ERROR, "Failed to create directory '%s'", file->path); + return FALSE; + } + return TRUE; +} + +static void cliprdr_local_file_free(CliprdrLocalFile* file) +{ + const CliprdrLocalFile empty = { 0 }; + if (!file) + return; + if (file->fp) + { + WLog_Print(file->context->log, WLOG_DEBUG, "closing file %s, discarding entry", file->name); + fclose(file->fp); + } + free(file->name); + *file = empty; +} + +static BOOL cliprdr_local_file_new(CliprdrFileContext* context, CliprdrLocalFile* f, + const char* path) +{ + const CliprdrLocalFile empty = { 0 }; + WINPR_ASSERT(f); + WINPR_ASSERT(context); + WINPR_ASSERT(path); + + *f = empty; + f->context = context; + f->name = winpr_str_url_decode(path, strlen(path)); + if (!f->name) + goto fail; + + return TRUE; +fail: + cliprdr_local_file_free(f); + return FALSE; +} + +static void cliprdr_local_files_free(CliprdrLocalStream* stream) +{ + WINPR_ASSERT(stream); + + for (size_t x = 0; x < stream->count; x++) + cliprdr_local_file_free(&stream->files[x]); + free(stream->files); + + stream->files = NULL; + stream->count = 0; +} + +static void cliprdr_local_stream_free(void* obj) +{ + CliprdrLocalStream* stream = (CliprdrLocalStream*)obj; + if (stream) + cliprdr_local_files_free(stream); + + free(stream); +} + +static BOOL append_entry(CliprdrLocalStream* stream, const char* path) +{ + CliprdrLocalFile* tmp = realloc(stream->files, sizeof(CliprdrLocalFile) * (stream->count + 1)); + if (!tmp) + return FALSE; + stream->files = tmp; + CliprdrLocalFile* f = &stream->files[stream->count++]; + + return cliprdr_local_file_new(stream->context, f, path); +} + +static BOOL is_directory(const char* path) +{ + WCHAR* wpath = ConvertUtf8ToWCharAlloc(path, NULL); + if (!wpath) + return FALSE; + + HANDLE hFile = + CreateFileW(wpath, 0, FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + free(wpath); + + if (hFile == INVALID_HANDLE_VALUE) + return FALSE; + + BY_HANDLE_FILE_INFORMATION fileInformation = { 0 }; + const BOOL status = GetFileInformationByHandle(hFile, &fileInformation); + CloseHandle(hFile); + if (!status) + return FALSE; + + return (fileInformation.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? TRUE : FALSE; +} + +static BOOL add_directory(CliprdrLocalStream* stream, const char* path) +{ + char* wildcardpath = GetCombinedPath(path, "*"); + if (!wildcardpath) + return FALSE; + WCHAR* wpath = ConvertUtf8ToWCharAlloc(wildcardpath, NULL); + free(wildcardpath); + if (!wpath) + return FALSE; + + WIN32_FIND_DATAW FindFileData = { 0 }; + HANDLE hFind = FindFirstFileW(wpath, &FindFileData); + free(wpath); + + if (hFind == INVALID_HANDLE_VALUE) + return FALSE; + + BOOL rc = FALSE; + char* next = NULL; + + WCHAR dotbuffer[6] = { 0 }; + WCHAR dotdotbuffer[6] = { 0 }; + const WCHAR* dot = InitializeConstWCharFromUtf8(".", dotbuffer, ARRAYSIZE(dotbuffer)); + const WCHAR* dotdot = InitializeConstWCharFromUtf8("..", dotdotbuffer, ARRAYSIZE(dotdotbuffer)); + do + { + if (_wcscmp(FindFileData.cFileName, dot) == 0) + continue; + if (_wcscmp(FindFileData.cFileName, dotdot) == 0) + continue; + + char cFileName[MAX_PATH] = { 0 }; + ConvertWCharNToUtf8(FindFileData.cFileName, ARRAYSIZE(FindFileData.cFileName), cFileName, + ARRAYSIZE(cFileName)); + + free(next); + next = GetCombinedPath(path, cFileName); + if (!next) + goto fail; + + if (!append_entry(stream, next)) + goto fail; + if (is_directory(next)) + { + if (!add_directory(stream, next)) + goto fail; + } + } while (FindNextFileW(hFind, &FindFileData)); + + rc = TRUE; +fail: + free(next); + FindClose(hFind); + + return rc; +} + +static BOOL cliprdr_local_stream_update(CliprdrLocalStream* stream, const char* data, size_t size) +{ + BOOL rc = FALSE; + WINPR_ASSERT(stream); + if (size == 0) + return TRUE; + + cliprdr_local_files_free(stream); + + stream->files = calloc(size, sizeof(CliprdrLocalFile)); + if (!stream->files) + return FALSE; + + char* copy = strndup(data, size); + if (!copy) + return FALSE; + char* ptr = strtok(copy, "\r\n"); + while (ptr) + { + const char* name = ptr; + if (strncmp("file:///", ptr, 8) == 0) + name = &ptr[7]; + else if (strncmp("file:/", ptr, 6) == 0) + name = &ptr[5]; + + if (!append_entry(stream, name)) + goto fail; + + if (is_directory(name)) + { + const BOOL res = add_directory(stream, name); + if (!res) + goto fail; + } + ptr = strtok(NULL, "\r\n"); + } + + rc = TRUE; +fail: + free(copy); + return rc; +} + +CliprdrLocalStream* cliprdr_local_stream_new(CliprdrFileContext* context, UINT32 lockId, + const char* data, size_t size) +{ + WINPR_ASSERT(context); + CliprdrLocalStream* stream = calloc(1, sizeof(CliprdrLocalStream)); + if (!stream) + return NULL; + + stream->context = context; + if (!cliprdr_local_stream_update(stream, data, size)) + goto fail; + + stream->lockId = lockId; + return stream; + +fail: + cliprdr_local_stream_free(stream); + return NULL; +} + +static UINT32 UINTPointerHash(const void* id) +{ + WINPR_ASSERT(id); + return *((const UINT32*)id); +} + +static BOOL UINTPointerCompare(const void* pointer1, const void* pointer2) +{ + if (!pointer1 || !pointer2) + return pointer1 == pointer2; + + const UINT32* a = pointer1; + const UINT32* b = pointer2; + return *a == *b; +} + +static void* UINTPointerClone(const void* other) +{ + const UINT32* src = other; + if (!src) + return NULL; + + UINT32* copy = calloc(1, sizeof(UINT32)); + if (!copy) + return NULL; + + *copy = *src; + return copy; +} + +#if defined(WITH_FUSE) +static CliprdrFuseFile* fuse_file_new_root(CliprdrFileContext* file_context) +{ + CliprdrFuseFile* root_dir = NULL; + + root_dir = fuse_file_new(); + if (!root_dir) + return NULL; + + root_dir->filename_with_root = calloc(2, sizeof(char)); + if (!root_dir->filename_with_root) + { + fuse_file_free(root_dir); + return NULL; + } + + _snprintf(root_dir->filename_with_root, 2, "/"); + root_dir->filename = root_dir->filename_with_root; + + root_dir->ino = FUSE_ROOT_ID; + root_dir->is_directory = TRUE; + root_dir->is_readonly = TRUE; + + if (!HashTable_Insert(file_context->inode_table, (void*)(UINT_PTR)root_dir->ino, root_dir)) + { + fuse_file_free(root_dir); + return NULL; + } + + return root_dir; +} +#endif + +CliprdrFileContext* cliprdr_file_context_new(void* context) +{ + CliprdrFileContext* file = calloc(1, sizeof(CliprdrFileContext)); + if (!file) + return NULL; + + file->log = WLog_Get(CLIENT_TAG("common.cliprdr.file")); + file->clipboard = context; + + file->local_streams = HashTable_New(FALSE); + if (!file->local_streams) + goto fail; + + if (!HashTable_SetHashFunction(file->local_streams, UINTPointerHash)) + goto fail; + + wObject* hkobj = HashTable_KeyObject(file->local_streams); + WINPR_ASSERT(hkobj); + hkobj->fnObjectEquals = UINTPointerCompare; + hkobj->fnObjectFree = free; + hkobj->fnObjectNew = UINTPointerClone; + + wObject* hobj = HashTable_ValueObject(file->local_streams); + WINPR_ASSERT(hobj); + hobj->fnObjectFree = cliprdr_local_stream_free; + +#if defined(WITH_FUSE) + file->inode_table = HashTable_New(FALSE); + file->clip_data_table = HashTable_New(FALSE); + file->request_table = HashTable_New(FALSE); + if (!file->inode_table || !file->clip_data_table || !file->request_table) + goto fail; + + wObject* ctobj = HashTable_ValueObject(file->clip_data_table); + WINPR_ASSERT(ctobj); + ctobj->fnObjectFree = clip_data_entry_free; + + file->root_dir = fuse_file_new_root(file); + if (!file->root_dir) + goto fail; +#endif + + if (!create_base_path(file)) + goto fail; + +#if defined(WITH_FUSE) + if (!(file->fuse_start_sync = CreateEvent(NULL, TRUE, FALSE, NULL))) + goto fail; + if (!(file->fuse_stop_sync = CreateEvent(NULL, TRUE, FALSE, NULL))) + goto fail; + if (!(file->fuse_thread = CreateThread(NULL, 0, cliprdr_file_fuse_thread, file, 0, NULL))) + goto fail; + + if (WaitForSingleObject(file->fuse_start_sync, INFINITE) == WAIT_FAILED) + WLog_Print(file->log, WLOG_ERROR, "Failed to wait for start sync"); +#endif + return file; + +fail: + WINPR_PRAGMA_DIAG_PUSH + WINPR_PRAGMA_DIAG_IGNORED_MISMATCHED_DEALLOC + cliprdr_file_context_free(file); + WINPR_PRAGMA_DIAG_POP + return NULL; +} + +BOOL local_stream_discard(const void* key, void* value, void* arg) +{ + CliprdrFileContext* file = arg; + CliprdrLocalStream* stream = value; + WINPR_ASSERT(file); + WINPR_ASSERT(stream); + + if (!stream->locked) + HashTable_Remove(file->local_streams, key); + return TRUE; +} + +BOOL cliprdr_file_context_clear(CliprdrFileContext* file) +{ + WINPR_ASSERT(file); + + WLog_Print(file->log, WLOG_DEBUG, "clear file clipboard..."); + + HashTable_Lock(file->local_streams); + HashTable_Foreach(file->local_streams, local_stream_discard, file); + HashTable_Unlock(file->local_streams); + + memset(file->server_data_hash, 0, sizeof(file->server_data_hash)); + memset(file->client_data_hash, 0, sizeof(file->client_data_hash)); + return TRUE; +} + +BOOL cliprdr_file_context_update_client_data(CliprdrFileContext* file, const char* data, + size_t size) +{ + BOOL rc = FALSE; + + WINPR_ASSERT(file); + if (!cliprdr_file_client_content_changed_and_update(file, data, size)) + return TRUE; + + if (!cliprdr_file_context_clear(file)) + return FALSE; + + UINT32 lockId = file->local_lock_id; + + HashTable_Lock(file->local_streams); + CliprdrLocalStream* stream = HashTable_GetItemValue(file->local_streams, &lockId); + + WLog_Print(file->log, WLOG_DEBUG, "update client file list (stream=%p)...", stream); + if (stream) + rc = cliprdr_local_stream_update(stream, data, size); + else + { + stream = cliprdr_local_stream_new(file, lockId, data, size); + rc = HashTable_Insert(file->local_streams, &stream->lockId, stream); + if (!rc) + cliprdr_local_stream_free(stream); + } + // HashTable_Insert owns stream + // NOLINTNEXTLINE(clang-analyzer-unix.Malloc) + HashTable_Unlock(file->local_streams); + return rc; +} + +UINT32 cliprdr_file_context_current_flags(CliprdrFileContext* file) +{ + WINPR_ASSERT(file); + + if ((file->file_capability_flags & CB_STREAM_FILECLIP_ENABLED) == 0) + return 0; + + if (!file->file_formats_registered) + return 0; + + return CB_STREAM_FILECLIP_ENABLED | CB_FILECLIP_NO_FILE_PATHS | + CB_HUGE_FILE_SUPPORT_ENABLED; // | CB_CAN_LOCK_CLIPDATA; +} + +BOOL cliprdr_file_context_set_locally_available(CliprdrFileContext* file, BOOL available) +{ + WINPR_ASSERT(file); + file->file_formats_registered = available; + return TRUE; +} + +BOOL cliprdr_file_context_remote_set_flags(CliprdrFileContext* file, UINT32 flags) +{ + WINPR_ASSERT(file); + file->file_capability_flags = flags; + return TRUE; +} + +UINT32 cliprdr_file_context_remote_get_flags(CliprdrFileContext* file) +{ + WINPR_ASSERT(file); + return file->file_capability_flags; +} + +BOOL cliprdr_file_context_has_local_support(CliprdrFileContext* file) +{ + WINPR_UNUSED(file); + +#if defined(WITH_FUSE) + return TRUE; +#else + return FALSE; +#endif +} diff --git a/client/common/cmdline.c b/client/common/cmdline.c new file mode 100644 index 0000000..2ce693b --- /dev/null +++ b/client/common/cmdline.c @@ -0,0 +1,5922 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Client Command-Line Interface + * + * Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * Copyright 2014 Norbert Federa <norbert.federa@thincast.com> + * Copyright 2016 Armin Novak <armin.novak@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <ctype.h> +#include <errno.h> + +#include <winpr/assert.h> +#include <winpr/crt.h> +#include <winpr/wlog.h> +#include <winpr/path.h> +#include <winpr/ncrypt.h> +#include <winpr/environment.h> + +#include <freerdp/freerdp.h> +#include <freerdp/addin.h> +#include <freerdp/settings.h> +#include <freerdp/client.h> +#include <freerdp/client/channels.h> +#include <freerdp/channels/drdynvc.h> +#include <freerdp/channels/cliprdr.h> +#include <freerdp/channels/encomsp.h> +#include <freerdp/channels/rdp2tcp.h> +#include <freerdp/channels/remdesk.h> +#include <freerdp/channels/rdpsnd.h> +#include <freerdp/channels/disp.h> +#include <freerdp/crypto/crypto.h> +#include <freerdp/locale/keyboard.h> +#include <freerdp/utils/passphrase.h> +#include <freerdp/utils/proxy_utils.h> +#include <freerdp/channels/urbdrc.h> +#include <freerdp/channels/rdpdr.h> + +#if defined(CHANNEL_AINPUT_CLIENT) +#include <freerdp/channels/ainput.h> +#endif + +#include <freerdp/channels/audin.h> +#include <freerdp/channels/echo.h> + +#include <freerdp/client/cmdline.h> +#include <freerdp/version.h> +#include <freerdp/client/utils/smartcard_cli.h> + +#include <openssl/tls1.h> +#include "cmdline.h" + +#include <freerdp/log.h> +#define TAG CLIENT_TAG("common.cmdline") + +static const char* option_starts_with(const char* what, const char* val); +static BOOL option_ends_with(const char* str, const char* ext); +static BOOL option_equals(const char* what, const char* val); + +static BOOL freerdp_client_print_codepages(const char* arg) +{ + size_t count = 0; + DWORD column = 2; + const char* filter = NULL; + RDP_CODEPAGE* pages = NULL; + + if (arg) + { + filter = strchr(arg, ','); + if (!filter) + filter = arg; + else + filter++; + } + pages = freerdp_keyboard_get_matching_codepages(column, filter, &count); + if (!pages) + return TRUE; + + printf("%-10s %-8s %-60s %-36s %-48s\n", "<id>", "<locale>", "<win langid>", "<language>", + "<country>"); + for (size_t x = 0; x < count; x++) + { + const RDP_CODEPAGE* page = &pages[x]; + char buffer[520] = { 0 }; + + if (strnlen(page->subLanguageSymbol, ARRAYSIZE(page->subLanguageSymbol)) > 0) + _snprintf(buffer, sizeof(buffer), "[%s|%s]", page->primaryLanguageSymbol, + page->subLanguageSymbol); + else + _snprintf(buffer, sizeof(buffer), "[%s]", page->primaryLanguageSymbol); + printf("id=0x%04" PRIx16 ": [%-6s] %-60s %-36s %-48s\n", page->id, page->locale, buffer, + page->primaryLanguage, page->subLanguage); + } + freerdp_codepages_free(pages); + return TRUE; +} + +static BOOL freerdp_path_valid(const char* path, BOOL* special) +{ + const char DynamicDrives[] = "DynamicDrives"; + BOOL isPath = FALSE; + BOOL isSpecial = 0; + if (!path) + return FALSE; + + isSpecial = + (option_equals("*", path) || option_equals(DynamicDrives, path) || option_equals("%", path)) + ? TRUE + : FALSE; + if (!isSpecial) + isPath = winpr_PathFileExists(path); + + if (special) + *special = isSpecial; + + return isSpecial || isPath; +} + +static BOOL freerdp_sanitize_drive_name(char* name, const char* invalid, const char* replacement) +{ + if (!name || !invalid || !replacement) + return FALSE; + if (strlen(invalid) != strlen(replacement)) + return FALSE; + + while (*invalid != '\0') + { + const char what = *invalid++; + const char with = *replacement++; + + char* cur = name; + while ((cur = strchr(cur, what)) != NULL) + *cur = with; + } + return TRUE; +} + +static char* name_from_path(const char* path) +{ + const char* name = "NULL"; + if (path) + { + if (option_equals("%", path)) + name = "home"; + else if (option_equals("*", path)) + name = "hotplug-all"; + else if (option_equals("DynamicDrives", path)) + name = "hotplug"; + else + name = path; + } + return _strdup(name); +} + +static BOOL freerdp_client_add_drive(rdpSettings* settings, const char* path, const char* name) +{ + char* dname = NULL; + RDPDR_DEVICE* device = NULL; + + if (name) + { + BOOL skip = FALSE; + if (path) + { + switch (path[0]) + { + case '*': + case '%': + skip = TRUE; + break; + default: + break; + } + } + /* Path was entered as secondary argument, swap */ + if (!skip && winpr_PathFileExists(name)) + { + if (!winpr_PathFileExists(path) || (!PathIsRelativeA(name) && PathIsRelativeA(path))) + { + const char* tmp = path; + path = name; + name = tmp; + } + } + } + + if (name) + dname = _strdup(name); + else /* We need a name to send to the server. */ + dname = name_from_path(path); + + if (freerdp_sanitize_drive_name(dname, "\\/", "__")) + { + const char* args[] = { dname, path }; + device = freerdp_device_new(RDPDR_DTYP_FILESYSTEM, ARRAYSIZE(args), args); + } + free(dname); + if (!device) + goto fail; + + if (!path) + goto fail; + else + { + BOOL isSpecial = FALSE; + BOOL isPath = freerdp_path_valid(path, &isSpecial); + + if (!isPath && !isSpecial) + goto fail; + } + + if (!freerdp_device_collection_add(settings, device)) + goto fail; + + return TRUE; + +fail: + freerdp_device_free(device); + return FALSE; +} + +static BOOL value_to_int(const char* value, LONGLONG* result, LONGLONG min, LONGLONG max) +{ + long long rc = 0; + + if (!value || !result) + return FALSE; + + errno = 0; + rc = _strtoi64(value, NULL, 0); + + if (errno != 0) + return FALSE; + + if ((rc < min) || (rc > max)) + return FALSE; + + *result = rc; + return TRUE; +} + +static BOOL value_to_uint(const char* value, ULONGLONG* result, ULONGLONG min, ULONGLONG max) +{ + unsigned long long rc = 0; + + if (!value || !result) + return FALSE; + + errno = 0; + rc = _strtoui64(value, NULL, 0); + + if (errno != 0) + return FALSE; + + if ((rc < min) || (rc > max)) + return FALSE; + + *result = rc; + return TRUE; +} + +BOOL freerdp_client_print_version(void) +{ + printf("This is FreeRDP version %s (%s)\n", FREERDP_VERSION_FULL, FREERDP_GIT_REVISION); + return TRUE; +} + +BOOL freerdp_client_print_buildconfig(void) +{ + printf("%s", freerdp_get_build_config()); + return TRUE; +} + +static void freerdp_client_print_scancodes(void) +{ + printf("RDP scancodes and their name for use with /kbd:remap\n"); + + for (UINT32 x = 0; x < UINT16_MAX; x++) + { + const char* name = freerdp_keyboard_scancode_name(x); + if (name) + printf("0x%04" PRIx32 " --> %s\n", x, name); + } +} + +static BOOL is_delimiter(char c, const char* delimiters) +{ + char d = 0; + while ((d = *delimiters++) != '\0') + { + if (c == d) + return TRUE; + } + return FALSE; +} + +static const char* get_last(const char* start, size_t len, const char* delimiters) +{ + const char* last = NULL; + for (size_t x = 0; x < len; x++) + { + char c = start[x]; + if (is_delimiter(c, delimiters)) + last = &start[x]; + } + return last; +} + +static SSIZE_T next_delimiter(const char* text, size_t len, size_t max, const char* delimiters) +{ + if (len < max) + return -1; + + const char* last = get_last(text, max, delimiters); + if (!last) + return -1; + + return (SSIZE_T)(last - text); +} + +static SSIZE_T forced_newline_at(const char* text, size_t len, size_t limit, + const char* force_newline) +{ + char d = 0; + while ((d = *force_newline++) != '\0') + { + const char* tok = strchr(text, d); + if (tok) + { + const size_t offset = tok - text; + if ((offset > len) || (offset > limit)) + continue; + return (SSIZE_T)(offset); + } + } + return -1; +} + +static BOOL print_align(size_t start_offset, size_t* current) +{ + WINPR_ASSERT(current); + if (*current < start_offset) + { + const int rc = printf("%*c", (int)(start_offset - *current), ' '); + if (rc < 0) + return FALSE; + *current += (size_t)rc; + } + return TRUE; +} + +static char* print_token(char* text, size_t start_offset, size_t* current, size_t limit, + const char* delimiters, const char* force_newline) +{ + int rc = 0; + const size_t tlen = strnlen(text, limit); + size_t len = tlen; + const SSIZE_T force_at = forced_newline_at(text, len, limit - *current, force_newline); + BOOL isForce = (force_at > 0); + + if (isForce) + len = MIN(len, (size_t)force_at); + + if (!print_align(start_offset, current)) + return NULL; + + const SSIZE_T delim = next_delimiter(text, len, limit - *current, delimiters); + const BOOL isDelim = delim > 0; + if (isDelim) + { + len = MIN(len, (size_t)delim + 1); + } + + rc = printf("%.*s", (int)len, text); + if (rc < 0) + return NULL; + + if (isForce || isDelim) + { + printf("\n"); + *current = 0; + + const size_t offset = len + (isForce ? 1 : 0); + return &text[offset]; + } + + *current += (size_t)rc; + + if (tlen == (size_t)rc) + return NULL; + return &text[(size_t)rc]; +} + +static size_t print_optionals(const char* text, size_t start_offset, size_t current) +{ + const size_t limit = 80; + char* str = _strdup(text); + char* cur = str; + + while ((cur = print_token(cur, start_offset + 1, ¤t, limit, "[], ", "\r\n")) != NULL) + ; + + free(str); + return current; +} + +static size_t print_description(const char* text, size_t start_offset, size_t current) +{ + const size_t limit = 80; + char* str = _strdup(text); + char* cur = str; + + while ((cur = print_token(cur, start_offset, ¤t, limit, " ", "\r\n")) != NULL) + ; + + free(str); + current += (size_t)printf("\n"); + return current; +} + +static int cmp_cmdline_args(const void* pva, const void* pvb) +{ + const COMMAND_LINE_ARGUMENT_A* a = (const COMMAND_LINE_ARGUMENT_A*)pva; + const COMMAND_LINE_ARGUMENT_A* b = (const COMMAND_LINE_ARGUMENT_A*)pvb; + + if (!a->Name && !b->Name) + return 0; + if (!a->Name) + return 1; + if (!b->Name) + return -1; + return strcmp(a->Name, b->Name); +} + +static void freerdp_client_print_command_line_args(COMMAND_LINE_ARGUMENT_A* parg, size_t count) +{ + if (!parg) + return; + + qsort(parg, count, sizeof(COMMAND_LINE_ARGUMENT_A), cmp_cmdline_args); + + const COMMAND_LINE_ARGUMENT_A* arg = parg; + do + { + int rc = 0; + size_t pos = 0; + const size_t description_offset = 30 + 8; + + if (arg->Flags & (COMMAND_LINE_VALUE_BOOL | COMMAND_LINE_VALUE_FLAG)) + { + if ((arg->Flags & ~COMMAND_LINE_VALUE_BOOL) == 0) + rc = printf(" %s%s", arg->Default ? "-" : "+", arg->Name); + else if ((arg->Flags & COMMAND_LINE_VALUE_OPTIONAL) != 0) + rc = printf(" [%s|/]%s", arg->Default ? "-" : "+", arg->Name); + else + { + rc = printf(" %s%s", arg->Default ? "-" : "+", arg->Name); + } + } + else + rc = printf(" /%s", arg->Name); + + if (rc < 0) + return; + pos += (size_t)rc; + + if ((arg->Flags & COMMAND_LINE_VALUE_REQUIRED) || + (arg->Flags & COMMAND_LINE_VALUE_OPTIONAL)) + { + if (arg->Format) + { + if (arg->Flags & COMMAND_LINE_VALUE_OPTIONAL) + { + rc = printf("[:"); + if (rc < 0) + return; + pos += (size_t)rc; + pos = print_optionals(arg->Format, pos, pos); + rc = printf("]"); + if (rc < 0) + return; + pos += (size_t)rc; + } + else + { + rc = printf(":"); + if (rc < 0) + return; + pos += (size_t)rc; + pos = print_optionals(arg->Format, pos, pos); + } + + if (pos > description_offset) + { + printf("\n"); + pos = 0; + } + } + } + + rc = printf("%*c", (int)(description_offset - pos), ' '); + if (rc < 0) + return; + pos += (size_t)rc; + + if (arg->Flags & COMMAND_LINE_VALUE_BOOL) + { + rc = printf("%s ", arg->Default ? "Disable" : "Enable"); + if (rc < 0) + return; + pos += (size_t)rc; + } + + print_description(arg->Text, description_offset, pos); + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); +} + +BOOL freerdp_client_print_command_line_help(int argc, char** argv) +{ + return freerdp_client_print_command_line_help_ex(argc, argv, NULL); +} + +static COMMAND_LINE_ARGUMENT_A* create_merged_args(const COMMAND_LINE_ARGUMENT_A* custom, + SSIZE_T count, size_t* pcount) +{ + WINPR_ASSERT(pcount); + if (count < 0) + { + const COMMAND_LINE_ARGUMENT_A* cur = custom; + count = 0; + while (cur && cur->Name) + { + count++; + cur++; + } + } + + COMMAND_LINE_ARGUMENT_A* largs = + calloc(count + ARRAYSIZE(global_cmd_args), sizeof(COMMAND_LINE_ARGUMENT_A)); + *pcount = 0; + if (!largs) + return NULL; + + size_t lcount = 0; + const COMMAND_LINE_ARGUMENT_A* cur = custom; + while (cur && cur->Name) + { + largs[lcount++] = *cur++; + } + + cur = global_cmd_args; + while (cur && cur->Name) + { + largs[lcount++] = *cur++; + } + *pcount = lcount; + return largs; +} + +BOOL freerdp_client_print_command_line_help_ex(int argc, char** argv, + const COMMAND_LINE_ARGUMENT_A* custom) +{ + const char* name = "FreeRDP"; + + /* allocate a merged copy of implementation defined and default arguments */ + size_t lcount = 0; + COMMAND_LINE_ARGUMENT_A* largs = create_merged_args(custom, -1, &lcount); + if (!largs) + return FALSE; + + if (argc > 0) + name = argv[0]; + + printf("\n"); + printf("FreeRDP - A Free Remote Desktop Protocol Implementation\n"); + printf("See www.freerdp.com for more information\n"); + printf("\n"); + printf("Usage: %s [file] [options] [/v:<server>[:port]]\n", argv[0]); + printf("\n"); + printf("Syntax:\n"); + printf(" /flag (enables flag)\n"); + printf(" /option:<value> (specifies option with value)\n"); + printf(" +toggle -toggle (enables or disables toggle, where '/' is a synonym of '+')\n"); + printf("\n"); + + freerdp_client_print_command_line_args(largs, lcount); + free(largs); + + printf("\n"); + printf("Examples:\n"); + printf(" %s connection.rdp /p:Pwd123! /f\n", name); + printf(" %s /u:CONTOSO\\JohnDoe /p:Pwd123! /v:rdp.contoso.com\n", name); + printf(" %s /u:JohnDoe /p:Pwd123! /w:1366 /h:768 /v:192.168.1.100:4489\n", name); + printf(" %s /u:JohnDoe /p:Pwd123! /vmconnect:C824F53E-95D2-46C6-9A18-23A5BB403532 " + "/v:192.168.1.100\n", + name); + printf(" %s /u:\\AzureAD\\user@corp.example /p:pwd /v:host\n", name); + printf("\n"); + printf("Clipboard Redirection: +clipboard\n"); + printf("\n"); + printf("Drive Redirection: /drive:home,/home/user\n"); + printf("Smartcard Redirection: /smartcard:<device>\n"); + printf("Smartcard logon with Kerberos authentication: /smartcard-logon /sec:nla\n"); + + printf("Serial Port Redirection: /serial:<name>,<device>,[SerCx2|SerCx|Serial],[permissive]\n"); + printf("Serial Port Redirection: /serial:COM1,/dev/ttyS0\n"); + printf("Parallel Port Redirection: /parallel:<name>,<device>\n"); + printf("Printer Redirection: /printer:<device>,<driver>,[default]\n"); + printf("TCP redirection: /rdp2tcp:/usr/bin/rdp2tcp\n"); + printf("\n"); + printf("Audio Output Redirection: /sound:sys:oss,dev:1,format:1\n"); + printf("Audio Output Redirection: /sound:sys:alsa\n"); + printf("Audio Input Redirection: /microphone:sys:oss,dev:1,format:1\n"); + printf("Audio Input Redirection: /microphone:sys:alsa\n"); + printf("\n"); + printf("Multimedia Redirection: /video\n"); +#ifdef CHANNEL_URBDRC_CLIENT + printf("USB Device Redirection: /usb:id:054c:0268#4669:6e6b,addr:04:0c\n"); +#endif + printf("\n"); + printf("For Gateways, the https_proxy environment variable is respected:\n"); +#ifdef _WIN32 + printf(" set HTTPS_PROXY=http://proxy.contoso.com:3128/\n"); +#else + printf(" export https_proxy=http://proxy.contoso.com:3128/\n"); +#endif + printf(" %s /g:rdp.contoso.com ...\n", name); + printf("\n"); + printf("More documentation is coming, in the meantime consult source files\n"); + printf("\n"); + return TRUE; +} + +static BOOL option_is_rdp_file(const char* option) +{ + WINPR_ASSERT(option); + + if (option_ends_with(option, ".rdp")) + return TRUE; + if (option_ends_with(option, ".rdpw")) + return TRUE; + return FALSE; +} + +static BOOL option_is_incident_file(const char* option) +{ + WINPR_ASSERT(option); + + if (option_ends_with(option, ".msrcIncident")) + return TRUE; + return FALSE; +} + +static int freerdp_client_command_line_pre_filter(void* context, int index, int argc, LPSTR* argv) +{ + if (index == 1) + { + size_t length = 0; + rdpSettings* settings = NULL; + + if (argc <= index) + return -1; + + length = strlen(argv[index]); + + if (length > 4) + { + if (option_is_rdp_file(argv[index])) + { + settings = (rdpSettings*)context; + + if (!freerdp_settings_set_string(settings, FreeRDP_ConnectionFile, argv[index])) + return COMMAND_LINE_ERROR_MEMORY; + + return 1; + } + } + + if (length > 13) + { + if (option_is_incident_file(argv[index])) + { + settings = (rdpSettings*)context; + + if (!freerdp_settings_set_string(settings, FreeRDP_AssistanceFile, argv[index])) + return COMMAND_LINE_ERROR_MEMORY; + + return 1; + } + } + } + + return 0; +} + +BOOL freerdp_client_add_device_channel(rdpSettings* settings, size_t count, const char** params) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(params); + WINPR_ASSERT(count > 0); + + if (option_equals(params[0], "drive")) + { + BOOL rc = 0; + if (count < 2) + return FALSE; + + if (!freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection, TRUE)) + return FALSE; + if (count < 3) + rc = freerdp_client_add_drive(settings, params[1], NULL); + else + rc = freerdp_client_add_drive(settings, params[2], params[1]); + + return rc; + } + else if (option_equals(params[0], "printer")) + { + RDPDR_DEVICE* printer = NULL; + + if (count < 1) + return FALSE; + + if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectPrinters, TRUE)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection, TRUE)) + return FALSE; + + printer = freerdp_device_new(RDPDR_DTYP_PRINT, count - 1, ¶ms[1]); + if (!printer) + return FALSE; + + if (!freerdp_device_collection_add(settings, printer)) + { + freerdp_device_free(printer); + return FALSE; + } + + return TRUE; + } + else if (option_equals(params[0], "smartcard")) + { + RDPDR_DEVICE* smartcard = NULL; + + if (count < 1) + return FALSE; + + if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectSmartCards, TRUE)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection, TRUE)) + return FALSE; + + smartcard = freerdp_device_new(RDPDR_DTYP_SMARTCARD, count - 1, ¶ms[1]); + + if (!smartcard) + return FALSE; + + if (!freerdp_device_collection_add(settings, smartcard)) + { + freerdp_device_free(smartcard); + return FALSE; + } + + return TRUE; + } + else if (option_equals(params[0], "serial")) + { + RDPDR_DEVICE* serial = NULL; + + if (count < 1) + return FALSE; + + if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectSerialPorts, TRUE)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection, TRUE)) + return FALSE; + + serial = freerdp_device_new(RDPDR_DTYP_SERIAL, count - 1, ¶ms[1]); + + if (!serial) + return FALSE; + + if (!freerdp_device_collection_add(settings, serial)) + { + freerdp_device_free(serial); + return FALSE; + } + + return TRUE; + } + else if (option_equals(params[0], "parallel")) + { + RDPDR_DEVICE* parallel = NULL; + + if (count < 1) + return FALSE; + + if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectParallelPorts, TRUE)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection, TRUE)) + return FALSE; + + parallel = freerdp_device_new(RDPDR_DTYP_PARALLEL, count - 1, ¶ms[1]); + + if (!parallel) + return FALSE; + + if (!freerdp_device_collection_add(settings, parallel)) + { + freerdp_device_free(parallel); + return FALSE; + } + + return TRUE; + } + + return FALSE; +} + +BOOL freerdp_client_del_static_channel(rdpSettings* settings, const char* name) +{ + return freerdp_static_channel_collection_del(settings, name); +} + +BOOL freerdp_client_add_static_channel(rdpSettings* settings, size_t count, const char** params) +{ + ADDIN_ARGV* _args = NULL; + + if (!settings || !params || !params[0] || (count > INT_MAX)) + return FALSE; + + if (freerdp_static_channel_collection_find(settings, params[0])) + return TRUE; + + _args = freerdp_addin_argv_new(count, (const char**)params); + + if (!_args) + return FALSE; + + if (!freerdp_static_channel_collection_add(settings, _args)) + goto fail; + + return TRUE; +fail: + freerdp_addin_argv_free(_args); + return FALSE; +} + +BOOL freerdp_client_del_dynamic_channel(rdpSettings* settings, const char* name) +{ + return freerdp_dynamic_channel_collection_del(settings, name); +} + +BOOL freerdp_client_add_dynamic_channel(rdpSettings* settings, size_t count, const char** params) +{ + ADDIN_ARGV* _args = NULL; + + if (!settings || !params || !params[0] || (count > INT_MAX)) + return FALSE; + + if (freerdp_dynamic_channel_collection_find(settings, params[0])) + return TRUE; + + _args = freerdp_addin_argv_new(count, params); + + if (!_args) + return FALSE; + + if (!freerdp_dynamic_channel_collection_add(settings, _args)) + goto fail; + + return TRUE; + +fail: + freerdp_addin_argv_free(_args); + return FALSE; +} + +static BOOL read_pem_file(rdpSettings* settings, FreeRDP_Settings_Keys_String id, const char* file) +{ + size_t length = 0; + char* pem = crypto_read_pem(file, &length); + if (!pem || (length == 0)) + return FALSE; + + BOOL rc = freerdp_settings_set_string_len(settings, id, pem, length); + free(pem); + return rc; +} + +/** @brief suboption type */ +typedef enum +{ + CMDLINE_SUBOPTION_STRING, + CMDLINE_SUBOPTION_FILE, +} CmdLineSubOptionType; + +typedef BOOL (*CmdLineSubOptionCb)(const char* value, rdpSettings* settings); +typedef struct +{ + const char* optname; + FreeRDP_Settings_Keys_String id; + CmdLineSubOptionType opttype; + CmdLineSubOptionCb cb; +} CmdLineSubOptions; + +static BOOL parseSubOptions(rdpSettings* settings, const CmdLineSubOptions* opts, size_t count, + const char* arg) +{ + BOOL found = FALSE; + + for (size_t xx = 0; xx < count; xx++) + { + const CmdLineSubOptions* opt = &opts[xx]; + + if (option_starts_with(opt->optname, arg)) + { + const size_t optlen = strlen(opt->optname); + const char* val = &arg[optlen]; + BOOL status = 0; + + switch (opt->opttype) + { + case CMDLINE_SUBOPTION_STRING: + status = freerdp_settings_set_string(settings, opt->id, val); + break; + case CMDLINE_SUBOPTION_FILE: + status = read_pem_file(settings, opt->id, val); + break; + default: + WLog_ERR(TAG, "invalid subOption type"); + return FALSE; + } + + if (!status) + return FALSE; + + if (opt->cb && !opt->cb(val, settings)) + return FALSE; + + found = TRUE; + break; + } + } + + if (!found) + WLog_ERR(TAG, "option %s not handled", arg); + + return found; +} + +static int freerdp_client_command_line_post_filter(void* context, COMMAND_LINE_ARGUMENT_A* arg) +{ + rdpSettings* settings = (rdpSettings*)context; + BOOL status = TRUE; + BOOL enable = arg->Value ? TRUE : FALSE; + union + { + char** p; + const char** pc; + } ptr; + + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "a") + { + size_t count = 0; + ptr.p = CommandLineParseCommaSeparatedValues(arg->Value, &count); + + if ((status = freerdp_client_add_device_channel(settings, count, ptr.pc))) + { + freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection, TRUE); + } + + free(ptr.p); + } + CommandLineSwitchCase(arg, "kerberos") + { + size_t count = 0; + + ptr.p = CommandLineParseCommaSeparatedValuesEx("kerberos", arg->Value, &count); + if (ptr.pc) + { + const CmdLineSubOptions opts[] = { + { "kdc-url:", FreeRDP_KerberosKdcUrl, CMDLINE_SUBOPTION_STRING, NULL }, + { "start-time:", FreeRDP_KerberosStartTime, CMDLINE_SUBOPTION_STRING, NULL }, + { "lifetime:", FreeRDP_KerberosLifeTime, CMDLINE_SUBOPTION_STRING, NULL }, + { "renewable-lifetime:", FreeRDP_KerberosRenewableLifeTime, + CMDLINE_SUBOPTION_STRING, NULL }, + { "cache:", FreeRDP_KerberosCache, CMDLINE_SUBOPTION_STRING, NULL }, + { "armor:", FreeRDP_KerberosArmor, CMDLINE_SUBOPTION_STRING, NULL }, + { "pkinit-anchors:", FreeRDP_PkinitAnchors, CMDLINE_SUBOPTION_STRING, NULL }, + { "pkcs11-module:", FreeRDP_Pkcs11Module, CMDLINE_SUBOPTION_STRING, NULL } + }; + + for (size_t x = 1; x < count; x++) + { + const char* cur = ptr.pc[x]; + if (!parseSubOptions(settings, opts, ARRAYSIZE(opts), cur)) + { + free(ptr.p); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + } + } + free(ptr.p); + } + + CommandLineSwitchCase(arg, "vc") + { + size_t count = 0; + ptr.p = CommandLineParseCommaSeparatedValues(arg->Value, &count); + status = freerdp_client_add_static_channel(settings, count, ptr.pc); + free(ptr.p); + } + CommandLineSwitchCase(arg, "dvc") + { + size_t count = 0; + ptr.p = CommandLineParseCommaSeparatedValues(arg->Value, &count); + status = freerdp_client_add_dynamic_channel(settings, count, ptr.pc); + free(ptr.p); + } + CommandLineSwitchCase(arg, "drive") + { + size_t count = 0; + ptr.p = CommandLineParseCommaSeparatedValuesEx(arg->Name, arg->Value, &count); + status = freerdp_client_add_device_channel(settings, count, ptr.pc); + free(ptr.p); + } + CommandLineSwitchCase(arg, "serial") + { + size_t count = 0; + ptr.p = CommandLineParseCommaSeparatedValuesEx(arg->Name, arg->Value, &count); + status = freerdp_client_add_device_channel(settings, count, ptr.pc); + free(ptr.p); + } + CommandLineSwitchCase(arg, "parallel") + { + size_t count = 0; + ptr.p = CommandLineParseCommaSeparatedValuesEx(arg->Name, arg->Value, &count); + status = freerdp_client_add_device_channel(settings, count, ptr.pc); + free(ptr.p); + } + CommandLineSwitchCase(arg, "smartcard") + { + size_t count = 0; + ptr.p = CommandLineParseCommaSeparatedValuesEx(arg->Name, arg->Value, &count); + status = freerdp_client_add_device_channel(settings, count, ptr.pc); + free(ptr.p); + } + CommandLineSwitchCase(arg, "printer") + { + size_t count = 0; + ptr.p = CommandLineParseCommaSeparatedValuesEx(arg->Name, arg->Value, &count); + status = freerdp_client_add_device_channel(settings, count, ptr.pc); + free(ptr.p); + } + CommandLineSwitchCase(arg, "usb") + { + size_t count = 0; + ptr.p = CommandLineParseCommaSeparatedValuesEx(URBDRC_CHANNEL_NAME, arg->Value, &count); + status = freerdp_client_add_dynamic_channel(settings, count, ptr.pc); + free(ptr.p); + } + CommandLineSwitchCase(arg, "multitouch") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_MultiTouchInput, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "gestures") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_MultiTouchGestures, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "echo") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportEchoChannel, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "ssh-agent") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportSSHAgentChannel, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "disp") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportDisplayControl, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "geometry") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportGeometryTracking, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "video") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportGeometryTracking, + enable)) /* this requires geometry tracking */ + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportVideoOptimized, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "sound") + { + size_t count = 0; + ptr.p = CommandLineParseCommaSeparatedValuesEx(RDPSND_CHANNEL_NAME, arg->Value, &count); + status = freerdp_client_add_static_channel(settings, count, ptr.pc); + if (status) + { + status = freerdp_client_add_dynamic_channel(settings, count, ptr.pc); + } + free(ptr.p); + } + CommandLineSwitchCase(arg, "microphone") + { + size_t count = 0; + ptr.p = CommandLineParseCommaSeparatedValuesEx(AUDIN_CHANNEL_NAME, arg->Value, &count); + status = freerdp_client_add_dynamic_channel(settings, count, ptr.pc); + free(ptr.p); + } +#if defined(CHANNEL_TSMF_CLIENT) + CommandLineSwitchCase(arg, "multimedia") + { + size_t count = 0; + ptr.p = CommandLineParseCommaSeparatedValuesEx("tsmf", arg->Value, &count); + status = freerdp_client_add_dynamic_channel(settings, count, ptr.pc); + free(ptr.p); + } +#endif + CommandLineSwitchCase(arg, "heartbeat") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportHeartbeatPdu, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "multitransport") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportMultitransport, enable)) + return COMMAND_LINE_ERROR; + + UINT32 flags = 0; + if (freerdp_settings_get_bool(settings, FreeRDP_SupportMultitransport)) + flags = + (TRANSPORT_TYPE_UDP_FECR | TRANSPORT_TYPE_UDP_FECL | TRANSPORT_TYPE_UDP_PREFERRED); + + if (!freerdp_settings_set_uint32(settings, FreeRDP_MultitransportFlags, flags)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchEnd(arg) + + return status + ? 1 + : -1; +} + +static BOOL freerdp_parse_username_ptr(const char* username, const char** user, size_t* userlen, + const char** domain, size_t* domainlen) +{ + const char* p = strchr(username, '\\'); + + *user = NULL; + *userlen = 0; + + *domain = NULL; + *domainlen = 0; + + if (p) + { + const size_t length = (size_t)(p - username); + *user = &p[1]; + *userlen = strlen(*user); + + *domain = username; + *domainlen = length; + } + else if (username) + { + /* Do not break up the name for '@'; both credSSP and the + * ClientInfo PDU expect 'user@corp.net' to be transmitted + * as username 'user@corp.net', domain empty (not NULL!). + */ + *user = username; + *userlen = strlen(username); + } + else + return FALSE; + + return TRUE; +} + +static BOOL freerdp_parse_username_settings(const char* username, rdpSettings* settings, + FreeRDP_Settings_Keys_String userID, + FreeRDP_Settings_Keys_String domainID) +{ + const char* user = NULL; + const char* domain = NULL; + size_t userlen = 0; + size_t domainlen = 0; + + const BOOL rc = freerdp_parse_username_ptr(username, &user, &userlen, &domain, &domainlen); + if (!rc) + return FALSE; + if (!freerdp_settings_set_string_len(settings, userID, user, userlen)) + return FALSE; + return freerdp_settings_set_string_len(settings, domainID, domain, domainlen); +} + +BOOL freerdp_parse_username(const char* username, char** puser, char** pdomain) +{ + const char* user = NULL; + const char* domain = NULL; + size_t userlen = 0; + size_t domainlen = 0; + + *puser = NULL; + *pdomain = NULL; + + const BOOL rc = freerdp_parse_username_ptr(username, &user, &userlen, &domain, &domainlen); + if (!rc) + return FALSE; + + if (userlen > 0) + { + *puser = strndup(user, userlen); + if (!*puser) + return FALSE; + } + + if (domainlen > 0) + { + *pdomain = strndup(domain, domainlen); + if (!*pdomain) + { + free(*puser); + *puser = NULL; + return FALSE; + } + } + + return TRUE; +} + +BOOL freerdp_parse_hostname(const char* hostname, char** host, int* port) +{ + char* p = NULL; + p = strrchr(hostname, ':'); + + if (p) + { + size_t length = (size_t)(p - hostname); + LONGLONG val = 0; + + if (!value_to_int(p + 1, &val, 1, UINT16_MAX)) + return FALSE; + + *host = (char*)calloc(length + 1UL, sizeof(char)); + + if (!(*host)) + return FALSE; + + CopyMemory(*host, hostname, length); + (*host)[length] = '\0'; + *port = (UINT16)val; + } + else + { + *host = _strdup(hostname); + + if (!(*host)) + return FALSE; + + *port = -1; + } + + return TRUE; +} + +static BOOL freerdp_apply_connection_type(rdpSettings* settings, UINT32 type) +{ + struct network_settings + { + FreeRDP_Settings_Keys_Bool id; + BOOL value[7]; + }; + const struct network_settings config[] = { + { FreeRDP_DisableWallpaper, { TRUE, TRUE, TRUE, TRUE, FALSE, FALSE, FALSE } }, + { FreeRDP_AllowFontSmoothing, { FALSE, FALSE, FALSE, FALSE, TRUE, TRUE, TRUE } }, + { FreeRDP_AllowDesktopComposition, { FALSE, FALSE, TRUE, TRUE, TRUE, TRUE, TRUE } }, + { FreeRDP_DisableFullWindowDrag, { TRUE, TRUE, TRUE, TRUE, FALSE, FALSE, FALSE } }, + { FreeRDP_DisableMenuAnims, { TRUE, TRUE, TRUE, TRUE, FALSE, FALSE, FALSE } }, + { FreeRDP_DisableThemes, { TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE } }, + { FreeRDP_NetworkAutoDetect, { FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE } } + }; + + switch (type) + { + case CONNECTION_TYPE_MODEM: + case CONNECTION_TYPE_BROADBAND_LOW: + case CONNECTION_TYPE_BROADBAND_HIGH: + case CONNECTION_TYPE_SATELLITE: + case CONNECTION_TYPE_WAN: + case CONNECTION_TYPE_LAN: + case CONNECTION_TYPE_AUTODETECT: + break; + default: + WLog_WARN(TAG, "Invalid ConnectionType %" PRIu32 ", aborting", type); + return FALSE; + } + + for (size_t x = 0; x < ARRAYSIZE(config); x++) + { + const struct network_settings* cur = &config[x]; + if (!freerdp_settings_set_bool(settings, cur->id, cur->value[type - 1])) + return FALSE; + } + return TRUE; +} + +BOOL freerdp_set_connection_type(rdpSettings* settings, UINT32 type) +{ + + if (!freerdp_settings_set_uint32(settings, FreeRDP_ConnectionType, type)) + return FALSE; + + switch (type) + { + case CONNECTION_TYPE_MODEM: + if (!freerdp_apply_connection_type(settings, type)) + return FALSE; + break; + case CONNECTION_TYPE_BROADBAND_LOW: + if (!freerdp_apply_connection_type(settings, type)) + return FALSE; + break; + case CONNECTION_TYPE_SATELLITE: + if (!freerdp_apply_connection_type(settings, type)) + return FALSE; + break; + case CONNECTION_TYPE_BROADBAND_HIGH: + if (!freerdp_apply_connection_type(settings, type)) + return FALSE; + break; + case CONNECTION_TYPE_WAN: + if (!freerdp_apply_connection_type(settings, type)) + return FALSE; + break; + case CONNECTION_TYPE_LAN: + if (!freerdp_apply_connection_type(settings, type)) + return FALSE; + break; + case CONNECTION_TYPE_AUTODETECT: + if (!freerdp_apply_connection_type(settings, type)) + return FALSE; + /* Automatically activate GFX and RFX codec support */ +#ifdef WITH_GFX_H264 + if (!freerdp_settings_set_bool(settings, FreeRDP_GfxAVC444v2, TRUE) || + !freerdp_settings_set_bool(settings, FreeRDP_GfxAVC444, TRUE) || + !freerdp_settings_set_bool(settings, FreeRDP_GfxH264, TRUE)) + return FALSE; +#endif + if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteFxCodec, TRUE) || + !freerdp_settings_set_bool(settings, FreeRDP_SupportGraphicsPipeline, TRUE)) + return FALSE; + break; + default: + return FALSE; + } + + return TRUE; +} + +static UINT32 freerdp_get_keyboard_layout_for_type(const char* name, DWORD type) +{ + size_t count = 0; + RDP_KEYBOARD_LAYOUT* layouts = + freerdp_keyboard_get_layouts(RDP_KEYBOARD_LAYOUT_TYPE_STANDARD, &count); + + if (!layouts || (count == 0)) + return FALSE; + + for (size_t x = 0; x < count; x++) + { + const RDP_KEYBOARD_LAYOUT* layout = &layouts[x]; + if (option_equals(layout->name, name)) + { + return layout->code; + } + } + + freerdp_keyboard_layouts_free(layouts, count); + return 0; +} + +static UINT32 freerdp_map_keyboard_layout_name_to_id(const char* name) +{ + const UINT32 variants[] = { RDP_KEYBOARD_LAYOUT_TYPE_STANDARD, RDP_KEYBOARD_LAYOUT_TYPE_VARIANT, + RDP_KEYBOARD_LAYOUT_TYPE_IME }; + + for (size_t x = 0; x < ARRAYSIZE(variants); x++) + { + UINT32 rc = freerdp_get_keyboard_layout_for_type(name, variants[x]); + if (rc > 0) + return rc; + } + + return 0; +} + +static int freerdp_detect_command_line_pre_filter(void* context, int index, int argc, LPSTR* argv) +{ + size_t length = 0; + WINPR_UNUSED(context); + + if (index == 1) + { + if (argc < index) + return -1; + + length = strlen(argv[index]); + + if (length > 4) + { + if (option_is_rdp_file(argv[index])) + { + return 1; + } + } + + if (length > 13) + { + if (option_is_incident_file(argv[index])) + { + return 1; + } + } + } + + return 0; +} + +static int freerdp_detect_windows_style_command_line_syntax(int argc, char** argv, size_t* count, + BOOL ignoreUnknown) +{ + int status = 0; + DWORD flags = 0; + int detect_status = 0; + const COMMAND_LINE_ARGUMENT_A* arg = NULL; + COMMAND_LINE_ARGUMENT_A largs[ARRAYSIZE(global_cmd_args)]; + memcpy(largs, global_cmd_args, sizeof(global_cmd_args)); + + flags = COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_SILENCE_PARSER; + flags |= COMMAND_LINE_SIGIL_SLASH | COMMAND_LINE_SIGIL_PLUS_MINUS; + + if (ignoreUnknown) + { + flags |= COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + } + + *count = 0; + detect_status = 0; + CommandLineClearArgumentsA(largs); + status = CommandLineParseArgumentsA(argc, argv, largs, flags, NULL, + freerdp_detect_command_line_pre_filter, NULL); + + if (status < 0) + return status; + + arg = largs; + + do + { + if (!(arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT)) + continue; + + (*count)++; + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + if ((status <= COMMAND_LINE_ERROR) && (status >= COMMAND_LINE_ERROR_LAST)) + detect_status = -1; + + return detect_status; +} + +static int freerdp_detect_posix_style_command_line_syntax(int argc, char** argv, size_t* count, + BOOL ignoreUnknown) +{ + int status = 0; + DWORD flags = 0; + int detect_status = 0; + const COMMAND_LINE_ARGUMENT_A* arg = NULL; + COMMAND_LINE_ARGUMENT_A largs[ARRAYSIZE(global_cmd_args)]; + memcpy(largs, global_cmd_args, sizeof(global_cmd_args)); + + flags = COMMAND_LINE_SEPARATOR_SPACE | COMMAND_LINE_SILENCE_PARSER; + flags |= COMMAND_LINE_SIGIL_DASH | COMMAND_LINE_SIGIL_DOUBLE_DASH; + flags |= COMMAND_LINE_SIGIL_ENABLE_DISABLE; + + if (ignoreUnknown) + { + flags |= COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + } + + *count = 0; + detect_status = 0; + CommandLineClearArgumentsA(largs); + status = CommandLineParseArgumentsA(argc, argv, largs, flags, NULL, + freerdp_detect_command_line_pre_filter, NULL); + + if (status < 0) + return status; + + arg = largs; + + do + { + if (!(arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT)) + continue; + + (*count)++; + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + if ((status <= COMMAND_LINE_ERROR) && (status >= COMMAND_LINE_ERROR_LAST)) + detect_status = -1; + + return detect_status; +} + +static BOOL freerdp_client_detect_command_line(int argc, char** argv, DWORD* flags) +{ + int posix_cli_status = 0; + size_t posix_cli_count = 0; + int windows_cli_status = 0; + size_t windows_cli_count = 0; + const BOOL ignoreUnknown = TRUE; + windows_cli_status = freerdp_detect_windows_style_command_line_syntax( + argc, argv, &windows_cli_count, ignoreUnknown); + posix_cli_status = + freerdp_detect_posix_style_command_line_syntax(argc, argv, &posix_cli_count, ignoreUnknown); + + /* Default is POSIX syntax */ + *flags = COMMAND_LINE_SEPARATOR_SPACE; + *flags |= COMMAND_LINE_SIGIL_DASH | COMMAND_LINE_SIGIL_DOUBLE_DASH; + *flags |= COMMAND_LINE_SIGIL_ENABLE_DISABLE; + + if (posix_cli_status <= COMMAND_LINE_STATUS_PRINT) + return FALSE; + + /* Check, if this may be windows style syntax... */ + if ((windows_cli_count && (windows_cli_count >= posix_cli_count)) || + (windows_cli_status <= COMMAND_LINE_STATUS_PRINT)) + { + windows_cli_count = 1; + *flags = COMMAND_LINE_SEPARATOR_COLON; + *flags |= COMMAND_LINE_SIGIL_SLASH | COMMAND_LINE_SIGIL_PLUS_MINUS; + } + + WLog_DBG(TAG, "windows: %d/%" PRIuz " posix: %d/%" PRIuz "", windows_cli_status, + windows_cli_count, posix_cli_status, posix_cli_count); + if ((posix_cli_count == 0) && (windows_cli_count == 0)) + { + if ((posix_cli_status == COMMAND_LINE_ERROR) && (windows_cli_status == COMMAND_LINE_ERROR)) + return TRUE; + } + return FALSE; +} + +int freerdp_client_settings_command_line_status_print(rdpSettings* settings, int status, int argc, + char** argv) +{ + return freerdp_client_settings_command_line_status_print_ex(settings, status, argc, argv, NULL); +} + +static void freerdp_client_print_keyboard_type_list(const char* msg, DWORD type) +{ + size_t count = 0; + RDP_KEYBOARD_LAYOUT* layouts = NULL; + layouts = freerdp_keyboard_get_layouts(type, &count); + + printf("\n%s\n", msg); + + for (size_t x = 0; x < count; x++) + { + const RDP_KEYBOARD_LAYOUT* layout = &layouts[x]; + printf("0x%08" PRIX32 "\t%s\n", layout->code, layout->name); + } + + freerdp_keyboard_layouts_free(layouts, count); +} + +static void freerdp_client_print_keyboard_list(void) +{ + freerdp_client_print_keyboard_type_list("Keyboard Layouts", RDP_KEYBOARD_LAYOUT_TYPE_STANDARD); + freerdp_client_print_keyboard_type_list("Keyboard Layout Variants", + RDP_KEYBOARD_LAYOUT_TYPE_VARIANT); + freerdp_client_print_keyboard_type_list("Keyboard Layout Variants", + RDP_KEYBOARD_LAYOUT_TYPE_IME); +} + +static void freerdp_client_print_tune_list(const rdpSettings* settings) +{ + SSIZE_T type = 0; + + printf("%s\t%50s\t%s\t%s", "<index>", "<key>", "<type>", "<default value>\n"); + for (size_t x = 0; x < FreeRDP_Settings_StableAPI_MAX; x++) + { + const char* name = freerdp_settings_get_name_for_key(x); + type = freerdp_settings_get_type_for_key(x); + + switch (type) + { + case RDP_SETTINGS_TYPE_BOOL: + printf("%" PRIuz "\t%50s\tBOOL\t%s\n", x, name, + freerdp_settings_get_bool(settings, (FreeRDP_Settings_Keys_Bool)x) + ? "TRUE" + : "FALSE"); + break; + case RDP_SETTINGS_TYPE_UINT16: + printf("%" PRIuz "\t%50s\tUINT16\t%" PRIu16 "\n", x, name, + freerdp_settings_get_uint16(settings, (FreeRDP_Settings_Keys_UInt16)x)); + break; + case RDP_SETTINGS_TYPE_INT16: + printf("%" PRIuz "\t%50s\tINT16\t%" PRId16 "\n", x, name, + freerdp_settings_get_int16(settings, (FreeRDP_Settings_Keys_Int16)x)); + break; + case RDP_SETTINGS_TYPE_UINT32: + printf("%" PRIuz "\t%50s\tUINT32\t%" PRIu32 "\n", x, name, + freerdp_settings_get_uint32(settings, (FreeRDP_Settings_Keys_UInt32)x)); + break; + case RDP_SETTINGS_TYPE_INT32: + printf("%" PRIuz "\t%50s\tINT32\t%" PRId32 "\n", x, name, + freerdp_settings_get_int32(settings, (FreeRDP_Settings_Keys_Int32)x)); + break; + case RDP_SETTINGS_TYPE_UINT64: + printf("%" PRIuz "\t%50s\tUINT64\t%" PRIu64 "\n", x, name, + freerdp_settings_get_uint64(settings, (FreeRDP_Settings_Keys_UInt64)x)); + break; + case RDP_SETTINGS_TYPE_INT64: + printf("%" PRIuz "\t%50s\tINT64\t%" PRId64 "\n", x, name, + freerdp_settings_get_int64(settings, (FreeRDP_Settings_Keys_Int64)x)); + break; + case RDP_SETTINGS_TYPE_STRING: + printf("%" PRIuz "\t%50s\tSTRING\t%s" + "\n", + x, name, + freerdp_settings_get_string(settings, (FreeRDP_Settings_Keys_String)x)); + break; + case RDP_SETTINGS_TYPE_POINTER: + printf("%" PRIuz "\t%50s\tPOINTER\t%p" + "\n", + x, name, + freerdp_settings_get_pointer(settings, (FreeRDP_Settings_Keys_Pointer)x)); + break; + default: + break; + } + } +} + +int freerdp_client_settings_command_line_status_print_ex(rdpSettings* settings, int status, + int argc, char** argv, + const COMMAND_LINE_ARGUMENT_A* custom) +{ + const COMMAND_LINE_ARGUMENT_A* arg = NULL; + COMMAND_LINE_ARGUMENT_A largs[ARRAYSIZE(global_cmd_args)]; + memcpy(largs, global_cmd_args, sizeof(global_cmd_args)); + + if (status == COMMAND_LINE_STATUS_PRINT_VERSION) + { + freerdp_client_print_version(); + goto out; + } + + if (status == COMMAND_LINE_STATUS_PRINT_BUILDCONFIG) + { + freerdp_client_print_version(); + freerdp_client_print_buildconfig(); + goto out; + } + else if (status == COMMAND_LINE_STATUS_PRINT) + { + CommandLineParseArgumentsA(argc, argv, largs, 0x112, NULL, NULL, NULL); + + arg = CommandLineFindArgumentA(largs, "list"); + WINPR_ASSERT(arg); + + if (arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT) + { + if (option_equals("tune", arg->Value)) + freerdp_client_print_tune_list(settings); + else if (option_equals("kbd", arg->Value)) + freerdp_client_print_keyboard_list(); + else if (option_starts_with("kbd-lang", arg->Value)) + { + const char* val = NULL; + if (option_starts_with("kbd-lang:", arg->Value)) + val = &arg->Value[9]; + else if (!option_equals("kbd-lang", arg->Value)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (val && strchr(val, ',')) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + freerdp_client_print_codepages(val); + } + else if (option_equals("kbd-scancode", arg->Value)) + freerdp_client_print_scancodes(); + else if (option_equals("monitor", arg->Value)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_ListMonitors, TRUE)) + return COMMAND_LINE_ERROR; + } + else if (option_starts_with("smartcard", arg->Value)) + { + BOOL opts = FALSE; + if (option_starts_with("smartcard:", arg->Value)) + opts = TRUE; + else if (!option_equals("smartcard", arg->Value)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (opts) + { + const char* sub = strchr(arg->Value, ':') + 1; + const CmdLineSubOptions options[] = { + { "pkinit-anchors:", FreeRDP_PkinitAnchors, CMDLINE_SUBOPTION_STRING, + NULL }, + { "pkcs11-module:", FreeRDP_Pkcs11Module, CMDLINE_SUBOPTION_STRING, NULL } + }; + + size_t count = 0; + + char** ptr = CommandLineParseCommaSeparatedValuesEx("smartcard", sub, &count); + if (!ptr) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + if (count < 2) + { + free(ptr); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + for (size_t x = 1; x < count; x++) + { + const char* cur = ptr[x]; + if (!parseSubOptions(settings, options, ARRAYSIZE(options), cur)) + { + free(ptr); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + } + + free(ptr); + } + + freerdp_smartcard_list(settings); + } + else + { + freerdp_client_print_command_line_help_ex(argc, argv, custom); + return COMMAND_LINE_ERROR; + } + } +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + arg = CommandLineFindArgumentA(largs, "tune-list"); + WINPR_ASSERT(arg); + + if (arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT) + { + WLog_WARN(TAG, "Option /tune-list is deprecated, use /list:tune instead"); + freerdp_client_print_tune_list(settings); + } + + arg = CommandLineFindArgumentA(largs, "kbd-lang-list"); + WINPR_ASSERT(arg); + + if (arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT) + { + WLog_WARN(TAG, "Option /kbd-lang-list is deprecated, use /list:kbd-lang instead"); + freerdp_client_print_codepages(arg->Value); + } + + arg = CommandLineFindArgumentA(largs, "kbd-list"); + WINPR_ASSERT(arg); + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + WLog_WARN(TAG, "Option /kbd-list is deprecated, use /list:kbd instead"); + freerdp_client_print_keyboard_list(); + } + + arg = CommandLineFindArgumentA(largs, "monitor-list"); + WINPR_ASSERT(arg); + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + WLog_WARN(TAG, "Option /monitor-list is deprecated, use /list:monitor instead"); + if (!freerdp_settings_set_bool(settings, FreeRDP_ListMonitors, TRUE)) + return COMMAND_LINE_ERROR; + } + + arg = CommandLineFindArgumentA(largs, "smartcard-list"); + WINPR_ASSERT(arg); + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + WLog_WARN(TAG, "Option /smartcard-list is deprecated, use /list:smartcard instead"); + freerdp_smartcard_list(settings); + } + + arg = CommandLineFindArgumentA(largs, "kbd-scancode-list"); + WINPR_ASSERT(arg); + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + WLog_WARN(TAG, + "Option /kbd-scancode-list is deprecated, use /list:kbd-scancode instead"); + freerdp_client_print_scancodes(); + goto out; + } +#endif + goto out; + } + else if (status < 0) + { + freerdp_client_print_command_line_help_ex(argc, argv, custom); + goto out; + } + +out: + if (status <= COMMAND_LINE_STATUS_PRINT && status >= COMMAND_LINE_STATUS_PRINT_LAST) + return 0; + return status; +} + +/** + * parses a string value with the format <v1>x<v2> + * + * @param input input string + * @param v1 pointer to output v1 + * @param v2 pointer to output v2 + * @return if the parsing was successful + */ +static BOOL parseSizeValue(const char* input, unsigned long* v1, unsigned long* v2) +{ + const char* xcharpos = NULL; + char* endPtr = NULL; + unsigned long v = 0; + errno = 0; + v = strtoul(input, &endPtr, 10); + + if ((v == 0 || v == ULONG_MAX) && (errno != 0)) + return FALSE; + + if (v1) + *v1 = v; + + xcharpos = strchr(input, 'x'); + + if (!xcharpos || xcharpos != endPtr) + return FALSE; + + errno = 0; + v = strtoul(xcharpos + 1, &endPtr, 10); + + if ((v == 0 || v == ULONG_MAX) && (errno != 0)) + return FALSE; + + if (*endPtr != '\0') + return FALSE; + + if (v2) + *v2 = v; + + return TRUE; +} + +static BOOL prepare_default_settings(rdpSettings* settings, COMMAND_LINE_ARGUMENT_A* args, + BOOL rdp_file) +{ + const char* arguments[] = { "network", "gfx", "rfx", "bpp" }; + WINPR_ASSERT(settings); + WINPR_ASSERT(args); + + if (rdp_file) + return FALSE; + + for (size_t x = 0; x < ARRAYSIZE(arguments); x++) + { + const char* arg = arguments[x]; + const COMMAND_LINE_ARGUMENT_A* p = CommandLineFindArgumentA(args, arg); + if (p && (p->Flags & COMMAND_LINE_ARGUMENT_PRESENT)) + return FALSE; + } + + return freerdp_set_connection_type(settings, CONNECTION_TYPE_AUTODETECT); +} + +static BOOL setSmartcardEmulation(const char* value, rdpSettings* settings) +{ + return freerdp_settings_set_bool(settings, FreeRDP_SmartcardEmulation, TRUE); +} + +const char* option_starts_with(const char* what, const char* val) +{ + WINPR_ASSERT(what); + WINPR_ASSERT(val); + const size_t wlen = strlen(what); + + if (_strnicmp(what, val, wlen) != 0) + return NULL; + return &val[wlen]; +} + +BOOL option_ends_with(const char* str, const char* ext) +{ + WINPR_ASSERT(str); + WINPR_ASSERT(ext); + const size_t strLen = strlen(str); + const size_t extLen = strlen(ext); + + if (strLen < extLen) + return FALSE; + + return _strnicmp(&str[strLen - extLen], ext, extLen) == 0; +} + +BOOL option_equals(const char* what, const char* val) +{ + WINPR_ASSERT(what); + WINPR_ASSERT(val); + return _stricmp(what, val) == 0; +} + +typedef enum +{ + PARSE_ON, + PARSE_OFF, + PARSE_NONE, + PARSE_FAIL +} PARSE_ON_OFF_RESULT; + +static PARSE_ON_OFF_RESULT parse_on_off_option(const char* value) +{ + WINPR_ASSERT(value); + const char* sep = strchr(value, ':'); + if (!sep) + return PARSE_NONE; + if (option_equals("on", &sep[1])) + return PARSE_ON; + if (option_equals("off", &sep[1])) + return PARSE_OFF; + return PARSE_FAIL; +} + +typedef enum +{ + CLIP_DIR_PARSE_ALL, + CLIP_DIR_PARSE_OFF, + CLIP_DIR_PARSE_LOCAL, + CLIP_DIR_PARSE_REMOTE, + CLIP_DIR_PARSE_FAIL +} PARSE_CLIP_DIR_RESULT; + +static PARSE_CLIP_DIR_RESULT parse_clip_direciton_to_option(const char* value) +{ + WINPR_ASSERT(value); + const char* sep = strchr(value, ':'); + if (!sep) + return CLIP_DIR_PARSE_FAIL; + if (option_equals("all", &sep[1])) + return CLIP_DIR_PARSE_ALL; + if (option_equals("off", &sep[1])) + return CLIP_DIR_PARSE_OFF; + if (option_equals("local", &sep[1])) + return CLIP_DIR_PARSE_LOCAL; + if (option_equals("remote", &sep[1])) + return CLIP_DIR_PARSE_REMOTE; + return CLIP_DIR_PARSE_FAIL; +} + +static int parse_tls_ciphers(rdpSettings* settings, const char* Value) +{ + const char* ciphers = NULL; + if (!Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (option_equals(Value, "netmon")) + { + ciphers = "ALL:!ECDH:!ADH:!DHE"; + } + else if (option_equals(Value, "ma")) + { + ciphers = "AES128-SHA"; + } + else + { + ciphers = Value; + } + + if (!freerdp_settings_set_string(settings, FreeRDP_AllowedTlsCiphers, ciphers)) + return COMMAND_LINE_ERROR_MEMORY; + return 0; +} + +static int parse_tls_seclevel(rdpSettings* settings, const char* Value) +{ + LONGLONG val = 0; + + if (!value_to_int(Value, &val, 0, 5)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_TlsSecLevel, (UINT32)val)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + return 0; +} + +static int parse_tls_secrets_file(rdpSettings* settings, const char* Value) +{ + if (!Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_string(settings, FreeRDP_TlsSecretsFile, Value)) + return COMMAND_LINE_ERROR_MEMORY; + return 0; +} + +static int parse_tls_enforce(rdpSettings* settings, const char* Value) +{ + UINT16 version = TLS1_2_VERSION; + + if (Value) + { + struct map_t + { + const char* name; + UINT16 version; + }; + const struct map_t map[] = { + { "1.0", TLS1_VERSION }, + { "1.1", TLS1_1_VERSION }, + { "1.2", TLS1_2_VERSION } +#if defined(TLS1_3_VERSION) + , + { "1.3", TLS1_3_VERSION } +#endif + }; + + for (size_t x = 0; x < ARRAYSIZE(map); x++) + { + const struct map_t* cur = &map[x]; + if (option_equals(cur->name, Value)) + { + version = cur->version; + break; + } + } + } + + if (!(freerdp_settings_set_uint16(settings, FreeRDP_TLSMinVersion, version) && + freerdp_settings_set_uint16(settings, FreeRDP_TLSMaxVersion, version))) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + return 0; +} + +static int parse_tls_cipher_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + int rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "tls") + { + if (option_starts_with("ciphers:", arg->Value)) + rc = parse_tls_ciphers(settings, &arg->Value[8]); + else if (option_starts_with("seclevel:", arg->Value)) + rc = parse_tls_seclevel(settings, &arg->Value[9]); + else if (option_starts_with("secrets-file:", arg->Value)) + rc = parse_tls_secrets_file(settings, &arg->Value[13]); + else if (option_starts_with("enforce:", arg->Value)) + rc = parse_tls_enforce(settings, &arg->Value[8]); + } + +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + CommandLineSwitchCase(arg, "tls-ciphers") + { + WLog_WARN(TAG, "Option /tls-ciphers is deprecated, use /tls:ciphers instead"); + rc = parse_tls_ciphers(settings, arg->Value); + } + CommandLineSwitchCase(arg, "tls-seclevel") + { + WLog_WARN(TAG, "Option /tls-seclevel is deprecated, use /tls:seclevel instead"); + rc = parse_tls_seclevel(settings, arg->Value); + } + CommandLineSwitchCase(arg, "tls-secrets-file") + { + WLog_WARN(TAG, "Option /tls-secrets-file is deprecated, use /tls:secrets-file instead"); + rc = parse_tls_secrets_file(settings, arg->Value); + } + CommandLineSwitchCase(arg, "enforce-tlsv1_2") + { + WLog_WARN(TAG, "Option /enforce-tlsv1_2 is deprecated, use /tls:enforce:1.2 instead"); + rc = parse_tls_enforce(settings, "1.2"); + } +#endif + CommandLineSwitchDefault(arg) + { + } + CommandLineSwitchEnd(arg) + + return rc; +} + +static int parse_tls_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + size_t count = 0; + char** ptr = CommandLineParseCommaSeparatedValues(arg->Value, &count); + for (size_t x = 0; x < count; x++) + { + COMMAND_LINE_ARGUMENT_A larg = *arg; + larg.Value = ptr[x]; + + int rc = parse_tls_cipher_options(settings, &larg); + if (rc != 0) + { + free(ptr); + return rc; + } + } + free(ptr); + return 0; +} + +static int parse_gfx_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportGraphicsPipeline, TRUE)) + return COMMAND_LINE_ERROR; + + if (arg->Value) + { + int rc = CHANNEL_RC_OK; + size_t count = 0; + char** ptr = CommandLineParseCommaSeparatedValues(arg->Value, &count); + if (!ptr || (count == 0)) + rc = COMMAND_LINE_ERROR; + else + { + BOOL GfxH264 = FALSE; + BOOL GfxAVC444 = FALSE; + BOOL RemoteFxCodec = FALSE; + BOOL GfxProgressive = FALSE; + BOOL codecSelected = FALSE; + + for (size_t x = 0; x < count; x++) + { + const char* val = ptr[x]; +#ifdef WITH_GFX_H264 + if (option_starts_with("AVC444", val)) + { + const PARSE_ON_OFF_RESULT bval = parse_on_off_option(val); + if (bval == PARSE_FAIL) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + else + GfxAVC444 = bval != PARSE_OFF; + codecSelected = TRUE; + } + else if (option_starts_with("AVC420", val)) + { + const PARSE_ON_OFF_RESULT bval = parse_on_off_option(val); + if (bval == PARSE_FAIL) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + else + GfxH264 = bval != PARSE_OFF; + codecSelected = TRUE; + } + else +#endif + if (option_starts_with("RFX", val)) + { + const PARSE_ON_OFF_RESULT bval = parse_on_off_option(val); + if (bval == PARSE_FAIL) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + else + RemoteFxCodec = bval != PARSE_OFF; + codecSelected = TRUE; + } + else if (option_starts_with("progressive", val)) + { + const PARSE_ON_OFF_RESULT bval = parse_on_off_option(val); + if (bval == PARSE_FAIL) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + else + GfxProgressive = bval != PARSE_OFF; + codecSelected = TRUE; + } + else if (option_starts_with("mask:", val)) + { + ULONGLONG v = 0; + const char* uv = &val[5]; + if (!value_to_uint(uv, &v, 0, UINT32_MAX)) + rc = COMMAND_LINE_ERROR; + else + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_GfxCapsFilter, v)) + rc = COMMAND_LINE_ERROR; + } + } + else if (option_starts_with("small-cache", val)) + { + const PARSE_ON_OFF_RESULT bval = parse_on_off_option(val); + if (bval == PARSE_FAIL) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + else if (!freerdp_settings_set_bool(settings, FreeRDP_GfxSmallCache, + bval != PARSE_OFF)) + rc = COMMAND_LINE_ERROR; + } + else if (option_starts_with("thin-client", val)) + { + const PARSE_ON_OFF_RESULT bval = parse_on_off_option(val); + if (bval == PARSE_FAIL) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + else if (!freerdp_settings_set_bool(settings, FreeRDP_GfxThinClient, + bval != PARSE_OFF)) + rc = COMMAND_LINE_ERROR; + if ((rc == CHANNEL_RC_OK) && (bval > 0)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_GfxSmallCache, + bval != PARSE_OFF)) + rc = COMMAND_LINE_ERROR; + } + } + } + + if ((rc == CHANNEL_RC_OK) && codecSelected) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_GfxAVC444, GfxAVC444)) + rc = COMMAND_LINE_ERROR; + if (!freerdp_settings_set_bool(settings, FreeRDP_GfxAVC444v2, GfxAVC444)) + rc = COMMAND_LINE_ERROR; + if (!freerdp_settings_set_bool(settings, FreeRDP_GfxH264, GfxH264)) + rc = COMMAND_LINE_ERROR; + if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteFxCodec, RemoteFxCodec)) + rc = COMMAND_LINE_ERROR; + if (!freerdp_settings_set_bool(settings, FreeRDP_GfxProgressive, GfxProgressive)) + rc = COMMAND_LINE_ERROR; + } + } + free(ptr); + if (rc != CHANNEL_RC_OK) + return rc; + } + return CHANNEL_RC_OK; +} + +static int parse_kbd_layout(rdpSettings* settings, const char* value) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(value); + + int rc = 0; + LONGLONG ival = 0; + const BOOL isInt = value_to_int(value, &ival, 1, UINT32_MAX); + if (!isInt) + { + ival = freerdp_map_keyboard_layout_name_to_id(value); + + if (ival == 0) + { + WLog_ERR(TAG, "Could not identify keyboard layout: %s", value); + WLog_ERR(TAG, "Use /list:kbd to list available layouts"); + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + } + + if (rc == 0) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_KeyboardLayout, (UINT32)ival)) + rc = COMMAND_LINE_ERROR; + } + return rc; +} + +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) +static int parse_codec_cache_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + if (!freerdp_settings_set_bool(settings, FreeRDP_BitmapCacheV3Enabled, TRUE)) + return COMMAND_LINE_ERROR; + + if (option_equals(arg->Value, "rfx")) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteFxCodec, TRUE)) + return COMMAND_LINE_ERROR; + } + else if (option_equals(arg->Value, "nsc")) + { + freerdp_settings_set_bool(settings, FreeRDP_NSCodec, TRUE); + } + +#if defined(WITH_JPEG) + else if (option_equals(arg->Value, "jpeg")) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_JpegCodec, TRUE)) + return COMMAND_LINE_ERROR; + + if (freerdp_settings_get_uint32(settings, FreeRDP_JpegQuality) == 0) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_JpegQuality, 75)) + return COMMAND_LINE_ERROR; + } + } + +#endif +} +#endif + +static BOOL check_kbd_remap_valid(const char* token) +{ + DWORD key = 0; + DWORD value = 0; + + WINPR_ASSERT(token); + /* The remapping is only allowed for scancodes, so maximum is 999=999 */ + if (strlen(token) > 10) + return FALSE; + + int rc = sscanf(token, "%" PRIu32 "=%" PRIu32, &key, &value); + if (rc != 2) + rc = sscanf(token, "%" PRIx32 "=%" PRIx32 "", &key, &value); + if (rc != 2) + rc = sscanf(token, "%" PRIu32 "=%" PRIx32, &key, &value); + if (rc != 2) + rc = sscanf(token, "%" PRIx32 "=%" PRIu32, &key, &value); + if (rc != 2) + { + WLog_WARN(TAG, "/kbd:remap invalid entry '%s'", token); + return FALSE; + } + return TRUE; +} + +static int parse_host_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + freerdp_settings_set_string(settings, FreeRDP_ServerHostname, NULL); + char* p = strchr(arg->Value, '['); + + /* ipv4 */ + if (!p) + { + const char scheme[] = "://"; + const char* val = strstr(arg->Value, scheme); + if (val) + val += strnlen(scheme, sizeof(scheme)); + else + val = arg->Value; + p = strchr(val, ':'); + + if (p) + { + LONGLONG val = 0; + size_t length = 0; + + if (!value_to_int(&p[1], &val, 1, UINT16_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + length = (size_t)(p - arg->Value); + if (!freerdp_settings_set_uint32(settings, FreeRDP_ServerPort, val)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + if (!freerdp_settings_set_string_len(settings, FreeRDP_ServerHostname, arg->Value, + length)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + else + { + if (!freerdp_settings_set_string(settings, FreeRDP_ServerHostname, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + } + else /* ipv6 */ + { + size_t length = 0; + char* p2 = strchr(arg->Value, ']'); + + /* not a valid [] ipv6 addr found */ + if (!p2) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + length = (size_t)(p2 - p); + if (!freerdp_settings_set_string_len(settings, FreeRDP_ServerHostname, p + 1, length - 1)) + return COMMAND_LINE_ERROR_MEMORY; + + if (*(p2 + 1) == ':') + { + LONGLONG val = 0; + + if (!value_to_int(&p2[2], &val, 0, UINT16_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_ServerPort, (UINT16)val)) + return COMMAND_LINE_ERROR; + } + + printf("hostname %s port %" PRIu32 "\n", + freerdp_settings_get_string(settings, FreeRDP_ServerHostname), + freerdp_settings_get_uint32(settings, FreeRDP_ServerPort)); + } + return 0; +} + +static int parse_redirect_prefer_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + size_t count = 0; + char* cur = arg->Value; + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_RedirectionPreferType, 0)) + return COMMAND_LINE_ERROR; + + UINT32 value = 0; + do + { + UINT32 mask = 0; + char* next = strchr(cur, ','); + + if (next) + { + *next = '\0'; + next++; + } + + if (option_equals("fqdn", cur)) + mask = 0x06U; + else if (option_equals("ip", cur)) + mask = 0x05U; + else if (option_equals("netbios", cur)) + mask = 0x03U; + else + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + cur = next; + mask = (mask & 0x07); + value |= mask << (count * 3); + count++; + } while (cur != NULL); + + if (!freerdp_settings_set_uint32(settings, FreeRDP_RedirectionPreferType, value)) + return COMMAND_LINE_ERROR; + + if (count > 3) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + return 0; +} + +static int parse_prevent_session_lock_options(rdpSettings* settings, + const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + if (!freerdp_settings_set_uint32(settings, FreeRDP_FakeMouseMotionInterval, 180)) + return COMMAND_LINE_ERROR_MEMORY; + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, 1, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_FakeMouseMotionInterval, (UINT32)val)) + return COMMAND_LINE_ERROR_MEMORY; + } + + return 0; +} + +static int parse_vmconnect_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + if (!freerdp_settings_set_bool(settings, FreeRDP_VmConnectMode, TRUE)) + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_uint32(settings, FreeRDP_ServerPort, 2179)) + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_bool(settings, FreeRDP_NegotiateSecurityLayer, FALSE)) + return COMMAND_LINE_ERROR; + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SendPreconnectionPdu, TRUE)) + return COMMAND_LINE_ERROR; + + if (!freerdp_settings_set_string(settings, FreeRDP_PreconnectionBlob, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + return 0; +} + +static int parse_size_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + int status = 0; + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + char* p = strchr(arg->Value, 'x'); + + if (p) + { + unsigned long w = 0; + unsigned long h = 0; + + if (!parseSizeValue(arg->Value, &w, &h) || (w > UINT16_MAX) || (h > UINT16_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, (UINT32)w)) + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, (UINT32)h)) + return COMMAND_LINE_ERROR; + } + else + { + char* str = _strdup(arg->Value); + if (!str) + return COMMAND_LINE_ERROR_MEMORY; + + p = strchr(str, '%'); + + if (p) + { + BOOL partial = FALSE; + + status = COMMAND_LINE_ERROR; + if (strchr(p, 'w')) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_PercentScreenUseWidth, TRUE)) + goto fail; + partial = TRUE; + } + + if (strchr(p, 'h')) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_PercentScreenUseHeight, TRUE)) + goto fail; + partial = TRUE; + } + + if (!partial) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_PercentScreenUseWidth, TRUE)) + goto fail; + if (!freerdp_settings_set_bool(settings, FreeRDP_PercentScreenUseHeight, TRUE)) + goto fail; + } + + *p = '\0'; + { + LONGLONG val = 0; + + if (!value_to_int(str, &val, 0, 100)) + { + status = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + goto fail; + } + + if (!freerdp_settings_set_uint32(settings, FreeRDP_PercentScreen, (UINT32)val)) + goto fail; + } + + status = 0; + } + + fail: + free(str); + } + + return status; +} + +static int parse_monitors_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + union + { + char** p; + const char** pc; + } ptr; + size_t count = 0; + UINT32* MonitorIds = NULL; + ptr.p = CommandLineParseCommaSeparatedValues(arg->Value, &count); + + if (!ptr.pc) + return COMMAND_LINE_ERROR_MEMORY; + + if (count > 16) + count = 16; + + if (!freerdp_settings_set_pointer_len(settings, FreeRDP_MonitorIds, NULL, count)) + { + free(ptr.p); + return FALSE; + } + + MonitorIds = freerdp_settings_get_pointer_array_writable(settings, FreeRDP_MonitorIds, 0); + for (UINT32 i = 0; i < count; i++) + { + LONGLONG val = 0; + + if (!value_to_int(ptr.pc[i], &val, 0, UINT16_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + MonitorIds[i] = (UINT32)val; + } + + free(ptr.p); + } + + return 0; +} + +static int parse_dynamic_resolution_options(rdpSettings* settings, + const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + if (freerdp_settings_get_bool(settings, FreeRDP_SmartSizing)) + { + WLog_ERR(TAG, "Smart sizing and dynamic resolution are mutually exclusive options"); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportDisplayControl, TRUE)) + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_bool(settings, FreeRDP_DynamicResolutionUpdate, TRUE)) + return COMMAND_LINE_ERROR; + + return 0; +} + +static int parse_smart_sizing_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + if (freerdp_settings_get_bool(settings, FreeRDP_DynamicResolutionUpdate)) + { + WLog_ERR(TAG, "Smart sizing and dynamic resolution are mutually exclusive options"); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + if (!freerdp_settings_set_bool(settings, FreeRDP_SmartSizing, TRUE)) + return COMMAND_LINE_ERROR; + + if (arg->Value) + { + unsigned long w = 0; + unsigned long h = 0; + + if (!parseSizeValue(arg->Value, &w, &h) || (w > UINT16_MAX) || (h > UINT16_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_SmartSizingWidth, (UINT32)w)) + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_uint32(settings, FreeRDP_SmartSizingHeight, (UINT32)h)) + return COMMAND_LINE_ERROR; + } + return 0; +} + +static int parse_bpp_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + switch (val) + { + case 32: + case 24: + case 16: + case 15: + case 8: + if (!freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth, (UINT32)val)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + break; + + default: + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + return 0; +} + +static int parse_kbd_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + int rc = CHANNEL_RC_OK; + size_t count = 0; + char** ptr = CommandLineParseCommaSeparatedValues(arg->Value, &count); + if (!ptr || (count == 0)) + rc = COMMAND_LINE_ERROR; + else + { + for (size_t x = 0; x < count; x++) + { + const char* val = ptr[x]; + + if (option_starts_with("remap:", val)) + { + /* Append this new occurance to the already existing list */ + char* now = _strdup(&val[6]); + const char* old = + freerdp_settings_get_string(settings, FreeRDP_KeyboardRemappingList); + + /* Basic sanity test. Entries must be like <key>=<value>, e.g. 1=2 */ + if (!check_kbd_remap_valid(now)) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + else if (old) + { + const size_t olen = strlen(old); + const size_t alen = strlen(now); + const size_t tlen = olen + alen + 2; + char* tmp = calloc(tlen, sizeof(char)); + if (!tmp) + rc = COMMAND_LINE_ERROR_MEMORY; + else + _snprintf(tmp, tlen, "%s,%s", old, now); + free(now); + now = tmp; + } + + if (rc == 0) + { + if (!freerdp_settings_set_string(settings, FreeRDP_KeyboardRemappingList, now)) + rc = COMMAND_LINE_ERROR; + } + free(now); + } + else if (option_starts_with("layout:", val)) + { + rc = parse_kbd_layout(settings, &val[7]); + } + else if (option_starts_with("lang:", val)) + { + LONGLONG ival = 0; + const BOOL isInt = value_to_int(&val[5], &ival, 1, UINT32_MAX); + if (!isInt) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + else if (!freerdp_settings_set_uint32(settings, FreeRDP_KeyboardCodePage, + (UINT32)ival)) + rc = COMMAND_LINE_ERROR; + } + else if (option_starts_with("type:", val)) + { + LONGLONG ival = 0; + const BOOL isInt = value_to_int(&val[5], &ival, 1, UINT32_MAX); + if (!isInt) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + else if (!freerdp_settings_set_uint32(settings, FreeRDP_KeyboardType, (UINT32)ival)) + rc = COMMAND_LINE_ERROR; + } + else if (option_starts_with("subtype:", val)) + { + LONGLONG ival = 0; + const BOOL isInt = value_to_int(&val[8], &ival, 1, UINT32_MAX); + if (!isInt) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + else if (!freerdp_settings_set_uint32(settings, FreeRDP_KeyboardSubType, + (UINT32)ival)) + rc = COMMAND_LINE_ERROR; + } + else if (option_starts_with("fn-key:", val)) + { + LONGLONG ival = 0; + const BOOL isInt = value_to_int(&val[7], &ival, 1, UINT32_MAX); + if (!isInt) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + else if (!freerdp_settings_set_uint32(settings, FreeRDP_KeyboardFunctionKey, + (UINT32)ival)) + rc = COMMAND_LINE_ERROR; + } + else if (option_starts_with("unicode", val)) + { + const PARSE_ON_OFF_RESULT bval = parse_on_off_option(val); + if (bval == PARSE_FAIL) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + else if (!freerdp_settings_set_bool(settings, FreeRDP_UnicodeInput, + bval != PARSE_OFF)) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + else if (option_starts_with("pipe:", val)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_UnicodeInput, TRUE)) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + else if (!freerdp_settings_set_string(settings, FreeRDP_KeyboardPipeName, &val[5])) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + else if (count == 1) + { + /* Legacy, allow /kbd:<value> for setting keyboard layout */ + rc = parse_kbd_layout(settings, val); + } +#endif + else + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (rc != 0) + break; + } + } + free(ptr); + return rc; +} + +static int parse_proxy_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + /* initial value */ + if (!freerdp_settings_set_uint32(settings, FreeRDP_ProxyType, PROXY_TYPE_HTTP)) + return COMMAND_LINE_ERROR_MEMORY; + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + const char* cur = arg->Value; + + if (!cur) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + /* value is [scheme://][user:password@]hostname:port */ + if (!proxy_parse_uri(settings, cur)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + else + { + WLog_ERR(TAG, "Option http-proxy needs argument."); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + return 0; +} + +static int parse_dump_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + BOOL failed = FALSE; + size_t count = 0; + char** args = CommandLineParseCommaSeparatedValues(arg->Value, &count); + if (!args || (count != 2)) + failed = TRUE; + else + { + if (!freerdp_settings_set_string(settings, FreeRDP_TransportDumpFile, args[1])) + failed = TRUE; + else if (option_equals(args[0], "replay")) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_TransportDump, FALSE)) + failed = TRUE; + else if (!freerdp_settings_set_bool(settings, FreeRDP_TransportDumpReplay, TRUE)) + failed = TRUE; + } + else if (option_equals(args[0], "record")) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_TransportDump, TRUE)) + failed = TRUE; + else if (!freerdp_settings_set_bool(settings, FreeRDP_TransportDumpReplay, FALSE)) + failed = TRUE; + } + else + { + failed = TRUE; + } + } + free(args); + if (failed) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + return 0; +} + +static int parse_clipboard_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + if (arg->Value == BoolValueTrue || arg->Value == BoolValueFalse) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectClipboard, + (arg->Value == BoolValueTrue))) + return COMMAND_LINE_ERROR; + } + else + { + int rc = 0; + union + { + char** p; + const char** pc; + } ptr; + size_t count = 0; + ptr.p = CommandLineParseCommaSeparatedValues(arg->Value, &count); + for (size_t x = 0; (x < count) && (rc == 0); x++) + { + const char* usesel = "use-selection:"; + + const char* cur = ptr.pc[x]; + if (option_starts_with(usesel, cur)) + { + const char* val = &cur[strlen(usesel)]; + if (!freerdp_settings_set_string(settings, FreeRDP_ClipboardUseSelection, val)) + rc = COMMAND_LINE_ERROR_MEMORY; + if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectClipboard, TRUE)) + return COMMAND_LINE_ERROR; + } + else if (option_starts_with("direction-to", cur)) + { + const UINT32 mask = + freerdp_settings_get_uint32(settings, FreeRDP_ClipboardFeatureMask) & + ~(CLIPRDR_FLAG_LOCAL_TO_REMOTE | CLIPRDR_FLAG_REMOTE_TO_LOCAL); + const PARSE_CLIP_DIR_RESULT bval = parse_clip_direciton_to_option(cur); + UINT32 bflags = 0; + switch (bval) + { + case CLIP_DIR_PARSE_ALL: + bflags |= CLIPRDR_FLAG_LOCAL_TO_REMOTE | CLIPRDR_FLAG_REMOTE_TO_LOCAL; + break; + case CLIP_DIR_PARSE_LOCAL: + bflags |= CLIPRDR_FLAG_REMOTE_TO_LOCAL; + break; + case CLIP_DIR_PARSE_REMOTE: + bflags |= CLIPRDR_FLAG_LOCAL_TO_REMOTE; + break; + case CLIP_DIR_PARSE_OFF: + break; + case CLIP_DIR_PARSE_FAIL: + default: + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + break; + } + + if (!freerdp_settings_set_uint32(settings, FreeRDP_ClipboardFeatureMask, + mask | bflags)) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + else if (option_starts_with("files-to", cur)) + { + const UINT32 mask = + freerdp_settings_get_uint32(settings, FreeRDP_ClipboardFeatureMask) & + ~(CLIPRDR_FLAG_LOCAL_TO_REMOTE_FILES | CLIPRDR_FLAG_REMOTE_TO_LOCAL_FILES); + const PARSE_CLIP_DIR_RESULT bval = parse_clip_direciton_to_option(cur); + UINT32 bflags = 0; + switch (bval) + { + case CLIP_DIR_PARSE_ALL: + bflags |= + CLIPRDR_FLAG_LOCAL_TO_REMOTE_FILES | CLIPRDR_FLAG_REMOTE_TO_LOCAL_FILES; + break; + case CLIP_DIR_PARSE_LOCAL: + bflags |= CLIPRDR_FLAG_REMOTE_TO_LOCAL_FILES; + break; + case CLIP_DIR_PARSE_REMOTE: + bflags |= CLIPRDR_FLAG_LOCAL_TO_REMOTE_FILES; + break; + case CLIP_DIR_PARSE_OFF: + break; + case CLIP_DIR_PARSE_FAIL: + default: + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + break; + } + + if (!freerdp_settings_set_uint32(settings, FreeRDP_ClipboardFeatureMask, + mask | bflags)) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + else + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + free(ptr.p); + + if (rc) + return rc; + } + return 0; +} + +static int parse_audio_mode_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + switch (val) + { + case AUDIO_MODE_REDIRECT: + if (!freerdp_settings_set_bool(settings, FreeRDP_AudioPlayback, TRUE)) + return COMMAND_LINE_ERROR; + break; + + case AUDIO_MODE_PLAY_ON_SERVER: + if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteConsoleAudio, TRUE)) + return COMMAND_LINE_ERROR; + break; + + case AUDIO_MODE_NONE: + if (!freerdp_settings_set_bool(settings, FreeRDP_AudioPlayback, FALSE)) + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteConsoleAudio, FALSE)) + return COMMAND_LINE_ERROR; + break; + + default: + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + return 0; +} + +static int parse_network_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + UINT32 type = 0; + + if (option_equals(arg->Value, "modem")) + type = CONNECTION_TYPE_MODEM; + else if (option_equals(arg->Value, "broadband")) + type = CONNECTION_TYPE_BROADBAND_HIGH; + else if (option_equals(arg->Value, "broadband-low")) + type = CONNECTION_TYPE_BROADBAND_LOW; + else if (option_equals(arg->Value, "broadband-high")) + type = CONNECTION_TYPE_BROADBAND_HIGH; + else if (option_equals(arg->Value, "wan")) + type = CONNECTION_TYPE_WAN; + else if (option_equals(arg->Value, "lan")) + type = CONNECTION_TYPE_LAN; + else if ((option_equals(arg->Value, "autodetect")) || (option_equals(arg->Value, "auto")) || + (option_equals(arg->Value, "detect"))) + { + type = CONNECTION_TYPE_AUTODETECT; + } + else + { + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, 1, 7)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + type = (UINT32)val; + } + + if (!freerdp_set_connection_type(settings, type)) + return COMMAND_LINE_ERROR; + return 0; +} + +static int parse_sec_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + size_t count = 0; + char** ptr = CommandLineParseCommaSeparatedValues(arg->Value, &count); + if (count == 0) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + FreeRDP_Settings_Keys_Bool singleOptionWithoutOnOff = FreeRDP_BOOL_UNUSED; + for (size_t x = 0; x < count; x++) + { + const char* cur = ptr[x]; + const PARSE_ON_OFF_RESULT bval = parse_on_off_option(cur); + if (bval == PARSE_FAIL) + { + free(ptr); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + const BOOL val = bval != PARSE_OFF; + FreeRDP_Settings_Keys_Bool id = FreeRDP_BOOL_UNUSED; + if (option_starts_with("rdp", cur)) /* Standard RDP */ + { + id = FreeRDP_RdpSecurity; + if (!freerdp_settings_set_bool(settings, FreeRDP_UseRdpSecurityLayer, val)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + else if (option_starts_with("tls", cur)) /* TLS */ + id = FreeRDP_TlsSecurity; + else if (option_starts_with("nla", cur)) /* NLA */ + id = FreeRDP_NlaSecurity; + else if (option_starts_with("ext", cur)) /* NLA Extended */ + id = FreeRDP_ExtSecurity; + else if (option_equals("aad", cur)) /* RDSAAD */ + id = FreeRDP_AadSecurity; + else + { + WLog_ERR(TAG, "unknown protocol security: %s", arg->Value); + free(ptr); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + if ((bval == PARSE_NONE) && (count == 1)) + singleOptionWithoutOnOff = id; + if (!freerdp_settings_set_bool(settings, id, val)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + if (singleOptionWithoutOnOff != FreeRDP_BOOL_UNUSED) + { + const FreeRDP_Settings_Keys_Bool options[] = { FreeRDP_AadSecurity, + FreeRDP_UseRdpSecurityLayer, + FreeRDP_RdpSecurity, FreeRDP_NlaSecurity, + FreeRDP_TlsSecurity }; + + for (size_t i = 0; i < ARRAYSIZE(options); i++) + { + if (!freerdp_settings_set_bool(settings, options[i], FALSE)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + if (!freerdp_settings_set_bool(settings, singleOptionWithoutOnOff, TRUE)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + if (singleOptionWithoutOnOff == FreeRDP_RdpSecurity) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_UseRdpSecurityLayer, TRUE)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + } + free(ptr); + return 0; +} + +static int parse_encryption_methods_options(rdpSettings* settings, + const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + union + { + char** p; + const char** pc; + } ptr; + size_t count = 0; + ptr.p = CommandLineParseCommaSeparatedValues(arg->Value, &count); + + UINT32 EncryptionMethods = 0; + for (UINT32 i = 0; i < count; i++) + { + if (option_equals(ptr.pc[i], "40")) + EncryptionMethods |= ENCRYPTION_METHOD_40BIT; + else if (option_equals(ptr.pc[i], "56")) + EncryptionMethods |= ENCRYPTION_METHOD_56BIT; + else if (option_equals(ptr.pc[i], "128")) + EncryptionMethods |= ENCRYPTION_METHOD_128BIT; + else if (option_equals(ptr.pc[i], "FIPS")) + EncryptionMethods |= ENCRYPTION_METHOD_FIPS; + else + WLog_ERR(TAG, "unknown encryption method '%s'", ptr.pc[i]); + } + + if (!freerdp_settings_set_uint32(settings, FreeRDP_EncryptionMethods, EncryptionMethods)) + return COMMAND_LINE_ERROR; + free(ptr.p); + } + return 0; +} + +static int parse_cert_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + int rc = 0; + union + { + char** p; + const char** pc; + } ptr; + size_t count = 0; + ptr.p = CommandLineParseCommaSeparatedValues(arg->Value, &count); + for (size_t x = 0; (x < count) && (rc == 0); x++) + { + const char deny[] = "deny"; + const char ignore[] = "ignore"; + const char tofu[] = "tofu"; + const char name[] = "name:"; + const char fingerprints[] = "fingerprint:"; + + const char* cur = ptr.pc[x]; + if (option_equals(deny, cur)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_AutoDenyCertificate, TRUE)) + return COMMAND_LINE_ERROR; + } + else if (option_equals(ignore, cur)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_IgnoreCertificate, TRUE)) + return COMMAND_LINE_ERROR; + } + else if (option_equals(tofu, cur)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_AutoAcceptCertificate, TRUE)) + return COMMAND_LINE_ERROR; + } + else if (option_starts_with(name, cur)) + { + const char* val = &cur[strnlen(name, sizeof(name))]; + if (!freerdp_settings_set_string(settings, FreeRDP_CertificateName, val)) + rc = COMMAND_LINE_ERROR_MEMORY; + } + else if (option_starts_with(fingerprints, cur)) + { + const char* val = &cur[strnlen(fingerprints, sizeof(fingerprints))]; + if (!freerdp_settings_append_string(settings, FreeRDP_CertificateAcceptedFingerprints, + ",", val)) + rc = COMMAND_LINE_ERROR_MEMORY; + } + else + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + free(ptr.p); + + return rc; +} + +static int parse_mouse_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + size_t count = 0; + char** ptr = CommandLineParseCommaSeparatedValuesEx("mouse", arg->Value, &count); + UINT rc = 0; + if (ptr) + { + for (size_t x = 1; x < count; x++) + { + const char* cur = ptr[x]; + + const PARSE_ON_OFF_RESULT bval = parse_on_off_option(cur); + if (bval == PARSE_FAIL) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + else + { + const BOOL val = bval != PARSE_OFF; + + if (option_starts_with("relative", cur)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_MouseUseRelativeMove, val)) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + else if (option_starts_with("grab", cur)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_GrabMouse, val)) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + } + + if (rc != 0) + break; + } + } + free(ptr); + + return rc; +} + +static int parse_floatbar_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + /* Defaults are enabled, visible, sticky, fullscreen */ + UINT32 Floatbar = 0x0017; + + if (arg->Value) + { + char* start = arg->Value; + + do + { + char* cur = start; + start = strchr(start, ','); + + if (start) + { + *start = '\0'; + start = start + 1; + } + + /* sticky:[on|off] */ + if (option_starts_with("sticky:", cur)) + { + Floatbar &= ~0x02u; + + const PARSE_ON_OFF_RESULT bval = parse_on_off_option(cur); + switch (bval) + { + case PARSE_ON: + case PARSE_NONE: + Floatbar |= 0x02u; + break; + case PARSE_OFF: + Floatbar &= ~0x02u; + break; + case PARSE_FAIL: + default: + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + } + /* default:[visible|hidden] */ + else if (option_starts_with("default:", cur)) + { + const char* val = cur + 8; + Floatbar &= ~0x04u; + + if (option_equals("visible", val)) + Floatbar |= 0x04u; + else if (option_equals("hidden", val)) + Floatbar &= ~0x04u; + else + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + /* show:[always|fullscreen|window] */ + else if (option_starts_with("show:", cur)) + { + const char* val = cur + 5; + Floatbar &= ~0x30u; + + if (option_equals("always", val)) + Floatbar |= 0x30u; + else if (option_equals("fullscreen", val)) + Floatbar |= 0x10u; + else if (option_equals("window", val)) + Floatbar |= 0x20u; + else + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + else + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } while (start); + } + if (!freerdp_settings_set_uint32(settings, FreeRDP_Floatbar, Floatbar)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + return 0; +} + +static int parse_reconnect_cookie_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + BYTE* base64 = NULL; + size_t length = 0; + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + crypto_base64_decode((const char*)(arg->Value), strlen(arg->Value), &base64, &length); + + if ((base64 != NULL) && (length == sizeof(ARC_SC_PRIVATE_PACKET))) + { + if (!freerdp_settings_set_pointer_len(settings, FreeRDP_ServerAutoReconnectCookie, base64, + 1)) + return COMMAND_LINE_ERROR; + } + else + { + WLog_ERR(TAG, "reconnect-cookie: invalid base64 '%s'", arg->Value); + } + + free(base64); + return 0; +} + +static int parse_scale_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, 100, 180)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + switch (val) + { + case 100: + case 140: + case 180: + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopScaleFactor, (UINT32)val)) + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_uint32(settings, FreeRDP_DeviceScaleFactor, (UINT32)val)) + return COMMAND_LINE_ERROR; + break; + + default: + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + return 0; +} + +static int parse_scale_device_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, 100, 180)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + switch (val) + { + case 100: + case 140: + case 180: + if (!freerdp_settings_set_uint32(settings, FreeRDP_DeviceScaleFactor, (UINT32)val)) + return COMMAND_LINE_ERROR; + break; + + default: + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + return 0; +} + +static int parse_smartcard_logon_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + size_t count = 0; + union + { + char** p; + const char** pc; + } ptr; + + if (!freerdp_settings_set_bool(settings, FreeRDP_SmartcardLogon, TRUE)) + return COMMAND_LINE_ERROR; + + ptr.p = CommandLineParseCommaSeparatedValuesEx("smartcard-logon", arg->Value, &count); + if (ptr.pc) + { + const CmdLineSubOptions opts[] = { + { "cert:", FreeRDP_SmartcardCertificate, CMDLINE_SUBOPTION_FILE, + setSmartcardEmulation }, + { "key:", FreeRDP_SmartcardPrivateKey, CMDLINE_SUBOPTION_FILE, setSmartcardEmulation }, + { "pin:", FreeRDP_Password, CMDLINE_SUBOPTION_STRING, NULL }, + { "csp:", FreeRDP_CspName, CMDLINE_SUBOPTION_STRING, NULL }, + { "reader:", FreeRDP_ReaderName, CMDLINE_SUBOPTION_STRING, NULL }, + { "card:", FreeRDP_CardName, CMDLINE_SUBOPTION_STRING, NULL }, + { "container:", FreeRDP_ContainerName, CMDLINE_SUBOPTION_STRING, NULL } + }; + + for (size_t x = 1; x < count; x++) + { + const char* cur = ptr.pc[x]; + if (!parseSubOptions(settings, opts, ARRAYSIZE(opts), cur)) + { + free(ptr.p); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + } + } + free(ptr.p); + return 0; +} + +static int parse_tune_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + size_t count = 0; + union + { + char** p; + const char** pc; + } ptr; + ptr.p = CommandLineParseCommaSeparatedValuesEx("tune", arg->Value, &count); + if (!ptr.pc) + return COMMAND_LINE_ERROR; + for (size_t x = 1; x < count; x++) + { + const char* cur = ptr.pc[x]; + char* sep = strchr(cur, ':'); + if (!sep) + { + free(ptr.p); + return COMMAND_LINE_ERROR; + } + *sep++ = '\0'; + if (!freerdp_settings_set_value_for_name(settings, cur, sep)) + { + free(ptr.p); + return COMMAND_LINE_ERROR; + } + } + + free(ptr.p); + return 0; +} + +static int parse_app_option_program(rdpSettings* settings, const char* cmd) +{ + const FreeRDP_Settings_Keys_Bool ids[] = { FreeRDP_RemoteApplicationMode, + FreeRDP_RemoteAppLanguageBarSupported, + FreeRDP_Workarea, FreeRDP_DisableWallpaper, + FreeRDP_DisableFullWindowDrag }; + + if (!freerdp_settings_set_string(settings, FreeRDP_RemoteApplicationProgram, cmd)) + return COMMAND_LINE_ERROR_MEMORY; + + for (size_t y = 0; y < ARRAYSIZE(ids); y++) + { + if (!freerdp_settings_set_bool(settings, ids[y], TRUE)) + return COMMAND_LINE_ERROR; + } + return CHANNEL_RC_OK; +} + +static int parse_app_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + int rc = CHANNEL_RC_OK; + size_t count = 0; + char** ptr = CommandLineParseCommaSeparatedValues(arg->Value, &count); + if (!ptr || (count == 0)) + rc = COMMAND_LINE_ERROR; + else + { + struct app_map + { + const char* name; + FreeRDP_Settings_Keys_String id; + int (*fkt)(rdpSettings* settings, const char* value); + }; + const struct app_map amap[] = { + { "program:", FreeRDP_RemoteApplicationProgram, parse_app_option_program }, + { "workdir:", FreeRDP_RemoteApplicationWorkingDir, NULL }, + { "name:", FreeRDP_RemoteApplicationName, NULL }, + { "icon:", FreeRDP_RemoteApplicationIcon, NULL }, + { "cmd:", FreeRDP_RemoteApplicationCmdLine, NULL }, + { "file:", FreeRDP_RemoteApplicationFile, NULL }, + { "guid:", FreeRDP_RemoteApplicationGuid, NULL }, + }; + for (size_t x = 0; x < count; x++) + { + BOOL handled = FALSE; + const char* val = ptr[x]; + + for (size_t y = 0; y < ARRAYSIZE(amap); y++) + { + const struct app_map* cur = &amap[y]; + if (option_starts_with(cur->name, val)) + { + const char* xval = &val[strlen(cur->name)]; + if (cur->fkt) + rc = cur->fkt(settings, xval); + else if (!freerdp_settings_set_string(settings, cur->id, xval)) + rc = COMMAND_LINE_ERROR_MEMORY; + + handled = TRUE; + break; + } + } + +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + if (!handled && (count == 1)) + { + /* Legacy path, allow /app:command and /app:||command syntax */ + rc = parse_app_option_program(settings, val); + } + else +#endif + if (!handled) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (rc != 0) + break; + } + } + + free(ptr); + return rc; +} + +static int parse_cache_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + int rc = CHANNEL_RC_OK; + size_t count = 0; + char** ptr = CommandLineParseCommaSeparatedValues(arg->Value, &count); + if (!ptr || (count == 0)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + for (size_t x = 0; x < count; x++) + { + const char* val = ptr[x]; + + if (option_starts_with("codec:", val)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_BitmapCacheV3Enabled, TRUE)) + rc = COMMAND_LINE_ERROR; + else if (option_equals(arg->Value, "rfx")) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteFxCodec, TRUE)) + rc = COMMAND_LINE_ERROR; + } + else if (option_equals(arg->Value, "nsc")) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_NSCodec, TRUE)) + rc = COMMAND_LINE_ERROR; + } + +#if defined(WITH_JPEG) + else if (option_equals(arg->Value, "jpeg")) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_JpegCodec, TRUE)) + rc = COMMAND_LINE_ERROR; + + if (freerdp_settings_get_uint32(settings, FreeRDP_JpegQuality) == 0) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_JpegQuality, 75)) + return COMMAND_LINE_ERROR; + } + } + +#endif + } + else if (option_starts_with("persist-file:", val)) + { + + if (!freerdp_settings_set_string(settings, FreeRDP_BitmapCachePersistFile, &val[13])) + rc = COMMAND_LINE_ERROR_MEMORY; + else if (!freerdp_settings_set_bool(settings, FreeRDP_BitmapCachePersistEnabled, TRUE)) + rc = COMMAND_LINE_ERROR; + } + else + { + const PARSE_ON_OFF_RESULT bval = parse_on_off_option(val); + if (bval == PARSE_FAIL) + rc = COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + else + { + if (option_starts_with("bitmap", val)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_BitmapCacheEnabled, + bval != PARSE_OFF)) + rc = COMMAND_LINE_ERROR; + } + else if (option_starts_with("glyph", val)) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_GlyphSupportLevel, + bval != PARSE_OFF ? GLYPH_SUPPORT_FULL + : GLYPH_SUPPORT_NONE)) + rc = COMMAND_LINE_ERROR; + } + else if (option_starts_with("persist", val)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_BitmapCachePersistEnabled, + bval != PARSE_OFF)) + rc = COMMAND_LINE_ERROR; + } + else if (option_starts_with("offscreen", val)) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_OffscreenSupportLevel, + bval != PARSE_OFF)) + rc = COMMAND_LINE_ERROR; + } + } + } + } + + free(ptr); + return rc; +} + +static BOOL parse_gateway_host_option(rdpSettings* settings, const char* host) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(host); + + char* name = NULL; + int port = -1; + if (!freerdp_parse_hostname(host, &name, &port)) + return FALSE; + const BOOL rc = freerdp_settings_set_string(settings, FreeRDP_GatewayHostname, name); + free(name); + if (!rc) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_GatewayUseSameCredentials, TRUE)) + return FALSE; + if (!freerdp_set_gateway_usage_method(settings, TSC_PROXY_MODE_DIRECT)) + return FALSE; + + return TRUE; +} + +static BOOL parse_gateway_cred_option(rdpSettings* settings, const char* value, + FreeRDP_Settings_Keys_String what) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(value); + + switch (what) + { + case FreeRDP_GatewayUsername: + if (!freerdp_parse_username_settings(value, settings, FreeRDP_GatewayUsername, + FreeRDP_GatewayDomain)) + return FALSE; + break; + default: + if (!freerdp_settings_set_string(settings, what, value)) + return FALSE; + break; + } + + return freerdp_settings_set_bool(settings, FreeRDP_GatewayUseSameCredentials, FALSE); +} + +static BOOL parse_gateway_type_option(rdpSettings* settings, const char* value) +{ + WINPR_ASSERT(settings); + WINPR_ASSERT(value); + + if (option_equals(value, "rpc")) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_GatewayRpcTransport, TRUE) || + !freerdp_settings_set_bool(settings, FreeRDP_GatewayHttpTransport, FALSE) || + !freerdp_settings_set_bool(settings, FreeRDP_GatewayHttpUseWebsockets, FALSE) || + !freerdp_settings_set_bool(settings, FreeRDP_GatewayArmTransport, FALSE)) + return FALSE; + } + else + { + if (option_equals(value, "http")) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_GatewayRpcTransport, FALSE) || + !freerdp_settings_set_bool(settings, FreeRDP_GatewayHttpTransport, TRUE) || + !freerdp_settings_set_bool(settings, FreeRDP_GatewayArmTransport, FALSE)) + return FALSE; + } + else if (option_equals(value, "auto")) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_GatewayRpcTransport, TRUE) || + !freerdp_settings_set_bool(settings, FreeRDP_GatewayHttpTransport, TRUE) || + !freerdp_settings_set_bool(settings, FreeRDP_GatewayArmTransport, FALSE)) + return FALSE; + } + else if (option_equals(value, "arm")) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_GatewayRpcTransport, FALSE) || + !freerdp_settings_set_bool(settings, FreeRDP_GatewayHttpTransport, FALSE) || + !freerdp_settings_set_bool(settings, FreeRDP_GatewayHttpUseWebsockets, FALSE) || + !freerdp_settings_set_bool(settings, FreeRDP_GatewayArmTransport, TRUE)) + return FALSE; + } + } + return TRUE; +} + +static BOOL parse_gateway_usage_option(rdpSettings* settings, const char* value) +{ + UINT32 type = 0; + + WINPR_ASSERT(settings); + WINPR_ASSERT(value); + + if (option_equals(value, "none")) + type = TSC_PROXY_MODE_NONE_DIRECT; + else if (option_equals(value, "direct")) + type = TSC_PROXY_MODE_DIRECT; + else if (option_equals(value, "detect")) + type = TSC_PROXY_MODE_DETECT; + else if (option_equals(value, "default")) + type = TSC_PROXY_MODE_DEFAULT; + else + { + LONGLONG val = 0; + + if (!value_to_int(value, &val, TSC_PROXY_MODE_NONE_DIRECT, TSC_PROXY_MODE_NONE_DETECT)) + return FALSE; + } + + return freerdp_set_gateway_usage_method(settings, type); +} + +static BOOL parse_gateway_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) +{ + BOOL rc = FALSE; + + WINPR_ASSERT(settings); + WINPR_ASSERT(arg); + + size_t count = 0; + char** args = CommandLineParseCommaSeparatedValues(arg->Value, &count); + if (count == 0) + return TRUE; + WINPR_ASSERT(args); + + if (!freerdp_settings_set_bool(settings, FreeRDP_GatewayEnabled, TRUE)) + goto fail; + + BOOL allowHttpOpts = FALSE; + for (size_t x = 0; x < count; x++) + { + BOOL validOption = FALSE; + const char* argval = args[x]; + + WINPR_ASSERT(argval); + + const char* gw = option_starts_with("g:", argval); + if (gw) + { + if (!parse_gateway_host_option(settings, gw)) + goto fail; + validOption = TRUE; + allowHttpOpts = FALSE; + } + + const char* gu = option_starts_with("u:", argval); + if (gu) + { + if (!parse_gateway_cred_option(settings, gu, FreeRDP_GatewayUsername)) + goto fail; + validOption = TRUE; + allowHttpOpts = FALSE; + } + + const char* gd = option_starts_with("d:", argval); + if (gd) + { + if (!parse_gateway_cred_option(settings, gd, FreeRDP_GatewayDomain)) + goto fail; + validOption = TRUE; + allowHttpOpts = FALSE; + } + + const char* gp = option_starts_with("p:", argval); + if (gp) + { + if (!parse_gateway_cred_option(settings, gp, FreeRDP_GatewayPassword)) + goto fail; + validOption = TRUE; + allowHttpOpts = FALSE; + } + + const char* gt = option_starts_with("type:", argval); + if (gt) + { + if (!parse_gateway_type_option(settings, gt)) + goto fail; + validOption = TRUE; + allowHttpOpts = freerdp_settings_get_bool(settings, FreeRDP_GatewayHttpTransport); + } + + const char* gat = option_starts_with("access-token:", argval); + if (gat) + { + if (!freerdp_settings_set_string(settings, FreeRDP_GatewayAccessToken, gat)) + goto fail; + validOption = TRUE; + allowHttpOpts = FALSE; + } + + const char* bearer = option_starts_with("bearer:", argval); + if (bearer) + { + if (!freerdp_settings_set_string(settings, FreeRDP_GatewayHttpExtAuthBearer, bearer)) + goto fail; + validOption = TRUE; + allowHttpOpts = FALSE; + } + + const char* gwurl = option_starts_with("url:", argval); + if (gwurl) + { + if (!freerdp_settings_set_string(settings, FreeRDP_GatewayUrl, gwurl)) + goto fail; + if (!freerdp_set_gateway_usage_method(settings, TSC_PROXY_MODE_DIRECT)) + goto fail; + validOption = TRUE; + allowHttpOpts = FALSE; + } + + const char* um = option_starts_with("usage-method:", argval); + if (um) + { + if (!parse_gateway_usage_option(settings, um)) + goto fail; + validOption = TRUE; + allowHttpOpts = FALSE; + } + + if (allowHttpOpts) + { + if (option_equals(argval, "no-websockets")) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_GatewayHttpUseWebsockets, FALSE)) + goto fail; + validOption = TRUE; + } + else if (option_equals(argval, "extauth-sspi-ntlm")) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_GatewayHttpExtAuthSspiNtlm, TRUE)) + goto fail; + validOption = TRUE; + } + } + + if (!validOption) + goto fail; + } + + rc = TRUE; +fail: + free(args); + return rc; +} + +static void fill_credential_string(COMMAND_LINE_ARGUMENT_A* args, const char* value) +{ + WINPR_ASSERT(args); + WINPR_ASSERT(value); + + const COMMAND_LINE_ARGUMENT_A* arg = CommandLineFindArgumentA(args, value); + if (!arg) + return; + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + FillMemory(arg->Value, strlen(arg->Value), '*'); +} + +static void fill_credential_strings(COMMAND_LINE_ARGUMENT_A* args) +{ + const char* credentials[] = { + "p", + "smartcard-logon", +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + "gp", + "gat", +#endif + "pth", + "reconnect-cookie", + "assistance" + }; + + for (size_t x = 0; x < ARRAYSIZE(credentials); x++) + { + const char* cred = credentials[x]; + fill_credential_string(args, cred); + } + + const COMMAND_LINE_ARGUMENT_A* arg = CommandLineFindArgumentA(args, "gateway"); + if (arg && ((arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT) != 0)) + { + const char* gwcreds[] = { "p:", "access-token:" }; + char* tok = strtok(arg->Value, ","); + while (tok) + { + for (size_t x = 0; x < ARRAYSIZE(gwcreds); x++) + { + const char* opt = gwcreds[x]; + if (option_starts_with(opt, tok)) + { + char* val = &tok[strlen(opt)]; + FillMemory(val, strlen(val), '*'); + } + } + tok = strtok(NULL, ","); + } + } +} + +static int freerdp_client_settings_parse_command_line_arguments_int( + rdpSettings* settings, int argc, char* argv[], BOOL allowUnknown, + COMMAND_LINE_ARGUMENT_A* largs, size_t count, + int (*handle_option)(const COMMAND_LINE_ARGUMENT* arg, void* custom), void* handle_userdata) +{ + char* user = NULL; + int status = 0; + BOOL ext = FALSE; + BOOL assist = FALSE; + DWORD flags = 0; + BOOL promptForPassword = FALSE; + BOOL compatibility = FALSE; + const COMMAND_LINE_ARGUMENT_A* arg = NULL; + + /* Command line detection fails if only a .rdp or .msrcIncident file + * is supplied. Check this case first, only then try to detect + * legacy command line syntax. */ + if (argc > 1) + { + ext = option_is_rdp_file(argv[1]); + assist = option_is_incident_file(argv[1]); + } + + if (!ext && !assist) + compatibility = freerdp_client_detect_command_line(argc, argv, &flags); + else + compatibility = freerdp_client_detect_command_line(argc - 1, &argv[1], &flags); + + freerdp_settings_set_string(settings, FreeRDP_ProxyHostname, NULL); + freerdp_settings_set_string(settings, FreeRDP_ProxyUsername, NULL); + freerdp_settings_set_string(settings, FreeRDP_ProxyPassword, NULL); + + if (compatibility) + { + WLog_WARN(TAG, "Unsupported command line syntax!"); + WLog_WARN(TAG, "FreeRDP 1.0 style syntax was dropped with version 3!"); + return -1; + } + else + { + if (allowUnknown) + flags |= COMMAND_LINE_IGN_UNKNOWN_KEYWORD; + + if (ext) + { + if (freerdp_client_settings_parse_connection_file(settings, argv[1])) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + if (assist) + { + if (freerdp_client_settings_parse_assistance_file(settings, argc, argv) < 0) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + CommandLineClearArgumentsA(largs); + status = CommandLineParseArgumentsA(argc, argv, largs, flags, settings, + freerdp_client_command_line_pre_filter, + freerdp_client_command_line_post_filter); + + if (status < 0) + return status; + + prepare_default_settings(settings, largs, ext); + } + + CommandLineFindArgumentA(largs, "v"); + arg = largs; + errno = 0; + + /* Disable unicode input unless requested. */ + if (!freerdp_settings_set_bool(settings, FreeRDP_UnicodeInput, FALSE)) + return COMMAND_LINE_ERROR_MEMORY; + + do + { + BOOL enable = arg->Value ? TRUE : FALSE; + + if (!(arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT)) + continue; + + CommandLineSwitchStart(arg) + + CommandLineSwitchCase(arg, "v") + { + const int rc = parse_host_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "spn-class") + { + if (!freerdp_settings_set_string(settings, FreeRDP_AuthenticationServiceClass, + arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "sspi-module") + { + if (!freerdp_settings_set_string(settings, FreeRDP_SspiModule, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "winscard-module") + { + if (!freerdp_settings_set_string(settings, FreeRDP_WinSCardModule, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "redirect-prefer") + { + const int rc = parse_redirect_prefer_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "credentials-delegation") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_DisableCredentialsDelegation, !enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "prevent-session-lock") + { + const int rc = parse_prevent_session_lock_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "vmconnect") + { + const int rc = parse_vmconnect_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "w") + { + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, -1, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, (UINT32)val)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "h") + { + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, -1, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, (UINT32)val)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "size") + { + const int rc = parse_size_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "f") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_Fullscreen, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "suppress-output") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SuppressOutput, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "multimon") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_UseMultimon, TRUE)) + return COMMAND_LINE_ERROR; + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + if (option_equals(arg->Value, "force")) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_ForceMultimon, TRUE)) + return COMMAND_LINE_ERROR; + } + } + } + CommandLineSwitchCase(arg, "span") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SpanMonitors, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "workarea") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_Workarea, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "monitors") + { + const int rc = parse_monitors_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "t") + { + if (!freerdp_settings_set_string(settings, FreeRDP_WindowTitle, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "decorations") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_Decorations, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "dynamic-resolution") + { + const int rc = parse_dynamic_resolution_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "smart-sizing") + { + const int rc = parse_smart_sizing_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "bpp") + { + const int rc = parse_bpp_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "admin") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_ConsoleSession, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "relax-order-checks") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_AllowUnanouncedOrdersFromServer, + enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "restricted-admin") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_ConsoleSession, enable)) + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_bool(settings, FreeRDP_RestrictedAdminModeRequired, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "pth") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_ConsoleSession, TRUE)) + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_bool(settings, FreeRDP_RestrictedAdminModeRequired, TRUE)) + return COMMAND_LINE_ERROR; + + if (!freerdp_settings_set_string(settings, FreeRDP_PasswordHash, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "client-hostname") + { + if (!freerdp_settings_set_string(settings, FreeRDP_ClientHostname, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "kbd") + { + int rc = parse_kbd_options(settings, arg); + if (rc != 0) + return rc; + } +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + CommandLineSwitchCase(arg, "kbd-remap") + { + WLog_WARN(TAG, "/kbd-remap:<key>=<value>,<key2>=<value2> is deprecated, use " + "/kbd:remap:<key>=<value>,remap:<key2>=<value2>,... instead"); + if (!freerdp_settings_set_string(settings, FreeRDP_KeyboardRemappingList, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "kbd-lang") + { + LONGLONG val = 0; + + WLog_WARN(TAG, "/kbd-lang:<value> is deprecated, use /kbd:lang:<value> instead"); + if (!value_to_int(arg->Value, &val, 1, UINT32_MAX)) + { + WLog_ERR(TAG, "Could not identify keyboard active language %s", arg->Value); + WLog_ERR(TAG, "Use /list:kbd-lang to list available layouts"); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + + if (!freerdp_settings_set_uint32(settings, FreeRDP_KeyboardCodePage, (UINT32)val)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "kbd-type") + { + LONGLONG val = 0; + + WLog_WARN(TAG, "/kbd-type:<value> is deprecated, use /kbd:type:<value> instead"); + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_KeyboardType, (UINT32)val)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "kbd-unicode") + { + WLog_WARN(TAG, "/kbd-unicode is deprecated, use /kbd:unicode[:on|off] instead"); + if (!freerdp_settings_set_bool(settings, FreeRDP_UnicodeInput, enable)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + CommandLineSwitchCase(arg, "kbd-subtype") + { + LONGLONG val = 0; + + WLog_WARN(TAG, "/kbd-subtype:<value> is deprecated, use /kbd:subtype:<value> instead"); + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_KeyboardSubType, (UINT32)val)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "kbd-fn-key") + { + LONGLONG val = 0; + + WLog_WARN(TAG, "/kbd-fn-key:<value> is deprecated, use /kbd:fn-key:<value> instead"); + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_KeyboardFunctionKey, (UINT32)val)) + return COMMAND_LINE_ERROR; + } +#endif + CommandLineSwitchCase(arg, "u") + { + WINPR_ASSERT(arg->Value); + user = arg->Value; + } + CommandLineSwitchCase(arg, "d") + { + if (!freerdp_settings_set_string(settings, FreeRDP_Domain, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "p") + { + if (!freerdp_settings_set_string(settings, FreeRDP_Password, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "gateway") + { + if (!parse_gateway_options(settings, arg)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "proxy") + { + const int rc = parse_proxy_options(settings, arg); + if (rc != 0) + return rc; + } +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + CommandLineSwitchCase(arg, "g") + { + if (!parse_gateway_host_option(settings, arg->Value)) + return FALSE; + } + CommandLineSwitchCase(arg, "gu") + { + if (!parse_gateway_cred_option(settings, arg->Value, FreeRDP_GatewayUsername)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + CommandLineSwitchCase(arg, "gd") + { + if (!parse_gateway_cred_option(settings, arg->Value, FreeRDP_GatewayDomain)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + CommandLineSwitchCase(arg, "gp") + { + if (!parse_gateway_cred_option(settings, arg->Value, FreeRDP_GatewayPassword)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + CommandLineSwitchCase(arg, "gt") + { + if (!parse_gateway_type_option(settings, arg->Value)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + CommandLineSwitchCase(arg, "gat") + { + if (!freerdp_settings_set_string(settings, FreeRDP_GatewayAccessToken, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "gateway-usage-method") + { + if (!parse_gateway_usage_option(settings, arg->Value)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } +#endif + CommandLineSwitchCase(arg, "app") + { + int rc = parse_app_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "load-balance-info") + { + if (!freerdp_settings_set_pointer_len(settings, FreeRDP_LoadBalanceInfo, arg->Value, + strlen(arg->Value))) + return COMMAND_LINE_ERROR; + } +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + CommandLineSwitchCase(arg, "app-workdir") + { + WLog_WARN( + TAG, + "/app-workdir:<directory> is deprecated, use /app:workdir:<directory> instead"); + if (!freerdp_settings_set_string(settings, FreeRDP_RemoteApplicationWorkingDir, + arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "app-name") + { + WLog_WARN(TAG, "/app-name:<directory> is deprecated, use /app:name:<name> instead"); + if (!freerdp_settings_set_string(settings, FreeRDP_RemoteApplicationName, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "app-icon") + { + WLog_WARN(TAG, "/app-icon:<filename> is deprecated, use /app:icon:<filename> instead"); + if (!freerdp_settings_set_string(settings, FreeRDP_RemoteApplicationIcon, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "app-cmd") + { + WLog_WARN(TAG, "/app-cmd:<command> is deprecated, use /app:cmd:<command> instead"); + if (!freerdp_settings_set_string(settings, FreeRDP_RemoteApplicationCmdLine, + arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "app-file") + { + WLog_WARN(TAG, "/app-file:<filename> is deprecated, use /app:file:<filename> instead"); + if (!freerdp_settings_set_string(settings, FreeRDP_RemoteApplicationFile, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "app-guid") + { + WLog_WARN(TAG, "/app-guid:<guid> is deprecated, use /app:guid:<guid> instead"); + if (!freerdp_settings_set_string(settings, FreeRDP_RemoteApplicationGuid, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } +#endif + CommandLineSwitchCase(arg, "compression") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_CompressionEnabled, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "compression-level") + { + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_CompressionLevel, (UINT32)val)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "drives") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectDrives, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "dump") + { + const int rc = parse_dump_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "disable-output") + { + freerdp_settings_set_bool(settings, FreeRDP_DeactivateClientDecoding, enable); + } + CommandLineSwitchCase(arg, "home-drive") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectHomeDrive, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "ipv6") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_PreferIPv6OverIPv4, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "clipboard") + { + const int rc = parse_clipboard_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "server-name") + { + if (!freerdp_settings_set_string(settings, FreeRDP_UserSpecifiedServerName, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "shell") + { + if (!freerdp_settings_set_string(settings, FreeRDP_AlternateShell, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "shell-dir") + { + if (!freerdp_settings_set_string(settings, FreeRDP_ShellWorkingDirectory, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "audio-mode") + { + const int rc = parse_audio_mode_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "network") + { + const int rc = parse_network_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "fonts") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_AllowFontSmoothing, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "wallpaper") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_DisableWallpaper, !enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "window-drag") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_DisableFullWindowDrag, !enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "window-position") + { + unsigned long x = 0; + unsigned long y = 0; + + if (!arg->Value) + return COMMAND_LINE_ERROR_MISSING_ARGUMENT; + + if (!parseSizeValue(arg->Value, &x, &y) || x > UINT16_MAX || y > UINT16_MAX) + { + WLog_ERR(TAG, "invalid window-position argument"); + return COMMAND_LINE_ERROR_MISSING_ARGUMENT; + } + + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopPosX, (UINT32)x)) + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopPosY, (UINT32)y)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "menu-anims") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_DisableMenuAnims, !enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "themes") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_DisableThemes, !enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "timeout") + { + ULONGLONG val = 0; + if (!value_to_uint(arg->Value, &val, 1, 600000)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_TcpAckTimeout, (UINT32)val)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + CommandLineSwitchCase(arg, "aero") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_AllowDesktopComposition, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "gdi") + { + if (option_equals(arg->Value, "sw")) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SoftwareGdi, TRUE)) + return COMMAND_LINE_ERROR; + } + else if (option_equals(arg->Value, "hw")) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SoftwareGdi, FALSE)) + return COMMAND_LINE_ERROR; + } + else + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + CommandLineSwitchCase(arg, "gfx") + { + int rc = parse_gfx_options(settings, arg); + if (rc != 0) + return rc; + } +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + CommandLineSwitchCase(arg, "gfx-thin-client") + { + WLog_WARN(TAG, "/gfx-thin-client is deprecated, use /gfx:thin-client[:on|off] instead"); + if (!freerdp_settings_set_bool(settings, FreeRDP_GfxThinClient, enable)) + return COMMAND_LINE_ERROR; + + if (freerdp_settings_get_bool(settings, FreeRDP_GfxThinClient)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_GfxSmallCache, TRUE)) + return COMMAND_LINE_ERROR; + } + + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportGraphicsPipeline, TRUE)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "gfx-small-cache") + { + WLog_WARN(TAG, "/gfx-small-cache is deprecated, use /gfx:small-cache[:on|off] instead"); + if (!freerdp_settings_set_bool(settings, FreeRDP_GfxSmallCache, enable)) + return COMMAND_LINE_ERROR; + + if (enable) + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportGraphicsPipeline, TRUE)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "gfx-progressive") + { + WLog_WARN(TAG, "/gfx-progressive is deprecated, use /gfx:progressive[:on|off] instead"); + if (!freerdp_settings_set_bool(settings, FreeRDP_GfxProgressive, enable)) + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_bool(settings, FreeRDP_GfxThinClient, !enable)) + return COMMAND_LINE_ERROR; + + if (enable) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportGraphicsPipeline, TRUE)) + return COMMAND_LINE_ERROR; + } + } +#ifdef WITH_GFX_H264 + CommandLineSwitchCase(arg, "gfx-h264") + { + WLog_WARN(TAG, "/gfx-h264 is deprecated, use /gfx:avc420 instead"); + int rc = parse_gfx_options(settings, arg); + if (rc != 0) + return rc; + } +#endif +#endif + CommandLineSwitchCase(arg, "rfx") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteFxCodec, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "rfx-mode") + { + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (option_equals(arg->Value, "video")) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_RemoteFxCodecMode, 0x00)) + return COMMAND_LINE_ERROR; + } + else if (option_equals(arg->Value, "image")) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteFxImageCodec, TRUE)) + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_uint32(settings, FreeRDP_RemoteFxCodecMode, 0x02)) + return COMMAND_LINE_ERROR; + } + } + CommandLineSwitchCase(arg, "frame-ack") + { + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_FrameAcknowledge, (UINT32)val)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "nsc") + { + freerdp_settings_set_bool(settings, FreeRDP_NSCodec, enable); + } +#if defined(WITH_JPEG) + CommandLineSwitchCase(arg, "jpeg") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_JpegCodec, enable)) + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_uint32(settings, FreeRDP_JpegQuality, 75)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "jpeg-quality") + { + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, 0, 100)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_JpegQuality, (UINT32)val)) + return COMMAND_LINE_ERROR; + } +#endif + CommandLineSwitchCase(arg, "nego") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_NegotiateSecurityLayer, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "pcb") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SendPreconnectionPdu, TRUE)) + return COMMAND_LINE_ERROR; + + if (!freerdp_settings_set_string(settings, FreeRDP_PreconnectionBlob, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "pcid") + { + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_bool(settings, FreeRDP_SendPreconnectionPdu, TRUE)) + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_uint32(settings, FreeRDP_PreconnectionId, (UINT32)val)) + return COMMAND_LINE_ERROR; + } +#ifdef _WIN32 + CommandLineSwitchCase(arg, "connect-child-session") + { + if (!freerdp_settings_set_string(settings, FreeRDP_AuthenticationServiceClass, + "vs-debug") || + !freerdp_settings_set_string(settings, FreeRDP_ServerHostname, "localhost") || + !freerdp_settings_set_string(settings, FreeRDP_AuthenticationPackageList, "ntlm") || + !freerdp_settings_set_bool(settings, FreeRDP_NegotiateSecurityLayer, FALSE) || + !freerdp_settings_set_bool(settings, FreeRDP_VmConnectMode, TRUE) || + !freerdp_settings_set_bool(settings, FreeRDP_ConnectChildSession, TRUE) || + !freerdp_settings_set_bool(settings, FreeRDP_NlaSecurity, TRUE) || + !freerdp_settings_set_uint32(settings, FreeRDP_AuthenticationLevel, 0)) + return COMMAND_LINE_ERROR_MEMORY; + } +#endif + CommandLineSwitchCase(arg, "sec") + { + const int rc = parse_sec_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "encryption-methods") + { + const int rc = parse_encryption_methods_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "args-from") + { + WLog_ERR(TAG, "/args-from:%s can not be used in combination with other arguments!", + arg->Value); + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + CommandLineSwitchCase(arg, "from-stdin") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_CredentialsFromStdin, TRUE)) + return COMMAND_LINE_ERROR; + + if (arg->Flags & COMMAND_LINE_VALUE_PRESENT) + { + if (!arg->Value) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + promptForPassword = (option_equals(arg->Value, "force")); + + if (!promptForPassword) + return COMMAND_LINE_ERROR; + } + } + CommandLineSwitchCase(arg, "log-level") + { + wLog* root = WLog_GetRoot(); + + if (!WLog_SetStringLogLevel(root, arg->Value)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "log-filters") + { + if (!WLog_AddStringLogFilters(arg->Value)) + return COMMAND_LINE_ERROR; + } +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + CommandLineSwitchCase(arg, "sec-rdp") + { + WLog_WARN(TAG, "/sec-rdp is deprecated, use /sec:rdp[:on|off] instead"); + if (!freerdp_settings_set_bool(settings, FreeRDP_RdpSecurity, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "sec-tls") + { + WLog_WARN(TAG, "/sec-tls is deprecated, use /sec:tls[:on|off] instead"); + if (!freerdp_settings_set_bool(settings, FreeRDP_TlsSecurity, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "sec-nla") + { + WLog_WARN(TAG, "/sec-nla is deprecated, use /sec:nla[:on|off] instead"); + if (!freerdp_settings_set_bool(settings, FreeRDP_NlaSecurity, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "sec-ext") + { + WLog_WARN(TAG, "/sec-ext is deprecated, use /sec:ext[:on|off] instead"); + if (!freerdp_settings_set_bool(settings, FreeRDP_ExtSecurity, enable)) + return COMMAND_LINE_ERROR; + } +#endif + CommandLineSwitchCase(arg, "tls") + { + int rc = parse_tls_options(settings, arg); + if (rc != 0) + return rc; + } +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + CommandLineSwitchCase(arg, "tls-ciphers") + { + WLog_WARN(TAG, "/tls-ciphers:<cipher list> is deprecated, use " + "/tls:ciphers:<cipher list> instead"); + int rc = parse_tls_cipher_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "tls-seclevel") + { + WLog_WARN(TAG, + "/tls-seclevel:<level> is deprecated, use /tls:sec-level:<level> instead"); + int rc = parse_tls_cipher_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "tls-secrets-file") + { + WLog_WARN(TAG, "/tls-secrets-file:<filename> is deprecated, use " + "/tls:secrets-file:<filename> instead"); + int rc = parse_tls_cipher_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "enforce-tlsv1_2") + { + WLog_WARN(TAG, "/enforce-tlsv1_2 is deprecated, use /tls:enforce:1.2 instead"); + int rc = parse_tls_cipher_options(settings, arg); + if (rc != 0) + return rc; + } +#endif + CommandLineSwitchCase(arg, "cert") + { + const int rc = parse_cert_options(settings, arg); + if (rc != 0) + return rc; + } + +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + CommandLineSwitchCase(arg, "cert-name") + { + WLog_WARN(TAG, "/cert-name is deprecated, use /cert:name instead"); + if (!freerdp_settings_set_string(settings, FreeRDP_CertificateName, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "cert-ignore") + { + WLog_WARN(TAG, "/cert-ignore is deprecated, use /cert:ignore instead"); + if (!freerdp_settings_set_bool(settings, FreeRDP_IgnoreCertificate, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "cert-tofu") + { + WLog_WARN(TAG, "/cert-tofu is deprecated, use /cert:tofu instead"); + if (!freerdp_settings_set_bool(settings, FreeRDP_AutoAcceptCertificate, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "cert-deny") + { + WLog_WARN(TAG, "/cert-deny is deprecated, use /cert:deny instead"); + if (!freerdp_settings_set_bool(settings, FreeRDP_AutoDenyCertificate, enable)) + return COMMAND_LINE_ERROR; + } +#endif + CommandLineSwitchCase(arg, "authentication") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_Authentication, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "encryption") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_UseRdpSecurityLayer, !enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "grab-keyboard") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_GrabKeyboard, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "grab-mouse") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_GrabMouse, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "mouse-relative") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_MouseUseRelativeMove, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "mouse") + { + const int rc = parse_mouse_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "unmap-buttons") + { + freerdp_settings_set_bool(settings, FreeRDP_UnmapButtons, enable); + } + CommandLineSwitchCase(arg, "toggle-fullscreen") + { + freerdp_settings_set_bool(settings, FreeRDP_ToggleFullscreen, enable); + } + CommandLineSwitchCase(arg, "force-console-callbacks") + { + freerdp_settings_set_bool(settings, FreeRDP_UseCommonStdioCallbacks, enable); + } + CommandLineSwitchCase(arg, "floatbar") + { + const int rc = parse_floatbar_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "mouse-motion") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_MouseMotion, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "parent-window") + { + ULONGLONG val = 0; + + if (!value_to_uint(arg->Value, &val, 0, UINT64_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint64(settings, FreeRDP_ParentWindowId, (UINT64)val)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "client-build-number") + { + ULONGLONG val = 0; + + if (!value_to_uint(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_ClientBuild, (UINT32)val)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + CommandLineSwitchCase(arg, "cache") + { + int rc = parse_cache_options(settings, arg); + if (rc != 0) + return rc; + } +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + CommandLineSwitchCase(arg, "bitmap-cache") + { + WLog_WARN(TAG, "/bitmap-cache is deprecated, use /cache:bitmap[:on|off] instead"); + if (!freerdp_settings_set_bool(settings, FreeRDP_BitmapCacheEnabled, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "persist-cache") + { + WLog_WARN(TAG, "/persist-cache is deprecated, use /cache:persist[:on|off] instead"); + if (!freerdp_settings_set_bool(settings, FreeRDP_BitmapCachePersistEnabled, enable)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + CommandLineSwitchCase(arg, "persist-cache-file") + { + WLog_WARN(TAG, "/persist-cache-file:<filename> is deprecated, use " + "/cache:persist-file:<filename> instead"); + if (!freerdp_settings_set_string(settings, FreeRDP_BitmapCachePersistFile, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + + if (!freerdp_settings_set_bool(settings, FreeRDP_BitmapCachePersistEnabled, TRUE)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + } + CommandLineSwitchCase(arg, "offscreen-cache") + { + WLog_WARN(TAG, "/bitmap-cache is deprecated, use /cache:bitmap[:on|off] instead"); + if (!freerdp_settings_set_uint32(settings, FreeRDP_OffscreenSupportLevel, + (UINT32)enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "glyph-cache") + { + WLog_WARN(TAG, "/glyph-cache is deprecated, use /cache:glyph[:on|off] instead"); + if (!freerdp_settings_set_uint32(settings, FreeRDP_GlyphSupportLevel, + arg->Value ? GLYPH_SUPPORT_FULL : GLYPH_SUPPORT_NONE)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "codec-cache") + { + WLog_WARN(TAG, + "/codec-cache:<option> is deprecated, use /cache:codec:<option> instead"); + const int rc = parse_codec_cache_options(settings, arg); + if (rc != 0) + return rc; + } +#endif + CommandLineSwitchCase(arg, "max-fast-path-size") + { + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_MultifragMaxRequestSize, + (UINT32)val)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "auto-request-control") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteAssistanceRequestControl, + enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "async-update") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_AsyncUpdate, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "async-channels") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_AsyncChannels, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "wm-class") + { + if (!freerdp_settings_set_string(settings, FreeRDP_WmClass, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "play-rfx") + { + if (!freerdp_settings_set_string(settings, FreeRDP_PlayRemoteFxFile, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + + if (!freerdp_settings_set_bool(settings, FreeRDP_PlayRemoteFx, TRUE)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "auth-only") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_AuthenticationOnly, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "auth-pkg-list") + { + if (!freerdp_settings_set_string(settings, FreeRDP_AuthenticationPackageList, + arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "auto-reconnect") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_AutoReconnectionEnabled, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "auto-reconnect-max-retries") + { + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, 0, 1000)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_AutoReconnectMaxRetries, + (UINT32)val)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "reconnect-cookie") + { + const int rc = parse_reconnect_cookie_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "print-reconnect-cookie") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_PrintReconnectCookie, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "pwidth") + { + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopPhysicalWidth, (UINT32)val)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "pheight") + { + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, 0, UINT32_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopPhysicalHeight, (UINT32)val)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "orientation") + { + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, 0, UINT16_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint16(settings, FreeRDP_DesktopOrientation, (UINT16)val)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "old-license") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_OldLicenseBehaviour, TRUE)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "scale") + { + const int rc = parse_scale_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "scale-desktop") + { + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, 100, 500)) + return FALSE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopScaleFactor, (UINT32)val)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "scale-device") + { + const int rc = parse_scale_device_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "action-script") + { + if (!freerdp_settings_set_string(settings, FreeRDP_ActionScript, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, RDP2TCP_DVC_CHANNEL_NAME) + { + if (!freerdp_settings_set_string(settings, FreeRDP_RDP2TCPArgs, arg->Value)) + return COMMAND_LINE_ERROR_MEMORY; + } + CommandLineSwitchCase(arg, "fipsmode") + { + if (!freerdp_settings_set_bool(settings, FreeRDP_FIPSMode, enable)) + return COMMAND_LINE_ERROR; + } + CommandLineSwitchCase(arg, "smartcard-logon") + { + const int rc = parse_smartcard_logon_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchCase(arg, "tune") + { + const int rc = parse_tune_options(settings, arg); + if (rc != 0) + return rc; + } + CommandLineSwitchDefault(arg) + { + if (handle_option) + { + const int rc = handle_option(arg, handle_userdata); + if (rc != 0) + return rc; + } + } + CommandLineSwitchEnd(arg) + } while ((arg = CommandLineFindNextArgumentA(arg)) != NULL); + + if (user) + { + if (!freerdp_settings_get_string(settings, FreeRDP_Domain) && user) + { + if (!freerdp_settings_set_string(settings, FreeRDP_Username, NULL)) + return COMMAND_LINE_ERROR; + + if (!freerdp_settings_set_string(settings, FreeRDP_Domain, NULL)) + return COMMAND_LINE_ERROR; + + if (!freerdp_parse_username_settings(user, settings, FreeRDP_Username, FreeRDP_Domain)) + return COMMAND_LINE_ERROR; + } + else + { + if (!freerdp_settings_set_string(settings, FreeRDP_Username, user)) + return COMMAND_LINE_ERROR; + } + } + + if (promptForPassword) + { + freerdp* instance = freerdp_settings_get_pointer_writable(settings, FreeRDP_instance); + if (!freerdp_settings_get_string(settings, FreeRDP_Password)) + { + char buffer[512 + 1] = { 0 }; + + if (!freerdp_passphrase_read(instance->context, "Password: ", buffer, + ARRAYSIZE(buffer) - 1, 1)) + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_string(settings, FreeRDP_Password, buffer)) + return COMMAND_LINE_ERROR; + } + + if (freerdp_settings_get_bool(settings, FreeRDP_GatewayEnabled) && + !freerdp_settings_get_bool(settings, FreeRDP_GatewayUseSameCredentials)) + { + if (!freerdp_settings_get_string(settings, FreeRDP_GatewayPassword)) + { + char buffer[512 + 1] = { 0 }; + + if (!freerdp_passphrase_read(instance->context, "Gateway Password: ", buffer, + ARRAYSIZE(buffer) - 1, 1)) + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_string(settings, FreeRDP_GatewayPassword, buffer)) + return COMMAND_LINE_ERROR; + } + } + } + + freerdp_performance_flags_make(settings); + + if (freerdp_settings_get_bool(settings, FreeRDP_RemoteFxCodec) || + freerdp_settings_get_bool(settings, FreeRDP_NSCodec) || + freerdp_settings_get_bool(settings, FreeRDP_SupportGraphicsPipeline)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_FastPathOutput, TRUE)) + return COMMAND_LINE_ERROR; + if (!freerdp_settings_set_bool(settings, FreeRDP_FrameMarkerCommandEnabled, TRUE)) + return COMMAND_LINE_ERROR; + } + + arg = CommandLineFindArgumentA(largs, "port"); + if (arg->Flags & COMMAND_LINE_ARGUMENT_PRESENT) + { + LONGLONG val = 0; + + if (!value_to_int(arg->Value, &val, 1, UINT16_MAX)) + return COMMAND_LINE_ERROR_UNEXPECTED_VALUE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_ServerPort, (UINT32)val)) + return COMMAND_LINE_ERROR; + } + + fill_credential_strings(largs); + + return status; +} + +static void argv_free(int* pargc, char** pargv[]) +{ + WINPR_ASSERT(pargc); + WINPR_ASSERT(pargv); + const int argc = *pargc; + char** argv = *pargv; + *pargc = 0; + *pargv = NULL; + + if (!argv) + return; + for (int x = 0; x < argc; x++) + free(argv[x]); + free(argv); +} + +static BOOL argv_append(int* pargc, char** pargv[], const char* what) +{ + WINPR_ASSERT(pargc); + WINPR_ASSERT(pargv); + + if (*pargc < 0) + return FALSE; + + if (!what) + return FALSE; + + int nargc = *pargc + 1; + char** tmp = realloc(*pargv, nargc * sizeof(char*)); + if (!tmp) + return FALSE; + + tmp[*pargc] = what; + *pargv = tmp; + *pargc = nargc; + return TRUE; +} + +static BOOL argv_append_dup(int* pargc, char** pargv[], const char* what) +{ + char* copy = NULL; + if (what) + copy = _strdup(what); + + const BOOL rc = argv_append(pargc, pargv, copy); + if (!rc) + free(copy); + return rc; +} + +static BOOL args_from_fp(FILE* fp, int* aargc, char** aargv[], const char* file, const char* cmd) +{ + BOOL success = FALSE; + + WINPR_ASSERT(aargc); + WINPR_ASSERT(aargv); + WINPR_ASSERT(cmd); + + if (!fp) + { + WLog_ERR(TAG, "Failed to read command line options from file '%s'", file); + return FALSE; + } + if (!argv_append_dup(aargc, aargv, cmd)) + goto fail; + while (!feof(fp)) + { + char* line = NULL; + size_t size = 0; + INT64 rc = GetLine(&line, &size, fp); + if ((rc < 0) || !line) + { + /* abort if GetLine failed due to reaching EOF */ + if (feof(fp)) + break; + goto fail; + } + + while (rc > 0) + { + const char cur = (line[rc - 1]); + if ((cur == '\n') || (cur == '\r')) + { + line[rc - 1] = '\0'; + rc--; + } + else + break; + } + /* abort on empty lines */ + if (rc == 0) + { + free(line); + break; + } + if (!argv_append(aargc, aargv, line)) + { + free(line); + goto fail; + } + } + + success = TRUE; +fail: + fclose(fp); + if (!success) + argv_free(aargc, aargv); + return success; +} + +static BOOL args_from_env(const char* name, int* aargc, char** aargv[], const char* arg, + const char* cmd) +{ + BOOL success = FALSE; + char* env = NULL; + + WINPR_ASSERT(aargc); + WINPR_ASSERT(aargv); + WINPR_ASSERT(cmd); + + if (!name) + { + WLog_ERR(TAG, "%s - environment variable name empty", arg); + goto cleanup; + } + + const DWORD size = GetEnvironmentVariableX(name, env, 0); + if (size == 0) + { + WLog_ERR(TAG, "%s - no environment variable '%s'", arg, name); + goto cleanup; + } + env = calloc(size + 1, sizeof(char)); + if (!env) + goto cleanup; + const DWORD rc = GetEnvironmentVariableX(name, env, size); + if (rc != size - 1) + goto cleanup; + if (rc == 0) + { + WLog_ERR(TAG, "%s - environment variable '%s' is empty", arg); + goto cleanup; + } + + if (!argv_append_dup(aargc, aargv, cmd)) + goto cleanup; + + char* context = NULL; + char* tok = strtok_s(env, "\n", &context); + while (tok) + { + if (!argv_append_dup(aargc, aargv, tok)) + goto cleanup; + tok = strtok_s(NULL, "\n", &context); + } + + success = TRUE; +cleanup: + free(env); + if (!success) + argv_free(aargc, aargv); + return success; +} + +int freerdp_client_settings_parse_command_line_arguments(rdpSettings* settings, int oargc, + char* oargv[], BOOL allowUnknown) +{ + return freerdp_client_settings_parse_command_line_arguments_ex( + settings, oargc, oargv, allowUnknown, NULL, 0, NULL, NULL); +} + +int freerdp_client_settings_parse_command_line_arguments_ex( + rdpSettings* settings, int oargc, char** oargv, BOOL allowUnknown, + COMMAND_LINE_ARGUMENT_A* args, size_t count, + int (*handle_option)(const COMMAND_LINE_ARGUMENT* arg, void* custom), void* handle_userdata) +{ + int argc = oargc; + char** argv = oargv; + int res = -1; + int aargc = 0; + char** aargv = NULL; + if ((argc == 2) && option_starts_with("/args-from:", argv[1])) + { + BOOL success = FALSE; + const char* file = strchr(argv[1], ':') + 1; + FILE* fp = stdin; + + if (option_starts_with("fd:", file)) + { + ULONGLONG result = 0; + const char* val = strchr(file, ':') + 1; + if (!value_to_uint(val, &result, 0, INT_MAX)) + return -1; + fp = fdopen((int)result, "r"); + success = args_from_fp(fp, &aargc, &aargv, file, oargv[0]); + } + else if (strncmp(file, "env:", 4) == 0) + { + const char* name = strchr(file, ':') + 1; + success = args_from_env(name, &aargc, &aargv, oargv[1], oargv[0]); + } + else if (strcmp(file, "stdin") != 0) + { + fp = winpr_fopen(file, "r"); + success = args_from_fp(fp, &aargc, &aargv, file, oargv[0]); + } + else + success = args_from_fp(fp, &aargc, &aargv, file, oargv[0]); + + if (!success) + return -1; + argc = aargc; + argv = aargv; + } + + size_t lcount = 0; + COMMAND_LINE_ARGUMENT_A* largs = create_merged_args(args, count, &lcount); + if (!largs) + goto fail; + + res = freerdp_client_settings_parse_command_line_arguments_int( + settings, argc, argv, allowUnknown, largs, lcount, handle_option, handle_userdata); +fail: + free(largs); + argv_free(&aargc, &aargv); + return res; +} + +static BOOL freerdp_client_load_static_channel_addin(rdpChannels* channels, rdpSettings* settings, + const char* name, void* data) +{ + PVIRTUALCHANNELENTRY entry = NULL; + PVIRTUALCHANNELENTRYEX entryEx = NULL; + entryEx = (PVIRTUALCHANNELENTRYEX)(void*)freerdp_load_channel_addin_entry( + name, NULL, NULL, FREERDP_ADDIN_CHANNEL_STATIC | FREERDP_ADDIN_CHANNEL_ENTRYEX); + + if (!entryEx) + entry = freerdp_load_channel_addin_entry(name, NULL, NULL, FREERDP_ADDIN_CHANNEL_STATIC); + + if (entryEx) + { + if (freerdp_channels_client_load_ex(channels, settings, entryEx, data) == 0) + { + WLog_DBG(TAG, "loading channelEx %s", name); + return TRUE; + } + } + else if (entry) + { + if (freerdp_channels_client_load(channels, settings, entry, data) == 0) + { + WLog_DBG(TAG, "loading channel %s", name); + return TRUE; + } + } + + return FALSE; +} + +typedef struct +{ + FreeRDP_Settings_Keys_Bool settingId; + const char* channelName; + void* args; +} ChannelToLoad; + +BOOL freerdp_client_load_addins(rdpChannels* channels, rdpSettings* settings) +{ + ChannelToLoad dynChannels[] = { +#if defined(CHANNEL_AINPUT_CLIENT) + { FreeRDP_BOOL_UNUSED, AINPUT_CHANNEL_NAME, NULL }, /* always loaded */ +#endif + { FreeRDP_AudioCapture, AUDIN_CHANNEL_NAME, NULL }, + { FreeRDP_AudioPlayback, RDPSND_CHANNEL_NAME, NULL }, +#ifdef CHANNEL_RDPEI_CLIENT + { FreeRDP_MultiTouchInput, RDPEI_CHANNEL_NAME, NULL }, +#endif + { FreeRDP_SupportGraphicsPipeline, RDPGFX_CHANNEL_NAME, NULL }, + { FreeRDP_SupportEchoChannel, ECHO_CHANNEL_NAME, NULL }, + { FreeRDP_SupportSSHAgentChannel, "sshagent", NULL }, + { FreeRDP_SupportDisplayControl, DISP_CHANNEL_NAME, NULL }, + { FreeRDP_SupportGeometryTracking, GEOMETRY_CHANNEL_NAME, NULL }, + { FreeRDP_SupportVideoOptimized, VIDEO_CHANNEL_NAME, NULL }, + }; + + ChannelToLoad staticChannels[] = { + { FreeRDP_AudioPlayback, RDPSND_CHANNEL_NAME, NULL }, + { FreeRDP_RedirectClipboard, CLIPRDR_SVC_CHANNEL_NAME, NULL }, +#if defined(CHANNEL_ENCOMSP_CLIENT) + { FreeRDP_EncomspVirtualChannel, ENCOMSP_SVC_CHANNEL_NAME, settings }, +#endif + { FreeRDP_RemdeskVirtualChannel, REMDESK_SVC_CHANNEL_NAME, settings }, + { FreeRDP_RemoteApplicationMode, RAIL_SVC_CHANNEL_NAME, settings } + }; + + /** + * Step 1: first load dynamic channels according to the settings + */ + for (size_t i = 0; i < ARRAYSIZE(dynChannels); i++) + { + if ((dynChannels[i].settingId == FreeRDP_BOOL_UNUSED) || + freerdp_settings_get_bool(settings, dynChannels[i].settingId)) + { + const char* p[] = { dynChannels[i].channelName }; + + if (!freerdp_client_add_dynamic_channel(settings, ARRAYSIZE(p), p)) + return FALSE; + } + } + + /** + * step 2: do various adjustements in the settings, to handle channels and settings dependencies + */ + if ((freerdp_static_channel_collection_find(settings, RDPSND_CHANNEL_NAME)) || + (freerdp_dynamic_channel_collection_find(settings, RDPSND_CHANNEL_NAME)) +#if defined(CHANNEL_TSMF_CLIENT) + || (freerdp_dynamic_channel_collection_find(settings, "tsmf")) +#endif + ) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection, TRUE)) + return COMMAND_LINE_ERROR; /* rdpsnd requires rdpdr to be registered */ + if (!freerdp_settings_set_bool(settings, FreeRDP_AudioPlayback, TRUE)) + return COMMAND_LINE_ERROR; /* Both rdpsnd and tsmf require this flag to be set */ + } + + if (freerdp_dynamic_channel_collection_find(settings, AUDIN_CHANNEL_NAME)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_AudioCapture, TRUE)) + return COMMAND_LINE_ERROR; + } + + if (freerdp_settings_get_bool(settings, FreeRDP_NetworkAutoDetect) || + freerdp_settings_get_bool(settings, FreeRDP_SupportHeartbeatPdu) || + freerdp_settings_get_bool(settings, FreeRDP_SupportMultitransport)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection, TRUE)) + return COMMAND_LINE_ERROR; /* these RDP8 features require rdpdr to be registered */ + } + + const char* DrivesToRedirect = freerdp_settings_get_string(settings, FreeRDP_DrivesToRedirect); + + if (DrivesToRedirect && (strlen(DrivesToRedirect) != 0)) + { + /* + * Drives to redirect: + * + * Very similar to DevicesToRedirect, but can contain a + * comma-separated list of drive letters to redirect. + */ + char* value = NULL; + char* tok = NULL; + char* context = NULL; + + value = _strdup(DrivesToRedirect); + if (!value) + return FALSE; + + tok = strtok_s(value, ";", &context); + if (!tok) + { + WLog_ERR(TAG, "DrivesToRedirect contains invalid data: '%s'", DrivesToRedirect); + free(value); + return FALSE; + } + + while (tok) + { + /* Syntax: Comma seperated list of the following entries: + * '*' ... Redirect all drives, including hotplug + * 'DynamicDrives' ... hotplug + * '%' ... user home directory + * <label>(<path>) ... One or more paths to redirect. + * <path>(<label>) ... One or more paths to redirect. + * <path> ... One or more paths to redirect. + */ + /* TODO: Need to properly escape labels and paths */ + BOOL success = 0; + const char* name = NULL; + const char* drive = tok; + char* subcontext = NULL; + char* start = strtok_s(tok, "(", &subcontext); + char* end = strtok_s(NULL, ")", &subcontext); + if (start && end) + name = end; + + if (freerdp_path_valid(name, NULL) && freerdp_path_valid(drive, NULL)) + { + success = freerdp_client_add_drive(settings, name, NULL); + if (success) + success = freerdp_client_add_drive(settings, drive, NULL); + } + else + success = freerdp_client_add_drive(settings, drive, name); + + if (!success) + { + free(value); + return FALSE; + } + + tok = strtok_s(NULL, ";", &context); + } + free(value); + + if (!freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection, TRUE)) + return FALSE; + + if (!freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection, TRUE)) + return COMMAND_LINE_ERROR; + } + else if (freerdp_settings_get_bool(settings, FreeRDP_RedirectDrives)) + { + if (!freerdp_device_collection_find(settings, "drive")) + { + const char* params[] = { "drive", "media", "*" }; + + if (!freerdp_client_add_device_channel(settings, ARRAYSIZE(params), params)) + return FALSE; + } + } + + if (freerdp_settings_get_bool(settings, FreeRDP_RedirectDrives) || + freerdp_settings_get_bool(settings, FreeRDP_RedirectHomeDrive) || + freerdp_settings_get_bool(settings, FreeRDP_RedirectSerialPorts) || + freerdp_settings_get_bool(settings, FreeRDP_RedirectSmartCards) || + freerdp_settings_get_bool(settings, FreeRDP_RedirectPrinters)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection, TRUE)) + return COMMAND_LINE_ERROR; /* All of these features require rdpdr */ + } + + if (freerdp_settings_get_bool(settings, FreeRDP_RedirectHomeDrive)) + { + if (!freerdp_device_collection_find(settings, "drive")) + { + const char* params[] = { "drive", "home", "%" }; + + if (!freerdp_client_add_device_channel(settings, ARRAYSIZE(params), params)) + return FALSE; + } + } + + if (freerdp_settings_get_bool(settings, FreeRDP_DeviceRedirection)) + { + if (!freerdp_client_load_static_channel_addin(channels, settings, RDPDR_SVC_CHANNEL_NAME, + settings)) + return FALSE; + + if (!freerdp_static_channel_collection_find(settings, RDPSND_CHANNEL_NAME) && + !freerdp_dynamic_channel_collection_find(settings, RDPSND_CHANNEL_NAME)) + { + const char* params[] = { RDPSND_CHANNEL_NAME, "sys:fake" }; + + if (!freerdp_client_add_static_channel(settings, ARRAYSIZE(params), params)) + return FALSE; + + if (!freerdp_client_add_dynamic_channel(settings, ARRAYSIZE(params), params)) + return FALSE; + } + } + + if (freerdp_settings_get_bool(settings, FreeRDP_RedirectSmartCards)) + { + if (!freerdp_device_collection_find_type(settings, RDPDR_DTYP_SMARTCARD)) + { + RDPDR_DEVICE* smartcard = freerdp_device_new(RDPDR_DTYP_SMARTCARD, 0, NULL); + + if (!smartcard) + return FALSE; + + if (!freerdp_device_collection_add(settings, smartcard)) + { + freerdp_device_free(smartcard); + return FALSE; + } + } + } + + if (freerdp_settings_get_bool(settings, FreeRDP_RedirectPrinters)) + { + if (!freerdp_device_collection_find_type(settings, RDPDR_DTYP_PRINT)) + { + RDPDR_DEVICE* printer = freerdp_device_new(RDPDR_DTYP_PRINT, 0, NULL); + + if (!printer) + return FALSE; + + if (!freerdp_device_collection_add(settings, printer)) + { + freerdp_device_free(printer); + return FALSE; + } + } + } + + if (freerdp_settings_get_bool(settings, FreeRDP_LyncRdpMode)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_EncomspVirtualChannel, TRUE)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_RemdeskVirtualChannel, TRUE)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_CompressionEnabled, FALSE)) + return FALSE; + } + + if (freerdp_settings_get_bool(settings, FreeRDP_RemoteAssistanceMode)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_EncomspVirtualChannel, TRUE)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_RemdeskVirtualChannel, TRUE)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_NlaSecurity, FALSE)) + return FALSE; + } + + /* step 3: schedule some static channels to load depending on the settings */ + for (size_t i = 0; i < ARRAYSIZE(staticChannels); i++) + { + if ((staticChannels[i].settingId == 0) || + freerdp_settings_get_bool(settings, staticChannels[i].settingId)) + { + if (staticChannels[i].args) + { + if (!freerdp_client_load_static_channel_addin( + channels, settings, staticChannels[i].channelName, staticChannels[i].args)) + return FALSE; + } + else + { + const char* p[] = { staticChannels[i].channelName }; + if (!freerdp_client_add_static_channel(settings, ARRAYSIZE(p), p)) + return FALSE; + } + } + } + + char* RDP2TCPArgs = freerdp_settings_get_string_writable(settings, FreeRDP_RDP2TCPArgs); + if (RDP2TCPArgs) + { + if (!freerdp_client_load_static_channel_addin(channels, settings, RDP2TCP_DVC_CHANNEL_NAME, + RDP2TCPArgs)) + return FALSE; + } + + /* step 4: do the static channels loading and init */ + for (UINT32 i = 0; i < freerdp_settings_get_uint32(settings, FreeRDP_StaticChannelCount); i++) + { + ADDIN_ARGV* _args = + freerdp_settings_get_pointer_array_writable(settings, FreeRDP_StaticChannelArray, i); + + if (!freerdp_client_load_static_channel_addin(channels, settings, _args->argv[0], _args)) + return FALSE; + } + + if (freerdp_settings_get_uint32(settings, FreeRDP_DynamicChannelCount) > 0) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportDynamicChannels, TRUE)) + return FALSE; + } + + if (freerdp_settings_get_bool(settings, FreeRDP_SupportDynamicChannels)) + { + if (!freerdp_client_load_static_channel_addin(channels, settings, DRDYNVC_SVC_CHANNEL_NAME, + settings)) + return FALSE; + } + + return TRUE; +} + +void freerdp_client_warn_unmaintained(int argc, char* argv[]) +{ + const char* app = (argc > 0) ? argv[0] : "INVALID_ARGV"; + const DWORD log_level = WLOG_WARN; + wLog* log = WLog_Get(TAG); + WINPR_ASSERT(log); + + if (!WLog_IsLevelActive(log, log_level)) + return; + + WLog_Print_unchecked(log, log_level, "[unmaintained] %s client is currently unmaintained!", + app); + WLog_Print_unchecked( + log, log_level, + " If problems occur please check https://github.com/FreeRDP/FreeRDP/issues for " + "known issues!"); + WLog_Print_unchecked( + log, log_level, + "Be prepared to fix issues yourself though as nobody is actively working on this."); + WLog_Print_unchecked( + log, log_level, + " Developers hang out in https://matrix.to/#/#FreeRDP:matrix.org?via=matrix.org " + "- dont hesitate to ask some questions. (replies might take some time depending " + "on your timezone) - if you intend using this component write us a message"); +} + +void freerdp_client_warn_experimental(int argc, char* argv[]) +{ + const char* app = (argc > 0) ? argv[0] : "INVALID_ARGV"; + const DWORD log_level = WLOG_WARN; + wLog* log = WLog_Get(TAG); + WINPR_ASSERT(log); + + if (!WLog_IsLevelActive(log, log_level)) + return; + + WLog_Print_unchecked(log, log_level, "[experimental] %s client is currently experimental!", + app); + WLog_Print_unchecked( + log, log_level, + " If problems occur please check https://github.com/FreeRDP/FreeRDP/issues for " + "known issues or create a new one!"); + WLog_Print_unchecked( + log, log_level, + " Developers hang out in https://matrix.to/#/#FreeRDP:matrix.org?via=matrix.org " + "- dont hesitate to ask some questions. (replies might take some time depending " + "on your timezone)"); +} + +void freerdp_client_warn_deprecated(int argc, char* argv[]) +{ + const char* app = (argc > 0) ? argv[0] : "INVALID_ARGV"; + const DWORD log_level = WLOG_WARN; + wLog* log = WLog_Get(TAG); + WINPR_ASSERT(log); + + if (!WLog_IsLevelActive(log, log_level)) + return; + + WLog_Print_unchecked(log, log_level, "[deprecated] %s client has been deprecated", app); + WLog_Print_unchecked(log, log_level, "As replacement there is a SDL based client available."); + WLog_Print_unchecked( + log, log_level, + "If you are interested in keeping %s alive get in touch with the developers", app); + WLog_Print_unchecked( + log, log_level, + "The project is hosted at https://github.com/freerdp/freerdp and " + " developers hang out in https://matrix.to/#/#FreeRDP:matrix.org?via=matrix.org " + "- dont hesitate to ask some questions. (replies might take some time depending " + "on your timezone)"); +} diff --git a/client/common/cmdline.h b/client/common/cmdline.h new file mode 100644 index 0000000..8186cc6 --- /dev/null +++ b/client/common/cmdline.h @@ -0,0 +1,519 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Client Command-Line Interface + * + * Copyright 2018 Bernhard Miklautz <bernhard.miklautz@thincast.com> + * Copyright 2018 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef CLIENT_COMMON_CMDLINE_H +#define CLIENT_COMMON_CMDLINE_H + +#include <freerdp/config.h> + +#include <winpr/cmdline.h> + +static const COMMAND_LINE_ARGUMENT_A global_cmd_args[] = { + { "a", COMMAND_LINE_VALUE_REQUIRED, "<addin>[,<options>]", NULL, NULL, -1, "addin", "Addin" }, + { "action-script", COMMAND_LINE_VALUE_REQUIRED, "<file-name>", "~/.config/freerdp/action.sh", + NULL, -1, NULL, "Action script" }, + { "admin", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, "console", + "Admin (or console) session" }, + { "aero", COMMAND_LINE_VALUE_BOOL, NULL, NULL, BoolValueFalse, -1, NULL, + "desktop composition" }, + { "app", COMMAND_LINE_VALUE_REQUIRED, + "program:[<path>|<||alias>],cmd:<command>,file:<filename>,guid:<guid>,icon:<filename>,name:<" + "name>,workdir:<directory>", + NULL, NULL, -1, NULL, "Remote application program" }, +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + { "app-cmd", COMMAND_LINE_VALUE_REQUIRED, "<parameters>", NULL, NULL, -1, NULL, + "[DEPRECATED, use /app:cmd:<command>] Remote application command-line parameters" }, + { "app-file", COMMAND_LINE_VALUE_REQUIRED, "<file-name>", NULL, NULL, -1, NULL, + "[DEPRECATED, use /app:file:<filename>] File to open with remote application" }, + { "app-guid", COMMAND_LINE_VALUE_REQUIRED, "<app-guid>", NULL, NULL, -1, NULL, + "[DEPRECATED, use /app:guid:<guid>] Remote application GUID" }, + { "app-icon", COMMAND_LINE_VALUE_REQUIRED, "<icon-path>", NULL, NULL, -1, NULL, + "[DEPRECATED, use /app:icon:<filename>] Remote application icon for user interface" }, + { "app-name", COMMAND_LINE_VALUE_REQUIRED, "<app-name>", NULL, NULL, -1, NULL, + "[DEPRECATED, use /app:name:<name>] Remote application name for user interface" }, + { "app-workdir", COMMAND_LINE_VALUE_REQUIRED, "<workspace path>", NULL, NULL, -1, NULL, + "[DEPRECATED, use /app:workdir:<directory>] Remote application workspace path" }, +#endif + { "assistance", COMMAND_LINE_VALUE_REQUIRED, "<password>", NULL, NULL, -1, NULL, + "Remote assistance password" }, + { "auto-request-control", COMMAND_LINE_VALUE_FLAG, "", NULL, NULL, -1, NULL, + "Automatically request remote assistance input control" }, + { "async-channels", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "Asynchronous channels (experimental)" }, + { "async-update", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "Asynchronous update" }, + { "audio-mode", COMMAND_LINE_VALUE_REQUIRED, "<mode>", NULL, NULL, -1, NULL, + "Audio output mode" }, + { "auth-only", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "Authenticate only" }, + { "auth-pkg-list", COMMAND_LINE_VALUE_REQUIRED, "<!ntlm,kerberos>", NULL, NULL, -1, NULL, + "Authentication package filter (comma-separated list, use '!' to exclude)" }, + { "authentication", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, + "Authentication (experimental)" }, + { "auto-reconnect", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "Automatic reconnection" }, + { "auto-reconnect-max-retries", COMMAND_LINE_VALUE_REQUIRED, "<retries>", NULL, NULL, -1, NULL, + "Automatic reconnection maximum retries, 0 for unlimited [0,1000]" }, +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + { "bitmap-cache", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "[DEPRECATED, use /cache:bitmap[:on|off]] bitmap cache" }, + { "persist-cache", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "[DEPRECATED, use /cache:persist[:on|off]] persistent bitmap cache" }, + { "persist-cache-file", COMMAND_LINE_VALUE_REQUIRED, "<filename>", NULL, NULL, -1, NULL, + "[DEPRECATED, use /cache:persist-file:<filename>] persistent bitmap cache file" }, +#endif + { "bpp", COMMAND_LINE_VALUE_REQUIRED, "<depth>", "16", NULL, -1, NULL, + "Session bpp (color depth)" }, + { "buildconfig", COMMAND_LINE_VALUE_FLAG | COMMAND_LINE_PRINT_BUILDCONFIG, NULL, NULL, NULL, -1, + NULL, "Print the build configuration" }, + { "cache", COMMAND_LINE_VALUE_REQUIRED, + "[bitmap[:on|off],codec[:rfx|nsc],glyph[:on|off],offscreen[:on|off],persist,persist-file:<" + "filename>]", + NULL, NULL, -1, NULL, "" }, + { "cert", COMMAND_LINE_VALUE_REQUIRED, + "[deny,ignore,name:<name>,tofu,fingerprint:<hash>:<hash as hex>[,fingerprint:<hash>:<another " + "hash>]]", + NULL, NULL, -1, NULL, + "Certificate accept options. Use with care!\n" + " * deny ... Automatically abort connection if the certificate does not match, no " + "user interaction.\n" + " * ignore ... Ignore the certificate checks altogether (overrules all other options)\n" + " * name ... Use the alternate <name> instead of the certificate subject to match " + "locally stored certificates\n" + " * tofu ... Accept certificate unconditionally on first connect and deny on " + "subsequent connections if the certificate does not match\n" + " * fingerprints ... A list of certificate hashes that are accepted unconditionally for a " + "connection" }, +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + { "cert-deny", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, NULL, + "[DEPRECATED, use /cert:deny] Automatically abort connection for any certificate that can " + "not be validated." }, + { "cert-ignore", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, NULL, + "[DEPRECATED, use /cert:ignore] Ignore certificate" }, + { "cert-name", COMMAND_LINE_VALUE_REQUIRED, "<name>", NULL, NULL, -1, NULL, + "[DEPRECATED, use /cert:name:<name>] Certificate name" }, + { "cert-tofu", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, NULL, + "[DEPRECATED, use /cert:tofu] Automatically accept certificate on first connect" }, +#endif +#ifdef _WIN32 + { "connect-child-session", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, "", + "connect to child session (win32)" }, +#endif + { "client-build-number", COMMAND_LINE_VALUE_REQUIRED, "<number>", NULL, NULL, -1, NULL, + "Client Build Number sent to server (influences smartcard behaviour, see [MS-RDPESC])" }, + { "client-hostname", COMMAND_LINE_VALUE_REQUIRED, "<name>", NULL, NULL, -1, NULL, + "Client Hostname to send to server" }, + { "clipboard", COMMAND_LINE_VALUE_BOOL | COMMAND_LINE_VALUE_OPTIONAL, + "[[use-selection:<atom>],[direction-to:[all|local|remote|off]],[files-to[:all|local|remote|" + "off]]]", + BoolValueTrue, NULL, -1, NULL, + "Redirect clipboard:\n" + " * use-selection:<atom> ... (X11) Specify which X selection to access. Default is " + "CLIPBOARD. PRIMARY is the X-style middle-click selection.\n" + " * direction-to:[all|local|remote|off] control enabled clipboard direction\n" + " * files-to:[all|local|remote|off] control enabled file clipboard directiont" }, +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + { "codec-cache", COMMAND_LINE_VALUE_REQUIRED, "[rfx|nsc|jpeg]", NULL, NULL, -1, NULL, + "[DEPRECATED, use /cache:codec:[rfx|nsc|jpeg]] Bitmap codec cache" }, +#endif + { "compression", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, "z", "compression" }, + { "compression-level", COMMAND_LINE_VALUE_REQUIRED, "<level>", NULL, NULL, -1, NULL, + "Compression level (0,1,2)" }, + { "credentials-delegation", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "credentials delegation" }, + { "d", COMMAND_LINE_VALUE_REQUIRED, "<domain>", NULL, NULL, -1, NULL, "Domain" }, + { "decorations", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, + "Window decorations" }, + { "disp", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, NULL, "Display control" }, + { "drive", COMMAND_LINE_VALUE_REQUIRED, "<name>,<path>", NULL, NULL, -1, NULL, + "Redirect directory <path> as named share <name>. Hotplug support is enabled with " + "/drive:hotplug,*. This argument provides the same function as \"Drives that I plug in " + "later\" option in MSTSC." }, + { "drives", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "Redirect all mount points as shares" }, + { "dump", COMMAND_LINE_VALUE_REQUIRED, "<record|replay>,<file>", NULL, NULL, -1, NULL, + "record or replay dump" }, + { "dvc", COMMAND_LINE_VALUE_REQUIRED, "<channel>[,<options>]", NULL, NULL, -1, NULL, + "Dynamic virtual channel" }, + { "dynamic-resolution", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, NULL, + "Send resolution updates when the window is resized" }, + { "echo", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, "echo", "Echo channel" }, + { "encryption", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, + "Encryption (experimental)" }, + { "encryption-methods", COMMAND_LINE_VALUE_REQUIRED, "[40,][56,][128,][FIPS]", NULL, NULL, -1, + NULL, "RDP standard security encryption methods" }, + { "f", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, NULL, + "Fullscreen mode (<Ctrl>+<Alt>+<Enter> toggles fullscreen)" }, + { "fipsmode", COMMAND_LINE_VALUE_BOOL, NULL, NULL, NULL, -1, NULL, "FIPS mode" }, + { "floatbar", COMMAND_LINE_VALUE_OPTIONAL, + "sticky:[on|off],default:[visible|hidden],show:[always|fullscreen|window]", NULL, NULL, -1, + NULL, + "floatbar is disabled by default (when enabled defaults to sticky in fullscreen mode)" }, + { "fonts", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, + "smooth fonts (ClearType)" }, + { "force-console-callbacks", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "Use default callbacks (console) for certificate/credential/..." }, + { "frame-ack", COMMAND_LINE_VALUE_REQUIRED, "<number>", NULL, NULL, -1, NULL, + "Number of frame acknowledgement" }, + { "args-from", COMMAND_LINE_VALUE_REQUIRED, "<file>|stdin|fd:<number>|env:<name>", NULL, NULL, + -1, NULL, + "Read command line from a file, stdin or file descriptor. This argument can not be combined " + "with any other. " + "Provide one argument per line." }, + { "from-stdin", COMMAND_LINE_VALUE_OPTIONAL, "force", NULL, NULL, -1, NULL, + "Read credentials from stdin. With <force> the prompt is done before connection, otherwise " + "on server request." }, + { "gateway", COMMAND_LINE_VALUE_REQUIRED, + "g:<gateway>[:<port>],u:<user>,d:<domain>,p:<password>,usage-method:[" + "direct|detect],access-token:<" + "token>,type:[rpc|http[,no-websockets][,extauth-sspi-ntlm]|auto[,no-websockets][,extauth-" + "sspi-ntlm]]|arm,url:<wss://url>,bearer:<oauth2-bearer-token>", + NULL, NULL, -1, "gw", "Gateway Hostname" }, +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + { "g", COMMAND_LINE_VALUE_REQUIRED, "<gateway>[:<port>]", NULL, NULL, -1, NULL, + "[DEPRECATED, use /gateway:g:<url>] Gateway Hostname" }, + { "gateway-usage-method", COMMAND_LINE_VALUE_REQUIRED, "[direct|detect]", NULL, NULL, -1, "gum", + "[DEPRECATED, use /gateway:usage-method:<method>] Gateway usage method" }, + { "gd", COMMAND_LINE_VALUE_REQUIRED, "<domain>", NULL, NULL, -1, NULL, + "[DEPRECATED, use /gateway:d:<domain>] Gateway domain" }, +#endif + { "gdi", COMMAND_LINE_VALUE_REQUIRED, "sw|hw", NULL, NULL, -1, NULL, "GDI rendering" }, + { "geometry", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, NULL, + "Geometry tracking channel" }, + { "gestures", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "Consume multitouch input locally" }, +#ifdef WITH_GFX_H264 + { "gfx", COMMAND_LINE_VALUE_OPTIONAL, + "[[progressive[:on|off]|RFX[:on|off]|AVC420[:on|off]AVC444[:on|off]],mask:<value>,small-" + "cache[:on|off],thin-client[:on|off],progressive[:on|" + "off]]", + NULL, NULL, -1, NULL, "RDP8 graphics pipeline" }, +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + { "gfx-h264", COMMAND_LINE_VALUE_OPTIONAL, "[[AVC420|AVC444],mask:<value>]", NULL, NULL, -1, + NULL, "[DEPRECATED, use /gfx:avc420] RDP8.1 graphics pipeline using H264 codec" }, +#endif +#else + { "gfx", COMMAND_LINE_VALUE_OPTIONAL, + "[progressive[:on|off]|RFX[:on|off]|AVC420[:on|off]AVC444[:on|off]],mask:<value>,small-cache[" + ":on|off],thin-client[:on|off],progressive[:on|off]]", + NULL, NULL, -1, NULL, "RDP8 graphics pipeline" }, +#endif +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + { "gfx-progressive", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "[DEPRECATED, use /gfx:progressive] RDP8 graphics pipeline using progressive codec" }, + { "gfx-small-cache", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, + "[DEPRECATED, use /gfx:small-cache] RDP8 graphics pipeline using small cache mode" }, + { "gfx-thin-client", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "[DEPRECATED, use /gfx:thin-client] RDP8 graphics pipeline using thin client mode" }, + { "glyph-cache", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "[DEPRECATED, use /cache:glyph[:on|off]] Glyph cache (experimental)" }, +#endif +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + { "gp", COMMAND_LINE_VALUE_REQUIRED, "<password>", NULL, NULL, -1, NULL, + "[DEPRECATED, use /gateway:p:<password>] Gateway password" }, +#endif + { "grab-keyboard", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, + "Grab keyboard" }, + { "grab-mouse", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, "Grab mouse" }, +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + { "gt", COMMAND_LINE_VALUE_REQUIRED, + "[rpc|http[,no-websockets][,extauth-sspi-ntlm]|auto[,no-websockets][,extauth-sspi-ntlm]]", + NULL, NULL, -1, NULL, "[DEPRECATED, use /gateway:type:<type>] Gateway transport type" }, + { "gu", COMMAND_LINE_VALUE_REQUIRED, "[[<domain>\\]<user>|<user>[@<domain>]]", NULL, NULL, -1, + NULL, "[DEPRECATED, use /gateway:u:<user>] Gateway username" }, + { "gat", COMMAND_LINE_VALUE_REQUIRED, "<access token>", NULL, NULL, -1, NULL, + "[DEPRECATED, use /gateway:access-token:<token>] Gateway Access Token" }, +#endif + { "h", COMMAND_LINE_VALUE_REQUIRED, "<height>", "768", NULL, -1, NULL, "Height" }, + { "heartbeat", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, + "Support heartbeat PDUs" }, + { "help", COMMAND_LINE_VALUE_FLAG | COMMAND_LINE_PRINT_HELP, NULL, NULL, NULL, -1, "?", + "Print help" }, + { "home-drive", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "Redirect user home as share" }, + { "ipv6", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, "6", + "Prefer IPv6 AAA record over IPv4 A record" }, +#if defined(WITH_JPEG) + { "jpeg", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, NULL, "JPEG codec support" }, + { "jpeg-quality", COMMAND_LINE_VALUE_REQUIRED, "<percentage>", NULL, NULL, -1, NULL, + "JPEG quality" }, +#endif + { "kbd", COMMAND_LINE_VALUE_REQUIRED, + "[layout:[0x<id>|<name>],lang:<0x<id>>,fn-key:<value>,type:<value>,subtype:<value>,unicode[:" + "on|off],remap:<key1>=<value1>,remap:<key2>=<value2>,pipe:<filename>]", + NULL, NULL, -1, NULL, + "Keyboard related options:\n" + " * layout: set the keybouard layout announced to the server\n" + " * lang: set the keyboard language identifier sent to the server\n" + " * fn-key: Function key value\n" + " * pipe: Name of a named pipe that can be used to type text into the RDP session\n" }, +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + { "kbd-lang", COMMAND_LINE_VALUE_REQUIRED, "0x<id>", NULL, NULL, -1, NULL, + "[DEPRECATED, use / kbd:lang:<value>] Keyboard active language identifier" }, + { "kbd-fn-key", COMMAND_LINE_VALUE_REQUIRED, "<value>", NULL, NULL, -1, NULL, + "[DEPRECATED, use /kbd:fn-key:<value>] Function key value" }, + { "kbd-list", COMMAND_LINE_VALUE_FLAG | COMMAND_LINE_PRINT, NULL, NULL, NULL, -1, NULL, + "[DEPRECATED, use /list:kbd] List keyboard layouts" }, + { "kbd-scancode-list", COMMAND_LINE_VALUE_FLAG | COMMAND_LINE_PRINT, NULL, NULL, NULL, -1, NULL, + "[DEPRECATED, use list:kbd-scancode] List keyboard RDP scancodes" }, + { "kbd-lang-list", COMMAND_LINE_VALUE_OPTIONAL | COMMAND_LINE_PRINT, NULL, NULL, NULL, -1, NULL, + "[DEPRECATED, use /list:kbd-lang] List keyboard languages" }, + { "kbd-remap", COMMAND_LINE_VALUE_REQUIRED, + "[DEPRECATED, use /kbd:remap] List of <key>=<value>,... pairs to remap scancodes", NULL, NULL, + -1, NULL, "Keyboard scancode remapping" }, + { "kbd-subtype", COMMAND_LINE_VALUE_REQUIRED, "<id>", NULL, NULL, -1, NULL, + "[DEPRECATED, use /kbd:subtype]Keyboard subtype" }, + { "kbd-type", COMMAND_LINE_VALUE_REQUIRED, "<id>", NULL, NULL, -1, NULL, + "[DEPRECATED, use /kbd:type] Keyboard type" }, + { "kbd-unicode", COMMAND_LINE_VALUE_FLAG, "", NULL, NULL, -1, NULL, + "[DEPRECATED, use /kbd:unicode[:on|off]] Send unicode symbols, e.g. use the local " + "keyboard map. ATTENTION: Does not work with every " + "RDP server!" }, +#endif + { "kerberos", COMMAND_LINE_VALUE_REQUIRED, + "[kdc-url:<url>,lifetime:<time>,start-time:<time>,renewable-lifetime:<time>,cache:<path>," + "armor:<path>,pkinit-anchors:<path>,pkcs11-module:<name>]", + NULL, NULL, -1, NULL, "Kerberos options" }, + { "load-balance-info", COMMAND_LINE_VALUE_REQUIRED, "<info-string>", NULL, NULL, -1, NULL, + "Load balance info" }, + { "list", COMMAND_LINE_VALUE_REQUIRED | COMMAND_LINE_PRINT, + "[kbd|kbd-scancode|kbd-lang[:<value>]|smartcard[:[pkinit-anchors:<path>][,pkcs11-module:<" + "name>]]|" + "monitor|tune]", + "List available options for subcommand", NULL, -1, NULL, + "List available options for subcommand" }, + { "log-filters", COMMAND_LINE_VALUE_REQUIRED, "<tag>:<level>[,<tag>:<level>[,...]]", NULL, NULL, + -1, NULL, "Set logger filters, see wLog(7) for details" }, + { "log-level", COMMAND_LINE_VALUE_REQUIRED, "[OFF|FATAL|ERROR|WARN|INFO|DEBUG|TRACE]", NULL, + NULL, -1, NULL, "Set the default log level, see wLog(7) for details" }, + { "max-fast-path-size", COMMAND_LINE_VALUE_REQUIRED, "<size>", NULL, NULL, -1, NULL, + "Specify maximum fast-path update size" }, + { "max-loop-time", COMMAND_LINE_VALUE_REQUIRED, "<time>", NULL, NULL, -1, NULL, + "Specify maximum time in milliseconds spend treating packets" }, + { "menu-anims", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "menu animations" }, + { "microphone", COMMAND_LINE_VALUE_OPTIONAL, + "[sys:<sys>,][dev:<dev>,][format:<format>,][rate:<rate>,][channel:<channel>]", NULL, NULL, -1, + "mic", "Audio input (microphone)" }, +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + { "smartcard-list", COMMAND_LINE_VALUE_FLAG | COMMAND_LINE_PRINT, NULL, NULL, NULL, -1, NULL, + "[DEPRECATED, use /list:smartcard] List smartcard informations" }, + { "monitor-list", COMMAND_LINE_VALUE_FLAG | COMMAND_LINE_PRINT, NULL, NULL, NULL, -1, NULL, + "[DEPRECATED, use /list:monitor] List detected monitors" }, +#endif + { "monitors", COMMAND_LINE_VALUE_REQUIRED, "<id>[,<id>[,...]]", NULL, NULL, -1, NULL, + "Select monitors to use" }, + { "mouse-motion", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, + "Send mouse motion" }, + { "mouse-relative", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "Send mouse motion with relative addressing" }, + { "mouse", COMMAND_LINE_VALUE_REQUIRED, "[relative:[on|off],grab:[on|off]]", NULL, NULL, -1, + NULL, + "Mouse related options:\n" + " * relative: send relative mouse movements if supported by server\n" + " * grab: grab the mouse if within the window" }, +#if defined(CHANNEL_TSMF_CLIENT) + { "multimedia", COMMAND_LINE_VALUE_OPTIONAL, "[sys:<sys>,][dev:<dev>,][decoder:<decoder>]", + NULL, NULL, -1, "mmr", "[DEPRECATED], use /video] Redirect multimedia (video)" }, +#endif + { "multimon", COMMAND_LINE_VALUE_OPTIONAL, "force", NULL, NULL, -1, NULL, + "Use multiple monitors" }, + { "multitouch", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "Redirect multitouch input" }, + { "multitransport", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, + "Support multitransport protocol" }, + { "nego", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, + "protocol security negotiation" }, + { "network", COMMAND_LINE_VALUE_REQUIRED, + "[modem|broadband|broadband-low|broadband-high|wan|lan|auto]", NULL, NULL, -1, NULL, + "Network connection type" }, + { "nsc", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, "nscodec", "NSCodec support" }, +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + { "offscreen-cache", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "[DEPRECATED, use /cache:offscreen[:on|off]] offscreen bitmap cache" }, +#endif + { "orientation", COMMAND_LINE_VALUE_REQUIRED, "[0|90|180|270]", NULL, NULL, -1, NULL, + "Orientation of display in degrees" }, + { "old-license", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "Use the old license workflow (no CAL and hwId set to 0)" }, + { "p", COMMAND_LINE_VALUE_REQUIRED, "<password>", NULL, NULL, -1, NULL, "Password" }, + { "parallel", COMMAND_LINE_VALUE_OPTIONAL, "<name>[,<path>]", NULL, NULL, -1, NULL, + "Redirect parallel device" }, + { "parent-window", COMMAND_LINE_VALUE_REQUIRED, "<window-id>", NULL, NULL, -1, NULL, + "Parent window id" }, + { "pcb", COMMAND_LINE_VALUE_REQUIRED, "<blob>", NULL, NULL, -1, NULL, "Preconnection Blob" }, + { "pcid", COMMAND_LINE_VALUE_REQUIRED, "<id>", NULL, NULL, -1, NULL, "Preconnection Id" }, + { "pheight", COMMAND_LINE_VALUE_REQUIRED, "<height>", NULL, NULL, -1, NULL, + "Physical height of display (in millimeters)" }, + { "play-rfx", COMMAND_LINE_VALUE_REQUIRED, "<pcap-file>", NULL, NULL, -1, NULL, + "Replay rfx pcap file" }, + { "port", COMMAND_LINE_VALUE_REQUIRED, "<number>", NULL, NULL, -1, NULL, "Server port" }, + { "suppress-output", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, + "suppress output when minimized" }, + { "print-reconnect-cookie", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "Print base64 reconnect cookie after connecting" }, + { "printer", COMMAND_LINE_VALUE_OPTIONAL, "<name>[,<driver>]", NULL, NULL, -1, NULL, + "Redirect printer device" }, + { "proxy", COMMAND_LINE_VALUE_REQUIRED, "[<proto>://][<user>:<password>@]<host>[:<port>]", NULL, + NULL, -1, NULL, + "Proxy settings: override env. var (see also environment variable below). Protocol " + "\"socks5\" should be given explicitly where \"http\" is default." }, + { "pth", COMMAND_LINE_VALUE_REQUIRED, "<password-hash>", NULL, NULL, -1, "pass-the-hash", + "Pass the hash (restricted admin mode)" }, + { "pwidth", COMMAND_LINE_VALUE_REQUIRED, "<width>", NULL, NULL, -1, NULL, + "Physical width of display (in millimeters)" }, + { "rdp2tcp", COMMAND_LINE_VALUE_REQUIRED, "<executable path[:arg...]>", NULL, NULL, -1, NULL, + "TCP redirection" }, + { "reconnect-cookie", COMMAND_LINE_VALUE_REQUIRED, "<base64-cookie>", NULL, NULL, -1, NULL, + "Pass base64 reconnect cookie to the connection" }, + { "redirect-prefer", COMMAND_LINE_VALUE_REQUIRED, "<FQDN|IP|NETBIOS>,[...]", NULL, NULL, -1, + NULL, "Override the preferred redirection order" }, + { "relax-order-checks", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, "relax-order-checks", + "Do not check if a RDP order was announced during capability exchange, only use when " + "connecting to a buggy server" }, + { "restricted-admin", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, "restrictedAdmin", + "Restricted admin mode" }, + { "rfx", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, NULL, "RemoteFX" }, + { "rfx-mode", COMMAND_LINE_VALUE_REQUIRED, "[image|video]", NULL, NULL, -1, NULL, + "RemoteFX mode" }, + { "scale", COMMAND_LINE_VALUE_REQUIRED, "[100|140|180]", "100", NULL, -1, NULL, + "Scaling factor of the display" }, + { "scale-desktop", COMMAND_LINE_VALUE_REQUIRED, "<percentage>", "100", NULL, -1, NULL, + "Scaling factor for desktop applications (value between 100 and 500)" }, + { "scale-device", COMMAND_LINE_VALUE_REQUIRED, "100|140|180", "100", NULL, -1, NULL, + "Scaling factor for app store applications" }, + { "sec", COMMAND_LINE_VALUE_REQUIRED, + "[rdp[:[on|off]]|tls[:[on|off]]|nla[:[on|off]]|ext[:[on|off]]|aad[:[on|off]]]", NULL, NULL, + -1, NULL, + "Force specific protocol security. e.g. /sec:nla enables NLA and disables all others, while " + "/sec:nla:[on|off] just toggles NLA" }, +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + { "sec-ext", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "[DEPRECATED, use /sec:ext] NLA extended protocol security" }, + { "sec-nla", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, + "[DEPRECATED, use /sec:nla] NLA protocol security" }, + { "sec-rdp", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, + "[DEPRECATED, use /sec:rdp] RDP protocol security" }, + { "sec-tls", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, + "[DEPRECATED, use /sec:tls] TLS protocol security" }, +#endif + { "serial", COMMAND_LINE_VALUE_OPTIONAL, "<name>[,<path>[,<driver>[,permissive]]]", NULL, NULL, + -1, "tty", "Redirect serial device" }, + { "server-name", COMMAND_LINE_VALUE_REQUIRED, "<name>", NULL, NULL, -1, NULL, + "User-specified server name to use for validation (TLS, Kerberos)" }, + { "shell", COMMAND_LINE_VALUE_REQUIRED, "<shell>", NULL, NULL, -1, NULL, "Alternate shell" }, + { "shell-dir", COMMAND_LINE_VALUE_REQUIRED, "<dir>", NULL, NULL, -1, NULL, + "Shell working directory" }, + { "size", COMMAND_LINE_VALUE_REQUIRED, "<width>x<height> or <percent>%[wh]", "1024x768", NULL, + -1, NULL, "Screen size" }, + { "smart-sizing", COMMAND_LINE_VALUE_OPTIONAL, "<width>x<height>", NULL, NULL, -1, NULL, + "Scale remote desktop to window size" }, + { "smartcard", COMMAND_LINE_VALUE_OPTIONAL, "<str>[,<str>...]", NULL, NULL, -1, NULL, + "Redirect the smartcard devices containing any of the <str> in their names." }, + { "smartcard-logon", COMMAND_LINE_VALUE_OPTIONAL, + "[cert:<path>,key:<key>,pin:<pin>,csp:<csp name>,reader:<reader>,card:<card>]", NULL, NULL, + -1, NULL, "Activates Smartcard (optional certificate) Logon authentication." }, + { "sound", COMMAND_LINE_VALUE_OPTIONAL, + "[sys:<sys>,][dev:<dev>,][format:<format>,][rate:<rate>,][channel:<channel>,][latency:<" + "latency>,][quality:<quality>]", + NULL, NULL, -1, "audio", "Audio output (sound)" }, + { "span", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, NULL, + "Span screen over multiple monitors" }, + { "spn-class", COMMAND_LINE_VALUE_REQUIRED, "<service-class>", NULL, NULL, -1, NULL, + "SPN authentication service class" }, + { "ssh-agent", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, "ssh-agent", + "SSH Agent forwarding channel" }, + { "sspi-module", COMMAND_LINE_VALUE_REQUIRED, "<SSPI module path>", NULL, NULL, -1, NULL, + "SSPI shared library module file path" }, + { "winscard-module", COMMAND_LINE_VALUE_REQUIRED, "<WinSCard module path>", NULL, NULL, -1, + NULL, "WinSCard shared library module file path" }, + { "disable-output", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, NULL, + "Deactivate all graphics decoding in the client session. Useful for load tests with many " + "simultaneous connections" }, + { "t", COMMAND_LINE_VALUE_REQUIRED, "<title>", NULL, NULL, -1, "title", "Window title" }, + { "themes", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, "themes" }, + { "timeout", COMMAND_LINE_VALUE_REQUIRED, "<time in ms>", "9000", NULL, -1, "timeout", + "Advanced setting for high latency links: Adjust connection timeout, use if you encounter " + "timeout failures with your connection" }, + { "tls", COMMAND_LINE_VALUE_REQUIRED, "[ciphers|seclevel|secrets-file|enforce]", NULL, NULL, -1, + NULL, + "TLS configuration options:" + " * ciphers:[netmon|ma|<cipher names>]\n" + " * seclevel:<level>, default: 1, range: [0-5] Override the default TLS security level, " + "might be required for older target servers\n" + " * secrets-file:<filename>\n" + " * enforce[:[ssl3|1.0|1.1|1.2|1.3]] Force use of SSL/TLS version for a connection. Some " + "servers have a buggy TLS " + "version negotiation and might fail without this. Defaults to TLS 1.2 if no argument is " + "supplied. Use 1.0 for windows 7" }, +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + { "tls-ciphers", COMMAND_LINE_VALUE_REQUIRED, "[netmon|ma|ciphers]", NULL, NULL, -1, NULL, + "[DEPRECATED, use /tls:ciphers] Allowed TLS ciphers" }, + { "tls-seclevel", COMMAND_LINE_VALUE_REQUIRED, "<level>", "1", NULL, -1, NULL, + "[DEPRECATED, use /tls:seclevel] TLS security level - defaults to 1" }, + { "tls-secrets-file", COMMAND_LINE_VALUE_REQUIRED, "<filename>", NULL, NULL, -1, NULL, + "[DEPRECATED, use /tls:secrets:file] File were TLS secrets will be stored in the " + "SSLKEYLOGFILE format" }, + { "enforce-tlsv1_2", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "[DEPRECATED, use /tls:enforce:1.2] Force use of TLS1.2 for connection. Some " + "servers have a buggy TLS version negotiation and " + "might fail without this" }, +#endif + { "toggle-fullscreen", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, + "Alt+Ctrl+Enter to toggle fullscreen" }, + { "tune", COMMAND_LINE_VALUE_REQUIRED, "<setting:value>,<setting:value>", "", NULL, -1, NULL, + "[experimental] directly manipulate freerdp settings, use with extreme caution!" }, +#if defined(WITH_FREERDP_DEPRECATED_COMMANDLINE) + { "tune-list", COMMAND_LINE_VALUE_FLAG | COMMAND_LINE_PRINT, NULL, NULL, NULL, -1, NULL, + "[DEPRECATED, use /list:tune] Print options allowed for /tune" }, +#endif + { "u", COMMAND_LINE_VALUE_REQUIRED, "[[<domain>\\]<user>|<user>[@<domain>]]", NULL, NULL, -1, + NULL, "Username" }, + { "unmap-buttons", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "Let server see real physical pointer button" }, +#ifdef CHANNEL_URBDRC_CLIENT + { "usb", COMMAND_LINE_VALUE_REQUIRED, + "[dbg,][id:<vid>:<pid>#...,][addr:<bus>:<addr>#...,][auto]", NULL, NULL, -1, NULL, + "Redirect USB device" }, +#endif + { "v", COMMAND_LINE_VALUE_REQUIRED, "<server>[:port]", NULL, NULL, -1, NULL, + "Server hostname" }, + { "vc", COMMAND_LINE_VALUE_REQUIRED, "<channel>[,<options>]", NULL, NULL, -1, NULL, + "Static virtual channel" }, + { "version", COMMAND_LINE_VALUE_FLAG | COMMAND_LINE_PRINT_VERSION, NULL, NULL, NULL, -1, NULL, + "Print version" }, + { "video", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, NULL, + "Video optimized remoting channel" }, + { "prevent-session-lock", COMMAND_LINE_VALUE_OPTIONAL, "<time in sec>", NULL, NULL, -1, NULL, + "Prevent session locking by injecting fake mouse motion events to the server " + "when the connection is idle (default interval: 180 seconds)" }, + { "vmconnect", COMMAND_LINE_VALUE_OPTIONAL, "<vmid>", NULL, NULL, -1, NULL, + "Hyper-V console (use port 2179, disable negotiation)" }, + { "w", COMMAND_LINE_VALUE_REQUIRED, "<width>", "1024", NULL, -1, NULL, "Width" }, + { "wallpaper", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, "wallpaper" }, + { "window-drag", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL, + "full window drag" }, + { "window-position", COMMAND_LINE_VALUE_REQUIRED, "<xpos>x<ypos>", NULL, NULL, -1, NULL, + "window position" }, + { "wm-class", COMMAND_LINE_VALUE_REQUIRED, "<class-name>", NULL, NULL, -1, NULL, + "Set the WM_CLASS hint for the window instance" }, + { "workarea", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, NULL, "Use available work area" }, + { NULL, 0, NULL, NULL, NULL, -1, NULL, NULL } +}; +#endif /* CLIENT_COMMON_CMDLINE_H */ diff --git a/client/common/file.c b/client/common/file.c new file mode 100644 index 0000000..760c62e --- /dev/null +++ b/client/common/file.c @@ -0,0 +1,2707 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * .rdp file + * + * Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <errno.h> +#include <ctype.h> +#include <stdlib.h> + +#include <winpr/string.h> +#include <winpr/file.h> + +#include <freerdp/client.h> +#include <freerdp/client/file.h> +#include <freerdp/client/cmdline.h> + +#include <freerdp/channels/urbdrc.h> +#include <freerdp/channels/rdpecam.h> +#include <freerdp/channels/location.h> + +/** + * Remote Desktop Plus - Overview of .rdp file settings: + * http://www.donkz.nl/files/rdpsettings.html + * + * RDP Settings for Remote Desktop Services in Windows Server 2008 R2: + * http://technet.microsoft.com/en-us/library/ff393699/ + * + * https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/rdp-files + */ + +#include <stdio.h> +#include <string.h> + +#include <winpr/wtypes.h> +#include <winpr/crt.h> +#include <winpr/path.h> +#include <freerdp/log.h> +#define TAG CLIENT_TAG("common") + +/*#define DEBUG_CLIENT_FILE 1*/ + +static const BYTE BOM_UTF16_LE[2] = { 0xFF, 0xFE }; + +#define INVALID_INTEGER_VALUE 0xFFFFFFFF + +#define RDP_FILE_LINE_FLAG_FORMATTED 0x00000001 +#define RDP_FILE_LINE_FLAG_STANDARD 0x00000002 +#define RDP_FILE_LINE_FLAG_TYPE_STRING 0x00000010 +#define RDP_FILE_LINE_FLAG_TYPE_INTEGER 0x00000020 +#define RDP_FILE_LINE_FLAG_TYPE_BINARY 0x00000040 + +struct rdp_file_line +{ + char* name; + LPSTR sValue; + PBYTE bValue; + + size_t index; + + long iValue; + DWORD flags; + int valueLength; +}; +typedef struct rdp_file_line rdpFileLine; + +struct rdp_file +{ + DWORD UseMultiMon; /* use multimon */ + LPSTR SelectedMonitors; /* selectedmonitors */ + DWORD MaximizeToCurrentDisplays; /* maximizetocurrentdisplays */ + DWORD SingleMonInWindowedMode; /* singlemoninwindowedmode */ + DWORD ScreenModeId; /* screen mode id */ + DWORD SpanMonitors; /* span monitors */ + DWORD SmartSizing; /* smartsizing */ + DWORD DynamicResolution; /* dynamic resolution */ + DWORD EnableSuperSpan; /* enablesuperpan */ + DWORD SuperSpanAccelerationFactor; /* superpanaccelerationfactor */ + + DWORD DesktopWidth; /* desktopwidth */ + DWORD DesktopHeight; /* desktopheight */ + DWORD DesktopSizeId; /* desktop size id */ + DWORD SessionBpp; /* session bpp */ + DWORD DesktopScaleFactor; /* desktopscalefactor */ + + DWORD Compression; /* compression */ + DWORD KeyboardHook; /* keyboardhook */ + DWORD DisableCtrlAltDel; /* disable ctrl+alt+del */ + + DWORD AudioMode; /* audiomode */ + DWORD AudioQualityMode; /* audioqualitymode */ + DWORD AudioCaptureMode; /* audiocapturemode */ + DWORD EncodeRedirectedVideoCapture; /* encode redirected video capture */ + DWORD RedirectedVideoCaptureEncodingQuality; /* redirected video capture encoding quality */ + DWORD VideoPlaybackMode; /* videoplaybackmode */ + + DWORD ConnectionType; /* connection type */ + + DWORD NetworkAutoDetect; /* networkautodetect */ + DWORD BandwidthAutoDetect; /* bandwidthautodetect */ + + DWORD PinConnectionBar; /* pinconnectionbar */ + DWORD DisplayConnectionBar; /* displayconnectionbar */ + + DWORD WorkspaceId; /* workspaceid */ + DWORD EnableWorkspaceReconnect; /* enableworkspacereconnect */ + + DWORD DisableWallpaper; /* disable wallpaper */ + DWORD AllowFontSmoothing; /* allow font smoothing */ + DWORD AllowDesktopComposition; /* allow desktop composition */ + DWORD DisableFullWindowDrag; /* disable full window drag */ + DWORD DisableMenuAnims; /* disable menu anims */ + DWORD DisableThemes; /* disable themes */ + DWORD DisableCursorSetting; /* disable cursor setting */ + + DWORD BitmapCacheSize; /* bitmapcachesize */ + DWORD BitmapCachePersistEnable; /* bitmapcachepersistenable */ + + DWORD ServerPort; /* server port */ + + LPSTR Username; /* username */ + LPSTR Domain; /* domain */ + LPSTR Password; /*password*/ + PBYTE Password51; /* password 51 */ + + LPSTR FullAddress; /* full address */ + LPSTR AlternateFullAddress; /* alternate full address */ + + LPSTR UsbDevicesToRedirect; /* usbdevicestoredirect */ + DWORD RedirectDrives; /* redirectdrives */ + DWORD RedirectPrinters; /* redirectprinters */ + DWORD RedirectComPorts; /* redirectcomports */ + DWORD RedirectLocation; /* redirectlocation */ + DWORD RedirectSmartCards; /* redirectsmartcards */ + DWORD RedirectWebauthN; /* redirectwebauthn */ + LPSTR RedirectCameras; /* camerastoredirect */ + DWORD RedirectClipboard; /* redirectclipboard */ + DWORD RedirectPosDevices; /* redirectposdevices */ + DWORD RedirectDirectX; /* redirectdirectx */ + DWORD DisablePrinterRedirection; /* disableprinterredirection */ + DWORD DisableClipboardRedirection; /* disableclipboardredirection */ + + DWORD ConnectToConsole; /* connect to console */ + DWORD AdministrativeSession; /* administrative session */ + DWORD AutoReconnectionEnabled; /* autoreconnection enabled */ + DWORD AutoReconnectMaxRetries; /* autoreconnect max retries */ + + DWORD PublicMode; /* public mode */ + DWORD AuthenticationLevel; /* authentication level */ + DWORD PromptCredentialOnce; /* promptcredentialonce */ + DWORD PromptForCredentials; /* prompt for credentials */ + DWORD NegotiateSecurityLayer; /* negotiate security layer */ + DWORD EnableCredSSPSupport; /* enablecredsspsupport */ + DWORD EnableRdsAadAuth; /* enablerdsaadauth */ + + DWORD RemoteApplicationMode; /* remoteapplicationmode */ + LPSTR LoadBalanceInfo; /* loadbalanceinfo */ + + LPSTR RemoteApplicationName; /* remoteapplicationname */ + LPSTR RemoteApplicationIcon; /* remoteapplicationicon */ + LPSTR RemoteApplicationProgram; /* remoteapplicationprogram */ + LPSTR RemoteApplicationFile; /* remoteapplicationfile */ + LPSTR RemoteApplicationGuid; /* remoteapplicationguid */ + LPSTR RemoteApplicationCmdLine; /* remoteapplicationcmdline */ + DWORD RemoteApplicationExpandCmdLine; /* remoteapplicationexpandcmdline */ + DWORD RemoteApplicationExpandWorkingDir; /* remoteapplicationexpandworkingdir */ + DWORD DisableConnectionSharing; /* disableconnectionsharing */ + DWORD DisableRemoteAppCapsCheck; /* disableremoteappcapscheck */ + + LPSTR AlternateShell; /* alternate shell */ + LPSTR ShellWorkingDirectory; /* shell working directory */ + + LPSTR GatewayHostname; /* gatewayhostname */ + DWORD GatewayUsageMethod; /* gatewayusagemethod */ + DWORD GatewayProfileUsageMethod; /* gatewayprofileusagemethod */ + DWORD GatewayCredentialsSource; /* gatewaycredentialssource */ + + LPSTR ResourceProvider; /* resourceprovider */ + + LPSTR WvdEndpointPool; /* wvd endpoint pool */ + LPSTR geo; /* geo */ + LPSTR armpath; /* armpath */ + LPSTR aadtenantid; /* aadtenantid" */ + LPSTR diagnosticserviceurl; /* diagnosticserviceurl */ + LPSTR hubdiscoverygeourl; /* hubdiscoverygeourl" */ + LPSTR activityhint; /* activityhint */ + + DWORD UseRedirectionServerName; /* use redirection server name */ + + LPSTR GatewayAccessToken; /* gatewayaccesstoken */ + + LPSTR DrivesToRedirect; /* drivestoredirect */ + LPSTR DevicesToRedirect; /* devicestoredirect */ + LPSTR WinPosStr; /* winposstr */ + + LPSTR PreconnectionBlob; /* pcb */ + + LPSTR KdcProxyName; /* kdcproxyname */ + DWORD RdgIsKdcProxy; /* rdgiskdcproxy */ + + DWORD align1; + + size_t lineCount; + size_t lineSize; + rdpFileLine* lines; + + ADDIN_ARGV* args; + void* context; + + DWORD flags; +}; + +static const char key_str_username[] = "username"; +static const char key_str_domain[] = "domain"; +static const char key_str_password[] = "password"; +static const char key_str_full_address[] = "full address"; +static const char key_str_alternate_full_address[] = "alternate full address"; +static const char key_str_usbdevicestoredirect[] = "usbdevicestoredirect"; +static const char key_str_camerastoredirect[] = "camerastoredirect"; +static const char key_str_loadbalanceinfo[] = "loadbalanceinfo"; +static const char key_str_remoteapplicationname[] = "remoteapplicationname"; +static const char key_str_remoteapplicationicon[] = "remoteapplicationicon"; +static const char key_str_remoteapplicationprogram[] = "remoteapplicationprogram"; +static const char key_str_remoteapplicationfile[] = "remoteapplicationfile"; +static const char key_str_remoteapplicationguid[] = "remoteapplicationguid"; +static const char key_str_remoteapplicationcmdline[] = "remoteapplicationcmdline"; +static const char key_str_alternate_shell[] = "alternate shell"; +static const char key_str_shell_working_directory[] = "shell working directory"; +static const char key_str_gatewayhostname[] = "gatewayhostname"; +static const char key_str_gatewayaccesstoken[] = "gatewayaccesstoken"; +static const char key_str_resourceprovider[] = "resourceprovider"; +static const char str_resourceprovider_arm[] = "arm"; +static const char key_str_kdcproxyname[] = "kdcproxyname"; +static const char key_str_drivestoredirect[] = "drivestoredirect"; +static const char key_str_devicestoredirect[] = "devicestoredirect"; +static const char key_str_winposstr[] = "winposstr"; +static const char key_str_pcb[] = "pcb"; +static const char key_str_selectedmonitors[] = "selectedmonitors"; + +static const char key_str_wvd[] = "wvd endpoint pool"; +static const char key_str_geo[] = "geo"; +static const char key_str_armpath[] = "armpath"; +static const char key_str_aadtenantid[] = "aadtenantid"; + +static const char key_str_diagnosticserviceurl[] = "diagnosticserviceurl"; +static const char key_str_hubdiscoverygeourl[] = "hubdiscoverygeourl"; + +static const char key_str_activityhint[] = "activityhint"; + +static const char key_int_rdgiskdcproxy[] = "rdgiskdcproxy"; +static const char key_int_use_redirection_server_name[] = "use redirection server name"; +static const char key_int_gatewaycredentialssource[] = "gatewaycredentialssource"; +static const char key_int_gatewayprofileusagemethod[] = "gatewayprofileusagemethod"; +static const char key_int_gatewayusagemethod[] = "gatewayusagemethod"; +static const char key_int_disableremoteappcapscheck[] = "disableremoteappcapscheck"; +static const char key_int_disableconnectionsharing[] = "disableconnectionsharing"; +static const char key_int_remoteapplicationexpandworkingdir[] = "remoteapplicationexpandworkingdir"; +static const char key_int_remoteapplicationexpandcmdline[] = "remoteapplicationexpandcmdline"; +static const char key_int_remoteapplicationmode[] = "remoteapplicationmode"; +static const char key_int_enablecredsspsupport[] = "enablecredsspsupport"; +static const char key_int_enablerdsaadauth[] = "enablerdsaadauth"; +static const char key_int_negotiate_security_layer[] = "negotiate security layer"; +static const char key_int_prompt_for_credentials[] = "prompt for credentials"; +static const char key_int_promptcredentialonce[] = "promptcredentialonce"; +static const char key_int_authentication_level[] = "authentication level"; +static const char key_int_public_mode[] = "public mode"; +static const char key_int_autoreconnect_max_retries[] = "autoreconnect max retries"; +static const char key_int_autoreconnection_enabled[] = "autoreconnection enabled"; +static const char key_int_administrative_session[] = "administrative session"; +static const char key_int_connect_to_console[] = "connect to console"; +static const char key_int_disableclipboardredirection[] = "disableclipboardredirection"; +static const char key_int_disableprinterredirection[] = "disableprinterredirection"; +static const char key_int_redirectdirectx[] = "redirectdirectx"; +static const char key_int_redirectposdevices[] = "redirectposdevices"; +static const char key_int_redirectclipboard[] = "redirectclipboard"; +static const char key_int_redirectsmartcards[] = "redirectsmartcards"; +static const char key_int_redirectcomports[] = "redirectcomports"; +static const char key_int_redirectlocation[] = "redirectlocation"; +static const char key_int_redirectprinters[] = "redirectprinters"; +static const char key_int_redirectdrives[] = "redirectdrives"; +static const char key_int_server_port[] = "server port"; +static const char key_int_bitmapcachepersistenable[] = "bitmapcachepersistenable"; +static const char key_int_bitmapcachesize[] = "bitmapcachesize"; +static const char key_int_disable_cursor_setting[] = "disable cursor setting"; +static const char key_int_disable_themes[] = "disable themes"; +static const char key_int_disable_menu_anims[] = "disable menu anims"; +static const char key_int_disable_full_window_drag[] = "disable full window drag"; +static const char key_int_allow_desktop_composition[] = "allow desktop composition"; +static const char key_int_allow_font_smoothing[] = "allow font smoothing"; +static const char key_int_disable_wallpaper[] = "disable wallpaper"; +static const char key_int_enableworkspacereconnect[] = "enableworkspacereconnect"; +static const char key_int_workspaceid[] = "workspaceid"; +static const char key_int_displayconnectionbar[] = "displayconnectionbar"; +static const char key_int_pinconnectionbar[] = "pinconnectionbar"; +static const char key_int_bandwidthautodetect[] = "bandwidthautodetect"; +static const char key_int_networkautodetect[] = "networkautodetect"; +static const char key_int_connection_type[] = "connection type"; +static const char key_int_videoplaybackmode[] = "videoplaybackmode"; +static const char key_int_redirected_video_capture_encoding_quality[] = + "redirected video capture encoding quality"; +static const char key_int_encode_redirected_video_capture[] = "encode redirected video capture"; +static const char key_int_audiocapturemode[] = "audiocapturemode"; +static const char key_int_audioqualitymode[] = "audioqualitymode"; +static const char key_int_audiomode[] = "audiomode"; +static const char key_int_disable_ctrl_alt_del[] = "disable ctrl+alt+del"; +static const char key_int_keyboardhook[] = "keyboardhook"; +static const char key_int_compression[] = "compression"; +static const char key_int_desktopscalefactor[] = "desktopscalefactor"; +static const char key_int_session_bpp[] = "session bpp"; +static const char key_int_desktop_size_id[] = "desktop size id"; +static const char key_int_desktopheight[] = "desktopheight"; +static const char key_int_desktopwidth[] = "desktopwidth"; +static const char key_int_superpanaccelerationfactor[] = "superpanaccelerationfactor"; +static const char key_int_enablesuperpan[] = "enablesuperpan"; +static const char key_int_dynamic_resolution[] = "dynamic resolution"; +static const char key_int_smart_sizing[] = "smart sizing"; +static const char key_int_span_monitors[] = "span monitors"; +static const char key_int_screen_mode_id[] = "screen mode id"; +static const char key_int_singlemoninwindowedmode[] = "singlemoninwindowedmode"; +static const char key_int_maximizetocurrentdisplays[] = "maximizetocurrentdisplays"; +static const char key_int_use_multimon[] = "use multimon"; +static const char key_int_redirectwebauthn[] = "redirectwebauthn"; + +static SSIZE_T freerdp_client_rdp_file_add_line(rdpFile* file); +static rdpFileLine* freerdp_client_rdp_file_find_line_by_name(const rdpFile* file, + const char* name); +static void freerdp_client_file_string_check_free(LPSTR str); + +static BOOL freerdp_client_rdp_file_find_integer_entry(rdpFile* file, const char* name, + DWORD** outValue, rdpFileLine** outLine) +{ + WINPR_ASSERT(file); + WINPR_ASSERT(name); + WINPR_ASSERT(outValue); + WINPR_ASSERT(outLine); + + *outValue = NULL; + *outLine = NULL; + + if (_stricmp(name, key_int_use_multimon) == 0) + *outValue = &file->UseMultiMon; + else if (_stricmp(name, key_int_maximizetocurrentdisplays) == 0) + *outValue = &file->MaximizeToCurrentDisplays; + else if (_stricmp(name, key_int_singlemoninwindowedmode) == 0) + *outValue = &file->SingleMonInWindowedMode; + else if (_stricmp(name, key_int_screen_mode_id) == 0) + *outValue = &file->ScreenModeId; + else if (_stricmp(name, key_int_span_monitors) == 0) + *outValue = &file->SpanMonitors; + else if (_stricmp(name, key_int_smart_sizing) == 0) + *outValue = &file->SmartSizing; + else if (_stricmp(name, key_int_dynamic_resolution) == 0) + *outValue = &file->DynamicResolution; + else if (_stricmp(name, key_int_enablesuperpan) == 0) + *outValue = &file->EnableSuperSpan; + else if (_stricmp(name, key_int_superpanaccelerationfactor) == 0) + *outValue = &file->SuperSpanAccelerationFactor; + else if (_stricmp(name, key_int_desktopwidth) == 0) + *outValue = &file->DesktopWidth; + else if (_stricmp(name, key_int_desktopheight) == 0) + *outValue = &file->DesktopHeight; + else if (_stricmp(name, key_int_desktop_size_id) == 0) + *outValue = &file->DesktopSizeId; + else if (_stricmp(name, key_int_session_bpp) == 0) + *outValue = &file->SessionBpp; + else if (_stricmp(name, key_int_desktopscalefactor) == 0) + *outValue = &file->DesktopScaleFactor; + else if (_stricmp(name, key_int_compression) == 0) + *outValue = &file->Compression; + else if (_stricmp(name, key_int_keyboardhook) == 0) + *outValue = &file->KeyboardHook; + else if (_stricmp(name, key_int_disable_ctrl_alt_del) == 0) + *outValue = &file->DisableCtrlAltDel; + else if (_stricmp(name, key_int_audiomode) == 0) + *outValue = &file->AudioMode; + else if (_stricmp(name, key_int_audioqualitymode) == 0) + *outValue = &file->AudioQualityMode; + else if (_stricmp(name, key_int_audiocapturemode) == 0) + *outValue = &file->AudioCaptureMode; + else if (_stricmp(name, key_int_encode_redirected_video_capture) == 0) + *outValue = &file->EncodeRedirectedVideoCapture; + else if (_stricmp(name, key_int_redirected_video_capture_encoding_quality) == 0) + *outValue = &file->RedirectedVideoCaptureEncodingQuality; + else if (_stricmp(name, key_int_videoplaybackmode) == 0) + *outValue = &file->VideoPlaybackMode; + else if (_stricmp(name, key_int_connection_type) == 0) + *outValue = &file->ConnectionType; + else if (_stricmp(name, key_int_networkautodetect) == 0) + *outValue = &file->NetworkAutoDetect; + else if (_stricmp(name, key_int_bandwidthautodetect) == 0) + *outValue = &file->BandwidthAutoDetect; + else if (_stricmp(name, key_int_pinconnectionbar) == 0) + *outValue = &file->PinConnectionBar; + else if (_stricmp(name, key_int_displayconnectionbar) == 0) + *outValue = &file->DisplayConnectionBar; + else if (_stricmp(name, key_int_workspaceid) == 0) + *outValue = &file->WorkspaceId; + else if (_stricmp(name, key_int_enableworkspacereconnect) == 0) + *outValue = &file->EnableWorkspaceReconnect; + else if (_stricmp(name, key_int_disable_wallpaper) == 0) + *outValue = &file->DisableWallpaper; + else if (_stricmp(name, key_int_allow_font_smoothing) == 0) + *outValue = &file->AllowFontSmoothing; + else if (_stricmp(name, key_int_allow_desktop_composition) == 0) + *outValue = &file->AllowDesktopComposition; + else if (_stricmp(name, key_int_disable_full_window_drag) == 0) + *outValue = &file->DisableFullWindowDrag; + else if (_stricmp(name, key_int_disable_menu_anims) == 0) + *outValue = &file->DisableMenuAnims; + else if (_stricmp(name, key_int_disable_themes) == 0) + *outValue = &file->DisableThemes; + else if (_stricmp(name, key_int_disable_cursor_setting) == 0) + *outValue = &file->DisableCursorSetting; + else if (_stricmp(name, key_int_bitmapcachesize) == 0) + *outValue = &file->BitmapCacheSize; + else if (_stricmp(name, key_int_bitmapcachepersistenable) == 0) + *outValue = &file->BitmapCachePersistEnable; + else if (_stricmp(name, key_int_server_port) == 0) + *outValue = &file->ServerPort; + else if (_stricmp(name, key_int_redirectdrives) == 0) + *outValue = &file->RedirectDrives; + else if (_stricmp(name, key_int_redirectprinters) == 0) + *outValue = &file->RedirectPrinters; + else if (_stricmp(name, key_int_redirectcomports) == 0) + *outValue = &file->RedirectComPorts; + else if (_stricmp(name, key_int_redirectlocation) == 0) + *outValue = &file->RedirectLocation; + else if (_stricmp(name, key_int_redirectsmartcards) == 0) + *outValue = &file->RedirectSmartCards; + else if (_stricmp(name, key_int_redirectclipboard) == 0) + *outValue = &file->RedirectClipboard; + else if (_stricmp(name, key_int_redirectposdevices) == 0) + *outValue = &file->RedirectPosDevices; + else if (_stricmp(name, key_int_redirectdirectx) == 0) + *outValue = &file->RedirectDirectX; + else if (_stricmp(name, key_int_disableprinterredirection) == 0) + *outValue = &file->DisablePrinterRedirection; + else if (_stricmp(name, key_int_disableclipboardredirection) == 0) + *outValue = &file->DisableClipboardRedirection; + else if (_stricmp(name, key_int_connect_to_console) == 0) + *outValue = &file->ConnectToConsole; + else if (_stricmp(name, key_int_administrative_session) == 0) + *outValue = &file->AdministrativeSession; + else if (_stricmp(name, key_int_autoreconnection_enabled) == 0) + *outValue = &file->AutoReconnectionEnabled; + else if (_stricmp(name, key_int_autoreconnect_max_retries) == 0) + *outValue = &file->AutoReconnectMaxRetries; + else if (_stricmp(name, key_int_public_mode) == 0) + *outValue = &file->PublicMode; + else if (_stricmp(name, key_int_authentication_level) == 0) + *outValue = &file->AuthenticationLevel; + else if (_stricmp(name, key_int_promptcredentialonce) == 0) + *outValue = &file->PromptCredentialOnce; + else if ((_stricmp(name, key_int_prompt_for_credentials) == 0)) + *outValue = &file->PromptForCredentials; + else if (_stricmp(name, key_int_negotiate_security_layer) == 0) + *outValue = &file->NegotiateSecurityLayer; + else if (_stricmp(name, key_int_enablecredsspsupport) == 0) + *outValue = &file->EnableCredSSPSupport; + else if (_stricmp(name, key_int_enablerdsaadauth) == 0) + *outValue = &file->EnableRdsAadAuth; + else if (_stricmp(name, key_int_remoteapplicationmode) == 0) + *outValue = &file->RemoteApplicationMode; + else if (_stricmp(name, key_int_remoteapplicationexpandcmdline) == 0) + *outValue = &file->RemoteApplicationExpandCmdLine; + else if (_stricmp(name, key_int_remoteapplicationexpandworkingdir) == 0) + *outValue = &file->RemoteApplicationExpandWorkingDir; + else if (_stricmp(name, key_int_disableconnectionsharing) == 0) + *outValue = &file->DisableConnectionSharing; + else if (_stricmp(name, key_int_disableremoteappcapscheck) == 0) + *outValue = &file->DisableRemoteAppCapsCheck; + else if (_stricmp(name, key_int_gatewayusagemethod) == 0) + *outValue = &file->GatewayUsageMethod; + else if (_stricmp(name, key_int_gatewayprofileusagemethod) == 0) + *outValue = &file->GatewayProfileUsageMethod; + else if (_stricmp(name, key_int_gatewaycredentialssource) == 0) + *outValue = &file->GatewayCredentialsSource; + else if (_stricmp(name, key_int_use_redirection_server_name) == 0) + *outValue = &file->UseRedirectionServerName; + else if (_stricmp(name, key_int_rdgiskdcproxy) == 0) + *outValue = &file->RdgIsKdcProxy; + else if (_stricmp(name, key_int_redirectwebauthn) == 0) + *outValue = &file->RedirectWebauthN; + else + { + rdpFileLine* line = freerdp_client_rdp_file_find_line_by_name(file, name); + if (!line) + return FALSE; + if (!(line->flags & RDP_FILE_LINE_FLAG_TYPE_INTEGER)) + return FALSE; + + *outLine = line; + } + + return TRUE; +} + +static BOOL freerdp_client_rdp_file_find_string_entry(rdpFile* file, const char* name, + LPSTR** outValue, rdpFileLine** outLine) +{ + WINPR_ASSERT(file); + WINPR_ASSERT(name); + WINPR_ASSERT(outValue); + WINPR_ASSERT(outLine); + + *outValue = NULL; + *outLine = NULL; + + if (_stricmp(name, key_str_username) == 0) + *outValue = &file->Username; + else if (_stricmp(name, key_str_domain) == 0) + *outValue = &file->Domain; + else if (_stricmp(name, key_str_password) == 0) + *outValue = &file->Password; + else if (_stricmp(name, key_str_full_address) == 0) + *outValue = &file->FullAddress; + else if (_stricmp(name, key_str_alternate_full_address) == 0) + *outValue = &file->AlternateFullAddress; + else if (_stricmp(name, key_str_usbdevicestoredirect) == 0) + *outValue = &file->UsbDevicesToRedirect; + else if (_stricmp(name, key_str_camerastoredirect) == 0) + *outValue = &file->RedirectCameras; + else if (_stricmp(name, key_str_loadbalanceinfo) == 0) + *outValue = &file->LoadBalanceInfo; + else if (_stricmp(name, key_str_remoteapplicationname) == 0) + *outValue = &file->RemoteApplicationName; + else if (_stricmp(name, key_str_remoteapplicationicon) == 0) + *outValue = &file->RemoteApplicationIcon; + else if (_stricmp(name, key_str_remoteapplicationprogram) == 0) + *outValue = &file->RemoteApplicationProgram; + else if (_stricmp(name, key_str_remoteapplicationfile) == 0) + *outValue = &file->RemoteApplicationFile; + else if (_stricmp(name, key_str_remoteapplicationguid) == 0) + *outValue = &file->RemoteApplicationGuid; + else if (_stricmp(name, key_str_remoteapplicationcmdline) == 0) + *outValue = &file->RemoteApplicationCmdLine; + else if (_stricmp(name, key_str_alternate_shell) == 0) + *outValue = &file->AlternateShell; + else if (_stricmp(name, key_str_shell_working_directory) == 0) + *outValue = &file->ShellWorkingDirectory; + else if (_stricmp(name, key_str_gatewayhostname) == 0) + *outValue = &file->GatewayHostname; + else if (_stricmp(name, key_str_resourceprovider) == 0) + *outValue = &file->ResourceProvider; + else if (_stricmp(name, key_str_wvd) == 0) + *outValue = &file->WvdEndpointPool; + else if (_stricmp(name, key_str_geo) == 0) + *outValue = &file->geo; + else if (_stricmp(name, key_str_armpath) == 0) + *outValue = &file->armpath; + else if (_stricmp(name, key_str_aadtenantid) == 0) + *outValue = &file->aadtenantid; + else if (_stricmp(name, key_str_diagnosticserviceurl) == 0) + *outValue = &file->diagnosticserviceurl; + else if (_stricmp(name, key_str_hubdiscoverygeourl) == 0) + *outValue = &file->hubdiscoverygeourl; + else if (_stricmp(name, key_str_activityhint) == 0) + *outValue = &file->activityhint; + else if (_stricmp(name, key_str_gatewayaccesstoken) == 0) + *outValue = &file->GatewayAccessToken; + else if (_stricmp(name, key_str_kdcproxyname) == 0) + *outValue = &file->KdcProxyName; + else if (_stricmp(name, key_str_drivestoredirect) == 0) + *outValue = &file->DrivesToRedirect; + else if (_stricmp(name, key_str_devicestoredirect) == 0) + *outValue = &file->DevicesToRedirect; + else if (_stricmp(name, key_str_winposstr) == 0) + *outValue = &file->WinPosStr; + else if (_stricmp(name, key_str_pcb) == 0) + *outValue = &file->PreconnectionBlob; + else if (_stricmp(name, key_str_selectedmonitors) == 0) + *outValue = &file->SelectedMonitors; + else + { + rdpFileLine* line = freerdp_client_rdp_file_find_line_by_name(file, name); + if (!line) + return FALSE; + if (!(line->flags & RDP_FILE_LINE_FLAG_TYPE_STRING)) + return FALSE; + + *outLine = line; + } + + return TRUE; +} + +/* + * Set an integer in a rdpFile + * + * @return FALSE if a standard name was set, TRUE for a non-standard name, FALSE on error + * + */ +static BOOL freerdp_client_rdp_file_set_integer(rdpFile* file, const char* name, long value) +{ + DWORD* targetValue = NULL; + rdpFileLine* line = NULL; +#ifdef DEBUG_CLIENT_FILE + WLog_DBG(TAG, "%s:i:%ld", name, value); +#endif + + if (value < 0) + return FALSE; + + if (!freerdp_client_rdp_file_find_integer_entry(file, name, &targetValue, &line)) + { + SSIZE_T index = freerdp_client_rdp_file_add_line(file); + if (index == -1) + return FALSE; + line = &file->lines[index]; + } + + if (targetValue) + { + *targetValue = (DWORD)value; + return TRUE; + } + + if (line) + { + free(line->name); + line->name = _strdup(name); + if (!line->name) + { + free(line->name); + line->name = NULL; + return FALSE; + } + + line->iValue = value; + line->flags = RDP_FILE_LINE_FLAG_FORMATTED; + line->flags |= RDP_FILE_LINE_FLAG_TYPE_INTEGER; + line->valueLength = 0; + return TRUE; + } + + return FALSE; +} + +static BOOL freerdp_client_parse_rdp_file_integer(rdpFile* file, const char* name, + const char* value) +{ + char* endptr = NULL; + long ivalue = 0; + errno = 0; + ivalue = strtol(value, &endptr, 0); + + if ((endptr == NULL) || (errno != 0) || (endptr == value) || (ivalue > INT32_MAX) || + (ivalue < INT32_MIN)) + { + if (file->flags & RDP_FILE_FLAG_PARSE_INT_RELAXED) + { + WLog_WARN(TAG, "Integer option %s has invalid value %s, using default", name, value); + return TRUE; + } + else + { + WLog_ERR(TAG, "Failed to convert RDP file integer option %s [value=%s]", name, value); + return FALSE; + } + } + + return freerdp_client_rdp_file_set_integer(file, name, ivalue); +} + +/** set a string value in the provided rdp file context + * + * @param file rdpFile + * @param name name of the string + * @param value value of the string to set + * @return 0 on success, 1 if the key wasn't found (not a standard key), -1 on error + */ + +static BOOL freerdp_client_rdp_file_set_string(rdpFile* file, const char* name, const char* value) +{ + LPSTR* targetValue = NULL; + rdpFileLine* line = NULL; +#ifdef DEBUG_CLIENT_FILE + WLog_DBG(TAG, "%s:s:%s", name, value); +#endif + + if (!name || !value) + return FALSE; + + if (!freerdp_client_rdp_file_find_string_entry(file, name, &targetValue, &line)) + { + SSIZE_T index = freerdp_client_rdp_file_add_line(file); + if (index == -1) + return FALSE; + line = &file->lines[index]; + } + + if (targetValue) + { + *targetValue = _strdup(value); + if (!(*targetValue)) + return FALSE; + return TRUE; + } + + if (line) + { + free(line->name); + free(line->sValue); + line->name = _strdup(name); + line->sValue = _strdup(value); + if (!line->name || !line->sValue) + { + free(line->name); + free(line->sValue); + line->name = NULL; + line->sValue = NULL; + return FALSE; + } + + line->flags = RDP_FILE_LINE_FLAG_FORMATTED; + line->flags |= RDP_FILE_LINE_FLAG_TYPE_STRING; + line->valueLength = 0; + return TRUE; + } + + return FALSE; +} + +static BOOL freerdp_client_add_option(rdpFile* file, const char* option) +{ + return freerdp_addin_argv_add_argument(file->args, option); +} + +static SSIZE_T freerdp_client_rdp_file_add_line(rdpFile* file) +{ + SSIZE_T index = (SSIZE_T)file->lineCount; + + while ((file->lineCount + 1) > file->lineSize) + { + size_t new_size = 0; + rdpFileLine* new_line = NULL; + new_size = file->lineSize * 2; + new_line = (rdpFileLine*)realloc(file->lines, new_size * sizeof(rdpFileLine)); + + if (!new_line) + return -1; + + file->lines = new_line; + file->lineSize = new_size; + } + + ZeroMemory(&(file->lines[file->lineCount]), sizeof(rdpFileLine)); + file->lines[file->lineCount].index = (size_t)index; + (file->lineCount)++; + return index; +} + +static BOOL freerdp_client_parse_rdp_file_string(rdpFile* file, char* name, char* value) +{ + return freerdp_client_rdp_file_set_string(file, name, value); +} + +static BOOL freerdp_client_parse_rdp_file_option(rdpFile* file, const char* option) +{ + return freerdp_client_add_option(file, option); +} + +BOOL freerdp_client_parse_rdp_file_buffer(rdpFile* file, const BYTE* buffer, size_t size) +{ + return freerdp_client_parse_rdp_file_buffer_ex(file, buffer, size, NULL); +} + +static BOOL trim(char** strptr) +{ + char* start = NULL; + char* str = NULL; + char* end = NULL; + + start = str = *strptr; + if (!str) + return TRUE; + if (!(~((size_t)str))) + return TRUE; + end = str + strlen(str) - 1; + + while (isspace(*str)) + str++; + + while ((end > str) && isspace(*end)) + end--; + end[1] = '\0'; + if (start == str) + *strptr = str; + else + { + *strptr = _strdup(str); + free(start); + return *strptr != NULL; + } + + return TRUE; +} + +static BOOL trim_strings(rdpFile* file) +{ + if (!trim(&file->Username)) + return FALSE; + if (!trim(&file->Domain)) + return FALSE; + if (!trim(&file->AlternateFullAddress)) + return FALSE; + if (!trim(&file->FullAddress)) + return FALSE; + if (!trim(&file->UsbDevicesToRedirect)) + return FALSE; + if (!trim(&file->RedirectCameras)) + return FALSE; + if (!trim(&file->LoadBalanceInfo)) + return FALSE; + if (!trim(&file->GatewayHostname)) + return FALSE; + if (!trim(&file->GatewayAccessToken)) + return FALSE; + if (!trim(&file->RemoteApplicationName)) + return FALSE; + if (!trim(&file->RemoteApplicationIcon)) + return FALSE; + if (!trim(&file->RemoteApplicationProgram)) + return FALSE; + if (!trim(&file->RemoteApplicationFile)) + return FALSE; + if (!trim(&file->RemoteApplicationGuid)) + return FALSE; + if (!trim(&file->RemoteApplicationCmdLine)) + return FALSE; + if (!trim(&file->AlternateShell)) + return FALSE; + if (!trim(&file->ShellWorkingDirectory)) + return FALSE; + if (!trim(&file->DrivesToRedirect)) + return FALSE; + if (!trim(&file->DevicesToRedirect)) + return FALSE; + if (!trim(&file->DevicesToRedirect)) + return FALSE; + if (!trim(&file->WinPosStr)) + return FALSE; + if (!trim(&file->PreconnectionBlob)) + return FALSE; + if (!trim(&file->KdcProxyName)) + return FALSE; + if (!trim(&file->SelectedMonitors)) + return FALSE; + + for (size_t i = 0; i < file->lineCount; ++i) + { + rdpFileLine* curLine = &file->lines[i]; + if (curLine->flags & RDP_FILE_LINE_FLAG_TYPE_STRING) + { + if (!trim(&curLine->sValue)) + return FALSE; + } + } + + return TRUE; +} + +BOOL freerdp_client_parse_rdp_file_buffer_ex(rdpFile* file, const BYTE* buffer, size_t size, + rdp_file_fkt_parse parse) +{ + BOOL rc = FALSE; + size_t length = 0; + char* line = NULL; + char* type = NULL; + char* context = NULL; + char* d1 = NULL; + char* d2 = NULL; + char* beg = NULL; + char* name = NULL; + char* value = NULL; + char* copy = NULL; + + if (!file) + return FALSE; + if (size < 2) + return FALSE; + + if ((buffer[0] == BOM_UTF16_LE[0]) && (buffer[1] == BOM_UTF16_LE[1])) + { + LPCWSTR uc = (LPCWSTR)(&buffer[2]); + size = size / sizeof(WCHAR) - 1; + + copy = ConvertWCharNToUtf8Alloc(uc, size, NULL); + if (!copy) + { + WLog_ERR(TAG, "Failed to convert RDP file from UCS2 to UTF8"); + return FALSE; + } + } + else + { + copy = calloc(1, size + sizeof(BYTE)); + + if (!copy) + return FALSE; + + memcpy(copy, buffer, size); + } + + line = strtok_s(copy, "\r\n", &context); + + while (line) + { + length = strnlen(line, size); + + if (length > 1) + { + beg = line; + if (beg[0] == '/') + { + if (!freerdp_client_parse_rdp_file_option(file, line)) + goto fail; + + goto next_line; /* FreeRDP option */ + } + + d1 = strchr(line, ':'); + + if (!d1) + goto next_line; /* not first delimiter */ + + type = &d1[1]; + d2 = strchr(type, ':'); + + if (!d2) + goto next_line; /* no second delimiter */ + + if ((d2 - d1) != 2) + goto next_line; /* improper type length */ + + *d1 = 0; + *d2 = 0; + name = beg; + value = &d2[1]; + + if (parse && parse(file->context, name, *type, value)) + { + } + else if (*type == 'i') + { + /* integer type */ + if (!freerdp_client_parse_rdp_file_integer(file, name, value)) + goto fail; + } + else if (*type == 's') + { + /* string type */ + if (!freerdp_client_parse_rdp_file_string(file, name, value)) + goto fail; + } + else if (*type == 'b') + { + /* binary type */ + WLog_ERR(TAG, "Unsupported RDP file binary option %s [value=%s]", name, value); + } + } + + next_line: + line = strtok_s(NULL, "\r\n", &context); + } + + rc = trim_strings(file); +fail: + free(copy); + return rc; +} + +BOOL freerdp_client_parse_rdp_file(rdpFile* file, const char* name) +{ + return freerdp_client_parse_rdp_file_ex(file, name, NULL); +} + +BOOL freerdp_client_parse_rdp_file_ex(rdpFile* file, const char* name, rdp_file_fkt_parse parse) +{ + BOOL status = 0; + BYTE* buffer = NULL; + FILE* fp = NULL; + size_t read_size = 0; + INT64 file_size = 0; + const char* fname = name; + + if (!file || !name) + return FALSE; + + if (_strnicmp(fname, "file://", 7) == 0) + fname = &name[7]; + + fp = winpr_fopen(fname, "r"); + if (!fp) + { + WLog_ERR(TAG, "Failed to open RDP file %s", name); + return FALSE; + } + + _fseeki64(fp, 0, SEEK_END); + file_size = _ftelli64(fp); + _fseeki64(fp, 0, SEEK_SET); + + if (file_size < 1) + { + WLog_ERR(TAG, "RDP file %s is empty", name); + fclose(fp); + return FALSE; + } + + buffer = (BYTE*)malloc((size_t)file_size + 2); + + if (!buffer) + { + fclose(fp); + return FALSE; + } + + read_size = fread(buffer, (size_t)file_size, 1, fp); + + if (!read_size) + { + if (!ferror(fp)) + read_size = (size_t)file_size; + } + + fclose(fp); + + if (read_size < 1) + { + WLog_ERR(TAG, "Could not read from RDP file %s", name); + free(buffer); + return FALSE; + } + + buffer[file_size] = '\0'; + buffer[file_size + 1] = '\0'; + status = freerdp_client_parse_rdp_file_buffer_ex(file, buffer, (size_t)file_size, parse); + free(buffer); + return status; +} + +static INLINE BOOL FILE_POPULATE_STRING(char** _target, const rdpSettings* _settings, + FreeRDP_Settings_Keys_String _option) +{ + WINPR_ASSERT(_target); + WINPR_ASSERT(_settings); + + const char* str = freerdp_settings_get_string(_settings, _option); + freerdp_client_file_string_check_free(*_target); + *_target = (void*)~((size_t)NULL); + if (str) + { + *_target = _strdup(str); + if (!_target) + return FALSE; + } + return TRUE; +} + +static char* freerdp_client_channel_args_to_string(const rdpSettings* settings, const char* channel, + const char* option) +{ + ADDIN_ARGV* args = freerdp_dynamic_channel_collection_find(settings, channel); + const char* filters[] = { option }; + if (!args || (args->argc < 2)) + return NULL; + + return CommandLineToCommaSeparatedValuesEx(args->argc - 1, args->argv + 1, filters, + ARRAYSIZE(filters)); +} + +static BOOL rdp_opt_duplicate(const rdpSettings* _settings, FreeRDP_Settings_Keys_String _id, + char** _key) +{ + WINPR_ASSERT(_settings); + WINPR_ASSERT(_key); + const char* tmp = freerdp_settings_get_string(_settings, _id); + + if (tmp) + { + *_key = _strdup(tmp); + if (!*_key) + return FALSE; + } + + return TRUE; +} + +BOOL freerdp_client_populate_rdp_file_from_settings(rdpFile* file, const rdpSettings* settings) +{ + FreeRDP_Settings_Keys_String index = FreeRDP_STRING_UNUSED; + UINT32 LoadBalanceInfoLength = 0; + const char* GatewayHostname = NULL; + char* redirectCameras = NULL; + char* redirectUsb = NULL; + + if (!file || !settings) + return FALSE; + + if (!FILE_POPULATE_STRING(&file->Domain, settings, FreeRDP_Domain) || + !FILE_POPULATE_STRING(&file->Username, settings, FreeRDP_Username) || + !FILE_POPULATE_STRING(&file->Password, settings, FreeRDP_Password) || + !FILE_POPULATE_STRING(&file->FullAddress, settings, FreeRDP_ServerHostname) || + !FILE_POPULATE_STRING(&file->AlternateFullAddress, settings, FreeRDP_ServerHostname) || + !FILE_POPULATE_STRING(&file->AlternateShell, settings, FreeRDP_AlternateShell) || + !FILE_POPULATE_STRING(&file->DrivesToRedirect, settings, FreeRDP_DrivesToRedirect)) + + return FALSE; + file->ServerPort = freerdp_settings_get_uint32(settings, FreeRDP_ServerPort); + + file->DesktopWidth = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth); + file->DesktopHeight = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight); + file->SessionBpp = freerdp_settings_get_uint32(settings, FreeRDP_ColorDepth); + file->DesktopScaleFactor = freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor); + file->DynamicResolution = freerdp_settings_get_bool(settings, FreeRDP_DynamicResolutionUpdate); + file->VideoPlaybackMode = freerdp_settings_get_bool(settings, FreeRDP_SupportVideoOptimized); + + // TODO file->MaximizeToCurrentDisplays; + // TODO file->SingleMonInWindowedMode; + // TODO file->EncodeRedirectedVideoCapture; + // TODO file->RedirectedVideoCaptureEncodingQuality; + file->ConnectToConsole = freerdp_settings_get_bool(settings, FreeRDP_ConsoleSession); + file->NegotiateSecurityLayer = + freerdp_settings_get_bool(settings, FreeRDP_NegotiateSecurityLayer); + file->EnableCredSSPSupport = freerdp_settings_get_bool(settings, FreeRDP_NlaSecurity); + file->EnableRdsAadAuth = freerdp_settings_get_bool(settings, FreeRDP_AadSecurity); + + if (freerdp_settings_get_bool(settings, FreeRDP_RemoteApplicationMode)) + index = FreeRDP_RemoteApplicationWorkingDir; + else + index = FreeRDP_ShellWorkingDirectory; + if (!FILE_POPULATE_STRING(&file->ShellWorkingDirectory, settings, index)) + return FALSE; + file->ConnectionType = freerdp_settings_get_uint32(settings, FreeRDP_ConnectionType); + + file->ScreenModeId = freerdp_settings_get_bool(settings, FreeRDP_Fullscreen) ? 2 : 1; + + LoadBalanceInfoLength = freerdp_settings_get_uint32(settings, FreeRDP_LoadBalanceInfoLength); + if (LoadBalanceInfoLength > 0) + { + const BYTE* LoadBalanceInfo = + freerdp_settings_get_pointer(settings, FreeRDP_LoadBalanceInfo); + file->LoadBalanceInfo = calloc(LoadBalanceInfoLength + 1, 1); + if (!file->LoadBalanceInfo) + return FALSE; + memcpy(file->LoadBalanceInfo, LoadBalanceInfo, LoadBalanceInfoLength); + } + + if (freerdp_settings_get_bool(settings, FreeRDP_AudioPlayback)) + file->AudioMode = AUDIO_MODE_REDIRECT; + else if (freerdp_settings_get_bool(settings, FreeRDP_RemoteConsoleAudio)) + file->AudioMode = AUDIO_MODE_PLAY_ON_SERVER; + else + file->AudioMode = AUDIO_MODE_NONE; + + /* The gateway hostname should also contain a port specifier unless it is the default port 443 + */ + GatewayHostname = freerdp_settings_get_string(settings, FreeRDP_GatewayHostname); + if (GatewayHostname) + { + const UINT32 GatewayPort = freerdp_settings_get_uint32(settings, FreeRDP_GatewayPort); + freerdp_client_file_string_check_free(file->GatewayHostname); + if (GatewayPort == 443) + file->GatewayHostname = _strdup(GatewayHostname); + else + { + int length = _scprintf("%s:%" PRIu32, GatewayHostname, GatewayPort); + if (length < 0) + return FALSE; + + file->GatewayHostname = (char*)malloc((size_t)length + 1); + if (!file->GatewayHostname) + return FALSE; + + if (sprintf_s(file->GatewayHostname, (size_t)length + 1, "%s:%" PRIu32, GatewayHostname, + GatewayPort) < 0) + return FALSE; + } + if (!file->GatewayHostname) + return FALSE; + } + + if (freerdp_settings_get_bool(settings, FreeRDP_GatewayArmTransport)) + file->ResourceProvider = _strdup(str_resourceprovider_arm); + + if (!rdp_opt_duplicate(settings, FreeRDP_GatewayAvdWvdEndpointPool, &file->WvdEndpointPool)) + return FALSE; + if (!rdp_opt_duplicate(settings, FreeRDP_GatewayAvdGeo, &file->geo)) + return FALSE; + if (!rdp_opt_duplicate(settings, FreeRDP_GatewayAvdArmpath, &file->armpath)) + return FALSE; + if (!rdp_opt_duplicate(settings, FreeRDP_GatewayAvdAadtenantid, &file->aadtenantid)) + return FALSE; + if (!rdp_opt_duplicate(settings, FreeRDP_GatewayAvdDiagnosticserviceurl, + &file->diagnosticserviceurl)) + return FALSE; + if (!rdp_opt_duplicate(settings, FreeRDP_GatewayAvdHubdiscoverygeourl, + &file->hubdiscoverygeourl)) + return FALSE; + if (!rdp_opt_duplicate(settings, FreeRDP_GatewayAvdActivityhint, &file->activityhint)) + return FALSE; + + file->AudioCaptureMode = freerdp_settings_get_bool(settings, FreeRDP_AudioCapture); + file->BitmapCachePersistEnable = + freerdp_settings_get_bool(settings, FreeRDP_BitmapCachePersistEnabled); + file->Compression = freerdp_settings_get_bool(settings, FreeRDP_CompressionEnabled); + file->AuthenticationLevel = freerdp_settings_get_uint32(settings, FreeRDP_AuthenticationLevel); + file->GatewayUsageMethod = freerdp_settings_get_uint32(settings, FreeRDP_GatewayUsageMethod); + file->GatewayCredentialsSource = + freerdp_settings_get_uint32(settings, FreeRDP_GatewayCredentialsSource); + file->PromptCredentialOnce = + freerdp_settings_get_bool(settings, FreeRDP_GatewayUseSameCredentials); + file->PromptForCredentials = freerdp_settings_get_bool(settings, FreeRDP_PromptForCredentials); + file->RemoteApplicationMode = + freerdp_settings_get_bool(settings, FreeRDP_RemoteApplicationMode); + if (!FILE_POPULATE_STRING(&file->GatewayAccessToken, settings, FreeRDP_GatewayAccessToken) || + !FILE_POPULATE_STRING(&file->RemoteApplicationProgram, settings, + FreeRDP_RemoteApplicationProgram) || + !FILE_POPULATE_STRING(&file->RemoteApplicationName, settings, + FreeRDP_RemoteApplicationName) || + !FILE_POPULATE_STRING(&file->RemoteApplicationIcon, settings, + FreeRDP_RemoteApplicationIcon) || + !FILE_POPULATE_STRING(&file->RemoteApplicationFile, settings, + FreeRDP_RemoteApplicationFile) || + !FILE_POPULATE_STRING(&file->RemoteApplicationGuid, settings, + FreeRDP_RemoteApplicationGuid) || + !FILE_POPULATE_STRING(&file->RemoteApplicationCmdLine, settings, + FreeRDP_RemoteApplicationCmdLine)) + return FALSE; + file->SpanMonitors = freerdp_settings_get_bool(settings, FreeRDP_SpanMonitors); + file->UseMultiMon = freerdp_settings_get_bool(settings, FreeRDP_UseMultimon); + file->AllowDesktopComposition = + freerdp_settings_get_bool(settings, FreeRDP_AllowDesktopComposition); + file->AllowFontSmoothing = freerdp_settings_get_bool(settings, FreeRDP_AllowFontSmoothing); + file->DisableWallpaper = freerdp_settings_get_bool(settings, FreeRDP_DisableWallpaper); + file->DisableFullWindowDrag = + freerdp_settings_get_bool(settings, FreeRDP_DisableFullWindowDrag); + file->DisableMenuAnims = freerdp_settings_get_bool(settings, FreeRDP_DisableMenuAnims); + file->DisableThemes = freerdp_settings_get_bool(settings, FreeRDP_DisableThemes); + file->BandwidthAutoDetect = + (freerdp_settings_get_uint32(settings, FreeRDP_ConnectionType) >= 7) ? TRUE : FALSE; + file->NetworkAutoDetect = + freerdp_settings_get_bool(settings, FreeRDP_NetworkAutoDetect) ? 1 : 0; + file->AutoReconnectionEnabled = + freerdp_settings_get_bool(settings, FreeRDP_AutoReconnectionEnabled); + file->RedirectSmartCards = freerdp_settings_get_bool(settings, FreeRDP_RedirectSmartCards); + file->RedirectWebauthN = freerdp_settings_get_bool(settings, FreeRDP_RedirectWebAuthN); + + redirectCameras = + freerdp_client_channel_args_to_string(settings, RDPECAM_DVC_CHANNEL_NAME, "device:"); + if (redirectCameras) + { + char* str = + freerdp_client_channel_args_to_string(settings, RDPECAM_DVC_CHANNEL_NAME, "encode:"); + file->EncodeRedirectedVideoCapture = 0; + if (str) + { + unsigned long val = 0; + errno = 0; + val = strtoul(str, NULL, 0); + if ((val < UINT32_MAX) && (errno == 0)) + file->EncodeRedirectedVideoCapture = val; + } + free(str); + + str = freerdp_client_channel_args_to_string(settings, RDPECAM_DVC_CHANNEL_NAME, "quality:"); + file->RedirectedVideoCaptureEncodingQuality = 0; + if (str) + { + unsigned long val = 0; + errno = 0; + val = strtoul(str, NULL, 0); + if ((val <= 2) && (errno == 0)) + { + file->RedirectedVideoCaptureEncodingQuality = val; + } + } + free(str); + + file->RedirectCameras = redirectCameras; + } +#ifdef CHANNEL_URBDRC_CLIENT + redirectUsb = freerdp_client_channel_args_to_string(settings, URBDRC_CHANNEL_NAME, "device:"); + if (redirectUsb) + file->UsbDevicesToRedirect = redirectUsb; + +#endif + file->RedirectClipboard = + freerdp_settings_get_bool(settings, FreeRDP_RedirectClipboard) ? 1 : 0; + file->RedirectPrinters = freerdp_settings_get_bool(settings, FreeRDP_RedirectPrinters) ? 1 : 0; + file->RedirectDrives = freerdp_settings_get_bool(settings, FreeRDP_RedirectDrives) ? 1 : 0; + file->RdgIsKdcProxy = freerdp_settings_get_bool(settings, FreeRDP_KerberosRdgIsProxy) ? 1 : 0; + file->RedirectComPorts = (freerdp_settings_get_bool(settings, FreeRDP_RedirectSerialPorts) || + freerdp_settings_get_bool(settings, FreeRDP_RedirectParallelPorts)); + file->RedirectLocation = + freerdp_dynamic_channel_collection_find(settings, LOCATION_DVC_CHANNEL_NAME) ? TRUE : FALSE; + if (!FILE_POPULATE_STRING(&file->DrivesToRedirect, settings, FreeRDP_DrivesToRedirect) || + !FILE_POPULATE_STRING(&file->PreconnectionBlob, settings, FreeRDP_PreconnectionBlob) || + !FILE_POPULATE_STRING(&file->KdcProxyName, settings, FreeRDP_KerberosKdcUrl)) + return FALSE; + + { + size_t offset = 0; + UINT32 count = freerdp_settings_get_uint32(settings, FreeRDP_NumMonitorIds); + const UINT32* MonitorIds = freerdp_settings_get_pointer(settings, FreeRDP_MonitorIds); + /* String size: 10 char UINT32 max string length, 1 char separator, one element NULL */ + size_t size = count * (10 + 1) + 1; + + char* str = calloc(size, sizeof(char)); + for (UINT32 x = 0; x < count; x++) + { + int rc = _snprintf(&str[offset], size - offset, "%" PRIu32 ",", MonitorIds[x]); + if (rc <= 0) + { + free(str); + return FALSE; + } + offset += (size_t)rc; + } + if (offset > 0) + str[offset - 1] = '\0'; + freerdp_client_file_string_check_free(file->SelectedMonitors); + file->SelectedMonitors = str; + } + + file->KeyboardHook = freerdp_settings_get_uint32(settings, FreeRDP_KeyboardHook); + + return TRUE; +} + +BOOL freerdp_client_write_rdp_file(const rdpFile* file, const char* name, BOOL unicode) +{ + FILE* fp = NULL; + size_t size = 0; + char* buffer = NULL; + int status = 0; + WCHAR* unicodestr = NULL; + + if (!file || !name) + return FALSE; + + size = freerdp_client_write_rdp_file_buffer(file, NULL, 0); + if (size == 0) + return FALSE; + buffer = (char*)calloc((size_t)(size + 1), sizeof(char)); + + if (freerdp_client_write_rdp_file_buffer(file, buffer, (size_t)size + 1) != size) + { + WLog_ERR(TAG, "freerdp_client_write_rdp_file: error writing to output buffer"); + free(buffer); + return FALSE; + } + + fp = winpr_fopen(name, "w+b"); + + if (fp) + { + if (unicode) + { + size_t len = 0; + unicodestr = ConvertUtf8NToWCharAlloc(buffer, size, &len); + + if (!unicodestr) + { + free(buffer); + fclose(fp); + return FALSE; + } + + /* Write multi-byte header */ + if ((fwrite(BOM_UTF16_LE, sizeof(BYTE), 2, fp) != 2) || + (fwrite(unicodestr, sizeof(WCHAR), len, fp) != len)) + { + free(buffer); + free(unicodestr); + fclose(fp); + return FALSE; + } + + free(unicodestr); + } + else + { + if (fwrite(buffer, 1, (size_t)size, fp) != (size_t)size) + { + free(buffer); + fclose(fp); + return FALSE; + } + } + + fflush(fp); + status = fclose(fp); + } + + free(buffer); + return (status == 0) ? TRUE : FALSE; +} + +WINPR_ATTR_FORMAT_ARG(3, 4) +static SSIZE_T freerdp_client_write_setting_to_buffer(char** buffer, size_t* bufferSize, + WINPR_FORMAT_ARG const char* fmt, ...) +{ + va_list ap; + SSIZE_T len = 0; + char* buf = NULL; + size_t bufSize = 0; + + if (!buffer || !bufferSize || !fmt) + return -1; + + buf = *buffer; + bufSize = *bufferSize; + + va_start(ap, fmt); + len = vsnprintf(buf, bufSize, fmt, ap); + va_end(ap); + if (len < 0) + return -1; + + /* _snprintf doesn't add the ending \0 to its return value */ + ++len; + + /* we just want to know the size - return it */ + if (!buf && !bufSize) + return len; + + if (!buf) + return -1; + + /* update buffer size and buffer position and replace \0 with \n */ + if (bufSize >= (size_t)len) + { + *bufferSize -= (size_t)len; + buf[len - 1] = '\n'; + *buffer = buf + len; + } + else + return -1; + + return len; +} + +size_t freerdp_client_write_rdp_file_buffer(const rdpFile* file, char* buffer, size_t size) +{ + size_t totalSize = 0; + + if (!file) + return 0; + + /* either buffer and size are null or non-null */ + if ((!buffer || !size) && (buffer || size)) + return 0; + +#define WRITE_SETTING_(fmt_, ...) \ + { \ + SSIZE_T res = freerdp_client_write_setting_to_buffer(&buffer, &size, fmt_, __VA_ARGS__); \ + if (res < 0) \ + return 0; \ + totalSize += (size_t)res; \ + } + +#define WRITE_SETTING_INT(key_, param_) \ + do \ + { \ + if (~(param_)) \ + WRITE_SETTING_("%s:i:%" PRIu32, key_, param_) \ + } while (0) + +#define WRITE_SETTING_STR(key_, param_) \ + do \ + { \ + if (~(size_t)(param_)) \ + WRITE_SETTING_("%s:s:%s", key_, param_) \ + } while (0) + + /* integer parameters */ + WRITE_SETTING_INT(key_int_use_multimon, file->UseMultiMon); + WRITE_SETTING_INT(key_int_maximizetocurrentdisplays, file->MaximizeToCurrentDisplays); + WRITE_SETTING_INT(key_int_singlemoninwindowedmode, file->SingleMonInWindowedMode); + WRITE_SETTING_INT(key_int_screen_mode_id, file->ScreenModeId); + WRITE_SETTING_INT(key_int_span_monitors, file->SpanMonitors); + WRITE_SETTING_INT(key_int_smart_sizing, file->SmartSizing); + WRITE_SETTING_INT(key_int_dynamic_resolution, file->DynamicResolution); + WRITE_SETTING_INT(key_int_enablesuperpan, file->EnableSuperSpan); + WRITE_SETTING_INT(key_int_superpanaccelerationfactor, file->SuperSpanAccelerationFactor); + WRITE_SETTING_INT(key_int_desktopwidth, file->DesktopWidth); + WRITE_SETTING_INT(key_int_desktopheight, file->DesktopHeight); + WRITE_SETTING_INT(key_int_desktop_size_id, file->DesktopSizeId); + WRITE_SETTING_INT(key_int_session_bpp, file->SessionBpp); + WRITE_SETTING_INT(key_int_desktopscalefactor, file->DesktopScaleFactor); + WRITE_SETTING_INT(key_int_compression, file->Compression); + WRITE_SETTING_INT(key_int_keyboardhook, file->KeyboardHook); + WRITE_SETTING_INT(key_int_disable_ctrl_alt_del, file->DisableCtrlAltDel); + WRITE_SETTING_INT(key_int_audiomode, file->AudioMode); + WRITE_SETTING_INT(key_int_audioqualitymode, file->AudioQualityMode); + WRITE_SETTING_INT(key_int_audiocapturemode, file->AudioCaptureMode); + WRITE_SETTING_INT(key_int_encode_redirected_video_capture, file->EncodeRedirectedVideoCapture); + WRITE_SETTING_INT(key_int_redirected_video_capture_encoding_quality, + file->RedirectedVideoCaptureEncodingQuality); + WRITE_SETTING_INT(key_int_videoplaybackmode, file->VideoPlaybackMode); + WRITE_SETTING_INT(key_int_connection_type, file->ConnectionType); + WRITE_SETTING_INT(key_int_networkautodetect, file->NetworkAutoDetect); + WRITE_SETTING_INT(key_int_bandwidthautodetect, file->BandwidthAutoDetect); + WRITE_SETTING_INT(key_int_pinconnectionbar, file->PinConnectionBar); + WRITE_SETTING_INT(key_int_displayconnectionbar, file->DisplayConnectionBar); + WRITE_SETTING_INT(key_int_workspaceid, file->WorkspaceId); + WRITE_SETTING_INT(key_int_enableworkspacereconnect, file->EnableWorkspaceReconnect); + WRITE_SETTING_INT(key_int_disable_wallpaper, file->DisableWallpaper); + WRITE_SETTING_INT(key_int_allow_font_smoothing, file->AllowFontSmoothing); + WRITE_SETTING_INT(key_int_allow_desktop_composition, file->AllowDesktopComposition); + WRITE_SETTING_INT(key_int_disable_full_window_drag, file->DisableFullWindowDrag); + WRITE_SETTING_INT(key_int_disable_menu_anims, file->DisableMenuAnims); + WRITE_SETTING_INT(key_int_disable_themes, file->DisableThemes); + WRITE_SETTING_INT(key_int_disable_cursor_setting, file->DisableCursorSetting); + WRITE_SETTING_INT(key_int_bitmapcachesize, file->BitmapCacheSize); + WRITE_SETTING_INT(key_int_bitmapcachepersistenable, file->BitmapCachePersistEnable); + WRITE_SETTING_INT(key_int_server_port, file->ServerPort); + WRITE_SETTING_INT(key_int_redirectdrives, file->RedirectDrives); + WRITE_SETTING_INT(key_int_redirectprinters, file->RedirectPrinters); + WRITE_SETTING_INT(key_int_redirectcomports, file->RedirectComPorts); + WRITE_SETTING_INT(key_int_redirectlocation, file->RedirectLocation); + WRITE_SETTING_INT(key_int_redirectsmartcards, file->RedirectSmartCards); + WRITE_SETTING_INT(key_int_redirectclipboard, file->RedirectClipboard); + WRITE_SETTING_INT(key_int_redirectposdevices, file->RedirectPosDevices); + WRITE_SETTING_INT(key_int_redirectdirectx, file->RedirectDirectX); + WRITE_SETTING_INT(key_int_disableprinterredirection, file->DisablePrinterRedirection); + WRITE_SETTING_INT(key_int_disableclipboardredirection, file->DisableClipboardRedirection); + WRITE_SETTING_INT(key_int_connect_to_console, file->ConnectToConsole); + WRITE_SETTING_INT(key_int_administrative_session, file->AdministrativeSession); + WRITE_SETTING_INT(key_int_autoreconnection_enabled, file->AutoReconnectionEnabled); + WRITE_SETTING_INT(key_int_autoreconnect_max_retries, file->AutoReconnectMaxRetries); + WRITE_SETTING_INT(key_int_public_mode, file->PublicMode); + WRITE_SETTING_INT(key_int_authentication_level, file->AuthenticationLevel); + WRITE_SETTING_INT(key_int_promptcredentialonce, file->PromptCredentialOnce); + WRITE_SETTING_INT(key_int_prompt_for_credentials, file->PromptForCredentials); + WRITE_SETTING_INT(key_int_negotiate_security_layer, file->NegotiateSecurityLayer); + WRITE_SETTING_INT(key_int_enablecredsspsupport, file->EnableCredSSPSupport); + WRITE_SETTING_INT(key_int_enablerdsaadauth, file->EnableRdsAadAuth); + WRITE_SETTING_INT(key_int_remoteapplicationmode, file->RemoteApplicationMode); + WRITE_SETTING_INT(key_int_remoteapplicationexpandcmdline, file->RemoteApplicationExpandCmdLine); + WRITE_SETTING_INT(key_int_remoteapplicationexpandworkingdir, + file->RemoteApplicationExpandWorkingDir); + WRITE_SETTING_INT(key_int_disableconnectionsharing, file->DisableConnectionSharing); + WRITE_SETTING_INT(key_int_disableremoteappcapscheck, file->DisableRemoteAppCapsCheck); + WRITE_SETTING_INT(key_int_gatewayusagemethod, file->GatewayUsageMethod); + WRITE_SETTING_INT(key_int_gatewayprofileusagemethod, file->GatewayProfileUsageMethod); + WRITE_SETTING_INT(key_int_gatewaycredentialssource, file->GatewayCredentialsSource); + WRITE_SETTING_INT(key_int_use_redirection_server_name, file->UseRedirectionServerName); + WRITE_SETTING_INT(key_int_rdgiskdcproxy, file->RdgIsKdcProxy); + WRITE_SETTING_INT(key_int_redirectwebauthn, file->RedirectWebauthN); + + /* string parameters */ + WRITE_SETTING_STR(key_str_username, file->Username); + WRITE_SETTING_STR(key_str_domain, file->Domain); + WRITE_SETTING_STR(key_str_password, file->Password); + WRITE_SETTING_STR(key_str_full_address, file->FullAddress); + WRITE_SETTING_STR(key_str_alternate_full_address, file->AlternateFullAddress); + WRITE_SETTING_STR(key_str_usbdevicestoredirect, file->UsbDevicesToRedirect); + WRITE_SETTING_STR(key_str_camerastoredirect, file->RedirectCameras); + WRITE_SETTING_STR(key_str_loadbalanceinfo, file->LoadBalanceInfo); + WRITE_SETTING_STR(key_str_remoteapplicationname, file->RemoteApplicationName); + WRITE_SETTING_STR(key_str_remoteapplicationicon, file->RemoteApplicationIcon); + WRITE_SETTING_STR(key_str_remoteapplicationprogram, file->RemoteApplicationProgram); + WRITE_SETTING_STR(key_str_remoteapplicationfile, file->RemoteApplicationFile); + WRITE_SETTING_STR(key_str_remoteapplicationguid, file->RemoteApplicationGuid); + WRITE_SETTING_STR(key_str_remoteapplicationcmdline, file->RemoteApplicationCmdLine); + WRITE_SETTING_STR(key_str_alternate_shell, file->AlternateShell); + WRITE_SETTING_STR(key_str_shell_working_directory, file->ShellWorkingDirectory); + WRITE_SETTING_STR(key_str_gatewayhostname, file->GatewayHostname); + WRITE_SETTING_STR(key_str_resourceprovider, file->ResourceProvider); + WRITE_SETTING_STR(key_str_wvd, file->WvdEndpointPool); + WRITE_SETTING_STR(key_str_geo, file->geo); + WRITE_SETTING_STR(key_str_armpath, file->armpath); + WRITE_SETTING_STR(key_str_aadtenantid, file->aadtenantid); + WRITE_SETTING_STR(key_str_diagnosticserviceurl, file->diagnosticserviceurl); + WRITE_SETTING_STR(key_str_hubdiscoverygeourl, file->hubdiscoverygeourl); + WRITE_SETTING_STR(key_str_activityhint, file->activityhint); + WRITE_SETTING_STR(key_str_gatewayaccesstoken, file->GatewayAccessToken); + WRITE_SETTING_STR(key_str_kdcproxyname, file->KdcProxyName); + WRITE_SETTING_STR(key_str_drivestoredirect, file->DrivesToRedirect); + WRITE_SETTING_STR(key_str_devicestoredirect, file->DevicesToRedirect); + WRITE_SETTING_STR(key_str_winposstr, file->WinPosStr); + WRITE_SETTING_STR(key_str_pcb, file->PreconnectionBlob); + WRITE_SETTING_STR(key_str_selectedmonitors, file->SelectedMonitors); + + /* custom parameters */ + for (size_t i = 0; i < file->lineCount; ++i) + { + SSIZE_T res = -1; + const rdpFileLine* curLine = &file->lines[i]; + + if (curLine->flags & RDP_FILE_LINE_FLAG_TYPE_INTEGER) + res = freerdp_client_write_setting_to_buffer(&buffer, &size, "%s:i:%" PRIu32, + curLine->name, (UINT32)curLine->iValue); + else if (curLine->flags & RDP_FILE_LINE_FLAG_TYPE_STRING) + res = freerdp_client_write_setting_to_buffer(&buffer, &size, "%s:s:%s", curLine->name, + curLine->sValue); + if (res < 0) + return 0; + + totalSize += (size_t)res; + } + + return totalSize; +} + +static ADDIN_ARGV* rdp_file_to_args(const char* channel, const char* values) +{ + size_t count = 0; + char** p = NULL; + ADDIN_ARGV* args = freerdp_addin_argv_new(0, NULL); + if (!args) + return NULL; + if (!freerdp_addin_argv_add_argument(args, channel)) + goto fail; + + p = CommandLineParseCommaSeparatedValues(values, &count); + for (size_t x = 0; x < count; x++) + { + BOOL rc = 0; + const char* val = p[x]; + const size_t len = strlen(val) + 8; + char* str = calloc(len, sizeof(char)); + if (!str) + goto fail; + + _snprintf(str, len, "device:%s", val); + rc = freerdp_addin_argv_add_argument(args, str); + free(str); + if (!rc) + goto fail; + } + free(p); + return args; + +fail: + free(p); + freerdp_addin_argv_free(args); + return NULL; +} + +BOOL freerdp_client_populate_settings_from_rdp_file(const rdpFile* file, rdpSettings* settings) +{ + BOOL setDefaultConnectionType = TRUE; + + if (!file || !settings) + return FALSE; + + if (~((size_t)file->Domain)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_Domain, file->Domain)) + return FALSE; + } + + if (~((size_t)file->Username)) + { + char* user = NULL; + char* domain = NULL; + + if (!freerdp_parse_username(file->Username, &user, &domain)) + return FALSE; + + if (!freerdp_settings_set_string(settings, FreeRDP_Username, user)) + return FALSE; + + if (!(~((size_t)file->Domain)) && domain) + { + if (!freerdp_settings_set_string(settings, FreeRDP_Domain, domain)) + return FALSE; + } + + free(user); + free(domain); + } + + if (~((size_t)file->Password)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_Password, file->Password)) + return FALSE; + } + + { + const char* address = NULL; + + /* With MSTSC alternate full address always wins, + * so mimic this. */ + if (~((size_t)file->AlternateFullAddress)) + address = file->AlternateFullAddress; + else if (~((size_t)file->FullAddress)) + address = file->FullAddress; + + if (address) + { + int port = -1; + char* host = NULL; + + if (!freerdp_parse_hostname(address, &host, &port)) + return FALSE; + + const BOOL rc = freerdp_settings_set_string(settings, FreeRDP_ServerHostname, host); + free(host); + if (!rc) + return FALSE; + + if (port > 0) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_ServerPort, (UINT32)port)) + return FALSE; + } + } + } + + if (~file->ServerPort) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_ServerPort, file->ServerPort)) + return FALSE; + } + + if (~file->DesktopSizeId) + { + switch (file->DesktopSizeId) + { + case 0: + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, 640)) + return FALSE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, 480)) + return FALSE; + break; + case 1: + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, 800)) + return FALSE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, 600)) + return FALSE; + break; + case 2: + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, 1024)) + return FALSE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, 768)) + return FALSE; + break; + case 3: + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, 1280)) + return FALSE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, 1024)) + return FALSE; + break; + case 4: + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, 1600)) + return FALSE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, 1200)) + return FALSE; + break; + default: + WLog_WARN(TAG, "Unsupported 'desktop size id' value %" PRIu32, file->DesktopSizeId); + break; + } + } + if (~file->DesktopWidth) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, file->DesktopWidth)) + return FALSE; + } + + if (~file->DesktopHeight) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, file->DesktopHeight)) + return FALSE; + } + + if (~file->SessionBpp) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth, file->SessionBpp)) + return FALSE; + } + + if (~file->ConnectToConsole) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_ConsoleSession, + file->ConnectToConsole != 0)) + return FALSE; + } + + if (~file->AdministrativeSession) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_ConsoleSession, + file->AdministrativeSession != 0)) + return FALSE; + } + + if (~file->NegotiateSecurityLayer) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_NegotiateSecurityLayer, + file->NegotiateSecurityLayer != 0)) + return FALSE; + } + + if (~file->EnableCredSSPSupport) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_NlaSecurity, + file->EnableCredSSPSupport != 0)) + return FALSE; + } + + if (~file->EnableRdsAadAuth) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_AadSecurity, file->EnableRdsAadAuth != 0)) + return FALSE; + } + + if (~((size_t)file->AlternateShell)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_AlternateShell, file->AlternateShell)) + return FALSE; + } + + if (~((size_t)file->ShellWorkingDirectory)) + { + /* ShellWorkingDir is used for either, shell working dir or remote app working dir */ + FreeRDP_Settings_Keys_String targetId = + (~file->RemoteApplicationMode && file->RemoteApplicationMode != 0) + ? FreeRDP_RemoteApplicationWorkingDir + : FreeRDP_ShellWorkingDirectory; + + if (!freerdp_settings_set_string(settings, targetId, file->ShellWorkingDirectory)) + return FALSE; + } + + if (~file->ScreenModeId) + { + /** + * Screen Mode Id: + * http://technet.microsoft.com/en-us/library/ff393692/ + * + * This setting corresponds to the selection in the Display + * configuration slider on the Display tab under Options in RDC. + * + * Values: + * + * 1: The remote session will appear in a window. + * 2: The remote session will appear full screen. + */ + if (!freerdp_settings_set_bool(settings, FreeRDP_Fullscreen, + (file->ScreenModeId == 2) ? TRUE : FALSE)) + return FALSE; + } + + if (~(file->SmartSizing)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SmartSizing, + (file->SmartSizing == 1) ? TRUE : FALSE)) + return FALSE; + /** + * SmartSizingWidth and SmartSizingHeight: + * + * Adding this option to use the DesktopHeight and DesktopWidth as + * parameters for the SmartSizingWidth and SmartSizingHeight, as there + * are no options for that in standard RDP files. + * + * Equivalent of doing /smart-sizing:WxH + */ + if (((~(file->DesktopWidth) && ~(file->DesktopHeight)) || ~(file->DesktopSizeId)) && + (file->SmartSizing == 1)) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_SmartSizingWidth, + file->DesktopWidth)) + return FALSE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_SmartSizingHeight, + file->DesktopHeight)) + return FALSE; + } + } + + if (~((size_t)file->LoadBalanceInfo)) + { + const size_t len = strlen(file->LoadBalanceInfo); + if (!freerdp_settings_set_pointer_len(settings, FreeRDP_LoadBalanceInfo, + file->LoadBalanceInfo, len)) + return FALSE; + } + + if (~file->AuthenticationLevel) + { + /** + * Authentication Level: + * http://technet.microsoft.com/en-us/library/ff393709/ + * + * This setting corresponds to the selection in the If server authentication + * fails drop-down list on the Advanced tab under Options in RDC. + * + * Values: + * + * 0: If server authentication fails, connect to the computer without warning (Connect and + * don’t warn me). 1: If server authentication fails, do not establish a connection (Do not + * connect). 2: If server authentication fails, show a warning and allow me to connect or + * refuse the connection (Warn me). 3: No authentication requirement is specified. + */ + if (!freerdp_settings_set_uint32(settings, FreeRDP_AuthenticationLevel, + file->AuthenticationLevel)) + return FALSE; + } + + if (~file->ConnectionType) + { + if (!freerdp_set_connection_type(settings, file->ConnectionType)) + return FALSE; + setDefaultConnectionType = FALSE; + } + + if (~file->AudioMode) + { + switch (file->AudioMode) + { + case AUDIO_MODE_REDIRECT: + if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteConsoleAudio, FALSE)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_AudioPlayback, TRUE)) + return FALSE; + break; + case AUDIO_MODE_PLAY_ON_SERVER: + if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteConsoleAudio, TRUE)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_AudioPlayback, FALSE)) + return FALSE; + break; + case AUDIO_MODE_NONE: + default: + if (!freerdp_settings_set_bool(settings, FreeRDP_AudioPlayback, FALSE)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteConsoleAudio, FALSE)) + return FALSE; + break; + } + } + + if (~file->AudioCaptureMode) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_AudioCapture, file->AudioCaptureMode != 0)) + return FALSE; + } + + if (~file->Compression) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_CompressionEnabled, + file->Compression != 0)) + return FALSE; + } + + if (~((size_t)file->GatewayHostname)) + { + int port = -1; + char* host = NULL; + + if (!freerdp_parse_hostname(file->GatewayHostname, &host, &port)) + return FALSE; + + const BOOL rc = freerdp_settings_set_string(settings, FreeRDP_GatewayHostname, host); + free(host); + if (!rc) + return FALSE; + + if (port > 0) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_GatewayPort, (UINT32)port)) + return FALSE; + } + } + + if (~((size_t)file->ResourceProvider)) + { + if (_stricmp(file->ResourceProvider, str_resourceprovider_arm) == 0) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_GatewayArmTransport, TRUE)) + return FALSE; + } + } + + if (~((size_t)file->WvdEndpointPool)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_GatewayAvdWvdEndpointPool, + file->WvdEndpointPool)) + return FALSE; + } + + if (~((size_t)file->geo)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_GatewayAvdGeo, file->geo)) + return FALSE; + } + + if (~((size_t)file->armpath)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_GatewayAvdArmpath, file->armpath)) + return FALSE; + } + + if (~((size_t)file->aadtenantid)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_GatewayAvdAadtenantid, + file->aadtenantid)) + return FALSE; + } + + if (~((size_t)file->diagnosticserviceurl)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_GatewayAvdDiagnosticserviceurl, + file->diagnosticserviceurl)) + return FALSE; + } + + if (~((size_t)file->hubdiscoverygeourl)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_GatewayAvdHubdiscoverygeourl, + file->hubdiscoverygeourl)) + return FALSE; + } + + if (~((size_t)file->activityhint)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_GatewayAvdActivityhint, + file->activityhint)) + return FALSE; + } + + if (~((size_t)file->GatewayAccessToken)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_GatewayAccessToken, + file->GatewayAccessToken)) + return FALSE; + } + + if (~file->GatewayUsageMethod) + { + if (!freerdp_set_gateway_usage_method(settings, file->GatewayUsageMethod)) + return FALSE; + } + + if (~file->PromptCredentialOnce) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_GatewayUseSameCredentials, + file->PromptCredentialOnce != 0)) + return FALSE; + } + + if (~file->PromptForCredentials) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_PromptForCredentials, + file->PromptForCredentials != 0)) + return FALSE; + } + + if (~file->RemoteApplicationMode) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteApplicationMode, + file->RemoteApplicationMode != 0)) + return FALSE; + } + + if (~((size_t)file->RemoteApplicationProgram)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_RemoteApplicationProgram, + file->RemoteApplicationProgram)) + return FALSE; + } + + if (~((size_t)file->RemoteApplicationName)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_RemoteApplicationName, + file->RemoteApplicationName)) + return FALSE; + } + + if (~((size_t)file->RemoteApplicationIcon)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_RemoteApplicationIcon, + file->RemoteApplicationIcon)) + return FALSE; + } + + if (~((size_t)file->RemoteApplicationFile)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_RemoteApplicationFile, + file->RemoteApplicationFile)) + return FALSE; + } + + if (~((size_t)file->RemoteApplicationGuid)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_RemoteApplicationGuid, + file->RemoteApplicationGuid)) + return FALSE; + } + + if (~((size_t)file->RemoteApplicationCmdLine)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_RemoteApplicationCmdLine, + file->RemoteApplicationCmdLine)) + return FALSE; + } + + if (~file->SpanMonitors) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SpanMonitors, file->SpanMonitors != 0)) + return FALSE; + } + + if (~file->UseMultiMon) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_UseMultimon, file->UseMultiMon != 0)) + return FALSE; + } + + if (~file->AllowFontSmoothing) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_AllowFontSmoothing, + file->AllowFontSmoothing != 0)) + return FALSE; + } + + if (~file->DisableWallpaper) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_DisableWallpaper, + file->DisableWallpaper != 0)) + return FALSE; + } + + if (~file->DisableFullWindowDrag) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_DisableFullWindowDrag, + file->DisableFullWindowDrag != 0)) + return FALSE; + } + + if (~file->DisableMenuAnims) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_DisableMenuAnims, + file->DisableMenuAnims != 0)) + return FALSE; + } + + if (~file->DisableThemes) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_DisableThemes, file->DisableThemes != 0)) + return FALSE; + } + + if (~file->AllowDesktopComposition) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_AllowDesktopComposition, + file->AllowDesktopComposition != 0)) + return FALSE; + } + + if (~file->BitmapCachePersistEnable) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_BitmapCachePersistEnabled, + file->BitmapCachePersistEnable != 0)) + return FALSE; + } + + if (~file->DisableRemoteAppCapsCheck) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_DisableRemoteAppCapsCheck, + file->DisableRemoteAppCapsCheck != 0)) + return FALSE; + } + + if (~file->BandwidthAutoDetect) + { + if (file->BandwidthAutoDetect != 0) + { + if ((~file->NetworkAutoDetect) && (file->NetworkAutoDetect == 0)) + { + WLog_WARN(TAG, + "Got networkautodetect:i:%" PRIu32 " and bandwidthautodetect:i:%" PRIu32 + ". Correcting to networkautodetect:i:1", + file->NetworkAutoDetect, file->BandwidthAutoDetect); + WLog_WARN(TAG, + "Add networkautodetect:i:1 to your RDP file to eliminate this warning."); + } + + if (!freerdp_set_connection_type(settings, CONNECTION_TYPE_AUTODETECT)) + return FALSE; + setDefaultConnectionType = FALSE; + } + if (!freerdp_settings_set_bool(settings, FreeRDP_NetworkAutoDetect, + (file->BandwidthAutoDetect != 0) || + (file->NetworkAutoDetect != 0))) + return FALSE; + } + + if (~file->NetworkAutoDetect) + { + if (file->NetworkAutoDetect != 0) + { + if ((~file->BandwidthAutoDetect) && (file->BandwidthAutoDetect == 0)) + { + WLog_WARN(TAG, + "Got networkautodetect:i:%" PRIu32 " and bandwidthautodetect:i:%" PRIu32 + ". Correcting to bandwidthautodetect:i:1", + file->NetworkAutoDetect, file->BandwidthAutoDetect); + WLog_WARN( + TAG, "Add bandwidthautodetect:i:1 to your RDP file to eliminate this warning."); + } + + if (!freerdp_set_connection_type(settings, CONNECTION_TYPE_AUTODETECT)) + return FALSE; + + setDefaultConnectionType = FALSE; + } + if (!freerdp_settings_set_bool(settings, FreeRDP_NetworkAutoDetect, + (file->BandwidthAutoDetect != 0) || + (file->NetworkAutoDetect != 0))) + return FALSE; + } + + if (~file->AutoReconnectionEnabled) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_AutoReconnectionEnabled, + file->AutoReconnectionEnabled != 0)) + return FALSE; + } + + if (~file->AutoReconnectMaxRetries) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_AutoReconnectMaxRetries, + file->AutoReconnectMaxRetries)) + return FALSE; + } + + if (~file->RedirectSmartCards) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectSmartCards, + file->RedirectSmartCards != 0)) + return FALSE; + } + + if (~file->RedirectWebauthN) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectWebAuthN, + file->RedirectWebauthN != 0)) + return FALSE; + } + + if (~file->RedirectClipboard) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectClipboard, + file->RedirectClipboard != 0)) + return FALSE; + } + + if (~file->RedirectPrinters) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectPrinters, + file->RedirectPrinters != 0)) + return FALSE; + } + + if (~file->RedirectDrives) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectDrives, file->RedirectDrives != 0)) + return FALSE; + } + + if (~file->RedirectPosDevices) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectSerialPorts, + file->RedirectComPorts != 0) || + !freerdp_settings_set_bool(settings, FreeRDP_RedirectParallelPorts, + file->RedirectComPorts != 0)) + return FALSE; + } + + if (~file->RedirectComPorts) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectSerialPorts, + file->RedirectComPorts != 0) || + !freerdp_settings_set_bool(settings, FreeRDP_RedirectParallelPorts, + file->RedirectComPorts != 0)) + return FALSE; + } + + if (~file->RedirectLocation) + { + size_t count = 0; + char** str = + CommandLineParseCommaSeparatedValuesEx(LOCATION_DVC_CHANNEL_NAME, NULL, &count); + const BOOL rc = freerdp_client_add_dynamic_channel(settings, count, str); + free(str); + if (!rc) + return FALSE; + } + + if (~file->RedirectDirectX) + { + /* What is this?! */ + } + + if (~((size_t)file->DevicesToRedirect)) + { + /** + * Devices to redirect: + * http://technet.microsoft.com/en-us/library/ff393728/ + * + * This setting corresponds to the selections for Other supported Plug and Play + * (PnP) devices under More on the Local Resources tab under Options in RDC. + * + * Values: + * + * '*': + * Redirect all supported Plug and Play devices. + * + * 'DynamicDevices': + * Redirect any supported Plug and Play devices that are connected later. + * + * The hardware ID for the supported Plug and Play device: + * Redirect the specified supported Plug and Play device. + * + * Examples: + * devicestoredirect:s:* + * devicestoredirect:s:DynamicDevices + * devicestoredirect:s:USB\VID_04A9&PID_30C1\6&4BD985D&0&2;,DynamicDevices + * + */ + if (!freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection, TRUE)) + return FALSE; + } + + if (~((size_t)file->DrivesToRedirect)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_DrivesToRedirect, + file->DrivesToRedirect)) + return FALSE; + } + + if (~((size_t)file->RedirectCameras)) + { +#if defined(CHANNEL_RDPECAM_CLIENT) + union + { + char** c; + const char** cc; + } cnv; + ADDIN_ARGV* args = rdp_file_to_args(RDPECAM_DVC_CHANNEL_NAME, file->RedirectCameras); + if (!args) + return FALSE; + + if (~file->EncodeRedirectedVideoCapture) + { + char encode[64]; + _snprintf(encode, sizeof(encode), "encode:%" PRIu32, + file->EncodeRedirectedVideoCapture); + freerdp_addin_argv_add_argument(args, encode); + } + if (~file->RedirectedVideoCaptureEncodingQuality) + { + char quality[64]; + _snprintf(quality, sizeof(quality), "quality:%" PRIu32, + file->RedirectedVideoCaptureEncodingQuality); + freerdp_addin_argv_add_argument(args, quality); + } + + cnv.c = args->argv; + const BOOL status = freerdp_client_add_dynamic_channel(settings, args->argc, cnv.cc); + freerdp_addin_argv_free(args); + if (!status) + return FALSE; +#else + WLog_WARN( + TAG, + "This build does not support [MS-RDPECAM] camera redirection channel. Ignoring '%s'", + key_str_camerastoredirect); +#endif + } + + if (~((size_t)file->UsbDevicesToRedirect)) + { +#ifdef CHANNEL_URBDRC_CLIENT + union + { + char** c; + const char** cc; + } cnv; + ADDIN_ARGV* args = rdp_file_to_args(URBDRC_CHANNEL_NAME, file->UsbDevicesToRedirect); + if (!args) + return FALSE; + cnv.c = args->argv; + const BOOL status = freerdp_client_add_dynamic_channel(settings, args->argc, cnv.cc); + freerdp_addin_argv_free(args); + if (!status) + return FALSE; +#else + WLog_WARN(TAG, + "This build does not support [MS-RDPEUSB] usb redirection channel. Ignoring '%s'", + key_str_usbdevicestoredirect); +#endif + } + + if (~file->KeyboardHook) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_KeyboardHook, file->KeyboardHook)) + return FALSE; + } + + if (~(size_t)file->SelectedMonitors) + { + size_t count = 0; + char** args = CommandLineParseCommaSeparatedValues(file->SelectedMonitors, &count); + UINT32* list = NULL; + + if (!freerdp_settings_set_pointer_len(settings, FreeRDP_MonitorIds, NULL, count)) + { + free(args); + return FALSE; + } + list = freerdp_settings_get_pointer_writable(settings, FreeRDP_MonitorIds); + if (!list && (count > 0)) + { + free(args); + return FALSE; + } + for (size_t x = 0; x < count; x++) + { + unsigned long val = 0; + errno = 0; + val = strtoul(args[x], NULL, 0); + if ((val >= UINT32_MAX) && (errno != 0)) + { + free(args); + free(list); + return FALSE; + } + list[x] = val; + } + free(args); + } + + if (~file->DynamicResolution) + { + const BOOL val = file->DynamicResolution != 0; + if (val) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportDisplayControl, TRUE)) + return FALSE; + } + if (!freerdp_settings_set_bool(settings, FreeRDP_DynamicResolutionUpdate, val)) + return FALSE; + } + + if (~file->DesktopScaleFactor) + { + if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopScaleFactor, + file->DesktopScaleFactor)) + return FALSE; + } + + if (~file->VideoPlaybackMode) + { + if (file->VideoPlaybackMode != 0) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportGeometryTracking, TRUE) || + !freerdp_settings_set_bool(settings, FreeRDP_SupportVideoOptimized, TRUE)) + return FALSE; + } + else + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportVideoOptimized, FALSE)) + return FALSE; + } + } + // TODO file->MaximizeToCurrentDisplays; + // TODO file->SingleMonInWindowedMode; + // TODO file->EncodeRedirectedVideoCapture; + // TODO file->RedirectedVideoCaptureEncodingQuality; + + if (~((size_t)file->PreconnectionBlob)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_PreconnectionBlob, + file->PreconnectionBlob) || + !freerdp_settings_set_bool(settings, FreeRDP_SendPreconnectionPdu, TRUE)) + return FALSE; + } + + if (~((size_t)file->KdcProxyName)) + { + if (!freerdp_settings_set_string(settings, FreeRDP_KerberosKdcUrl, file->KdcProxyName)) + return FALSE; + } + + if (~((size_t)file->RdgIsKdcProxy)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_KerberosRdgIsProxy, + file->RdgIsKdcProxy != 0)) + return FALSE; + } + + if (file->args->argc > 1) + { + WCHAR* ConnectionFile = + freerdp_settings_get_string_as_utf16(settings, FreeRDP_ConnectionFile, NULL); + + if (freerdp_client_settings_parse_command_line(settings, file->args->argc, file->args->argv, + FALSE) < 0) + { + free(ConnectionFile); + return FALSE; + } + + BOOL rc = freerdp_settings_set_string_from_utf16(settings, FreeRDP_ConnectionFile, + ConnectionFile); + free(ConnectionFile); + if (!rc) + return FALSE; + } + + if (setDefaultConnectionType) + { + if (!freerdp_set_connection_type(settings, CONNECTION_TYPE_AUTODETECT)) + return FALSE; + } + + return TRUE; +} + +static rdpFileLine* freerdp_client_rdp_file_find_line_by_name(const rdpFile* file, const char* name) +{ + BOOL bFound = FALSE; + rdpFileLine* line = NULL; + + for (size_t index = 0; index < file->lineCount; index++) + { + line = &(file->lines[index]); + + if (line->flags & RDP_FILE_LINE_FLAG_FORMATTED) + { + if (_stricmp(name, line->name) == 0) + { + bFound = TRUE; + break; + } + } + } + + return (bFound) ? line : NULL; +} +/** + * Set a string option to a rdpFile + * @param file rdpFile + * @param name name of the option + * @param value value of the option + * @return 0 on success + */ +int freerdp_client_rdp_file_set_string_option(rdpFile* file, const char* name, const char* value) +{ + return freerdp_client_rdp_file_set_string(file, name, value); +} + +const char* freerdp_client_rdp_file_get_string_option(const rdpFile* file, const char* name) +{ + LPSTR* value = NULL; + rdpFileLine* line = NULL; + + if (freerdp_client_rdp_file_find_string_entry((rdpFile*)file, name, &value, &line)) + { + if (value && ~(size_t)(*value)) + return *value; + if (line) + return line->sValue; + } + + return NULL; +} + +int freerdp_client_rdp_file_set_integer_option(rdpFile* file, const char* name, int value) +{ + return freerdp_client_rdp_file_set_integer(file, name, value); +} + +int freerdp_client_rdp_file_get_integer_option(const rdpFile* file, const char* name) +{ + DWORD* value = NULL; + rdpFileLine* line = NULL; + + if (freerdp_client_rdp_file_find_integer_entry((rdpFile*)file, name, &value, &line)) + { + if (value && ~(*value)) + return *value; + if (line) + return (int)line->iValue; + } + + return -1; +} + +static void freerdp_client_file_string_check_free(LPSTR str) +{ + if (~((size_t)str)) + free(str); +} + +rdpFile* freerdp_client_rdp_file_new(void) +{ + return freerdp_client_rdp_file_new_ex(0); +} + +rdpFile* freerdp_client_rdp_file_new_ex(DWORD flags) +{ + rdpFile* file = (rdpFile*)calloc(1, sizeof(rdpFile)); + + if (!file) + return NULL; + + file->flags = flags; + + FillMemory(file, sizeof(rdpFile), 0xFF); + file->lines = NULL; + file->lineCount = 0; + file->lineSize = 32; + file->GatewayProfileUsageMethod = 1; + file->lines = (rdpFileLine*)calloc(file->lineSize, sizeof(rdpFileLine)); + + file->args = freerdp_addin_argv_new(0, NULL); + if (!file->lines || !file->args) + goto fail; + + if (!freerdp_client_add_option(file, "freerdp")) + goto fail; + + return file; +fail: + WINPR_PRAGMA_DIAG_PUSH + WINPR_PRAGMA_DIAG_IGNORED_MISMATCHED_DEALLOC + freerdp_client_rdp_file_free(file); + WINPR_PRAGMA_DIAG_POP + return NULL; +} +void freerdp_client_rdp_file_free(rdpFile* file) +{ + if (file) + { + if (file->lineCount) + { + for (size_t i = 0; i < file->lineCount; i++) + { + free(file->lines[i].name); + free(file->lines[i].sValue); + } + } + free(file->lines); + + freerdp_addin_argv_free(file->args); + + freerdp_client_file_string_check_free(file->Username); + freerdp_client_file_string_check_free(file->Domain); + freerdp_client_file_string_check_free(file->Password); + freerdp_client_file_string_check_free(file->FullAddress); + freerdp_client_file_string_check_free(file->AlternateFullAddress); + freerdp_client_file_string_check_free(file->UsbDevicesToRedirect); + freerdp_client_file_string_check_free(file->RedirectCameras); + freerdp_client_file_string_check_free(file->SelectedMonitors); + freerdp_client_file_string_check_free(file->LoadBalanceInfo); + freerdp_client_file_string_check_free(file->RemoteApplicationName); + freerdp_client_file_string_check_free(file->RemoteApplicationIcon); + freerdp_client_file_string_check_free(file->RemoteApplicationProgram); + freerdp_client_file_string_check_free(file->RemoteApplicationFile); + freerdp_client_file_string_check_free(file->RemoteApplicationGuid); + freerdp_client_file_string_check_free(file->RemoteApplicationCmdLine); + freerdp_client_file_string_check_free(file->AlternateShell); + freerdp_client_file_string_check_free(file->ShellWorkingDirectory); + freerdp_client_file_string_check_free(file->GatewayHostname); + freerdp_client_file_string_check_free(file->GatewayAccessToken); + freerdp_client_file_string_check_free(file->KdcProxyName); + freerdp_client_file_string_check_free(file->DrivesToRedirect); + freerdp_client_file_string_check_free(file->DevicesToRedirect); + freerdp_client_file_string_check_free(file->WinPosStr); + freerdp_client_file_string_check_free(file->ResourceProvider); + freerdp_client_file_string_check_free(file->WvdEndpointPool); + freerdp_client_file_string_check_free(file->geo); + freerdp_client_file_string_check_free(file->armpath); + freerdp_client_file_string_check_free(file->aadtenantid); + freerdp_client_file_string_check_free(file->diagnosticserviceurl); + freerdp_client_file_string_check_free(file->hubdiscoverygeourl); + freerdp_client_file_string_check_free(file->activityhint); + free(file); + } +} + +void freerdp_client_rdp_file_set_callback_context(rdpFile* file, void* context) +{ + file->context = context; +} diff --git a/client/common/geometry.c b/client/common/geometry.c new file mode 100644 index 0000000..83347ea --- /dev/null +++ b/client/common/geometry.c @@ -0,0 +1,43 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Geometry tracking Virtual Channel Extension + * + * Copyright 2017 David Fort <contact@hardening-consulting.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <freerdp/config.h> + +#include <freerdp/client/geometry.h> +#include <winpr/interlocked.h> + +void mappedGeometryRef(MAPPED_GEOMETRY* g) +{ + InterlockedIncrement(&g->refCounter); +} + +void mappedGeometryUnref(MAPPED_GEOMETRY* g) +{ + if (!g) + return; + + if (InterlockedDecrement(&g->refCounter)) + return; + + g->MappedGeometryUpdate = NULL; + g->MappedGeometryClear = NULL; + g->custom = NULL; + free(g->geometry.rects); + free(g); +} diff --git a/client/common/man/CMakeLists.txt b/client/common/man/CMakeLists.txt new file mode 100644 index 0000000..b601f1d --- /dev/null +++ b/client/common/man/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(generate_argument_docbook + generate_argument_docbook.c +) diff --git a/client/common/man/generate_argument_docbook.c b/client/common/man/generate_argument_docbook.c new file mode 100644 index 0000000..156d809 --- /dev/null +++ b/client/common/man/generate_argument_docbook.c @@ -0,0 +1,210 @@ +#include <stdlib.h> +#include <stdbool.h> +#include <stdio.h> +#include <ctype.h> +#include <string.h> + +#include "../cmdline.h" + +static char* resize(char** buffer, size_t* size, size_t increment) +{ + const size_t nsize = *size + increment; + char* tmp = realloc(*buffer, nsize); + if (!tmp) + { + fprintf(stderr, "Could not reallocate string buffer from %" PRIuz " to %" PRIuz " bytes.\n", + *size, nsize); + free(*buffer); + } + memset(&tmp[*size], '\0', increment); + *size = nsize; + *buffer = tmp; + return tmp; +} + +static char* append(char** buffer, size_t* size, const char* str) +{ + const size_t len = strnlen(*buffer, *size); + const size_t add = strlen(str); + const size_t required = len + add + 1; + + if (required > *size) + { + if (!resize(buffer, size, required - *size)) + return NULL; + } + strcat(*buffer, str); + return *buffer; +} + +static LPSTR tr_esc_str(LPCSTR arg, bool format) +{ + const char* str = NULL; + LPSTR tmp = NULL; + size_t ds = 0; + + if (NULL == arg) + return NULL; + + const size_t s = strlen(arg) + 1; + if (!resize(&tmp, &ds, s)) + exit(-2); + + for (size_t x = 0; x < s; x++) + { + char data[2] = { 0 }; + switch (arg[x]) + { + case '<': + if (format) + str = "<replaceable>"; + else + str = "<"; + + if (!append(&tmp, &ds, str)) + exit(-3); + break; + + case '>': + if (format) + str = "</replaceable>"; + else + str = ">"; + + if (!append(&tmp, &ds, str)) + exit(-4); + break; + + case '\'': + if (!append(&tmp, &ds, "'")) + exit(-5); + break; + + case '"': + if (!append(&tmp, &ds, """)) + exit(-6); + break; + + case '&': + if (!append(&tmp, &ds, "&")) + exit(-6); + break; + + case '\r': + case '\n': + if (!append(&tmp, &ds, "<sbr/>")) + exit(-7); + break; + + default: + data[0] = arg[x]; + if (!append(&tmp, &ds, data)) + exit(-8); + break; + } + } + + return tmp; +} + +int main(int argc, char* argv[]) +{ + size_t elements = sizeof(global_cmd_args) / sizeof(global_cmd_args[0]); + const char* fname = "freerdp-argument.1.xml"; + + fprintf(stdout, "Generating docbook file '%s'\n", fname); + FILE* fp = fopen(fname, "w"); + if (NULL == fp) + { + fprintf(stderr, "Could not open '%s' for writing.\n", fname); + return -1; + } + + /* The tag used as header in the manpage */ + fprintf(fp, "<refsect1>\n"); + fprintf(fp, "\t<title>Options</title>\n"); + fprintf(fp, "\t\t<variablelist>\n"); + + /* Iterate over argument struct and write data to docbook 4.5 + * compatible XML */ + if (elements < 2) + { + fprintf(stderr, "The argument array 'args' is empty, writing an empty file.\n"); + elements = 1; + } + + for (size_t x = 0; x < elements - 1; x++) + { + const COMMAND_LINE_ARGUMENT_A* arg = &global_cmd_args[x]; + char* name = tr_esc_str(arg->Name, FALSE); + char* alias = tr_esc_str(arg->Alias, FALSE); + char* format = tr_esc_str(arg->Format, TRUE); + char* text = tr_esc_str(arg->Text, FALSE); + fprintf(fp, "\t\t\t<varlistentry>\n"); + + do + { + fprintf(fp, "\t\t\t\t<term><option>"); + + if (arg->Flags == COMMAND_LINE_VALUE_BOOL) + fprintf(fp, "%s", arg->Default ? "-" : "+"); + else + fprintf(fp, "/"); + + fprintf(fp, "%s</option>", name); + + if (format) + { + if (arg->Flags == COMMAND_LINE_VALUE_OPTIONAL) + fprintf(fp, "["); + + fprintf(fp, ":%s", format); + + if (arg->Flags == COMMAND_LINE_VALUE_OPTIONAL) + fprintf(fp, "]"); + } + + fprintf(fp, "</term>\n"); + + if (alias == name) + break; + + free(name); + name = alias; + } while (alias); + + if (text) + { + fprintf(fp, "\t\t\t\t<listitem>\n"); + fprintf(fp, "\t\t\t\t\t<para>"); + + if (text) + fprintf(fp, "%s", text); + + if (arg->Flags & COMMAND_LINE_VALUE_BOOL && + (!arg->Default || arg->Default == BoolValueTrue)) + fprintf(fp, " (default:%s)", arg->Default ? "on" : "off"); + else if (arg->Default) + { + char* value = tr_esc_str(arg->Default, FALSE); + fprintf(fp, " (default:%s)", value); + free(value); + } + + fprintf(fp, "</para>\n"); + fprintf(fp, "\t\t\t\t</listitem>\n"); + } + + fprintf(fp, "\t\t\t</varlistentry>\n"); + free(name); + free(format); + free(text); + } + + fprintf(fp, "\t\t</variablelist>\n"); + fprintf(fp, "\t</refsect1>\n"); + fclose(fp); + + fprintf(stdout, "successfully generated '%s'\n", fname); + return 0; +} diff --git a/client/common/smartcard_cli.c b/client/common/smartcard_cli.c new file mode 100644 index 0000000..2832e92 --- /dev/null +++ b/client/common/smartcard_cli.c @@ -0,0 +1,60 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Smartcard client functions + * + * Copyright 2021 David Fort <contact@hardening-consulting.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <winpr/assert.h> + +#include <freerdp/client/utils/smartcard_cli.h> +#include <freerdp/utils/smartcardlogon.h> + +BOOL freerdp_smartcard_list(const rdpSettings* settings) +{ + SmartcardCertInfo** certs = NULL; + size_t count = 0; + + if (!smartcard_enumerateCerts(settings, &certs, &count, FALSE)) + return FALSE; + + printf("smartcard reader detected, listing %" PRIuz " certificates:\n", count); + for (size_t i = 0; i < count; i++) + { + const SmartcardCertInfo* info = certs[i]; + char asciiStr[256] = { 0 }; + + WINPR_ASSERT(info); + + printf("%" PRIuz ": %s\n", i, info->subject); + + if (ConvertWCharToUtf8(info->csp, asciiStr, ARRAYSIZE(asciiStr))) + printf("\t* CSP: %s\n", asciiStr); + + if (ConvertWCharToUtf8(info->reader, asciiStr, ARRAYSIZE(asciiStr))) + printf("\t* reader: %s\n", asciiStr); +#ifndef _WIN32 + printf("\t* slotId: %" PRIu32 "\n", info->slotId); + printf("\t* pkinitArgs: %s\n", info->pkinitArgs); +#endif + if (ConvertWCharToUtf8(info->containerName, asciiStr, ARRAYSIZE(asciiStr))) + printf("\t* containerName: %s\n", asciiStr); + if (info->upn) + printf("\t* UPN: %s\n", info->upn); + } + smartcardCertList_Free(certs, count); + + return TRUE; +} diff --git a/client/common/test/CMakeLists.txt b/client/common/test/CMakeLists.txt new file mode 100644 index 0000000..1e31f7c --- /dev/null +++ b/client/common/test/CMakeLists.txt @@ -0,0 +1,30 @@ + +set(MODULE_NAME "TestClient") +set(MODULE_PREFIX "TEST_CLIENT") + +set(${MODULE_PREFIX}_DRIVER ${MODULE_NAME}.c) + +set(${MODULE_PREFIX}_TESTS + TestClientRdpFile.c + TestClientChannels.c + TestClientCmdLine.c) + +create_test_sourcelist(${MODULE_PREFIX}_SRCS + ${${MODULE_PREFIX}_DRIVER} + ${${MODULE_PREFIX}_TESTS}) + +add_executable(${MODULE_NAME} ${${MODULE_PREFIX}_SRCS}) + +set(${MODULE_PREFIX}_LIBS ${${MODULE_PREFIX}_LIBS} freerdp-client freerdp) + +target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS}) + +set_target_properties(${MODULE_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${TESTING_OUTPUT_DIRECTORY}") + +foreach(test ${${MODULE_PREFIX}_TESTS}) + get_filename_component(TestName ${test} NAME_WE) + add_test(${TestName} ${TESTING_OUTPUT_DIRECTORY}/${MODULE_NAME} ${TestName}) +endforeach() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "FreeRDP/Client/Test") + diff --git a/client/common/test/TestClientChannels.c b/client/common/test/TestClientChannels.c new file mode 100644 index 0000000..b15a734 --- /dev/null +++ b/client/common/test/TestClientChannels.c @@ -0,0 +1,87 @@ + +#include <stdio.h> +#include <winpr/crt.h> +#include <winpr/windows.h> + +#include <freerdp/client/channels.h> +#include <freerdp/channels/rdpsnd.h> + +int TestClientChannels(int argc, char* argv[]) +{ + DWORD dwFlags = 0; + FREERDP_ADDIN** ppAddins = NULL; + + WINPR_UNUSED(argc); + WINPR_UNUSED(argv); + dwFlags = FREERDP_ADDIN_DYNAMIC; + + printf("Enumerate all\n"); + ppAddins = freerdp_channels_list_addins(NULL, NULL, NULL, dwFlags); + + for (size_t index = 0; ppAddins[index] != NULL; index++) + { + FREERDP_ADDIN* pAddin = ppAddins[index]; + + printf("Addin: Name: %s Subsystem: %s Type: %s\n", pAddin->cName, pAddin->cSubsystem, + pAddin->cType); + } + + freerdp_channels_addin_list_free(ppAddins); + + printf("Enumerate rdpsnd\n"); + ppAddins = freerdp_channels_list_addins(RDPSND_CHANNEL_NAME, NULL, NULL, dwFlags); + + for (size_t index = 0; ppAddins[index] != NULL; index++) + { + FREERDP_ADDIN* pAddin = ppAddins[index]; + + printf("Addin: Name: %s Subsystem: %s Type: %s\n", pAddin->cName, pAddin->cSubsystem, + pAddin->cType); + } + + freerdp_channels_addin_list_free(ppAddins); + +#if defined(CHANNEL_TSMF_CLIENT) + printf("Enumerate tsmf video\n"); + ppAddins = freerdp_channels_list_addins("tsmf", NULL, "video", dwFlags); + + for (size_t index = 0; ppAddins[index] != NULL; index++) + { + FREERDP_ADDIN* pAddin = ppAddins[index]; + + printf("Addin: Name: %s Subsystem: %s Type: %s\n", pAddin->cName, pAddin->cSubsystem, + pAddin->cType); + } + + freerdp_channels_addin_list_free(ppAddins); +#endif + + ppAddins = freerdp_channels_list_addins("unknown", NULL, NULL, dwFlags); + + for (size_t index = 0; ppAddins[index] != NULL; index++) + { + FREERDP_ADDIN* pAddin = ppAddins[index]; + + printf("Addin: Name: %s Subsystem: %s Type: %s\n", pAddin->cName, pAddin->cSubsystem, + pAddin->cType); + } + + freerdp_channels_addin_list_free(ppAddins); + + printf("Enumerate static addins\n"); + + dwFlags = FREERDP_ADDIN_STATIC; + ppAddins = freerdp_channels_list_addins(NULL, NULL, NULL, dwFlags); + + for (size_t index = 0; ppAddins[index] != NULL; index++) + { + FREERDP_ADDIN* pAddin = ppAddins[index]; + + printf("Addin: Name: %s Subsystem: %s Type: %s\n", pAddin->cName, pAddin->cSubsystem, + pAddin->cType); + } + + freerdp_channels_addin_list_free(ppAddins); + + return 0; +} diff --git a/client/common/test/TestClientCmdLine.c b/client/common/test/TestClientCmdLine.c new file mode 100644 index 0000000..2ce0c47 --- /dev/null +++ b/client/common/test/TestClientCmdLine.c @@ -0,0 +1,263 @@ +#include <freerdp/client.h> +#include <freerdp/client/cmdline.h> +#include <freerdp/settings.h> +#include <winpr/cmdline.h> +#include <winpr/spec.h> +#include <winpr/strlst.h> +#include <winpr/collections.h> + +typedef BOOL (*validate_settings_pr)(rdpSettings* settings); + +#define printref() printf("%s:%d: in function %-40s:", __FILE__, __LINE__, __func__) + +#define TEST_ERROR(format, ...) \ + do \ + { \ + fprintf(stderr, format, ##__VA_ARGS__); \ + printref(); \ + printf(format, ##__VA_ARGS__); \ + fflush(stdout); \ + } while (0) + +#define TEST_FAILURE(format, ...) \ + do \ + { \ + printref(); \ + printf(" FAILURE "); \ + printf(format, ##__VA_ARGS__); \ + fflush(stdout); \ + } while (0) + +static void print_test_title(int argc, char** argv) +{ + printf("Running test:"); + + for (int i = 0; i < argc; i++) + { + printf(" %s", argv[i]); + } + + printf("\n"); +} + +static INLINE BOOL testcase(const char* name, char** argv, size_t argc, int expected_return, + validate_settings_pr validate_settings) +{ + int status = 0; + BOOL valid_settings = TRUE; + rdpSettings* settings = freerdp_settings_new(0); + print_test_title(argc, argv); + + if (!settings) + { + TEST_ERROR("Test %s could not allocate settings!\n", name); + return FALSE; + } + + status = freerdp_client_settings_parse_command_line(settings, argc, argv, FALSE); + + if (validate_settings) + { + valid_settings = validate_settings(settings); + } + + freerdp_settings_free(settings); + + if (status == expected_return) + { + if (!valid_settings) + { + return FALSE; + } + } + else + { + TEST_FAILURE("Expected status %d, got status %d\n", expected_return, status); + return FALSE; + } + + return TRUE; +} + +#if defined(_WIN32) +#define DRIVE_REDIRECT_PATH "c:\\Windows" +#else +#define DRIVE_REDIRECT_PATH "/tmp" +#endif + +static BOOL check_settings_smartcard_no_redirection(rdpSettings* settings) +{ + BOOL result = TRUE; + + if (freerdp_settings_get_bool(settings, FreeRDP_RedirectSmartCards)) + { + TEST_FAILURE("Expected RedirectSmartCards = FALSE, but RedirectSmartCards = TRUE!\n"); + result = FALSE; + } + + if (freerdp_device_collection_find_type(settings, RDPDR_DTYP_SMARTCARD)) + { + TEST_FAILURE("Expected no SMARTCARD device, but found at least one!\n"); + result = FALSE; + } + + return result; +} + +typedef struct +{ + int expected_status; + validate_settings_pr validate_settings; + const char* command_line[128]; + struct + { + int index; + const char* expected_value; + } modified_arguments[8]; +} test; + +static const test tests[] = { + { COMMAND_LINE_STATUS_PRINT_HELP, + check_settings_smartcard_no_redirection, + { "testfreerdp", "--help", 0 }, + { { 0 } } }, + { COMMAND_LINE_STATUS_PRINT_HELP, + check_settings_smartcard_no_redirection, + { "testfreerdp", "/help", 0 }, + { { 0 } } }, + { COMMAND_LINE_STATUS_PRINT_HELP, + check_settings_smartcard_no_redirection, + { "testfreerdp", "-help", 0 }, + { { 0 } } }, + { COMMAND_LINE_STATUS_PRINT_VERSION, + check_settings_smartcard_no_redirection, + { "testfreerdp", "--version", 0 }, + { { 0 } } }, + { COMMAND_LINE_STATUS_PRINT_VERSION, + check_settings_smartcard_no_redirection, + { "testfreerdp", "/version", 0 }, + { { 0 } } }, + { COMMAND_LINE_STATUS_PRINT_VERSION, + check_settings_smartcard_no_redirection, + { "testfreerdp", "-version", 0 }, + { { 0 } } }, + { 0, + check_settings_smartcard_no_redirection, + { "testfreerdp", "-v", "test.freerdp.com", 0 }, + { { 0 } } }, + { 0, + check_settings_smartcard_no_redirection, + { "testfreerdp", "--v", "test.freerdp.com", 0 }, + { { 0 } } }, + { 0, + check_settings_smartcard_no_redirection, + { "testfreerdp", "/v:test.freerdp.com", 0 }, + { { 0 } } }, + { 0, + check_settings_smartcard_no_redirection, + { "testfreerdp", "/sound", "/drive:media," DRIVE_REDIRECT_PATH, "/v:test.freerdp.com", 0 }, + { { 0 } } }, + { 0, + check_settings_smartcard_no_redirection, + { "testfreerdp", "-u", "test", "-p", "test", "-v", "test.freerdp.com", 0 }, + { { 4, "****" }, { 0 } } }, + { 0, + check_settings_smartcard_no_redirection, + { "testfreerdp", "/u:test", "/p:test", "/v:test.freerdp.com", 0 }, + { { 2, "/p:****" }, { 0 } } }, + { COMMAND_LINE_ERROR_NO_KEYWORD, + check_settings_smartcard_no_redirection, + { "testfreerdp", "-invalid", 0 }, + { { 0 } } }, + { COMMAND_LINE_ERROR_NO_KEYWORD, + check_settings_smartcard_no_redirection, + { "testfreerdp", "--invalid", 0 }, + { { 0 } } }, +#if defined(WITH_FREERDP_DEPRECATED_CMDLINE) + { COMMAND_LINE_STATUS_PRINT, + check_settings_smartcard_no_redirection, + { "testfreerdp", "/kbd-list", 0 }, + { { 0 } } }, + { COMMAND_LINE_STATUS_PRINT, + check_settings_smartcard_no_redirection, + { "testfreerdp", "/monitor-list", 0 }, + { { 0 } } }, +#endif + { COMMAND_LINE_STATUS_PRINT, + check_settings_smartcard_no_redirection, + { "testfreerdp", "/list:kbd", 0 }, + { { 0 } } }, + { COMMAND_LINE_STATUS_PRINT, + check_settings_smartcard_no_redirection, + { "testfreerdp", "/list:monitor", 0 }, + { { 0 } } }, + { COMMAND_LINE_ERROR, + check_settings_smartcard_no_redirection, + { "testfreerdp", "/sound", "/drive:media:" DRIVE_REDIRECT_PATH, "/v:test.freerdp.com", 0 }, + { { 0 } } }, + { COMMAND_LINE_ERROR, + check_settings_smartcard_no_redirection, + { "testfreerdp", "/sound", "/drive:media,/foo/bar/blabla", "/v:test.freerdp.com", 0 }, + { { 0 } } }, + +#if 0 + { + COMMAND_LINE_STATUS_PRINT, check_settings_smartcard_no_redirection, + {"testfreerdp", "-z", "--plugin", "cliprdr", "--plugin", "rdpsnd", "--data", "alsa", "latency:100", "--", "--plugin", "rdpdr", "--data", "disk:w7share:/home/w7share", "--", "--plugin", "drdynvc", "--data", "tsmf:decoder:gstreamer", "--", "-u", "test", "host.example.com", 0}, + {{0}} + }, +#endif +}; + +static void check_modified_arguments(const test* test, char** command_line, int* rc) +{ + const char* expected_argument = NULL; + + for (int k = 0; (expected_argument = test->modified_arguments[k].expected_value); k++) + { + int index = test->modified_arguments[k].index; + char* actual_argument = command_line[index]; + + if (0 != strcmp(actual_argument, expected_argument)) + { + printref(); + printf("Failure: overridden argument %d is %s but it should be %s\n", index, + actual_argument, expected_argument); + fflush(stdout); + *rc = -1; + } + } +} + +int TestClientCmdLine(int argc, char* argv[]) +{ + int rc = 0; + + WINPR_UNUSED(argc); + WINPR_UNUSED(argv); + for (size_t i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) + { + const test* current = &tests[i]; + int failure = 0; + char** command_line = string_list_copy(current->command_line); + + if (!testcase(__func__, command_line, string_list_length((const char* const*)command_line), + current->expected_status, current->validate_settings)) + { + TEST_FAILURE("parsing arguments.\n"); + failure = 1; + } + + check_modified_arguments(current, command_line, &failure); + + if (failure) + { + string_list_print(stdout, (const char* const*)command_line); + rc = -1; + } + + string_list_free(command_line); + } + + return rc; +} diff --git a/client/common/test/TestClientRdpFile.c b/client/common/test/TestClientRdpFile.c new file mode 100644 index 0000000..e631bc8 --- /dev/null +++ b/client/common/test/TestClientRdpFile.c @@ -0,0 +1,600 @@ +#include <freerdp/config.h> + +#include <stdio.h> +#include <winpr/crt.h> +#include <winpr/windows.h> +#include <winpr/path.h> +#include <winpr/crypto.h> + +#include <freerdp/client/file.h> +#include <freerdp/channels/rdpecam.h> + +static const BYTE testRdpFileUTF16[] = { + 0xff, 0xfe, 0x73, 0x00, 0x63, 0x00, 0x72, 0x00, 0x65, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x20, 0x00, + 0x6d, 0x00, 0x6f, 0x00, 0x64, 0x00, 0x65, 0x00, 0x20, 0x00, 0x69, 0x00, 0x64, 0x00, 0x3a, 0x00, + 0x69, 0x00, 0x3a, 0x00, 0x32, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x75, 0x00, 0x73, 0x00, 0x65, 0x00, + 0x20, 0x00, 0x6d, 0x00, 0x75, 0x00, 0x6c, 0x00, 0x74, 0x00, 0x69, 0x00, 0x6d, 0x00, 0x6f, 0x00, + 0x6e, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x30, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x64, 0x00, + 0x65, 0x00, 0x73, 0x00, 0x6b, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x70, 0x00, 0x77, 0x00, 0x69, 0x00, + 0x64, 0x00, 0x74, 0x00, 0x68, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x31, 0x00, 0x39, 0x00, + 0x32, 0x00, 0x30, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x64, 0x00, 0x65, 0x00, 0x73, 0x00, 0x6b, 0x00, + 0x74, 0x00, 0x6f, 0x00, 0x70, 0x00, 0x68, 0x00, 0x65, 0x00, 0x69, 0x00, 0x67, 0x00, 0x68, 0x00, + 0x74, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x31, 0x00, 0x30, 0x00, 0x38, 0x00, 0x30, 0x00, + 0x0d, 0x00, 0x0a, 0x00, 0x73, 0x00, 0x65, 0x00, 0x73, 0x00, 0x73, 0x00, 0x69, 0x00, 0x6f, 0x00, + 0x6e, 0x00, 0x20, 0x00, 0x62, 0x00, 0x70, 0x00, 0x70, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, + 0x33, 0x00, 0x32, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x77, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x70, 0x00, + 0x6f, 0x00, 0x73, 0x00, 0x73, 0x00, 0x74, 0x00, 0x72, 0x00, 0x3a, 0x00, 0x73, 0x00, 0x3a, 0x00, + 0x30, 0x00, 0x2c, 0x00, 0x31, 0x00, 0x2c, 0x00, 0x35, 0x00, 0x35, 0x00, 0x33, 0x00, 0x2c, 0x00, + 0x32, 0x00, 0x31, 0x00, 0x31, 0x00, 0x2c, 0x00, 0x31, 0x00, 0x33, 0x00, 0x35, 0x00, 0x33, 0x00, + 0x2c, 0x00, 0x38, 0x00, 0x31, 0x00, 0x31, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x63, 0x00, 0x6f, 0x00, + 0x6d, 0x00, 0x70, 0x00, 0x72, 0x00, 0x65, 0x00, 0x73, 0x00, 0x73, 0x00, 0x69, 0x00, 0x6f, 0x00, + 0x6e, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x31, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x6b, 0x00, + 0x65, 0x00, 0x79, 0x00, 0x62, 0x00, 0x6f, 0x00, 0x61, 0x00, 0x72, 0x00, 0x64, 0x00, 0x68, 0x00, + 0x6f, 0x00, 0x6f, 0x00, 0x6b, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x32, 0x00, 0x0d, 0x00, + 0x0a, 0x00, 0x61, 0x00, 0x75, 0x00, 0x64, 0x00, 0x69, 0x00, 0x6f, 0x00, 0x63, 0x00, 0x61, 0x00, + 0x70, 0x00, 0x74, 0x00, 0x75, 0x00, 0x72, 0x00, 0x65, 0x00, 0x6d, 0x00, 0x6f, 0x00, 0x64, 0x00, + 0x65, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x30, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x76, 0x00, + 0x69, 0x00, 0x64, 0x00, 0x65, 0x00, 0x6f, 0x00, 0x70, 0x00, 0x6c, 0x00, 0x61, 0x00, 0x79, 0x00, + 0x62, 0x00, 0x61, 0x00, 0x63, 0x00, 0x6b, 0x00, 0x6d, 0x00, 0x6f, 0x00, 0x64, 0x00, 0x65, 0x00, + 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x31, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x63, 0x00, 0x6f, 0x00, + 0x6e, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x63, 0x00, 0x74, 0x00, 0x69, 0x00, 0x6f, 0x00, 0x6e, 0x00, + 0x20, 0x00, 0x74, 0x00, 0x79, 0x00, 0x70, 0x00, 0x65, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, + 0x37, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x74, 0x00, 0x77, 0x00, 0x6f, 0x00, + 0x72, 0x00, 0x6b, 0x00, 0x61, 0x00, 0x75, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x64, 0x00, 0x65, 0x00, + 0x74, 0x00, 0x65, 0x00, 0x63, 0x00, 0x74, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x31, 0x00, + 0x0d, 0x00, 0x0a, 0x00, 0x62, 0x00, 0x61, 0x00, 0x6e, 0x00, 0x64, 0x00, 0x77, 0x00, 0x69, 0x00, + 0x64, 0x00, 0x74, 0x00, 0x68, 0x00, 0x61, 0x00, 0x75, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x64, 0x00, + 0x65, 0x00, 0x74, 0x00, 0x65, 0x00, 0x63, 0x00, 0x74, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, + 0x31, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x64, 0x00, 0x69, 0x00, 0x73, 0x00, 0x70, 0x00, 0x6c, 0x00, + 0x61, 0x00, 0x79, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x63, 0x00, + 0x74, 0x00, 0x69, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x62, 0x00, 0x61, 0x00, 0x72, 0x00, 0x3a, 0x00, + 0x69, 0x00, 0x3a, 0x00, 0x31, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x61, 0x00, + 0x62, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x77, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x6b, 0x00, 0x73, 0x00, + 0x70, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00, 0x72, 0x00, 0x65, 0x00, 0x63, 0x00, 0x6f, 0x00, + 0x6e, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x63, 0x00, 0x74, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, + 0x30, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x64, 0x00, 0x69, 0x00, 0x73, 0x00, 0x61, 0x00, 0x62, 0x00, + 0x6c, 0x00, 0x65, 0x00, 0x20, 0x00, 0x77, 0x00, 0x61, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x70, 0x00, + 0x61, 0x00, 0x70, 0x00, 0x65, 0x00, 0x72, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x30, 0x00, + 0x0d, 0x00, 0x0a, 0x00, 0x61, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x6f, 0x00, 0x77, 0x00, 0x20, 0x00, + 0x66, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x20, 0x00, 0x73, 0x00, 0x6d, 0x00, 0x6f, 0x00, + 0x6f, 0x00, 0x74, 0x00, 0x68, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x67, 0x00, 0x3a, 0x00, 0x69, 0x00, + 0x3a, 0x00, 0x30, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x61, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x6f, 0x00, + 0x77, 0x00, 0x20, 0x00, 0x64, 0x00, 0x65, 0x00, 0x73, 0x00, 0x6b, 0x00, 0x74, 0x00, 0x6f, 0x00, + 0x70, 0x00, 0x20, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x70, 0x00, 0x6f, 0x00, 0x73, 0x00, + 0x69, 0x00, 0x74, 0x00, 0x69, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, + 0x30, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x64, 0x00, 0x69, 0x00, 0x73, 0x00, 0x61, 0x00, 0x62, 0x00, + 0x6c, 0x00, 0x65, 0x00, 0x20, 0x00, 0x66, 0x00, 0x75, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x20, 0x00, + 0x77, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x77, 0x00, 0x20, 0x00, 0x64, 0x00, + 0x72, 0x00, 0x61, 0x00, 0x67, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x31, 0x00, 0x0d, 0x00, + 0x0a, 0x00, 0x64, 0x00, 0x69, 0x00, 0x73, 0x00, 0x61, 0x00, 0x62, 0x00, 0x6c, 0x00, 0x65, 0x00, + 0x20, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x75, 0x00, 0x20, 0x00, 0x61, 0x00, 0x6e, 0x00, + 0x69, 0x00, 0x6d, 0x00, 0x73, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x31, 0x00, 0x0d, 0x00, + 0x0a, 0x00, 0x64, 0x00, 0x69, 0x00, 0x73, 0x00, 0x61, 0x00, 0x62, 0x00, 0x6c, 0x00, 0x65, 0x00, + 0x20, 0x00, 0x74, 0x00, 0x68, 0x00, 0x65, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x73, 0x00, 0x3a, 0x00, + 0x69, 0x00, 0x3a, 0x00, 0x30, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x64, 0x00, 0x69, 0x00, 0x73, 0x00, + 0x61, 0x00, 0x62, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x20, 0x00, 0x63, 0x00, 0x75, 0x00, 0x72, 0x00, + 0x73, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x20, 0x00, 0x73, 0x00, 0x65, 0x00, 0x74, 0x00, 0x74, 0x00, + 0x69, 0x00, 0x6e, 0x00, 0x67, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x30, 0x00, 0x0d, 0x00, + 0x0a, 0x00, 0x62, 0x00, 0x69, 0x00, 0x74, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x70, 0x00, 0x63, 0x00, + 0x61, 0x00, 0x63, 0x00, 0x68, 0x00, 0x65, 0x00, 0x70, 0x00, 0x65, 0x00, 0x72, 0x00, 0x73, 0x00, + 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x61, 0x00, 0x62, 0x00, 0x6c, 0x00, + 0x65, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x31, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x66, 0x00, + 0x75, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x20, 0x00, 0x61, 0x00, 0x64, 0x00, 0x64, 0x00, 0x72, 0x00, + 0x65, 0x00, 0x73, 0x00, 0x73, 0x00, 0x3a, 0x00, 0x73, 0x00, 0x3a, 0x00, 0x4c, 0x00, 0x41, 0x00, + 0x42, 0x00, 0x31, 0x00, 0x2d, 0x00, 0x57, 0x00, 0x37, 0x00, 0x2d, 0x00, 0x44, 0x00, 0x4d, 0x00, + 0x2d, 0x00, 0x30, 0x00, 0x31, 0x00, 0x2e, 0x00, 0x6c, 0x00, 0x61, 0x00, 0x62, 0x00, 0x31, 0x00, + 0x2e, 0x00, 0x61, 0x00, 0x77, 0x00, 0x61, 0x00, 0x6b, 0x00, 0x65, 0x00, 0x2e, 0x00, 0x6c, 0x00, + 0x6f, 0x00, 0x63, 0x00, 0x61, 0x00, 0x6c, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x61, 0x00, 0x75, 0x00, + 0x64, 0x00, 0x69, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x6f, 0x00, 0x64, 0x00, 0x65, 0x00, 0x3a, 0x00, + 0x69, 0x00, 0x3a, 0x00, 0x30, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x72, 0x00, 0x65, 0x00, 0x64, 0x00, + 0x69, 0x00, 0x72, 0x00, 0x65, 0x00, 0x63, 0x00, 0x74, 0x00, 0x70, 0x00, 0x72, 0x00, 0x69, 0x00, + 0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, 0x72, 0x00, 0x73, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, + 0x31, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x72, 0x00, 0x65, 0x00, 0x64, 0x00, 0x69, 0x00, 0x72, 0x00, + 0x65, 0x00, 0x63, 0x00, 0x74, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x70, 0x00, 0x6f, 0x00, + 0x72, 0x00, 0x74, 0x00, 0x73, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x30, 0x00, 0x0d, 0x00, + 0x0a, 0x00, 0x72, 0x00, 0x65, 0x00, 0x64, 0x00, 0x69, 0x00, 0x72, 0x00, 0x65, 0x00, 0x63, 0x00, + 0x74, 0x00, 0x73, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x72, 0x00, 0x74, 0x00, 0x63, 0x00, 0x61, 0x00, + 0x72, 0x00, 0x64, 0x00, 0x73, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x31, 0x00, 0x0d, 0x00, + 0x0a, 0x00, 0x72, 0x00, 0x65, 0x00, 0x64, 0x00, 0x69, 0x00, 0x72, 0x00, 0x65, 0x00, 0x63, 0x00, + 0x74, 0x00, 0x63, 0x00, 0x6c, 0x00, 0x69, 0x00, 0x70, 0x00, 0x62, 0x00, 0x6f, 0x00, 0x61, 0x00, + 0x72, 0x00, 0x64, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x31, 0x00, 0x0d, 0x00, 0x0a, 0x00, + 0x72, 0x00, 0x65, 0x00, 0x64, 0x00, 0x69, 0x00, 0x72, 0x00, 0x65, 0x00, 0x63, 0x00, 0x74, 0x00, + 0x70, 0x00, 0x6f, 0x00, 0x73, 0x00, 0x64, 0x00, 0x65, 0x00, 0x76, 0x00, 0x69, 0x00, 0x63, 0x00, + 0x65, 0x00, 0x73, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x30, 0x00, 0x0d, 0x00, 0x0a, 0x00, + 0x61, 0x00, 0x75, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x65, 0x00, 0x63, 0x00, 0x6f, 0x00, + 0x6e, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x63, 0x00, 0x74, 0x00, 0x69, 0x00, 0x6f, 0x00, 0x6e, 0x00, + 0x20, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x61, 0x00, 0x62, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x64, 0x00, + 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x31, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x61, 0x00, 0x75, 0x00, + 0x74, 0x00, 0x68, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x69, 0x00, 0x63, 0x00, 0x61, 0x00, + 0x74, 0x00, 0x69, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x20, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x76, 0x00, + 0x65, 0x00, 0x6c, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x32, 0x00, 0x0d, 0x00, 0x0a, 0x00, + 0x70, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x70, 0x00, 0x74, 0x00, 0x20, 0x00, 0x66, 0x00, + 0x6f, 0x00, 0x72, 0x00, 0x20, 0x00, 0x63, 0x00, 0x72, 0x00, 0x65, 0x00, 0x64, 0x00, 0x65, 0x00, + 0x6e, 0x00, 0x74, 0x00, 0x69, 0x00, 0x61, 0x00, 0x6c, 0x00, 0x73, 0x00, 0x3a, 0x00, 0x69, 0x00, + 0x3a, 0x00, 0x30, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x67, 0x00, 0x6f, 0x00, + 0x74, 0x00, 0x69, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65, 0x00, 0x20, 0x00, 0x73, 0x00, 0x65, 0x00, + 0x63, 0x00, 0x75, 0x00, 0x72, 0x00, 0x69, 0x00, 0x74, 0x00, 0x79, 0x00, 0x20, 0x00, 0x6c, 0x00, + 0x61, 0x00, 0x79, 0x00, 0x65, 0x00, 0x72, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x31, 0x00, + 0x0d, 0x00, 0x0a, 0x00, 0x72, 0x00, 0x65, 0x00, 0x6d, 0x00, 0x6f, 0x00, 0x74, 0x00, 0x65, 0x00, + 0x61, 0x00, 0x70, 0x00, 0x70, 0x00, 0x6c, 0x00, 0x69, 0x00, 0x63, 0x00, 0x61, 0x00, 0x74, 0x00, + 0x69, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x6d, 0x00, 0x6f, 0x00, 0x64, 0x00, 0x65, 0x00, 0x3a, 0x00, + 0x69, 0x00, 0x3a, 0x00, 0x30, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x61, 0x00, 0x6c, 0x00, 0x74, 0x00, + 0x65, 0x00, 0x72, 0x00, 0x6e, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65, 0x00, 0x20, 0x00, 0x73, 0x00, + 0x68, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x3a, 0x00, 0x73, 0x00, 0x3a, 0x00, 0x0d, 0x00, + 0x0a, 0x00, 0x73, 0x00, 0x68, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x20, 0x00, 0x77, 0x00, + 0x6f, 0x00, 0x72, 0x00, 0x6b, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x67, 0x00, 0x20, 0x00, 0x64, 0x00, + 0x69, 0x00, 0x72, 0x00, 0x65, 0x00, 0x63, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x79, 0x00, + 0x3a, 0x00, 0x73, 0x00, 0x3a, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x67, 0x00, 0x61, 0x00, 0x74, 0x00, + 0x65, 0x00, 0x77, 0x00, 0x61, 0x00, 0x79, 0x00, 0x68, 0x00, 0x6f, 0x00, 0x73, 0x00, 0x74, 0x00, + 0x6e, 0x00, 0x61, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x3a, 0x00, 0x73, 0x00, 0x3a, 0x00, 0x4c, 0x00, + 0x41, 0x00, 0x42, 0x00, 0x31, 0x00, 0x2d, 0x00, 0x57, 0x00, 0x32, 0x00, 0x4b, 0x00, 0x38, 0x00, + 0x52, 0x00, 0x32, 0x00, 0x2d, 0x00, 0x47, 0x00, 0x57, 0x00, 0x2e, 0x00, 0x6c, 0x00, 0x61, 0x00, + 0x62, 0x00, 0x31, 0x00, 0x2e, 0x00, 0x61, 0x00, 0x77, 0x00, 0x61, 0x00, 0x6b, 0x00, 0x65, 0x00, + 0x2e, 0x00, 0x6c, 0x00, 0x6f, 0x00, 0x63, 0x00, 0x61, 0x00, 0x6c, 0x00, 0x0d, 0x00, 0x0a, 0x00, + 0x67, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65, 0x00, 0x77, 0x00, 0x61, 0x00, 0x79, 0x00, 0x75, 0x00, + 0x73, 0x00, 0x61, 0x00, 0x67, 0x00, 0x65, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x74, 0x00, 0x68, 0x00, + 0x6f, 0x00, 0x64, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x31, 0x00, 0x0d, 0x00, 0x0a, 0x00, + 0x67, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65, 0x00, 0x77, 0x00, 0x61, 0x00, 0x79, 0x00, 0x63, 0x00, + 0x72, 0x00, 0x65, 0x00, 0x64, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x69, 0x00, 0x61, 0x00, + 0x6c, 0x00, 0x73, 0x00, 0x73, 0x00, 0x6f, 0x00, 0x75, 0x00, 0x72, 0x00, 0x63, 0x00, 0x65, 0x00, + 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x30, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x67, 0x00, 0x61, 0x00, + 0x74, 0x00, 0x65, 0x00, 0x77, 0x00, 0x61, 0x00, 0x79, 0x00, 0x70, 0x00, 0x72, 0x00, 0x6f, 0x00, + 0x66, 0x00, 0x69, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x75, 0x00, 0x73, 0x00, 0x61, 0x00, 0x67, 0x00, + 0x65, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x74, 0x00, 0x68, 0x00, 0x6f, 0x00, 0x64, 0x00, 0x3a, 0x00, + 0x69, 0x00, 0x3a, 0x00, 0x31, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x70, 0x00, 0x72, 0x00, 0x6f, 0x00, + 0x6d, 0x00, 0x70, 0x00, 0x74, 0x00, 0x63, 0x00, 0x72, 0x00, 0x65, 0x00, 0x64, 0x00, 0x65, 0x00, + 0x6e, 0x00, 0x74, 0x00, 0x69, 0x00, 0x61, 0x00, 0x6c, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x63, 0x00, + 0x65, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x31, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x75, 0x00, + 0x73, 0x00, 0x65, 0x00, 0x20, 0x00, 0x72, 0x00, 0x65, 0x00, 0x64, 0x00, 0x69, 0x00, 0x72, 0x00, + 0x65, 0x00, 0x63, 0x00, 0x74, 0x00, 0x69, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x20, 0x00, 0x73, 0x00, + 0x65, 0x00, 0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x6e, 0x00, 0x61, 0x00, + 0x6d, 0x00, 0x65, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, 0x30, 0x00, 0x0d, 0x00, 0x0a, 0x00, + 0x72, 0x00, 0x64, 0x00, 0x67, 0x00, 0x69, 0x00, 0x73, 0x00, 0x6b, 0x00, 0x64, 0x00, 0x63, 0x00, + 0x70, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x78, 0x00, 0x79, 0x00, 0x3a, 0x00, 0x69, 0x00, 0x3a, 0x00, + 0x30, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x6b, 0x00, 0x64, 0x00, 0x63, 0x00, 0x70, 0x00, 0x72, 0x00, + 0x6f, 0x00, 0x78, 0x00, 0x79, 0x00, 0x6e, 0x00, 0x61, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x3a, 0x00, + 0x73, 0x00, 0x3a, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x64, 0x00, 0x72, 0x00, 0x69, 0x00, 0x76, 0x00, + 0x65, 0x00, 0x73, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x65, 0x00, 0x64, 0x00, 0x69, 0x00, + 0x72, 0x00, 0x65, 0x00, 0x63, 0x00, 0x74, 0x00, 0x3a, 0x00, 0x73, 0x00, 0x3a, 0x00, 0x2a, 0x00, + 0x0d, 0x00, 0x0a, 0x00, 0x75, 0x00, 0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x6e, 0x00, 0x61, 0x00, + 0x6d, 0x00, 0x65, 0x00, 0x3a, 0x00, 0x73, 0x00, 0x3a, 0x00, 0x4c, 0x00, 0x41, 0x00, 0x42, 0x00, + 0x31, 0x00, 0x5c, 0x00, 0x4a, 0x00, 0x6f, 0x00, 0x68, 0x00, 0x6e, 0x00, 0x44, 0x00, 0x6f, 0x00, + 0x65, 0x00, 0x0d, 0x00, 0x0a, 0x00 +}; + +#if defined(CHANNEL_RDPECAM_CLIENT) +static const char* camera_args[] = { RDPECAM_DVC_CHANNEL_NAME, + "device:*", + "device:\\?\\usb#vid_0bda&pid_58b0&mi", + "device:-\\?\\usb#vid_0bdc&pid_58b1&mi", + "encode:1", + "quality:2" }; +#endif + +#if defined(CHANNEL_URBDRC_CLIENT) +static const char* urbdrc_args[] = { "urbdrc", "device:*", "device:USBInstanceID:someid", + "device:{72631e54-78a4-11d0-bcf7-00aa00b7b32a}" }; +#endif + +static char testRdpFileUTF8[] = + "screen mode id:i:2\n" + "use multimon:i:0\n" + "desktopwidth:i:1920\n" + "desktopheight:i:1080\n" + "dynamic resolution:i:1080\n" + "desktopscalefactor:i:1080\n" + "redirected video capture encoding quality:i:2\n" + "encode redirected video capture:i:1\n" + "camerastoredirect:s:*,\\?\\usb#vid_0bda&pid_58b0&mi,-\\?\\usb#vid_0bdc&pid_58b1&mi\n" + "usbdevicestoredirect:s:*,USBInstanceID:someid,{72631e54-78a4-11d0-bcf7-00aa00b7b32a}\n" + "selectedmonitors:s:3,2,42,23" + "session bpp:i:32\n" + "winposstr:s:0,1,553,211,1353,811\n" + "compression:i:1\n" + "keyboardhook:i:2\n" + "audiocapturemode:i:0\n" + "videoplaybackmode:i:2\n" + "connection type:i:7\n" + "networkautodetect:i:1\n" + "bandwidthautodetect:i:1\n" + "displayconnectionbar:i:1\n" + "enableworkspacereconnect:i:0\n" + "disable wallpaper:i:0\n" + "allow font smoothing:i:0\n" + "allow desktop composition:i:0\n" + "disable full window drag:i:1\n" + "disable menu anims:i:1\n" + "disable themes:i:0\n" + "disable cursor setting:i:0\n" + "bitmapcachepersistenable:i:1\n" + "full address:s:LAB1-W7-DM-01.lab1.awake.local\n" + "alternate full address:s:LAB1-W7-DM-01.lab1.awake.global\n" + "audiomode:i:0\n" + "redirectprinters:i:1\n" + "redirectcomports:i:0\n" + "redirectsmartcards:i:1\n" + "redirectclipboard:i:1\n" + "redirectposdevices:i:0\n" + "autoreconnection enabled:i:1\n" + "authentication level:i:2\n" + "prompt for credentials:i:0\n" + "negotiate security layer:i:1\n" + "remoteapplicationmode:i:0\n" + "alternate shell:s:\n" + "shell working directory:s:\n" + "gatewayhostname:s:LAB1-W2K8R2-GW.lab1.awake.local\n" + "gatewayusagemethod:i:1\n" + "gatewaycredentialssource:i:0\n" + "gatewayprofileusagemethod:i:1\n" + "promptcredentialonce:i:1\n" + "use redirection server name:i:0\n" + "rdgiskdcproxy:i:0\n" + "kdcproxyname:s:\n" + "drivestoredirect:s:*\n" + "username:s:LAB1\\JohnDoe\n" + "vendor integer:i:123\n" + "vendor string:s:microsoft\n"; + +static char* append(const char* fmt, ...) +{ + int rc = 0; + char* dst = NULL; + va_list ap; + + va_start(ap, fmt); + rc = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + if (rc < 0) + return NULL; + dst = malloc((size_t)rc + 1); + if (!dst) + return NULL; + + va_start(ap, fmt); + rc = vsnprintf(dst, (size_t)rc + 1, fmt, ap); + va_end(ap); + if (rc < 0) + { + free(dst); + return NULL; + } + return dst; +} + +int TestClientRdpFile(int argc, char* argv[]) +{ + int rc = -1; + int iValue = 0; + UINT32 uValue = 0; + const UINT32* puValue = NULL; + const char* sValue = NULL; + char* utfname = NULL; + char* uniname = NULL; + char* base = NULL; + char* tmp = NULL; + UINT64 id = 0; + rdpFile* file = NULL; + rdpSettings* settings = NULL; + + WINPR_UNUSED(argc); + WINPR_UNUSED(argv); + winpr_RAND(&id, sizeof(id)); + + /* Unicode */ + file = freerdp_client_rdp_file_new(); + settings = freerdp_settings_new(0); + + if (!file || !settings) + { + printf("rdp_file_new failed\n"); + goto fail; + } + + if (!freerdp_client_parse_rdp_file_buffer(file, testRdpFileUTF16, sizeof(testRdpFileUTF16))) + goto fail; + + if (!freerdp_client_populate_settings_from_rdp_file(file, settings)) + goto fail; + + if (freerdp_settings_get_bool(settings, FreeRDP_UseMultimon)) + { + printf("UseMultiMon mismatch: Actual: %" PRIu32 ", Expected: 0\n", + freerdp_settings_get_bool(settings, FreeRDP_UseMultimon)); + goto fail; + } + + if (!freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)) + { + printf("ScreenModeId mismatch: Actual: %" PRIu32 ", Expected: TRUE\n", + freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)); + goto fail; + } + +#if 0 /* TODO: Currently unused */ + if (freerdp_settings_get_uint32(settings, FreeRDP_GatewayProfileUsageMethod) != 1) + { + printf("GatewayProfileUsageMethod mismatch: Actual: %"PRIu32", Expected: 1\n", + freerdp_settings_get_uint32(settings, FreeRDP_GatewayProfileUsageMethod)); + goto fail; + } +#endif + + if (strcmp(freerdp_settings_get_string(settings, FreeRDP_GatewayHostname), + "LAB1-W2K8R2-GW.lab1.awake.local") != 0) + { + printf("GatewayHostname mismatch: Actual: %s, Expected: %s\n", + freerdp_settings_get_string(settings, FreeRDP_GatewayHostname), + "LAB1-W2K8R2-GW.lab1.awake.local"); + goto fail; + } + + if (strcmp(freerdp_settings_get_string(settings, FreeRDP_ServerHostname), + "LAB1-W7-DM-01.lab1.awake.local") != 0) + { + printf("ServerHostname mismatch: Actual: %s, Expected: %s\n", + freerdp_settings_get_string(settings, FreeRDP_ServerHostname), + "LAB1-W7-DM-01.lab1.awake.local"); + goto fail; + } + + freerdp_client_rdp_file_free(file); + freerdp_settings_free(settings); + /* Ascii */ + file = freerdp_client_rdp_file_new(); + settings = freerdp_settings_new(0); + if (!file || !settings) + { + printf("rdp_file_new failed\n"); + goto fail; + } + + if (!freerdp_client_parse_rdp_file_buffer(file, (BYTE*)testRdpFileUTF8, + sizeof(testRdpFileUTF8))) + goto fail; + + if (!freerdp_client_populate_settings_from_rdp_file(file, settings)) + goto fail; + + if (freerdp_settings_get_bool(settings, FreeRDP_UseMultimon)) + { + printf("UseMultiMon mismatch: Actual: %" PRIu32 ", Expected: 0\n", + freerdp_settings_get_bool(settings, FreeRDP_UseMultimon)); + return -1; + } + + if (!freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)) + { + printf("ScreenModeId mismatch: Actual: %" PRIu32 ", Expected: TRUE\n", + freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)); + return -1; + } + +#if 0 /* TODO: Currently unused */ + if (freerdp_settings_get_uint32(settings, FreeRDP_GatewayProfileUsageMethod) != 1) + { + printf("GatewayProfileUsageMethod mismatch: Actual: %"PRIu32", Expected: 1\n", + freerdp_settings_get_uint32(settings, FreeRDP_GatewayProfileUsageMethod)); + goto fail; + } +#endif + + if (strcmp(freerdp_settings_get_string(settings, FreeRDP_ServerHostname), + "LAB1-W7-DM-01.lab1.awake.global") != 0) + { + printf("ServerHostname mismatch: Actual: %s, Expected: %s\n", + freerdp_settings_get_string(settings, FreeRDP_ServerHostname), + "LAB1-W7-DM-01.lab1.awake.global"); + goto fail; + } + + if (strcmp(freerdp_settings_get_string(settings, FreeRDP_GatewayHostname), + "LAB1-W2K8R2-GW.lab1.awake.local") != 0) + { + printf("GatewayHostname mismatch: Actual: %s, Expected: %s\n", + freerdp_settings_get_string(settings, FreeRDP_GatewayHostname), + "LAB1-W2K8R2-GW.lab1.awake.local"); + goto fail; + } + + iValue = freerdp_client_rdp_file_get_integer_option(file, "dynamic resolution"); + if (iValue != 1080) + { + printf("dynamic resolution uses invalid default value %d", iValue); + goto fail; + } + if (!freerdp_settings_get_bool(settings, FreeRDP_DynamicResolutionUpdate)) + { + printf("FreeRDP_DynamicResolutionUpdate has invalid value"); + goto fail; + } + iValue = freerdp_client_rdp_file_get_integer_option(file, "desktopscalefactor"); + if (iValue != 1080) + { + printf("desktopscalefactor uses invalid default value %d", iValue); + goto fail; + } + if ((INT64)freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor) != iValue) + { + printf("FreeRDP_DesktopScaleFactor has invalid value"); + goto fail; + } + + /* Check [MS-RDPECAM] related options */ +#if defined(CHANNEL_RDPECAM_CLIENT) + { + ADDIN_ARGV* args = NULL; + iValue = + freerdp_client_rdp_file_get_integer_option(file, "encode redirected video capture"); + if (iValue != 1) + { + printf("encode redirected video capture uses invalid default value %d", iValue); + goto fail; + } + iValue = freerdp_client_rdp_file_get_integer_option( + file, "redirected video capture encoding quality"); + if (iValue != 2) + { + printf("redirected video capture encoding quality uses invalid default value %d", + iValue); + goto fail; + } + args = freerdp_dynamic_channel_collection_find(settings, RDPECAM_DVC_CHANNEL_NAME); + if (!args) + { + printf("rdpecam channel was not loaded"); + goto fail; + } + if (args->argc != 6) + { + printf("rdpecam channel was not loaded"); + goto fail; + } + + for (int x = 0; x < args->argc; x++) + { + if (strcmp(args->argv[x], camera_args[x]) != 0) + { + printf("rdpecam invalid argument argv[%d]: %s", x, args->argv[x]); + goto fail; + } + } + } +#endif + + /* Check [URBDRC] related options */ +#if defined(CHANNEL_URBDRC_CLIENT) + { + ADDIN_ARGV* args = freerdp_dynamic_channel_collection_find(settings, "urbdrc"); + if (!args) + { + printf("urbdrc channel was not loaded"); + goto fail; + } + if (args->argc != 4) + { + printf("urbdrc channel was not loaded"); + goto fail; + } + + for (int x = 0; x < args->argc; x++) + { + if (strcmp(args->argv[x], urbdrc_args[x]) != 0) + { + printf("urbdrc invalid argument argv[%d]: %s", x, args->argv[x]); + goto fail; + } + } + } +#endif + + /* Validate selectedmonitors:s:3,2,42,23 */ + uValue = freerdp_settings_get_uint32(settings, FreeRDP_NumMonitorIds); + if (uValue != 4) + { + printf("FreeRDP_NumMonitorIds has invalid value %" PRIu32, uValue); + goto fail; + } + puValue = (const UINT32*)freerdp_settings_get_pointer_array(settings, FreeRDP_MonitorIds, 0); + if (!puValue) + { + printf("FreeRDP_MonitorIds has invalid value %p", (const void*)puValue); + goto fail; + } + if ((puValue[0] != 3) || (puValue[1] != 2) || (puValue[2] != 42) || (puValue[3] != 23)) + { + printf("FreeRDP_MonitorIds has invalid values: [%" PRIu32 ",%" PRIu32 ",%" PRIu32 + ",%" PRIu32 "]", + puValue[0], puValue[1], puValue[2], puValue[3]); + goto fail; + } + + iValue = freerdp_client_rdp_file_get_integer_option(file, "videoplaybackmode"); + if (iValue != 2) + { + printf("videoplaybackmode uses invalid default value %d", iValue); + goto fail; + } + if (!freerdp_settings_get_bool(settings, FreeRDP_SupportVideoOptimized)) + { + printf("FreeRDP_SupportVideoOptimized has invalid value"); + goto fail; + } + if (!freerdp_settings_get_bool(settings, FreeRDP_SupportGeometryTracking)) + { + printf("FreeRDP_SupportGeometryTracking has invalid value"); + goto fail; + } + + iValue = freerdp_client_rdp_file_get_integer_option(file, "vendor integer"); + if (iValue != 123) + goto fail; + + if (freerdp_client_rdp_file_set_integer_option(file, "vendor integer", 456) == -1) + { + printf("failed to set integer: vendor integer"); + goto fail; + } + + iValue = freerdp_client_rdp_file_get_integer_option(file, "vendor integer"); + if (iValue != 456) + return -1; + + sValue = freerdp_client_rdp_file_get_string_option(file, "vendor string"); + if (strncmp(sValue, "microsoft", 10) != 0) + goto fail; + + freerdp_client_rdp_file_set_string_option(file, "vendor string", "apple"); + sValue = freerdp_client_rdp_file_get_string_option(file, "vendor string"); + if (strncmp(sValue, "apple", 6) != 0) + goto fail; + + freerdp_client_rdp_file_set_string_option(file, "fruits", "banana,oranges"); + + if (freerdp_client_rdp_file_set_integer_option(file, "numbers", 123456789) == -1) + { + printf("failed to set integer: numbers"); + return -1; + } + + freerdp_client_rdp_file_free(file); + + tmp = GetKnownPath(KNOWN_PATH_TEMP); + if (!tmp) + goto fail; + + base = append("%s/rdp-file-test-%" PRIx64, tmp, id); + if (!base) + goto fail; + if (!CreateDirectoryA(base, NULL)) + goto fail; + utfname = append("%s/utfname", base); + uniname = append("%s/uniname", base); + file = freerdp_client_rdp_file_new(); + if (!file || !utfname || !uniname) + goto fail; + + if (!freerdp_client_populate_rdp_file_from_settings(file, settings)) + goto fail; + + if (!freerdp_client_write_rdp_file(file, utfname, FALSE)) + goto fail; + + if (!freerdp_client_write_rdp_file(file, uniname, TRUE)) + goto fail; + + rc = 0; +fail: + if (utfname) + winpr_DeleteFile(utfname); + if (uniname) + winpr_DeleteFile(uniname); + if (base) + winpr_RemoveDirectory(base); + free(utfname); + free(uniname); + free(base); + free(tmp); + freerdp_client_rdp_file_free(file); + freerdp_settings_free(settings); + return rc; +} diff --git a/client/freerdp-client.pc.in b/client/freerdp-client.pc.in new file mode 100644 index 0000000..eca4ab8 --- /dev/null +++ b/client/freerdp-client.pc.in @@ -0,0 +1,15 @@ +prefix=@PKG_CONFIG_INSTALL_PREFIX@ +exec_prefix=${prefix} +libdir=${prefix}/@CMAKE_INSTALL_LIBDIR@ +includedir=${prefix}/@FREERDP_INCLUDE_DIR@ +libs=-lfreerdp-client@FREERDP_API_VERSION@ + +Name: FreeRDP client +Description: FreeRDP: A Remote Desktop Protocol Implementation +URL: http://www.freerdp.com/ +Version: @FREERDP_VERSION@ +Requires: +Requires.private: @WINPR_PKG_CONFIG_FILENAME@ freerdp@FREERDP_VERSION_MAJOR@ +Libs: -L${libdir} ${libs} +Libs.private: -ldl -lpthread +Cflags: -I${includedir} |