diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 01:24:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 01:24:41 +0000 |
commit | a9bcc81f821d7c66f623779fa5147e728eb3c388 (patch) | |
tree | 98676963bcdd537ae5908a067a8eb110b93486a6 /server/proxy | |
parent | Initial commit. (diff) | |
download | freerdp3-a9bcc81f821d7c66f623779fa5147e728eb3c388.tar.xz freerdp3-a9bcc81f821d7c66f623779fa5147e728eb3c388.zip |
Adding upstream version 3.3.0+dfsg1.upstream/3.3.0+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
38 files changed, 11530 insertions, 0 deletions
diff --git a/server/proxy/CMakeLists.txt b/server/proxy/CMakeLists.txt new file mode 100644 index 0000000..4693b17 --- /dev/null +++ b/server/proxy/CMakeLists.txt @@ -0,0 +1,131 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP Proxy Server +# +# Copyright 2019 Mati Shabtay <matishabtay@gmail.com> +# Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com> +# Copyright 2019 Idan Freiberg <speidy@gmail.com> +# Copyright 2021 Armin Novak <anovak@thincast.com> +# Copyright 2021 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(CMakeDependentOption) +set(MODULE_NAME "freerdp-server-proxy") +set(MODULE_PREFIX "FREERDP_SERVER_PROXY") + +set(${MODULE_PREFIX}_SRCS + pf_context.c + pf_channel.c + pf_channel.h + pf_client.c + pf_client.h + pf_input.c + pf_input.h + pf_update.c + pf_update.h + pf_server.c + pf_server.h + pf_config.c + pf_modules.c + pf_utils.h + pf_utils.c + $<TARGET_OBJECTS:pf_channels> + ) + +set(PROXY_APP_SRCS freerdp_proxy.c) + +option(WITH_PROXY_EMULATE_SMARTCARD "Compile proxy smartcard emulation" OFF) +add_subdirectory("channels") + +# 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 "${MODULE_NAME}${CMAKE_EXECUTABLE_SUFFIX}" ) + + configure_file( + ${PROJECT_SOURCE_DIR}/cmake/WindowsDLLVersion.rc.in + ${CMAKE_CURRENT_BINARY_DIR}/version.rc + @ONLY) + + set ( ${MODULE_PREFIX}_SRCS ${${MODULE_PREFIX}_SRCS} ${CMAKE_CURRENT_BINARY_DIR}/version.rc) + list(APPEND PROXY_APP_SRCS ${CMAKE_CURRENT_BINARY_DIR}/version.rc) +endif() + +add_library(${MODULE_NAME} ${${MODULE_PREFIX}_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() + +set(PRIVATE_LIBS + freerdp-client + freerdp-server +) + +set(PUBLIC_LIBS + winpr + freerdp +) + +target_include_directories(${MODULE_NAME} INTERFACE $<INSTALL_INTERFACE:include>) +target_link_libraries(${MODULE_NAME} PRIVATE ${PRIVATE_LIBS} PUBLIC ${PUBLIC_LIBS}) + +install(TARGETS ${MODULE_NAME} COMPONENT server EXPORT FreeRDP-ProxyTargets + 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 "Server/Proxy") + +# pkg-config +include(pkg-config-install-prefix) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/freerdp-proxy.pc.in ${CMAKE_CURRENT_BINARY_DIR}/${MODULE_NAME}${FREERDP_VERSION_MAJOR}.pc @ONLY) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${MODULE_NAME}${FREERDP_VERSION_MAJOR}.pc DESTINATION ${PKG_CONFIG_PC_INSTALL_DIR}) + +export(PACKAGE freerdp-proxy) + +SetFreeRDPCMakeInstallDir(FREERDP_PROXY_CMAKE_INSTALL_DIR "FreeRDP-Proxy${FREERDP_VERSION_MAJOR}") + +configure_package_config_file(FreeRDP-ProxyConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/FreeRDP-ProxyConfig.cmake + INSTALL_DESTINATION ${FREERDP_PROXY_CMAKE_INSTALL_DIR} + PATH_VARS FREERDP_INCLUDE_DIR) + +write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/FreeRDP-ProxyConfigVersion.cmake + VERSION ${FREERDP_VERSION} COMPATIBILITY SameMajorVersion) + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/FreeRDP-ProxyConfig.cmake ${CMAKE_CURRENT_BINARY_DIR}/FreeRDP-ProxyConfigVersion.cmake + DESTINATION ${FREERDP_PROXY_CMAKE_INSTALL_DIR}) +install(EXPORT FreeRDP-ProxyTargets DESTINATION ${FREERDP_PROXY_CMAKE_INSTALL_DIR}) + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "Server/proxy") + +option(WITH_PROXY_APP "Compile proxy application" ON) + +if (WITH_PROXY_APP) + add_subdirectory("cli") +endif() + +option(WITH_PROXY_MODULES "Compile proxy modules" ON) +if (WITH_PROXY_MODULES) + add_subdirectory("modules") +endif() + diff --git a/server/proxy/FreeRDP-ProxyConfig.cmake.in b/server/proxy/FreeRDP-ProxyConfig.cmake.in new file mode 100644 index 0000000..406da3a --- /dev/null +++ b/server/proxy/FreeRDP-ProxyConfig.cmake.in @@ -0,0 +1,10 @@ + +@PACKAGE_INIT@ + +set(FreeRDP-Proxy_VERSION_MAJOR "@FREERDP_VERSION_MAJOR@") +set(FreeRDP-Proxy_VERSION_MINOR "@FREERDP_VERSION_MINOR@") +set(FreeRDP-Proxy_VERSION_REVISION "@FREERDP_VERSION_REVISION@") + +set_and_check(FreeRDP-Proxy_INCLUDE_DIR "@PACKAGE_FREERDP_INCLUDE_DIR@") + +include("${CMAKE_CURRENT_LIST_DIR}/FreeRDP-ProxyTargets.cmake") diff --git a/server/proxy/channels/CMakeLists.txt b/server/proxy/channels/CMakeLists.txt new file mode 100644 index 0000000..1915d83 --- /dev/null +++ b/server/proxy/channels/CMakeLists.txt @@ -0,0 +1,17 @@ + +set(MODULE_NAME pf_channels) +set(SOURCES + pf_channel_rdpdr.c + pf_channel_rdpdr.h + pf_channel_drdynvc.c + pf_channel_drdynvc.h +) + +if (WITH_PROXY_EMULATE_SMARTCARD) + list(APPEND SOURCES + pf_channel_smartcard.c + pf_channel_smartcard.h + ) +endif() + +add_library(${MODULE_NAME} OBJECT ${SOURCES}) diff --git a/server/proxy/channels/pf_channel_drdynvc.c b/server/proxy/channels/pf_channel_drdynvc.c new file mode 100644 index 0000000..9d8cab9 --- /dev/null +++ b/server/proxy/channels/pf_channel_drdynvc.c @@ -0,0 +1,711 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * pf_channel_drdynvc + * + * Copyright 2022 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/channels/drdynvc.h> +#include <freerdp/utils/drdynvc.h> +#include <freerdp/server/proxy/proxy_log.h> + +#include "pf_channel_drdynvc.h" +#include "../pf_channel.h" +#include "../proxy_modules.h" +#include "../pf_utils.h" + +#define DTAG PROXY_TAG("drdynvc") + +/** @brief channel opened status */ +typedef enum +{ + CHANNEL_OPENSTATE_WAITING_OPEN_STATUS, /*!< dynamic channel waiting for create response */ + CHANNEL_OPENSTATE_OPENED, /*!< opened */ + CHANNEL_OPENSTATE_CLOSED /*!< dynamic channel has been opened then closed */ +} PfDynChannelOpenStatus; + +typedef struct p_server_dynamic_channel_context pServerDynamicChannelContext; +typedef struct DynChannelTrackerState DynChannelTrackerState; + +typedef PfChannelResult (*dynamic_channel_on_data_fn)(pServerContext* ps, + pServerDynamicChannelContext* channel, + BOOL isBackData, ChannelStateTracker* tracker, + BOOL firstPacket, BOOL lastPacket); + +/** @brief tracker state for a drdynvc stream */ +struct DynChannelTrackerState +{ + UINT32 currentDataLength; + UINT32 CurrentDataReceived; + UINT32 CurrentDataFragments; + wStream* currentPacket; + dynamic_channel_on_data_fn dataCallback; +}; + +typedef void (*channel_data_dtor_fn)(void** user_data); + +struct p_server_dynamic_channel_context +{ + char* channelName; + UINT32 channelId; + PfDynChannelOpenStatus openStatus; + pf_utils_channel_mode channelMode; + BOOL packetReassembly; + DynChannelTrackerState backTracker; + DynChannelTrackerState frontTracker; + + void* channelData; + channel_data_dtor_fn channelDataDtor; +}; + +/** @brief context for the dynamic channel */ +typedef struct +{ + wHashTable* channels; + ChannelStateTracker* backTracker; + ChannelStateTracker* frontTracker; + wLog* log; +} DynChannelContext; + +/** @brief result of dynamic channel packet treatment */ +typedef enum +{ + DYNCVC_READ_OK, /*!< read was OK */ + DYNCVC_READ_ERROR, /*!< an error happened during read */ + DYNCVC_READ_INCOMPLETE /*!< missing bytes to read the complete packet */ +} DynvcReadResult; + +static const char* openstatus2str(PfDynChannelOpenStatus status) +{ + switch (status) + { + case CHANNEL_OPENSTATE_WAITING_OPEN_STATUS: + return "CHANNEL_OPENSTATE_WAITING_OPEN_STATUS"; + case CHANNEL_OPENSTATE_CLOSED: + return "CHANNEL_OPENSTATE_CLOSED"; + case CHANNEL_OPENSTATE_OPENED: + return "CHANNEL_OPENSTATE_OPENED"; + default: + return "CHANNEL_OPENSTATE_UNKNOWN"; + } +} + +static PfChannelResult data_cb(pServerContext* ps, pServerDynamicChannelContext* channel, + BOOL isBackData, ChannelStateTracker* tracker, BOOL firstPacket, + BOOL lastPacket) +{ + WINPR_ASSERT(ps); + WINPR_ASSERT(channel); + WINPR_ASSERT(tracker); + WINPR_ASSERT(ps->pdata); + + wStream* currentPacket = channelTracker_getCurrentPacket(tracker); + proxyDynChannelInterceptData dyn = { .name = channel->channelName, + .channelId = channel->channelId, + .data = currentPacket, + .isBackData = isBackData, + .first = firstPacket, + .last = lastPacket, + .rewritten = FALSE, + .packetSize = channelTracker_getCurrentPacketSize(tracker), + .result = PF_CHANNEL_RESULT_ERROR }; + Stream_SealLength(dyn.data); + if (!pf_modules_run_filter(ps->pdata->module, FILTER_TYPE_INTERCEPT_CHANNEL, ps->pdata, &dyn)) + return PF_CHANNEL_RESULT_ERROR; + + channelTracker_setCurrentPacketSize(tracker, dyn.packetSize); + if (dyn.rewritten) + return channelTracker_flushCurrent(tracker, firstPacket, lastPacket, !isBackData); + return dyn.result; +} + +static pServerDynamicChannelContext* DynamicChannelContext_new(wLog* log, pServerContext* ps, + const char* name, UINT32 id) +{ + WINPR_ASSERT(log); + + pServerDynamicChannelContext* ret = calloc(1, sizeof(*ret)); + if (!ret) + { + WLog_Print(log, WLOG_ERROR, "error allocating dynamic channel context '%s'", name); + return NULL; + } + + ret->channelId = id; + ret->channelName = _strdup(name); + if (!ret->channelName) + { + WLog_Print(log, WLOG_ERROR, "error allocating name in dynamic channel context '%s'", name); + free(ret); + return NULL; + } + + ret->frontTracker.dataCallback = data_cb; + ret->backTracker.dataCallback = data_cb; + + proxyChannelToInterceptData dyn = { .name = name, .channelId = id, .intercept = FALSE }; + if (pf_modules_run_filter(ps->pdata->module, FILTER_TYPE_DYN_INTERCEPT_LIST, ps->pdata, &dyn) && + dyn.intercept) + ret->channelMode = PF_UTILS_CHANNEL_INTERCEPT; + else + ret->channelMode = pf_utils_get_channel_mode(ps->pdata->config, name); + ret->openStatus = CHANNEL_OPENSTATE_OPENED; + ret->packetReassembly = (ret->channelMode == PF_UTILS_CHANNEL_INTERCEPT); + + return ret; +} + +static void DynamicChannelContext_free(void* ptr) +{ + pServerDynamicChannelContext* c = (pServerDynamicChannelContext*)ptr; + if (!c) + return; + + if (c->backTracker.currentPacket) + Stream_Free(c->backTracker.currentPacket, TRUE); + + if (c->frontTracker.currentPacket) + Stream_Free(c->frontTracker.currentPacket, TRUE); + + if (c->channelDataDtor) + c->channelDataDtor(&c->channelData); + + free(c->channelName); + free(c); +} + +static UINT32 ChannelId_Hash(const void* key) +{ + const UINT32* v = (const UINT32*)key; + return *v; +} + +static BOOL ChannelId_Compare(const void* objA, const void* objB) +{ + const UINT32* v1 = objA; + const UINT32* v2 = objB; + return (*v1 == *v2); +} + +static DynvcReadResult dynvc_read_varInt(wLog* log, wStream* s, size_t len, UINT64* varInt, + BOOL last) +{ + WINPR_ASSERT(varInt); + switch (len) + { + case 0x00: + if (!Stream_CheckAndLogRequiredLengthWLog(log, s, 1)) + return last ? DYNCVC_READ_ERROR : DYNCVC_READ_INCOMPLETE; + Stream_Read_UINT8(s, *varInt); + break; + case 0x01: + if (!Stream_CheckAndLogRequiredLengthWLog(log, s, 2)) + return last ? DYNCVC_READ_ERROR : DYNCVC_READ_INCOMPLETE; + Stream_Read_UINT16(s, *varInt); + break; + case 0x02: + if (!Stream_CheckAndLogRequiredLengthWLog(log, s, 4)) + return last ? DYNCVC_READ_ERROR : DYNCVC_READ_INCOMPLETE; + Stream_Read_UINT32(s, *varInt); + break; + case 0x03: + default: + WLog_Print(log, WLOG_ERROR, "Unknown int len %" PRIuz, len); + return DYNCVC_READ_ERROR; + } + return DYNCVC_READ_OK; +} + +static PfChannelResult DynvcTrackerPeekFn(ChannelStateTracker* tracker, BOOL firstPacket, + BOOL lastPacket) +{ + BYTE cmd = 0; + BYTE byte0 = 0; + wStream* s = NULL; + wStream sbuffer; + BOOL haveChannelId = 0; + BOOL haveLength = 0; + UINT64 dynChannelId = 0; + UINT64 Length = 0; + pServerDynamicChannelContext* dynChannel = NULL; + + WINPR_ASSERT(tracker); + + DynChannelContext* dynChannelContext = + (DynChannelContext*)channelTracker_getCustomData(tracker); + WINPR_ASSERT(dynChannelContext); + + BOOL isBackData = (tracker == dynChannelContext->backTracker); + DynChannelTrackerState* trackerState = NULL; + + UINT32 flags = lastPacket ? CHANNEL_FLAG_LAST : 0; + proxyData* pdata = channelTracker_getPData(tracker); + WINPR_ASSERT(pdata); + + const char* direction = isBackData ? "B->F" : "F->B"; + + { + wStream* currentPacket = channelTracker_getCurrentPacket(tracker); + s = Stream_StaticConstInit(&sbuffer, Stream_Buffer(currentPacket), + Stream_GetPosition(currentPacket)); + } + + if (!Stream_CheckAndLogRequiredLengthWLog(dynChannelContext->log, s, 1)) + return PF_CHANNEL_RESULT_ERROR; + + Stream_Read_UINT8(s, byte0); + cmd = byte0 >> 4; + + switch (cmd) + { + case CREATE_REQUEST_PDU: + case CLOSE_REQUEST_PDU: + case DATA_PDU: + case DATA_COMPRESSED_PDU: + haveChannelId = TRUE; + haveLength = FALSE; + break; + case DATA_FIRST_PDU: + case DATA_FIRST_COMPRESSED_PDU: + haveLength = TRUE; + haveChannelId = TRUE; + break; + default: + haveChannelId = FALSE; + haveLength = FALSE; + break; + } + + if (haveChannelId) + { + BYTE cbId = byte0 & 0x03; + + switch (dynvc_read_varInt(dynChannelContext->log, s, cbId, &dynChannelId, lastPacket)) + { + case DYNCVC_READ_OK: + break; + case DYNCVC_READ_INCOMPLETE: + return PF_CHANNEL_RESULT_DROP; + case DYNCVC_READ_ERROR: + default: + WLog_Print(dynChannelContext->log, WLOG_ERROR, + "DynvcTrackerPeekFn: invalid channelId field"); + return PF_CHANNEL_RESULT_ERROR; + } + + /* we always try to retrieve the dynamic channel in case it would have been opened + * and closed + */ + dynChannel = (pServerDynamicChannelContext*)HashTable_GetItemValue( + dynChannelContext->channels, &dynChannelId); + if (cmd != CREATE_REQUEST_PDU || !isBackData) + { + if (!dynChannel) + { + /* we've not found the target channel, so we drop this chunk, plus all the rest of + * the packet */ + channelTracker_setMode(tracker, CHANNEL_TRACKER_DROP); + return PF_CHANNEL_RESULT_DROP; + } + } + } + + if (haveLength) + { + BYTE lenLen = (byte0 >> 2) & 0x03; + switch (dynvc_read_varInt(dynChannelContext->log, s, lenLen, &Length, lastPacket)) + { + case DYNCVC_READ_OK: + break; + case DYNCVC_READ_INCOMPLETE: + return PF_CHANNEL_RESULT_DROP; + case DYNCVC_READ_ERROR: + default: + WLog_Print(dynChannelContext->log, WLOG_ERROR, + "DynvcTrackerPeekFn: invalid length field"); + return PF_CHANNEL_RESULT_ERROR; + } + } + + switch (cmd) + { + case CAPABILITY_REQUEST_PDU: + WLog_Print(dynChannelContext->log, WLOG_DEBUG, "DynvcTracker: %s CAPABILITY_%s", + direction, isBackData ? "REQUEST" : "RESPONSE"); + channelTracker_setMode(tracker, CHANNEL_TRACKER_PASS); + return PF_CHANNEL_RESULT_PASS; + + case CREATE_REQUEST_PDU: + { + UINT32 creationStatus = 0; + + /* we only want the full packet */ + if (!lastPacket) + return PF_CHANNEL_RESULT_DROP; + + if (isBackData) + { + proxyChannelDataEventInfo dev = { 0 }; + const char* name = Stream_ConstPointer(s); + const size_t nameLen = Stream_GetRemainingLength(s); + + const size_t len = strnlen(name, nameLen); + if ((len == 0) || (len == nameLen)) + return PF_CHANNEL_RESULT_ERROR; + + wStream* currentPacket = channelTracker_getCurrentPacket(tracker); + dev.channel_id = dynChannelId; + dev.channel_name = name; + dev.data = Stream_Buffer(s); + dev.data_len = Stream_GetPosition(currentPacket); + dev.flags = flags; + dev.total_size = Stream_GetPosition(currentPacket); + + if (dynChannel) + { + WLog_Print( + dynChannelContext->log, WLOG_WARN, + "Reusing channel id %" PRIu32 ", previously %s [state %s, mode %s], now %s", + dynChannel->channelId, dynChannel->channelName, + openstatus2str(dynChannel->openStatus), + pf_utils_channel_mode_string(dynChannel->channelMode), dev.channel_name); + + HashTable_Remove(dynChannelContext->channels, &dynChannel->channelId); + } + + if (!pf_modules_run_filter(pdata->module, + FILTER_TYPE_CLIENT_PASSTHROUGH_DYN_CHANNEL_CREATE, pdata, + &dev)) + return PF_CHANNEL_RESULT_DROP; /* Silently drop */ + + dynChannel = DynamicChannelContext_new(dynChannelContext->log, pdata->ps, name, + dynChannelId); + if (!dynChannel) + { + WLog_Print(dynChannelContext->log, WLOG_ERROR, + "unable to create dynamic channel context data"); + return PF_CHANNEL_RESULT_ERROR; + } + + WLog_Print(dynChannelContext->log, WLOG_DEBUG, "Adding channel '%s'[%d]", + dynChannel->channelName, dynChannel->channelId); + if (!HashTable_Insert(dynChannelContext->channels, &dynChannel->channelId, + dynChannel)) + { + WLog_Print(dynChannelContext->log, WLOG_ERROR, + "unable register dynamic channel context data"); + DynamicChannelContext_free(dynChannel); + return PF_CHANNEL_RESULT_ERROR; + } + + dynChannel->openStatus = CHANNEL_OPENSTATE_WAITING_OPEN_STATUS; + + // NOLINTNEXTLINE(clang-analyzer-unix.Malloc): HashTable_Insert owns dynChannel + return channelTracker_flushCurrent(tracker, firstPacket, lastPacket, FALSE); + } + + /* CREATE_REQUEST_PDU response */ + if (!Stream_CheckAndLogRequiredLengthWLog(dynChannelContext->log, s, 4)) + return PF_CHANNEL_RESULT_ERROR; + + Stream_Read_UINT32(s, creationStatus); + WLog_Print(dynChannelContext->log, WLOG_DEBUG, + "DynvcTracker(%" PRIu64 ",%s): %s CREATE_RESPONSE openStatus=%" PRIu32, + dynChannelId, dynChannel->channelName, direction, creationStatus); + + if (creationStatus == 0) + dynChannel->openStatus = CHANNEL_OPENSTATE_OPENED; + + return channelTracker_flushCurrent(tracker, firstPacket, lastPacket, TRUE); + } + + case CLOSE_REQUEST_PDU: + if (!lastPacket) + return PF_CHANNEL_RESULT_DROP; + + WLog_Print(dynChannelContext->log, WLOG_DEBUG, + "DynvcTracker(%s): %s Close request on channel", dynChannel->channelName, + direction); + channelTracker_setMode(tracker, CHANNEL_TRACKER_PASS); + if (dynChannel->openStatus != CHANNEL_OPENSTATE_OPENED) + { + WLog_Print(dynChannelContext->log, WLOG_WARN, + "DynvcTracker(%s): is in state %s, expected %s", dynChannel->channelName, + openstatus2str(dynChannel->openStatus), + openstatus2str(CHANNEL_OPENSTATE_OPENED)); + } + dynChannel->openStatus = CHANNEL_OPENSTATE_CLOSED; + return channelTracker_flushCurrent(tracker, firstPacket, lastPacket, !isBackData); + + case SOFT_SYNC_REQUEST_PDU: + /* just pass then as is for now */ + WLog_Print(dynChannelContext->log, WLOG_DEBUG, "SOFT_SYNC_REQUEST_PDU"); + channelTracker_setMode(tracker, CHANNEL_TRACKER_PASS); + /*TODO: return pf_treat_softsync_req(pdata, s);*/ + return PF_CHANNEL_RESULT_PASS; + + case SOFT_SYNC_RESPONSE_PDU: + /* just pass then as is for now */ + WLog_Print(dynChannelContext->log, WLOG_DEBUG, "SOFT_SYNC_RESPONSE_PDU"); + channelTracker_setMode(tracker, CHANNEL_TRACKER_PASS); + return PF_CHANNEL_RESULT_PASS; + + case DATA_FIRST_PDU: + case DATA_PDU: + /* treat these below */ + trackerState = isBackData ? &dynChannel->backTracker : &dynChannel->frontTracker; + break; + + case DATA_FIRST_COMPRESSED_PDU: + case DATA_COMPRESSED_PDU: + WLog_Print(dynChannelContext->log, WLOG_DEBUG, + "TODO: compressed data packets, pass them as is for now"); + channelTracker_setMode(tracker, CHANNEL_TRACKER_PASS); + return channelTracker_flushCurrent(tracker, firstPacket, lastPacket, !isBackData); + + default: + return PF_CHANNEL_RESULT_ERROR; + } + + if (dynChannel->openStatus != CHANNEL_OPENSTATE_OPENED) + { + WLog_Print(dynChannelContext->log, WLOG_ERROR, + "DynvcTracker(%s [%s]): channel is not opened", dynChannel->channelName, + drdynvc_get_packet_type(cmd)); + return PF_CHANNEL_RESULT_ERROR; + } + + if ((cmd == DATA_FIRST_PDU) || (cmd == DATA_FIRST_COMPRESSED_PDU)) + { + WLog_Print(dynChannelContext->log, WLOG_DEBUG, + "DynvcTracker(%s [%s]): %s DATA_FIRST currentPacketLength=%" PRIu64 "", + dynChannel->channelName, drdynvc_get_packet_type(cmd), direction, Length); + trackerState->currentDataLength = Length; + trackerState->CurrentDataReceived = 0; + trackerState->CurrentDataFragments = 0; + + if (dynChannel->packetReassembly) + { + if (trackerState->currentPacket) + Stream_SetPosition(trackerState->currentPacket, 0); + } + } + + if (cmd == DATA_PDU || cmd == DATA_FIRST_PDU) + { + size_t extraSize = Stream_GetRemainingLength(s); + + trackerState->CurrentDataFragments++; + trackerState->CurrentDataReceived += extraSize; + + if (dynChannel->packetReassembly) + { + if (!trackerState->currentPacket) + { + trackerState->currentPacket = Stream_New(NULL, 1024); + if (!trackerState->currentPacket) + { + WLog_Print(dynChannelContext->log, WLOG_ERROR, + "unable to create current packet"); + return PF_CHANNEL_RESULT_ERROR; + } + } + + if (!Stream_EnsureRemainingCapacity(trackerState->currentPacket, extraSize)) + { + WLog_Print(dynChannelContext->log, WLOG_ERROR, "unable to grow current packet"); + return PF_CHANNEL_RESULT_ERROR; + } + + Stream_Write(trackerState->currentPacket, Stream_ConstPointer(s), extraSize); + } + WLog_Print(dynChannelContext->log, WLOG_DEBUG, + "DynvcTracker(%s [%s]): %s frags=%" PRIu32 " received=%" PRIu32 "(%" PRIu32 ")", + dynChannel->channelName, drdynvc_get_packet_type(cmd), direction, + trackerState->CurrentDataFragments, trackerState->CurrentDataReceived, + trackerState->currentDataLength); + } + + if (cmd == DATA_PDU) + { + if (trackerState->currentDataLength) + { + if (trackerState->CurrentDataReceived > trackerState->currentDataLength) + { + WLog_Print(dynChannelContext->log, WLOG_ERROR, + "DynvcTracker (%s [%s]): reassembled packet (%" PRIu32 + ") is bigger than announced length (%" PRIu32 ")", + dynChannel->channelName, drdynvc_get_packet_type(cmd), + trackerState->CurrentDataReceived, trackerState->currentDataLength); + return PF_CHANNEL_RESULT_ERROR; + } + } + else + { + trackerState->CurrentDataFragments = 0; + trackerState->CurrentDataReceived = 0; + } + } + + PfChannelResult result = PF_CHANNEL_RESULT_ERROR; + switch (dynChannel->channelMode) + { + case PF_UTILS_CHANNEL_PASSTHROUGH: + result = channelTracker_flushCurrent(tracker, firstPacket, lastPacket, !isBackData); + break; + case PF_UTILS_CHANNEL_BLOCK: + channelTracker_setMode(tracker, CHANNEL_TRACKER_DROP); + result = PF_CHANNEL_RESULT_DROP; + break; + case PF_UTILS_CHANNEL_INTERCEPT: + if (trackerState->dataCallback) + { + result = trackerState->dataCallback(pdata->ps, dynChannel, isBackData, tracker, + firstPacket, lastPacket); + } + else + { + WLog_Print(dynChannelContext->log, WLOG_ERROR, + "no intercept callback for channel %s(fromBack=%d), dropping packet", + dynChannel->channelName, isBackData); + result = PF_CHANNEL_RESULT_DROP; + } + break; + default: + WLog_Print(dynChannelContext->log, WLOG_ERROR, "unknown channel mode %d", + dynChannel->channelMode); + result = PF_CHANNEL_RESULT_ERROR; + break; + } + + if (!trackerState->currentDataLength || + (trackerState->CurrentDataReceived == trackerState->currentDataLength)) + { + trackerState->currentDataLength = 0; + trackerState->CurrentDataFragments = 0; + trackerState->CurrentDataReceived = 0; + + if (dynChannel->packetReassembly && trackerState->currentPacket) + Stream_SetPosition(trackerState->currentPacket, 0); + } + + return result; +} + +static void DynChannelContext_free(void* context) +{ + DynChannelContext* c = context; + if (!c) + return; + channelTracker_free(c->backTracker); + channelTracker_free(c->frontTracker); + HashTable_Free(c->channels); + free(c); +} + +static const char* dynamic_context(void* arg) +{ + proxyData* pdata = arg; + if (!pdata) + return "pdata=null"; + return pdata->session_id; +} + +static DynChannelContext* DynChannelContext_new(proxyData* pdata, + pServerStaticChannelContext* channel) +{ + DynChannelContext* dyn = calloc(1, sizeof(DynChannelContext)); + if (!dyn) + return FALSE; + + dyn->log = WLog_Get(DTAG); + WINPR_ASSERT(dyn->log); + WLog_SetContext(dyn->log, dynamic_context, pdata); + + dyn->backTracker = channelTracker_new(channel, DynvcTrackerPeekFn, dyn); + if (!dyn->backTracker) + goto fail; + if (!channelTracker_setPData(dyn->backTracker, pdata)) + goto fail; + + dyn->frontTracker = channelTracker_new(channel, DynvcTrackerPeekFn, dyn); + if (!dyn->frontTracker) + goto fail; + if (!channelTracker_setPData(dyn->frontTracker, pdata)) + goto fail; + + dyn->channels = HashTable_New(FALSE); + if (!dyn->channels) + goto fail; + + if (!HashTable_SetHashFunction(dyn->channels, ChannelId_Hash)) + goto fail; + + wObject* kobj = HashTable_KeyObject(dyn->channels); + WINPR_ASSERT(kobj); + kobj->fnObjectEquals = ChannelId_Compare; + + wObject* vobj = HashTable_ValueObject(dyn->channels); + WINPR_ASSERT(vobj); + vobj->fnObjectFree = DynamicChannelContext_free; + + return dyn; + +fail: + DynChannelContext_free(dyn); + return NULL; +} + +static PfChannelResult pf_dynvc_back_data(proxyData* pdata, + const pServerStaticChannelContext* channel, + const BYTE* xdata, size_t xsize, UINT32 flags, + size_t totalSize) +{ + WINPR_ASSERT(channel); + + DynChannelContext* dyn = (DynChannelContext*)channel->context; + WINPR_UNUSED(pdata); + WINPR_ASSERT(dyn); + + return channelTracker_update(dyn->backTracker, xdata, xsize, flags, totalSize); +} + +static PfChannelResult pf_dynvc_front_data(proxyData* pdata, + const pServerStaticChannelContext* channel, + const BYTE* xdata, size_t xsize, UINT32 flags, + size_t totalSize) +{ + WINPR_ASSERT(channel); + + DynChannelContext* dyn = (DynChannelContext*)channel->context; + WINPR_UNUSED(pdata); + WINPR_ASSERT(dyn); + + return channelTracker_update(dyn->frontTracker, xdata, xsize, flags, totalSize); +} + +BOOL pf_channel_setup_drdynvc(proxyData* pdata, pServerStaticChannelContext* channel) +{ + DynChannelContext* ret = DynChannelContext_new(pdata, channel); + if (!ret) + return FALSE; + + channel->onBackData = pf_dynvc_back_data; + channel->onFrontData = pf_dynvc_front_data; + channel->contextDtor = DynChannelContext_free; + channel->context = ret; + return TRUE; +} diff --git a/server/proxy/channels/pf_channel_drdynvc.h b/server/proxy/channels/pf_channel_drdynvc.h new file mode 100644 index 0000000..b084143 --- /dev/null +++ b/server/proxy/channels/pf_channel_drdynvc.h @@ -0,0 +1,26 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * pf_channel_drdynvc + * + * Copyright 2022 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 SERVER_PROXY_CHANNELS_PF_CHANNEL_DRDYNVC_H_ +#define SERVER_PROXY_CHANNELS_PF_CHANNEL_DRDYNVC_H_ + +#include <freerdp/server/proxy/proxy_context.h> + +BOOL pf_channel_setup_drdynvc(proxyData* pdata, pServerStaticChannelContext* channel); + +#endif /* SERVER_PROXY_CHANNELS_PF_CHANNEL_DRDYNVC_H_ */ diff --git a/server/proxy/channels/pf_channel_rdpdr.c b/server/proxy/channels/pf_channel_rdpdr.c new file mode 100644 index 0000000..cb79266 --- /dev/null +++ b/server/proxy/channels/pf_channel_rdpdr.c @@ -0,0 +1,2017 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2021 Armin Novak <armin.novak@thincast.com> + * Copyright 2021 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 <winpr/string.h> +#include <winpr/print.h> + +#include "pf_channel_rdpdr.h" +#include "pf_channel_smartcard.h" + +#include <freerdp/server/proxy/proxy_log.h> +#include <freerdp/channels/rdpdr.h> +#include <freerdp/channels/channels.h> +#include <freerdp/utils/rdpdr_utils.h> + +#define RTAG PROXY_TAG("channel.rdpdr") + +#define SCARD_DEVICE_ID UINT32_MAX + +typedef struct +{ + InterceptContextMapEntry base; + wStream* s; + wStream* buffer; + UINT16 versionMajor; + UINT16 versionMinor; + UINT32 clientID; + UINT32 computerNameLen; + BOOL computerNameUnicode; + union + { + WCHAR* wc; + char* c; + void* v; + } computerName; + UINT32 SpecialDeviceCount; + UINT32 capabilityVersions[6]; +} pf_channel_common_context; + +typedef enum +{ + STATE_CLIENT_EXPECT_SERVER_ANNOUNCE_REQUEST = 0x01, + STATE_CLIENT_EXPECT_SERVER_CORE_CAPABILITY_REQUEST = 0x02, + STATE_CLIENT_EXPECT_SERVER_CLIENT_ID_CONFIRM = 0x04, + STATE_CLIENT_CHANNEL_RUNNING = 0x10 +} pf_channel_client_state; + +typedef struct +{ + pf_channel_common_context common; + pf_channel_client_state state; + UINT32 flags; + UINT16 maxMajorVersion; + UINT16 maxMinorVersion; + wQueue* queue; + wLog* log; +} pf_channel_client_context; + +typedef enum +{ + STATE_SERVER_INITIAL, + STATE_SERVER_EXPECT_CLIENT_ANNOUNCE_REPLY, + STATE_SERVER_EXPECT_CLIENT_NAME_REQUEST, + STATE_SERVER_EXPECT_EXPECT_CLIENT_CAPABILITY_RESPONE, + STATE_SERVER_CHANNEL_RUNNING +} pf_channel_server_state; + +typedef struct +{ + pf_channel_common_context common; + pf_channel_server_state state; + DWORD SessionId; + HANDLE handle; + wArrayList* blockedDevices; + wLog* log; +} pf_channel_server_context; + +#define proxy_client "[proxy<-->client]" +#define proxy_server "[proxy<-->server]" + +#define proxy_client_rx proxy_client " receive" +#define proxy_client_tx proxy_client " send" +#define proxy_server_rx proxy_server " receive" +#define proxy_server_tx proxy_server " send" + +#define SERVER_RX_LOG(log, lvl, fmt, ...) WLog_Print(log, lvl, proxy_client_rx fmt, ##__VA_ARGS__) +#define CLIENT_RX_LOG(log, lvl, fmt, ...) WLog_Print(log, lvl, proxy_server_rx fmt, ##__VA_ARGS__) +#define SERVER_TX_LOG(log, lvl, fmt, ...) WLog_Print(log, lvl, proxy_client_tx fmt, ##__VA_ARGS__) +#define CLIENT_TX_LOG(log, lvl, fmt, ...) WLog_Print(log, lvl, proxy_server_tx fmt, ##__VA_ARGS__) +#define RX_LOG(srv, lvl, fmt, ...) \ + do \ + { \ + if (srv) \ + { \ + SERVER_RX_LOG(lvl, fmt, ##__VA_ARGS__); \ + } \ + else \ + { \ + CLIENT_RX_LOG(lvl, fmt, ##__VA_ARGS__); \ + } \ + } while (0) + +#define SERVER_RXTX_LOG(send, log, lvl, fmt, ...) \ + do \ + { \ + if (send) \ + { \ + SERVER_TX_LOG(log, lvl, fmt, ##__VA_ARGS__); \ + } \ + else \ + { \ + SERVER_RX_LOG(log, lvl, fmt, ##__VA_ARGS__); \ + } \ + } while (0) + +#define Stream_CheckAndLogRequiredLengthSrv(log, s, len) \ + Stream_CheckAndLogRequiredLengthWLogEx(log, WLOG_WARN, s, len, 1, \ + proxy_client_rx " %s(%s:%" PRIuz ")", __func__, \ + __FILE__, (size_t)__LINE__) +#define Stream_CheckAndLogRequiredLengthClient(log, s, len) \ + Stream_CheckAndLogRequiredLengthWLogEx(log, WLOG_WARN, s, len, 1, \ + proxy_server_rx " %s(%s:%" PRIuz ")", __func__, \ + __FILE__, (size_t)__LINE__) +#define Stream_CheckAndLogRequiredLengthRx(srv, log, s, len) \ + Stream_CheckAndLogRequiredLengthRx_(srv, log, s, len, 1, __func__, __FILE__, __LINE__) +static BOOL Stream_CheckAndLogRequiredLengthRx_(BOOL srv, wLog* log, wStream* s, size_t nmemb, + size_t size, const char* fkt, const char* file, + size_t line) +{ + const char* fmt = + srv ? proxy_server_rx " %s(%s:%" PRIuz ")" : proxy_client_rx " %s(%s:%" PRIuz ")"; + + return Stream_CheckAndLogRequiredLengthWLogEx(log, WLOG_WARN, s, nmemb, size, fmt, fkt, file, + line); +} + +static const char* rdpdr_server_state_to_string(pf_channel_server_state state) +{ + switch (state) + { + case STATE_SERVER_INITIAL: + return "STATE_SERVER_INITIAL"; + case STATE_SERVER_EXPECT_CLIENT_ANNOUNCE_REPLY: + return "STATE_SERVER_EXPECT_CLIENT_ANNOUNCE_REPLY"; + case STATE_SERVER_EXPECT_CLIENT_NAME_REQUEST: + return "STATE_SERVER_EXPECT_CLIENT_NAME_REQUEST"; + case STATE_SERVER_EXPECT_EXPECT_CLIENT_CAPABILITY_RESPONE: + return "STATE_SERVER_EXPECT_EXPECT_CLIENT_CAPABILITY_RESPONE"; + case STATE_SERVER_CHANNEL_RUNNING: + return "STATE_SERVER_CHANNEL_RUNNING"; + default: + return "STATE_SERVER_UNKNOWN"; + } +} + +static const char* rdpdr_client_state_to_string(pf_channel_client_state state) +{ + switch (state) + { + case STATE_CLIENT_EXPECT_SERVER_ANNOUNCE_REQUEST: + return "STATE_CLIENT_EXPECT_SERVER_ANNOUNCE_REQUEST"; + case STATE_CLIENT_EXPECT_SERVER_CORE_CAPABILITY_REQUEST: + return "STATE_CLIENT_EXPECT_SERVER_CORE_CAPABILITY_REQUEST"; + case STATE_CLIENT_EXPECT_SERVER_CLIENT_ID_CONFIRM: + return "STATE_CLIENT_EXPECT_SERVER_CLIENT_ID_CONFIRM"; + case STATE_CLIENT_CHANNEL_RUNNING: + return "STATE_CLIENT_CHANNEL_RUNNING"; + default: + return "STATE_CLIENT_UNKNOWN"; + } +} + +static wStream* rdpdr_get_send_buffer(pf_channel_common_context* rdpdr, UINT16 component, + UINT16 PacketID, size_t capacity) +{ + WINPR_ASSERT(rdpdr); + WINPR_ASSERT(rdpdr->s); + if (!Stream_SetPosition(rdpdr->s, 0)) + return NULL; + if (!Stream_EnsureCapacity(rdpdr->s, capacity + 4)) + return NULL; + Stream_Write_UINT16(rdpdr->s, component); + Stream_Write_UINT16(rdpdr->s, PacketID); + return rdpdr->s; +} + +static wStream* rdpdr_client_get_send_buffer(pf_channel_client_context* rdpdr, UINT16 component, + UINT16 PacketID, size_t capacity) +{ + WINPR_ASSERT(rdpdr); + return rdpdr_get_send_buffer(&rdpdr->common, component, PacketID, capacity); +} + +static wStream* rdpdr_server_get_send_buffer(pf_channel_server_context* rdpdr, UINT16 component, + UINT16 PacketID, size_t capacity) +{ + WINPR_ASSERT(rdpdr); + return rdpdr_get_send_buffer(&rdpdr->common, component, PacketID, capacity); +} + +static UINT rdpdr_client_send(wLog* log, pClientContext* pc, wStream* s) +{ + UINT16 channelId = 0; + + WINPR_ASSERT(log); + WINPR_ASSERT(pc); + WINPR_ASSERT(s); + WINPR_ASSERT(pc->context.instance); + + if (!pc->connected) + { + CLIENT_TX_LOG(log, WLOG_WARN, "Ignoring channel %s message, not connected!", + RDPDR_SVC_CHANNEL_NAME); + return CHANNEL_RC_OK; + } + + channelId = freerdp_channels_get_id_by_name(pc->context.instance, RDPDR_SVC_CHANNEL_NAME); + /* Ignore unmappable channels. Might happen when the channel was already down and + * some delayed message is tried to be sent. */ + if ((channelId == 0) || (channelId == UINT16_MAX)) + return ERROR_INTERNAL_ERROR; + + Stream_SealLength(s); + rdpdr_dump_send_packet(log, WLOG_TRACE, s, proxy_server_tx); + WINPR_ASSERT(pc->context.instance->SendChannelData); + if (!pc->context.instance->SendChannelData(pc->context.instance, channelId, Stream_Buffer(s), + Stream_Length(s))) + return ERROR_EVT_CHANNEL_NOT_FOUND; + return CHANNEL_RC_OK; +} + +static UINT rdpdr_seal_send_free_request(pf_channel_server_context* context, wStream* s) +{ + BOOL status = 0; + size_t len = 0; + + WINPR_ASSERT(context); + WINPR_ASSERT(context->handle); + WINPR_ASSERT(s); + + Stream_SealLength(s); + len = Stream_Length(s); + WINPR_ASSERT(len <= ULONG_MAX); + + rdpdr_dump_send_packet(context->log, WLOG_TRACE, s, proxy_client_tx); + status = WTSVirtualChannelWrite(context->handle, (char*)Stream_Buffer(s), (ULONG)len, NULL); + return (status) ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR; +} + +static BOOL rdpdr_process_server_header(BOOL server, wLog* log, wStream* s, UINT16 component, + UINT16 PacketId, size_t expect) +{ + UINT16 rpacketid = 0; + UINT16 rcomponent = 0; + + WINPR_ASSERT(s); + if (!Stream_CheckAndLogRequiredLengthRx(server, log, s, 4)) + { + RX_LOG(server, log, WLOG_WARN, "RDPDR_HEADER[%s | %s]: expected length 4, got %" PRIuz, + rdpdr_component_string(component), rdpdr_packetid_string(PacketId), + Stream_GetRemainingLength(s)); + return FALSE; + } + + Stream_Read_UINT16(s, rcomponent); + Stream_Read_UINT16(s, rpacketid); + + if (rcomponent != component) + { + RX_LOG(server, log, WLOG_WARN, "RDPDR_HEADER[%s | %s]: got component %s", + rdpdr_component_string(component), rdpdr_packetid_string(PacketId), + rdpdr_component_string(rcomponent)); + return FALSE; + } + + if (rpacketid != PacketId) + { + RX_LOG(server, log, WLOG_WARN, "RDPDR_HEADER[%s | %s]: got PacketID %s", + rdpdr_component_string(component), rdpdr_packetid_string(PacketId), + rdpdr_packetid_string(rpacketid)); + return FALSE; + } + + if (!Stream_CheckAndLogRequiredLengthRx(server, log, s, expect)) + { + RX_LOG(server, log, WLOG_WARN, + "RDPDR_HEADER[%s | %s] not enought data, expected %" PRIuz ", " + "got %" PRIuz, + rdpdr_component_string(component), rdpdr_packetid_string(PacketId), expect, + Stream_GetRemainingLength(s)); + return ERROR_INVALID_DATA; + } + + return TRUE; +} + +static BOOL rdpdr_check_version(BOOL server, wLog* log, UINT16 versionMajor, UINT16 versionMinor, + UINT16 component, UINT16 PacketId) +{ + if (versionMajor != RDPDR_VERSION_MAJOR) + { + RX_LOG(server, log, WLOG_WARN, "[%s | %s] expected MajorVersion %" PRIu16 ", got %" PRIu16, + rdpdr_component_string(component), rdpdr_packetid_string(PacketId), + RDPDR_VERSION_MAJOR, versionMajor); + return FALSE; + } + switch (versionMinor) + { + case RDPDR_VERSION_MINOR_RDP50: + case RDPDR_VERSION_MINOR_RDP51: + case RDPDR_VERSION_MINOR_RDP52: + case RDPDR_VERSION_MINOR_RDP6X: + case RDPDR_VERSION_MINOR_RDP10X: + break; + default: + { + RX_LOG(server, log, WLOG_WARN, "[%s | %s] unsupported MinorVersion %" PRIu16, + rdpdr_component_string(component), rdpdr_packetid_string(PacketId), + versionMinor); + return FALSE; + } + } + return TRUE; +} + +static UINT rdpdr_process_server_announce_request(pf_channel_client_context* rdpdr, wStream* s) +{ + const UINT16 component = RDPDR_CTYP_CORE; + const UINT16 packetid = PAKID_CORE_SERVER_ANNOUNCE; + WINPR_ASSERT(rdpdr); + WINPR_ASSERT(s); + + if (!rdpdr_process_server_header(FALSE, rdpdr->log, s, component, packetid, 8)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, rdpdr->common.versionMajor); + Stream_Read_UINT16(s, rdpdr->common.versionMinor); + + if (!rdpdr_check_version(FALSE, rdpdr->log, rdpdr->common.versionMajor, + rdpdr->common.versionMinor, component, packetid)) + return ERROR_INVALID_DATA; + + /* Limit maximum channel protocol version to the one set by proxy server */ + if (rdpdr->common.versionMajor > rdpdr->maxMajorVersion) + { + rdpdr->common.versionMajor = rdpdr->maxMajorVersion; + rdpdr->common.versionMinor = rdpdr->maxMinorVersion; + } + else if (rdpdr->common.versionMinor > rdpdr->maxMinorVersion) + rdpdr->common.versionMinor = rdpdr->maxMinorVersion; + + Stream_Read_UINT32(s, rdpdr->common.clientID); + return CHANNEL_RC_OK; +} + +static UINT rdpdr_server_send_announce_request(pf_channel_server_context* context) +{ + wStream* s = + rdpdr_server_get_send_buffer(context, RDPDR_CTYP_CORE, PAKID_CORE_SERVER_ANNOUNCE, 8); + if (!s) + return CHANNEL_RC_NO_MEMORY; + + Stream_Write_UINT16(s, context->common.versionMajor); /* VersionMajor (2 bytes) */ + Stream_Write_UINT16(s, context->common.versionMinor); /* VersionMinor (2 bytes) */ + Stream_Write_UINT32(s, context->common.clientID); /* ClientId (4 bytes) */ + return rdpdr_seal_send_free_request(context, s); +} + +static UINT rdpdr_process_client_announce_reply(pf_channel_server_context* rdpdr, wStream* s) +{ + const UINT16 component = RDPDR_CTYP_CORE; + const UINT16 packetid = PAKID_CORE_CLIENTID_CONFIRM; + UINT16 versionMajor = 0; + UINT16 versionMinor = 0; + UINT32 clientID = 0; + + WINPR_ASSERT(rdpdr); + WINPR_ASSERT(s); + + if (!rdpdr_process_server_header(TRUE, rdpdr->log, s, component, packetid, 8)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, versionMajor); + Stream_Read_UINT16(s, versionMinor); + + if (!rdpdr_check_version(TRUE, rdpdr->log, versionMajor, versionMinor, component, packetid)) + return ERROR_INVALID_DATA; + + if ((rdpdr->common.versionMajor != versionMajor) || + (rdpdr->common.versionMinor != versionMinor)) + { + SERVER_RX_LOG( + rdpdr->log, WLOG_WARN, + "[%s | %s] downgrading version from %" PRIu16 ".%" PRIu16 " to %" PRIu16 ".%" PRIu16, + rdpdr_component_string(component), rdpdr_packetid_string(packetid), + rdpdr->common.versionMajor, rdpdr->common.versionMinor, versionMajor, versionMinor); + rdpdr->common.versionMajor = versionMajor; + rdpdr->common.versionMinor = versionMinor; + } + Stream_Read_UINT32(s, clientID); + if (rdpdr->common.clientID != clientID) + { + SERVER_RX_LOG(rdpdr->log, WLOG_WARN, + "[%s | %s] changing clientID 0x%08" PRIu32 " to 0x%08" PRIu32, + rdpdr_component_string(component), rdpdr_packetid_string(packetid), + rdpdr->common.clientID, clientID); + rdpdr->common.clientID = clientID; + } + + return CHANNEL_RC_OK; +} + +static UINT rdpdr_send_client_announce_reply(pClientContext* pc, pf_channel_client_context* rdpdr) +{ + wStream* s = + rdpdr_client_get_send_buffer(rdpdr, RDPDR_CTYP_CORE, PAKID_CORE_CLIENTID_CONFIRM, 8); + if (!s) + return CHANNEL_RC_NO_MEMORY; + + Stream_Write_UINT16(s, rdpdr->common.versionMajor); + Stream_Write_UINT16(s, rdpdr->common.versionMinor); + Stream_Write_UINT32(s, rdpdr->common.clientID); + return rdpdr_client_send(rdpdr->log, pc, s); +} + +static UINT rdpdr_process_client_name_request(pf_channel_server_context* rdpdr, wStream* s, + pClientContext* pc) +{ + UINT32 unicodeFlag = 0; + UINT32 codePage = 0; + + WINPR_ASSERT(rdpdr); + WINPR_ASSERT(s); + WINPR_ASSERT(pc); + + if (!rdpdr_process_server_header(TRUE, rdpdr->log, s, RDPDR_CTYP_CORE, PAKID_CORE_CLIENT_NAME, + 12)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, unicodeFlag); + rdpdr->common.computerNameUnicode = (unicodeFlag & 1); + + Stream_Read_UINT32(s, codePage); + WINPR_UNUSED(codePage); /* Field is ignored */ + Stream_Read_UINT32(s, rdpdr->common.computerNameLen); + if (!Stream_CheckAndLogRequiredLengthSrv(rdpdr->log, s, rdpdr->common.computerNameLen)) + { + SERVER_RX_LOG( + rdpdr->log, WLOG_WARN, "[%s | %s]: missing data, got %" PRIu32 ", expected %" PRIu32, + rdpdr_component_string(RDPDR_CTYP_CORE), rdpdr_packetid_string(PAKID_CORE_CLIENT_NAME), + Stream_GetRemainingLength(s), rdpdr->common.computerNameLen); + return ERROR_INVALID_DATA; + } + void* tmp = realloc(rdpdr->common.computerName.v, rdpdr->common.computerNameLen); + if (!tmp) + return CHANNEL_RC_NO_MEMORY; + rdpdr->common.computerName.v = tmp; + + Stream_Read(s, rdpdr->common.computerName.v, rdpdr->common.computerNameLen); + + pc->computerNameLen = rdpdr->common.computerNameLen; + pc->computerNameUnicode = rdpdr->common.computerNameUnicode; + tmp = realloc(pc->computerName.v, pc->computerNameLen); + if (!tmp) + return CHANNEL_RC_NO_MEMORY; + pc->computerName.v = tmp; + memcpy(pc->computerName.v, rdpdr->common.computerName.v, pc->computerNameLen); + + return CHANNEL_RC_OK; +} + +static UINT rdpdr_send_client_name_request(pClientContext* pc, pf_channel_client_context* rdpdr) +{ + wStream* s = NULL; + + WINPR_ASSERT(rdpdr); + WINPR_ASSERT(pc); + + { + void* tmp = realloc(rdpdr->common.computerName.v, pc->computerNameLen); + if (!tmp) + return CHANNEL_RC_NO_MEMORY; + rdpdr->common.computerName.v = tmp; + rdpdr->common.computerNameLen = pc->computerNameLen; + rdpdr->common.computerNameUnicode = pc->computerNameUnicode; + memcpy(rdpdr->common.computerName.v, pc->computerName.v, pc->computerNameLen); + } + s = rdpdr_client_get_send_buffer(rdpdr, RDPDR_CTYP_CORE, PAKID_CORE_CLIENT_NAME, + 12U + rdpdr->common.computerNameLen); + if (!s) + return CHANNEL_RC_NO_MEMORY; + + Stream_Write_UINT32(s, rdpdr->common.computerNameUnicode + ? 1 + : 0); /* unicodeFlag, 0 for ASCII and 1 for Unicode */ + Stream_Write_UINT32(s, 0); /* codePage, must be set to zero */ + Stream_Write_UINT32(s, rdpdr->common.computerNameLen); + Stream_Write(s, rdpdr->common.computerName.v, rdpdr->common.computerNameLen); + return rdpdr_client_send(rdpdr->log, pc, s); +} + +#define rdpdr_ignore_capset(srv, log, s, header) \ + rdpdr_ignore_capset_((srv), (log), (s), header, __func__) +static UINT rdpdr_ignore_capset_(BOOL srv, wLog* log, wStream* s, + const RDPDR_CAPABILITY_HEADER* header, const char* fkt) +{ + WINPR_ASSERT(s); + WINPR_ASSERT(header); + + Stream_Seek(s, header->CapabilityLength); + return CHANNEL_RC_OK; +} + +static UINT rdpdr_client_process_general_capset(pf_channel_client_context* rdpdr, wStream* s, + const RDPDR_CAPABILITY_HEADER* header) +{ + WINPR_UNUSED(rdpdr); + return rdpdr_ignore_capset(FALSE, rdpdr->log, s, header); +} + +static UINT rdpdr_process_printer_capset(pf_channel_client_context* rdpdr, wStream* s, + const RDPDR_CAPABILITY_HEADER* header) +{ + WINPR_UNUSED(rdpdr); + return rdpdr_ignore_capset(FALSE, rdpdr->log, s, header); +} + +static UINT rdpdr_process_port_capset(pf_channel_client_context* rdpdr, wStream* s, + const RDPDR_CAPABILITY_HEADER* header) +{ + WINPR_UNUSED(rdpdr); + return rdpdr_ignore_capset(FALSE, rdpdr->log, s, header); +} + +static UINT rdpdr_process_drive_capset(pf_channel_client_context* rdpdr, wStream* s, + const RDPDR_CAPABILITY_HEADER* header) +{ + WINPR_UNUSED(rdpdr); + return rdpdr_ignore_capset(FALSE, rdpdr->log, s, header); +} + +static UINT rdpdr_process_smartcard_capset(pf_channel_client_context* rdpdr, wStream* s, + const RDPDR_CAPABILITY_HEADER* header) +{ + WINPR_UNUSED(rdpdr); + return rdpdr_ignore_capset(FALSE, rdpdr->log, s, header); +} + +static UINT rdpdr_process_server_core_capability_request(pf_channel_client_context* rdpdr, + wStream* s) +{ + UINT status = CHANNEL_RC_OK; + UINT16 numCapabilities = 0; + + WINPR_ASSERT(rdpdr); + + if (!rdpdr_process_server_header(FALSE, rdpdr->log, s, RDPDR_CTYP_CORE, + PAKID_CORE_SERVER_CAPABILITY, 4)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, numCapabilities); + Stream_Seek(s, 2); /* pad (2 bytes) */ + + for (UINT16 i = 0; i < numCapabilities; i++) + { + RDPDR_CAPABILITY_HEADER header = { 0 }; + UINT error = rdpdr_read_capset_header(rdpdr->log, s, &header); + if (error != CHANNEL_RC_OK) + return error; + + if (header.CapabilityType < ARRAYSIZE(rdpdr->common.capabilityVersions)) + { + if (rdpdr->common.capabilityVersions[header.CapabilityType] > header.Version) + rdpdr->common.capabilityVersions[header.CapabilityType] = header.Version; + + WLog_Print(rdpdr->log, WLOG_TRACE, + "capability %s got version %" PRIu32 ", will use version %" PRIu32, + rdpdr_cap_type_string(header.CapabilityType), header.Version, + rdpdr->common.capabilityVersions[header.CapabilityType]); + } + + switch (header.CapabilityType) + { + case CAP_GENERAL_TYPE: + status = rdpdr_client_process_general_capset(rdpdr, s, &header); + break; + + case CAP_PRINTER_TYPE: + status = rdpdr_process_printer_capset(rdpdr, s, &header); + break; + + case CAP_PORT_TYPE: + status = rdpdr_process_port_capset(rdpdr, s, &header); + break; + + case CAP_DRIVE_TYPE: + status = rdpdr_process_drive_capset(rdpdr, s, &header); + break; + + case CAP_SMARTCARD_TYPE: + status = rdpdr_process_smartcard_capset(rdpdr, s, &header); + break; + + default: + WLog_Print(rdpdr->log, WLOG_WARN, + "unknown capability 0x%04" PRIx16 ", length %" PRIu16 + ", version %" PRIu32, + header.CapabilityType, header.CapabilityLength, header.Version); + Stream_Seek(s, header.CapabilityLength); + break; + } + + if (status != CHANNEL_RC_OK) + return status; + } + + return CHANNEL_RC_OK; +} + +static BOOL rdpdr_write_general_capset(wLog* log, pf_channel_common_context* rdpdr, wStream* s) +{ + WINPR_ASSERT(rdpdr); + WINPR_ASSERT(s); + + const RDPDR_CAPABILITY_HEADER header = { CAP_GENERAL_TYPE, 44, + rdpdr->capabilityVersions[CAP_GENERAL_TYPE] }; + if (rdpdr_write_capset_header(log, s, &header) != CHANNEL_RC_OK) + return FALSE; + Stream_Write_UINT32(s, 0); /* osType, ignored on receipt */ + Stream_Write_UINT32(s, 0); /* osVersion, should be ignored */ + Stream_Write_UINT16(s, rdpdr->versionMajor); /* protocolMajorVersion, must be set to 1 */ + Stream_Write_UINT16(s, rdpdr->versionMinor); /* protocolMinorVersion */ + Stream_Write_UINT32(s, 0x0000FFFF); /* ioCode1 */ + Stream_Write_UINT32(s, 0); /* ioCode2, must be set to zero, reserved for future use */ + Stream_Write_UINT32(s, RDPDR_DEVICE_REMOVE_PDUS | RDPDR_CLIENT_DISPLAY_NAME_PDU | + RDPDR_USER_LOGGEDON_PDU); /* extendedPDU */ + Stream_Write_UINT32(s, ENABLE_ASYNCIO); /* extraFlags1 */ + Stream_Write_UINT32(s, 0); /* extraFlags2, must be set to zero, reserved for future use */ + Stream_Write_UINT32(s, rdpdr->SpecialDeviceCount); /* SpecialTypeDeviceCap, number of special + devices to be redirected before logon */ + return TRUE; +} + +static BOOL rdpdr_write_printer_capset(wLog* log, pf_channel_common_context* rdpdr, wStream* s) +{ + WINPR_ASSERT(rdpdr); + WINPR_ASSERT(s); + + const RDPDR_CAPABILITY_HEADER header = { CAP_PRINTER_TYPE, 8, + rdpdr->capabilityVersions[CAP_PRINTER_TYPE] }; + if (rdpdr_write_capset_header(log, s, &header) != CHANNEL_RC_OK) + return FALSE; + return TRUE; +} + +static BOOL rdpdr_write_port_capset(wLog* log, pf_channel_common_context* rdpdr, wStream* s) +{ + WINPR_ASSERT(rdpdr); + WINPR_ASSERT(s); + + const RDPDR_CAPABILITY_HEADER header = { CAP_PORT_TYPE, 8, + rdpdr->capabilityVersions[CAP_PORT_TYPE] }; + if (rdpdr_write_capset_header(log, s, &header) != CHANNEL_RC_OK) + return FALSE; + return TRUE; +} + +static BOOL rdpdr_write_drive_capset(wLog* log, pf_channel_common_context* rdpdr, wStream* s) +{ + WINPR_ASSERT(rdpdr); + WINPR_ASSERT(s); + + const RDPDR_CAPABILITY_HEADER header = { CAP_DRIVE_TYPE, 8, + rdpdr->capabilityVersions[CAP_DRIVE_TYPE] }; + if (rdpdr_write_capset_header(log, s, &header) != CHANNEL_RC_OK) + return FALSE; + return TRUE; +} + +static BOOL rdpdr_write_smartcard_capset(wLog* log, pf_channel_common_context* rdpdr, wStream* s) +{ + WINPR_ASSERT(rdpdr); + WINPR_ASSERT(s); + + const RDPDR_CAPABILITY_HEADER header = { CAP_SMARTCARD_TYPE, 8, + rdpdr->capabilityVersions[CAP_SMARTCARD_TYPE] }; + if (rdpdr_write_capset_header(log, s, &header) != CHANNEL_RC_OK) + return FALSE; + return TRUE; +} + +static UINT rdpdr_send_server_capability_request(pf_channel_server_context* rdpdr) +{ + wStream* s = + rdpdr_server_get_send_buffer(rdpdr, RDPDR_CTYP_CORE, PAKID_CORE_SERVER_CAPABILITY, 8); + if (!s) + return CHANNEL_RC_NO_MEMORY; + Stream_Write_UINT16(s, 5); /* numCapabilities */ + Stream_Write_UINT16(s, 0); /* pad */ + if (!rdpdr_write_general_capset(rdpdr->log, &rdpdr->common, s)) + return CHANNEL_RC_NO_MEMORY; + if (!rdpdr_write_printer_capset(rdpdr->log, &rdpdr->common, s)) + return CHANNEL_RC_NO_MEMORY; + if (!rdpdr_write_port_capset(rdpdr->log, &rdpdr->common, s)) + return CHANNEL_RC_NO_MEMORY; + if (!rdpdr_write_drive_capset(rdpdr->log, &rdpdr->common, s)) + return CHANNEL_RC_NO_MEMORY; + if (!rdpdr_write_smartcard_capset(rdpdr->log, &rdpdr->common, s)) + return CHANNEL_RC_NO_MEMORY; + return rdpdr_seal_send_free_request(rdpdr, s); +} + +static UINT rdpdr_process_client_capability_response(pf_channel_server_context* rdpdr, wStream* s) +{ + const UINT16 component = RDPDR_CTYP_CORE; + const UINT16 packetid = PAKID_CORE_CLIENT_CAPABILITY; + UINT status = CHANNEL_RC_OK; + UINT16 numCapabilities = 0; + WINPR_ASSERT(rdpdr); + + if (!rdpdr_process_server_header(TRUE, rdpdr->log, s, component, packetid, 4)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, numCapabilities); + Stream_Seek_UINT16(s); /* padding */ + + for (UINT16 x = 0; x < numCapabilities; x++) + { + RDPDR_CAPABILITY_HEADER header = { 0 }; + UINT error = rdpdr_read_capset_header(rdpdr->log, s, &header); + if (error != CHANNEL_RC_OK) + return error; + if (header.CapabilityType < ARRAYSIZE(rdpdr->common.capabilityVersions)) + { + if (rdpdr->common.capabilityVersions[header.CapabilityType] > header.Version) + rdpdr->common.capabilityVersions[header.CapabilityType] = header.Version; + + WLog_Print(rdpdr->log, WLOG_TRACE, + "capability %s got version %" PRIu32 ", will use version %" PRIu32, + rdpdr_cap_type_string(header.CapabilityType), header.Version, + rdpdr->common.capabilityVersions[header.CapabilityType]); + } + + switch (header.CapabilityType) + { + case CAP_GENERAL_TYPE: + status = rdpdr_ignore_capset(TRUE, rdpdr->log, s, &header); + break; + + case CAP_PRINTER_TYPE: + status = rdpdr_ignore_capset(TRUE, rdpdr->log, s, &header); + break; + + case CAP_PORT_TYPE: + status = rdpdr_ignore_capset(TRUE, rdpdr->log, s, &header); + break; + + case CAP_DRIVE_TYPE: + status = rdpdr_ignore_capset(TRUE, rdpdr->log, s, &header); + break; + + case CAP_SMARTCARD_TYPE: + status = rdpdr_ignore_capset(TRUE, rdpdr->log, s, &header); + break; + + default: + SERVER_RX_LOG(rdpdr->log, WLOG_WARN, + "[%s | %s] invalid capability type 0x%04" PRIx16, + rdpdr_component_string(component), rdpdr_packetid_string(packetid), + header.CapabilityType); + status = ERROR_INVALID_DATA; + break; + } + + if (status != CHANNEL_RC_OK) + break; + } + + return status; +} + +static UINT rdpdr_send_client_capability_response(pClientContext* pc, + pf_channel_client_context* rdpdr) +{ + wStream* s = NULL; + + WINPR_ASSERT(rdpdr); + s = rdpdr_client_get_send_buffer(rdpdr, RDPDR_CTYP_CORE, PAKID_CORE_CLIENT_CAPABILITY, 4); + if (!s) + return CHANNEL_RC_NO_MEMORY; + + Stream_Write_UINT16(s, 5); /* numCapabilities */ + Stream_Write_UINT16(s, 0); /* pad */ + if (!rdpdr_write_general_capset(rdpdr->log, &rdpdr->common, s)) + return CHANNEL_RC_NO_MEMORY; + if (!rdpdr_write_printer_capset(rdpdr->log, &rdpdr->common, s)) + return CHANNEL_RC_NO_MEMORY; + if (!rdpdr_write_port_capset(rdpdr->log, &rdpdr->common, s)) + return CHANNEL_RC_NO_MEMORY; + if (!rdpdr_write_drive_capset(rdpdr->log, &rdpdr->common, s)) + return CHANNEL_RC_NO_MEMORY; + if (!rdpdr_write_smartcard_capset(rdpdr->log, &rdpdr->common, s)) + return CHANNEL_RC_NO_MEMORY; + return rdpdr_client_send(rdpdr->log, pc, s); +} + +static UINT rdpdr_send_server_clientid_confirm(pf_channel_server_context* rdpdr) +{ + wStream* s = NULL; + + s = rdpdr_server_get_send_buffer(rdpdr, RDPDR_CTYP_CORE, PAKID_CORE_CLIENTID_CONFIRM, 8); + if (!s) + return CHANNEL_RC_NO_MEMORY; + Stream_Write_UINT16(s, rdpdr->common.versionMajor); + Stream_Write_UINT16(s, rdpdr->common.versionMinor); + Stream_Write_UINT32(s, rdpdr->common.clientID); + return rdpdr_seal_send_free_request(rdpdr, s); +} + +static UINT rdpdr_process_server_clientid_confirm(pf_channel_client_context* rdpdr, wStream* s) +{ + UINT16 versionMajor = 0; + UINT16 versionMinor = 0; + UINT32 clientID = 0; + + WINPR_ASSERT(rdpdr); + WINPR_ASSERT(s); + + if (!rdpdr_process_server_header(FALSE, rdpdr->log, s, RDPDR_CTYP_CORE, + PAKID_CORE_CLIENTID_CONFIRM, 8)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT16(s, versionMajor); + Stream_Read_UINT16(s, versionMinor); + if (!rdpdr_check_version(FALSE, rdpdr->log, versionMajor, versionMinor, RDPDR_CTYP_CORE, + PAKID_CORE_CLIENTID_CONFIRM)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, clientID); + + if ((versionMajor != rdpdr->common.versionMajor) || + (versionMinor != rdpdr->common.versionMinor)) + { + CLIENT_RX_LOG(rdpdr->log, WLOG_WARN, + "[%s | %s] Version mismatch, sent %" PRIu16 ".%" PRIu16 + ", downgraded to %" PRIu16 ".%" PRIu16, + rdpdr_component_string(RDPDR_CTYP_CORE), + rdpdr_packetid_string(PAKID_CORE_CLIENTID_CONFIRM), + rdpdr->common.versionMajor, rdpdr->common.versionMinor, versionMajor, + versionMinor); + rdpdr->common.versionMajor = versionMajor; + rdpdr->common.versionMinor = versionMinor; + } + + if (clientID != rdpdr->common.clientID) + { + CLIENT_RX_LOG(rdpdr->log, WLOG_WARN, + "[%s | %s] clientID mismatch, sent 0x%08" PRIx32 ", changed to 0x%08" PRIx32, + rdpdr_component_string(RDPDR_CTYP_CORE), + rdpdr_packetid_string(PAKID_CORE_CLIENTID_CONFIRM), rdpdr->common.clientID, + clientID); + rdpdr->common.clientID = clientID; + } + + return CHANNEL_RC_OK; +} + +static BOOL +rdpdr_process_server_capability_request_or_clientid_confirm(pf_channel_client_context* rdpdr, + wStream* s) +{ + const UINT32 mask = STATE_CLIENT_EXPECT_SERVER_CLIENT_ID_CONFIRM | + STATE_CLIENT_EXPECT_SERVER_CORE_CAPABILITY_REQUEST; + const UINT16 rcomponent = RDPDR_CTYP_CORE; + UINT16 component = 0; + UINT16 packetid = 0; + + WINPR_ASSERT(rdpdr); + WINPR_ASSERT(s); + + if ((rdpdr->flags & mask) == mask) + { + CLIENT_RX_LOG(rdpdr->log, WLOG_WARN, "already past this state, abort!"); + return FALSE; + } + + if (!Stream_CheckAndLogRequiredLengthClient(rdpdr->log, s, 4)) + return FALSE; + + Stream_Read_UINT16(s, component); + if (rcomponent != component) + { + CLIENT_RX_LOG(rdpdr->log, WLOG_WARN, "got component %s, expected %s", + rdpdr_component_string(component), rdpdr_component_string(rcomponent)); + return FALSE; + } + Stream_Read_UINT16(s, packetid); + Stream_Rewind(s, 4); + + switch (packetid) + { + case PAKID_CORE_SERVER_CAPABILITY: + if (rdpdr->flags & STATE_CLIENT_EXPECT_SERVER_CORE_CAPABILITY_REQUEST) + { + CLIENT_RX_LOG(rdpdr->log, WLOG_WARN, "got duplicate packetid %s", + rdpdr_packetid_string(packetid)); + return FALSE; + } + rdpdr->flags |= STATE_CLIENT_EXPECT_SERVER_CORE_CAPABILITY_REQUEST; + return rdpdr_process_server_core_capability_request(rdpdr, s) == CHANNEL_RC_OK; + case PAKID_CORE_CLIENTID_CONFIRM: + default: + if (rdpdr->flags & STATE_CLIENT_EXPECT_SERVER_CLIENT_ID_CONFIRM) + { + CLIENT_RX_LOG(rdpdr->log, WLOG_WARN, "got duplicate packetid %s", + rdpdr_packetid_string(packetid)); + return FALSE; + } + rdpdr->flags |= STATE_CLIENT_EXPECT_SERVER_CLIENT_ID_CONFIRM; + return rdpdr_process_server_clientid_confirm(rdpdr, s) == CHANNEL_RC_OK; + } +} + +#if defined(WITH_PROXY_EMULATE_SMARTCARD) +static UINT rdpdr_send_emulated_scard_device_list_announce_request(pClientContext* pc, + pf_channel_client_context* rdpdr) +{ + wStream* s = NULL; + + s = rdpdr_client_get_send_buffer(rdpdr, RDPDR_CTYP_CORE, PAKID_CORE_DEVICELIST_ANNOUNCE, 24); + if (!s) + return CHANNEL_RC_NO_MEMORY; + + Stream_Write_UINT32(s, 1); /* deviceCount -> our emulated smartcard only */ + Stream_Write_UINT32(s, RDPDR_DTYP_SMARTCARD); /* deviceType */ + Stream_Write_UINT32( + s, SCARD_DEVICE_ID); /* deviceID -> reserve highest value for the emulated smartcard */ + Stream_Write(s, "SCARD\0\0\0", 8); + Stream_Write_UINT32(s, 6); + Stream_Write(s, "SCARD\0", 6); + + return rdpdr_client_send(rdpdr->log, pc, s); +} + +static UINT rdpdr_send_emulated_scard_device_remove(pClientContext* pc, + pf_channel_client_context* rdpdr) +{ + wStream* s = NULL; + + s = rdpdr_client_get_send_buffer(rdpdr, RDPDR_CTYP_CORE, PAKID_CORE_DEVICELIST_REMOVE, 24); + if (!s) + return CHANNEL_RC_NO_MEMORY; + + Stream_Write_UINT32(s, 1); /* deviceCount -> our emulated smartcard only */ + Stream_Write_UINT32( + s, SCARD_DEVICE_ID); /* deviceID -> reserve highest value for the emulated smartcard */ + + return rdpdr_client_send(rdpdr->log, pc, s); +} + +static UINT rdpdr_process_server_device_announce_response(pf_channel_client_context* rdpdr, + wStream* s) +{ + const UINT16 component = RDPDR_CTYP_CORE; + const UINT16 packetid = PAKID_CORE_DEVICE_REPLY; + UINT32 deviceID = 0; + UINT32 resultCode = 0; + + WINPR_ASSERT(rdpdr); + WINPR_ASSERT(s); + + if (!rdpdr_process_server_header(TRUE, rdpdr->log, s, component, packetid, 8)) + return ERROR_INVALID_DATA; + + Stream_Read_UINT32(s, deviceID); + Stream_Read_UINT32(s, resultCode); + + if (deviceID != SCARD_DEVICE_ID) + { + CLIENT_RX_LOG(rdpdr->log, WLOG_WARN, + "[%s | %s] deviceID mismatch, sent 0x%08" PRIx32 ", changed to 0x%08" PRIx32, + rdpdr_component_string(component), rdpdr_packetid_string(packetid), + SCARD_DEVICE_ID, deviceID); + } + else if (resultCode != 0) + { + CLIENT_RX_LOG(rdpdr->log, WLOG_WARN, + "[%s | %s] deviceID 0x%08" PRIx32 " resultCode=0x%08" PRIx32, + rdpdr_component_string(component), rdpdr_packetid_string(packetid), deviceID, + resultCode); + } + else + CLIENT_RX_LOG(rdpdr->log, WLOG_DEBUG, + "[%s | %s] deviceID 0x%08" PRIx32 " resultCode=0x%08" PRIx32 + " -> emulated smartcard redirected!", + rdpdr_component_string(component), rdpdr_packetid_string(packetid), deviceID, + resultCode); + + return CHANNEL_RC_OK; +} +#endif + +static BOOL pf_channel_rdpdr_rewrite_device_list_to(wStream* s, UINT32 fromVersion, + UINT32 toVersion) +{ + BOOL rc = FALSE; + if (fromVersion == toVersion) + return TRUE; + + const size_t cap = Stream_GetRemainingLength(s); + wStream* clone = Stream_New(NULL, cap); + if (!clone) + goto fail; + const size_t pos = Stream_GetPosition(s); + Stream_Copy(s, clone, cap); + Stream_SealLength(clone); + + Stream_SetPosition(clone, 0); + Stream_SetPosition(s, pos); + + /* Skip device count */ + if (!Stream_SafeSeek(s, 4)) + goto fail; + + UINT32 count = 0; + if (Stream_GetRemainingLength(clone) < 4) + goto fail; + Stream_Read_UINT32(clone, count); + + for (UINT32 x = 0; x < count; x++) + { + RdpdrDevice device = { 0 }; + const size_t charCount = ARRAYSIZE(device.PreferredDosName); + if (Stream_GetRemainingLength(clone) < 20) + goto fail; + + Stream_Read_UINT32(clone, device.DeviceType); /* DeviceType (4 bytes) */ + Stream_Read_UINT32(clone, device.DeviceId); /* DeviceId (4 bytes) */ + Stream_Read(clone, device.PreferredDosName, charCount); /* PreferredDosName (8 bytes) */ + Stream_Read_UINT32(clone, device.DeviceDataLength); /* DeviceDataLength (4 bytes) */ + device.DeviceData = Stream_Pointer(clone); + if (!Stream_SafeSeek(clone, device.DeviceDataLength)) + goto fail; + + if (!Stream_EnsureRemainingCapacity(s, 20)) + goto fail; + Stream_Write_UINT32(s, device.DeviceType); + Stream_Write_UINT32(s, device.DeviceId); + Stream_Write(s, device.PreferredDosName, charCount); + + if (device.DeviceType == RDPDR_DTYP_FILESYSTEM) + { + if (toVersion == DRIVE_CAPABILITY_VERSION_01) + Stream_Write_UINT32(s, 0); /* No unicode name */ + else + { + const size_t datalen = charCount * sizeof(WCHAR); + if (!Stream_EnsureRemainingCapacity(s, datalen + sizeof(UINT32))) + goto fail; + Stream_Write_UINT32(s, datalen); + + const SSIZE_T rcw = Stream_Write_UTF16_String_From_UTF8( + s, charCount, device.PreferredDosName, charCount - 1, TRUE); + if (rcw < 0) + goto fail; + } + } + else + { + Stream_Write_UINT32(s, device.DeviceDataLength); + if (!Stream_EnsureRemainingCapacity(s, device.DeviceDataLength)) + goto fail; + Stream_Write(s, device.DeviceData, device.DeviceDataLength); + } + } + + Stream_SealLength(s); + rc = TRUE; + +fail: + Stream_Free(clone, TRUE); + return rc; +} + +static BOOL pf_channel_rdpdr_rewrite_device_list(pf_channel_client_context* rdpdr, + pServerContext* ps, wStream* s, BOOL toServer) +{ + WINPR_ASSERT(rdpdr); + WINPR_ASSERT(ps); + + const size_t pos = Stream_GetPosition(s); + UINT16 component = 0; + UINT16 packetid = 0; + Stream_SetPosition(s, 0); + + if (!Stream_CheckAndLogRequiredLengthWLog(rdpdr->log, s, 4)) + return FALSE; + + Stream_Read_UINT16(s, component); + Stream_Read_UINT16(s, packetid); + if ((component != RDPDR_CTYP_CORE) || (packetid != PAKID_CORE_DEVICELIST_ANNOUNCE)) + { + Stream_SetPosition(s, pos); + return TRUE; + } + + const pf_channel_server_context* srv = + HashTable_GetItemValue(ps->interceptContextMap, RDPDR_SVC_CHANNEL_NAME); + if (!srv) + { + WLog_Print(rdpdr->log, WLOG_ERROR, "No channel %s in intercep map", RDPDR_SVC_CHANNEL_NAME); + return FALSE; + } + + UINT32 from = srv->common.capabilityVersions[CAP_DRIVE_TYPE]; + UINT32 to = rdpdr->common.capabilityVersions[CAP_DRIVE_TYPE]; + if (toServer) + { + from = rdpdr->common.capabilityVersions[CAP_DRIVE_TYPE]; + to = srv->common.capabilityVersions[CAP_DRIVE_TYPE]; + } + if (!pf_channel_rdpdr_rewrite_device_list_to(s, from, to)) + return FALSE; + + Stream_SetPosition(s, pos); + return TRUE; +} + +static BOOL pf_channel_rdpdr_client_send_to_server(pf_channel_client_context* rdpdr, + pServerContext* ps, wStream* s) +{ + WINPR_ASSERT(rdpdr); + if (ps) + { + UINT16 server_channel_id = WTSChannelGetId(ps->context.peer, RDPDR_SVC_CHANNEL_NAME); + + /* Ignore messages for channels that can not be mapped. + * The client might not have enabled support for this specific channel, + * so just drop the message. */ + if (server_channel_id == 0) + return TRUE; + + if (!pf_channel_rdpdr_rewrite_device_list(rdpdr, ps, s, TRUE)) + return FALSE; + size_t len = Stream_Length(s); + Stream_SetPosition(s, len); + rdpdr_dump_send_packet(rdpdr->log, WLOG_TRACE, s, proxy_client_tx); + WINPR_ASSERT(ps->context.peer); + WINPR_ASSERT(ps->context.peer->SendChannelData); + return ps->context.peer->SendChannelData(ps->context.peer, server_channel_id, + Stream_Buffer(s), len); + } + return TRUE; +} + +static BOOL pf_channel_send_client_queue(pClientContext* pc, pf_channel_client_context* rdpdr); + +#if defined(WITH_PROXY_EMULATE_SMARTCARD) +static BOOL rdpdr_process_server_loggedon_request(pServerContext* ps, pClientContext* pc, + pf_channel_client_context* rdpdr, wStream* s, + UINT16 component, UINT16 packetid) +{ + WINPR_ASSERT(rdpdr); + WLog_Print(rdpdr->log, WLOG_DEBUG, "[%s | %s]", rdpdr_component_string(component), + rdpdr_packetid_string(packetid)); + if (rdpdr_send_emulated_scard_device_remove(pc, rdpdr) != CHANNEL_RC_OK) + return FALSE; + if (rdpdr_send_emulated_scard_device_list_announce_request(pc, rdpdr) != CHANNEL_RC_OK) + return FALSE; + return pf_channel_rdpdr_client_send_to_server(rdpdr, ps, s); +} + +static BOOL filter_smartcard_io_requests(pf_channel_client_context* rdpdr, wStream* s, + UINT16* pPacketid) +{ + BOOL rc = FALSE; + UINT16 component = 0; + UINT16 packetid = 0; + UINT32 deviceID = 0; + size_t pos = 0; + + WINPR_ASSERT(rdpdr); + WINPR_ASSERT(pPacketid); + + if (!Stream_CheckAndLogRequiredLengthWLog(rdpdr->log, s, 4)) + return FALSE; + + pos = Stream_GetPosition(s); + Stream_Read_UINT16(s, component); + Stream_Read_UINT16(s, packetid); + + if (Stream_GetRemainingLength(s) >= 4) + Stream_Read_UINT32(s, deviceID); + + WLog_Print(rdpdr->log, WLOG_DEBUG, "got: [%s | %s]: [0x%08" PRIx32 "]", + rdpdr_component_string(component), rdpdr_packetid_string(packetid), deviceID); + + if (component != RDPDR_CTYP_CORE) + goto fail; + + switch (packetid) + { + case PAKID_CORE_SERVER_ANNOUNCE: + case PAKID_CORE_CLIENTID_CONFIRM: + case PAKID_CORE_CLIENT_NAME: + case PAKID_CORE_DEVICELIST_ANNOUNCE: + case PAKID_CORE_DEVICELIST_REMOVE: + case PAKID_CORE_SERVER_CAPABILITY: + case PAKID_CORE_CLIENT_CAPABILITY: + WLog_Print(rdpdr->log, WLOG_WARN, "Filtering client -> server message [%s | %s]", + rdpdr_component_string(component), rdpdr_packetid_string(packetid)); + *pPacketid = packetid; + break; + case PAKID_CORE_USER_LOGGEDON: + *pPacketid = packetid; + break; + case PAKID_CORE_DEVICE_REPLY: + case PAKID_CORE_DEVICE_IOREQUEST: + if (deviceID != SCARD_DEVICE_ID) + goto fail; + *pPacketid = packetid; + break; + default: + if (deviceID != SCARD_DEVICE_ID) + goto fail; + WLog_Print(rdpdr->log, WLOG_WARN, + "Got [%s | %s] for deviceID 0x%08" PRIx32 ", TODO: Not handled!", + rdpdr_component_string(component), rdpdr_packetid_string(packetid), + deviceID); + goto fail; + } + + rc = TRUE; + +fail: + Stream_SetPosition(s, pos); + return rc; +} +#endif + +BOOL pf_channel_send_client_queue(pClientContext* pc, pf_channel_client_context* rdpdr) +{ + UINT16 channelId = 0; + + WINPR_ASSERT(pc); + WINPR_ASSERT(rdpdr); + + if (rdpdr->state != STATE_CLIENT_CHANNEL_RUNNING) + return FALSE; + channelId = freerdp_channels_get_id_by_name(pc->context.instance, RDPDR_SVC_CHANNEL_NAME); + if ((channelId == 0) || (channelId == UINT16_MAX)) + return TRUE; + + Queue_Lock(rdpdr->queue); + while (Queue_Count(rdpdr->queue) > 0) + { + wStream* s = Queue_Dequeue(rdpdr->queue); + if (!s) + continue; + + size_t len = Stream_Length(s); + Stream_SetPosition(s, len); + + rdpdr_dump_send_packet(rdpdr->log, WLOG_TRACE, s, proxy_server_tx " (queue) "); + WINPR_ASSERT(pc->context.instance->SendChannelData); + if (!pc->context.instance->SendChannelData(pc->context.instance, channelId, + Stream_Buffer(s), len)) + { + CLIENT_TX_LOG(rdpdr->log, WLOG_ERROR, "xxxxxx TODO: Failed to send data!"); + } + Stream_Free(s, TRUE); + } + Queue_Unlock(rdpdr->queue); + return TRUE; +} + +static BOOL rdpdr_handle_server_announce_request(pClientContext* pc, + pf_channel_client_context* rdpdr, wStream* s) +{ + WINPR_ASSERT(pc); + WINPR_ASSERT(rdpdr); + WINPR_ASSERT(s); + + if (rdpdr_process_server_announce_request(rdpdr, s) != CHANNEL_RC_OK) + return FALSE; + if (rdpdr_send_client_announce_reply(pc, rdpdr) != CHANNEL_RC_OK) + return FALSE; + if (rdpdr_send_client_name_request(pc, rdpdr) != CHANNEL_RC_OK) + return FALSE; + rdpdr->state = STATE_CLIENT_EXPECT_SERVER_CORE_CAPABILITY_REQUEST; + return TRUE; +} + +BOOL pf_channel_rdpdr_client_handle(pClientContext* pc, UINT16 channelId, const char* channel_name, + const BYTE* xdata, size_t xsize, UINT32 flags, size_t totalSize) +{ + pf_channel_client_context* rdpdr = NULL; + pServerContext* ps = NULL; + wStream* s = NULL; +#if defined(WITH_PROXY_EMULATE_SMARTCARD) + UINT16 packetid = 0; +#endif + + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->pdata); + WINPR_ASSERT(pc->interceptContextMap); + WINPR_ASSERT(channel_name); + WINPR_ASSERT(xdata); + + ps = pc->pdata->ps; + + rdpdr = HashTable_GetItemValue(pc->interceptContextMap, channel_name); + if (!rdpdr) + { + CLIENT_RX_LOG(WLog_Get(RTAG), WLOG_ERROR, + "Channel %s [0x%04" PRIx16 "] missing context in interceptContextMap", + channel_name, channelId); + return FALSE; + } + s = rdpdr->common.buffer; + if (flags & CHANNEL_FLAG_FIRST) + Stream_SetPosition(s, 0); + if (!Stream_EnsureRemainingCapacity(s, xsize)) + { + CLIENT_RX_LOG(rdpdr->log, WLOG_ERROR, + "Channel %s [0x%04" PRIx16 "] not enough memory [need %" PRIuz "]", + channel_name, channelId, xsize); + return FALSE; + } + Stream_Write(s, xdata, xsize); + if ((flags & CHANNEL_FLAG_LAST) == 0) + return TRUE; + + Stream_SealLength(s); + Stream_SetPosition(s, 0); + if (Stream_Length(s) != totalSize) + { + CLIENT_RX_LOG(rdpdr->log, WLOG_WARN, + "Received invalid %s channel data (server -> proxy), expected %" PRIuz + "bytes, got %" PRIuz, + channel_name, totalSize, Stream_Length(s)); + return FALSE; + } + + rdpdr_dump_received_packet(rdpdr->log, WLOG_TRACE, s, proxy_server_rx); + switch (rdpdr->state) + { + case STATE_CLIENT_EXPECT_SERVER_ANNOUNCE_REQUEST: + if (!rdpdr_handle_server_announce_request(pc, rdpdr, s)) + return FALSE; + break; + case STATE_CLIENT_EXPECT_SERVER_CORE_CAPABILITY_REQUEST: + if (!rdpdr_process_server_capability_request_or_clientid_confirm(rdpdr, s)) + return FALSE; + rdpdr->state = STATE_CLIENT_EXPECT_SERVER_CLIENT_ID_CONFIRM; + break; + case STATE_CLIENT_EXPECT_SERVER_CLIENT_ID_CONFIRM: + if (!rdpdr_process_server_capability_request_or_clientid_confirm(rdpdr, s)) + return FALSE; + if (rdpdr_send_client_capability_response(pc, rdpdr) != CHANNEL_RC_OK) + return FALSE; +#if defined(WITH_PROXY_EMULATE_SMARTCARD) + if (pf_channel_smartcard_client_emulate(pc)) + { + if (rdpdr_send_emulated_scard_device_list_announce_request(pc, rdpdr) != + CHANNEL_RC_OK) + return FALSE; + rdpdr->state = STATE_CLIENT_CHANNEL_RUNNING; + } + else +#endif + { + rdpdr->state = STATE_CLIENT_CHANNEL_RUNNING; + pf_channel_send_client_queue(pc, rdpdr); + } + + break; + case STATE_CLIENT_CHANNEL_RUNNING: +#if defined(WITH_PROXY_EMULATE_SMARTCARD) + if (!pf_channel_smartcard_client_emulate(pc) || + !filter_smartcard_io_requests(rdpdr, s, &packetid)) + return pf_channel_rdpdr_client_send_to_server(rdpdr, ps, s); + else + { + switch (packetid) + { + case PAKID_CORE_USER_LOGGEDON: + return rdpdr_process_server_loggedon_request(ps, pc, rdpdr, s, + RDPDR_CTYP_CORE, packetid); + case PAKID_CORE_DEVICE_IOREQUEST: + { + wStream* out = rdpdr_client_get_send_buffer( + rdpdr, RDPDR_CTYP_CORE, PAKID_CORE_DEVICE_IOCOMPLETION, 0); + WINPR_ASSERT(out); + + if (!rdpdr_process_server_header(FALSE, rdpdr->log, s, RDPDR_CTYP_CORE, + PAKID_CORE_DEVICE_IOREQUEST, 20)) + return FALSE; + + if (!pf_channel_smartcard_client_handle(rdpdr->log, pc, s, out, + rdpdr_client_send)) + return FALSE; + } + break; + case PAKID_CORE_SERVER_ANNOUNCE: + pf_channel_rdpdr_client_reset(pc); + if (!rdpdr_handle_server_announce_request(pc, rdpdr, s)) + return FALSE; + break; + case PAKID_CORE_SERVER_CAPABILITY: + rdpdr->state = STATE_CLIENT_EXPECT_SERVER_CORE_CAPABILITY_REQUEST; + rdpdr->flags = 0; + return pf_channel_rdpdr_client_handle(pc, channelId, channel_name, xdata, + xsize, flags, totalSize); + case PAKID_CORE_DEVICE_REPLY: + break; + default: + CLIENT_RX_LOG( + rdpdr->log, WLOG_ERROR, + "Channel %s [0x%04" PRIx16 + "] we´ve reached an impossible state %s! [%s] aliens invaded!", + channel_name, channelId, rdpdr_client_state_to_string(rdpdr->state), + rdpdr_packetid_string(packetid)); + return FALSE; + } + } + break; +#else + return pf_channel_rdpdr_client_send_to_server(rdpdr, ps, s); +#endif + default: + CLIENT_RX_LOG(rdpdr->log, WLOG_ERROR, + "Channel %s [0x%04" PRIx16 + "] we´ve reached an impossible state %s! aliens invaded!", + channel_name, channelId, rdpdr_client_state_to_string(rdpdr->state)); + return FALSE; + } + + return TRUE; +} + +static void pf_channel_rdpdr_common_context_free(pf_channel_common_context* common) +{ + if (!common) + return; + free(common->computerName.v); + Stream_Free(common->s, TRUE); + Stream_Free(common->buffer, TRUE); +} + +static void pf_channel_rdpdr_client_context_free(InterceptContextMapEntry* base) +{ + pf_channel_client_context* entry = (pf_channel_client_context*)base; + if (!entry) + return; + + pf_channel_rdpdr_common_context_free(&entry->common); + Queue_Free(entry->queue); + free(entry); +} + +static BOOL pf_channel_rdpdr_common_context_new(pf_channel_common_context* common, + void (*fkt)(InterceptContextMapEntry*)) +{ + if (!common) + return FALSE; + common->base.free = fkt; + common->s = Stream_New(NULL, 1024); + if (!common->s) + return FALSE; + common->buffer = Stream_New(NULL, 1024); + if (!common->buffer) + return FALSE; + common->computerNameUnicode = 1; + common->computerName.v = NULL; + common->versionMajor = RDPDR_VERSION_MAJOR; + common->versionMinor = RDPDR_VERSION_MINOR_RDP10X; + common->clientID = SCARD_DEVICE_ID; + + const UINT32 versions[] = { 0, + GENERAL_CAPABILITY_VERSION_02, + PRINT_CAPABILITY_VERSION_01, + PORT_CAPABILITY_VERSION_01, + DRIVE_CAPABILITY_VERSION_02, + SMARTCARD_CAPABILITY_VERSION_01 }; + + memcpy(common->capabilityVersions, versions, sizeof(common->capabilityVersions)); + return TRUE; +} + +static BOOL pf_channel_rdpdr_client_pass_message(pServerContext* ps, pClientContext* pc, + UINT16 channelId, const char* channel_name, + wStream* s) +{ + pf_channel_client_context* rdpdr = NULL; + + WINPR_ASSERT(ps); + WINPR_ASSERT(pc); + + rdpdr = HashTable_GetItemValue(pc->interceptContextMap, channel_name); + if (!rdpdr) + return TRUE; /* Ignore data for channels not available on proxy -> server connection */ + WINPR_ASSERT(rdpdr->queue); + + if (!pf_channel_rdpdr_rewrite_device_list(rdpdr, ps, s, FALSE)) + return FALSE; + if (!Queue_Enqueue(rdpdr->queue, s)) + return FALSE; + pf_channel_send_client_queue(pc, rdpdr); + return TRUE; +} + +#if defined(WITH_PROXY_EMULATE_SMARTCARD) +static BOOL filter_smartcard_device_list_remove(pf_channel_server_context* rdpdr, wStream* s) +{ + size_t pos = 0; + UINT32 count = 0; + + WINPR_ASSERT(rdpdr); + if (!Stream_CheckAndLogRequiredLengthWLog(rdpdr->log, s, sizeof(UINT32))) + return TRUE; + pos = Stream_GetPosition(s); + Stream_Read_UINT32(s, count); + + if (count == 0) + return TRUE; + + if (!Stream_CheckAndLogRequiredLengthOfSizeWLog(rdpdr->log, s, count, sizeof(UINT32))) + return TRUE; + + for (UINT32 x = 0; x < count; x++) + { + UINT32 deviceID = 0; + BYTE* dst = Stream_Pointer(s); + Stream_Read_UINT32(s, deviceID); + if (deviceID == SCARD_DEVICE_ID) + { + ArrayList_Remove(rdpdr->blockedDevices, (void*)(size_t)deviceID); + + /* This is the only device, filter it! */ + if (count == 1) + return TRUE; + + /* Remove this device from the list */ + memmove(dst, Stream_ConstPointer(s), (count - x - 1) * sizeof(UINT32)); + + count--; + Stream_SetPosition(s, pos); + Stream_Write_UINT32(s, count); + return FALSE; + } + } + + return FALSE; +} + +static BOOL filter_smartcard_device_io_request(pf_channel_server_context* rdpdr, wStream* s) +{ + UINT32 DeviceID = 0; + WINPR_ASSERT(rdpdr); + WINPR_ASSERT(s); + Stream_Read_UINT32(s, DeviceID); + return ArrayList_Contains(rdpdr->blockedDevices, (void*)(size_t)DeviceID); +} + +static BOOL filter_smartcard_device_list_announce(pf_channel_server_context* rdpdr, wStream* s) +{ + UINT32 count = 0; + + WINPR_ASSERT(rdpdr); + if (!Stream_CheckAndLogRequiredLengthWLog(rdpdr->log, s, sizeof(UINT32))) + return TRUE; + const size_t pos = Stream_GetPosition(s); + Stream_Read_UINT32(s, count); + + if (count == 0) + return TRUE; + + for (UINT32 x = 0; x < count; x++) + { + UINT32 DeviceType = 0; + UINT32 DeviceId = 0; + char PreferredDosName[8]; + UINT32 DeviceDataLength = 0; + BYTE* dst = Stream_Pointer(s); + if (!Stream_CheckAndLogRequiredLengthWLog(rdpdr->log, s, 20)) + return TRUE; + Stream_Read_UINT32(s, DeviceType); + Stream_Read_UINT32(s, DeviceId); + Stream_Read(s, PreferredDosName, ARRAYSIZE(PreferredDosName)); + Stream_Read_UINT32(s, DeviceDataLength); + if (!Stream_SafeSeek(s, DeviceDataLength)) + return TRUE; + if (DeviceType == RDPDR_DTYP_SMARTCARD) + { + ArrayList_Append(rdpdr->blockedDevices, (void*)(size_t)DeviceId); + if (count == 1) + return TRUE; + + WLog_Print(rdpdr->log, WLOG_INFO, "Filtering smartcard device 0x%08" PRIx32 "", + DeviceId); + + memmove(dst, Stream_ConstPointer(s), Stream_GetRemainingLength(s)); + Stream_SetPosition(s, pos); + Stream_Write_UINT32(s, count - 1); + return FALSE; + } + } + + return FALSE; +} + +static BOOL filter_smartcard_device_list_announce_request(pf_channel_server_context* rdpdr, + wStream* s) +{ + BOOL rc = TRUE; + size_t pos = 0; + UINT16 component = 0; + UINT16 packetid = 0; + + WINPR_ASSERT(rdpdr); + if (!Stream_CheckAndLogRequiredLengthWLog(rdpdr->log, s, 8)) + return FALSE; + + pos = Stream_GetPosition(s); + + Stream_Read_UINT16(s, component); + Stream_Read_UINT16(s, packetid); + + if (component != RDPDR_CTYP_CORE) + goto fail; + + switch (packetid) + { + case PAKID_CORE_DEVICELIST_ANNOUNCE: + if (filter_smartcard_device_list_announce(rdpdr, s)) + goto fail; + break; + case PAKID_CORE_DEVICELIST_REMOVE: + if (filter_smartcard_device_list_remove(rdpdr, s)) + goto fail; + break; + case PAKID_CORE_DEVICE_IOREQUEST: + if (filter_smartcard_device_io_request(rdpdr, s)) + goto fail; + break; + + case PAKID_CORE_SERVER_ANNOUNCE: + case PAKID_CORE_CLIENTID_CONFIRM: + case PAKID_CORE_CLIENT_NAME: + case PAKID_CORE_DEVICE_REPLY: + case PAKID_CORE_SERVER_CAPABILITY: + case PAKID_CORE_CLIENT_CAPABILITY: + case PAKID_CORE_USER_LOGGEDON: + WLog_Print(rdpdr->log, WLOG_WARN, "Filtering client -> server message [%s | %s]", + rdpdr_component_string(component), rdpdr_packetid_string(packetid)); + goto fail; + default: + break; + } + + rc = FALSE; +fail: + Stream_SetPosition(s, pos); + return rc; +} +#endif + +static void* stream_copy(const void* obj) +{ + const wStream* src = obj; + wStream* dst = Stream_New(NULL, Stream_Capacity(src)); + if (!dst) + return NULL; + memcpy(Stream_Buffer(dst), Stream_ConstBuffer(src), Stream_Capacity(dst)); + Stream_SetLength(dst, Stream_Length(src)); + Stream_SetPosition(dst, Stream_GetPosition(src)); + return dst; +} + +static void stream_free(void* obj) +{ + wStream* s = obj; + Stream_Free(s, TRUE); +} + +static const char* pf_channel_rdpdr_client_context(void* arg) +{ + pClientContext* pc = arg; + if (!pc) + return "pc=null"; + if (!pc->pdata) + return "pc->pdata=null"; + return pc->pdata->session_id; +} + +BOOL pf_channel_rdpdr_client_new(pClientContext* pc) +{ + wObject* obj = NULL; + pf_channel_client_context* rdpdr = NULL; + + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->interceptContextMap); + + rdpdr = calloc(1, sizeof(pf_channel_client_context)); + if (!rdpdr) + return FALSE; + rdpdr->log = WLog_Get(RTAG); + WINPR_ASSERT(rdpdr->log); + + WLog_SetContext(rdpdr->log, pf_channel_rdpdr_client_context, pc); + if (!pf_channel_rdpdr_common_context_new(&rdpdr->common, pf_channel_rdpdr_client_context_free)) + goto fail; + + rdpdr->maxMajorVersion = RDPDR_VERSION_MAJOR; + rdpdr->maxMinorVersion = RDPDR_VERSION_MINOR_RDP10X; + rdpdr->state = STATE_CLIENT_EXPECT_SERVER_ANNOUNCE_REQUEST; + + rdpdr->queue = Queue_New(TRUE, 0, 0); + if (!rdpdr->queue) + goto fail; + obj = Queue_Object(rdpdr->queue); + WINPR_ASSERT(obj); + obj->fnObjectNew = stream_copy; + obj->fnObjectFree = stream_free; + if (!HashTable_Insert(pc->interceptContextMap, RDPDR_SVC_CHANNEL_NAME, rdpdr)) + goto fail; + // NOLINTNEXTLINE(clang-analyzer-unix.Malloc): HashTable_Insert takes ownership of rdpdr + return TRUE; +fail: + pf_channel_rdpdr_client_context_free(&rdpdr->common.base); + return FALSE; +} + +void pf_channel_rdpdr_client_free(pClientContext* pc) +{ + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->interceptContextMap); + HashTable_Remove(pc->interceptContextMap, RDPDR_SVC_CHANNEL_NAME); +} + +static void pf_channel_rdpdr_server_context_free(InterceptContextMapEntry* base) +{ + pf_channel_server_context* entry = (pf_channel_server_context*)base; + if (!entry) + return; + + WTSVirtualChannelClose(entry->handle); + pf_channel_rdpdr_common_context_free(&entry->common); + ArrayList_Free(entry->blockedDevices); + free(entry); +} + +static const char* pf_channel_rdpdr_server_context(void* arg) +{ + pServerContext* ps = arg; + if (!ps) + return "ps=null"; + if (!ps->pdata) + return "ps->pdata=null"; + return ps->pdata->session_id; +} + +BOOL pf_channel_rdpdr_server_new(pServerContext* ps) +{ + pf_channel_server_context* rdpdr = NULL; + PULONG pSessionId = NULL; + DWORD BytesReturned = 0; + + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->interceptContextMap); + + rdpdr = calloc(1, sizeof(pf_channel_server_context)); + if (!rdpdr) + return FALSE; + rdpdr->log = WLog_Get(RTAG); + WINPR_ASSERT(rdpdr->log); + WLog_SetContext(rdpdr->log, pf_channel_rdpdr_server_context, ps); + + if (!pf_channel_rdpdr_common_context_new(&rdpdr->common, pf_channel_rdpdr_server_context_free)) + goto fail; + rdpdr->state = STATE_SERVER_INITIAL; + + rdpdr->blockedDevices = ArrayList_New(FALSE); + if (!rdpdr->blockedDevices) + goto fail; + + rdpdr->SessionId = WTS_CURRENT_SESSION; + if (WTSQuerySessionInformationA(ps->vcm, WTS_CURRENT_SESSION, WTSSessionId, (LPSTR*)&pSessionId, + &BytesReturned)) + { + rdpdr->SessionId = (DWORD)*pSessionId; + WTSFreeMemory(pSessionId); + } + + rdpdr->handle = WTSVirtualChannelOpenEx(rdpdr->SessionId, RDPDR_SVC_CHANNEL_NAME, 0); + if (rdpdr->handle == 0) + goto fail; + if (!HashTable_Insert(ps->interceptContextMap, RDPDR_SVC_CHANNEL_NAME, rdpdr)) + goto fail; + + // NOLINTNEXTLINE(clang-analyzer-unix.Malloc): HashTable_Insert takes ownership of rdpdr + return TRUE; +fail: + pf_channel_rdpdr_server_context_free(&rdpdr->common.base); + return FALSE; +} + +void pf_channel_rdpdr_server_free(pServerContext* ps) +{ + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->interceptContextMap); + HashTable_Remove(ps->interceptContextMap, RDPDR_SVC_CHANNEL_NAME); +} + +static pf_channel_server_context* get_channel(pServerContext* ps, BOOL send) +{ + pf_channel_server_context* rdpdr = NULL; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->interceptContextMap); + + rdpdr = HashTable_GetItemValue(ps->interceptContextMap, RDPDR_SVC_CHANNEL_NAME); + if (!rdpdr) + { + SERVER_RXTX_LOG(send, WLog_Get(RTAG), WLOG_ERROR, + "Channel %s missing context in interceptContextMap", + RDPDR_SVC_CHANNEL_NAME); + return NULL; + } + + return rdpdr; +} + +BOOL pf_channel_rdpdr_server_handle(pServerContext* ps, UINT16 channelId, const char* channel_name, + const BYTE* xdata, size_t xsize, UINT32 flags, size_t totalSize) +{ + wStream* s = NULL; + pClientContext* pc = NULL; + pf_channel_server_context* rdpdr = get_channel(ps, FALSE); + if (!rdpdr) + return FALSE; + + WINPR_ASSERT(ps->pdata); + pc = ps->pdata->pc; + + s = rdpdr->common.buffer; + + if (flags & CHANNEL_FLAG_FIRST) + Stream_SetPosition(s, 0); + + if (!Stream_EnsureRemainingCapacity(s, xsize)) + return FALSE; + Stream_Write(s, xdata, xsize); + + if ((flags & CHANNEL_FLAG_LAST) == 0) + return TRUE; + + Stream_SealLength(s); + Stream_SetPosition(s, 0); + + if (Stream_Length(s) != totalSize) + { + SERVER_RX_LOG(rdpdr->log, WLOG_WARN, + "Received invalid %s channel data (client -> proxy), expected %" PRIuz + "bytes, got %" PRIuz, + channel_name, totalSize, Stream_Length(s)); + return FALSE; + } + + rdpdr_dump_received_packet(rdpdr->log, WLOG_TRACE, s, proxy_client_rx); + switch (rdpdr->state) + { + case STATE_SERVER_EXPECT_CLIENT_ANNOUNCE_REPLY: + if (rdpdr_process_client_announce_reply(rdpdr, s) != CHANNEL_RC_OK) + return FALSE; + rdpdr->state = STATE_SERVER_EXPECT_CLIENT_NAME_REQUEST; + break; + case STATE_SERVER_EXPECT_CLIENT_NAME_REQUEST: + if (rdpdr_process_client_name_request(rdpdr, s, pc) != CHANNEL_RC_OK) + return FALSE; + if (rdpdr_send_server_capability_request(rdpdr) != CHANNEL_RC_OK) + return FALSE; + if (rdpdr_send_server_clientid_confirm(rdpdr) != CHANNEL_RC_OK) + return FALSE; + rdpdr->state = STATE_SERVER_EXPECT_EXPECT_CLIENT_CAPABILITY_RESPONE; + break; + case STATE_SERVER_EXPECT_EXPECT_CLIENT_CAPABILITY_RESPONE: + if (rdpdr_process_client_capability_response(rdpdr, s) != CHANNEL_RC_OK) + return FALSE; + rdpdr->state = STATE_SERVER_CHANNEL_RUNNING; + break; + case STATE_SERVER_CHANNEL_RUNNING: +#if defined(WITH_PROXY_EMULATE_SMARTCARD) + if (!pf_channel_smartcard_client_emulate(pc) || + !filter_smartcard_device_list_announce_request(rdpdr, s)) + { + if (!pf_channel_rdpdr_client_pass_message(ps, pc, channelId, channel_name, s)) + return FALSE; + } + else + return pf_channel_smartcard_server_handle(ps, s); +#else + if (!pf_channel_rdpdr_client_pass_message(ps, pc, channelId, channel_name, s)) + return FALSE; +#endif + break; + default: + case STATE_SERVER_INITIAL: + SERVER_RX_LOG(rdpdr->log, WLOG_WARN, "Invalid state %s", + rdpdr_server_state_to_string(rdpdr->state)); + return FALSE; + } + + return TRUE; +} + +BOOL pf_channel_rdpdr_server_announce(pServerContext* ps) +{ + pf_channel_server_context* rdpdr = get_channel(ps, TRUE); + if (!rdpdr) + return FALSE; + + WINPR_ASSERT(rdpdr->state == STATE_SERVER_INITIAL); + if (rdpdr_server_send_announce_request(rdpdr) != CHANNEL_RC_OK) + return FALSE; + rdpdr->state = STATE_SERVER_EXPECT_CLIENT_ANNOUNCE_REPLY; + return TRUE; +} + +BOOL pf_channel_rdpdr_client_reset(pClientContext* pc) +{ + pf_channel_client_context* rdpdr = NULL; + + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->pdata); + WINPR_ASSERT(pc->interceptContextMap); + + rdpdr = HashTable_GetItemValue(pc->interceptContextMap, RDPDR_SVC_CHANNEL_NAME); + if (!rdpdr) + return TRUE; + + Queue_Clear(rdpdr->queue); + rdpdr->flags = 0; + rdpdr->state = STATE_CLIENT_EXPECT_SERVER_ANNOUNCE_REQUEST; + + return TRUE; +} + +static PfChannelResult pf_rdpdr_back_data(proxyData* pdata, + const pServerStaticChannelContext* channel, + const BYTE* xdata, size_t xsize, UINT32 flags, + size_t totalSize) +{ + WINPR_ASSERT(pdata); + WINPR_ASSERT(channel); + + if (!pf_channel_rdpdr_client_handle(pdata->pc, channel->back_channel_id, channel->channel_name, + xdata, xsize, flags, totalSize)) + return PF_CHANNEL_RESULT_ERROR; + +#if defined(WITH_PROXY_EMULATE_SMARTCARD) + if (pf_channel_smartcard_client_emulate(pdata->pc)) + return PF_CHANNEL_RESULT_DROP; +#endif + return PF_CHANNEL_RESULT_DROP; +} + +static PfChannelResult pf_rdpdr_front_data(proxyData* pdata, + const pServerStaticChannelContext* channel, + const BYTE* xdata, size_t xsize, UINT32 flags, + size_t totalSize) +{ + WINPR_ASSERT(pdata); + WINPR_ASSERT(channel); + + if (!pf_channel_rdpdr_server_handle(pdata->ps, channel->front_channel_id, channel->channel_name, + xdata, xsize, flags, totalSize)) + return PF_CHANNEL_RESULT_ERROR; + +#if defined(WITH_PROXY_EMULATE_SMARTCARD) + if (pf_channel_smartcard_client_emulate(pdata->pc)) + return PF_CHANNEL_RESULT_DROP; +#endif + return PF_CHANNEL_RESULT_DROP; +} + +BOOL pf_channel_setup_rdpdr(pServerContext* ps, pServerStaticChannelContext* channel) +{ + channel->onBackData = pf_rdpdr_back_data; + channel->onFrontData = pf_rdpdr_front_data; + + if (!pf_channel_rdpdr_server_new(ps)) + return FALSE; + if (!pf_channel_rdpdr_server_announce(ps)) + return FALSE; + + return TRUE; +} diff --git a/server/proxy/channels/pf_channel_rdpdr.h b/server/proxy/channels/pf_channel_rdpdr.h new file mode 100644 index 0000000..dbc2e75 --- /dev/null +++ b/server/proxy/channels/pf_channel_rdpdr.h @@ -0,0 +1,47 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2021 Armin Novak <armin.novak@thincast.com> + * Copyright 2021 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_SERVER_PROXY_RDPDR_H +#define FREERDP_SERVER_PROXY_RDPDR_H + +#include <freerdp/server/proxy/proxy_context.h> + +BOOL pf_channel_setup_rdpdr(pServerContext* ps, pServerStaticChannelContext* channel); + +void pf_channel_rdpdr_client_free(pClientContext* pc); + +BOOL pf_channel_rdpdr_client_new(pClientContext* pc); + +BOOL pf_channel_rdpdr_client_reset(pClientContext* pc); + +BOOL pf_channel_rdpdr_client_handle(pClientContext* pc, UINT16 channelId, const char* channel_name, + const BYTE* xdata, size_t xsize, UINT32 flags, + size_t totalSize); + +void pf_channel_rdpdr_server_free(pServerContext* ps); + +BOOL pf_channel_rdpdr_server_new(pServerContext* ps); + +BOOL pf_channel_rdpdr_server_announce(pServerContext* ps); +BOOL pf_channel_rdpdr_server_handle(pServerContext* ps, UINT16 channelId, const char* channel_name, + const BYTE* xdata, size_t xsize, UINT32 flags, + size_t totalSize); + +#endif /* FREERDP_SERVER_PROXY_RDPDR_H */ diff --git a/server/proxy/channels/pf_channel_smartcard.c b/server/proxy/channels/pf_channel_smartcard.c new file mode 100644 index 0000000..1d2bdfe --- /dev/null +++ b/server/proxy/channels/pf_channel_smartcard.c @@ -0,0 +1,397 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2021 Armin Novak <armin.novak@thincast.com> + * Copyright 2021 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 <winpr/string.h> + +#include <winpr/smartcard.h> +#include <winpr/pool.h> + +#include <freerdp/server/proxy/proxy_log.h> +#include <freerdp/emulate/scard/smartcard_emulate.h> +#include <freerdp/channels/scard.h> +#include <freerdp/channels/rdpdr.h> +#include <freerdp/utils/rdpdr_utils.h> + +#include <freerdp/utils/smartcard_operations.h> +#include <freerdp/utils/smartcard_call.h> + +#include "pf_channel_smartcard.h" +#include "pf_channel_rdpdr.h" + +#define TAG PROXY_TAG("channel.scard") + +#define SCARD_SVC_CHANNEL_NAME "SCARD" + +typedef struct +{ + InterceptContextMapEntry base; + scard_call_context* callctx; + PTP_POOL ThreadPool; + TP_CALLBACK_ENVIRON ThreadPoolEnv; + wArrayList* workObjects; +} pf_channel_client_context; + +typedef struct +{ + SMARTCARD_OPERATION op; + wStream* out; + pClientContext* pc; + wLog* log; + pf_scard_send_fkt_t send_fkt; +} pf_channel_client_queue_element; + +static pf_channel_client_context* scard_get_client_context(pClientContext* pc) +{ + pf_channel_client_context* scard = NULL; + + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->interceptContextMap); + + scard = HashTable_GetItemValue(pc->interceptContextMap, SCARD_SVC_CHANNEL_NAME); + if (!scard) + WLog_WARN(TAG, "[%s] missing in pc->interceptContextMap", SCARD_SVC_CHANNEL_NAME); + return scard; +} + +static BOOL pf_channel_client_write_iostatus(wStream* out, const SMARTCARD_OPERATION* op, + UINT32 ioStatus) +{ + UINT16 component = 0; + UINT16 packetid = 0; + UINT32 dID = 0; + UINT32 cID = 0; + size_t pos = 0; + + WINPR_ASSERT(op); + WINPR_ASSERT(out); + + pos = Stream_GetPosition(out); + Stream_SetPosition(out, 0); + if (!Stream_CheckAndLogRequiredLength(TAG, out, 16)) + return FALSE; + + Stream_Read_UINT16(out, component); + Stream_Read_UINT16(out, packetid); + + Stream_Read_UINT32(out, dID); + Stream_Read_UINT32(out, cID); + + WINPR_ASSERT(component == RDPDR_CTYP_CORE); + WINPR_ASSERT(packetid == PAKID_CORE_DEVICE_IOCOMPLETION); + WINPR_ASSERT(dID == op->deviceID); + WINPR_ASSERT(cID == op->completionID); + + Stream_Write_UINT32(out, ioStatus); + Stream_SetPosition(out, pos); + return TRUE; +} + +struct thread_arg +{ + pf_channel_client_context* scard; + pf_channel_client_queue_element* e; +}; + +static void queue_free(void* obj); +static void* queue_copy(const void* obj); + +static VOID irp_thread(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work) +{ + struct thread_arg* arg = Context; + pf_channel_client_context* scard = arg->scard; + { + UINT32 ioStatus = 0; + LONG rc = smartcard_irp_device_control_call(arg->scard->callctx, arg->e->out, &ioStatus, + &arg->e->op); + if (rc == CHANNEL_RC_OK) + { + if (pf_channel_client_write_iostatus(arg->e->out, &arg->e->op, ioStatus)) + arg->e->send_fkt(arg->e->log, arg->e->pc, arg->e->out); + } + } + queue_free(arg->e); + free(arg); + ArrayList_Remove(scard->workObjects, Work); +} + +static BOOL start_irp_thread(pf_channel_client_context* scard, + const pf_channel_client_queue_element* e) +{ + PTP_WORK work = NULL; + struct thread_arg* arg = calloc(1, sizeof(struct thread_arg)); + if (!arg) + return FALSE; + arg->scard = scard; + arg->e = queue_copy(e); + if (!arg->e) + goto fail; + + work = CreateThreadpoolWork(irp_thread, arg, &scard->ThreadPoolEnv); + if (!work) + goto fail; + ArrayList_Append(scard->workObjects, work); + SubmitThreadpoolWork(work); + + return TRUE; + +fail: + if (arg) + queue_free(arg->e); + free(arg); + return FALSE; +} + +BOOL pf_channel_smartcard_client_handle(wLog* log, pClientContext* pc, wStream* s, wStream* out, + pf_scard_send_fkt_t send_fkt) +{ + BOOL rc = FALSE; + LONG status = 0; + UINT32 FileId = 0; + UINT32 CompletionId = 0; + UINT32 ioStatus = 0; + pf_channel_client_queue_element e = { 0 }; + pf_channel_client_context* scard = scard_get_client_context(pc); + + WINPR_ASSERT(log); + WINPR_ASSERT(send_fkt); + WINPR_ASSERT(s); + + if (!scard) + return FALSE; + + e.log = log; + e.pc = pc; + e.out = out; + e.send_fkt = send_fkt; + + /* Skip IRP header */ + if (!Stream_CheckAndLogRequiredLength(TAG, s, 20)) + return FALSE; + else + { + UINT32 DeviceId = 0; + UINT32 MajorFunction = 0; + UINT32 MinorFunction = 0; + + Stream_Read_UINT32(s, DeviceId); /* DeviceId (4 bytes) */ + Stream_Read_UINT32(s, FileId); /* FileId (4 bytes) */ + Stream_Read_UINT32(s, CompletionId); /* CompletionId (4 bytes) */ + Stream_Read_UINT32(s, MajorFunction); /* MajorFunction (4 bytes) */ + Stream_Read_UINT32(s, MinorFunction); /* MinorFunction (4 bytes) */ + + if (MajorFunction != IRP_MJ_DEVICE_CONTROL) + { + WLog_WARN(TAG, "[%s] Invalid IRP received, expected %s, got %2", SCARD_SVC_CHANNEL_NAME, + rdpdr_irp_string(IRP_MJ_DEVICE_CONTROL), rdpdr_irp_string(MajorFunction)); + return FALSE; + } + e.op.completionID = CompletionId; + e.op.deviceID = DeviceId; + + if (!rdpdr_write_iocompletion_header(out, DeviceId, CompletionId, 0)) + return FALSE; + } + + status = smartcard_irp_device_control_decode(s, CompletionId, FileId, &e.op); + if (status != 0) + goto fail; + + switch (e.op.ioControlCode) + { + case SCARD_IOCTL_LISTREADERGROUPSA: + case SCARD_IOCTL_LISTREADERGROUPSW: + case SCARD_IOCTL_LISTREADERSA: + case SCARD_IOCTL_LISTREADERSW: + case SCARD_IOCTL_LOCATECARDSA: + case SCARD_IOCTL_LOCATECARDSW: + case SCARD_IOCTL_LOCATECARDSBYATRA: + case SCARD_IOCTL_LOCATECARDSBYATRW: + case SCARD_IOCTL_GETSTATUSCHANGEA: + case SCARD_IOCTL_GETSTATUSCHANGEW: + case SCARD_IOCTL_CONNECTA: + case SCARD_IOCTL_CONNECTW: + case SCARD_IOCTL_RECONNECT: + case SCARD_IOCTL_DISCONNECT: + case SCARD_IOCTL_BEGINTRANSACTION: + case SCARD_IOCTL_ENDTRANSACTION: + case SCARD_IOCTL_STATE: + case SCARD_IOCTL_STATUSA: + case SCARD_IOCTL_STATUSW: + case SCARD_IOCTL_TRANSMIT: + case SCARD_IOCTL_CONTROL: + case SCARD_IOCTL_GETATTRIB: + case SCARD_IOCTL_SETATTRIB: + if (!start_irp_thread(scard, &e)) + goto fail; + return TRUE; + + default: + status = smartcard_irp_device_control_call(scard->callctx, out, &ioStatus, &e.op); + if (status != 0) + goto fail; + if (!pf_channel_client_write_iostatus(out, &e.op, ioStatus)) + goto fail; + break; + } + + rc = send_fkt(log, pc, out) == CHANNEL_RC_OK; + +fail: + smartcard_operation_free(&e.op, FALSE); + return rc; +} + +BOOL pf_channel_smartcard_server_handle(pServerContext* ps, wStream* s) +{ + WLog_ERR(TAG, "TODO: unimplemented"); + return TRUE; +} + +static void channel_stop_and_wait(pf_channel_client_context* scard, BOOL reset) +{ + WINPR_ASSERT(scard); + smartcard_call_context_signal_stop(scard->callctx, FALSE); + + while (ArrayList_Count(scard->workObjects) > 0) + { + PTP_WORK work = ArrayList_GetItem(scard->workObjects, 0); + if (!work) + continue; + WaitForThreadpoolWorkCallbacks(work, TRUE); + } + + smartcard_call_context_signal_stop(scard->callctx, reset); +} + +static void pf_channel_scard_client_context_free(InterceptContextMapEntry* base) +{ + pf_channel_client_context* entry = (pf_channel_client_context*)base; + if (!entry) + return; + + /* Set the stop event. + * All threads waiting in blocking operations will abort at the next + * available polling slot */ + channel_stop_and_wait(entry, FALSE); + ArrayList_Free(entry->workObjects); + CloseThreadpool(entry->ThreadPool); + DestroyThreadpoolEnvironment(&entry->ThreadPoolEnv); + + smartcard_call_context_free(entry->callctx); + free(entry); +} + +static void queue_free(void* obj) +{ + pf_channel_client_queue_element* element = obj; + if (!element) + return; + smartcard_operation_free(&element->op, FALSE); + Stream_Free(element->out, TRUE); + free(element); +} + +static void* queue_copy(const void* obj) +{ + const pf_channel_client_queue_element* other = obj; + pf_channel_client_queue_element* copy = NULL; + if (!other) + return NULL; + copy = calloc(1, sizeof(pf_channel_client_queue_element)); + if (!copy) + return NULL; + + *copy = *other; + copy->out = Stream_New(NULL, Stream_Capacity(other->out)); + if (!copy->out) + goto fail; + Stream_Write(copy->out, Stream_Buffer(other->out), Stream_GetPosition(other->out)); + return copy; +fail: + queue_free(copy); + return NULL; +} + +static void work_object_free(void* arg) +{ + PTP_WORK work = arg; + CloseThreadpoolWork(work); +} + +BOOL pf_channel_smartcard_client_new(pClientContext* pc) +{ + pf_channel_client_context* scard = NULL; + wObject* obj = NULL; + + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->interceptContextMap); + + scard = calloc(1, sizeof(pf_channel_client_context)); + if (!scard) + return FALSE; + scard->base.free = pf_channel_scard_client_context_free; + scard->callctx = smartcard_call_context_new(pc->context.settings); + if (!scard->callctx) + goto fail; + + scard->workObjects = ArrayList_New(TRUE); + if (!scard->workObjects) + goto fail; + obj = ArrayList_Object(scard->workObjects); + WINPR_ASSERT(obj); + obj->fnObjectFree = work_object_free; + + scard->ThreadPool = CreateThreadpool(NULL); + if (!scard->ThreadPool) + goto fail; + InitializeThreadpoolEnvironment(&scard->ThreadPoolEnv); + SetThreadpoolCallbackPool(&scard->ThreadPoolEnv, scard->ThreadPool); + + return HashTable_Insert(pc->interceptContextMap, SCARD_SVC_CHANNEL_NAME, scard); +fail: + pf_channel_scard_client_context_free(&scard->base); + return FALSE; +} + +void pf_channel_smartcard_client_free(pClientContext* pc) +{ + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->interceptContextMap); + HashTable_Remove(pc->interceptContextMap, SCARD_SVC_CHANNEL_NAME); +} + +BOOL pf_channel_smartcard_client_emulate(pClientContext* pc) +{ + pf_channel_client_context* scard = scard_get_client_context(pc); + if (!scard) + return FALSE; + return smartcard_call_is_configured(scard->callctx); +} + +BOOL pf_channel_smartcard_client_reset(pClientContext* pc) +{ + pf_channel_client_context* scard = scard_get_client_context(pc); + if (!scard) + return TRUE; + + channel_stop_and_wait(scard, TRUE); + return TRUE; +} diff --git a/server/proxy/channels/pf_channel_smartcard.h b/server/proxy/channels/pf_channel_smartcard.h new file mode 100644 index 0000000..975636d --- /dev/null +++ b/server/proxy/channels/pf_channel_smartcard.h @@ -0,0 +1,39 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2021 Armin Novak <armin.novak@thincast.com> + * Copyright 2021 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_SERVER_PROXY_SCARD_H +#define FREERDP_SERVER_PROXY_SCARD_H + +#include <winpr/wlog.h> +#include <freerdp/server/proxy/proxy_context.h> + +typedef UINT (*pf_scard_send_fkt_t)(wLog* log, pClientContext*, wStream*); + +BOOL pf_channel_smartcard_client_new(pClientContext* pc); +void pf_channel_smartcard_client_free(pClientContext* pc); + +BOOL pf_channel_smartcard_client_reset(pClientContext* pc); +BOOL pf_channel_smartcard_client_emulate(pClientContext* pc); + +BOOL pf_channel_smartcard_client_handle(wLog* log, pClientContext* pc, wStream* s, wStream* out, + pf_scard_send_fkt_t fkt); +BOOL pf_channel_smartcard_server_handle(pServerContext* ps, wStream* s); + +#endif /* FREERDP_SERVER_PROXY_SCARD_H */ diff --git a/server/proxy/cli/CMakeLists.txt b/server/proxy/cli/CMakeLists.txt new file mode 100644 index 0000000..1416b4a --- /dev/null +++ b/server/proxy/cli/CMakeLists.txt @@ -0,0 +1,60 @@ +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP Proxy Server +# +# Copyright 2021 Armin Novak <armin.novak@thincast.com> +# Copyright 2021 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. + +set(PROXY_APP_SRCS freerdp_proxy.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 "${MODULE_NAME}${CMAKE_EXECUTABLE_SUFFIX}" ) + + configure_file( + ${PROJECT_SOURCE_DIR}/cmake/WindowsDLLVersion.rc.in + ${CMAKE_CURRENT_BINARY_DIR}/version.rc + @ONLY) + + list(APPEND PROXY_APP_SRCS ${CMAKE_CURRENT_BINARY_DIR}/version.rc) +endif() + +set(APP_NAME "freerdp-proxy") +add_executable(${APP_NAME} + ${PROXY_APP_SRCS} +) + +set(MANPAGE_NAME ${APP_NAME}.1) +if (WITH_BINARY_VERSIONING) + set_target_properties(${APP_NAME} + PROPERTIES + OUTPUT_NAME "${APP_NAME}${FREERDP_API_VERSION}" + ) + set(MANPAGE_NAME ${APP_NAME}${FREERDP_API_VERSION}.1) +endif() + +target_link_libraries(${APP_NAME} ${MODULE_NAME}) +install(TARGETS ${APP_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT server) +if (WITH_DEBUG_SYMBOLS AND MSVC) + install(FILES ${CMAKE_PDB_BINARY_DIR}/${APP_NAME}.pdb DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT symbols) +endif() + +set_property(TARGET ${APP_NAME} PROPERTY FOLDER "Server/proxy") + +configure_file(${APP_NAME}.1.in ${CMAKE_CURRENT_BINARY_DIR}/${MANPAGE_NAME}) +install_freerdp_man(${CMAKE_CURRENT_BINARY_DIR}/${MANPAGE_NAME} 1) diff --git a/server/proxy/cli/freerdp-proxy.1.in b/server/proxy/cli/freerdp-proxy.1.in new file mode 100644 index 0000000..c41f97c --- /dev/null +++ b/server/proxy/cli/freerdp-proxy.1.in @@ -0,0 +1,85 @@ +.de URL +\\$2 \(laURL: \\$1 \(ra\\$3 +.. +.if \n[.g] .mso www.tmac +.TH @MANPAGE_NAME@ 1 2023-12-14 "@FREERDP_VERSION_FULL@" "FreeRDP" +.SH NAME +@MANPAGE_NAME@ \- A server binary allowing MITM proxying of RDP connections +.SH SYNOPSIS +.B @MANPAGE_NAME@ +[\fB-h\fP] +[\fB--help\fP] +[\fB--buildconfig\fP] +[\fB--dump-config\fP \fB<config file>\fP] +[\fB-v\fP] +[\fB--version\fP] +[\fB<config file>\fP] +.SH DESCRIPTION +.B @MANPAGE_NAME@ +can be used to proxy a RDP connection between a target server and connecting clients. +Possible usage scenarios are: +.IP Proxying +Connect outdated/insecure RDP servers from behind a (more secure) proxy +.IP Analysis +Allow detailed protocol analysis of (many) unknown protocol features (channels) +.IP Inspection +MITM proxy for session inspection and recording + +.SH OPTIONS +.IP -h,--help +Display a help text explaining usage. +.IP --buildconfig +Print the build configuration of the proxy and exit. +.IP -v,--version +Print the version of the proxy and exit. +.IP --dump-config \fB<config-ini-file>\fP +Dump a template configuration to \fB<config-ini-file>\fP +.IP \fB<config-ini-file>\fP +Start the proxy with settings read from \fB<config-ini-file>\fP + +.SH WARNING +The proxy does not support authentication out of the box but acts simply as intermediary. +Only \fBRDP\fP and \fBTLS\fP security modes are supported, \fBNLA\fP will fail for connections to the proxy. +To implement authentication a \fBproxy-module\fP can be implemented that can authenticate against some backend +and map connecting users and credentials to target server users and credentials. + +.SH EXAMPLES +@MANPAGE_NAME@ /some/config/file + +@MANPAGE_NAME@ --dump-config /some/config/file + +.SH PREPARATIONS + +1. generate certificates for proxy + +\fBwinpr-makecert -rdp -path . proxy\fP + +2. generate proxy configuration + +\fB@MANPAGE_NAME@ --dump-config proxy.ini\fP + +3. edit configurartion and: + + * provide (preferrably absolute) paths for \fBCertificateFile\fP and \fBPrivateKeyFile\fP generated previously + * remove the \fBCertificateContents\fP and \fBPrivateKeyContents\fP + * Adjust the \fB[Server]\fP settings \fBHost\fP and \fBPort\fP to bind a specific port on a network interface + * Adjust the \fB[Target]\fP \fBHost\fP and \fBPort\fP settings to the \fBRDP\fP target server + * Adjust (or remove if unuse) the \fBPlugins\fP settings + +3. start proxy server + + \fB@MANPAGE_NAME@ proxy.ini\fP + +.SH EXIT STATUS +.TP +.B 0 +Successful program execution. +.TP +.B 1 +Otherwise. + +.SH SEE ALSO +wlog(7) + +.SH AUTHOR +FreeRDP <team@freerdp.com> diff --git a/server/proxy/cli/freerdp_proxy.c b/server/proxy/cli/freerdp_proxy.c new file mode 100644 index 0000000..bc53ae2 --- /dev/null +++ b/server/proxy/cli/freerdp_proxy.c @@ -0,0 +1,161 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2019 Mati Shabtay <matishabtay@gmail.com> + * Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com> + * Copyright 2019 Idan Freiberg <speidy@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 <winpr/collections.h> + +#include <freerdp/version.h> +#include <freerdp/freerdp.h> + +#include <freerdp/server/proxy/proxy_server.h> +#include <freerdp/server/proxy/proxy_log.h> + +#include <stdlib.h> +#include <signal.h> + +#define TAG PROXY_TAG("server") + +static proxyServer* server = NULL; + +#if defined(_WIN32) +static const char* strsignal(int signum) +{ + switch (signum) + { + case SIGINT: + return "SIGINT"; + case SIGTERM: + return "SIGTERM"; + default: + return "UNKNOWN"; + } +} +#endif + +static void cleanup_handler(int signum) +{ + printf("\n"); + WLog_INFO(TAG, "caught signal %s [%d], starting cleanup...", strsignal(signum), signum); + + WLog_INFO(TAG, "stopping all connections."); + pf_server_stop(server); +} + +static void pf_server_register_signal_handlers(void) +{ + signal(SIGINT, cleanup_handler); + signal(SIGTERM, cleanup_handler); +#ifndef _WIN32 + signal(SIGQUIT, cleanup_handler); + signal(SIGKILL, cleanup_handler); +#endif +} + +static WINPR_NORETURN(void usage(const char* app)) +{ + printf("Usage:\n"); + printf("%s -h Display this help text.\n", app); + printf("%s --help Display this help text.\n", app); + printf("%s --buildconfig Print the build configuration.\n", app); + printf("%s <config ini file> Start the proxy with <config.ini>\n", app); + printf("%s --dump-config <config ini file> Create a template <config.ini>\n", app); + printf("%s -v Print out binary version.\n", app); + printf("%s --version Print out binary version.\n", app); + exit(0); +} + +static void version(const char* app) +{ + printf("%s version %s", app, freerdp_get_version_string()); + exit(0); +} + +static WINPR_NORETURN(void buildconfig(const char* app)) +{ + printf("This is FreeRDP version %s (%s)\n", FREERDP_VERSION_FULL, FREERDP_GIT_REVISION); + printf("%s", freerdp_get_build_config()); + exit(0); +} + +int main(int argc, char* argv[]) +{ + proxyConfig* config = NULL; + char* config_path = "config.ini"; + int status = -1; + + pf_server_register_signal_handlers(); + + WLog_INFO(TAG, "freerdp-proxy version info:"); + WLog_INFO(TAG, "\tFreeRDP version: %s", FREERDP_VERSION_FULL); + WLog_INFO(TAG, "\tGit commit: %s", FREERDP_GIT_REVISION); + WLog_DBG(TAG, "\tBuild config: %s", freerdp_get_build_config()); + + if (argc < 2) + usage(argv[0]); + + { + const char* arg = argv[1]; + + if (_stricmp(arg, "-h") == 0) + usage(argv[0]); + else if (_stricmp(arg, "--help") == 0) + usage(argv[0]); + else if (_stricmp(arg, "--buildconfig") == 0) + buildconfig(argv[0]); + else if (_stricmp(arg, "--dump-config") == 0) + { + if (argc <= 2) + usage(argv[0]); + pf_server_config_dump(argv[2]); + status = 0; + goto fail; + } + else if (_stricmp(arg, "-v") == 0) + version(argv[0]); + else if (_stricmp(arg, "--version") == 0) + version(argv[0]); + config_path = argv[1]; + } + + config = pf_server_config_load_file(config_path); + if (!config) + goto fail; + + pf_server_config_print(config); + + server = pf_server_new(config); + pf_server_config_free(config); + + if (!server) + goto fail; + + if (!pf_server_start(server)) + goto fail; + + if (!pf_server_run(server)) + goto fail; + + status = 0; + +fail: + pf_server_free(server); + + return status; +} diff --git a/server/proxy/config.ini b/server/proxy/config.ini new file mode 100644 index 0000000..a8ac44b --- /dev/null +++ b/server/proxy/config.ini @@ -0,0 +1,53 @@ +[Server] +Host = 0.0.0.0 +Port = 3389 + +[Target] +; If this value is set to TRUE, the target server info will be parsed using the +; load balance info setting at runtime. The format is +; "Cookie: msts=<target server>", and can be set in an rdp file for windows/mac, +; and the /load-balance-info: CLI option for xfreerdp. Otherwise, the server +; will always connect to the same target, using the configured values of `Host` +; and `Port`. +FixedTarget = TRUE +Host = CustomHost +Port = 3389 + +[Input] +Mouse = TRUE +Keyboard = TRUE + +[Security] +ServerTlsSecurity = TRUE +ServerRdpSecurity = FALSE +ClientTlsSecurity = TRUE +ClientRdpSecurity = FALSE +ClientNlaSecurity = TRUE +ClientAllowFallbackToTls = TRUE + +[Channels] +GFX = TRUE +DisplayControl = TRUE +Clipboard = TRUE +AudioOutput = TRUE +RemoteApp = TRUE +; a list of comma seperated static channels that will be proxied. This feature is useful, +; for example when there's a custom static channel that isn't implemented in freerdp/proxy, and is needed to be proxied when connecting through the proxy. +; Passthrough = "" + +[Clipboard] +TextOnly = FALSE +MaxTextLength = 10 # 0 for no limit. + +[GFXSettings] +DecodeGFX = TRUE + +[Plugins] +; An optional, comma separated list of paths to modules that the proxy should load at startup. +; +; Modules = "proxy-demo-plugin.so" + +; An optional, comma separated list of required plugins (names), +; that the proxy won't start without having them loaded. +; +; Required = "demo" diff --git a/server/proxy/freerdp-proxy.pc.in b/server/proxy/freerdp-proxy.pc.in new file mode 100644 index 0000000..4655075 --- /dev/null +++ b/server/proxy/freerdp-proxy.pc.in @@ -0,0 +1,16 @@ +prefix=@PKG_CONFIG_INSTALL_PREFIX@ +exec_prefix=${prefix} +libdir=${prefix}/@CMAKE_INSTALL_LIBDIR@ +includedir=${prefix}/@FREERDP_INCLUDE_DIR@ +libs=-lfreerdp-server-proxy@FREERDP_API_VERSION@ + +Name: FreeRDP proxy +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@ freerdp-server@FREERDP_VERSION_MAJOR@ freerdp-client@FREERDP_VERSION_MAJOR@ + +Libs: -L${libdir} ${libs} +Libs.private: -ldl -lpthread +Cflags: -I${includedir} diff --git a/server/proxy/modules/CMakeLists.txt b/server/proxy/modules/CMakeLists.txt new file mode 100644 index 0000000..500a3e7 --- /dev/null +++ b/server/proxy/modules/CMakeLists.txt @@ -0,0 +1,33 @@ +# Copyright 2019 Kobi Mizrachi <kmizrachi18@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. + +# The third-party directory is meant for third-party components to be built +# as part of the main FreeRDP build system, making separate maintenance easier. +# Subdirectories of the third-party directory are ignored by git, but are +# automatically included by CMake when the -DWITH_THIRD_PARTY=on option is used. + +# include proxy header files for proxy modules +include_directories("${PROJECT_SOURCE_DIR}/server/proxy") +include_directories("${PROJECT_SOURCE_DIR}/server/proxy/modules") + +# taken from FreeRDP/third-party/CMakeLists.txt +file(GLOB all_valid_subdirs RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*/CMakeLists.txt") + +foreach(dir ${all_valid_subdirs}) + if(${dir} MATCHES "^([^/]*)/+CMakeLists.txt") + string(REGEX REPLACE "^([^/]*)/+CMakeLists.txt" "\\1" dir_trimmed ${dir}) + message(STATUS "Adding proxy module ${dir_trimmed}") + add_subdirectory(${dir_trimmed}) + endif() +endforeach(dir) diff --git a/server/proxy/modules/README.md b/server/proxy/modules/README.md new file mode 100644 index 0000000..f1d2206 --- /dev/null +++ b/server/proxy/modules/README.md @@ -0,0 +1,66 @@ +# Proxy module API + +`freerdp-proxy` has an API for hooking/filtering certain events/messages. +A module can register callbacks to events, allowing to record the data and control whether to pass/ignore, or right out drop the connection. + +During startup, the proxy reads its modules from the configuration: + +```ini +[Plugins] +Modules = demo,cap +``` + +These modules are loaded in a best effort manner. Additionally there is a configuration field for modules that must be loaded, +so the proxy refuses to start if they are not found: + +```ini +[Plugins] +Required = demo,cap +``` + +Modules must be installed as shared libraris in the `<base install>/lib/freerdp3/proxy` folder and match the pattern +`proxy-<name>-plugin.<ext>` (e.g. `proxy-demo-plugin.so`) to be found. +For security reasons loading by full path is not supported and only the installation path is used for lookup. + +## Currently supported hook events + +### Client + +* ClientInitConnect: Called before the client tries to open a connection +* ClientUninitConnect: Called after the client has disconnected +* ClientPreConnect: Called in client PreConnect callback +* ClientPostConnect: Called in client PostConnect callback +* ClientPostDisconnect: Called in client PostDisconnect callback +* ClientX509Certificate: Called in client X509 certificate verification callback +* ClientLoginFailure: Called in client login failure callback +* ClientEndPaint: Called in client EndPaint callback + +### Server + +* ServerPostConnect: Called after a client has connected +* ServerPeerActivate: Called after a client has activated +* ServerChannelsInit: Called after channels are initialized +* ServerChannelsFree: Called after channels are cleaned up +* ServerSessionEnd: Called after the client connection disconnected + +## Currently supported filter events + +* KeyboardEvent: Keyboard event, e.g. all key press and release events +* MouseEvent: Mouse event, e.g. mouse movement and button press/release events +* ClientChannelData: Client static channel data +* ServerChannelData: Server static channel data +* DynamicChannelCreate: Dynamic channel create +* ServerFetchTargetAddr: Fetch target address (e.g. RDP TargetInfo) +* ServerPeerLogon: A peer is logging on + +## Developing a new module +* Create a new file that includes `freerdp/server/proxy/proxy_modules_api.h`. +* Implement the `proxy_module_entry_point` function and register the callbacks you are interested in. +* Each callback receives two parameters: + * `connectionInfo* info` holds connection info of the raised event. + * `void* param` holds the actual event data. It should be casted by the filter to the suitable struct from `filters_api.h`. +* Each callback must return a `BOOL`: + * `FALSE`: The event will not be proxied. + * `TRUE`: The event will be proxied. + +A demo can be found in `filter_demo.c`. diff --git a/server/proxy/modules/bitmap-filter/CMakeLists.txt b/server/proxy/modules/bitmap-filter/CMakeLists.txt new file mode 100644 index 0000000..d2cc03b --- /dev/null +++ b/server/proxy/modules/bitmap-filter/CMakeLists.txt @@ -0,0 +1,60 @@ +# +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP Proxy Server Demo C++ Module +# +# Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com> +# Copyright 2021 Armin Novak <anovak@thincast.com> +# Copyright 2021 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. +# + +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(proxy-bitmap-filter-plugin + VERSION ${FREERDP_DEFAULT_PROJECT_VERSION} + LANGUAGES CXX +) + +message("project ${PROJECT_NAME} is using version ${PROJECT_VERSION}") + +project(proxy-bitmap-filter-plugin + VERSION ${FREERDP_DEFAULT_PROJECT_VERSION} + LANGUAGES CXX +) + +message("project ${PROJECT_NAME} is using version ${PROJECT_VERSION}") + +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../../../cmake/) +include(CommonConfigOptions) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_library(${PROJECT_NAME} SHARED + bitmap-filter.cpp +) + +target_link_libraries(${PROJECT_NAME} winpr freerdp) + +set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "") +set_target_properties(${PROJECT_NAME} PROPERTIES NO_SONAME 1) + +install(TARGETS ${PROJECT_NAME} DESTINATION ${FREERDP_PROXY_PLUGINDIR}) diff --git a/server/proxy/modules/bitmap-filter/bitmap-filter.cpp b/server/proxy/modules/bitmap-filter/bitmap-filter.cpp new file mode 100644 index 0000000..d1b2e78 --- /dev/null +++ b/server/proxy/modules/bitmap-filter/bitmap-filter.cpp @@ -0,0 +1,453 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server persist-bitmap-filter Module + * + * this module is designed to deactivate all persistent bitmap cache settings a + * client might send. + * + * Copyright 2023 Armin Novak <anovak@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 <iostream> +#include <vector> +#include <string> +#include <algorithm> +#include <map> +#include <memory> +#include <mutex> + +#include <freerdp/server/proxy/proxy_modules_api.h> +#include <freerdp/server/proxy/proxy_context.h> + +#include <freerdp/channels/drdynvc.h> +#include <freerdp/channels/rdpgfx.h> +#include <freerdp/utils/gfx.h> + +#define TAG MODULE_TAG("persist-bitmap-filter") + +static constexpr char plugin_name[] = "bitmap-filter"; +static constexpr char plugin_desc[] = + "this plugin deactivates and filters persistent bitmap cache."; + +static const std::vector<std::string> plugin_static_intercept = { DRDYNVC_SVC_CHANNEL_NAME }; +static const std::vector<std::string> plugin_dyn_intercept = { RDPGFX_DVC_CHANNEL_NAME }; + +class DynChannelState +{ + + public: + bool skip() const + { + return _toSkip != 0; + } + + bool skip(size_t s) + { + if (s > _toSkip) + _toSkip = 0; + else + _toSkip -= s; + return skip(); + } + + size_t remaining() const + { + return _toSkip; + } + + size_t total() const + { + return _totalSkipSize; + } + + void setSkipSize(size_t len) + { + _toSkip = _totalSkipSize = len; + } + + bool drop() const + { + return _drop; + } + + void setDrop(bool d) + { + _drop = d; + } + + uint32_t channelId() const + { + return _channelId; + } + + void setChannelId(uint32_t id) + { + _channelId = id; + } + + private: + size_t _toSkip = 0; + size_t _totalSkipSize = 0; + bool _drop = false; + uint32_t _channelId = 0; +}; + +static BOOL filter_client_pre_connect(proxyPlugin* plugin, proxyData* pdata, void* custom) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(pdata->pc); + WINPR_ASSERT(custom); + + auto settings = pdata->pc->context.settings; + + /* We do not want persistent bitmap cache to be used with proxy */ + return freerdp_settings_set_bool(settings, FreeRDP_BitmapCachePersistEnabled, FALSE); +} + +static BOOL filter_dyn_channel_intercept_list(proxyPlugin* plugin, proxyData* pdata, void* arg) +{ + auto data = static_cast<proxyChannelToInterceptData*>(arg); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(data); + + auto intercept = std::find(plugin_dyn_intercept.begin(), plugin_dyn_intercept.end(), + data->name) != plugin_dyn_intercept.end(); + if (intercept) + data->intercept = TRUE; + return TRUE; +} + +static BOOL filter_static_channel_intercept_list(proxyPlugin* plugin, proxyData* pdata, void* arg) +{ + auto data = static_cast<proxyChannelToInterceptData*>(arg); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(data); + + auto intercept = std::find(plugin_static_intercept.begin(), plugin_static_intercept.end(), + data->name) != plugin_static_intercept.end(); + if (intercept) + data->intercept = TRUE; + return TRUE; +} + +static size_t drdynvc_cblen_to_bytes(UINT8 cbLen) +{ + switch (cbLen) + { + case 0: + return 1; + + case 1: + return 2; + + default: + return 4; + } +} + +static UINT32 drdynvc_read_variable_uint(wStream* s, UINT8 cbLen) +{ + UINT32 val = 0; + + switch (cbLen) + { + case 0: + Stream_Read_UINT8(s, val); + break; + + case 1: + Stream_Read_UINT16(s, val); + break; + + default: + Stream_Read_UINT32(s, val); + break; + } + + return val; +} + +static BOOL drdynvc_try_read_header(wStream* s, size_t& channelId, size_t& length) +{ + UINT8 value = 0; + Stream_SetPosition(s, 0); + if (Stream_GetRemainingLength(s) < 1) + return FALSE; + Stream_Read_UINT8(s, value); + + const UINT8 cmd = (value & 0xf0) >> 4; + const UINT8 Sp = (value & 0x0c) >> 2; + const UINT8 cbChId = (value & 0x03); + + switch (cmd) + { + case DATA_PDU: + case DATA_FIRST_PDU: + break; + default: + return FALSE; + } + + const size_t channelIdLen = drdynvc_cblen_to_bytes(cbChId); + if (Stream_GetRemainingLength(s) < channelIdLen) + return FALSE; + + channelId = drdynvc_read_variable_uint(s, cbChId); + length = Stream_Length(s); + if (cmd == DATA_FIRST_PDU) + { + const size_t dataLen = drdynvc_cblen_to_bytes(Sp); + if (Stream_GetRemainingLength(s) < dataLen) + return FALSE; + + length = drdynvc_read_variable_uint(s, Sp); + } + + return TRUE; +} + +static DynChannelState* filter_get_plugin_data(proxyPlugin* plugin, proxyData* pdata) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + + auto mgr = static_cast<proxyPluginsManager*>(plugin->custom); + WINPR_ASSERT(mgr); + + WINPR_ASSERT(mgr->GetPluginData); + return static_cast<DynChannelState*>(mgr->GetPluginData(mgr, plugin_name, pdata)); +} + +static BOOL filter_set_plugin_data(proxyPlugin* plugin, proxyData* pdata, DynChannelState* data) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + + auto mgr = static_cast<proxyPluginsManager*>(plugin->custom); + WINPR_ASSERT(mgr); + + WINPR_ASSERT(mgr->SetPluginData); + return mgr->SetPluginData(mgr, plugin_name, pdata, data); +} + +static UINT8 drdynvc_value_to_cblen(UINT32 value) +{ + if (value <= 0xFF) + return 0; + if (value <= 0xFFFF) + return 1; + return 2; +} + +static BOOL drdynvc_write_variable_uint(wStream* s, UINT32 value, UINT8 cbLen) +{ + switch (cbLen) + { + case 0: + Stream_Write_UINT8(s, static_cast<UINT8>(value)); + break; + + case 1: + Stream_Write_UINT16(s, static_cast<UINT16>(value)); + break; + + default: + Stream_Write_UINT32(s, value); + break; + } + + return TRUE; +} + +static BOOL drdynvc_write_header(wStream* s, UINT32 channelId) +{ + const UINT8 cbChId = drdynvc_value_to_cblen(channelId); + const UINT8 value = (DATA_PDU << 4) | cbChId; + const size_t len = drdynvc_cblen_to_bytes(cbChId) + 1; + + if (!Stream_EnsureRemainingCapacity(s, len)) + return FALSE; + + Stream_Write_UINT8(s, value); + return drdynvc_write_variable_uint(s, value, cbChId); +} + +static BOOL filter_forward_empty_offer(const char* sessionID, proxyDynChannelInterceptData* data, + size_t startPosition, UINT32 channelId) +{ + WINPR_ASSERT(data); + + Stream_SetPosition(data->data, startPosition); + if (!drdynvc_write_header(data->data, channelId)) + return FALSE; + + if (!Stream_EnsureRemainingCapacity(data->data, sizeof(UINT16))) + return FALSE; + Stream_Write_UINT16(data->data, 0); + Stream_SealLength(data->data); + + WLog_INFO(TAG, "[SessionID=%s][%s] forwarding empty %s", sessionID, plugin_name, + rdpgfx_get_cmd_id_string(RDPGFX_CMDID_CACHEIMPORTOFFER)); + data->rewritten = TRUE; + return TRUE; +} + +static BOOL filter_dyn_channel_intercept(proxyPlugin* plugin, proxyData* pdata, void* arg) +{ + auto data = static_cast<proxyDynChannelInterceptData*>(arg); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(data); + + data->result = PF_CHANNEL_RESULT_PASS; + if (!data->isBackData && + (strncmp(data->name, RDPGFX_DVC_CHANNEL_NAME, ARRAYSIZE(RDPGFX_DVC_CHANNEL_NAME)) == 0)) + { + auto state = filter_get_plugin_data(plugin, pdata); + if (!state) + { + WLog_ERR(TAG, "[SessionID=%s][%s] missing custom data, aborting!", pdata->session_id, + plugin_name); + return FALSE; + } + const size_t inputDataLength = Stream_Length(data->data); + UINT16 cmdId = RDPGFX_CMDID_UNUSED_0000; + + const auto pos = Stream_GetPosition(data->data); + if (!state->skip()) + { + if (data->first) + { + size_t channelId = 0; + size_t length = 0; + if (drdynvc_try_read_header(data->data, channelId, length)) + { + if (Stream_GetRemainingLength(data->data) >= 2) + { + Stream_Read_UINT16(data->data, cmdId); + state->setSkipSize(length); + state->setDrop(false); + } + } + + switch (cmdId) + { + case RDPGFX_CMDID_CACHEIMPORTOFFER: + state->setDrop(true); + state->setChannelId(channelId); + break; + default: + break; + } + Stream_SetPosition(data->data, pos); + } + } + + if (state->skip()) + { + state->skip(inputDataLength); + if (state->drop()) + { + WLog_WARN(TAG, + "[SessionID=%s][%s] dropping %s packet [total:%" PRIuz ", current:%" PRIuz + ", remaining: %" PRIuz "]", + pdata->session_id, plugin_name, + rdpgfx_get_cmd_id_string(RDPGFX_CMDID_CACHEIMPORTOFFER), state->total(), + inputDataLength, state->remaining()); + data->result = PF_CHANNEL_RESULT_DROP; + +#if 0 // TODO: Sending this does screw up some windows RDP server versions :/ + if (state->remaining() == 0) + { + if (!filter_forward_empty_offer(pdata->session_id, data, pos, + state->channelId())) + return FALSE; + } +#endif + } + } + } + + return TRUE; +} + +static BOOL filter_server_session_started(proxyPlugin* plugin, proxyData* pdata, void*) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + + auto state = filter_get_plugin_data(plugin, pdata); + delete state; + + auto newstate = new DynChannelState(); + if (!filter_set_plugin_data(plugin, pdata, newstate)) + { + delete newstate; + return FALSE; + } + + return TRUE; +} + +static BOOL filter_server_session_end(proxyPlugin* plugin, proxyData* pdata, void*) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + + auto state = filter_get_plugin_data(plugin, pdata); + delete state; + filter_set_plugin_data(plugin, pdata, nullptr); + return TRUE; +} + +#ifdef __cplusplus +extern "C" +{ +#endif + FREERDP_API BOOL proxy_module_entry_point(proxyPluginsManager* plugins_manager, void* userdata); +#ifdef __cplusplus +} +#endif + +BOOL proxy_module_entry_point(proxyPluginsManager* plugins_manager, void* userdata) +{ + proxyPlugin plugin = {}; + + plugin.name = plugin_name; + plugin.description = plugin_desc; + + plugin.ServerSessionStarted = filter_server_session_started; + plugin.ServerSessionEnd = filter_server_session_end; + + plugin.ClientPreConnect = filter_client_pre_connect; + + plugin.StaticChannelToIntercept = filter_static_channel_intercept_list; + plugin.DynChannelToIntercept = filter_dyn_channel_intercept_list; + plugin.DynChannelIntercept = filter_dyn_channel_intercept; + + plugin.custom = plugins_manager; + if (!plugin.custom) + return FALSE; + plugin.userdata = userdata; + + return plugins_manager->RegisterPlugin(plugins_manager, &plugin); +} diff --git a/server/proxy/modules/demo/CMakeLists.txt b/server/proxy/modules/demo/CMakeLists.txt new file mode 100644 index 0000000..bdd85a3 --- /dev/null +++ b/server/proxy/modules/demo/CMakeLists.txt @@ -0,0 +1,53 @@ +# +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP Proxy Server Demo C++ Module +# +# Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com> +# Copyright 2021 Armin Novak <anovak@thincast.com> +# Copyright 2021 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. +# + +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(proxy-demo-plugin + VERSION ${FREERDP_DEFAULT_PROJECT_VERSION} + LANGUAGES CXX +) + +message("project ${PROJECT_NAME} is using version ${PROJECT_VERSION}") + +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../../../cmake/) +include(CommonConfigOptions) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_library(${PROJECT_NAME} SHARED + demo.cpp +) + +target_link_libraries(${PROJECT_NAME} winpr) + +set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "") +set_target_properties(${PROJECT_NAME} PROPERTIES NO_SONAME 1) + +install(TARGETS ${PROJECT_NAME} DESTINATION ${FREERDP_PROXY_PLUGINDIR}) diff --git a/server/proxy/modules/demo/demo.cpp b/server/proxy/modules/demo/demo.cpp new file mode 100644 index 0000000..75526ef --- /dev/null +++ b/server/proxy/modules/demo/demo.cpp @@ -0,0 +1,422 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server Demo C++ Module + * + * Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com> + * Copyright 2021 Armin Novak <anovak@thincast.com> + * Copyright 2021 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 <iostream> + +#include <freerdp/api.h> +#include <freerdp/scancode.h> +#include <freerdp/server/proxy/proxy_modules_api.h> + +#define TAG MODULE_TAG("demo") + +struct demo_custom_data +{ + proxyPluginsManager* mgr; + int somesetting; +}; + +static constexpr char plugin_name[] = "demo"; +static constexpr char plugin_desc[] = "this is a test plugin"; + +static BOOL demo_plugin_unload(proxyPlugin* plugin) +{ + WINPR_ASSERT(plugin); + + std::cout << "C++ demo plugin: unloading..." << std::endl; + + /* Here we have to free up our custom data storage. */ + if (plugin) + delete static_cast<struct demo_custom_data*>(plugin->custom); + + return TRUE; +} + +static BOOL demo_client_init_connect(proxyPlugin* plugin, proxyData* pdata, void* custom) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(custom); + + WLog_INFO(TAG, "called"); + return TRUE; +} + +static BOOL demo_client_uninit_connect(proxyPlugin* plugin, proxyData* pdata, void* custom) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(custom); + + WLog_INFO(TAG, "called"); + return TRUE; +} + +static BOOL demo_client_pre_connect(proxyPlugin* plugin, proxyData* pdata, void* custom) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(custom); + + WLog_INFO(TAG, "called"); + return TRUE; +} + +static BOOL demo_client_post_connect(proxyPlugin* plugin, proxyData* pdata, void* custom) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(custom); + + WLog_INFO(TAG, "called"); + return TRUE; +} + +static BOOL demo_client_post_disconnect(proxyPlugin* plugin, proxyData* pdata, void* custom) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(custom); + + WLog_INFO(TAG, "called"); + return TRUE; +} + +static BOOL demo_client_x509_certificate(proxyPlugin* plugin, proxyData* pdata, void* custom) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(custom); + + WLog_INFO(TAG, "called"); + return TRUE; +} + +static BOOL demo_client_login_failure(proxyPlugin* plugin, proxyData* pdata, void* custom) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(custom); + + WLog_INFO(TAG, "called"); + return TRUE; +} + +static BOOL demo_client_end_paint(proxyPlugin* plugin, proxyData* pdata, void* custom) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(custom); + + WLog_INFO(TAG, "called"); + return TRUE; +} + +static BOOL demo_client_redirect(proxyPlugin* plugin, proxyData* pdata, void* custom) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(custom); + + WLog_INFO(TAG, "called"); + return TRUE; +} + +static BOOL demo_server_post_connect(proxyPlugin* plugin, proxyData* pdata, void* custom) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(custom); + + WLog_INFO(TAG, "called"); + return TRUE; +} + +static BOOL demo_server_peer_activate(proxyPlugin* plugin, proxyData* pdata, void* custom) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(custom); + + WLog_INFO(TAG, "called"); + return TRUE; +} + +static BOOL demo_server_channels_init(proxyPlugin* plugin, proxyData* pdata, void* custom) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(custom); + + WLog_INFO(TAG, "called"); + return TRUE; +} + +static BOOL demo_server_channels_free(proxyPlugin* plugin, proxyData* pdata, void* custom) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(custom); + + WLog_INFO(TAG, "called"); + return TRUE; +} + +static BOOL demo_server_session_end(proxyPlugin* plugin, proxyData* pdata, void* custom) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(custom); + + WLog_INFO(TAG, "called"); + return TRUE; +} + +static BOOL demo_filter_keyboard_event(proxyPlugin* plugin, proxyData* pdata, void* param) +{ + proxyPluginsManager* mgr = nullptr; + auto event_data = static_cast<const proxyKeyboardEventInfo*>(param); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(event_data); + + mgr = plugin->mgr; + WINPR_ASSERT(mgr); + + if (event_data == nullptr) + return FALSE; + + if (event_data->rdp_scan_code == RDP_SCANCODE_KEY_B) + { + /* user typed 'B', that means bye :) */ + std::cout << "C++ demo plugin: aborting connection" << std::endl; + mgr->AbortConnect(mgr, pdata); + } + + return TRUE; +} + +static BOOL demo_filter_unicode_event(proxyPlugin* plugin, proxyData* pdata, void* param) +{ + proxyPluginsManager* mgr = nullptr; + auto event_data = static_cast<const proxyUnicodeEventInfo*>(param); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(event_data); + + mgr = plugin->mgr; + WINPR_ASSERT(mgr); + + if (event_data == nullptr) + return FALSE; + + if (event_data->code == 'b') + { + /* user typed 'B', that means bye :) */ + std::cout << "C++ demo plugin: aborting connection" << std::endl; + mgr->AbortConnect(mgr, pdata); + } + + return TRUE; +} + +static BOOL demo_mouse_event(proxyPlugin* plugin, proxyData* pdata, void* param) +{ + auto event_data = static_cast<const proxyMouseEventInfo*>(param); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(event_data); + + WLog_INFO(TAG, "called"); + return TRUE; +} + +static BOOL demo_mouse_ex_event(proxyPlugin* plugin, proxyData* pdata, void* param) +{ + auto event_data = static_cast<const proxyMouseExEventInfo*>(param); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(event_data); + + WLog_INFO(TAG, "called"); + return TRUE; +} + +static BOOL demo_client_channel_data(proxyPlugin* plugin, proxyData* pdata, void* param) +{ + const proxyChannelDataEventInfo* channel = static_cast<const proxyChannelDataEventInfo*>(param); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(channel); + + WLog_INFO(TAG, "%s [0x%04" PRIx16 "] got %" PRIuz, channel->channel_name, channel->channel_id, + channel->data_len); + return TRUE; +} + +static BOOL demo_server_channel_data(proxyPlugin* plugin, proxyData* pdata, void* param) +{ + const proxyChannelDataEventInfo* channel = static_cast<const proxyChannelDataEventInfo*>(param); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(channel); + + WLog_WARN(TAG, "%s [0x%04" PRIx16 "] got %" PRIuz, channel->channel_name, channel->channel_id, + channel->data_len); + return TRUE; +} + +static BOOL demo_dynamic_channel_create(proxyPlugin* plugin, proxyData* pdata, void* param) +{ + const proxyChannelDataEventInfo* channel = static_cast<const proxyChannelDataEventInfo*>(param); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(channel); + + WLog_WARN(TAG, "%s [0x%04" PRIx16 "]", channel->channel_name, channel->channel_id); + return TRUE; +} + +static BOOL demo_server_fetch_target_addr(proxyPlugin* plugin, proxyData* pdata, void* param) +{ + auto event_data = static_cast<const proxyFetchTargetEventInfo*>(param); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(event_data); + + WLog_INFO(TAG, "called"); + return TRUE; +} + +static BOOL demo_server_peer_logon(proxyPlugin* plugin, proxyData* pdata, void* param) +{ + auto info = static_cast<const proxyServerPeerLogon*>(param); + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(info); + WINPR_ASSERT(info->identity); + + WLog_INFO(TAG, "%d", info->automatic); + return TRUE; +} + +static BOOL demo_dyn_channel_intercept_list(proxyPlugin* plugin, proxyData* pdata, void* arg) +{ + auto data = static_cast<proxyChannelToInterceptData*>(arg); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(data); + + WLog_INFO(TAG, "%s", __func__); + return TRUE; +} + +static BOOL demo_static_channel_intercept_list(proxyPlugin* plugin, proxyData* pdata, void* arg) +{ + auto data = static_cast<proxyChannelToInterceptData*>(arg); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(data); + + WLog_INFO(TAG, "%s", __func__); + return TRUE; +} + +static BOOL demo_dyn_channel_intercept(proxyPlugin* plugin, proxyData* pdata, void* arg) +{ + auto data = static_cast<proxyDynChannelInterceptData*>(arg); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(data); + + WLog_INFO(TAG, "%s", __func__); + return TRUE; +} + +#ifdef __cplusplus +extern "C" +{ +#endif + FREERDP_API BOOL proxy_module_entry_point(proxyPluginsManager* plugins_manager, void* userdata); +#ifdef __cplusplus +} +#endif + +BOOL proxy_module_entry_point(proxyPluginsManager* plugins_manager, void* userdata) +{ + struct demo_custom_data* custom = nullptr; + proxyPlugin plugin = {}; + + plugin.name = plugin_name; + plugin.description = plugin_desc; + plugin.PluginUnload = demo_plugin_unload; + plugin.ClientInitConnect = demo_client_init_connect; + plugin.ClientUninitConnect = demo_client_uninit_connect; + plugin.ClientPreConnect = demo_client_pre_connect; + plugin.ClientPostConnect = demo_client_post_connect; + plugin.ClientPostDisconnect = demo_client_post_disconnect; + plugin.ClientX509Certificate = demo_client_x509_certificate; + plugin.ClientLoginFailure = demo_client_login_failure; + plugin.ClientEndPaint = demo_client_end_paint; + plugin.ClientRedirect = demo_client_redirect; + plugin.ServerPostConnect = demo_server_post_connect; + plugin.ServerPeerActivate = demo_server_peer_activate; + plugin.ServerChannelsInit = demo_server_channels_init; + plugin.ServerChannelsFree = demo_server_channels_free; + plugin.ServerSessionEnd = demo_server_session_end; + plugin.KeyboardEvent = demo_filter_keyboard_event; + plugin.UnicodeEvent = demo_filter_unicode_event; + plugin.MouseEvent = demo_mouse_event; + plugin.MouseExEvent = demo_mouse_ex_event; + plugin.ClientChannelData = demo_client_channel_data; + plugin.ServerChannelData = demo_server_channel_data; + plugin.DynamicChannelCreate = demo_dynamic_channel_create; + plugin.ServerFetchTargetAddr = demo_server_fetch_target_addr; + plugin.ServerPeerLogon = demo_server_peer_logon; + + plugin.StaticChannelToIntercept = demo_static_channel_intercept_list; + plugin.DynChannelToIntercept = demo_dyn_channel_intercept_list; + plugin.DynChannelIntercept = demo_dyn_channel_intercept; + + plugin.userdata = userdata; + + custom = new (struct demo_custom_data); + if (!custom) + return FALSE; + + custom->mgr = plugins_manager; + custom->somesetting = 42; + + plugin.custom = custom; + plugin.userdata = userdata; + + return plugins_manager->RegisterPlugin(plugins_manager, &plugin); +} diff --git a/server/proxy/modules/dyn-channel-dump/CMakeLists.txt b/server/proxy/modules/dyn-channel-dump/CMakeLists.txt new file mode 100644 index 0000000..dc0fc53 --- /dev/null +++ b/server/proxy/modules/dyn-channel-dump/CMakeLists.txt @@ -0,0 +1,58 @@ +# +# FreeRDP: A Remote Desktop Protocol Implementation +# FreeRDP Proxy Server Demo C++ Module +# +# Copyright 2023 Armin Novak <anovak@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. +# + +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(proxy-dyn-channel-dump-plugin + VERSION ${FREERDP_DEFAULT_PROJECT_VERSION} + LANGUAGES CXX +) + +message("project ${PROJECT_NAME} is using version ${PROJECT_VERSION}") + +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../../../cmake/) +include(CommonConfigOptions) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_library(${PROJECT_NAME} SHARED + dyn-channel-dump.cpp +) + +target_link_libraries(${PROJECT_NAME} PRIVATE + winpr + freerdp + freerdp-client + freerdp-server + freerdp-server-proxy +) + +set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "") +set_target_properties(${PROJECT_NAME} PROPERTIES NO_SONAME 1) + +install(TARGETS ${PROJECT_NAME} DESTINATION ${FREERDP_PROXY_PLUGINDIR}) diff --git a/server/proxy/modules/dyn-channel-dump/dyn-channel-dump.cpp b/server/proxy/modules/dyn-channel-dump/dyn-channel-dump.cpp new file mode 100644 index 0000000..d80902f --- /dev/null +++ b/server/proxy/modules/dyn-channel-dump/dyn-channel-dump.cpp @@ -0,0 +1,436 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server persist-bitmap-filter Module + * + * this module is designed to deactivate all persistent bitmap cache settings a + * client might send. + * + * Copyright 2023 Armin Novak <anovak@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 <fstream> +#include <iostream> +#include <regex> +#include <vector> +#include <sstream> +#include <string> +#include <algorithm> +#include <map> +#include <memory> +#include <mutex> +#include <atomic> +#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 <freerdp/server/proxy/proxy_modules_api.h> +#include <freerdp/server/proxy/proxy_context.h> + +#include <freerdp/channels/drdynvc.h> +#include <freerdp/channels/rdpgfx.h> +#include <freerdp/utils/gfx.h> + +#define TAG MODULE_TAG("dyn-channel-dump") + +static constexpr char plugin_name[] = "dyn-channel-dump"; +static constexpr char plugin_desc[] = + "This plugin dumps configurable dynamic channel data to a file."; + +static const std::vector<std::string> plugin_static_intercept = { DRDYNVC_SVC_CHANNEL_NAME }; + +static constexpr char key_path[] = "path"; +static constexpr char key_channels[] = "channels"; + +class PluginData +{ + public: + PluginData(proxyPluginsManager* mgr) : _mgr(mgr), _sessionid(0) + { + } + + proxyPluginsManager* mgr() const + { + return _mgr; + } + + uint64_t session() + { + return _sessionid++; + } + + private: + proxyPluginsManager* _mgr; + uint64_t _sessionid; +}; + +class ChannelData +{ + public: + ChannelData(const std::string& base, const std::vector<std::string>& list, uint64_t sessionid) + : _base(base), _channels_to_dump(list), _session_id(sessionid) + { + char str[64] = {}; + _snprintf(str, sizeof(str), "session-%016" PRIx64, _session_id); + _base /= str; + } + + bool add(const std::string& name, bool back) + { + std::lock_guard<std::mutex> guard(_mux); + if (_map.find(name) == _map.end()) + { + WLog_INFO(TAG, "adding '%s' to dump list", name.c_str()); + _map.insert({ name, 0 }); + } + return true; + } + + std::ofstream stream(const std::string& name, bool back) + { + std::lock_guard<std::mutex> guard(_mux); + auto& atom = _map[name]; + auto count = atom++; + auto path = filepath(name, back, count); + WLog_DBG(TAG, "[%s] writing file '%s'", name.c_str(), path.c_str()); + return std::ofstream(path); + } + + bool dump_enabled(const std::string& name) const + { + if (name.empty()) + { + WLog_WARN(TAG, "empty dynamic channel name, skipping"); + return false; + } + + auto enabled = std::find(_channels_to_dump.begin(), _channels_to_dump.end(), name) != + _channels_to_dump.end(); + WLog_DBG(TAG, "channel '%s' dumping %s", name.c_str(), enabled ? "enabled" : "disabled"); + return enabled; + } + + bool ensure_path_exists() + { + if (!fs::exists(_base)) + { + if (!fs::create_directories(_base)) + { + WLog_ERR(TAG, "Failed to create dump directory %s", _base.c_str()); + return false; + } + } + else if (!fs::is_directory(_base)) + { + WLog_ERR(TAG, "dump path %s is not a directory", _base.c_str()); + return false; + } + return true; + } + + bool create() + { + if (!ensure_path_exists()) + return false; + + if (_channels_to_dump.empty()) + { + WLog_ERR(TAG, "Empty configuration entry [%s/%s], can not continue", plugin_name, + key_channels); + return false; + } + return true; + } + + uint64_t session() const + { + return _session_id; + } + + private: + fs::path filepath(const std::string& channel, bool back, uint64_t count) const + { + auto name = idstr(channel, back); + char cstr[32] = {}; + _snprintf(cstr, sizeof(cstr), "%016" PRIx64 "-", count); + auto path = _base / cstr; + path += name; + path += ".dump"; + return path; + } + + std::string idstr(const std::string& name, bool back) const + { + std::stringstream ss; + ss << name << "."; + if (back) + ss << "back"; + else + ss << "front"; + return ss.str(); + } + + private: + fs::path _base; + std::vector<std::string> _channels_to_dump; + + std::mutex _mux; + std::map<std::string, uint64_t> _map; + uint64_t _session_id; +}; + +static PluginData* dump_get_plugin_data(proxyPlugin* plugin) +{ + WINPR_ASSERT(plugin); + + auto plugindata = static_cast<PluginData*>(plugin->custom); + WINPR_ASSERT(plugindata); + return plugindata; +} + +static ChannelData* dump_get_plugin_data(proxyPlugin* plugin, proxyData* pdata) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + + auto plugindata = dump_get_plugin_data(plugin); + WINPR_ASSERT(plugindata); + + auto mgr = plugindata->mgr(); + WINPR_ASSERT(mgr); + + WINPR_ASSERT(mgr->GetPluginData); + return static_cast<ChannelData*>(mgr->GetPluginData(mgr, plugin_name, pdata)); +} + +static BOOL dump_set_plugin_data(proxyPlugin* plugin, proxyData* pdata, ChannelData* data) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + + auto plugindata = dump_get_plugin_data(plugin); + WINPR_ASSERT(plugindata); + + auto mgr = plugindata->mgr(); + WINPR_ASSERT(mgr); + + auto cdata = dump_get_plugin_data(plugin, pdata); + delete cdata; + + WINPR_ASSERT(mgr->SetPluginData); + return mgr->SetPluginData(mgr, plugin_name, pdata, data); +} + +static bool dump_channel_enabled(proxyPlugin* plugin, proxyData* pdata, const std::string& name) +{ + auto config = dump_get_plugin_data(plugin, pdata); + if (!config) + { + WLog_ERR(TAG, "Missing channel data"); + return false; + } + return config->dump_enabled(name); +} + +static BOOL dump_dyn_channel_intercept_list(proxyPlugin* plugin, proxyData* pdata, void* arg) +{ + auto data = static_cast<proxyChannelToInterceptData*>(arg); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(data); + + data->intercept = dump_channel_enabled(plugin, pdata, data->name); + if (data->intercept) + { + auto cdata = dump_get_plugin_data(plugin, pdata); + if (!cdata) + return FALSE; + + if (!cdata->add(data->name, false)) + { + WLog_ERR(TAG, "failed to create files for '%s'", data->name); + } + if (!cdata->add(data->name, true)) + { + WLog_ERR(TAG, "failed to create files for '%s'", data->name); + } + WLog_INFO(TAG, "Dumping channel '%s'", data->name); + } + return TRUE; +} + +static BOOL dump_static_channel_intercept_list(proxyPlugin* plugin, proxyData* pdata, void* arg) +{ + auto data = static_cast<proxyChannelToInterceptData*>(arg); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(data); + + auto intercept = std::find(plugin_static_intercept.begin(), plugin_static_intercept.end(), + data->name) != plugin_static_intercept.end(); + if (intercept) + { + WLog_INFO(TAG, "intercepting channel '%s'", data->name); + data->intercept = TRUE; + } + + return TRUE; +} + +static BOOL dump_dyn_channel_intercept(proxyPlugin* plugin, proxyData* pdata, void* arg) +{ + auto data = static_cast<proxyDynChannelInterceptData*>(arg); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(data); + + data->result = PF_CHANNEL_RESULT_PASS; + if (dump_channel_enabled(plugin, pdata, data->name)) + { + WLog_DBG(TAG, "intercepting channel '%s'", data->name); + auto cdata = dump_get_plugin_data(plugin, pdata); + if (!cdata) + { + WLog_ERR(TAG, "Missing channel data"); + return FALSE; + } + + if (!cdata->ensure_path_exists()) + return FALSE; + + auto stream = cdata->stream(data->name, data->isBackData); + auto buffer = reinterpret_cast<const char*>(Stream_ConstBuffer(data->data)); + if (!stream.is_open() || !stream.good()) + { + WLog_ERR(TAG, "Could not write to stream"); + return FALSE; + } + stream.write(buffer, Stream_Length(data->data)); + if (stream.fail()) + { + WLog_ERR(TAG, "Could not write to stream"); + return FALSE; + } + stream.flush(); + } + + return TRUE; +} + +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 BOOL dump_session_started(proxyPlugin* plugin, proxyData* pdata, void*) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + + auto custom = dump_get_plugin_data(plugin); + WINPR_ASSERT(custom); + + auto config = pdata->config; + WINPR_ASSERT(config); + + auto cpath = pf_config_get(config, plugin_name, key_path); + if (!cpath) + { + WLog_ERR(TAG, "Missing configuration entry [%s/%s], can not continue", plugin_name, + key_path); + return FALSE; + } + auto cchannels = pf_config_get(config, plugin_name, key_channels); + if (!cchannels) + { + WLog_ERR(TAG, "Missing configuration entry [%s/%s], can not continue", plugin_name, + key_channels); + return FALSE; + } + + std::string path(cpath); + std::string channels(cchannels); + std::vector<std::string> list = split(channels, "[;,]"); + auto cfg = new ChannelData(path, list, custom->session()); + if (!cfg || !cfg->create()) + { + delete cfg; + return FALSE; + } + + dump_set_plugin_data(plugin, pdata, cfg); + + WLog_DBG(TAG, "starting session dump %" PRIu64, cfg->session()); + return TRUE; +} + +static BOOL dump_session_end(proxyPlugin* plugin, proxyData* pdata, void*) +{ + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + + auto cfg = dump_get_plugin_data(plugin, pdata); + if (cfg) + WLog_DBG(TAG, "ending session dump %" PRIu64, cfg->session()); + dump_set_plugin_data(plugin, pdata, nullptr); + return TRUE; +} + +static BOOL dump_unload(proxyPlugin* plugin) +{ + if (!plugin) + return TRUE; + delete static_cast<PluginData*>(plugin->custom); + return TRUE; +} + +extern "C" FREERDP_API BOOL proxy_module_entry_point(proxyPluginsManager* plugins_manager, + void* userdata); + +BOOL proxy_module_entry_point(proxyPluginsManager* plugins_manager, void* userdata) +{ + proxyPlugin plugin = {}; + + plugin.name = plugin_name; + plugin.description = plugin_desc; + + plugin.PluginUnload = dump_unload; + plugin.ServerSessionStarted = dump_session_started; + plugin.ServerSessionEnd = dump_session_end; + + plugin.StaticChannelToIntercept = dump_static_channel_intercept_list; + plugin.DynChannelToIntercept = dump_dyn_channel_intercept_list; + plugin.DynChannelIntercept = dump_dyn_channel_intercept; + + plugin.custom = new PluginData(plugins_manager); + if (!plugin.custom) + return FALSE; + plugin.userdata = userdata; + + return plugins_manager->RegisterPlugin(plugins_manager, &plugin); +} diff --git a/server/proxy/pf_channel.c b/server/proxy/pf_channel.c new file mode 100644 index 0000000..a8f66c4 --- /dev/null +++ b/server/proxy/pf_channel.c @@ -0,0 +1,355 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * Copyright 2022 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/freerdp.h> +#include <freerdp/server/proxy/proxy_log.h> + +#include "proxy_modules.h" +#include "pf_channel.h" + +#define TAG PROXY_TAG("channel") + +/** @brief a tracker for channel packets */ +struct _ChannelStateTracker +{ + pServerStaticChannelContext* channel; + ChannelTrackerMode mode; + wStream* currentPacket; + size_t currentPacketReceived; + size_t currentPacketSize; + size_t currentPacketFragments; + + ChannelTrackerPeekFn peekFn; + void* trackerData; + proxyData* pdata; +}; + +static BOOL channelTracker_resetCurrentPacket(ChannelStateTracker* tracker) +{ + WINPR_ASSERT(tracker); + + BOOL create = TRUE; + if (tracker->currentPacket) + { + const size_t cap = Stream_Capacity(tracker->currentPacket); + if (cap < 1 * 1000 * 1000) + create = FALSE; + else + Stream_Free(tracker->currentPacket, TRUE); + } + + if (create) + tracker->currentPacket = Stream_New(NULL, 10 * 1024); + if (!tracker->currentPacket) + return FALSE; + Stream_SetPosition(tracker->currentPacket, 0); + return TRUE; +} + +ChannelStateTracker* channelTracker_new(pServerStaticChannelContext* channel, + ChannelTrackerPeekFn fn, void* data) +{ + ChannelStateTracker* ret = calloc(1, sizeof(ChannelStateTracker)); + if (!ret) + return ret; + + WINPR_ASSERT(fn); + + ret->channel = channel; + ret->peekFn = fn; + + if (!channelTracker_setCustomData(ret, data)) + goto fail; + + if (!channelTracker_resetCurrentPacket(ret)) + goto fail; + + return ret; + +fail: + WINPR_PRAGMA_DIAG_PUSH + WINPR_PRAGMA_DIAG_IGNORED_MISMATCHED_DEALLOC + channelTracker_free(ret); + WINPR_PRAGMA_DIAG_POP + return NULL; +} + +PfChannelResult channelTracker_update(ChannelStateTracker* tracker, const BYTE* xdata, size_t xsize, + UINT32 flags, size_t totalSize) +{ + PfChannelResult result = PF_CHANNEL_RESULT_ERROR; + BOOL firstPacket = (flags & CHANNEL_FLAG_FIRST); + BOOL lastPacket = (flags & CHANNEL_FLAG_LAST); + + WINPR_ASSERT(tracker); + + WLog_VRB(TAG, "channelTracker_update(%s): sz=%" PRIuz " first=%d last=%d", + tracker->channel->channel_name, xsize, firstPacket, lastPacket); + if (flags & CHANNEL_FLAG_FIRST) + { + if (!channelTracker_resetCurrentPacket(tracker)) + return FALSE; + channelTracker_setCurrentPacketSize(tracker, totalSize); + tracker->currentPacketReceived = 0; + tracker->currentPacketFragments = 0; + } + + { + const size_t currentPacketSize = channelTracker_getCurrentPacketSize(tracker); + if (tracker->currentPacketReceived + xsize > currentPacketSize) + WLog_INFO(TAG, "cumulated size is bigger (%" PRIuz ") than total size (%" PRIuz ")", + tracker->currentPacketReceived + xsize, currentPacketSize); + } + + tracker->currentPacketReceived += xsize; + tracker->currentPacketFragments++; + + switch (channelTracker_getMode(tracker)) + { + case CHANNEL_TRACKER_PEEK: + { + wStream* currentPacket = channelTracker_getCurrentPacket(tracker); + if (!Stream_EnsureRemainingCapacity(currentPacket, xsize)) + return PF_CHANNEL_RESULT_ERROR; + + Stream_Write(currentPacket, xdata, xsize); + + WINPR_ASSERT(tracker->peekFn); + result = tracker->peekFn(tracker, firstPacket, lastPacket); + } + break; + case CHANNEL_TRACKER_PASS: + result = PF_CHANNEL_RESULT_PASS; + break; + case CHANNEL_TRACKER_DROP: + result = PF_CHANNEL_RESULT_DROP; + break; + } + + if (lastPacket) + { + const size_t currentPacketSize = channelTracker_getCurrentPacketSize(tracker); + channelTracker_setMode(tracker, CHANNEL_TRACKER_PEEK); + + if (tracker->currentPacketReceived != currentPacketSize) + WLog_INFO(TAG, "cumulated size(%" PRIuz ") does not match total size (%" PRIuz ")", + tracker->currentPacketReceived, currentPacketSize); + } + + return result; +} + +void channelTracker_free(ChannelStateTracker* t) +{ + if (!t) + return; + + Stream_Free(t->currentPacket, TRUE); + free(t); +} + +/** + * Flushes the current accumulated tracker content, if it's the first packet, then + * when can just return that the packet shall be passed, otherwise to have to refragment + * the accumulated current packet. + */ + +PfChannelResult channelTracker_flushCurrent(ChannelStateTracker* t, BOOL first, BOOL last, + BOOL toBack) +{ + proxyData* pdata = NULL; + pServerContext* ps = NULL; + pServerStaticChannelContext* channel = NULL; + UINT32 flags = CHANNEL_FLAG_FIRST; + BOOL r = 0; + const char* direction = toBack ? "F->B" : "B->F"; + const size_t currentPacketSize = channelTracker_getCurrentPacketSize(t); + wStream* currentPacket = channelTracker_getCurrentPacket(t); + + WINPR_ASSERT(t); + + WLog_VRB(TAG, "channelTracker_flushCurrent(%s): %s sz=%" PRIuz " first=%d last=%d", + t->channel->channel_name, direction, Stream_GetPosition(currentPacket), first, last); + + if (first) + return PF_CHANNEL_RESULT_PASS; + + pdata = t->pdata; + channel = t->channel; + if (last) + flags |= CHANNEL_FLAG_LAST; + + if (toBack) + { + proxyChannelDataEventInfo ev; + + ev.channel_id = channel->front_channel_id; + ev.channel_name = channel->channel_name; + ev.data = Stream_Buffer(currentPacket); + ev.data_len = Stream_GetPosition(currentPacket); + ev.flags = flags; + ev.total_size = currentPacketSize; + + if (!pdata->pc->sendChannelData) + return PF_CHANNEL_RESULT_ERROR; + + return pdata->pc->sendChannelData(pdata->pc, &ev) ? PF_CHANNEL_RESULT_DROP + : PF_CHANNEL_RESULT_ERROR; + } + + ps = pdata->ps; + r = ps->context.peer->SendChannelPacket(ps->context.peer, channel->front_channel_id, + currentPacketSize, flags, Stream_Buffer(currentPacket), + Stream_GetPosition(currentPacket)); + + return r ? PF_CHANNEL_RESULT_DROP : PF_CHANNEL_RESULT_ERROR; +} + +static PfChannelResult pf_channel_generic_back_data(proxyData* pdata, + const pServerStaticChannelContext* channel, + const BYTE* xdata, size_t xsize, UINT32 flags, + size_t totalSize) +{ + proxyChannelDataEventInfo ev = { 0 }; + + WINPR_ASSERT(pdata); + WINPR_ASSERT(channel); + + switch (channel->channelMode) + { + case PF_UTILS_CHANNEL_PASSTHROUGH: + ev.channel_id = channel->back_channel_id; + ev.channel_name = channel->channel_name; + ev.data = xdata; + ev.data_len = xsize; + ev.flags = flags; + ev.total_size = totalSize; + + if (!pf_modules_run_filter(pdata->module, FILTER_TYPE_CLIENT_PASSTHROUGH_CHANNEL_DATA, + pdata, &ev)) + return PF_CHANNEL_RESULT_DROP; /* Silently drop */ + + return PF_CHANNEL_RESULT_PASS; + + case PF_UTILS_CHANNEL_INTERCEPT: + /* TODO */ + case PF_UTILS_CHANNEL_BLOCK: + default: + return PF_CHANNEL_RESULT_DROP; + } +} + +static PfChannelResult pf_channel_generic_front_data(proxyData* pdata, + const pServerStaticChannelContext* channel, + const BYTE* xdata, size_t xsize, UINT32 flags, + size_t totalSize) +{ + proxyChannelDataEventInfo ev = { 0 }; + + WINPR_ASSERT(pdata); + WINPR_ASSERT(channel); + + switch (channel->channelMode) + { + case PF_UTILS_CHANNEL_PASSTHROUGH: + ev.channel_id = channel->front_channel_id; + ev.channel_name = channel->channel_name; + ev.data = xdata; + ev.data_len = xsize; + ev.flags = flags; + ev.total_size = totalSize; + + if (!pf_modules_run_filter(pdata->module, FILTER_TYPE_SERVER_PASSTHROUGH_CHANNEL_DATA, + pdata, &ev)) + return PF_CHANNEL_RESULT_DROP; /* Silently drop */ + + return PF_CHANNEL_RESULT_PASS; + + case PF_UTILS_CHANNEL_INTERCEPT: + /* TODO */ + case PF_UTILS_CHANNEL_BLOCK: + default: + return PF_CHANNEL_RESULT_DROP; + } +} + +BOOL pf_channel_setup_generic(pServerStaticChannelContext* channel) +{ + channel->onBackData = pf_channel_generic_back_data; + channel->onFrontData = pf_channel_generic_front_data; + return TRUE; +} + +BOOL channelTracker_setMode(ChannelStateTracker* tracker, ChannelTrackerMode mode) +{ + WINPR_ASSERT(tracker); + tracker->mode = mode; + return TRUE; +} + +ChannelTrackerMode channelTracker_getMode(ChannelStateTracker* tracker) +{ + WINPR_ASSERT(tracker); + return tracker->mode; +} + +BOOL channelTracker_setPData(ChannelStateTracker* tracker, proxyData* pdata) +{ + WINPR_ASSERT(tracker); + tracker->pdata = pdata; + return TRUE; +} + +proxyData* channelTracker_getPData(ChannelStateTracker* tracker) +{ + WINPR_ASSERT(tracker); + return tracker->pdata; +} + +wStream* channelTracker_getCurrentPacket(ChannelStateTracker* tracker) +{ + WINPR_ASSERT(tracker); + return tracker->currentPacket; +} + +BOOL channelTracker_setCustomData(ChannelStateTracker* tracker, void* data) +{ + WINPR_ASSERT(tracker); + tracker->trackerData = data; + return TRUE; +} + +void* channelTracker_getCustomData(ChannelStateTracker* tracker) +{ + WINPR_ASSERT(tracker); + return tracker->trackerData; +} + +size_t channelTracker_getCurrentPacketSize(ChannelStateTracker* tracker) +{ + WINPR_ASSERT(tracker); + return tracker->currentPacketSize; +} + +BOOL channelTracker_setCurrentPacketSize(ChannelStateTracker* tracker, size_t size) +{ + WINPR_ASSERT(tracker); + tracker->currentPacketSize = size; + return TRUE; +} diff --git a/server/proxy/pf_channel.h b/server/proxy/pf_channel.h new file mode 100644 index 0000000..9e0da7e --- /dev/null +++ b/server/proxy/pf_channel.h @@ -0,0 +1,64 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * + * Copyright 2022 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 SERVER_PROXY_PF_CHANNEL_H_ +#define SERVER_PROXY_PF_CHANNEL_H_ + +#include <freerdp/server/proxy/proxy_context.h> + +/** @brief operating mode of a channel tracker */ +typedef enum +{ + CHANNEL_TRACKER_PEEK, /*!< inquiring content, accumulating packet fragments */ + CHANNEL_TRACKER_PASS, /*!< pass all the fragments of the current packet */ + CHANNEL_TRACKER_DROP /*!< drop all the fragments of the current packet */ +} ChannelTrackerMode; + +typedef struct _ChannelStateTracker ChannelStateTracker; +typedef PfChannelResult (*ChannelTrackerPeekFn)(ChannelStateTracker* tracker, BOOL first, + BOOL lastPacket); + +void channelTracker_free(ChannelStateTracker* t); + +WINPR_ATTR_MALLOC(channelTracker_free, 1) +ChannelStateTracker* channelTracker_new(pServerStaticChannelContext* channel, + ChannelTrackerPeekFn fn, void* data); + +BOOL channelTracker_setMode(ChannelStateTracker* tracker, ChannelTrackerMode mode); +ChannelTrackerMode channelTracker_getMode(ChannelStateTracker* tracker); + +BOOL channelTracker_setPData(ChannelStateTracker* tracker, proxyData* pdata); +proxyData* channelTracker_getPData(ChannelStateTracker* tracker); + +BOOL channelTracker_setCustomData(ChannelStateTracker* tracker, void* data); +void* channelTracker_getCustomData(ChannelStateTracker* tracker); + +wStream* channelTracker_getCurrentPacket(ChannelStateTracker* tracker); + +size_t channelTracker_getCurrentPacketSize(ChannelStateTracker* tracker); +BOOL channelTracker_setCurrentPacketSize(ChannelStateTracker* tracker, size_t size); + +PfChannelResult channelTracker_update(ChannelStateTracker* tracker, const BYTE* xdata, size_t xsize, + UINT32 flags, size_t totalSize); + +PfChannelResult channelTracker_flushCurrent(ChannelStateTracker* t, BOOL first, BOOL last, + BOOL toFront); + +BOOL pf_channel_setup_generic(pServerStaticChannelContext* channel); + +#endif /* SERVER_PROXY_PF_CHANNEL_H_ */ diff --git a/server/proxy/pf_client.c b/server/proxy/pf_client.c new file mode 100644 index 0000000..2f34f97 --- /dev/null +++ b/server/proxy/pf_client.c @@ -0,0 +1,1102 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2019 Mati Shabtay <matishabtay@gmail.com> + * Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com> + * Copyright 2019 Idan Freiberg <speidy@gmail.com> + * Copyright 2021 Armin Novak <anovak@thincast.com> + * Copyright 2021 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 <freerdp/freerdp.h> +#include <freerdp/gdi/gdi.h> +#include <freerdp/client/cmdline.h> + +#include <freerdp/server/proxy/proxy_log.h> +#include <freerdp/channels/drdynvc.h> +#include <freerdp/channels/encomsp.h> +#include <freerdp/channels/rdpdr.h> +#include <freerdp/channels/rdpsnd.h> +#include <freerdp/channels/cliprdr.h> +#include <freerdp/channels/channels.h> + +#include "pf_client.h" +#include "pf_channel.h" +#include <freerdp/server/proxy/proxy_context.h> +#include "pf_update.h" +#include "pf_input.h" +#include <freerdp/server/proxy/proxy_config.h> +#include "proxy_modules.h" +#include "pf_utils.h" +#include "channels/pf_channel_rdpdr.h" +#include "channels/pf_channel_smartcard.h" + +#define TAG PROXY_TAG("client") + +static void channel_data_free(void* obj); +static BOOL proxy_server_reactivate(rdpContext* ps, const rdpContext* pc) +{ + WINPR_ASSERT(ps); + WINPR_ASSERT(pc); + + if (!pf_context_copy_settings(ps->settings, pc->settings)) + return FALSE; + + /* + * DesktopResize causes internal function rdp_server_reactivate to be called, + * which causes the reactivation. + */ + WINPR_ASSERT(ps->update); + if (!ps->update->DesktopResize(ps)) + return FALSE; + + return TRUE; +} + +static void pf_client_on_error_info(void* ctx, const ErrorInfoEventArgs* e) +{ + pClientContext* pc = (pClientContext*)ctx; + pServerContext* ps = NULL; + + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->pdata); + WINPR_ASSERT(e); + ps = pc->pdata->ps; + WINPR_ASSERT(ps); + + if (e->code == ERRINFO_NONE) + return; + + PROXY_LOG_WARN(TAG, pc, "received ErrorInfo PDU. code=0x%08" PRIu32 ", message: %s", e->code, + freerdp_get_error_info_string(e->code)); + + /* forward error back to client */ + freerdp_set_error_info(ps->context.rdp, e->code); + freerdp_send_error_info(ps->context.rdp); +} + +static void pf_client_on_activated(void* ctx, const ActivatedEventArgs* e) +{ + pClientContext* pc = (pClientContext*)ctx; + pServerContext* ps = NULL; + freerdp_peer* peer = NULL; + + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->pdata); + WINPR_ASSERT(e); + + ps = pc->pdata->ps; + WINPR_ASSERT(ps); + peer = ps->context.peer; + WINPR_ASSERT(peer); + WINPR_ASSERT(peer->context); + + PROXY_LOG_INFO(TAG, pc, "client activated, registering server input callbacks"); + + /* Register server input/update callbacks only after proxy client is fully activated */ + pf_server_register_input_callbacks(peer->context->input); + pf_server_register_update_callbacks(peer->context->update); +} + +static BOOL pf_client_load_rdpsnd(pClientContext* pc) +{ + rdpContext* context = (rdpContext*)pc; + pServerContext* ps = NULL; + const proxyConfig* config = NULL; + + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->pdata); + ps = pc->pdata->ps; + WINPR_ASSERT(ps); + config = pc->pdata->config; + WINPR_ASSERT(config); + + /* + * if AudioOutput is enabled in proxy and client connected with rdpsnd, use proxy as rdpsnd + * backend. Otherwise, use sys:fake. + */ + if (!freerdp_static_channel_collection_find(context->settings, RDPSND_CHANNEL_NAME)) + { + const char* params[2] = { RDPSND_CHANNEL_NAME, "sys:fake" }; + + if (!freerdp_client_add_static_channel(context->settings, ARRAYSIZE(params), params)) + return FALSE; + } + + return TRUE; +} + +static BOOL pf_client_use_peer_load_balance_info(pClientContext* pc) +{ + pServerContext* ps = NULL; + rdpSettings* settings = NULL; + DWORD lb_info_len = 0; + const char* lb_info = NULL; + + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->pdata); + ps = pc->pdata->ps; + WINPR_ASSERT(ps); + settings = pc->context.settings; + WINPR_ASSERT(settings); + + lb_info = freerdp_nego_get_routing_token(&ps->context, &lb_info_len); + if (!lb_info) + return TRUE; + + return freerdp_settings_set_pointer_len(settings, FreeRDP_LoadBalanceInfo, lb_info, + lb_info_len); +} + +static BOOL str_is_empty(const char* str) +{ + if (!str) + return TRUE; + if (strlen(str) == 0) + return TRUE; + return FALSE; +} + +static BOOL pf_client_use_proxy_smartcard_auth(const rdpSettings* settings) +{ + BOOL enable = freerdp_settings_get_bool(settings, FreeRDP_SmartcardLogon); + const char* key = freerdp_settings_get_string(settings, FreeRDP_SmartcardPrivateKey); + const char* cert = freerdp_settings_get_string(settings, FreeRDP_SmartcardCertificate); + + if (!enable) + return FALSE; + + if (str_is_empty(key)) + return FALSE; + + if (str_is_empty(cert)) + return FALSE; + + return TRUE; +} + +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_INFO(TAG, "loading channelEx %s", name); + return TRUE; + } + } + else if (entry) + { + if (freerdp_channels_client_load(channels, settings, entry, data) == 0) + { + WLog_INFO(TAG, "loading channel %s", name); + return TRUE; + } + } + + return FALSE; +} + +static BOOL pf_client_pre_connect(freerdp* instance) +{ + pClientContext* pc = NULL; + pServerContext* ps = NULL; + const proxyConfig* config = NULL; + rdpSettings* settings = NULL; + + WINPR_ASSERT(instance); + pc = (pClientContext*)instance->context; + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->pdata); + ps = pc->pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->pdata); + config = ps->pdata->config; + WINPR_ASSERT(config); + settings = instance->context->settings; + WINPR_ASSERT(settings); + + /* + * as the client's settings are copied from the server's, GlyphSupportLevel might not be + * GLYPH_SUPPORT_NONE. the proxy currently do not support GDI & GLYPH_SUPPORT_CACHE, so + * GlyphCacheSupport must be explicitly set to GLYPH_SUPPORT_NONE. + * + * Also, OrderSupport need to be zeroed, because it is currently not supported. + */ + if (!freerdp_settings_set_uint32(settings, FreeRDP_GlyphSupportLevel, GLYPH_SUPPORT_NONE)) + return FALSE; + + void* OrderSupport = freerdp_settings_get_pointer_writable(settings, FreeRDP_OrderSupport); + ZeroMemory(OrderSupport, 32); + + if (WTSVirtualChannelManagerIsChannelJoined(ps->vcm, DRDYNVC_SVC_CHANNEL_NAME)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportDynamicChannels, TRUE)) + return FALSE; + } + + /* Multimon */ + if (!freerdp_settings_set_bool(settings, FreeRDP_UseMultimon, TRUE)) + return FALSE; + + /* Sound */ + if (!freerdp_settings_set_bool(settings, FreeRDP_AudioCapture, config->AudioInput) || + !freerdp_settings_set_bool(settings, FreeRDP_AudioPlayback, config->AudioOutput) || + !freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection, + config->DeviceRedirection) || + !freerdp_settings_set_bool(settings, FreeRDP_SupportDisplayControl, + config->DisplayControl) || + !freerdp_settings_set_bool(settings, FreeRDP_MultiTouchInput, config->Multitouch)) + return FALSE; + + if (config->RemoteApp) + { + if (WTSVirtualChannelManagerIsChannelJoined(ps->vcm, RAIL_SVC_CHANNEL_NAME)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteApplicationMode, TRUE)) + return FALSE; + } + } + + if (config->DeviceRedirection) + { + if (WTSVirtualChannelManagerIsChannelJoined(ps->vcm, RDPDR_SVC_CHANNEL_NAME)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_DeviceRedirection, TRUE)) + return FALSE; + } + } + + /* Display control */ + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportDisplayControl, config->DisplayControl)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_DynamicResolutionUpdate, + config->DisplayControl)) + return FALSE; + + if (WTSVirtualChannelManagerIsChannelJoined(ps->vcm, ENCOMSP_SVC_CHANNEL_NAME)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_EncomspVirtualChannel, TRUE)) + return FALSE; + } + + if (config->Clipboard) + { + if (WTSVirtualChannelManagerIsChannelJoined(ps->vcm, CLIPRDR_SVC_CHANNEL_NAME)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectClipboard, config->Clipboard)) + return FALSE; + } + } + + if (!freerdp_settings_set_bool(settings, FreeRDP_AutoReconnectionEnabled, TRUE)) + return FALSE; + + PubSub_SubscribeErrorInfo(instance->context->pubSub, pf_client_on_error_info); + PubSub_SubscribeActivated(instance->context->pubSub, pf_client_on_activated); + if (!pf_client_use_peer_load_balance_info(pc)) + return FALSE; + + return pf_modules_run_hook(pc->pdata->module, HOOK_TYPE_CLIENT_PRE_CONNECT, pc->pdata, pc); +} + +/** @brief arguments for updateBackIdFn */ +typedef struct +{ + pServerContext* ps; + const char* name; + UINT32 backId; +} UpdateBackIdArgs; + +static BOOL updateBackIdFn(const void* key, void* value, void* arg) +{ + pServerStaticChannelContext* current = (pServerStaticChannelContext*)value; + UpdateBackIdArgs* updateArgs = (UpdateBackIdArgs*)arg; + + if (strcmp(updateArgs->name, current->channel_name) != 0) + return TRUE; + + current->back_channel_id = updateArgs->backId; + if (!HashTable_Insert(updateArgs->ps->channelsByBackId, ¤t->back_channel_id, current)) + { + WLog_ERR(TAG, "error inserting channel in channelsByBackId table"); + } + return FALSE; +} + +static BOOL pf_client_update_back_id(pServerContext* ps, const char* name, UINT32 backId) +{ + UpdateBackIdArgs res = { ps, name, backId }; + + return HashTable_Foreach(ps->channelsByFrontId, updateBackIdFn, &res) == FALSE; +} + +static BOOL pf_client_load_channels(freerdp* instance) +{ + pClientContext* pc = NULL; + pServerContext* ps = NULL; + const proxyConfig* config = NULL; + rdpSettings* settings = NULL; + + WINPR_ASSERT(instance); + pc = (pClientContext*)instance->context; + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->pdata); + ps = pc->pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->pdata); + config = ps->pdata->config; + WINPR_ASSERT(config); + settings = instance->context->settings; + WINPR_ASSERT(settings); + /** + * Load all required plugins / channels / libraries specified by current + * settings. + */ + PROXY_LOG_INFO(TAG, pc, "Loading addins"); + + if (!pf_client_load_rdpsnd(pc)) + { + PROXY_LOG_ERR(TAG, pc, "Failed to load rdpsnd client"); + return FALSE; + } + + if (!pf_utils_is_passthrough(config)) + { + if (!freerdp_client_load_addins(instance->context->channels, settings)) + { + PROXY_LOG_ERR(TAG, pc, "Failed to load addins"); + return FALSE; + } + } + else + { + if (!pf_channel_rdpdr_client_new(pc)) + return FALSE; +#if defined(WITH_PROXY_EMULATE_SMARTCARD) + if (!pf_channel_smartcard_client_new(pc)) + return FALSE; +#endif + /* Copy the current channel settings from the peer connection to the client. */ + if (!freerdp_channels_from_mcs(settings, &ps->context)) + return FALSE; + + /* Filter out channels we do not want */ + { + CHANNEL_DEF* channels = (CHANNEL_DEF*)freerdp_settings_get_pointer_array_writable( + settings, FreeRDP_ChannelDefArray, 0); + size_t size = freerdp_settings_get_uint32(settings, FreeRDP_ChannelCount); + UINT32 id = MCS_GLOBAL_CHANNEL_ID + 1; + + WINPR_ASSERT(channels || (size == 0)); + + size_t x = 0; + for (; x < size;) + { + CHANNEL_DEF* cur = &channels[x]; + proxyChannelDataEventInfo dev = { 0 }; + + dev.channel_name = cur->name; + dev.flags = cur->options; + + /* Filter out channels blocked by config */ + if (!pf_modules_run_filter(pc->pdata->module, + FILTER_TYPE_CLIENT_PASSTHROUGH_CHANNEL_CREATE, pc->pdata, + &dev)) + { + const size_t s = size - MIN(size, x + 1); + memmove(cur, &cur[1], sizeof(CHANNEL_DEF) * s); + size--; + } + else + { + if (!pf_client_update_back_id(ps, cur->name, id++)) + { + WLog_ERR(TAG, "unable to update backid for channel %s", cur->name); + return FALSE; + } + x++; + } + } + + if (!freerdp_settings_set_uint32(settings, FreeRDP_ChannelCount, x)) + return FALSE; + } + } + return pf_modules_run_hook(pc->pdata->module, HOOK_TYPE_CLIENT_LOAD_CHANNELS, pc->pdata, pc); +} + +static BOOL pf_client_receive_channel_data_hook(freerdp* instance, UINT16 channelId, + const BYTE* xdata, size_t xsize, UINT32 flags, + size_t totalSize) +{ + pClientContext* pc = NULL; + pServerContext* ps = NULL; + proxyData* pdata = NULL; + pServerStaticChannelContext* channel = NULL; + UINT64 channelId64 = channelId; + + WINPR_ASSERT(instance); + WINPR_ASSERT(xdata || (xsize == 0)); + + pc = (pClientContext*)instance->context; + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->pdata); + + ps = pc->pdata->ps; + WINPR_ASSERT(ps); + + pdata = ps->pdata; + WINPR_ASSERT(pdata); + + channel = HashTable_GetItemValue(ps->channelsByBackId, &channelId64); + if (!channel) + return TRUE; + + WINPR_ASSERT(channel->onBackData); + switch (channel->onBackData(pdata, channel, xdata, xsize, flags, totalSize)) + { + case PF_CHANNEL_RESULT_PASS: + /* Ignore messages for channels that can not be mapped. + * The client might not have enabled support for this specific channel, + * so just drop the message. */ + if (channel->front_channel_id == 0) + return TRUE; + + return ps->context.peer->SendChannelPacket(ps->context.peer, channel->front_channel_id, + totalSize, flags, xdata, xsize); + case PF_CHANNEL_RESULT_DROP: + return TRUE; + case PF_CHANNEL_RESULT_ERROR: + default: + return FALSE; + } +} + +static BOOL pf_client_on_server_heartbeat(freerdp* instance, BYTE period, BYTE count1, BYTE count2) +{ + pClientContext* pc = NULL; + pServerContext* ps = NULL; + + WINPR_ASSERT(instance); + pc = (pClientContext*)instance->context; + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->pdata); + ps = pc->pdata->ps; + WINPR_ASSERT(ps); + + return freerdp_heartbeat_send_heartbeat_pdu(ps->context.peer, period, count1, count2); +} + +static BOOL pf_client_send_channel_data(pClientContext* pc, const proxyChannelDataEventInfo* ev) +{ + WINPR_ASSERT(pc); + WINPR_ASSERT(ev); + + return Queue_Enqueue(pc->cached_server_channel_data, ev); +} + +static BOOL sendQueuedChannelData(pClientContext* pc) +{ + BOOL rc = TRUE; + + WINPR_ASSERT(pc); + + if (pc->connected) + { + proxyChannelDataEventInfo* ev = NULL; + + Queue_Lock(pc->cached_server_channel_data); + while (rc && (ev = Queue_Dequeue(pc->cached_server_channel_data))) + { + UINT16 channelId = 0; + WINPR_ASSERT(pc->context.instance); + + channelId = freerdp_channels_get_id_by_name(pc->context.instance, ev->channel_name); + /* Ignore unmappable channels */ + if ((channelId == 0) || (channelId == UINT16_MAX)) + rc = TRUE; + else + { + WINPR_ASSERT(pc->context.instance->SendChannelPacket); + rc = pc->context.instance->SendChannelPacket(pc->context.instance, channelId, + ev->total_size, ev->flags, ev->data, + ev->data_len); + } + channel_data_free(ev); + } + + Queue_Unlock(pc->cached_server_channel_data); + } + + return rc; +} + +/** + * Called after a RDP connection was successfully established. + * Settings might have changed during negotiation of client / server feature + * support. + * + * Set up local framebuffers and painting callbacks. + * If required, register pointer callbacks to change the local mouse cursor + * when hovering over the RDP window + */ +static BOOL pf_client_post_connect(freerdp* instance) +{ + rdpContext* context = NULL; + rdpSettings* settings = NULL; + rdpUpdate* update = NULL; + rdpContext* ps = NULL; + pClientContext* pc = NULL; + const proxyConfig* config = NULL; + + WINPR_ASSERT(instance); + context = instance->context; + WINPR_ASSERT(context); + settings = context->settings; + WINPR_ASSERT(settings); + update = context->update; + WINPR_ASSERT(update); + pc = (pClientContext*)context; + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->pdata); + ps = (rdpContext*)pc->pdata->ps; + WINPR_ASSERT(ps); + config = pc->pdata->config; + WINPR_ASSERT(config); + + if (!pf_modules_run_hook(pc->pdata->module, HOOK_TYPE_CLIENT_POST_CONNECT, pc->pdata, pc)) + return FALSE; + + if (!gdi_init(instance, PIXEL_FORMAT_BGRA32)) + return FALSE; + + WINPR_ASSERT(freerdp_settings_get_bool(settings, FreeRDP_SoftwareGdi)); + + pf_client_register_update_callbacks(update); + + /* virtual channels receive data hook */ + pc->client_receive_channel_data_original = instance->ReceiveChannelData; + instance->ReceiveChannelData = pf_client_receive_channel_data_hook; + + instance->heartbeat->ServerHeartbeat = pf_client_on_server_heartbeat; + + pc->connected = TRUE; + + /* Send cached channel data */ + sendQueuedChannelData(pc); + + /* + * after the connection fully established and settings were negotiated with target server, + * send a reactivation sequence to the client with the negotiated settings. This way, + * settings are synchorinized between proxy's peer and and remote target. + */ + return proxy_server_reactivate(ps, context); +} + +/* This function is called whether a session ends by failure or success. + * Clean up everything allocated by pre_connect and post_connect. + */ +static void pf_client_post_disconnect(freerdp* instance) +{ + pClientContext* pc = NULL; + proxyData* pdata = NULL; + + if (!instance) + return; + + if (!instance->context) + return; + + pc = (pClientContext*)instance->context; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + +#if defined(WITH_PROXY_EMULATE_SMARTCARD) + pf_channel_smartcard_client_free(pc); +#endif + + pf_channel_rdpdr_client_free(pc); + + pc->connected = FALSE; + pf_modules_run_hook(pc->pdata->module, HOOK_TYPE_CLIENT_POST_DISCONNECT, pc->pdata, pc); + + PubSub_UnsubscribeErrorInfo(instance->context->pubSub, pf_client_on_error_info); + gdi_free(instance); + + /* Only close the connection if NLA fallback process is done */ + if (!pc->allow_next_conn_failure) + proxy_data_abort_connect(pdata); +} + +static BOOL pf_client_redirect(freerdp* instance) +{ + pClientContext* pc = NULL; + proxyData* pdata = NULL; + + if (!instance) + return FALSE; + + if (!instance->context) + return FALSE; + + pc = (pClientContext*)instance->context; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + +#if defined(WITH_PROXY_EMULATE_SMARTCARD) + pf_channel_smartcard_client_reset(pc); +#endif + pf_channel_rdpdr_client_reset(pc); + + return pf_modules_run_hook(pc->pdata->module, HOOK_TYPE_CLIENT_REDIRECT, pc->pdata, pc); +} + +/* + * pf_client_should_retry_without_nla: + * + * returns TRUE if in case of connection failure, the client should try again without NLA. + * Otherwise, returns FALSE. + */ +static BOOL pf_client_should_retry_without_nla(pClientContext* pc) +{ + rdpSettings* settings = NULL; + const proxyConfig* config = NULL; + + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->pdata); + settings = pc->context.settings; + WINPR_ASSERT(settings); + config = pc->pdata->config; + WINPR_ASSERT(config); + + if (!config->ClientAllowFallbackToTls || + !freerdp_settings_get_bool(settings, FreeRDP_NlaSecurity)) + return FALSE; + + return config->ClientTlsSecurity || config->ClientRdpSecurity; +} + +static void pf_client_set_security_settings(pClientContext* pc) +{ + rdpSettings* settings = NULL; + const proxyConfig* config = NULL; + + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->pdata); + settings = pc->context.settings; + WINPR_ASSERT(settings); + config = pc->pdata->config; + WINPR_ASSERT(config); + + freerdp_settings_set_bool(settings, FreeRDP_RdpSecurity, config->ClientRdpSecurity); + freerdp_settings_set_bool(settings, FreeRDP_TlsSecurity, config->ClientTlsSecurity); + freerdp_settings_set_bool(settings, FreeRDP_NlaSecurity, config->ClientNlaSecurity); + + if (pf_client_use_proxy_smartcard_auth(settings)) + { + /* Smartcard authentication requires smartcard redirection to be enabled */ + freerdp_settings_set_bool(settings, FreeRDP_RedirectSmartCards, TRUE); + + /* Reset username/domain, we will get that info later from the sc cert */ + freerdp_settings_set_string(settings, FreeRDP_Username, NULL); + freerdp_settings_set_string(settings, FreeRDP_Domain, NULL); + } +} + +static BOOL pf_client_connect_without_nla(pClientContext* pc) +{ + freerdp* instance = NULL; + rdpSettings* settings = NULL; + + WINPR_ASSERT(pc); + instance = pc->context.instance; + WINPR_ASSERT(instance); + + if (!freerdp_context_reset(instance)) + return FALSE; + + settings = pc->context.settings; + WINPR_ASSERT(settings); + + /* If already disabled abort early. */ + if (!freerdp_settings_get_bool(settings, FreeRDP_NlaSecurity)) + return FALSE; + + /* disable NLA */ + if (!freerdp_settings_set_bool(settings, FreeRDP_NlaSecurity, FALSE)) + return FALSE; + + /* do not allow next connection failure */ + pc->allow_next_conn_failure = FALSE; + return freerdp_connect(instance); +} + +static BOOL pf_client_connect(freerdp* instance) +{ + pClientContext* pc = NULL; + rdpSettings* settings = NULL; + BOOL rc = FALSE; + BOOL retry = FALSE; + + WINPR_ASSERT(instance); + pc = (pClientContext*)instance->context; + WINPR_ASSERT(pc); + settings = instance->context->settings; + WINPR_ASSERT(settings); + + PROXY_LOG_INFO(TAG, pc, "connecting using client info: Username: %s, Domain: %s", + freerdp_settings_get_string(settings, FreeRDP_Username), + freerdp_settings_get_string(settings, FreeRDP_Domain)); + + pf_client_set_security_settings(pc); + if (pf_client_should_retry_without_nla(pc)) + retry = pc->allow_next_conn_failure = TRUE; + + PROXY_LOG_INFO(TAG, pc, "connecting using security settings: rdp=%d, tls=%d, nla=%d", + freerdp_settings_get_bool(settings, FreeRDP_RdpSecurity), + freerdp_settings_get_bool(settings, FreeRDP_TlsSecurity), + freerdp_settings_get_bool(settings, FreeRDP_NlaSecurity)); + + if (!freerdp_connect(instance)) + { + if (!pf_modules_run_hook(pc->pdata->module, HOOK_TYPE_CLIENT_LOGIN_FAILURE, pc->pdata, pc)) + goto out; + + if (!retry) + goto out; + + PROXY_LOG_ERR(TAG, pc, "failed to connect with NLA. retrying to connect without NLA"); + if (!pf_client_connect_without_nla(pc)) + { + PROXY_LOG_ERR(TAG, pc, "pf_client_connect_without_nla failed!"); + goto out; + } + } + + rc = TRUE; +out: + pc->allow_next_conn_failure = FALSE; + return rc; +} + +/** + * RDP main loop. + * Connects RDP, loops while running and handles event and dispatch, cleans up + * after the connection ends. + */ +static DWORD WINAPI pf_client_thread_proc(pClientContext* pc) +{ + freerdp* instance = NULL; + proxyData* pdata = NULL; + DWORD nCount = 0; + DWORD status = 0; + HANDLE handles[MAXIMUM_WAIT_OBJECTS] = { 0 }; + + WINPR_ASSERT(pc); + + instance = pc->context.instance; + WINPR_ASSERT(instance); + + pdata = pc->pdata; + WINPR_ASSERT(pdata); + /* + * during redirection, freerdp's abort event might be overriden (reset) by the library, after + * the server set it in order to shutdown the connection. it means that the server might signal + * the client to abort, but the library code will override the signal and the client will + * continue its work instead of exiting. That's why the client must wait on `pdata->abort_event` + * too, which will never be modified by the library. + */ + handles[nCount++] = pdata->abort_event; + + if (!pf_modules_run_hook(pdata->module, HOOK_TYPE_CLIENT_INIT_CONNECT, pdata, pc)) + { + proxy_data_abort_connect(pdata); + goto end; + } + + if (!pf_client_connect(instance)) + { + proxy_data_abort_connect(pdata); + goto end; + } + handles[nCount++] = Queue_Event(pc->cached_server_channel_data); + + while (!freerdp_shall_disconnect_context(instance->context)) + { + UINT32 tmp = freerdp_get_event_handles(instance->context, &handles[nCount], + ARRAYSIZE(handles) - nCount); + + if (tmp == 0) + { + PROXY_LOG_ERR(TAG, pc, "freerdp_get_event_handles failed!"); + break; + } + + status = WaitForMultipleObjects(nCount + tmp, handles, FALSE, INFINITE); + + if (status == WAIT_FAILED) + { + WLog_ERR(TAG, "WaitForMultipleObjects failed with %" PRIu32 "", status); + break; + } + + /* abort_event triggered */ + if (status == WAIT_OBJECT_0) + break; + + if (freerdp_shall_disconnect_context(instance->context)) + break; + + if (proxy_data_shall_disconnect(pdata)) + 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; + } + sendQueuedChannelData(pc); + } + + freerdp_disconnect(instance); + +end: + pf_modules_run_hook(pdata->module, HOOK_TYPE_CLIENT_UNINIT_CONNECT, pdata, pc); + + return 0; +} + +static int pf_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 void pf_client_context_free(freerdp* instance, rdpContext* context) +{ + pClientContext* pc = (pClientContext*)context; + WINPR_UNUSED(instance); + + if (!pc) + return; + + pc->sendChannelData = NULL; + Queue_Free(pc->cached_server_channel_data); + Stream_Free(pc->remote_pem, TRUE); + free(pc->remote_hostname); + free(pc->computerName.v); + HashTable_Free(pc->interceptContextMap); +} + +static int pf_client_verify_X509_certificate(freerdp* instance, const BYTE* data, size_t length, + const char* hostname, UINT16 port, DWORD flags) +{ + pClientContext* pc = NULL; + + WINPR_ASSERT(instance); + WINPR_ASSERT(data); + WINPR_ASSERT(length > 0); + WINPR_ASSERT(hostname); + + pc = (pClientContext*)instance->context; + WINPR_ASSERT(pc); + + if (!Stream_EnsureCapacity(pc->remote_pem, length)) + return 0; + Stream_SetPosition(pc->remote_pem, 0); + + free(pc->remote_hostname); + pc->remote_hostname = NULL; + + if (length > 0) + Stream_Write(pc->remote_pem, data, length); + + if (hostname) + pc->remote_hostname = _strdup(hostname); + pc->remote_port = port; + pc->remote_flags = flags; + + Stream_SealLength(pc->remote_pem); + if (!pf_modules_run_hook(pc->pdata->module, HOOK_TYPE_CLIENT_VERIFY_X509, pc->pdata, pc)) + return 0; + return 1; +} + +void channel_data_free(void* obj) +{ + union + { + const void* cpv; + void* pv; + } cnv; + proxyChannelDataEventInfo* dst = obj; + if (dst) + { + cnv.cpv = dst->data; + free(cnv.pv); + + cnv.cpv = dst->channel_name; + free(cnv.pv); + free(dst); + } +} + +static void* channel_data_copy(const void* obj) +{ + union + { + const void* cpv; + void* pv; + } cnv; + const proxyChannelDataEventInfo* src = obj; + proxyChannelDataEventInfo* dst = NULL; + + WINPR_ASSERT(src); + + dst = calloc(1, sizeof(proxyChannelDataEventInfo)); + if (!dst) + goto fail; + + *dst = *src; + if (src->channel_name) + { + dst->channel_name = _strdup(src->channel_name); + if (!dst->channel_name) + goto fail; + } + dst->data = malloc(src->data_len); + if (!dst->data) + goto fail; + + cnv.cpv = dst->data; + memcpy(cnv.pv, src->data, src->data_len); + return dst; + +fail: + channel_data_free(dst); + return NULL; +} + +static BOOL pf_client_client_new(freerdp* instance, rdpContext* context) +{ + wObject* obj = NULL; + pClientContext* pc = (pClientContext*)context; + + if (!instance || !context) + return FALSE; + + instance->LoadChannels = pf_client_load_channels; + instance->PreConnect = pf_client_pre_connect; + instance->PostConnect = pf_client_post_connect; + instance->PostDisconnect = pf_client_post_disconnect; + instance->Redirect = pf_client_redirect; + instance->LogonErrorInfo = pf_logon_error_info; + instance->VerifyX509Certificate = pf_client_verify_X509_certificate; + + pc->remote_pem = Stream_New(NULL, 4096); + if (!pc->remote_pem) + return FALSE; + + pc->sendChannelData = pf_client_send_channel_data; + pc->cached_server_channel_data = Queue_New(TRUE, -1, -1); + if (!pc->cached_server_channel_data) + return FALSE; + obj = Queue_Object(pc->cached_server_channel_data); + WINPR_ASSERT(obj); + obj->fnObjectNew = channel_data_copy; + obj->fnObjectFree = channel_data_free; + + pc->interceptContextMap = HashTable_New(FALSE); + if (!pc->interceptContextMap) + return FALSE; + + if (!HashTable_SetupForStringData(pc->interceptContextMap, FALSE)) + return FALSE; + + obj = HashTable_ValueObject(pc->interceptContextMap); + WINPR_ASSERT(obj); + obj->fnObjectFree = intercept_context_entry_free; + + return TRUE; +} + +static int pf_client_client_stop(rdpContext* context) +{ + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + + PROXY_LOG_DBG(TAG, pc, "aborting client connection"); + proxy_data_abort_connect(pdata); + freerdp_abort_connect_context(context); + + return 0; +} + +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->ContextSize = sizeof(pClientContext); + /* Client init and finish */ + pEntryPoints->ClientNew = pf_client_client_new; + pEntryPoints->ClientFree = pf_client_context_free; + pEntryPoints->ClientStop = pf_client_client_stop; + return 0; +} + +/** + * Starts running a client connection towards target server. + */ +DWORD WINAPI pf_client_start(LPVOID arg) +{ + DWORD rc = 1; + pClientContext* pc = (pClientContext*)arg; + + WINPR_ASSERT(pc); + if (freerdp_client_start(&pc->context) == 0) + rc = pf_client_thread_proc(pc); + freerdp_client_stop(&pc->context); + return rc; +} diff --git a/server/proxy/pf_client.h b/server/proxy/pf_client.h new file mode 100644 index 0000000..cda87a8 --- /dev/null +++ b/server/proxy/pf_client.h @@ -0,0 +1,31 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2019 Mati Shabtay <matishabtay@gmail.com> + * Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com> + * Copyright 2019 Idan Freiberg <speidy@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_SERVER_PROXY_PFCLIENT_H +#define FREERDP_SERVER_PROXY_PFCLIENT_H + +#include <freerdp/freerdp.h> +#include <winpr/wtypes.h> + +int RdpClientEntry(RDP_CLIENT_ENTRY_POINTS* pEntryPoints); +DWORD WINAPI pf_client_start(LPVOID arg); + +#endif /* FREERDP_SERVER_PROXY_PFCLIENT_H */ diff --git a/server/proxy/pf_config.c b/server/proxy/pf_config.c new file mode 100644 index 0000000..bcce1b1 --- /dev/null +++ b/server/proxy/pf_config.c @@ -0,0 +1,1348 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com> + * Copyright 2019 Idan Freiberg <speidy@gmail.com> + * Copyright 2021,2023 Armin Novak <anovak@thincast.com> + * Copyright 2021,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 <stdio.h> +#include <string.h> +#include <winpr/crt.h> +#include <winpr/path.h> +#include <winpr/collections.h> +#include <winpr/cmdline.h> + +#include "pf_server.h" +#include <freerdp/server/proxy/proxy_config.h> + +#include <freerdp/server/proxy/proxy_log.h> + +#include <freerdp/crypto/crypto.h> +#include <freerdp/channels/cliprdr.h> +#include <freerdp/channels/rdpsnd.h> +#include <freerdp/channels/audin.h> +#include <freerdp/channels/rdpdr.h> +#include <freerdp/channels/disp.h> +#include <freerdp/channels/rail.h> +#include <freerdp/channels/rdpei.h> +#include <freerdp/channels/tsmf.h> +#include <freerdp/channels/video.h> +#include <freerdp/channels/rdpecam.h> + +#include "pf_utils.h" + +#define TAG PROXY_TAG("config") + +#define CONFIG_PRINT_SECTION(section) WLog_INFO(TAG, "\t%s:", section) +#define CONFIG_PRINT_SECTION_KEY(section, key) WLog_INFO(TAG, "\t%s/%s:", section, key) +#define CONFIG_PRINT_STR(config, key) WLog_INFO(TAG, "\t\t%s: %s", #key, config->key) +#define CONFIG_PRINT_STR_CONTENT(config, key) \ + WLog_INFO(TAG, "\t\t%s: %s", #key, config->key ? "set" : NULL) +#define CONFIG_PRINT_BOOL(config, key) WLog_INFO(TAG, "\t\t%s: %s", #key, boolstr(config->key)) +#define CONFIG_PRINT_UINT16(config, key) WLog_INFO(TAG, "\t\t%s: %" PRIu16 "", #key, config->key) +#define CONFIG_PRINT_UINT32(config, key) WLog_INFO(TAG, "\t\t%s: %" PRIu32 "", #key, config->key) + +static const char* bool_str_true = "true"; +static const char* bool_str_false = "false"; +static const char* boolstr(BOOL rc) +{ + return rc ? bool_str_true : bool_str_false; +} + +static const char* section_server = "Server"; +static const char* key_host = "Host"; +static const char* key_port = "Port"; + +static const char* section_target = "Target"; +static const char* key_target_fixed = "FixedTarget"; +static const char* key_target_user = "User"; +static const char* key_target_pwd = "Password"; +static const char* key_target_domain = "Domain"; +static const char* key_target_tls_seclevel = "TlsSecLevel"; + +static const char* section_clipboard = "Clipboard"; +static const char* key_clip_text_only = "TextOnly"; +static const char* key_clip_text_max_len = "MaxTextLength"; + +static const char* section_gfx_settings = "GFXSettings"; +static const char* key_gfx_decode = "DecodeGFX"; + +static const char* section_plugins = "Plugins"; +static const char* key_plugins_modules = "Modules"; +static const char* key_plugins_required = "Required"; + +static const char* section_channels = "Channels"; +static const char* key_channels_gfx = "GFX"; +static const char* key_channels_disp = "DisplayControl"; +static const char* key_channels_clip = "Clipboard"; +static const char* key_channels_mic = "AudioInput"; +static const char* key_channels_sound = "AudioOutput"; +static const char* key_channels_rdpdr = "DeviceRedirection"; +static const char* key_channels_video = "VideoRedirection"; +static const char* key_channels_camera = "CameraRedirection"; +static const char* key_channels_rails = "RemoteApp"; +static const char* key_channels_blacklist = "PassthroughIsBlacklist"; +static const char* key_channels_pass = "Passthrough"; +static const char* key_channels_intercept = "Intercept"; + +static const char* section_input = "Input"; +static const char* key_input_kbd = "Keyboard"; +static const char* key_input_mouse = "Mouse"; +static const char* key_input_multitouch = "Multitouch"; + +static const char* section_security = "Security"; +static const char* key_security_server_nla = "ServerNlaSecurity"; +static const char* key_security_server_tls = "ServerTlsSecurity"; +static const char* key_security_server_rdp = "ServerRdpSecurity"; +static const char* key_security_client_nla = "ClientNlaSecurity"; +static const char* key_security_client_tls = "ClientTlsSecurity"; +static const char* key_security_client_rdp = "ClientRdpSecurity"; +static const char* key_security_client_fallback = "ClientAllowFallbackToTls"; + +static const char* section_certificates = "Certificates"; +static const char* key_private_key_file = "PrivateKeyFile"; +static const char* key_private_key_content = "PrivateKeyContent"; +static const char* key_cert_file = "CertificateFile"; +static const char* key_cert_content = "CertificateContent"; + +static char** pf_config_parse_comma_separated_list(const char* list, size_t* count) +{ + if (!list || !count) + return NULL; + + if (strlen(list) == 0) + { + *count = 0; + return NULL; + } + + return CommandLineParseCommaSeparatedValues(list, count); +} + +static BOOL pf_config_get_uint16(wIniFile* ini, const char* section, const char* key, + UINT16* result, BOOL required) +{ + int val = 0; + const char* strval = NULL; + + WINPR_ASSERT(result); + + strval = IniFile_GetKeyValueString(ini, section, key); + if (!strval && required) + { + WLog_ERR(TAG, "key '%s.%s' does not exist.", section, key); + return FALSE; + } + val = IniFile_GetKeyValueInt(ini, section, key); + if ((val <= 0) || (val > UINT16_MAX)) + { + WLog_ERR(TAG, "invalid value %d for key '%s.%s'.", val, section, key); + return FALSE; + } + + *result = (UINT16)val; + return TRUE; +} + +static BOOL pf_config_get_uint32(wIniFile* ini, const char* section, const char* key, + UINT32* result, BOOL required) +{ + int val = 0; + const char* strval = NULL; + + WINPR_ASSERT(result); + + strval = IniFile_GetKeyValueString(ini, section, key); + if (!strval) + { + if (required) + WLog_ERR(TAG, "key '%s.%s' does not exist.", section, key); + return !required; + } + + val = IniFile_GetKeyValueInt(ini, section, key); + if ((val < 0) || (val > INT32_MAX)) + { + WLog_ERR(TAG, "invalid value %d for key '%s.%s'.", val, section, key); + return FALSE; + } + + *result = (UINT32)val; + return TRUE; +} + +static BOOL pf_config_get_bool(wIniFile* ini, const char* section, const char* key, BOOL fallback) +{ + int num_value = 0; + const char* str_value = NULL; + + str_value = IniFile_GetKeyValueString(ini, section, key); + if (!str_value) + { + WLog_WARN(TAG, "key '%s.%s' not found, value defaults to %s.", section, key, + fallback ? bool_str_true : bool_str_false); + return fallback; + } + + if (_stricmp(str_value, bool_str_true) == 0) + return TRUE; + if (_stricmp(str_value, bool_str_false) == 0) + return FALSE; + + num_value = IniFile_GetKeyValueInt(ini, section, key); + + if (num_value != 0) + return TRUE; + + return FALSE; +} + +static const char* pf_config_get_str(wIniFile* ini, const char* section, const char* key, + BOOL required) +{ + const char* value = NULL; + + value = IniFile_GetKeyValueString(ini, section, key); + + if (!value) + { + if (required) + WLog_ERR(TAG, "key '%s.%s' not found.", section, key); + return NULL; + } + + return value; +} + +static BOOL pf_config_load_server(wIniFile* ini, proxyConfig* config) +{ + const char* host = NULL; + + WINPR_ASSERT(config); + host = pf_config_get_str(ini, section_server, key_host, FALSE); + + if (!host) + return TRUE; + + config->Host = _strdup(host); + + if (!config->Host) + return FALSE; + + if (!pf_config_get_uint16(ini, section_server, key_port, &config->Port, TRUE)) + return FALSE; + + return TRUE; +} + +static BOOL pf_config_load_target(wIniFile* ini, proxyConfig* config) +{ + const char* target_value = NULL; + + WINPR_ASSERT(config); + config->FixedTarget = pf_config_get_bool(ini, section_target, key_target_fixed, FALSE); + + if (!pf_config_get_uint16(ini, section_target, key_port, &config->TargetPort, + config->FixedTarget)) + return FALSE; + + if (!pf_config_get_uint32(ini, section_target, key_target_tls_seclevel, + &config->TargetTlsSecLevel, FALSE)) + return FALSE; + + if (config->FixedTarget) + { + target_value = pf_config_get_str(ini, section_target, key_host, TRUE); + if (!target_value) + return FALSE; + + config->TargetHost = _strdup(target_value); + if (!config->TargetHost) + return FALSE; + } + + target_value = pf_config_get_str(ini, section_target, key_target_user, FALSE); + if (target_value) + { + config->TargetUser = _strdup(target_value); + if (!config->TargetUser) + return FALSE; + } + + target_value = pf_config_get_str(ini, section_target, key_target_pwd, FALSE); + if (target_value) + { + config->TargetPassword = _strdup(target_value); + if (!config->TargetPassword) + return FALSE; + } + + target_value = pf_config_get_str(ini, section_target, key_target_domain, FALSE); + if (target_value) + { + config->TargetDomain = _strdup(target_value); + if (!config->TargetDomain) + return FALSE; + } + + return TRUE; +} + +static BOOL pf_config_load_channels(wIniFile* ini, proxyConfig* config) +{ + WINPR_ASSERT(config); + config->GFX = pf_config_get_bool(ini, section_channels, key_channels_gfx, TRUE); + config->DisplayControl = pf_config_get_bool(ini, section_channels, key_channels_disp, TRUE); + config->Clipboard = pf_config_get_bool(ini, section_channels, key_channels_clip, FALSE); + config->AudioOutput = pf_config_get_bool(ini, section_channels, key_channels_mic, TRUE); + config->AudioInput = pf_config_get_bool(ini, section_channels, key_channels_sound, TRUE); + config->DeviceRedirection = pf_config_get_bool(ini, section_channels, key_channels_rdpdr, TRUE); + config->VideoRedirection = pf_config_get_bool(ini, section_channels, key_channels_video, TRUE); + config->CameraRedirection = + pf_config_get_bool(ini, section_channels, key_channels_camera, TRUE); + config->RemoteApp = pf_config_get_bool(ini, section_channels, key_channels_rails, FALSE); + config->PassthroughIsBlacklist = + pf_config_get_bool(ini, section_channels, key_channels_blacklist, FALSE); + config->Passthrough = pf_config_parse_comma_separated_list( + pf_config_get_str(ini, section_channels, key_channels_pass, FALSE), + &config->PassthroughCount); + config->Intercept = pf_config_parse_comma_separated_list( + pf_config_get_str(ini, section_channels, key_channels_intercept, FALSE), + &config->InterceptCount); + + return TRUE; +} + +static BOOL pf_config_load_input(wIniFile* ini, proxyConfig* config) +{ + WINPR_ASSERT(config); + config->Keyboard = pf_config_get_bool(ini, section_input, key_input_kbd, TRUE); + config->Mouse = pf_config_get_bool(ini, section_input, key_input_mouse, TRUE); + config->Multitouch = pf_config_get_bool(ini, section_input, key_input_multitouch, TRUE); + return TRUE; +} + +static BOOL pf_config_load_security(wIniFile* ini, proxyConfig* config) +{ + WINPR_ASSERT(config); + config->ServerTlsSecurity = + pf_config_get_bool(ini, section_security, key_security_server_tls, TRUE); + config->ServerNlaSecurity = + pf_config_get_bool(ini, section_security, key_security_server_nla, FALSE); + config->ServerRdpSecurity = + pf_config_get_bool(ini, section_security, key_security_server_rdp, TRUE); + + config->ClientTlsSecurity = + pf_config_get_bool(ini, section_security, key_security_client_tls, TRUE); + config->ClientNlaSecurity = + pf_config_get_bool(ini, section_security, key_security_client_nla, TRUE); + config->ClientRdpSecurity = + pf_config_get_bool(ini, section_security, key_security_client_rdp, TRUE); + config->ClientAllowFallbackToTls = + pf_config_get_bool(ini, section_security, key_security_client_fallback, TRUE); + return TRUE; +} + +static BOOL pf_config_load_clipboard(wIniFile* ini, proxyConfig* config) +{ + WINPR_ASSERT(config); + config->TextOnly = pf_config_get_bool(ini, section_clipboard, key_clip_text_only, FALSE); + + if (!pf_config_get_uint32(ini, section_clipboard, key_clip_text_max_len, &config->MaxTextLength, + FALSE)) + return FALSE; + + return TRUE; +} + +static BOOL pf_config_load_modules(wIniFile* ini, proxyConfig* config) +{ + const char* modules_to_load = NULL; + const char* required_modules = NULL; + + modules_to_load = pf_config_get_str(ini, section_plugins, key_plugins_modules, FALSE); + required_modules = pf_config_get_str(ini, section_plugins, key_plugins_required, FALSE); + + WINPR_ASSERT(config); + config->Modules = pf_config_parse_comma_separated_list(modules_to_load, &config->ModulesCount); + + config->RequiredPlugins = + pf_config_parse_comma_separated_list(required_modules, &config->RequiredPluginsCount); + return TRUE; +} + +static BOOL pf_config_load_gfx_settings(wIniFile* ini, proxyConfig* config) +{ + WINPR_ASSERT(config); + config->DecodeGFX = pf_config_get_bool(ini, section_gfx_settings, key_gfx_decode, FALSE); + return TRUE; +} + +static char* pf_config_decode_base64(const char* data, const char* name, size_t* pLength) +{ + const char* headers[] = { "-----BEGIN PUBLIC KEY-----", "-----BEGIN RSA PUBLIC KEY-----", + "-----BEGIN CERTIFICATE-----", "-----BEGIN PRIVATE KEY-----", + "-----BEGIN RSA PRIVATE KEY-----" }; + + size_t decoded_length = 0; + char* decoded = NULL; + if (!data) + { + WLog_ERR(TAG, "Invalid base64 data [%p] for %s", data, name); + return NULL; + } + + WINPR_ASSERT(name); + WINPR_ASSERT(pLength); + + const size_t length = strlen(data); + + if (strncmp(data, "-----", 5) == 0) + { + BOOL expected = FALSE; + for (size_t x = 0; x < ARRAYSIZE(headers); x++) + { + const char* header = headers[x]; + + if (strncmp(data, header, strlen(header)) == 0) + expected = TRUE; + } + + if (!expected) + { + /* Extract header for log message + * expected format is '----- SOMETEXT -----' + */ + char hdr[128] = { 0 }; + const char* end = strchr(&data[5], '-'); + if (end) + { + while (*end == '-') + end++; + + const size_t s = MIN(ARRAYSIZE(hdr) - 1, end - data); + memcpy(hdr, data, s); + } + + WLog_WARN(TAG, "PEM has unexpected header '%s'. Known supported headers are:", hdr); + for (size_t x = 0; x < ARRAYSIZE(headers); x++) + { + const char* header = headers[x]; + WLog_WARN(TAG, "%s", header); + } + } + + *pLength = length + 1; + return _strdup(data); + } + + crypto_base64_decode(data, length, (BYTE**)&decoded, &decoded_length); + if (!decoded || decoded_length == 0) + { + WLog_ERR(TAG, "Failed to decode base64 data of length %" PRIuz " for %s", length, name); + free(decoded); + return NULL; + } + + *pLength = strnlen(decoded, decoded_length) + 1; + return decoded; +} + +static BOOL pf_config_load_certificates(wIniFile* ini, proxyConfig* config) +{ + const char* tmp1 = NULL; + const char* tmp2 = NULL; + + WINPR_ASSERT(ini); + WINPR_ASSERT(config); + + tmp1 = pf_config_get_str(ini, section_certificates, key_cert_file, FALSE); + if (tmp1) + { + if (!winpr_PathFileExists(tmp1)) + { + WLog_ERR(TAG, "%s/%s file %s does not exist", section_certificates, key_cert_file, + tmp1); + return FALSE; + } + config->CertificateFile = _strdup(tmp1); + config->CertificatePEM = + crypto_read_pem(config->CertificateFile, &config->CertificatePEMLength); + if (!config->CertificatePEM) + return FALSE; + config->CertificatePEMLength += 1; + } + tmp2 = pf_config_get_str(ini, section_certificates, key_cert_content, FALSE); + if (tmp2) + { + if (strlen(tmp2) < 1) + { + WLog_ERR(TAG, "%s/%s has invalid empty value", section_certificates, key_cert_content); + return FALSE; + } + config->CertificateContent = _strdup(tmp2); + config->CertificatePEM = pf_config_decode_base64( + config->CertificateContent, "CertificateContent", &config->CertificatePEMLength); + if (!config->CertificatePEM) + return FALSE; + } + if (tmp1 && tmp2) + { + WLog_ERR(TAG, + "%s/%s and %s/%s are " + "mutually exclusive options", + section_certificates, key_cert_file, section_certificates, key_cert_content); + return FALSE; + } + else if (!tmp1 && !tmp2) + { + WLog_ERR(TAG, + "%s/%s or %s/%s are " + "required settings", + section_certificates, key_cert_file, section_certificates, key_cert_content); + return FALSE; + } + + tmp1 = pf_config_get_str(ini, section_certificates, key_private_key_file, FALSE); + if (tmp1) + { + if (!winpr_PathFileExists(tmp1)) + { + WLog_ERR(TAG, "%s/%s file %s does not exist", section_certificates, + key_private_key_file, tmp1); + return FALSE; + } + config->PrivateKeyFile = _strdup(tmp1); + config->PrivateKeyPEM = + crypto_read_pem(config->PrivateKeyFile, &config->PrivateKeyPEMLength); + if (!config->PrivateKeyPEM) + return FALSE; + config->PrivateKeyPEMLength += 1; + } + tmp2 = pf_config_get_str(ini, section_certificates, key_private_key_content, FALSE); + if (tmp2) + { + if (strlen(tmp2) < 1) + { + WLog_ERR(TAG, "%s/%s has invalid empty value", section_certificates, + key_private_key_content); + return FALSE; + } + config->PrivateKeyContent = _strdup(tmp2); + config->PrivateKeyPEM = pf_config_decode_base64( + config->PrivateKeyContent, "PrivateKeyContent", &config->PrivateKeyPEMLength); + if (!config->PrivateKeyPEM) + return FALSE; + } + + if (tmp1 && tmp2) + { + WLog_ERR(TAG, + "%s/%s and %s/%s are " + "mutually exclusive options", + section_certificates, key_private_key_file, section_certificates, + key_private_key_content); + return FALSE; + } + else if (!tmp1 && !tmp2) + { + WLog_ERR(TAG, + "%s/%s or %s/%s are " + "are required settings", + section_certificates, key_private_key_file, section_certificates, + key_private_key_content); + return FALSE; + } + + return TRUE; +} + +proxyConfig* server_config_load_ini(wIniFile* ini) +{ + proxyConfig* config = NULL; + + WINPR_ASSERT(ini); + + config = calloc(1, sizeof(proxyConfig)); + if (config) + { + /* Set default values != 0 */ + config->TargetTlsSecLevel = 1; + + /* Load from ini */ + if (!pf_config_load_server(ini, config)) + goto out; + + if (!pf_config_load_target(ini, config)) + goto out; + + if (!pf_config_load_channels(ini, config)) + goto out; + + if (!pf_config_load_input(ini, config)) + goto out; + + if (!pf_config_load_security(ini, config)) + goto out; + + if (!pf_config_load_modules(ini, config)) + goto out; + + if (!pf_config_load_clipboard(ini, config)) + goto out; + + if (!pf_config_load_gfx_settings(ini, config)) + goto out; + + if (!pf_config_load_certificates(ini, config)) + goto out; + config->ini = IniFile_Clone(ini); + if (!config->ini) + goto out; + } + return config; +out: + pf_server_config_free(config); + return NULL; +} + +BOOL pf_server_config_dump(const char* file) +{ + BOOL rc = FALSE; + wIniFile* ini = IniFile_New(); + if (!ini) + return FALSE; + + /* Proxy server configuration */ + if (IniFile_SetKeyValueString(ini, section_server, key_host, "0.0.0.0") < 0) + goto fail; + if (IniFile_SetKeyValueInt(ini, section_server, key_port, 3389) < 0) + goto fail; + + /* Target configuration */ + if (IniFile_SetKeyValueString(ini, section_target, key_host, "somehost.example.com") < 0) + goto fail; + if (IniFile_SetKeyValueInt(ini, section_target, key_port, 3389) < 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_target, key_target_fixed, bool_str_true) < 0) + goto fail; + if (IniFile_SetKeyValueInt(ini, section_target, key_target_tls_seclevel, 1) < 0) + goto fail; + + /* Channel configuration */ + if (IniFile_SetKeyValueString(ini, section_channels, key_channels_gfx, bool_str_true) < 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_channels, key_channels_disp, bool_str_true) < 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_channels, key_channels_clip, bool_str_true) < 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_channels, key_channels_mic, bool_str_true) < 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_channels, key_channels_sound, bool_str_true) < 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_channels, key_channels_rdpdr, bool_str_true) < 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_channels, key_channels_video, bool_str_true) < 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_channels, key_channels_camera, bool_str_true) < 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_channels, key_channels_rails, bool_str_false) < 0) + goto fail; + + if (IniFile_SetKeyValueString(ini, section_channels, key_channels_blacklist, bool_str_true) < 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_channels, key_channels_pass, "") < 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_channels, key_channels_intercept, "") < 0) + goto fail; + + /* Input configuration */ + if (IniFile_SetKeyValueString(ini, section_input, key_input_kbd, bool_str_true) < 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_input, key_input_mouse, bool_str_true) < 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_input, key_input_multitouch, bool_str_true) < 0) + goto fail; + + /* Security settings */ + if (IniFile_SetKeyValueString(ini, section_security, key_security_server_tls, bool_str_true) < + 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_security, key_security_server_nla, bool_str_false) < + 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_security, key_security_server_rdp, bool_str_true) < + 0) + goto fail; + + if (IniFile_SetKeyValueString(ini, section_security, key_security_client_tls, bool_str_true) < + 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_security, key_security_client_nla, bool_str_true) < + 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_security, key_security_client_rdp, bool_str_true) < + 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_security, key_security_client_fallback, + bool_str_true) < 0) + goto fail; + + /* Module configuration */ + if (IniFile_SetKeyValueString(ini, section_plugins, key_plugins_modules, + "module1,module2,...") < 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_plugins, key_plugins_required, + "module1,module2,...") < 0) + goto fail; + + /* Clipboard configuration */ + if (IniFile_SetKeyValueString(ini, section_clipboard, key_clip_text_only, bool_str_false) < 0) + goto fail; + if (IniFile_SetKeyValueInt(ini, section_clipboard, key_clip_text_max_len, 0) < 0) + goto fail; + + /* GFX configuration */ + if (IniFile_SetKeyValueString(ini, section_gfx_settings, key_gfx_decode, bool_str_false) < 0) + goto fail; + + /* Certificate configuration */ + if (IniFile_SetKeyValueString(ini, section_certificates, key_cert_file, + "<absolute path to some certificate file> OR") < 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_certificates, key_cert_content, + "<Contents of some certificate file in PEM format>") < 0) + goto fail; + + if (IniFile_SetKeyValueString(ini, section_certificates, key_private_key_file, + "<absolute path to some private key file> OR") < 0) + goto fail; + if (IniFile_SetKeyValueString(ini, section_certificates, key_private_key_content, + "<Contents of some private key file in PEM format>") < 0) + goto fail; + + /* store configuration */ + if (IniFile_WriteFile(ini, file) < 0) + goto fail; + + rc = TRUE; + +fail: + IniFile_Free(ini); + return rc; +} + +proxyConfig* pf_server_config_load_buffer(const char* buffer) +{ + proxyConfig* config = NULL; + wIniFile* ini = NULL; + + ini = IniFile_New(); + + if (!ini) + { + WLog_ERR(TAG, "IniFile_New() failed!"); + return NULL; + } + + if (IniFile_ReadBuffer(ini, buffer) < 0) + { + WLog_ERR(TAG, "failed to parse ini: '%s'", buffer); + goto out; + } + + config = server_config_load_ini(ini); +out: + IniFile_Free(ini); + return config; +} + +proxyConfig* pf_server_config_load_file(const char* path) +{ + proxyConfig* config = NULL; + wIniFile* ini = IniFile_New(); + + if (!ini) + { + WLog_ERR(TAG, "IniFile_New() failed!"); + return NULL; + } + + if (IniFile_ReadFile(ini, path) < 0) + { + WLog_ERR(TAG, "failed to parse ini file: '%s'", path); + goto out; + } + + config = server_config_load_ini(ini); +out: + IniFile_Free(ini); + return config; +} + +static void pf_server_config_print_list(char** list, size_t count) +{ + WINPR_ASSERT(list); + for (size_t i = 0; i < count; i++) + WLog_INFO(TAG, "\t\t- %s", list[i]); +} + +void pf_server_config_print(const proxyConfig* config) +{ + WINPR_ASSERT(config); + WLog_INFO(TAG, "Proxy configuration:"); + + CONFIG_PRINT_SECTION(section_server); + CONFIG_PRINT_STR(config, Host); + CONFIG_PRINT_UINT16(config, Port); + + if (config->FixedTarget) + { + CONFIG_PRINT_SECTION(section_target); + CONFIG_PRINT_STR(config, TargetHost); + CONFIG_PRINT_UINT16(config, TargetPort); + CONFIG_PRINT_UINT32(config, TargetTlsSecLevel); + + if (config->TargetUser) + CONFIG_PRINT_STR(config, TargetUser); + if (config->TargetDomain) + CONFIG_PRINT_STR(config, TargetDomain); + } + + CONFIG_PRINT_SECTION(section_input); + CONFIG_PRINT_BOOL(config, Keyboard); + CONFIG_PRINT_BOOL(config, Mouse); + CONFIG_PRINT_BOOL(config, Multitouch); + + CONFIG_PRINT_SECTION(section_security); + CONFIG_PRINT_BOOL(config, ServerNlaSecurity); + CONFIG_PRINT_BOOL(config, ServerTlsSecurity); + CONFIG_PRINT_BOOL(config, ServerRdpSecurity); + CONFIG_PRINT_BOOL(config, ClientNlaSecurity); + CONFIG_PRINT_BOOL(config, ClientTlsSecurity); + CONFIG_PRINT_BOOL(config, ClientRdpSecurity); + CONFIG_PRINT_BOOL(config, ClientAllowFallbackToTls); + + CONFIG_PRINT_SECTION(section_channels); + CONFIG_PRINT_BOOL(config, GFX); + CONFIG_PRINT_BOOL(config, DisplayControl); + CONFIG_PRINT_BOOL(config, Clipboard); + CONFIG_PRINT_BOOL(config, AudioOutput); + CONFIG_PRINT_BOOL(config, AudioInput); + CONFIG_PRINT_BOOL(config, DeviceRedirection); + CONFIG_PRINT_BOOL(config, VideoRedirection); + CONFIG_PRINT_BOOL(config, CameraRedirection); + CONFIG_PRINT_BOOL(config, RemoteApp); + CONFIG_PRINT_BOOL(config, PassthroughIsBlacklist); + + if (config->PassthroughCount) + { + WLog_INFO(TAG, "\tStatic Channels Proxy:"); + pf_server_config_print_list(config->Passthrough, config->PassthroughCount); + } + + if (config->InterceptCount) + { + WLog_INFO(TAG, "\tStatic Channels Proxy-Intercept:"); + pf_server_config_print_list(config->Intercept, config->InterceptCount); + } + + CONFIG_PRINT_SECTION(section_clipboard); + CONFIG_PRINT_BOOL(config, TextOnly); + if (config->MaxTextLength > 0) + CONFIG_PRINT_UINT32(config, MaxTextLength); + + CONFIG_PRINT_SECTION(section_gfx_settings); + CONFIG_PRINT_BOOL(config, DecodeGFX); + + /* modules */ + CONFIG_PRINT_SECTION_KEY(section_plugins, key_plugins_modules); + for (size_t x = 0; x < config->ModulesCount; x++) + CONFIG_PRINT_STR(config, Modules[x]); + + /* Required plugins */ + CONFIG_PRINT_SECTION_KEY(section_plugins, key_plugins_required); + for (size_t x = 0; x < config->RequiredPluginsCount; x++) + CONFIG_PRINT_STR(config, RequiredPlugins[x]); + + CONFIG_PRINT_SECTION(section_certificates); + CONFIG_PRINT_STR(config, CertificateFile); + CONFIG_PRINT_STR_CONTENT(config, CertificateContent); + CONFIG_PRINT_STR(config, PrivateKeyFile); + CONFIG_PRINT_STR_CONTENT(config, PrivateKeyContent); +} + +void pf_server_config_free(proxyConfig* config) +{ + if (config == NULL) + return; + + free(config->Passthrough); + free(config->Intercept); + free(config->RequiredPlugins); + free(config->Modules); + free(config->TargetHost); + free(config->Host); + free(config->CertificateFile); + free(config->CertificateContent); + if (config->CertificatePEM) + memset(config->CertificatePEM, 0, config->CertificatePEMLength); + free(config->CertificatePEM); + free(config->PrivateKeyFile); + free(config->PrivateKeyContent); + if (config->PrivateKeyPEM) + memset(config->PrivateKeyPEM, 0, config->PrivateKeyPEMLength); + free(config->PrivateKeyPEM); + IniFile_Free(config->ini); + free(config); +} + +size_t pf_config_required_plugins_count(const proxyConfig* config) +{ + WINPR_ASSERT(config); + return config->RequiredPluginsCount; +} + +const char* pf_config_required_plugin(const proxyConfig* config, size_t index) +{ + WINPR_ASSERT(config); + if (index >= config->RequiredPluginsCount) + return NULL; + + return config->RequiredPlugins[index]; +} + +size_t pf_config_modules_count(const proxyConfig* config) +{ + WINPR_ASSERT(config); + return config->ModulesCount; +} + +const char** pf_config_modules(const proxyConfig* config) +{ + union + { + char** ppc; + const char** cppc; + } cnv; + + WINPR_ASSERT(config); + + cnv.ppc = config->Modules; + return cnv.cppc; +} + +static BOOL pf_config_copy_string(char** dst, const char* src) +{ + *dst = NULL; + if (src) + *dst = _strdup(src); + return TRUE; +} + +static BOOL pf_config_copy_string_n(char** dst, const char* src, size_t size) +{ + *dst = NULL; + + if (src && (size > 0)) + { + WINPR_ASSERT(strnlen(src, size) == size - 1); + *dst = calloc(size, sizeof(char)); + if (!*dst) + return FALSE; + memcpy(*dst, src, size); + } + + return TRUE; +} + +static BOOL pf_config_copy_string_list(char*** dst, size_t* size, char** src, size_t srcSize) +{ + WINPR_ASSERT(dst); + WINPR_ASSERT(size); + WINPR_ASSERT(src || (srcSize == 0)); + + *dst = NULL; + *size = 0; + if (srcSize == 0) + return TRUE; + { + char* csv = CommandLineToCommaSeparatedValues(srcSize, src); + *dst = CommandLineParseCommaSeparatedValues(csv, size); + free(csv); + } + + return TRUE; +} + +BOOL pf_config_clone(proxyConfig** dst, const proxyConfig* config) +{ + proxyConfig* tmp = calloc(1, sizeof(proxyConfig)); + + WINPR_ASSERT(dst); + WINPR_ASSERT(config); + + if (!tmp) + return FALSE; + + *tmp = *config; + + if (!pf_config_copy_string(&tmp->Host, config->Host)) + goto fail; + if (!pf_config_copy_string(&tmp->TargetHost, config->TargetHost)) + goto fail; + + if (!pf_config_copy_string_list(&tmp->Passthrough, &tmp->PassthroughCount, config->Passthrough, + config->PassthroughCount)) + goto fail; + if (!pf_config_copy_string_list(&tmp->Intercept, &tmp->InterceptCount, config->Intercept, + config->InterceptCount)) + goto fail; + if (!pf_config_copy_string_list(&tmp->Modules, &tmp->ModulesCount, config->Modules, + config->ModulesCount)) + goto fail; + if (!pf_config_copy_string_list(&tmp->RequiredPlugins, &tmp->RequiredPluginsCount, + config->RequiredPlugins, config->RequiredPluginsCount)) + goto fail; + if (!pf_config_copy_string(&tmp->CertificateFile, config->CertificateFile)) + goto fail; + if (!pf_config_copy_string(&tmp->CertificateContent, config->CertificateContent)) + goto fail; + if (!pf_config_copy_string_n(&tmp->CertificatePEM, config->CertificatePEM, + config->CertificatePEMLength)) + goto fail; + if (!pf_config_copy_string(&tmp->PrivateKeyFile, config->PrivateKeyFile)) + goto fail; + if (!pf_config_copy_string(&tmp->PrivateKeyContent, config->PrivateKeyContent)) + goto fail; + if (!pf_config_copy_string_n(&tmp->PrivateKeyPEM, config->PrivateKeyPEM, + config->PrivateKeyPEMLength)) + goto fail; + + tmp->ini = IniFile_Clone(config->ini); + if (!tmp->ini) + goto fail; + + *dst = tmp; + return TRUE; + +fail: + pf_server_config_free(tmp); + return FALSE; +} + +struct config_plugin_data +{ + proxyPluginsManager* mgr; + const proxyConfig* config; +}; + +static const char config_plugin_name[] = "config"; +static const char config_plugin_desc[] = + "A plugin filtering according to proxy configuration file rules"; + +static BOOL config_plugin_unload(proxyPlugin* plugin) +{ + WINPR_ASSERT(plugin); + + /* Here we have to free up our custom data storage. */ + if (plugin) + { + free(plugin->custom); + plugin->custom = NULL; + } + + return TRUE; +} + +static BOOL config_plugin_keyboard_event(proxyPlugin* plugin, proxyData* pdata, void* param) +{ + BOOL rc = 0; + const struct config_plugin_data* custom = NULL; + const proxyConfig* cfg = NULL; + const proxyKeyboardEventInfo* event_data = (const proxyKeyboardEventInfo*)(param); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(event_data); + + WINPR_UNUSED(event_data); + + custom = plugin->custom; + WINPR_ASSERT(custom); + + cfg = custom->config; + WINPR_ASSERT(cfg); + + rc = cfg->Keyboard; + WLog_DBG(TAG, "%s", boolstr(rc)); + return rc; +} + +static BOOL config_plugin_unicode_event(proxyPlugin* plugin, proxyData* pdata, void* param) +{ + BOOL rc = 0; + const struct config_plugin_data* custom = NULL; + const proxyConfig* cfg = NULL; + const proxyUnicodeEventInfo* event_data = (const proxyUnicodeEventInfo*)(param); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(event_data); + + WINPR_UNUSED(event_data); + + custom = plugin->custom; + WINPR_ASSERT(custom); + + cfg = custom->config; + WINPR_ASSERT(cfg); + + rc = cfg->Keyboard; + WLog_DBG(TAG, "%s", boolstr(rc)); + return rc; +} + +static BOOL config_plugin_mouse_event(proxyPlugin* plugin, proxyData* pdata, void* param) +{ + BOOL rc = 0; + const struct config_plugin_data* custom = NULL; + const proxyConfig* cfg = NULL; + const proxyMouseEventInfo* event_data = (const proxyMouseEventInfo*)(param); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(event_data); + + WINPR_UNUSED(event_data); + + custom = plugin->custom; + WINPR_ASSERT(custom); + + cfg = custom->config; + WINPR_ASSERT(cfg); + + rc = cfg->Mouse; + return rc; +} + +static BOOL config_plugin_mouse_ex_event(proxyPlugin* plugin, proxyData* pdata, void* param) +{ + BOOL rc = 0; + const struct config_plugin_data* custom = NULL; + const proxyConfig* cfg = NULL; + const proxyMouseExEventInfo* event_data = (const proxyMouseExEventInfo*)(param); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(event_data); + + WINPR_UNUSED(event_data); + + custom = plugin->custom; + WINPR_ASSERT(custom); + + cfg = custom->config; + WINPR_ASSERT(cfg); + + rc = cfg->Mouse; + return rc; +} + +static BOOL config_plugin_client_channel_data(proxyPlugin* plugin, proxyData* pdata, void* param) +{ + const proxyChannelDataEventInfo* channel = (const proxyChannelDataEventInfo*)(param); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(channel); + + WLog_DBG(TAG, "%s [0x%04" PRIx16 "] got %" PRIuz, channel->channel_name, channel->channel_id, + channel->data_len); + return TRUE; +} + +static BOOL config_plugin_server_channel_data(proxyPlugin* plugin, proxyData* pdata, void* param) +{ + const proxyChannelDataEventInfo* channel = (const proxyChannelDataEventInfo*)(param); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(channel); + + WLog_DBG(TAG, "%s [0x%04" PRIx16 "] got %" PRIuz, channel->channel_name, channel->channel_id, + channel->data_len); + return TRUE; +} + +static BOOL config_plugin_dynamic_channel_create(proxyPlugin* plugin, proxyData* pdata, void* param) +{ + BOOL accept = 0; + const proxyChannelDataEventInfo* channel = (const proxyChannelDataEventInfo*)(param); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(channel); + + const struct config_plugin_data* custom = plugin->custom; + WINPR_ASSERT(custom); + + const proxyConfig* cfg = custom->config; + WINPR_ASSERT(cfg); + + pf_utils_channel_mode rc = pf_utils_get_channel_mode(cfg, channel->channel_name); + switch (rc) + { + + case PF_UTILS_CHANNEL_INTERCEPT: + case PF_UTILS_CHANNEL_PASSTHROUGH: + accept = TRUE; + break; + case PF_UTILS_CHANNEL_BLOCK: + default: + accept = FALSE; + break; + } + + if (accept) + { + if (strncmp(RDPGFX_DVC_CHANNEL_NAME, channel->channel_name, + sizeof(RDPGFX_DVC_CHANNEL_NAME)) == 0) + accept = cfg->GFX; + else if (strncmp(RDPSND_DVC_CHANNEL_NAME, channel->channel_name, + sizeof(RDPSND_DVC_CHANNEL_NAME)) == 0) + accept = cfg->AudioOutput; + else if (strncmp(RDPSND_LOSSY_DVC_CHANNEL_NAME, channel->channel_name, + sizeof(RDPSND_LOSSY_DVC_CHANNEL_NAME)) == 0) + accept = cfg->AudioOutput; + else if (strncmp(AUDIN_DVC_CHANNEL_NAME, channel->channel_name, + sizeof(AUDIN_DVC_CHANNEL_NAME)) == 0) + accept = cfg->AudioInput; + else if (strncmp(RDPEI_DVC_CHANNEL_NAME, channel->channel_name, + sizeof(RDPEI_DVC_CHANNEL_NAME)) == 0) + accept = cfg->Multitouch; + else if (strncmp(TSMF_DVC_CHANNEL_NAME, channel->channel_name, + sizeof(TSMF_DVC_CHANNEL_NAME)) == 0) + accept = cfg->VideoRedirection; + else if (strncmp(VIDEO_CONTROL_DVC_CHANNEL_NAME, channel->channel_name, + sizeof(VIDEO_CONTROL_DVC_CHANNEL_NAME)) == 0) + accept = cfg->VideoRedirection; + else if (strncmp(VIDEO_DATA_DVC_CHANNEL_NAME, channel->channel_name, + sizeof(VIDEO_DATA_DVC_CHANNEL_NAME)) == 0) + accept = cfg->VideoRedirection; + else if (strncmp(RDPECAM_DVC_CHANNEL_NAME, channel->channel_name, + sizeof(RDPECAM_DVC_CHANNEL_NAME)) == 0) + accept = cfg->CameraRedirection; + } + + WLog_DBG(TAG, "%s [0x%04" PRIx16 "]: %s", channel->channel_name, channel->channel_id, + boolstr(accept)); + return accept; +} + +static BOOL config_plugin_channel_create(proxyPlugin* plugin, proxyData* pdata, void* param) +{ + BOOL accept = 0; + const proxyChannelDataEventInfo* channel = (const proxyChannelDataEventInfo*)(param); + + WINPR_ASSERT(plugin); + WINPR_ASSERT(pdata); + WINPR_ASSERT(channel); + + const struct config_plugin_data* custom = plugin->custom; + WINPR_ASSERT(custom); + + const proxyConfig* cfg = custom->config; + WINPR_ASSERT(cfg); + + pf_utils_channel_mode rc = pf_utils_get_channel_mode(cfg, channel->channel_name); + switch (rc) + { + case PF_UTILS_CHANNEL_INTERCEPT: + case PF_UTILS_CHANNEL_PASSTHROUGH: + accept = TRUE; + break; + case PF_UTILS_CHANNEL_BLOCK: + default: + accept = FALSE; + break; + } + if (accept) + { + if (strncmp(CLIPRDR_SVC_CHANNEL_NAME, channel->channel_name, + sizeof(CLIPRDR_SVC_CHANNEL_NAME)) == 0) + accept = cfg->Clipboard; + else if (strncmp(RDPSND_CHANNEL_NAME, channel->channel_name, sizeof(RDPSND_CHANNEL_NAME)) == + 0) + accept = cfg->AudioOutput; + else if (strncmp(RDPDR_SVC_CHANNEL_NAME, channel->channel_name, + sizeof(RDPDR_SVC_CHANNEL_NAME)) == 0) + accept = cfg->DeviceRedirection; + else if (strncmp(DISP_DVC_CHANNEL_NAME, channel->channel_name, + sizeof(DISP_DVC_CHANNEL_NAME)) == 0) + accept = cfg->DisplayControl; + else if (strncmp(RAIL_SVC_CHANNEL_NAME, channel->channel_name, + sizeof(RAIL_SVC_CHANNEL_NAME)) == 0) + accept = cfg->RemoteApp; + } + + WLog_DBG(TAG, "%s [static]: %s", channel->channel_name, boolstr(accept)); + return accept; +} + +BOOL pf_config_plugin(proxyPluginsManager* plugins_manager, void* userdata) +{ + struct config_plugin_data* custom = NULL; + proxyPlugin plugin = { 0 }; + + plugin.name = config_plugin_name; + plugin.description = config_plugin_desc; + plugin.PluginUnload = config_plugin_unload; + + plugin.KeyboardEvent = config_plugin_keyboard_event; + plugin.UnicodeEvent = config_plugin_unicode_event; + plugin.MouseEvent = config_plugin_mouse_event; + plugin.MouseExEvent = config_plugin_mouse_ex_event; + plugin.ClientChannelData = config_plugin_client_channel_data; + plugin.ServerChannelData = config_plugin_server_channel_data; + plugin.ChannelCreate = config_plugin_channel_create; + plugin.DynamicChannelCreate = config_plugin_dynamic_channel_create; + plugin.userdata = userdata; + + custom = calloc(1, sizeof(struct config_plugin_data)); + if (!custom) + return FALSE; + + custom->mgr = plugins_manager; + custom->config = userdata; + + plugin.custom = custom; + plugin.userdata = userdata; + + return plugins_manager->RegisterPlugin(plugins_manager, &plugin); +} + +const char* pf_config_get(const proxyConfig* config, const char* section, const char* key) +{ + WINPR_ASSERT(config); + WINPR_ASSERT(config->ini); + WINPR_ASSERT(section); + WINPR_ASSERT(key); + + return IniFile_GetKeyValueString(config->ini, section, key); +} diff --git a/server/proxy/pf_context.c b/server/proxy/pf_context.c new file mode 100644 index 0000000..04433d9 --- /dev/null +++ b/server/proxy/pf_context.c @@ -0,0 +1,394 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2019 Mati Shabtay <matishabtay@gmail.com> + * Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com> + * Copyright 2019 Idan Freiberg <speidy@gmail.com> + * Copyright 2021 Armin Novak <anovak@thincast.com> + * Copyright 2021 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/crypto.h> +#include <winpr/print.h> + +#include <freerdp/server/proxy/proxy_log.h> +#include <freerdp/server/proxy/proxy_server.h> + +#include "pf_client.h" +#include "pf_utils.h" +#include "proxy_modules.h" + +#include <freerdp/server/proxy/proxy_context.h> + +#include "channels/pf_channel_rdpdr.h" + +#define TAG PROXY_TAG("server") + +static UINT32 ChannelId_Hash(const void* key) +{ + const UINT32* v = (const UINT32*)key; + return *v; +} + +static BOOL ChannelId_Compare(const void* pv1, const void* pv2) +{ + const UINT32* v1 = pv1; + const UINT32* v2 = pv2; + WINPR_ASSERT(v1); + WINPR_ASSERT(v2); + return (*v1 == *v2); +} + +pServerStaticChannelContext* StaticChannelContext_new(pServerContext* ps, const char* name, + UINT32 id) +{ + pServerStaticChannelContext* ret = calloc(1, sizeof(*ret)); + if (!ret) + { + PROXY_LOG_ERR(TAG, ps, "error allocating channel context for '%s'", name); + return NULL; + } + + ret->front_channel_id = id; + ret->channel_name = _strdup(name); + if (!ret->channel_name) + { + PROXY_LOG_ERR(TAG, ps, "error allocating name in channel context for '%s'", name); + free(ret); + return NULL; + } + + proxyChannelToInterceptData channel = { .name = name, .channelId = id, .intercept = FALSE }; + + if (pf_modules_run_filter(ps->pdata->module, FILTER_TYPE_STATIC_INTERCEPT_LIST, ps->pdata, + &channel) && + channel.intercept) + ret->channelMode = PF_UTILS_CHANNEL_INTERCEPT; + else + ret->channelMode = pf_utils_get_channel_mode(ps->pdata->config, name); + return ret; +} + +void StaticChannelContext_free(pServerStaticChannelContext* ctx) +{ + if (!ctx) + return; + + IFCALL(ctx->contextDtor, ctx->context); + + free(ctx->channel_name); + free(ctx); +} + +static void HashStaticChannelContext_free(void* ptr) +{ + pServerStaticChannelContext* ctx = (pServerStaticChannelContext*)ptr; + StaticChannelContext_free(ctx); +} + +/* Proxy context initialization callback */ +static void client_to_proxy_context_free(freerdp_peer* client, rdpContext* ctx); +static BOOL client_to_proxy_context_new(freerdp_peer* client, rdpContext* ctx) +{ + wObject* obj = NULL; + pServerContext* context = (pServerContext*)ctx; + + WINPR_ASSERT(client); + WINPR_ASSERT(context); + + context->dynvcReady = NULL; + + context->vcm = WTSOpenServerA((LPSTR)client->context); + + if (!context->vcm || context->vcm == INVALID_HANDLE_VALUE) + goto error; + + if (!(context->dynvcReady = CreateEvent(NULL, TRUE, FALSE, NULL))) + goto error; + + context->interceptContextMap = HashTable_New(FALSE); + if (!context->interceptContextMap) + goto error; + if (!HashTable_SetupForStringData(context->interceptContextMap, FALSE)) + goto error; + obj = HashTable_ValueObject(context->interceptContextMap); + WINPR_ASSERT(obj); + obj->fnObjectFree = intercept_context_entry_free; + + /* channels by ids */ + context->channelsByFrontId = HashTable_New(FALSE); + if (!context->channelsByFrontId) + goto error; + if (!HashTable_SetHashFunction(context->channelsByFrontId, ChannelId_Hash)) + goto error; + + obj = HashTable_KeyObject(context->channelsByFrontId); + obj->fnObjectEquals = ChannelId_Compare; + + obj = HashTable_ValueObject(context->channelsByFrontId); + obj->fnObjectFree = HashStaticChannelContext_free; + + context->channelsByBackId = HashTable_New(FALSE); + if (!context->channelsByBackId) + goto error; + if (!HashTable_SetHashFunction(context->channelsByBackId, ChannelId_Hash)) + goto error; + + obj = HashTable_KeyObject(context->channelsByBackId); + obj->fnObjectEquals = ChannelId_Compare; + + return TRUE; + +error: + client_to_proxy_context_free(client, ctx); + + return FALSE; +} + +/* Proxy context free callback */ +void client_to_proxy_context_free(freerdp_peer* client, rdpContext* ctx) +{ + pServerContext* context = (pServerContext*)ctx; + + WINPR_UNUSED(client); + + if (!context) + return; + + if (context->dynvcReady) + { + CloseHandle(context->dynvcReady); + context->dynvcReady = NULL; + } + + HashTable_Free(context->interceptContextMap); + HashTable_Free(context->channelsByFrontId); + HashTable_Free(context->channelsByBackId); + + if (context->vcm && (context->vcm != INVALID_HANDLE_VALUE)) + WTSCloseServer((HANDLE)context->vcm); + context->vcm = NULL; +} + +BOOL pf_context_init_server_context(freerdp_peer* client) +{ + WINPR_ASSERT(client); + + client->ContextSize = sizeof(pServerContext); + client->ContextNew = client_to_proxy_context_new; + client->ContextFree = client_to_proxy_context_free; + + return freerdp_peer_context_new(client); +} + +static BOOL pf_context_revert_str_settings(rdpSettings* dst, const rdpSettings* before, size_t nr, + const FreeRDP_Settings_Keys_String* ids) +{ + WINPR_ASSERT(dst); + WINPR_ASSERT(before); + WINPR_ASSERT(ids || (nr == 0)); + + for (size_t x = 0; x < nr; x++) + { + FreeRDP_Settings_Keys_String id = ids[x]; + const char* what = freerdp_settings_get_string(before, id); + if (!freerdp_settings_set_string(dst, id, what)) + return FALSE; + } + + return TRUE; +} + +void intercept_context_entry_free(void* obj) +{ + InterceptContextMapEntry* entry = obj; + if (!entry) + return; + if (!entry->free) + return; + entry->free(entry); +} + +BOOL pf_context_copy_settings(rdpSettings* dst, const rdpSettings* src) +{ + BOOL rc = FALSE; + rdpSettings* before_copy = NULL; + const FreeRDP_Settings_Keys_String to_revert[] = { FreeRDP_ConfigPath, + FreeRDP_CertificateName }; + + if (!dst || !src) + return FALSE; + + before_copy = freerdp_settings_clone(dst); + if (!before_copy) + return FALSE; + + if (!freerdp_settings_copy(dst, src)) + goto out_fail; + + /* keep original ServerMode value */ + if (!freerdp_settings_copy_item(dst, before_copy, FreeRDP_ServerMode)) + goto out_fail; + + /* revert some values that must not be changed */ + if (!pf_context_revert_str_settings(dst, before_copy, ARRAYSIZE(to_revert), to_revert)) + goto out_fail; + + if (!freerdp_settings_get_bool(dst, FreeRDP_ServerMode)) + { + /* adjust instance pointer */ + if (!freerdp_settings_copy_item(dst, before_copy, FreeRDP_instance)) + goto out_fail; + + /* + * RdpServerRsaKey must be set to NULL if `dst` is client's context + * it must be freed before setting it to NULL to avoid a memory leak! + */ + + if (!freerdp_settings_set_pointer_len(dst, FreeRDP_RdpServerRsaKey, NULL, 1)) + goto out_fail; + } + + /* We handle certificate management for this client ourselfes. */ + rc = freerdp_settings_set_bool(dst, FreeRDP_ExternalCertificateManagement, TRUE); + +out_fail: + freerdp_settings_free(before_copy); + return rc; +} + +pClientContext* pf_context_create_client_context(const rdpSettings* clientSettings) +{ + RDP_CLIENT_ENTRY_POINTS clientEntryPoints; + pClientContext* pc = NULL; + rdpContext* context = NULL; + + WINPR_ASSERT(clientSettings); + + RdpClientEntry(&clientEntryPoints); + context = freerdp_client_context_new(&clientEntryPoints); + + if (!context) + return NULL; + + pc = (pClientContext*)context; + + if (!pf_context_copy_settings(context->settings, clientSettings)) + goto error; + + return pc; +error: + freerdp_client_context_free(context); + return NULL; +} + +proxyData* proxy_data_new(void) +{ + BYTE temp[16]; + char* hex = NULL; + proxyData* pdata = NULL; + + pdata = calloc(1, sizeof(proxyData)); + if (!pdata) + return NULL; + + if (!(pdata->abort_event = CreateEvent(NULL, TRUE, FALSE, NULL))) + goto error; + + if (!(pdata->gfx_server_ready = CreateEvent(NULL, TRUE, FALSE, NULL))) + goto error; + + winpr_RAND(&temp, 16); + hex = winpr_BinToHexString(temp, 16, FALSE); + if (!hex) + goto error; + + CopyMemory(pdata->session_id, hex, PROXY_SESSION_ID_LENGTH); + pdata->session_id[PROXY_SESSION_ID_LENGTH] = '\0'; + free(hex); + + if (!(pdata->modules_info = HashTable_New(FALSE))) + goto error; + + /* modules_info maps between plugin name to custom data */ + if (!HashTable_SetupForStringData(pdata->modules_info, FALSE)) + goto error; + + return pdata; +error: + WINPR_PRAGMA_DIAG_PUSH + WINPR_PRAGMA_DIAG_IGNORED_MISMATCHED_DEALLOC + proxy_data_free(pdata); + WINPR_PRAGMA_DIAG_POP + return NULL; +} + +/* updates circular pointers between proxyData and pClientContext instances */ +void proxy_data_set_client_context(proxyData* pdata, pClientContext* context) +{ + WINPR_ASSERT(pdata); + WINPR_ASSERT(context); + pdata->pc = context; + context->pdata = pdata; +} + +/* updates circular pointers between proxyData and pServerContext instances */ +void proxy_data_set_server_context(proxyData* pdata, pServerContext* context) +{ + WINPR_ASSERT(pdata); + WINPR_ASSERT(context); + pdata->ps = context; + context->pdata = pdata; +} + +void proxy_data_free(proxyData* pdata) +{ + if (!pdata) + return; + + if (pdata->abort_event) + CloseHandle(pdata->abort_event); + + if (pdata->client_thread) + CloseHandle(pdata->client_thread); + + if (pdata->gfx_server_ready) + CloseHandle(pdata->gfx_server_ready); + + if (pdata->modules_info) + HashTable_Free(pdata->modules_info); + + if (pdata->pc) + freerdp_client_context_free(&pdata->pc->context); + + free(pdata); +} + +void proxy_data_abort_connect(proxyData* pdata) +{ + WINPR_ASSERT(pdata); + WINPR_ASSERT(pdata->abort_event); + SetEvent(pdata->abort_event); + if (pdata->pc) + freerdp_abort_connect_context(&pdata->pc->context); +} + +BOOL proxy_data_shall_disconnect(proxyData* pdata) +{ + WINPR_ASSERT(pdata); + WINPR_ASSERT(pdata->abort_event); + return WaitForSingleObject(pdata->abort_event, 0) == WAIT_OBJECT_0; +} diff --git a/server/proxy/pf_input.c b/server/proxy/pf_input.c new file mode 100644 index 0000000..2ed1c14 --- /dev/null +++ b/server/proxy/pf_input.c @@ -0,0 +1,206 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2019 Mati Shabtay <matishabtay@gmail.com> + * Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com> + * Copyright 2019 Idan Freiberg <speidy@gmail.com> + * Copyright 2021 Armin Novak <anovak@thincast.com> + * Copyright 2021 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 "pf_input.h" +#include <freerdp/server/proxy/proxy_config.h> +#include <freerdp/server/proxy/proxy_context.h> + +#include "proxy_modules.h" + +static BOOL pf_server_check_and_sync_input_state(pClientContext* pc) +{ + WINPR_ASSERT(pc); + + if (!freerdp_is_active_state(&pc->context)) + return FALSE; + if (pc->input_state_sync_pending) + { + BOOL rc = freerdp_input_send_synchronize_event(pc->context.input, pc->input_state); + if (rc) + pc->input_state_sync_pending = FALSE; + } + return TRUE; +} + +static BOOL pf_server_synchronize_event(rdpInput* input, UINT32 flags) +{ + pServerContext* ps = NULL; + pClientContext* pc = NULL; + + WINPR_ASSERT(input); + ps = (pServerContext*)input->context; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->pdata); + + pc = ps->pdata->pc; + WINPR_ASSERT(pc); + + pc->input_state = flags; + pc->input_state_sync_pending = TRUE; + + pf_server_check_and_sync_input_state(pc); + return TRUE; +} + +static BOOL pf_server_keyboard_event(rdpInput* input, UINT16 flags, UINT8 code) +{ + const proxyConfig* config = NULL; + proxyKeyboardEventInfo event = { 0 }; + pServerContext* ps = NULL; + pClientContext* pc = NULL; + + WINPR_ASSERT(input); + ps = (pServerContext*)input->context; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->pdata); + + pc = ps->pdata->pc; + WINPR_ASSERT(pc); + + config = ps->pdata->config; + WINPR_ASSERT(config); + + if (!pf_server_check_and_sync_input_state(pc)) + return TRUE; + + if (!config->Keyboard) + return TRUE; + + event.flags = flags; + event.rdp_scan_code = code; + + if (pf_modules_run_filter(pc->pdata->module, FILTER_TYPE_KEYBOARD, pc->pdata, &event)) + return freerdp_input_send_keyboard_event(pc->context.input, flags, code); + + return TRUE; +} + +static BOOL pf_server_unicode_keyboard_event(rdpInput* input, UINT16 flags, UINT16 code) +{ + const proxyConfig* config = NULL; + proxyUnicodeEventInfo event = { 0 }; + pServerContext* ps = NULL; + pClientContext* pc = NULL; + + WINPR_ASSERT(input); + ps = (pServerContext*)input->context; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->pdata); + + pc = ps->pdata->pc; + WINPR_ASSERT(pc); + + config = ps->pdata->config; + WINPR_ASSERT(config); + + if (!pf_server_check_and_sync_input_state(pc)) + return TRUE; + + if (!config->Keyboard) + return TRUE; + + event.flags = flags; + event.code = code; + if (pf_modules_run_filter(pc->pdata->module, FILTER_TYPE_UNICODE, pc->pdata, &event)) + return freerdp_input_send_unicode_keyboard_event(pc->context.input, flags, code); + return TRUE; +} + +static BOOL pf_server_mouse_event(rdpInput* input, UINT16 flags, UINT16 x, UINT16 y) +{ + proxyMouseEventInfo event = { 0 }; + const proxyConfig* config = NULL; + pServerContext* ps = NULL; + pClientContext* pc = NULL; + + WINPR_ASSERT(input); + ps = (pServerContext*)input->context; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->pdata); + + pc = ps->pdata->pc; + WINPR_ASSERT(pc); + + config = ps->pdata->config; + WINPR_ASSERT(config); + + if (!pf_server_check_and_sync_input_state(pc)) + return TRUE; + + if (!config->Mouse) + return TRUE; + + event.flags = flags; + event.x = x; + event.y = y; + + if (pf_modules_run_filter(pc->pdata->module, FILTER_TYPE_MOUSE, pc->pdata, &event)) + return freerdp_input_send_mouse_event(pc->context.input, flags, x, y); + + return TRUE; +} + +static BOOL pf_server_extended_mouse_event(rdpInput* input, UINT16 flags, UINT16 x, UINT16 y) +{ + const proxyConfig* config = NULL; + proxyMouseExEventInfo event = { 0 }; + pServerContext* ps = NULL; + pClientContext* pc = NULL; + + WINPR_ASSERT(input); + ps = (pServerContext*)input->context; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->pdata); + + pc = ps->pdata->pc; + WINPR_ASSERT(pc); + + config = ps->pdata->config; + WINPR_ASSERT(config); + + if (!pf_server_check_and_sync_input_state(pc)) + return TRUE; + + if (!config->Mouse) + return TRUE; + + event.flags = flags; + event.x = x; + event.y = y; + if (pf_modules_run_filter(pc->pdata->module, FILTER_TYPE_MOUSE, pc->pdata, &event)) + return freerdp_input_send_extended_mouse_event(pc->context.input, flags, x, y); + return TRUE; +} + +void pf_server_register_input_callbacks(rdpInput* input) +{ + WINPR_ASSERT(input); + + input->SynchronizeEvent = pf_server_synchronize_event; + input->KeyboardEvent = pf_server_keyboard_event; + input->UnicodeKeyboardEvent = pf_server_unicode_keyboard_event; + input->MouseEvent = pf_server_mouse_event; + input->ExtendedMouseEvent = pf_server_extended_mouse_event; +} diff --git a/server/proxy/pf_input.h b/server/proxy/pf_input.h new file mode 100644 index 0000000..ea9c542 --- /dev/null +++ b/server/proxy/pf_input.h @@ -0,0 +1,29 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2019 Mati Shabtay <matishabtay@gmail.com> + * Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com> + * Copyright 2019 Idan Freiberg <speidy@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_SERVER_PROXY_PFINPUT_H +#define FREERDP_SERVER_PROXY_PFINPUT_H + +#include <freerdp/freerdp.h> + +void pf_server_register_input_callbacks(rdpInput* input); + +#endif /* FREERDP_SERVER_PROXY_PFINPUT_H */ diff --git a/server/proxy/pf_modules.c b/server/proxy/pf_modules.c new file mode 100644 index 0000000..e3447cb --- /dev/null +++ b/server/proxy/pf_modules.c @@ -0,0 +1,633 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server modules API + * + * Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com> + * Copyright 2019 Idan Freiberg <speidy@gmail.com> + * Copyright 2021 Armin Novak <anovak@thincast.com> + * Copyright 2021 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 <winpr/file.h> +#include <winpr/wlog.h> +#include <winpr/path.h> +#include <winpr/library.h> +#include <freerdp/api.h> +#include <freerdp/build-config.h> + +#include <freerdp/server/proxy/proxy_log.h> +#include <freerdp/server/proxy/proxy_modules_api.h> + +#include <freerdp/server/proxy/proxy_context.h> +#include "proxy_modules.h" + +#define TAG PROXY_TAG("modules") + +#define MODULE_ENTRY_POINT "proxy_module_entry_point" + +struct proxy_module +{ + proxyPluginsManager mgr; + wArrayList* plugins; + wArrayList* handles; +}; + +static const char* pf_modules_get_filter_type_string(PF_FILTER_TYPE result) +{ + switch (result) + { + case FILTER_TYPE_KEYBOARD: + return "FILTER_TYPE_KEYBOARD"; + case FILTER_TYPE_UNICODE: + return "FILTER_TYPE_UNICODE"; + case FILTER_TYPE_MOUSE: + return "FILTER_TYPE_MOUSE"; + case FILTER_TYPE_MOUSE_EX: + return "FILTER_TYPE_MOUSE_EX"; + case FILTER_TYPE_CLIENT_PASSTHROUGH_CHANNEL_DATA: + return "FILTER_TYPE_CLIENT_PASSTHROUGH_CHANNEL_DATA"; + case FILTER_TYPE_SERVER_PASSTHROUGH_CHANNEL_DATA: + return "FILTER_TYPE_SERVER_PASSTHROUGH_CHANNEL_DATA"; + case FILTER_TYPE_CLIENT_PASSTHROUGH_DYN_CHANNEL_CREATE: + return "FILTER_TYPE_CLIENT_PASSTHROUGH_DYN_CHANNEL_CREATE"; + case FILTER_TYPE_SERVER_FETCH_TARGET_ADDR: + return "FILTER_TYPE_SERVER_FETCH_TARGET_ADDR"; + case FILTER_TYPE_SERVER_PEER_LOGON: + return "FILTER_TYPE_SERVER_PEER_LOGON"; + case FILTER_TYPE_CLIENT_PASSTHROUGH_CHANNEL_CREATE: + return "FILTER_TYPE_CLIENT_PASSTHROUGH_CHANNEL_CREATE"; + case FILTER_LAST: + return "FILTER_LAST"; + default: + return "FILTER_UNKNOWN"; + } +} + +static const char* pf_modules_get_hook_type_string(PF_HOOK_TYPE result) +{ + switch (result) + { + case HOOK_TYPE_CLIENT_INIT_CONNECT: + return "HOOK_TYPE_CLIENT_INIT_CONNECT"; + case HOOK_TYPE_CLIENT_UNINIT_CONNECT: + return "HOOK_TYPE_CLIENT_UNINIT_CONNECT"; + case HOOK_TYPE_CLIENT_PRE_CONNECT: + return "HOOK_TYPE_CLIENT_PRE_CONNECT"; + case HOOK_TYPE_CLIENT_POST_CONNECT: + return "HOOK_TYPE_CLIENT_POST_CONNECT"; + case HOOK_TYPE_CLIENT_POST_DISCONNECT: + return "HOOK_TYPE_CLIENT_POST_DISCONNECT"; + case HOOK_TYPE_CLIENT_REDIRECT: + return "HOOK_TYPE_CLIENT_REDIRECT"; + case HOOK_TYPE_CLIENT_VERIFY_X509: + return "HOOK_TYPE_CLIENT_VERIFY_X509"; + case HOOK_TYPE_CLIENT_LOGIN_FAILURE: + return "HOOK_TYPE_CLIENT_LOGIN_FAILURE"; + case HOOK_TYPE_CLIENT_END_PAINT: + return "HOOK_TYPE_CLIENT_END_PAINT"; + case HOOK_TYPE_SERVER_POST_CONNECT: + return "HOOK_TYPE_SERVER_POST_CONNECT"; + case HOOK_TYPE_SERVER_ACTIVATE: + return "HOOK_TYPE_SERVER_ACTIVATE"; + case HOOK_TYPE_SERVER_CHANNELS_INIT: + return "HOOK_TYPE_SERVER_CHANNELS_INIT"; + case HOOK_TYPE_SERVER_CHANNELS_FREE: + return "HOOK_TYPE_SERVER_CHANNELS_FREE"; + case HOOK_TYPE_SERVER_SESSION_END: + return "HOOK_TYPE_SERVER_SESSION_END"; + case HOOK_TYPE_CLIENT_LOAD_CHANNELS: + return "HOOK_TYPE_CLIENT_LOAD_CHANNELS"; + case HOOK_TYPE_SERVER_SESSION_INITIALIZE: + return "HOOK_TYPE_SERVER_SESSION_INITIALIZE"; + case HOOK_TYPE_SERVER_SESSION_STARTED: + return "HOOK_TYPE_SERVER_SESSION_STARTED"; + case HOOK_LAST: + return "HOOK_LAST"; + default: + return "HOOK_TYPE_UNKNOWN"; + } +} + +static BOOL pf_modules_proxy_ArrayList_ForEachFkt(void* data, size_t index, va_list ap) +{ + proxyPlugin* plugin = (proxyPlugin*)data; + BOOL ok = FALSE; + + WINPR_UNUSED(index); + + PF_HOOK_TYPE type = va_arg(ap, PF_HOOK_TYPE); + proxyData* pdata = va_arg(ap, proxyData*); + void* custom = va_arg(ap, void*); + + WLog_VRB(TAG, "running hook %s.%s", plugin->name, pf_modules_get_hook_type_string(type)); + + switch (type) + { + case HOOK_TYPE_CLIENT_INIT_CONNECT: + ok = IFCALLRESULT(TRUE, plugin->ClientInitConnect, plugin, pdata, custom); + break; + case HOOK_TYPE_CLIENT_UNINIT_CONNECT: + ok = IFCALLRESULT(TRUE, plugin->ClientUninitConnect, plugin, pdata, custom); + break; + case HOOK_TYPE_CLIENT_PRE_CONNECT: + ok = IFCALLRESULT(TRUE, plugin->ClientPreConnect, plugin, pdata, custom); + break; + + case HOOK_TYPE_CLIENT_POST_CONNECT: + ok = IFCALLRESULT(TRUE, plugin->ClientPostConnect, plugin, pdata, custom); + break; + + case HOOK_TYPE_CLIENT_REDIRECT: + ok = IFCALLRESULT(TRUE, plugin->ClientRedirect, plugin, pdata, custom); + break; + + case HOOK_TYPE_CLIENT_POST_DISCONNECT: + ok = IFCALLRESULT(TRUE, plugin->ClientPostDisconnect, plugin, pdata, custom); + break; + + case HOOK_TYPE_CLIENT_VERIFY_X509: + ok = IFCALLRESULT(TRUE, plugin->ClientX509Certificate, plugin, pdata, custom); + break; + + case HOOK_TYPE_CLIENT_LOGIN_FAILURE: + ok = IFCALLRESULT(TRUE, plugin->ClientLoginFailure, plugin, pdata, custom); + break; + + case HOOK_TYPE_CLIENT_END_PAINT: + ok = IFCALLRESULT(TRUE, plugin->ClientEndPaint, plugin, pdata, custom); + break; + + case HOOK_TYPE_CLIENT_LOAD_CHANNELS: + ok = IFCALLRESULT(TRUE, plugin->ClientLoadChannels, plugin, pdata, custom); + break; + + case HOOK_TYPE_SERVER_POST_CONNECT: + ok = IFCALLRESULT(TRUE, plugin->ServerPostConnect, plugin, pdata, custom); + break; + + case HOOK_TYPE_SERVER_ACTIVATE: + ok = IFCALLRESULT(TRUE, plugin->ServerPeerActivate, plugin, pdata, custom); + break; + + case HOOK_TYPE_SERVER_CHANNELS_INIT: + ok = IFCALLRESULT(TRUE, plugin->ServerChannelsInit, plugin, pdata, custom); + break; + + case HOOK_TYPE_SERVER_CHANNELS_FREE: + ok = IFCALLRESULT(TRUE, plugin->ServerChannelsFree, plugin, pdata, custom); + break; + + case HOOK_TYPE_SERVER_SESSION_END: + ok = IFCALLRESULT(TRUE, plugin->ServerSessionEnd, plugin, pdata, custom); + break; + + case HOOK_TYPE_SERVER_SESSION_INITIALIZE: + ok = IFCALLRESULT(TRUE, plugin->ServerSessionInitialize, plugin, pdata, custom); + break; + + case HOOK_TYPE_SERVER_SESSION_STARTED: + ok = IFCALLRESULT(TRUE, plugin->ServerSessionStarted, plugin, pdata, custom); + break; + + case HOOK_LAST: + default: + WLog_ERR(TAG, "invalid hook called"); + } + + if (!ok) + { + WLog_INFO(TAG, "plugin %s, hook %s failed!", plugin->name, + pf_modules_get_hook_type_string(type)); + return FALSE; + } + return TRUE; +} + +/* + * runs all hooks of type `type`. + * + * @type: hook type to run. + * @server: pointer of server's rdpContext struct of the current session. + */ +BOOL pf_modules_run_hook(proxyModule* module, PF_HOOK_TYPE type, proxyData* pdata, void* custom) +{ + WINPR_ASSERT(module); + WINPR_ASSERT(module->plugins); + return ArrayList_ForEach(module->plugins, pf_modules_proxy_ArrayList_ForEachFkt, type, pdata, + custom); +} + +static BOOL pf_modules_ArrayList_ForEachFkt(void* data, size_t index, va_list ap) +{ + proxyPlugin* plugin = (proxyPlugin*)data; + BOOL result = FALSE; + + WINPR_UNUSED(index); + + PF_FILTER_TYPE type = va_arg(ap, PF_FILTER_TYPE); + proxyData* pdata = va_arg(ap, proxyData*); + void* param = va_arg(ap, void*); + + WLog_VRB(TAG, "running filter: %s", plugin->name); + + switch (type) + { + case FILTER_TYPE_KEYBOARD: + result = IFCALLRESULT(TRUE, plugin->KeyboardEvent, plugin, pdata, param); + break; + + case FILTER_TYPE_UNICODE: + result = IFCALLRESULT(TRUE, plugin->UnicodeEvent, plugin, pdata, param); + break; + + case FILTER_TYPE_MOUSE: + result = IFCALLRESULT(TRUE, plugin->MouseEvent, plugin, pdata, param); + break; + + case FILTER_TYPE_MOUSE_EX: + result = IFCALLRESULT(TRUE, plugin->MouseExEvent, plugin, pdata, param); + break; + + case FILTER_TYPE_CLIENT_PASSTHROUGH_CHANNEL_DATA: + result = IFCALLRESULT(TRUE, plugin->ClientChannelData, plugin, pdata, param); + break; + + case FILTER_TYPE_SERVER_PASSTHROUGH_CHANNEL_DATA: + result = IFCALLRESULT(TRUE, plugin->ServerChannelData, plugin, pdata, param); + break; + + case FILTER_TYPE_CLIENT_PASSTHROUGH_CHANNEL_CREATE: + result = IFCALLRESULT(TRUE, plugin->ChannelCreate, plugin, pdata, param); + break; + + case FILTER_TYPE_CLIENT_PASSTHROUGH_DYN_CHANNEL_CREATE: + result = IFCALLRESULT(TRUE, plugin->DynamicChannelCreate, plugin, pdata, param); + break; + + case FILTER_TYPE_SERVER_FETCH_TARGET_ADDR: + result = IFCALLRESULT(TRUE, plugin->ServerFetchTargetAddr, plugin, pdata, param); + break; + + case FILTER_TYPE_SERVER_PEER_LOGON: + result = IFCALLRESULT(TRUE, plugin->ServerPeerLogon, plugin, pdata, param); + break; + + case FILTER_TYPE_INTERCEPT_CHANNEL: + result = IFCALLRESULT(TRUE, plugin->DynChannelIntercept, plugin, pdata, param); + break; + + case FILTER_TYPE_DYN_INTERCEPT_LIST: + result = IFCALLRESULT(TRUE, plugin->DynChannelToIntercept, plugin, pdata, param); + break; + + case FILTER_TYPE_STATIC_INTERCEPT_LIST: + result = IFCALLRESULT(TRUE, plugin->StaticChannelToIntercept, plugin, pdata, param); + break; + + case FILTER_LAST: + default: + WLog_ERR(TAG, "invalid filter called"); + } + + if (!result) + { + /* current filter return FALSE, no need to run other filters. */ + WLog_DBG(TAG, "plugin %s, filter type [%s] returned FALSE", plugin->name, + pf_modules_get_filter_type_string(type)); + } + return result; +} + +/* + * runs all filters of type `type`. + * + * @type: filter type to run. + * @server: pointer of server's rdpContext struct of the current session. + */ +BOOL pf_modules_run_filter(proxyModule* module, PF_FILTER_TYPE type, proxyData* pdata, void* param) +{ + WINPR_ASSERT(module); + WINPR_ASSERT(module->plugins); + + return ArrayList_ForEach(module->plugins, pf_modules_ArrayList_ForEachFkt, type, pdata, param); +} + +/* + * stores per-session data needed by a plugin. + * + * @context: current session server's rdpContext instance. + * @info: pointer to per-session data. + */ +static BOOL pf_modules_set_plugin_data(proxyPluginsManager* mgr, const char* plugin_name, + proxyData* pdata, void* data) +{ + union + { + const char* ccp; + char* cp; + } ccharconv; + + WINPR_ASSERT(plugin_name); + + ccharconv.ccp = plugin_name; + if (data == NULL) /* no need to store anything */ + return FALSE; + + if (!HashTable_Insert(pdata->modules_info, ccharconv.cp, data)) + { + WLog_ERR(TAG, "[%s]: HashTable_Insert failed!"); + return FALSE; + } + + return TRUE; +} + +/* + * returns per-session data needed a plugin. + * + * @context: current session server's rdpContext instance. + * if there's no data related to `plugin_name` in `context` (current session), a NULL will be + * returned. + */ +static void* pf_modules_get_plugin_data(proxyPluginsManager* mgr, const char* plugin_name, + proxyData* pdata) +{ + union + { + const char* ccp; + char* cp; + } ccharconv; + WINPR_ASSERT(plugin_name); + WINPR_ASSERT(pdata); + ccharconv.ccp = plugin_name; + + return HashTable_GetItemValue(pdata->modules_info, ccharconv.cp); +} + +static void pf_modules_abort_connect(proxyPluginsManager* mgr, proxyData* pdata) +{ + WINPR_ASSERT(pdata); + WLog_DBG(TAG, "is called!"); + proxy_data_abort_connect(pdata); +} + +static BOOL pf_modules_register_ArrayList_ForEachFkt(void* data, size_t index, va_list ap) +{ + proxyPlugin* plugin = (proxyPlugin*)data; + proxyPlugin* plugin_to_register = va_arg(ap, proxyPlugin*); + + WINPR_UNUSED(index); + + if (strcmp(plugin->name, plugin_to_register->name) == 0) + { + WLog_ERR(TAG, "can not register plugin '%s', it is already registered!", plugin->name); + return FALSE; + } + return TRUE; +} + +static BOOL pf_modules_register_plugin(proxyPluginsManager* mgr, + const proxyPlugin* plugin_to_register) +{ + proxyPlugin internal = { 0 }; + proxyModule* module = (proxyModule*)mgr; + WINPR_ASSERT(module); + + if (!plugin_to_register) + return FALSE; + + internal = *plugin_to_register; + internal.mgr = mgr; + + /* make sure there's no other loaded plugin with the same name of `plugin_to_register`. */ + if (!ArrayList_ForEach(module->plugins, pf_modules_register_ArrayList_ForEachFkt, &internal)) + return FALSE; + + if (!ArrayList_Append(module->plugins, &internal)) + { + WLog_ERR(TAG, "failed adding plugin to list: %s", plugin_to_register->name); + return FALSE; + } + + return TRUE; +} + +static BOOL pf_modules_load_ArrayList_ForEachFkt(void* data, size_t index, va_list ap) +{ + proxyPlugin* plugin = (proxyPlugin*)data; + const char* plugin_name = va_arg(ap, const char*); + BOOL* res = va_arg(ap, BOOL*); + + WINPR_UNUSED(index); + WINPR_UNUSED(ap); + WINPR_ASSERT(res); + + if (strcmp(plugin->name, plugin_name) == 0) + *res = TRUE; + return TRUE; +} + +BOOL pf_modules_is_plugin_loaded(proxyModule* module, const char* plugin_name) +{ + BOOL rc = FALSE; + WINPR_ASSERT(module); + if (ArrayList_Count(module->plugins) < 1) + return FALSE; + if (!ArrayList_ForEach(module->plugins, pf_modules_load_ArrayList_ForEachFkt, plugin_name, &rc)) + return FALSE; + return rc; +} + +static BOOL pf_modules_print_ArrayList_ForEachFkt(void* data, size_t index, va_list ap) +{ + proxyPlugin* plugin = (proxyPlugin*)data; + + WINPR_UNUSED(index); + WINPR_UNUSED(ap); + + WLog_INFO(TAG, "\tName: %s", plugin->name); + WLog_INFO(TAG, "\tDescription: %s", plugin->description); + return TRUE; +} + +void pf_modules_list_loaded_plugins(proxyModule* module) +{ + size_t count = 0; + + WINPR_ASSERT(module); + WINPR_ASSERT(module->plugins); + + count = ArrayList_Count(module->plugins); + + if (count > 0) + WLog_INFO(TAG, "Loaded plugins:"); + + ArrayList_ForEach(module->plugins, pf_modules_print_ArrayList_ForEachFkt); +} + +static BOOL pf_modules_load_module(const char* module_path, proxyModule* module, void* userdata) +{ + HMODULE handle = NULL; + proxyModuleEntryPoint pEntryPoint = NULL; + WINPR_ASSERT(module); + + handle = LoadLibraryX(module_path); + + if (handle == NULL) + { + WLog_ERR(TAG, "failed loading external library: %s", module_path); + return FALSE; + } + + pEntryPoint = (proxyModuleEntryPoint)GetProcAddress(handle, MODULE_ENTRY_POINT); + if (!pEntryPoint) + { + WLog_ERR(TAG, "GetProcAddress failed while loading %s", module_path); + goto error; + } + if (!ArrayList_Append(module->handles, handle)) + { + WLog_ERR(TAG, "ArrayList_Append failed!"); + goto error; + } + return pf_modules_add(module, pEntryPoint, userdata); + +error: + FreeLibrary(handle); + return FALSE; +} + +static void free_handle(void* obj) +{ + HANDLE handle = (HANDLE)obj; + if (handle) + FreeLibrary(handle); +} + +static void free_plugin(void* obj) +{ + proxyPlugin* plugin = (proxyPlugin*)obj; + WINPR_ASSERT(plugin); + + if (!IFCALLRESULT(TRUE, plugin->PluginUnload, plugin)) + WLog_WARN(TAG, "PluginUnload failed for plugin '%s'", plugin->name); + + free(plugin); +} + +static void* new_plugin(const void* obj) +{ + const proxyPlugin* src = obj; + proxyPlugin* proxy = calloc(1, sizeof(proxyPlugin)); + if (!proxy) + return NULL; + *proxy = *src; + return proxy; +} + +proxyModule* pf_modules_new(const char* root_dir, const char** modules, size_t count) +{ + wObject* obj = NULL; + char* path = NULL; + proxyModule* module = calloc(1, sizeof(proxyModule)); + if (!module) + return NULL; + + module->mgr.RegisterPlugin = pf_modules_register_plugin; + module->mgr.SetPluginData = pf_modules_set_plugin_data; + module->mgr.GetPluginData = pf_modules_get_plugin_data; + module->mgr.AbortConnect = pf_modules_abort_connect; + module->plugins = ArrayList_New(FALSE); + + if (module->plugins == NULL) + { + WLog_ERR(TAG, "ArrayList_New failed!"); + goto error; + } + obj = ArrayList_Object(module->plugins); + WINPR_ASSERT(obj); + + obj->fnObjectFree = free_plugin; + obj->fnObjectNew = new_plugin; + + module->handles = ArrayList_New(FALSE); + if (module->handles == NULL) + { + + WLog_ERR(TAG, "ArrayList_New failed!"); + goto error; + } + ArrayList_Object(module->handles)->fnObjectFree = free_handle; + + if (count > 0) + { + WINPR_ASSERT(root_dir); + if (!winpr_PathFileExists(root_dir)) + path = GetCombinedPath(FREERDP_INSTALL_PREFIX, root_dir); + else + path = _strdup(root_dir); + + if (!winpr_PathFileExists(path)) + { + if (!winpr_PathMakePath(path, NULL)) + { + WLog_ERR(TAG, "error occurred while creating modules directory: %s", root_dir); + goto error; + } + } + + if (winpr_PathFileExists(path)) + WLog_DBG(TAG, "modules root directory: %s", path); + + for (size_t i = 0; i < count; i++) + { + char name[8192] = { 0 }; + char* fullpath = NULL; + _snprintf(name, sizeof(name), "proxy-%s-plugin%s", modules[i], + FREERDP_SHARED_LIBRARY_SUFFIX); + fullpath = GetCombinedPath(path, name); + pf_modules_load_module(fullpath, module, NULL); + free(fullpath); + } + } + + free(path); + return module; + +error: + free(path); + pf_modules_free(module); + return NULL; +} + +void pf_modules_free(proxyModule* module) +{ + if (!module) + return; + + ArrayList_Free(module->plugins); + ArrayList_Free(module->handles); + free(module); +} + +BOOL pf_modules_add(proxyModule* module, proxyModuleEntryPoint ep, void* userdata) +{ + WINPR_ASSERT(module); + WINPR_ASSERT(ep); + + return ep(&module->mgr, userdata); +} diff --git a/server/proxy/pf_server.c b/server/proxy/pf_server.c new file mode 100644 index 0000000..545ab93 --- /dev/null +++ b/server/proxy/pf_server.c @@ -0,0 +1,1073 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2019 Mati Shabtay <matishabtay@gmail.com> + * Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com> + * Copyright 2019 Idan Freiberg <speidy@gmail.com> + * Copyright 2021 Armin Novak <anovak@thincast.com> + * Copyright 2021 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/crt.h> +#include <winpr/ssl.h> +#include <winpr/path.h> +#include <winpr/synch.h> +#include <winpr/string.h> +#include <winpr/winsock.h> +#include <winpr/thread.h> +#include <errno.h> + +#include <freerdp/freerdp.h> +#include <freerdp/streamdump.h> +#include <freerdp/channels/wtsvc.h> +#include <freerdp/channels/channels.h> +#include <freerdp/channels/drdynvc.h> +#include <freerdp/build-config.h> + +#include <freerdp/channels/rdpdr.h> + +#include <freerdp/server/proxy/proxy_server.h> +#include <freerdp/server/proxy/proxy_log.h> + +#include "pf_server.h" +#include "pf_channel.h" +#include <freerdp/server/proxy/proxy_config.h> +#include "pf_client.h" +#include <freerdp/server/proxy/proxy_context.h> +#include "pf_update.h" +#include "proxy_modules.h" +#include "pf_utils.h" +#include "channels/pf_channel_drdynvc.h" +#include "channels/pf_channel_rdpdr.h" + +#define TAG PROXY_TAG("server") + +typedef struct +{ + HANDLE thread; + freerdp_peer* client; +} peer_thread_args; + +static BOOL pf_server_parse_target_from_routing_token(rdpContext* context, rdpSettings* settings, + FreeRDP_Settings_Keys_String targetID, + FreeRDP_Settings_Keys_UInt32 portID) +{ +#define TARGET_MAX (100) +#define ROUTING_TOKEN_PREFIX "Cookie: msts=" + char* colon = NULL; + size_t len = 0; + DWORD routing_token_length = 0; + const size_t prefix_len = strnlen(ROUTING_TOKEN_PREFIX, sizeof(ROUTING_TOKEN_PREFIX)); + const char* routing_token = freerdp_nego_get_routing_token(context, &routing_token_length); + pServerContext* ps = (pServerContext*)context; + + if (!routing_token) + return FALSE; + + if ((routing_token_length <= prefix_len) || (routing_token_length >= TARGET_MAX)) + { + PROXY_LOG_ERR(TAG, ps, "invalid routing token length: %" PRIu32 "", routing_token_length); + return FALSE; + } + + len = routing_token_length - prefix_len; + + if (!freerdp_settings_set_string_len(settings, targetID, routing_token + prefix_len, len)) + return FALSE; + + const char* target = freerdp_settings_get_string(settings, targetID); + colon = strchr(target, ':'); + + if (colon) + { + /* port is specified */ + unsigned long p = strtoul(colon + 1, NULL, 10); + + if (p > USHRT_MAX) + return FALSE; + + if (!freerdp_settings_set_uint32(settings, portID, p)) + return FALSE; + } + + return TRUE; +} + +static BOOL pf_server_get_target_info(rdpContext* context, rdpSettings* settings, + const proxyConfig* config) +{ + pServerContext* ps = (pServerContext*)context; + proxyFetchTargetEventInfo ev = { 0 }; + + WINPR_ASSERT(settings); + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->pdata); + + ev.fetch_method = config->FixedTarget ? PROXY_FETCH_TARGET_METHOD_CONFIG + : PROXY_FETCH_TARGET_METHOD_LOAD_BALANCE_INFO; + + if (!pf_modules_run_filter(ps->pdata->module, FILTER_TYPE_SERVER_FETCH_TARGET_ADDR, ps->pdata, + &ev)) + return FALSE; + + switch (ev.fetch_method) + { + case PROXY_FETCH_TARGET_METHOD_DEFAULT: + case PROXY_FETCH_TARGET_METHOD_LOAD_BALANCE_INFO: + return pf_server_parse_target_from_routing_token( + context, settings, FreeRDP_ServerHostname, FreeRDP_ServerPort); + + case PROXY_FETCH_TARGET_METHOD_CONFIG: + { + WINPR_ASSERT(config); + + if (config->TargetPort > 0) + freerdp_settings_set_uint32(settings, FreeRDP_ServerPort, config->TargetPort); + else + freerdp_settings_set_uint32(settings, FreeRDP_ServerPort, 3389); + + if (!freerdp_settings_set_uint32(settings, FreeRDP_TlsSecLevel, + config->TargetTlsSecLevel)) + return FALSE; + + if (!freerdp_settings_set_string(settings, FreeRDP_ServerHostname, config->TargetHost)) + { + PROXY_LOG_ERR(TAG, ps, "strdup failed!"); + return FALSE; + } + + if (config->TargetUser) + freerdp_settings_set_string(settings, FreeRDP_Username, config->TargetUser); + + if (config->TargetDomain) + freerdp_settings_set_string(settings, FreeRDP_Domain, config->TargetDomain); + + if (config->TargetPassword) + freerdp_settings_set_string(settings, FreeRDP_Password, config->TargetPassword); + + return TRUE; + } + case PROXY_FETCH_TARGET_USE_CUSTOM_ADDR: + { + if (!ev.target_address) + { + PROXY_LOG_ERR(TAG, ps, + "router: using CUSTOM_ADDR fetch method, but target_address == NULL"); + return FALSE; + } + + if (!freerdp_settings_set_string(settings, FreeRDP_ServerHostname, ev.target_address)) + { + PROXY_LOG_ERR(TAG, ps, "strdup failed!"); + return FALSE; + } + + free(ev.target_address); + return freerdp_settings_set_uint32(settings, FreeRDP_ServerPort, ev.target_port); + } + default: + PROXY_LOG_ERR(TAG, ps, "unknown target fetch method: %d", ev.fetch_method); + return FALSE; + } + + return TRUE; +} + +static BOOL pf_server_setup_channels(freerdp_peer* peer) +{ + char** accepted_channels = NULL; + size_t accepted_channels_count = 0; + pServerContext* ps = (pServerContext*)peer->context; + + accepted_channels = WTSGetAcceptedChannelNames(peer, &accepted_channels_count); + if (!accepted_channels) + return TRUE; + + for (size_t i = 0; i < accepted_channels_count; i++) + { + pServerStaticChannelContext* channelContext = NULL; + const char* cname = accepted_channels[i]; + UINT16 channelId = WTSChannelGetId(peer, cname); + + PROXY_LOG_INFO(TAG, ps, "Accepted channel: %s (%" PRIu16 ")", cname, channelId); + channelContext = StaticChannelContext_new(ps, cname, channelId); + if (!channelContext) + { + PROXY_LOG_ERR(TAG, ps, "error seting up channelContext for '%s'", cname); + return FALSE; + } + + if (strcmp(cname, DRDYNVC_SVC_CHANNEL_NAME) == 0) + { + if (!pf_channel_setup_drdynvc(ps->pdata, channelContext)) + { + PROXY_LOG_ERR(TAG, ps, "error while setting up dynamic channel"); + StaticChannelContext_free(channelContext); + return FALSE; + } + } + else if (strcmp(cname, RDPDR_SVC_CHANNEL_NAME) == 0 && + (channelContext->channelMode == PF_UTILS_CHANNEL_INTERCEPT)) + { + if (!pf_channel_setup_rdpdr(ps, channelContext)) + { + PROXY_LOG_ERR(TAG, ps, "error while setting up redirection channel"); + StaticChannelContext_free(channelContext); + return FALSE; + } + } + else + { + if (!pf_channel_setup_generic(channelContext)) + { + PROXY_LOG_ERR(TAG, ps, "error while setting up generic channel"); + StaticChannelContext_free(channelContext); + return FALSE; + } + } + + if (!HashTable_Insert(ps->channelsByFrontId, &channelContext->front_channel_id, + channelContext)) + { + StaticChannelContext_free(channelContext); + PROXY_LOG_ERR(TAG, ps, "error inserting channelContext in byId table for '%s'", cname); + return FALSE; + } + } + + free(accepted_channels); + return TRUE; +} + +/* Event callbacks */ + +/** + * This callback is called when the entire connection sequence is done (as + * described in MS-RDPBCGR section 1.3) + * + * The server may start sending graphics output and receiving keyboard/mouse + * input after this callback returns. + */ +static BOOL pf_server_post_connect(freerdp_peer* peer) +{ + pServerContext* ps = NULL; + pClientContext* pc = NULL; + rdpSettings* client_settings = NULL; + proxyData* pdata = NULL; + rdpSettings* frontSettings = NULL; + + WINPR_ASSERT(peer); + + ps = (pServerContext*)peer->context; + WINPR_ASSERT(ps); + + frontSettings = peer->context->settings; + WINPR_ASSERT(frontSettings); + + pdata = ps->pdata; + WINPR_ASSERT(pdata); + + const char* ClientHostname = freerdp_settings_get_string(frontSettings, FreeRDP_ClientHostname); + PROXY_LOG_INFO(TAG, ps, "Accepted client: %s", ClientHostname); + if (!pf_server_setup_channels(peer)) + { + PROXY_LOG_ERR(TAG, ps, "error setting up channels"); + return FALSE; + } + + pc = pf_context_create_client_context(frontSettings); + if (pc == NULL) + { + PROXY_LOG_ERR(TAG, ps, "failed to create client context!"); + return FALSE; + } + + client_settings = pc->context.settings; + + /* keep both sides of the connection in pdata */ + proxy_data_set_client_context(pdata, pc); + + if (!pf_server_get_target_info(peer->context, client_settings, pdata->config)) + { + PROXY_LOG_INFO(TAG, ps, "pf_server_get_target_info failed!"); + return FALSE; + } + + PROXY_LOG_INFO(TAG, ps, "remote target is %s:%" PRIu32 "", + freerdp_settings_get_string(client_settings, FreeRDP_ServerHostname), + freerdp_settings_get_uint32(client_settings, FreeRDP_ServerPort)); + + if (!pf_modules_run_hook(pdata->module, HOOK_TYPE_SERVER_POST_CONNECT, pdata, peer)) + return FALSE; + + /* Start a proxy's client in it's own thread */ + if (!(pdata->client_thread = CreateThread(NULL, 0, pf_client_start, pc, 0, NULL))) + { + PROXY_LOG_ERR(TAG, ps, "failed to create client thread"); + return FALSE; + } + + return TRUE; +} + +static BOOL pf_server_activate(freerdp_peer* peer) +{ + pServerContext* ps = NULL; + proxyData* pdata = NULL; + rdpSettings* settings = NULL; + + WINPR_ASSERT(peer); + + ps = (pServerContext*)peer->context; + WINPR_ASSERT(ps); + + pdata = ps->pdata; + WINPR_ASSERT(pdata); + + settings = peer->context->settings; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_CompressionLevel, PACKET_COMPR_TYPE_RDP8)) + return FALSE; + if (!pf_modules_run_hook(pdata->module, HOOK_TYPE_SERVER_ACTIVATE, pdata, peer)) + return FALSE; + + return TRUE; +} + +static BOOL pf_server_logon(freerdp_peer* peer, const SEC_WINNT_AUTH_IDENTITY* identity, + BOOL automatic) +{ + pServerContext* ps = NULL; + proxyData* pdata = NULL; + proxyServerPeerLogon info = { 0 }; + + WINPR_ASSERT(peer); + + ps = (pServerContext*)peer->context; + WINPR_ASSERT(ps); + + pdata = ps->pdata; + WINPR_ASSERT(pdata); + WINPR_ASSERT(identity); + + info.identity = identity; + info.automatic = automatic; + if (!pf_modules_run_filter(pdata->module, FILTER_TYPE_SERVER_PEER_LOGON, pdata, &info)) + return FALSE; + return TRUE; +} + +static BOOL pf_server_adjust_monitor_layout(freerdp_peer* peer) +{ + WINPR_ASSERT(peer); + /* proxy as is, there's no need to do anything here */ + return TRUE; +} + +static BOOL pf_server_receive_channel_data_hook(freerdp_peer* peer, UINT16 channelId, + const BYTE* data, size_t size, UINT32 flags, + size_t totalSize) +{ + pServerContext* ps = NULL; + pClientContext* pc = NULL; + proxyData* pdata = NULL; + const proxyConfig* config = NULL; + const pServerStaticChannelContext* channel = NULL; + UINT64 channelId64 = channelId; + + WINPR_ASSERT(peer); + + ps = (pServerContext*)peer->context; + WINPR_ASSERT(ps); + + pdata = ps->pdata; + WINPR_ASSERT(pdata); + + pc = pdata->pc; + config = pdata->config; + WINPR_ASSERT(config); + /* + * client side is not initialized yet, call original callback. + * this is probably a drdynvc message between peer and proxy server, + * which doesn't need to be proxied. + */ + if (!pc) + goto original_cb; + + channel = HashTable_GetItemValue(ps->channelsByFrontId, &channelId64); + if (!channel) + { + PROXY_LOG_ERR(TAG, ps, "channel id=%" PRIu64 " not registered here, dropping", channelId64); + return TRUE; + } + + WINPR_ASSERT(channel->onFrontData); + switch (channel->onFrontData(pdata, channel, data, size, flags, totalSize)) + { + case PF_CHANNEL_RESULT_PASS: + { + proxyChannelDataEventInfo ev = { 0 }; + + ev.channel_id = channelId; + ev.channel_name = channel->channel_name; + ev.data = data; + ev.data_len = size; + ev.flags = flags; + ev.total_size = totalSize; + return IFCALLRESULT(TRUE, pc->sendChannelData, pc, &ev); + } + case PF_CHANNEL_RESULT_DROP: + return TRUE; + case PF_CHANNEL_RESULT_ERROR: + return FALSE; + } + +original_cb: + WINPR_ASSERT(pdata->server_receive_channel_data_original); + return pdata->server_receive_channel_data_original(peer, channelId, data, size, flags, + totalSize); +} + +static BOOL pf_server_initialize_peer_connection(freerdp_peer* peer) +{ + WINPR_ASSERT(peer); + + pServerContext* ps = (pServerContext*)peer->context; + if (!ps) + return FALSE; + + rdpSettings* settings = peer->context->settings; + WINPR_ASSERT(settings); + + proxyData* pdata = proxy_data_new(); + if (!pdata) + return FALSE; + proxyServer* server = (proxyServer*)peer->ContextExtra; + WINPR_ASSERT(server); + proxy_data_set_server_context(pdata, ps); + + pdata->module = server->module; + const proxyConfig* config = pdata->config = server->config; + + rdpPrivateKey* key = freerdp_key_new_from_pem(config->PrivateKeyPEM); + if (!key) + return FALSE; + + if (!freerdp_settings_set_pointer_len(settings, FreeRDP_RdpServerRsaKey, key, 1)) + return FALSE; + + rdpCertificate* cert = freerdp_certificate_new_from_pem(config->CertificatePEM); + if (!cert) + return FALSE; + + if (!freerdp_settings_set_pointer_len(settings, FreeRDP_RdpServerCertificate, cert, 1)) + return FALSE; + + /* currently not supporting GDI orders */ + { + void* OrderSupport = freerdp_settings_get_pointer_writable(settings, FreeRDP_OrderSupport); + ZeroMemory(OrderSupport, 32); + } + + WINPR_ASSERT(peer->context->update); + peer->context->update->autoCalculateBitmapData = FALSE; + + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportMonitorLayoutPdu, TRUE)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_SupportGraphicsPipeline, config->GFX)) + return FALSE; + + if (pf_utils_is_passthrough(config)) + { + if (!freerdp_settings_set_bool(settings, FreeRDP_DeactivateClientDecoding, TRUE)) + return FALSE; + } + + if (config->RemoteApp) + { + const UINT32 mask = + RAIL_LEVEL_SUPPORTED | RAIL_LEVEL_DOCKED_LANGBAR_SUPPORTED | + RAIL_LEVEL_SHELL_INTEGRATION_SUPPORTED | RAIL_LEVEL_LANGUAGE_IME_SYNC_SUPPORTED | + RAIL_LEVEL_SERVER_TO_CLIENT_IME_SYNC_SUPPORTED | + RAIL_LEVEL_HIDE_MINIMIZED_APPS_SUPPORTED | RAIL_LEVEL_WINDOW_CLOAKING_SUPPORTED | + RAIL_LEVEL_HANDSHAKE_EX_SUPPORTED; + if (!freerdp_settings_set_uint32(settings, FreeRDP_RemoteApplicationSupportLevel, mask)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteAppLanguageBarSupported, TRUE)) + return FALSE; + } + + if (!freerdp_settings_set_bool(settings, FreeRDP_RdpSecurity, config->ServerRdpSecurity)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_TlsSecurity, config->ServerTlsSecurity)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_NlaSecurity, config->ServerNlaSecurity)) + return FALSE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_EncryptionLevel, + ENCRYPTION_LEVEL_CLIENT_COMPATIBLE)) + return FALSE; + if (!freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth, 32)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_SuppressOutput, TRUE)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_RefreshRect, TRUE)) + return FALSE; + if (!freerdp_settings_set_bool(settings, FreeRDP_DesktopResize, TRUE)) + return FALSE; + + if (!freerdp_settings_set_uint32(settings, FreeRDP_MultifragMaxRequestSize, + 0xFFFFFF)) /* FIXME */ + return FALSE; + + peer->PostConnect = pf_server_post_connect; + peer->Activate = pf_server_activate; + peer->Logon = pf_server_logon; + peer->AdjustMonitorsLayout = pf_server_adjust_monitor_layout; + + /* virtual channels receive data hook */ + pdata->server_receive_channel_data_original = peer->ReceiveChannelData; + peer->ReceiveChannelData = pf_server_receive_channel_data_hook; + + if (!stream_dump_register_handlers(peer->context, CONNECTION_STATE_NEGO, TRUE)) + return FALSE; + return TRUE; +} + +/** + * Handles an incoming client connection, to be run in it's own thread. + * + * arg is a pointer to a freerdp_peer representing the client. + */ +static DWORD WINAPI pf_server_handle_peer(LPVOID arg) +{ + HANDLE eventHandles[MAXIMUM_WAIT_OBJECTS] = { 0 }; + pServerContext* ps = NULL; + proxyData* pdata = NULL; + peer_thread_args* args = arg; + + WINPR_ASSERT(args); + + freerdp_peer* client = args->client; + WINPR_ASSERT(client); + + proxyServer* server = (proxyServer*)client->ContextExtra; + WINPR_ASSERT(server); + + size_t count = ArrayList_Count(server->peer_list); + + if (!pf_context_init_server_context(client)) + goto out_free_peer; + + if (!pf_server_initialize_peer_connection(client)) + goto out_free_peer; + + ps = (pServerContext*)client->context; + WINPR_ASSERT(ps); + PROXY_LOG_DBG(TAG, ps, "Added peer, %" PRIuz " connected", count); + + pdata = ps->pdata; + WINPR_ASSERT(pdata); + + if (!pf_modules_run_hook(pdata->module, HOOK_TYPE_SERVER_SESSION_INITIALIZE, pdata, client)) + goto out_free_peer; + + WINPR_ASSERT(client->Initialize); + client->Initialize(client); + + PROXY_LOG_INFO(TAG, ps, "new connection: proxy address: %s, client address: %s", + pdata->config->Host, client->hostname); + + if (!pf_modules_run_hook(pdata->module, HOOK_TYPE_SERVER_SESSION_STARTED, pdata, client)) + goto out_free_peer; + + while (1) + { + HANDLE ChannelEvent = INVALID_HANDLE_VALUE; + DWORD eventCount = 0; + { + WINPR_ASSERT(client->GetEventHandles); + const DWORD tmp = client->GetEventHandles(client, &eventHandles[eventCount], + ARRAYSIZE(eventHandles) - eventCount); + + if (tmp == 0) + { + PROXY_LOG_ERR(TAG, ps, "Failed to get FreeRDP transport event handles"); + break; + } + + eventCount += tmp; + } + /* Main client event handling loop */ + ChannelEvent = WTSVirtualChannelManagerGetEventHandle(ps->vcm); + + WINPR_ASSERT(ChannelEvent && (ChannelEvent != INVALID_HANDLE_VALUE)); + WINPR_ASSERT(pdata->abort_event && (pdata->abort_event != INVALID_HANDLE_VALUE)); + eventHandles[eventCount++] = ChannelEvent; + eventHandles[eventCount++] = pdata->abort_event; + eventHandles[eventCount++] = server->stopEvent; + + const DWORD status = WaitForMultipleObjects( + eventCount, eventHandles, FALSE, 1000); /* Do periodic polling to avoid client hang */ + + if (status == WAIT_FAILED) + { + PROXY_LOG_ERR(TAG, ps, "WaitForMultipleObjects failed (status: %" PRIu32 ")", status); + break; + } + + WINPR_ASSERT(client->CheckFileDescriptor); + if (client->CheckFileDescriptor(client) != TRUE) + break; + + if (WaitForSingleObject(ChannelEvent, 0) == WAIT_OBJECT_0) + { + if (!WTSVirtualChannelManagerCheckFileDescriptor(ps->vcm)) + { + PROXY_LOG_ERR(TAG, ps, "WTSVirtualChannelManagerCheckFileDescriptor failure"); + goto fail; + } + } + + /* only disconnect after checking client's and vcm's file descriptors */ + if (proxy_data_shall_disconnect(pdata)) + { + PROXY_LOG_INFO(TAG, ps, "abort event is set, closing connection with peer %s", + client->hostname); + break; + } + + if (WaitForSingleObject(server->stopEvent, 0) == WAIT_OBJECT_0) + { + PROXY_LOG_INFO(TAG, ps, "Server shutting down, terminating peer"); + break; + } + + switch (WTSVirtualChannelManagerGetDrdynvcState(ps->vcm)) + { + /* Dynamic channel status may have been changed after processing */ + case DRDYNVC_STATE_NONE: + + /* Initialize drdynvc channel */ + if (!WTSVirtualChannelManagerCheckFileDescriptor(ps->vcm)) + { + PROXY_LOG_ERR(TAG, ps, "Failed to initialize drdynvc channel"); + goto fail; + } + + break; + + case DRDYNVC_STATE_READY: + if (WaitForSingleObject(ps->dynvcReady, 0) == WAIT_TIMEOUT) + { + SetEvent(ps->dynvcReady); + } + + break; + + default: + break; + } + } + +fail: + + PROXY_LOG_INFO(TAG, ps, "starting shutdown of connection"); + PROXY_LOG_INFO(TAG, ps, "stopping proxy's client"); + + /* Abort the client. */ + proxy_data_abort_connect(pdata); + + pf_modules_run_hook(pdata->module, HOOK_TYPE_SERVER_SESSION_END, pdata, client); + + PROXY_LOG_INFO(TAG, ps, "freeing server's channels"); + + WINPR_ASSERT(client->Close); + client->Close(client); + + WINPR_ASSERT(client->Disconnect); + client->Disconnect(client); + +out_free_peer: + PROXY_LOG_INFO(TAG, ps, "freeing proxy data"); + + if (pdata && pdata->client_thread) + { + proxy_data_abort_connect(pdata); + WaitForSingleObject(pdata->client_thread, INFINITE); + } + + { + ArrayList_Lock(server->peer_list); + ArrayList_Remove(server->peer_list, args->thread); + count = ArrayList_Count(server->peer_list); + ArrayList_Unlock(server->peer_list); + } + PROXY_LOG_DBG(TAG, ps, "Removed peer, %" PRIuz " connected", count); + freerdp_peer_context_free(client); + freerdp_peer_free(client); + proxy_data_free(pdata); + +#if defined(WITH_DEBUG_EVENTS) + DumpEventHandles(); +#endif + free(args); + ExitThread(0); + return 0; +} + +static BOOL pf_server_start_peer(freerdp_peer* client) +{ + HANDLE hThread = NULL; + proxyServer* server = NULL; + peer_thread_args* args = calloc(1, sizeof(peer_thread_args)); + if (!args) + return FALSE; + + WINPR_ASSERT(client); + args->client = client; + + server = (proxyServer*)client->ContextExtra; + WINPR_ASSERT(server); + + hThread = CreateThread(NULL, 0, pf_server_handle_peer, args, CREATE_SUSPENDED, NULL); + if (!hThread) + return FALSE; + + args->thread = hThread; + if (!ArrayList_Append(server->peer_list, hThread)) + { + CloseHandle(hThread); + return FALSE; + } + + return ResumeThread(hThread) != (DWORD)-1; +} + +static BOOL pf_server_peer_accepted(freerdp_listener* listener, freerdp_peer* client) +{ + WINPR_ASSERT(listener); + WINPR_ASSERT(client); + + client->ContextExtra = listener->info; + + return pf_server_start_peer(client); +} + +BOOL pf_server_start(proxyServer* server) +{ + WSADATA wsaData; + + WINPR_ASSERT(server); + + WTSRegisterWtsApiFunctionTable(FreeRDP_InitWtsApi()); + winpr_InitializeSSL(WINPR_SSL_INIT_DEFAULT); + + if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) + goto error; + + WINPR_ASSERT(server->config); + WINPR_ASSERT(server->listener); + WINPR_ASSERT(server->listener->Open); + if (!server->listener->Open(server->listener, server->config->Host, server->config->Port)) + { + switch (errno) + { + case EADDRINUSE: + WLog_ERR(TAG, "failed to start listener: address already in use!"); + break; + case EACCES: + WLog_ERR(TAG, "failed to start listener: insufficent permissions!"); + break; + default: + WLog_ERR(TAG, "failed to start listener: errno=%d", errno); + break; + } + + goto error; + } + + return TRUE; + +error: + WSACleanup(); + return FALSE; +} + +BOOL pf_server_start_from_socket(proxyServer* server, int socket) +{ + WSADATA wsaData; + + WINPR_ASSERT(server); + + WTSRegisterWtsApiFunctionTable(FreeRDP_InitWtsApi()); + winpr_InitializeSSL(WINPR_SSL_INIT_DEFAULT); + + if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) + goto error; + + WINPR_ASSERT(server->listener); + WINPR_ASSERT(server->listener->OpenFromSocket); + if (!server->listener->OpenFromSocket(server->listener, socket)) + { + switch (errno) + { + case EADDRINUSE: + WLog_ERR(TAG, "failed to start listener: address already in use!"); + break; + case EACCES: + WLog_ERR(TAG, "failed to start listener: insufficent permissions!"); + break; + default: + WLog_ERR(TAG, "failed to start listener: errno=%d", errno); + break; + } + + goto error; + } + + return TRUE; + +error: + WSACleanup(); + return FALSE; +} + +BOOL pf_server_start_with_peer_socket(proxyServer* server, int peer_fd) +{ + struct sockaddr_storage peer_addr; + socklen_t len = sizeof(peer_addr); + freerdp_peer* client = NULL; + + WINPR_ASSERT(server); + + if (WaitForSingleObject(server->stopEvent, 0) == WAIT_OBJECT_0) + goto fail; + + client = freerdp_peer_new(peer_fd); + if (!client) + goto fail; + + if (getpeername(peer_fd, (struct sockaddr*)&peer_addr, &len) != 0) + goto fail; + + if (!freerdp_peer_set_local_and_hostname(client, &peer_addr)) + goto fail; + + client->ContextExtra = server; + + if (!pf_server_start_peer(client)) + goto fail; + + return TRUE; + +fail: + WLog_ERR(TAG, "PeerAccepted callback failed"); + freerdp_peer_free(client); + return FALSE; +} + +static BOOL are_all_required_modules_loaded(proxyModule* module, const proxyConfig* config) +{ + for (size_t i = 0; i < pf_config_required_plugins_count(config); i++) + { + const char* plugin_name = pf_config_required_plugin(config, i); + + if (!pf_modules_is_plugin_loaded(module, plugin_name)) + { + WLog_ERR(TAG, "Required plugin '%s' is not loaded. stopping.", plugin_name); + return FALSE; + } + } + + return TRUE; +} + +static void peer_free(void* obj) +{ + HANDLE hdl = (HANDLE)obj; + CloseHandle(hdl); +} + +proxyServer* pf_server_new(const proxyConfig* config) +{ + wObject* obj = NULL; + proxyServer* server = NULL; + + WINPR_ASSERT(config); + + server = calloc(1, sizeof(proxyServer)); + if (!server) + return NULL; + + if (!pf_config_clone(&server->config, config)) + goto out; + + server->module = pf_modules_new(FREERDP_PROXY_PLUGINDIR, pf_config_modules(server->config), + pf_config_modules_count(server->config)); + if (!server->module) + { + WLog_ERR(TAG, "failed to initialize proxy modules!"); + goto out; + } + + pf_modules_list_loaded_plugins(server->module); + if (!are_all_required_modules_loaded(server->module, server->config)) + goto out; + + server->stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + if (!server->stopEvent) + goto out; + + server->listener = freerdp_listener_new(); + if (!server->listener) + goto out; + + server->peer_list = ArrayList_New(FALSE); + if (!server->peer_list) + goto out; + + obj = ArrayList_Object(server->peer_list); + WINPR_ASSERT(obj); + + obj->fnObjectFree = peer_free; + + server->listener->info = server; + server->listener->PeerAccepted = pf_server_peer_accepted; + + if (!pf_modules_add(server->module, pf_config_plugin, (void*)server->config)) + goto out; + + return server; + +out: + WINPR_PRAGMA_DIAG_PUSH + WINPR_PRAGMA_DIAG_IGNORED_MISMATCHED_DEALLOC + pf_server_free(server); + WINPR_PRAGMA_DIAG_POP + return NULL; +} + +BOOL pf_server_run(proxyServer* server) +{ + BOOL rc = TRUE; + HANDLE eventHandles[MAXIMUM_WAIT_OBJECTS] = { 0 }; + DWORD eventCount = 0; + DWORD status = 0; + freerdp_listener* listener = NULL; + + WINPR_ASSERT(server); + + listener = server->listener; + WINPR_ASSERT(listener); + + while (1) + { + WINPR_ASSERT(listener->GetEventHandles); + eventCount = listener->GetEventHandles(listener, eventHandles, ARRAYSIZE(eventHandles)); + + if ((0 == eventCount) || (eventCount >= ARRAYSIZE(eventHandles))) + { + WLog_ERR(TAG, "Failed to get FreeRDP event handles"); + break; + } + + WINPR_ASSERT(server->stopEvent); + eventHandles[eventCount++] = server->stopEvent; + status = WaitForMultipleObjects(eventCount, eventHandles, FALSE, 1000); + + if (WAIT_FAILED == status) + break; + + if (WaitForSingleObject(server->stopEvent, 0) == WAIT_OBJECT_0) + break; + + if (WAIT_FAILED == status) + { + WLog_ERR(TAG, "select failed"); + rc = FALSE; + break; + } + + WINPR_ASSERT(listener->CheckFileDescriptor); + if (listener->CheckFileDescriptor(listener) != TRUE) + { + WLog_ERR(TAG, "Failed to accept new peer"); + // TODO: Set out of resource error + continue; + } + } + + WINPR_ASSERT(listener->Close); + listener->Close(listener); + return rc; +} + +void pf_server_stop(proxyServer* server) +{ + + if (!server) + return; + + /* signal main thread to stop and wait for the thread to exit */ + SetEvent(server->stopEvent); +} + +void pf_server_free(proxyServer* server) +{ + if (!server) + return; + + pf_server_stop(server); + + if (server->peer_list) + { + while (ArrayList_Count(server->peer_list) > 0) + { + /* pf_server_stop triggers the threads to shut down. + * loop here until all of them stopped. + * + * This must be done before ArrayList_Free otherwise the thread removal + * in pf_server_handle_peer will deadlock due to both threads trying to + * lock the list. + */ + Sleep(100); + } + } + ArrayList_Free(server->peer_list); + freerdp_listener_free(server->listener); + + if (server->stopEvent) + CloseHandle(server->stopEvent); + + pf_server_config_free(server->config); + pf_modules_free(server->module); + free(server); + +#if defined(WITH_DEBUG_EVENTS) + DumpEventHandles(); +#endif +} + +BOOL pf_server_add_module(proxyServer* server, proxyModuleEntryPoint ep, void* userdata) +{ + WINPR_ASSERT(server); + WINPR_ASSERT(ep); + + return pf_modules_add(server->module, ep, userdata); +} diff --git a/server/proxy/pf_server.h b/server/proxy/pf_server.h new file mode 100644 index 0000000..2ac84f2 --- /dev/null +++ b/server/proxy/pf_server.h @@ -0,0 +1,43 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2019 Mati Shabtay <matishabtay@gmail.com> + * Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com> + * Copyright 2019 Idan Freiberg <speidy@gmail.com> + * Copyright 2021 Armin Novak <anovak@thincast.com> + * Copyright 2021 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 INT_FREERDP_SERVER_PROXY_SERVER_H +#define INT_FREERDP_SERVER_PROXY_SERVER_H + +#include <winpr/collections.h> +#include <freerdp/listener.h> + +#include <freerdp/server/proxy/proxy_config.h> +#include "proxy_modules.h" + +struct proxy_server +{ + proxyModule* module; + proxyConfig* config; + + freerdp_listener* listener; + HANDLE stopEvent; /* an event used to signal the main thread to stop */ + wArrayList* peer_list; +}; + +#endif /* INT_FREERDP_SERVER_PROXY_SERVER_H */ diff --git a/server/proxy/pf_update.c b/server/proxy/pf_update.c new file mode 100644 index 0000000..e572810 --- /dev/null +++ b/server/proxy/pf_update.c @@ -0,0 +1,629 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2019 Mati Shabtay <matishabtay@gmail.com> + * Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com> + * Copyright 2019 Idan Freiberg <speidy@gmail.com> + * Copyright 2021 Armin Novak <anovak@thincast.com> + * Copyright 2021 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/display.h> +#include <freerdp/session.h> +#include <winpr/assert.h> +#include <winpr/image.h> +#include <winpr/sysinfo.h> + +#include <freerdp/server/proxy/proxy_log.h> + +#include "pf_update.h" +#include <freerdp/server/proxy/proxy_context.h> +#include "proxy_modules.h" + +#define TAG PROXY_TAG("update") + +static BOOL pf_server_refresh_rect(rdpContext* context, BYTE count, const RECTANGLE_16* areas) +{ + pServerContext* ps = (pServerContext*)context; + rdpContext* pc = NULL; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->pdata); + pc = (rdpContext*)ps->pdata->pc; + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->update); + WINPR_ASSERT(pc->update->RefreshRect); + return pc->update->RefreshRect(pc, count, areas); +} + +static BOOL pf_server_suppress_output(rdpContext* context, BYTE allow, const RECTANGLE_16* area) +{ + pServerContext* ps = (pServerContext*)context; + rdpContext* pc = NULL; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->pdata); + pc = (rdpContext*)ps->pdata->pc; + WINPR_ASSERT(pc); + WINPR_ASSERT(pc->update); + WINPR_ASSERT(pc->update->SuppressOutput); + return pc->update->SuppressOutput(pc, allow, area); +} + +/* Proxy from PC to PS */ + +/** + * This function is called whenever a new frame starts. + * It can be used to reset invalidated areas. + */ +static BOOL pf_client_begin_paint(rdpContext* context) +{ + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->BeginPaint); + WLog_DBG(TAG, "called"); + return ps->update->BeginPaint(ps); +} + +/** + * 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 pf_client_end_paint(rdpContext* context) +{ + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->EndPaint); + + WLog_DBG(TAG, "called"); + + /* proxy end paint */ + if (!ps->update->EndPaint(ps)) + return FALSE; + + if (!pf_modules_run_hook(pdata->module, HOOK_TYPE_CLIENT_END_PAINT, pdata, context)) + return FALSE; + + return TRUE; +} + +static BOOL pf_client_bitmap_update(rdpContext* context, const BITMAP_UPDATE* bitmap) +{ + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->BitmapUpdate); + WLog_DBG(TAG, "called"); + return ps->update->BitmapUpdate(ps, bitmap); +} + +static BOOL pf_client_desktop_resize(rdpContext* context) +{ + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->DesktopResize); + WINPR_ASSERT(context->settings); + WINPR_ASSERT(ps->settings); + WLog_DBG(TAG, "called"); + if (!freerdp_settings_copy_item(ps->settings, context->settings, FreeRDP_DesktopWidth)) + return FALSE; + if (!freerdp_settings_copy_item(ps->settings, context->settings, FreeRDP_DesktopHeight)) + return FALSE; + return ps->update->DesktopResize(ps); +} + +static BOOL pf_client_remote_monitors(rdpContext* context, UINT32 count, + const MONITOR_DEF* monitors) +{ + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WLog_DBG(TAG, "called"); + return freerdp_display_send_monitor_layout(ps, count, monitors); +} + +static BOOL pf_client_send_pointer_system(rdpContext* context, + const POINTER_SYSTEM_UPDATE* pointer_system) +{ + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->pointer); + WINPR_ASSERT(ps->update->pointer->PointerSystem); + WLog_DBG(TAG, "called"); + return ps->update->pointer->PointerSystem(ps, pointer_system); +} + +static BOOL pf_client_send_pointer_position(rdpContext* context, + const POINTER_POSITION_UPDATE* pointerPosition) +{ + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->pointer); + WINPR_ASSERT(ps->update->pointer->PointerPosition); + WLog_DBG(TAG, "called"); + return ps->update->pointer->PointerPosition(ps, pointerPosition); +} + +static BOOL pf_client_send_pointer_color(rdpContext* context, + const POINTER_COLOR_UPDATE* pointer_color) +{ + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->pointer); + WINPR_ASSERT(ps->update->pointer->PointerColor); + WLog_DBG(TAG, "called"); + return ps->update->pointer->PointerColor(ps, pointer_color); +} + +static BOOL pf_client_send_pointer_large(rdpContext* context, + const POINTER_LARGE_UPDATE* pointer_large) +{ + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->pointer); + WINPR_ASSERT(ps->update->pointer->PointerLarge); + WLog_DBG(TAG, "called"); + return ps->update->pointer->PointerLarge(ps, pointer_large); +} + +static BOOL pf_client_send_pointer_new(rdpContext* context, const POINTER_NEW_UPDATE* pointer_new) +{ + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->pointer); + WINPR_ASSERT(ps->update->pointer->PointerNew); + WLog_DBG(TAG, "called"); + return ps->update->pointer->PointerNew(ps, pointer_new); +} + +static BOOL pf_client_send_pointer_cached(rdpContext* context, + const POINTER_CACHED_UPDATE* pointer_cached) +{ + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->pointer); + WINPR_ASSERT(ps->update->pointer->PointerCached); + WLog_DBG(TAG, "called"); + return ps->update->pointer->PointerCached(ps, pointer_cached); +} + +static BOOL pf_client_save_session_info(rdpContext* context, UINT32 type, void* data) +{ + logon_info* logonInfo = NULL; + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->SaveSessionInfo); + + WLog_DBG(TAG, "called"); + + switch (type) + { + case INFO_TYPE_LOGON: + case INFO_TYPE_LOGON_LONG: + { + logonInfo = (logon_info*)data; + PROXY_LOG_INFO(TAG, pc, "client logon info: Username: %s, Domain: %s", + logonInfo->username, logonInfo->domain); + break; + } + + default: + break; + } + + return ps->update->SaveSessionInfo(ps, type, data); +} + +static BOOL pf_client_server_status_info(rdpContext* context, UINT32 status) +{ + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->ServerStatusInfo); + + WLog_DBG(TAG, "called"); + return ps->update->ServerStatusInfo(ps, status); +} + +static BOOL pf_client_set_keyboard_indicators(rdpContext* context, UINT16 led_flags) +{ + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->SetKeyboardIndicators); + + WLog_DBG(TAG, "called"); + return ps->update->SetKeyboardIndicators(ps, led_flags); +} + +static BOOL pf_client_set_keyboard_ime_status(rdpContext* context, UINT16 imeId, UINT32 imeState, + UINT32 imeConvMode) +{ + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->SetKeyboardImeStatus); + + WLog_DBG(TAG, "called"); + return ps->update->SetKeyboardImeStatus(ps, imeId, imeState, imeConvMode); +} + +static BOOL pf_client_window_create(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const WINDOW_STATE_ORDER* windowState) +{ + BOOL rc = 0; + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->window); + WINPR_ASSERT(ps->update->window->WindowCreate); + + WLog_DBG(TAG, "called"); + rdp_update_lock(ps->update); + rc = ps->update->window->WindowCreate(ps, orderInfo, windowState); + rdp_update_unlock(ps->update); + return rc; +} + +static BOOL pf_client_window_update(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const WINDOW_STATE_ORDER* windowState) +{ + BOOL rc = 0; + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->window); + WINPR_ASSERT(ps->update->window->WindowUpdate); + + WLog_DBG(TAG, "called"); + rdp_update_lock(ps->update); + rc = ps->update->window->WindowUpdate(ps, orderInfo, windowState); + rdp_update_unlock(ps->update); + return rc; +} + +static BOOL pf_client_window_icon(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const WINDOW_ICON_ORDER* windowIcon) +{ + BOOL rc = 0; + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->window); + WINPR_ASSERT(ps->update->window->WindowIcon); + + WLog_DBG(TAG, "called"); + rdp_update_lock(ps->update); + rc = ps->update->window->WindowIcon(ps, orderInfo, windowIcon); + rdp_update_unlock(ps->update); + return rc; +} + +static BOOL pf_client_window_cached_icon(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const WINDOW_CACHED_ICON_ORDER* windowCachedIcon) +{ + BOOL rc = 0; + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->window); + WINPR_ASSERT(ps->update->window->WindowCachedIcon); + + WLog_DBG(TAG, "called"); + rdp_update_lock(ps->update); + rc = ps->update->window->WindowCachedIcon(ps, orderInfo, windowCachedIcon); + rdp_update_unlock(ps->update); + return rc; +} + +static BOOL pf_client_window_delete(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo) +{ + BOOL rc = 0; + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->window); + WINPR_ASSERT(ps->update->window->WindowDelete); + + WLog_DBG(TAG, "called"); + rdp_update_lock(ps->update); + rc = ps->update->window->WindowDelete(ps, orderInfo); + rdp_update_unlock(ps->update); + return rc; +} + +static BOOL pf_client_notify_icon_create(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const NOTIFY_ICON_STATE_ORDER* notifyIconState) +{ + BOOL rc = 0; + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->window); + WINPR_ASSERT(ps->update->window->NotifyIconCreate); + + WLog_DBG(TAG, "called"); + rdp_update_lock(ps->update); + rc = ps->update->window->NotifyIconCreate(ps, orderInfo, notifyIconState); + rdp_update_unlock(ps->update); + return rc; +} + +static BOOL pf_client_notify_icon_update(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const NOTIFY_ICON_STATE_ORDER* notifyIconState) +{ + BOOL rc = 0; + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->window); + WINPR_ASSERT(ps->update->window->NotifyIconUpdate); + + WLog_DBG(TAG, "called"); + rdp_update_lock(ps->update); + rc = ps->update->window->NotifyIconUpdate(ps, orderInfo, notifyIconState); + rdp_update_unlock(ps->update); + return rc; +} + +static BOOL pf_client_notify_icon_delete(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo) +{ + BOOL rc = 0; + + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->window); + WINPR_ASSERT(ps->update->window->NotifyIconDelete); + + WLog_DBG(TAG, "called"); + rdp_update_lock(ps->update); + rc = ps->update->window->NotifyIconDelete(ps, orderInfo); + rdp_update_unlock(ps->update); + return rc; +} + +static BOOL pf_client_monitored_desktop(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo, + const MONITORED_DESKTOP_ORDER* monitoredDesktop) +{ + BOOL rc = 0; + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->window); + WINPR_ASSERT(ps->update->window->MonitoredDesktop); + + WLog_DBG(TAG, "called"); + rdp_update_lock(ps->update); + rc = ps->update->window->MonitoredDesktop(ps, orderInfo, monitoredDesktop); + rdp_update_unlock(ps->update); + return rc; +} + +static BOOL pf_client_non_monitored_desktop(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo) +{ + BOOL rc = 0; + pClientContext* pc = (pClientContext*)context; + proxyData* pdata = NULL; + rdpContext* ps = NULL; + WINPR_ASSERT(pc); + pdata = pc->pdata; + WINPR_ASSERT(pdata); + ps = (rdpContext*)pdata->ps; + WINPR_ASSERT(ps); + WINPR_ASSERT(ps->update); + WINPR_ASSERT(ps->update->window); + WINPR_ASSERT(ps->update->window->NonMonitoredDesktop); + + WLog_DBG(TAG, "called"); + rdp_update_lock(ps->update); + rc = ps->update->window->NonMonitoredDesktop(ps, orderInfo); + rdp_update_unlock(ps->update); + return rc; +} + +void pf_server_register_update_callbacks(rdpUpdate* update) +{ + WINPR_ASSERT(update); + update->RefreshRect = pf_server_refresh_rect; + update->SuppressOutput = pf_server_suppress_output; +} + +void pf_client_register_update_callbacks(rdpUpdate* update) +{ + WINPR_ASSERT(update); + update->BeginPaint = pf_client_begin_paint; + update->EndPaint = pf_client_end_paint; + update->BitmapUpdate = pf_client_bitmap_update; + update->DesktopResize = pf_client_desktop_resize; + update->RemoteMonitors = pf_client_remote_monitors; + update->SaveSessionInfo = pf_client_save_session_info; + update->ServerStatusInfo = pf_client_server_status_info; + update->SetKeyboardIndicators = pf_client_set_keyboard_indicators; + update->SetKeyboardImeStatus = pf_client_set_keyboard_ime_status; + + /* Rail window updates */ + update->window->WindowCreate = pf_client_window_create; + update->window->WindowUpdate = pf_client_window_update; + update->window->WindowIcon = pf_client_window_icon; + update->window->WindowCachedIcon = pf_client_window_cached_icon; + update->window->WindowDelete = pf_client_window_delete; + update->window->NotifyIconCreate = pf_client_notify_icon_create; + update->window->NotifyIconUpdate = pf_client_notify_icon_update; + update->window->NotifyIconDelete = pf_client_notify_icon_delete; + update->window->MonitoredDesktop = pf_client_monitored_desktop; + update->window->NonMonitoredDesktop = pf_client_non_monitored_desktop; + + /* Pointer updates */ + update->pointer->PointerSystem = pf_client_send_pointer_system; + update->pointer->PointerPosition = pf_client_send_pointer_position; + update->pointer->PointerColor = pf_client_send_pointer_color; + update->pointer->PointerLarge = pf_client_send_pointer_large; + update->pointer->PointerNew = pf_client_send_pointer_new; + update->pointer->PointerCached = pf_client_send_pointer_cached; +} diff --git a/server/proxy/pf_update.h b/server/proxy/pf_update.h new file mode 100644 index 0000000..81a9c32 --- /dev/null +++ b/server/proxy/pf_update.h @@ -0,0 +1,34 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2019 Mati Shabtay <matishabtay@gmail.com> + * Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com> + * Copyright 2019 Idan Freiberg <speidy@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_SERVER_PROXY_PFUPDATE_H +#define FREERDP_SERVER_PROXY_PFUPDATE_H + +#include <freerdp/freerdp.h> +#include <freerdp/gdi/gdi.h> +#include <freerdp/gdi/bitmap.h> + +#include <freerdp/server/proxy/proxy_context.h> + +void pf_server_register_update_callbacks(rdpUpdate* update); +void pf_client_register_update_callbacks(rdpUpdate* update); + +#endif /* FREERDP_SERVER_PROXY_PFUPDATE_H */ diff --git a/server/proxy/pf_utils.c b/server/proxy/pf_utils.c new file mode 100644 index 0000000..f8c17af --- /dev/null +++ b/server/proxy/pf_utils.c @@ -0,0 +1,95 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2021 Armin Novak <armin.novak@thincast.com> + * * Copyright 2021 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 <winpr/string.h> +#include <winpr/wtsapi.h> + +#include <freerdp/server/proxy/proxy_log.h> +#include "pf_utils.h" + +#define TAG PROXY_TAG("utils") + +pf_utils_channel_mode pf_utils_get_channel_mode(const proxyConfig* config, const char* name) +{ + pf_utils_channel_mode rc = PF_UTILS_CHANNEL_NOT_HANDLED; + BOOL found = FALSE; + + WINPR_ASSERT(config); + WINPR_ASSERT(name); + + for (size_t i = 0; i < config->InterceptCount; i++) + { + const char* channel_name = config->Intercept[i]; + if (strcmp(name, channel_name) == 0) + { + rc = PF_UTILS_CHANNEL_INTERCEPT; + goto end; + } + } + + for (size_t i = 0; i < config->PassthroughCount; i++) + { + const char* channel_name = config->Passthrough[i]; + if (strcmp(name, channel_name) == 0) + { + found = TRUE; + break; + } + } + + if (found) + { + if (config->PassthroughIsBlacklist) + rc = PF_UTILS_CHANNEL_BLOCK; + else + rc = PF_UTILS_CHANNEL_PASSTHROUGH; + } + else if (config->PassthroughIsBlacklist) + rc = PF_UTILS_CHANNEL_PASSTHROUGH; + +end: + WLog_DBG(TAG, "%s -> %s", name, pf_utils_channel_mode_string(rc)); + return rc; +} + +BOOL pf_utils_is_passthrough(const proxyConfig* config) +{ + WINPR_ASSERT(config); + + /* TODO: For the time being only passthrough mode is supported. */ + return TRUE; +} + +const char* pf_utils_channel_mode_string(pf_utils_channel_mode mode) +{ + switch (mode) + { + case PF_UTILS_CHANNEL_BLOCK: + return "blocked"; + case PF_UTILS_CHANNEL_PASSTHROUGH: + return "passthrough"; + case PF_UTILS_CHANNEL_INTERCEPT: + return "intercepted"; + case PF_UTILS_CHANNEL_NOT_HANDLED: + default: + return "ignored"; + } +} diff --git a/server/proxy/pf_utils.h b/server/proxy/pf_utils.h new file mode 100644 index 0000000..0e899e9 --- /dev/null +++ b/server/proxy/pf_utils.h @@ -0,0 +1,43 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2021 Armin Novak <armin.novak@thincast.com> + * Copyright 2021 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_SERVER_PROXY_PFUTILS_H +#define FREERDP_SERVER_PROXY_PFUTILS_H + +#include <freerdp/server/proxy/proxy_config.h> +#include <freerdp/server/proxy/proxy_context.h> + +/** + * @brief pf_utils_channel_is_passthrough Checks of a channel identified by 'name' + * should be handled as passthrough. + * + * @param config The proxy configuration to check against. Must NOT be NULL. + * @param name The name of the channel. Must NOT be NULL. + * @return -1 if the channel is not handled, 0 if the channel should be ignored, + * 1 if the channel should be passed, 2 the channel will be intercepted + * e.g. proxy client and server are termination points and data passed + * between. + */ +pf_utils_channel_mode pf_utils_get_channel_mode(const proxyConfig* config, const char* name); +const char* pf_utils_channel_mode_string(pf_utils_channel_mode mode); + +BOOL pf_utils_is_passthrough(const proxyConfig* config); + +#endif /* FREERDP_SERVER_PROXY_PFUTILS_H */ diff --git a/server/proxy/proxy_modules.h b/server/proxy/proxy_modules.h new file mode 100644 index 0000000..d5883a5 --- /dev/null +++ b/server/proxy/proxy_modules.h @@ -0,0 +1,100 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * FreeRDP Proxy Server + * + * Copyright 2019 Kobi Mizrachi <kmizrachi18@gmail.com> + * Copyright 2019 Idan Freiberg <speidy@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_SERVER_PROXY_MODULES_H +#define FREERDP_SERVER_PROXY_MODULES_H + +#include <winpr/wtypes.h> +#include <winpr/collections.h> + +#include <freerdp/server/proxy/proxy_modules_api.h> + +typedef enum +{ + FILTER_TYPE_KEYBOARD, /* proxyKeyboardEventInfo */ + FILTER_TYPE_UNICODE, /* proxyUnicodeEventInfo */ + FILTER_TYPE_MOUSE, /* proxyMouseEventInfo */ + FILTER_TYPE_MOUSE_EX, /* proxyMouseExEventInfo */ + FILTER_TYPE_CLIENT_PASSTHROUGH_CHANNEL_DATA, /* proxyChannelDataEventInfo */ + FILTER_TYPE_SERVER_PASSTHROUGH_CHANNEL_DATA, /* proxyChannelDataEventInfo */ + FILTER_TYPE_CLIENT_PASSTHROUGH_DYN_CHANNEL_CREATE, /* proxyChannelDataEventInfo */ + FILTER_TYPE_SERVER_FETCH_TARGET_ADDR, /* proxyFetchTargetEventInfo */ + FILTER_TYPE_SERVER_PEER_LOGON, /* proxyServerPeerLogon */ + FILTER_TYPE_CLIENT_PASSTHROUGH_CHANNEL_CREATE, /* proxyChannelDataEventInfo */ + + FILTER_TYPE_STATIC_INTERCEPT_LIST, /* proxyChannelToInterceptData */ + FILTER_TYPE_DYN_INTERCEPT_LIST, /* proxyChannelToInterceptData */ + FILTER_TYPE_INTERCEPT_CHANNEL, /* proxyDynChannelInterceptData */ + FILTER_LAST +} PF_FILTER_TYPE; + +typedef enum +{ + HOOK_TYPE_CLIENT_INIT_CONNECT, + HOOK_TYPE_CLIENT_UNINIT_CONNECT, + HOOK_TYPE_CLIENT_PRE_CONNECT, + HOOK_TYPE_CLIENT_POST_CONNECT, + HOOK_TYPE_CLIENT_POST_DISCONNECT, + HOOK_TYPE_CLIENT_REDIRECT, + HOOK_TYPE_CLIENT_VERIFY_X509, + HOOK_TYPE_CLIENT_LOGIN_FAILURE, + HOOK_TYPE_CLIENT_END_PAINT, + HOOK_TYPE_CLIENT_LOAD_CHANNELS, + + HOOK_TYPE_SERVER_POST_CONNECT, + HOOK_TYPE_SERVER_ACTIVATE, + HOOK_TYPE_SERVER_CHANNELS_INIT, + HOOK_TYPE_SERVER_CHANNELS_FREE, + HOOK_TYPE_SERVER_SESSION_END, + HOOK_TYPE_SERVER_SESSION_INITIALIZE, + HOOK_TYPE_SERVER_SESSION_STARTED, + + HOOK_LAST +} PF_HOOK_TYPE; + +#ifdef __cplusplus +extern "C" +{ +#endif + + proxyModule* pf_modules_new(const char* root_dir, const char** modules, size_t count); + + /** + * @brief pf_modules_add Registers a new plugin + * @param ep A module entry point function, must NOT be NULL + * @return TRUE for success, FALSE otherwise + */ + BOOL pf_modules_add(proxyModule* module, proxyModuleEntryPoint ep, void* userdata); + + BOOL pf_modules_is_plugin_loaded(proxyModule* module, const char* plugin_name); + void pf_modules_list_loaded_plugins(proxyModule* module); + + BOOL pf_modules_run_filter(proxyModule* module, PF_FILTER_TYPE type, proxyData* pdata, + void* param); + BOOL pf_modules_run_hook(proxyModule* module, PF_HOOK_TYPE type, proxyData* pdata, + void* custom); + + void pf_modules_free(proxyModule* module); + +#ifdef __cplusplus +} +#endif + +#endif /* FREERDP_SERVER_PROXY_MODULES_H */ |