FreeRDP
dyn-channel-dump.cpp
1 
24 #include <fstream>
25 #include <iostream>
26 #include <regex>
27 #include <limits>
28 #include <utility>
29 #include <vector>
30 #include <sstream>
31 #include <string>
32 #include <algorithm>
33 #include <map>
34 #include <memory>
35 #include <mutex>
36 #include <atomic>
37 #if __has_include(<filesystem>)
38 #include <filesystem>
39 namespace fs = std::filesystem;
40 #elif __has_include(<experimental/filesystem>)
41 #include <experimental/filesystem>
42 namespace fs = std::experimental::filesystem;
43 #else
44 #error Could not find system header "<filesystem>" or "<experimental/filesystem>"
45 #endif
46 
47 #include <freerdp/server/proxy/proxy_modules_api.h>
48 #include <freerdp/server/proxy/proxy_context.h>
49 
50 #include <freerdp/channels/drdynvc.h>
51 #include <freerdp/channels/rdpgfx.h>
52 #include <freerdp/utils/gfx.h>
53 
54 #define TAG MODULE_TAG("dyn-channel-dump")
55 
56 static constexpr char plugin_name[] = "dyn-channel-dump";
57 static constexpr char plugin_desc[] =
58  "This plugin dumps configurable dynamic channel data to a file.";
59 
60 static const std::vector<std::string>& plugin_static_intercept()
61 {
62  static std::vector<std::string> vec;
63  if (vec.empty())
64  vec.emplace_back(DRDYNVC_SVC_CHANNEL_NAME);
65  return vec;
66 }
67 
68 static constexpr char key_path[] = "path";
69 static constexpr char key_channels[] = "channels";
70 
71 class PluginData
72 {
73  public:
74  explicit PluginData(proxyPluginsManager* mgr) : _mgr(mgr)
75  {
76  }
77 
78  [[nodiscard]] proxyPluginsManager* mgr() const
79  {
80  return _mgr;
81  }
82 
83  uint64_t session()
84  {
85  return _sessionid++;
86  }
87 
88  private:
89  proxyPluginsManager* _mgr;
90  uint64_t _sessionid{ 0 };
91 };
92 
93 class ChannelData
94 {
95  public:
96  ChannelData(const std::string& base, std::vector<std::string> list, uint64_t sessionid)
97  : _base(base), _channels_to_dump(std::move(list)), _session_id(sessionid)
98  {
99  char str[64] = {};
100  (void)_snprintf(str, sizeof(str), "session-%016" PRIx64, _session_id);
101  _base /= str;
102  }
103 
104  bool add(const std::string& name, bool back)
105  {
106  std::lock_guard<std::mutex> guard(_mux);
107  if (_map.find(name) == _map.end())
108  {
109  WLog_INFO(TAG, "adding '%s' to dump list", name.c_str());
110  _map.insert({ name, 0 });
111  }
112  return true;
113  }
114 
115  std::ofstream stream(const std::string& name, bool back)
116  {
117  std::lock_guard<std::mutex> guard(_mux);
118  auto& atom = _map[name];
119  auto count = atom++;
120  auto path = filepath(name, back, count);
121  WLog_DBG(TAG, "[%s] writing file '%s'", name.c_str(), path.c_str());
122  return std::ofstream(path);
123  }
124 
125  [[nodiscard]] bool dump_enabled(const std::string& name) const
126  {
127  if (name.empty())
128  {
129  WLog_WARN(TAG, "empty dynamic channel name, skipping");
130  return false;
131  }
132 
133  auto enabled = std::find(_channels_to_dump.begin(), _channels_to_dump.end(), name) !=
134  _channels_to_dump.end();
135  WLog_DBG(TAG, "channel '%s' dumping %s", name.c_str(), enabled ? "enabled" : "disabled");
136  return enabled;
137  }
138 
139  bool ensure_path_exists()
140  {
141  if (!fs::exists(_base))
142  {
143  if (!fs::create_directories(_base))
144  {
145  WLog_ERR(TAG, "Failed to create dump directory %s", _base.c_str());
146  return false;
147  }
148  }
149  else if (!fs::is_directory(_base))
150  {
151  WLog_ERR(TAG, "dump path %s is not a directory", _base.c_str());
152  return false;
153  }
154  return true;
155  }
156 
157  bool create()
158  {
159  if (!ensure_path_exists())
160  return false;
161 
162  if (_channels_to_dump.empty())
163  {
164  WLog_ERR(TAG, "Empty configuration entry [%s/%s], can not continue", plugin_name,
165  key_channels);
166  return false;
167  }
168  return true;
169  }
170 
171  [[nodiscard]] uint64_t session() const
172  {
173  return _session_id;
174  }
175 
176  private:
177  [[nodiscard]] fs::path filepath(const std::string& channel, bool back, uint64_t count) const
178  {
179  auto name = idstr(channel, back);
180  char cstr[32] = {};
181  (void)_snprintf(cstr, sizeof(cstr), "%016" PRIx64 "-", count);
182  auto path = _base / cstr;
183  path += name;
184  path += ".dump";
185  return path;
186  }
187 
188  [[nodiscard]] static std::string idstr(const std::string& name, bool back)
189  {
190  std::stringstream ss;
191  ss << name << ".";
192  if (back)
193  ss << "back";
194  else
195  ss << "front";
196  return ss.str();
197  }
198 
199  fs::path _base;
200  std::vector<std::string> _channels_to_dump;
201 
202  std::mutex _mux;
203  std::map<std::string, uint64_t> _map;
204  uint64_t _session_id;
205 };
206 
207 static PluginData* dump_get_plugin_data(proxyPlugin* plugin)
208 {
209  WINPR_ASSERT(plugin);
210 
211  auto plugindata = static_cast<PluginData*>(plugin->custom);
212  WINPR_ASSERT(plugindata);
213  return plugindata;
214 }
215 
216 static ChannelData* dump_get_plugin_data(proxyPlugin* plugin, proxyData* pdata)
217 {
218  WINPR_ASSERT(plugin);
219  WINPR_ASSERT(pdata);
220 
221  auto plugindata = dump_get_plugin_data(plugin);
222  WINPR_ASSERT(plugindata);
223 
224  auto mgr = plugindata->mgr();
225  WINPR_ASSERT(mgr);
226 
227  WINPR_ASSERT(mgr->GetPluginData);
228  return static_cast<ChannelData*>(mgr->GetPluginData(mgr, plugin_name, pdata));
229 }
230 
231 static BOOL dump_set_plugin_data(proxyPlugin* plugin, proxyData* pdata, ChannelData* data)
232 {
233  WINPR_ASSERT(plugin);
234  WINPR_ASSERT(pdata);
235 
236  auto plugindata = dump_get_plugin_data(plugin);
237  WINPR_ASSERT(plugindata);
238 
239  auto mgr = plugindata->mgr();
240  WINPR_ASSERT(mgr);
241 
242  auto cdata = dump_get_plugin_data(plugin, pdata);
243  delete cdata;
244 
245  WINPR_ASSERT(mgr->SetPluginData);
246  return mgr->SetPluginData(mgr, plugin_name, pdata, data);
247 }
248 
249 static bool dump_channel_enabled(proxyPlugin* plugin, proxyData* pdata, const std::string& name)
250 {
251  auto config = dump_get_plugin_data(plugin, pdata);
252  if (!config)
253  {
254  WLog_ERR(TAG, "Missing channel data");
255  return false;
256  }
257  return config->dump_enabled(name);
258 }
259 
260 static BOOL dump_dyn_channel_intercept_list(proxyPlugin* plugin, proxyData* pdata, void* arg)
261 {
262  auto data = static_cast<proxyChannelToInterceptData*>(arg);
263 
264  WINPR_ASSERT(plugin);
265  WINPR_ASSERT(pdata);
266  WINPR_ASSERT(data);
267 
268  data->intercept = dump_channel_enabled(plugin, pdata, data->name);
269  if (data->intercept)
270  {
271  auto cdata = dump_get_plugin_data(plugin, pdata);
272  if (!cdata)
273  return FALSE;
274 
275  if (!cdata->add(data->name, false))
276  {
277  WLog_ERR(TAG, "failed to create files for '%s'", data->name);
278  }
279  if (!cdata->add(data->name, true))
280  {
281  WLog_ERR(TAG, "failed to create files for '%s'", data->name);
282  }
283  WLog_INFO(TAG, "Dumping channel '%s'", data->name);
284  }
285  return TRUE;
286 }
287 
288 static BOOL dump_static_channel_intercept_list(proxyPlugin* plugin, proxyData* pdata, void* arg)
289 {
290  auto data = static_cast<proxyChannelToInterceptData*>(arg);
291 
292  WINPR_ASSERT(plugin);
293  WINPR_ASSERT(pdata);
294  WINPR_ASSERT(data);
295 
296  auto intercept = std::find(plugin_static_intercept().begin(), plugin_static_intercept().end(),
297  data->name) != plugin_static_intercept().end();
298  if (intercept)
299  {
300  WLog_INFO(TAG, "intercepting channel '%s'", data->name);
301  data->intercept = TRUE;
302  }
303 
304  return TRUE;
305 }
306 
307 static BOOL dump_dyn_channel_intercept(proxyPlugin* plugin, proxyData* pdata, void* arg)
308 {
309  auto data = static_cast<proxyDynChannelInterceptData*>(arg);
310 
311  WINPR_ASSERT(plugin);
312  WINPR_ASSERT(pdata);
313  WINPR_ASSERT(data);
314 
315  data->result = PF_CHANNEL_RESULT_PASS;
316  if (dump_channel_enabled(plugin, pdata, data->name))
317  {
318  WLog_DBG(TAG, "intercepting channel '%s'", data->name);
319  auto cdata = dump_get_plugin_data(plugin, pdata);
320  if (!cdata)
321  {
322  WLog_ERR(TAG, "Missing channel data");
323  return FALSE;
324  }
325 
326  if (!cdata->ensure_path_exists())
327  return FALSE;
328 
329  auto stream = cdata->stream(data->name, data->isBackData);
330  auto buffer = reinterpret_cast<const char*>(Stream_ConstBuffer(data->data));
331  if (!stream.is_open() || !stream.good())
332  {
333  WLog_ERR(TAG, "Could not write to stream");
334  return FALSE;
335  }
336  const auto s = Stream_Length(data->data);
337  if (s > std::numeric_limits<std::streamsize>::max())
338  {
339  WLog_ERR(TAG, "Stream length %" PRIuz " exceeds std::streamsize::max", s);
340  return FALSE;
341  }
342  stream.write(buffer, static_cast<std::streamsize>(s));
343  if (stream.fail())
344  {
345  WLog_ERR(TAG, "Could not write to stream");
346  return FALSE;
347  }
348  stream.flush();
349  }
350 
351  return TRUE;
352 }
353 
354 static std::vector<std::string> split(const std::string& input, const std::string& regex)
355 {
356  // passing -1 as the submatch index parameter performs splitting
357  std::regex re(regex);
358  std::sregex_token_iterator first{ input.begin(), input.end(), re, -1 };
359  std::sregex_token_iterator last;
360  return { first, last };
361 }
362 
363 static BOOL dump_session_started(proxyPlugin* plugin, proxyData* pdata, void* /*unused*/)
364 {
365  WINPR_ASSERT(plugin);
366  WINPR_ASSERT(pdata);
367 
368  auto custom = dump_get_plugin_data(plugin);
369  WINPR_ASSERT(custom);
370 
371  auto config = pdata->config;
372  WINPR_ASSERT(config);
373 
374  auto cpath = pf_config_get(config, plugin_name, key_path);
375  if (!cpath)
376  {
377  WLog_ERR(TAG, "Missing configuration entry [%s/%s], can not continue", plugin_name,
378  key_path);
379  return FALSE;
380  }
381  auto cchannels = pf_config_get(config, plugin_name, key_channels);
382  if (!cchannels)
383  {
384  WLog_ERR(TAG, "Missing configuration entry [%s/%s], can not continue", plugin_name,
385  key_channels);
386  return FALSE;
387  }
388 
389  std::string path(cpath);
390  std::string channels(cchannels);
391  std::vector<std::string> list = split(channels, "[;,]");
392  auto cfg = new ChannelData(path, std::move(list), custom->session());
393  if (!cfg || !cfg->create())
394  {
395  delete cfg;
396  return FALSE;
397  }
398 
399  dump_set_plugin_data(plugin, pdata, cfg);
400 
401  WLog_DBG(TAG, "starting session dump %" PRIu64, cfg->session());
402  return TRUE;
403 }
404 
405 static BOOL dump_session_end(proxyPlugin* plugin, proxyData* pdata, void* /*unused*/)
406 {
407  WINPR_ASSERT(plugin);
408  WINPR_ASSERT(pdata);
409 
410  auto cfg = dump_get_plugin_data(plugin, pdata);
411  if (cfg)
412  WLog_DBG(TAG, "ending session dump %" PRIu64, cfg->session());
413  dump_set_plugin_data(plugin, pdata, nullptr);
414  return TRUE;
415 }
416 
417 static BOOL dump_unload(proxyPlugin* plugin)
418 {
419  if (!plugin)
420  return TRUE;
421  delete static_cast<PluginData*>(plugin->custom);
422  return TRUE;
423 }
424 
425 extern "C" FREERDP_API BOOL proxy_module_entry_point(proxyPluginsManager* plugins_manager,
426  void* userdata);
427 
428 BOOL proxy_module_entry_point(proxyPluginsManager* plugins_manager, void* userdata)
429 {
430  proxyPlugin plugin = {};
431 
432  plugin.name = plugin_name;
433  plugin.description = plugin_desc;
434 
435  plugin.PluginUnload = dump_unload;
436  plugin.ServerSessionStarted = dump_session_started;
437  plugin.ServerSessionEnd = dump_session_end;
438 
439  plugin.StaticChannelToIntercept = dump_static_channel_intercept_list;
440  plugin.DynChannelToIntercept = dump_dyn_channel_intercept_list;
441  plugin.DynChannelIntercept = dump_dyn_channel_intercept;
442 
443  plugin.custom = new PluginData(plugins_manager);
444  if (!plugin.custom)
445  return FALSE;
446  plugin.userdata = userdata;
447 
448  return plugins_manager->RegisterPlugin(plugins_manager, &plugin);
449 }
FREERDP_API const char * pf_config_get(const proxyConfig *config, const char *section, const char *key)
pf_config_get get a value for a section/key
Definition: pf_config.c:1297