summaryrefslogtreecommitdiffstats
path: root/server/proxy/modules
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--server/proxy/modules/CMakeLists.txt33
-rw-r--r--server/proxy/modules/README.md66
-rw-r--r--server/proxy/modules/bitmap-filter/CMakeLists.txt60
-rw-r--r--server/proxy/modules/bitmap-filter/bitmap-filter.cpp453
-rw-r--r--server/proxy/modules/demo/CMakeLists.txt53
-rw-r--r--server/proxy/modules/demo/demo.cpp422
-rw-r--r--server/proxy/modules/dyn-channel-dump/CMakeLists.txt58
-rw-r--r--server/proxy/modules/dyn-channel-dump/dyn-channel-dump.cpp436
8 files changed, 1581 insertions, 0 deletions
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);
+}