summaryrefslogtreecommitdiffstats
path: root/server/proxy/modules/dyn-channel-dump/dyn-channel-dump.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'server/proxy/modules/dyn-channel-dump/dyn-channel-dump.cpp')
-rw-r--r--server/proxy/modules/dyn-channel-dump/dyn-channel-dump.cpp436
1 files changed, 436 insertions, 0 deletions
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);
+}