feat: initiale Version 0.1.0 des MSTSC Script Hooks

- Native x64 MSTSC-DVC-Erweiterung für lokalen Programmstart
- Automatischer Script-/Programmstart bei RDP-Verbindungsaufbau
- Separater Programmstart über Trigger innerhalb der RDP-Sitzung
- Bidirektionaler DVC zur Ermittlung des lokalen Client-Hostnamens
- SIOMIN-Trigger zur Anbindung lokaler Sidexis-Installationen
- SIOMIN-Datensatzlängen und Hostname-Felder bytegenau angepasst
- Temporäre SIOMIN-Einträge nach erfolgreicher Übernahme bereinigt
- Native Win32-Konfigurationsoberfläche ohne .NET-Abhängigkeit
- Mehrfachstartschutz und optionaler unsichtbarer Programmstart
- Debug- und Plugin-Logging ergänzt
- RemoteVDDS-Anwendungsfall integriert
- Installer/Registrierung des MSTSC-Plugins über Client-Konfigurator
- Techniker-, Entwickler- und Projektdokumentation ergänzt
- Proprietäre Lizenz für Copyright Patrick Gniza hinzugefügt
This commit is contained in:
2026-08-11 16:32:33 +02:00
parent ee88f1ff30
commit 4c51989600
37 changed files with 4885 additions and 1475 deletions
+2
View File
@@ -53,6 +53,7 @@
<PreprocessorDefinitions>WIN32_LEAN_AND_MEAN;NOMINMAX;UNICODE;_UNICODE;_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
@@ -71,6 +72,7 @@
<PreprocessorDefinitions>WIN32_LEAN_AND_MEAN;NOMINMAX;UNICODE;_UNICODE;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
+171 -16
View File
@@ -10,10 +10,80 @@
namespace
{
std::wstring GetLocalClientHostname()
{
// PhysicalDnsHostname liefert den Hostteil ohne DNS-Domaene und ist nicht von
// Variablen in der Remotesitzung abhaengig. Auf normalen Workstations entspricht
// das dem lokalen Rechnernamen. GetComputerNameW dient als Rueckfall.
wchar_t hostname[256]{};
DWORD hostnameChars = static_cast<DWORD>(sizeof(hostname) / sizeof(hostname[0]));
if (GetComputerNameExW(ComputerNamePhysicalDnsHostname, hostname, &hostnameChars))
{
return std::wstring(hostname, hostnameChars);
}
wchar_t fallback[MAX_COMPUTERNAME_LENGTH + 1]{};
DWORD fallbackChars = static_cast<DWORD>(sizeof(fallback) / sizeof(fallback[0]));
if (GetComputerNameW(fallback, &fallbackChars))
{
return std::wstring(fallback, fallbackChars);
}
return {};
}
std::string WideToUtf8(const std::wstring& value)
{
if (value.empty())
{
return {};
}
const int required = WideCharToMultiByte(
CP_UTF8,
WC_ERR_INVALID_CHARS,
value.data(),
static_cast<int>(value.size()),
nullptr,
0,
nullptr,
nullptr);
if (required <= 0)
{
return {};
}
std::string result(static_cast<std::size_t>(required), '\0');
const int written = WideCharToMultiByte(
CP_UTF8,
WC_ERR_INVALID_CHARS,
value.data(),
static_cast<int>(value.size()),
result.data(),
required,
nullptr,
nullptr);
if (written != required)
{
return {};
}
return result;
}
class TriggerChannelCallback final : public IWTSVirtualChannelCallback
{
public:
TriggerChannelCallback() = default;
explicit TriggerChannelCallback(IWTSVirtualChannel* channel)
: channel_(channel)
{
if (channel_ != nullptr)
{
channel_->AddRef();
}
}
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override
{
@@ -57,32 +127,111 @@ namespace
return S_OK;
}
if (cbSize == hook::protocol::kGetClientHostnameMessageLength &&
std::memcmp(buffer,
hook::protocol::kGetClientHostnameMessage,
hook::protocol::kGetClientHostnameMessageLength) == 0)
{
const std::wstring hostname = GetLocalClientHostname();
const std::string hostnameUtf8 = WideToUtf8(hostname);
if (channel_ == nullptr || hostnameUtf8.empty())
{
hook::Log(config.enableLogging,
L"DVC-Hostname: Lokaler Client-Hostname konnte nicht ermittelt oder beantwortet werden.");
return S_OK;
}
std::string response;
response.reserve(hook::protocol::kClientHostnameResponsePrefixLength + hostnameUtf8.size());
response.append(hook::protocol::kClientHostnameResponsePrefix,
hook::protocol::kClientHostnameResponsePrefixLength);
response.append(hostnameUtf8);
const HRESULT hr = channel_->Write(
static_cast<ULONG>(response.size()),
reinterpret_cast<BYTE*>(response.data()),
nullptr);
if (FAILED(hr))
{
hook::Log(config.enableLogging,
L"DVC-Hostname: Antwort konnte nicht geschrieben werden. HRESULT=" +
std::to_wstring(static_cast<unsigned long>(hr)) + L".");
}
else
{
hook::Log(config.enableLogging,
L"DVC-Hostname: Client-Hostname an Terminalserver beantwortet: " + hostname);
}
return S_OK;
}
if (cbSize != hook::protocol::kStartMessageLength ||
std::memcmp(buffer, hook::protocol::kStartMessage, hook::protocol::kStartMessageLength) != 0)
{
hook::Log(config.enableLogging,
L"DVC-Trigger: Unbekannte Nachricht empfangen; aus Sicherheitsgründen ignoriert.");
L"DVC-Trigger: Unbekannte Nachricht empfangen; aus Sicherheitsgruenden ignoriert.");
return S_OK;
}
hook::Log(config.enableLogging, L"DVC-Trigger: START vom Terminalserver empfangen.");
bool startSucceeded = false;
if (!config.enabled)
{
hook::Log(config.enableLogging, L"DVC-Trigger: Plugin-Funktion ist deaktiviert; Start ignoriert.");
return S_OK;
}
if (!config.triggerEnabled)
else if (!config.triggerEnabled)
{
hook::Log(config.enableLogging, L"DVC-Trigger: Server-Trigger ist in der Client-Konfiguration deaktiviert.");
return S_OK;
}
else
{
// Der Terminalserver liefert absichtlich weder Pfad noch Argumente.
// Beides wird ausschliesslich aus HKCU auf dem lokalen Client gelesen.
hook::ProcessLauncher launcher;
startSucceeded = launcher.Start(
config.triggerProgram,
config.enableLogging,
L"Server-Trigger-Programm");
}
// Der Server wartet auf diese Quittung, bevor er den kurzlebigen DVC
// schliesst. Das verhindert die beobachtete Race Condition, bei der
// WTSVirtualChannelWrite erfolgreich war, der Kanal aber geschlossen
// wurde, bevor OnDataReceived die START-Nachricht erhalten hatte.
if (channel_ != nullptr)
{
const char* response = startSucceeded
? hook::protocol::kStartOkResponse
: hook::protocol::kStartFailedResponse;
const std::size_t responseLength = startSucceeded
? hook::protocol::kStartOkResponseLength
: hook::protocol::kStartFailedResponseLength;
const HRESULT hr = channel_->Write(
static_cast<ULONG>(responseLength),
reinterpret_cast<BYTE*>(const_cast<char*>(response)),
nullptr);
if (FAILED(hr))
{
hook::Log(config.enableLogging,
L"DVC-Trigger: START-Quittung konnte nicht geschrieben werden. HRESULT=" +
std::to_wstring(static_cast<unsigned long>(hr)) + L".");
}
else
{
hook::Log(config.enableLogging,
startSucceeded
? L"DVC-Trigger: START_OK an Terminalserver gesendet."
: L"DVC-Trigger: START_FAILED an Terminalserver gesendet.");
}
}
// Der Terminalserver liefert absichtlich weder Pfad noch Argumente.
// Beides wird ausschließlich aus HKCU auf dem lokalen Client gelesen.
hook::ProcessLauncher launcher;
launcher.Start(config.triggerProgram, config.enableLogging, L"Server-Trigger-Programm");
return S_OK;
}
@@ -94,8 +243,17 @@ namespace
}
private:
~TriggerChannelCallback() = default;
~TriggerChannelCallback()
{
if (channel_ != nullptr)
{
channel_->Release();
channel_ = nullptr;
}
}
LONG refCount_ = 1;
IWTSVirtualChannel* channel_ = nullptr;
};
}
@@ -176,13 +334,11 @@ HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Initialize(IWTSVirtualChannelManager
L". Der automatische Start bei RDP-Verbindung bleibt trotzdem aktiv.");
ReleaseDvcObjects();
// Der DVC-Trigger ist eine optionale Zusatzfunktion. Ein nicht verfügbarer
// DVC darf den bestehenden Connected()/Disconnected()-Hook nicht deaktivieren.
return S_OK;
}
hook::Log(config.enableLogging,
L"DVC-Listener für 'plandent::mstsc-script-hook' wurde registriert.");
L"DVC-Listener fuer 'plandent::mstsc-script-hook' wurde registriert.");
return S_OK;
}
@@ -197,7 +353,6 @@ HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Connected()
return S_OK;
}
// Nur Registry lesen + CreateProcess aufrufen. Es wird niemals auf das Programm gewartet.
connectLauncher_.Start(config.connectProgram, config.enableLogging, L"RDP-Verbindungsprogramm");
return S_OK;
}
@@ -256,7 +411,7 @@ HRESULT STDMETHODCALLTYPE ScriptHookPlugin::OnNewChannelConnection(
return E_POINTER;
}
auto* channelCallback = new (std::nothrow) TriggerChannelCallback();
auto* channelCallback = new (std::nothrow) TriggerChannelCallback(channel);
if (channelCallback == nullptr)
{
hook::Log(config.enableLogging, L"DVC-Trigger: Callback konnte nicht angelegt werden.");