Files
patrick 4c51989600 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
2026-08-11 16:32:33 +02:00

1216 lines
37 KiB
C++

#include "../Shared/TriggerProtocol.h"
#include "../Shared/DvcServerIo.h"
#include <windows.h>
#include <wtsapi32.h>
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <cwchar>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
namespace
{
constexpr wchar_t kIniSection[] = L"SIOMIN";
constexpr wchar_t kIniTempPathKey[] = L"SIOMIN_TEMP_PATH";
constexpr wchar_t kIniPathKey[] = L"SIOMIN_PATH";
constexpr wchar_t kIniDebugKey[] = L"DEBUG";
constexpr wchar_t kDefaultTempPath[] = L"C:\\PDATA\\temp_siomin.sdx";
constexpr wchar_t kDefaultSiominPath[] = L"C:\\PDATA\\siomin.sdx";
constexpr DWORD kDvcTimeoutMs = 5000;
enum class ExitCode : int
{
Success = 0,
IniPathFailed = 20,
IniCreateFailed = 21,
ChannelOpenFailed = 22,
HostnameRequestWriteFailed = 23,
HostnameResponseReadFailed = 24,
InvalidHostnameResponse = 25,
InvalidClientHostname = 26,
TempFileReadFailed = 27,
NoSiominHostFieldFound = 28,
FinalPathInvalid = 29,
FinalFileOpenFailed = 30,
FinalFileBoundaryInvalid = 31,
FinalFileWriteFailed = 32,
StartTriggerWriteFailed = 33,
TempFileClearFailed = 34,
StartAckReadFailed = 35,
StartRejected = 36,
InvalidStartAck = 37,
RollbackFailed = 38
};
std::filesystem::path GetOwnPath()
{
std::vector<wchar_t> buffer(32768, L'\0');
const DWORD chars = GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
if (chars == 0 || chars >= buffer.size())
{
return {};
}
return std::filesystem::path(std::wstring(buffer.data(), chars));
}
std::string WideToUtf8(const std::wstring& value)
{
if (value.empty())
{
return {};
}
const int required = WideCharToMultiByte(
CP_UTF8,
0,
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,
0,
value.data(),
static_cast<int>(value.size()),
result.data(),
required,
nullptr,
nullptr);
if (written != required)
{
return {};
}
return result;
}
class DebugLogger
{
public:
void Initialize(bool enabled, const std::filesystem::path& exePath)
{
enabled_ = enabled;
logPath_ = exePath;
logPath_.replace_extension(L".log");
}
void Log(const std::wstring& message) const
{
const std::wstring debugLine = L"PlandentSiominClientTrigger: " + message + L"\n";
OutputDebugStringW(debugLine.c_str());
if (!enabled_ || logPath_.empty())
{
return;
}
SYSTEMTIME st{};
GetLocalTime(&st);
wchar_t prefix[96]{};
swprintf_s(
prefix,
sizeof(prefix) / sizeof(prefix[0]),
L"%04u-%02u-%02u %02u:%02u:%02u.%03u [PID:%lu] ",
st.wYear,
st.wMonth,
st.wDay,
st.wHour,
st.wMinute,
st.wSecond,
st.wMilliseconds,
GetCurrentProcessId());
const std::string utf8 = WideToUtf8(std::wstring(prefix) + message + L"\r\n");
if (utf8.empty())
{
return;
}
HANDLE file = CreateFileW(
logPath_.c_str(),
FILE_APPEND_DATA,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (file == INVALID_HANDLE_VALUE)
{
return;
}
DWORD written = 0;
WriteFile(
file,
utf8.data(),
static_cast<DWORD>(utf8.size()),
&written,
nullptr);
CloseHandle(file);
}
void LogWin32Error(const std::wstring& step, DWORD error) const
{
Log(step + L" fehlgeschlagen, Win32=" + std::to_wstring(error));
}
const std::filesystem::path& Path() const
{
return logPath_;
}
private:
bool enabled_ = false;
std::filesystem::path logPath_;
};
void DebugMessage(const std::wstring& message)
{
const std::wstring line = L"PlandentSiominClientTrigger: " + message + L"\n";
OutputDebugStringW(line.c_str());
}
bool WriteNewUnicodeIni(const std::filesystem::path& iniPath)
{
const std::wstring content =
std::wstring(L"[") + kIniSection + L"]\r\n" +
kIniTempPathKey + L"=" + kDefaultTempPath + L"\r\n" +
kIniPathKey + L"=" + kDefaultSiominPath + L"\r\n" +
kIniDebugKey + L"=0\r\n";
HANDLE file = CreateFileW(
iniPath.c_str(),
GENERIC_WRITE,
FILE_SHARE_READ,
nullptr,
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (file == INVALID_HANDLE_VALUE)
{
return GetLastError() == ERROR_FILE_EXISTS;
}
const BYTE bom[] = { 0xFF, 0xFE };
DWORD written = 0;
BOOL ok = WriteFile(file, bom, static_cast<DWORD>(sizeof(bom)), &written, nullptr);
if (!ok || written != sizeof(bom))
{
CloseHandle(file);
return false;
}
const DWORD bytes = static_cast<DWORD>(content.size() * sizeof(wchar_t));
written = 0;
ok = WriteFile(file, content.data(), bytes, &written, nullptr);
const bool result = ok && written == bytes;
if (result)
{
FlushFileBuffers(file);
}
CloseHandle(file);
return result;
}
std::wstring ReadIniValue(
const std::filesystem::path& iniPath,
const wchar_t* key,
const wchar_t* defaultValue)
{
std::vector<wchar_t> buffer(32768, L'\0');
const DWORD chars = GetPrivateProfileStringW(
kIniSection,
key,
defaultValue,
buffer.data(),
static_cast<DWORD>(buffer.size()),
iniPath.c_str());
if (chars == 0)
{
return defaultValue;
}
return std::wstring(buffer.data(), chars);
}
bool ReadIniBool(const std::filesystem::path& iniPath, const wchar_t* key, bool defaultValue)
{
return GetPrivateProfileIntW(
kIniSection,
key,
defaultValue ? 1 : 0,
iniPath.c_str()) != 0;
}
bool EnsureDebugIniKey(const std::filesystem::path& iniPath)
{
constexpr wchar_t kMissingMarker[] = L"__PLANDENT_MISSING__";
wchar_t value[64]{};
GetPrivateProfileStringW(
kIniSection,
kIniDebugKey,
kMissingMarker,
value,
static_cast<DWORD>(sizeof(value) / sizeof(value[0])),
iniPath.c_str());
if (wcscmp(value, kMissingMarker) != 0)
{
return true;
}
return WritePrivateProfileStringW(
kIniSection,
kIniDebugKey,
L"0",
iniPath.c_str()) != FALSE;
}
bool WriteChannelMessage(HANDLE channel, const char* message, std::size_t length, DWORD& win32Error)
{
win32Error = ERROR_SUCCESS;
ULONG bytesWritten = 0;
const BOOL ok = WTSVirtualChannelWrite(
channel,
const_cast<PCHAR>(message),
static_cast<ULONG>(length),
&bytesWritten);
if (!ok)
{
win32Error = GetLastError();
return false;
}
if (bytesWritten != static_cast<ULONG>(length))
{
win32Error = ERROR_WRITE_FAULT;
return false;
}
return true;
}
bool Utf8ToWide(const std::string& value, std::wstring& result)
{
result.clear();
if (value.empty())
{
return false;
}
const int required = MultiByteToWideChar(
CP_UTF8,
MB_ERR_INVALID_CHARS,
value.data(),
static_cast<int>(value.size()),
nullptr,
0);
if (required <= 0)
{
return false;
}
result.resize(static_cast<std::size_t>(required));
const int written = MultiByteToWideChar(
CP_UTF8,
MB_ERR_INVALID_CHARS,
value.data(),
static_cast<int>(value.size()),
result.data(),
required);
return written == required;
}
enum class HostnameQueryResult
{
Success,
WriteFailed,
ReadFailed,
InvalidResponse
};
HostnameQueryResult QueryClientHostname(
HANDLE channel,
std::wstring& hostname,
DWORD& win32Error,
const DebugLogger& log)
{
hostname.clear();
win32Error = ERROR_SUCCESS;
log.Log(L"Sende GET_CLIENT_HOSTNAME ueber DVC.");
if (!WriteChannelMessage(
channel,
hook::protocol::kGetClientHostnameMessage,
hook::protocol::kGetClientHostnameMessageLength,
win32Error))
{
return HostnameQueryResult::WriteFailed;
}
std::vector<char> response;
if (!hook::dvc::ReadMessage(channel, kDvcTimeoutMs, response, win32Error))
{
return HostnameQueryResult::ReadFailed;
}
log.Log(L"DVC-Hostname-Antwort empfangen, Payload-Bytes=" + std::to_wstring(response.size()) + L".");
if (response.size() <= hook::protocol::kClientHostnameResponsePrefixLength ||
std::memcmp(
response.data(),
hook::protocol::kClientHostnameResponsePrefix,
hook::protocol::kClientHostnameResponsePrefixLength) != 0)
{
win32Error = ERROR_INVALID_DATA;
return HostnameQueryResult::InvalidResponse;
}
const std::string hostnameUtf8(
response.data() + hook::protocol::kClientHostnameResponsePrefixLength,
response.data() + response.size());
if (!Utf8ToWide(hostnameUtf8, hostname))
{
win32Error = ERROR_NO_UNICODE_TRANSLATION;
return HostnameQueryResult::InvalidResponse;
}
return HostnameQueryResult::Success;
}
bool HostnameToSiominBytes(const std::wstring& hostname, std::vector<std::uint8_t>& result)
{
result.clear();
if (hostname.empty() || hostname.size() > 255)
{
return false;
}
// Das beobachtete SIOMIN-Format verwendet einbyteige, NUL-separierte Felder.
// Fuer Rechnernamen akzeptieren wir daher bewusst nur druckbares ASCII.
result.reserve(hostname.size());
for (wchar_t ch : hostname)
{
if (ch < 0x20 || ch > 0x7E || ch == L'\\' || ch == L'/')
{
return false;
}
result.push_back(static_cast<std::uint8_t>(ch));
}
return !result.empty();
}
bool ReadAllBytes(const std::filesystem::path& path, std::vector<std::uint8_t>& bytes)
{
bytes.clear();
std::ifstream stream(path, std::ios::binary | std::ios::ate);
if (!stream)
{
return false;
}
const std::streamoff length = stream.tellg();
if (length < 0)
{
return false;
}
bytes.resize(static_cast<std::size_t>(length));
stream.seekg(0, std::ios::beg);
if (!bytes.empty())
{
stream.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
if (!stream)
{
return false;
}
}
return true;
}
std::size_t FindByte(
const std::vector<std::uint8_t>& data,
std::size_t begin,
std::size_t end,
std::uint8_t value)
{
for (std::size_t i = begin; i < end; ++i)
{
if (data[i] == value)
{
return i;
}
}
return end;
}
bool IsSiominActionRecordAt(
const std::vector<std::uint8_t>& data,
std::size_t recordStart,
std::size_t& recordEnd)
{
recordEnd = recordStart;
// WICHTIG:
// Das erste Byte eines SIOMIN-Datensatzes ist KEIN Kennzeichen,
// sondern die Gesamtlaenge des Datensatzes in Bytes (inkl. Laengenbyte
// und abschliessendem CRLF).
//
// Beispiele aus realen Dateien:
// 0x3A 00 'N' 00 ... -> Laenge 58
// 0x40 00 'N' 00 ... -> Laenge 64
// 0x46 00 'A' 00 ... -> Laenge 70
// 0x4C 00 'A' 00 ... -> Laenge 76
//
// Der Datensatztyp steht erst im dritten Byte ('N' bzw. 'A').
if (recordStart + 4 > data.size())
{
return false;
}
const std::size_t recordLength = data[recordStart];
if (recordLength < 6 || recordStart + recordLength > data.size())
{
return false;
}
recordEnd = recordStart + recordLength;
if (data[recordStart + 1] != 0x00 ||
data[recordStart + 2] != static_cast<std::uint8_t>('A') ||
data[recordStart + 3] != 0x00)
{
return false;
}
// Die beobachteten SIOMIN-Datensaetze enden mit CRLF. Damit verhindern
// wir, dass bei einer korrupten Laengenangabe in Folgedaten geparst wird.
if (recordLength < 2 ||
data[recordEnd - 2] != 0x0D ||
data[recordEnd - 1] != 0x0A)
{
return false;
}
return true;
}
std::wstring HexPreview(const std::vector<std::uint8_t>& data, std::size_t maxBytes = 256)
{
if (data.empty())
{
return L"<leer>";
}
const std::size_t count = std::min<std::size_t>(data.size(), maxBytes);
std::wstring result;
result.reserve(count * 3 + 32);
constexpr wchar_t hex[] = L"0123456789ABCDEF";
for (std::size_t i = 0; i < count; ++i)
{
if (i != 0)
{
result.push_back(L' ');
}
const std::uint8_t value = data[i];
result.push_back(hex[(value >> 4) & 0x0F]);
result.push_back(hex[value & 0x0F]);
}
if (count < data.size())
{
result += L" ...";
}
return result;
}
struct HostFieldRange
{
std::size_t recordStart = 0;
std::size_t recordEnd = 0; // exklusiv
std::size_t begin = 0;
std::size_t end = 0; // exklusiv; das abschliessende NUL bleibt erhalten
std::uint8_t originalRecordLength = 0;
};
bool TryGetHostnameField(
const std::vector<std::uint8_t>& source,
std::size_t recordStart,
HostFieldRange& range)
{
range = {};
std::size_t recordEnd = 0;
if (!IsSiominActionRecordAt(source, recordStart, recordEnd))
{
return false;
}
// SIOMIN-Aktionsdatensatz:
// LENGTH\0 A\0 Nachname\0 Vorname\0 Geburtsdatum\0 ID\0 HOSTNAME\0 Datum\0 Uhrzeit...
//
// Vom Beginn des Datensatzes aus muessen sechs NUL-Trenner uebersprungen
// werden. Danach beginnt das Hostname-Feld.
std::size_t fieldStart = recordStart;
for (int field = 0; field < 6; ++field)
{
const std::size_t separator = FindByte(source, fieldStart, recordEnd, 0x00);
if (separator == recordEnd)
{
return false;
}
for (std::size_t i = fieldStart; i < separator; ++i)
{
if (source[i] == 0x0D || source[i] == 0x0A)
{
return false;
}
}
fieldStart = separator + 1;
}
if (fieldStart >= recordEnd)
{
return false;
}
const std::size_t fieldEnd = FindByte(source, fieldStart, recordEnd, 0x00);
if (fieldEnd == recordEnd || fieldEnd == fieldStart)
{
return false;
}
for (std::size_t i = fieldStart; i < fieldEnd; ++i)
{
if (source[i] == 0x0D || source[i] == 0x0A)
{
return false;
}
}
range.recordStart = recordStart;
range.recordEnd = recordEnd;
range.begin = fieldStart;
range.end = fieldEnd;
range.originalRecordLength = source[recordStart];
return true;
}
bool ReplaceSiominHostnameFields(
const std::vector<std::uint8_t>& source,
const std::vector<std::uint8_t>& hostname,
std::vector<std::uint8_t>& transformed,
std::size_t& replacementCount)
{
transformed.clear();
replacementCount = 0;
if (source.empty() || hostname.empty())
{
return false;
}
// SIOMIN ist laengencodiert: Das erste Byte jedes Datensatzes enthaelt
// seine Gesamtlaenge. Deshalb duerfen wir beim Ersetzen eines Hostnamens
// nicht nur den Text austauschen, sondern MUESSEN auch dieses Laengenbyte
// um die Groessendifferenz korrigieren.
//
// Beispiel:
// alter Datensatz: 0x4C = 76 Bytes, Host "TS-FR" = 5 Bytes
// neuer Host: "FR-Empfang-3R" = 13 Bytes
// neue Laenge: 76 + 8 = 84 = 0x54
std::vector<HostFieldRange> replacements;
std::size_t recordStart = 0;
while (recordStart < source.size())
{
if (recordStart + 1 > source.size())
{
transformed = source;
return false;
}
const std::size_t recordLength = source[recordStart];
if (recordLength == 0 || recordStart + recordLength > source.size())
{
transformed = source;
return false;
}
const std::size_t recordEnd = recordStart + recordLength;
// Grundlegende Integritaetspruefung fuer alle beobachteten Datensaetze.
if (recordLength < 2 ||
source[recordEnd - 2] != 0x0D ||
source[recordEnd - 1] != 0x0A)
{
transformed = source;
return false;
}
HostFieldRange range{};
if (TryGetHostnameField(source, recordStart, range))
{
const std::size_t oldHostnameLength = range.end - range.begin;
const std::ptrdiff_t delta =
static_cast<std::ptrdiff_t>(hostname.size()) -
static_cast<std::ptrdiff_t>(oldHostnameLength);
const std::ptrdiff_t newRecordLength =
static_cast<std::ptrdiff_t>(range.originalRecordLength) + delta;
// Das Format hat nur ein Byte fuer die Datensatzlaenge.
if (newRecordLength <= 0 || newRecordLength > 255)
{
transformed = source;
return false;
}
replacements.push_back(range);
}
recordStart = recordEnd;
}
if (recordStart != source.size() || replacements.empty())
{
transformed = source;
return false;
}
std::size_t outputSize = source.size();
for (const HostFieldRange& range : replacements)
{
outputSize -= (range.end - range.begin);
outputSize += hostname.size();
}
transformed.reserve(outputSize);
std::size_t copyFrom = 0;
for (const HostFieldRange& range : replacements)
{
// Alle Bytes vor diesem Datensatz unveraendert uebernehmen.
transformed.insert(
transformed.end(),
source.begin() + static_cast<std::ptrdiff_t>(copyFrom),
source.begin() + static_cast<std::ptrdiff_t>(range.recordStart));
const std::size_t outputRecordStart = transformed.size();
// Datensatz bis direkt vor den Hostnamen kopieren.
transformed.insert(
transformed.end(),
source.begin() + static_cast<std::ptrdiff_t>(range.recordStart),
source.begin() + static_cast<std::ptrdiff_t>(range.begin));
const std::size_t oldHostnameLength = range.end - range.begin;
const std::ptrdiff_t delta =
static_cast<std::ptrdiff_t>(hostname.size()) -
static_cast<std::ptrdiff_t>(oldHostnameLength);
const std::size_t newRecordLength =
static_cast<std::size_t>(
static_cast<std::ptrdiff_t>(range.originalRecordLength) + delta);
// Das bereits kopierte erste Byte ist die alte Datensatzlaenge.
// Es muss auf die neue tatsaechliche Laenge gesetzt werden.
transformed[outputRecordStart] = static_cast<std::uint8_t>(newRecordLength);
transformed.insert(transformed.end(), hostname.begin(), hostname.end());
// NUL hinter dem alten Hostnamen und den gesamten Rest des Datensatzes
// unveraendert uebernehmen.
transformed.insert(
transformed.end(),
source.begin() + static_cast<std::ptrdiff_t>(range.end),
source.begin() + static_cast<std::ptrdiff_t>(range.recordEnd));
copyFrom = range.recordEnd;
++replacementCount;
}
transformed.insert(
transformed.end(),
source.begin() + static_cast<std::ptrdiff_t>(copyFrom),
source.end());
return replacementCount > 0;
}
bool IsSamePath(const std::filesystem::path& a, const std::filesystem::path& b)
{
const std::wstring aa = std::filesystem::absolute(a).lexically_normal().wstring();
const std::wstring bb = std::filesystem::absolute(b).lexically_normal().wstring();
return _wcsicmp(aa.c_str(), bb.c_str()) == 0;
}
struct AppendState
{
bool existed = false;
std::uint64_t originalSize = 0;
};
bool AppendBytesPreservingExistingFile(
const std::filesystem::path& finalPath,
const std::vector<std::uint8_t>& bytes,
bool& invalidBoundary,
AppendState& appendState)
{
invalidBoundary = false;
appendState = {};
std::error_code ec;
appendState.existed = std::filesystem::exists(finalPath, ec);
if (ec)
{
return false;
}
if (appendState.existed)
{
const std::uintmax_t size = std::filesystem::file_size(finalPath, ec);
if (ec)
{
return false;
}
appendState.originalSize = static_cast<std::uint64_t>(size);
if (size > 0)
{
if (size < 2)
{
invalidBoundary = true;
return false;
}
HANDLE existing = CreateFileW(
finalPath.c_str(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (existing == INVALID_HANDLE_VALUE)
{
return false;
}
LARGE_INTEGER offset{};
offset.QuadPart = -2;
if (!SetFilePointerEx(existing, offset, nullptr, FILE_END))
{
CloseHandle(existing);
return false;
}
BYTE tail[2]{};
DWORD read = 0;
const BOOL readOk = ReadFile(existing, tail, 2, &read, nullptr);
CloseHandle(existing);
if (!readOk || read != 2)
{
return false;
}
if (!(tail[0] == 0x0D && tail[1] == 0x0A))
{
invalidBoundary = true;
return false;
}
}
}
else
{
const std::filesystem::path parent = finalPath.parent_path();
if (!parent.empty())
{
std::filesystem::create_directories(parent, ec);
if (ec)
{
return false;
}
}
}
HANDLE file = CreateFileW(
finalPath.c_str(),
FILE_APPEND_DATA,
FILE_SHARE_READ,
nullptr,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (file == INVALID_HANDLE_VALUE)
{
return false;
}
std::size_t offset = 0;
bool ok = true;
while (offset < bytes.size())
{
const DWORD chunk = static_cast<DWORD>(
std::min<std::size_t>(bytes.size() - offset, 1024 * 1024));
DWORD written = 0;
if (!WriteFile(file, bytes.data() + offset, chunk, &written, nullptr) || written != chunk)
{
ok = false;
break;
}
offset += written;
}
if (ok)
{
ok = FlushFileBuffers(file) != FALSE;
}
CloseHandle(file);
return ok;
}
bool TruncateFileToSize(const std::filesystem::path& path, std::uint64_t size)
{
HANDLE file = CreateFileW(
path.c_str(),
GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (file == INVALID_HANDLE_VALUE)
{
return false;
}
LARGE_INTEGER position{};
position.QuadPart = static_cast<LONGLONG>(size);
bool ok = SetFilePointerEx(file, position, nullptr, FILE_BEGIN) != FALSE;
if (ok)
{
ok = SetEndOfFile(file) != FALSE;
}
if (ok)
{
ok = FlushFileBuffers(file) != FALSE;
}
CloseHandle(file);
return ok;
}
bool ClearSourceFile(const std::filesystem::path& path)
{
return TruncateFileToSize(path, 0);
}
bool RollbackAppend(const std::filesystem::path& finalPath, const AppendState& state)
{
if (!state.existed)
{
return DeleteFileW(finalPath.c_str()) != FALSE;
}
return TruncateFileToSize(finalPath, state.originalSize);
}
enum class StartTriggerResult
{
Success,
OpenFailed,
WriteFailed,
AckReadFailed,
Rejected,
InvalidAck
};
StartTriggerResult SendStartTrigger(DWORD& win32Error, const DebugLogger& log)
{
win32Error = ERROR_SUCCESS;
log.Log(L"Oeffne neuen DVC fuer START-Trigger.");
HANDLE channel = WTSVirtualChannelOpenEx(
WTS_CURRENT_SESSION,
const_cast<LPSTR>(hook::protocol::kChannelName),
WTS_CHANNEL_OPTION_DYNAMIC | WTS_CHANNEL_OPTION_DYNAMIC_PRI_MED);
if (channel == nullptr)
{
win32Error = GetLastError();
return StartTriggerResult::OpenFailed;
}
log.Log(L"Sende START ueber DVC.");
if (!WriteChannelMessage(
channel,
hook::protocol::kStartMessage,
hook::protocol::kStartMessageLength,
win32Error))
{
WTSVirtualChannelClose(channel);
return StartTriggerResult::WriteFailed;
}
std::vector<char> response;
if (!hook::dvc::ReadMessage(channel, kDvcTimeoutMs, response, win32Error))
{
WTSVirtualChannelClose(channel);
return StartTriggerResult::AckReadFailed;
}
WTSVirtualChannelClose(channel);
log.Log(L"START-Quittung empfangen, Payload-Bytes=" + std::to_wstring(response.size()) + L".");
if (response.size() == hook::protocol::kStartOkResponseLength &&
std::memcmp(
response.data(),
hook::protocol::kStartOkResponse,
hook::protocol::kStartOkResponseLength) == 0)
{
return StartTriggerResult::Success;
}
if (response.size() == hook::protocol::kStartFailedResponseLength &&
std::memcmp(
response.data(),
hook::protocol::kStartFailedResponse,
hook::protocol::kStartFailedResponseLength) == 0)
{
return StartTriggerResult::Rejected;
}
win32Error = ERROR_INVALID_DATA;
return StartTriggerResult::InvalidAck;
}
}
int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
const std::filesystem::path exePath = GetOwnPath();
if (exePath.empty())
{
DebugMessage(L"Eigener EXE-Pfad konnte nicht ermittelt werden.");
return static_cast<int>(ExitCode::IniPathFailed);
}
std::filesystem::path iniPath = exePath;
iniPath.replace_extension(L".ini");
std::error_code ec;
if (!std::filesystem::exists(iniPath, ec))
{
if (ec || !WriteNewUnicodeIni(iniPath))
{
DebugMessage(L"INI konnte nicht angelegt werden: " + iniPath.wstring());
return static_cast<int>(ExitCode::IniCreateFailed);
}
}
// Auch bei bereits vorhandenen INIs aus aelteren Versionen wird DEBUG=0
// einmalig ergaenzt, ohne die bestehenden Pfade zu veraendern.
if (!EnsureDebugIniKey(iniPath))
{
DebugMessage(L"DEBUG-Schluessel konnte in der INI nicht ergaenzt werden; Verarbeitung laeuft ohne Datei-Log weiter.");
}
DebugLogger log;
log.Initialize(ReadIniBool(iniPath, kIniDebugKey, false), exePath);
log.Log(L"=== Start ===");
log.Log(L"EXE=" + exePath.wstring());
log.Log(L"INI=" + iniPath.wstring());
if (ReadIniBool(iniPath, kIniDebugKey, false))
{
log.Log(L"DEBUG=1, Logdatei=" + log.Path().wstring());
}
const std::filesystem::path tempPath = ReadIniValue(iniPath, kIniTempPathKey, kDefaultTempPath);
const std::filesystem::path finalPath = ReadIniValue(iniPath, kIniPathKey, kDefaultSiominPath);
log.Log(L"SIOMIN_TEMP_PATH=" + tempPath.wstring());
log.Log(L"SIOMIN_PATH=" + finalPath.wstring());
if (tempPath.empty() || finalPath.empty() || IsSamePath(tempPath, finalPath))
{
log.Log(L"SIOMIN_TEMP_PATH/SIOMIN_PATH sind ungueltig oder identisch.");
return static_cast<int>(ExitCode::FinalPathInvalid);
}
log.Log(L"Oeffne DVC fuer Client-Hostname-Abfrage.");
HANDLE hostnameChannel = WTSVirtualChannelOpenEx(
WTS_CURRENT_SESSION,
const_cast<LPSTR>(hook::protocol::kChannelName),
WTS_CHANNEL_OPTION_DYNAMIC | WTS_CHANNEL_OPTION_DYNAMIC_PRI_MED);
if (hostnameChannel == nullptr)
{
log.LogWin32Error(L"WTSVirtualChannelOpenEx (Hostname)", GetLastError());
return static_cast<int>(ExitCode::ChannelOpenFailed);
}
std::wstring clientHostname;
DWORD dvcError = ERROR_SUCCESS;
const HostnameQueryResult hostnameResult = QueryClientHostname(
hostnameChannel,
clientHostname,
dvcError,
log);
WTSVirtualChannelClose(hostnameChannel);
if (hostnameResult != HostnameQueryResult::Success)
{
if (hostnameResult == HostnameQueryResult::WriteFailed)
{
log.LogWin32Error(L"Hostname-Anfrage ueber DVC schreiben", dvcError);
return static_cast<int>(ExitCode::HostnameRequestWriteFailed);
}
if (hostnameResult == HostnameQueryResult::ReadFailed)
{
log.LogWin32Error(L"Client-Hostname ueber DVC lesen", dvcError);
return static_cast<int>(ExitCode::HostnameResponseReadFailed);
}
log.LogWin32Error(L"Ungueltige DVC-Hostname-Antwort", dvcError);
return static_cast<int>(ExitCode::InvalidHostnameResponse);
}
log.Log(L"Client-Hostname=" + clientHostname);
std::vector<std::uint8_t> hostnameBytes;
if (!HostnameToSiominBytes(clientHostname, hostnameBytes))
{
log.Log(L"Client-Hostname ist fuer das SIOMIN-Byteformat nicht geeignet: " + clientHostname);
return static_cast<int>(ExitCode::InvalidClientHostname);
}
std::vector<std::uint8_t> tempBytes;
if (!ReadAllBytes(tempPath, tempBytes))
{
log.Log(L"SIOMIN_TEMP_PATH konnte nicht gelesen werden: " + tempPath.wstring());
return static_cast<int>(ExitCode::TempFileReadFailed);
}
log.Log(L"Temp-SDX gelesen, Bytes=" + std::to_wstring(tempBytes.size()) + L".");
std::vector<std::uint8_t> transformed;
std::size_t replacementCount = 0;
if (!ReplaceSiominHostnameFields(tempBytes, hostnameBytes, transformed, replacementCount))
{
log.Log(L"Kein gueltiges SIOMIN-Aktions-/Hostname-Feld in der temp_siomin.sdx gefunden.");
log.Log(L"Temp-SDX Hex-Vorschau: " + HexPreview(tempBytes));
return static_cast<int>(ExitCode::NoSiominHostFieldFound);
}
log.Log(
L"SIOMIN-Transformation erfolgreich: Hostfelder=" +
std::to_wstring(replacementCount) +
L", Ausgabe-Bytes=" + std::to_wstring(transformed.size()) + L".");
bool invalidBoundary = false;
AppendState appendState{};
if (!AppendBytesPreservingExistingFile(finalPath, transformed, invalidBoundary, appendState))
{
if (invalidBoundary)
{
log.Log(L"Bestehende siomin.sdx endet nicht mit CRLF; aus Sicherheitsgruenden nicht veraendert.");
return static_cast<int>(ExitCode::FinalFileBoundaryInvalid);
}
log.LogWin32Error(L"SIOMIN_PATH anlegen/erweitern", GetLastError());
return static_cast<int>(ExitCode::FinalFileWriteFailed);
}
log.Log(
L"Finale SIOMIN-Datei erweitert. Vorherige Groesse=" +
std::to_wstring(appendState.originalSize) +
L", angehaengte Bytes=" + std::to_wstring(transformed.size()) + L".");
// Erst nachdem die finale Datei sicher geschrieben und geflusht wurde, wird
// die Quell-Datei geleert. Dadurch wird derselbe SIOMIN-Eintrag beim naechsten
// Aufruf nicht erneut verarbeitet.
if (!ClearSourceFile(tempPath))
{
const DWORD clearError = GetLastError();
log.LogWin32Error(L"temp_siomin.sdx leeren", clearError);
// Verhindert Dubletten: Wenn die Quelle nicht geleert werden konnte,
// versuchen wir den soeben angehaengten Block wieder zurueckzurollen.
if (!RollbackAppend(finalPath, appendState))
{
log.LogWin32Error(L"Rollback der finalen siomin.sdx", GetLastError());
return static_cast<int>(ExitCode::RollbackFailed);
}
log.Log(L"Finale siomin.sdx wurde wegen Fehler beim Leeren der Quelle erfolgreich zurueckgerollt.");
return static_cast<int>(ExitCode::TempFileClearFailed);
}
log.Log(L"temp_siomin.sdx nach erfolgreicher Uebernahme auf 0 Bytes geleert.");
DWORD startError = ERROR_SUCCESS;
const StartTriggerResult startResult = SendStartTrigger(startError, log);
if (startResult != StartTriggerResult::Success)
{
if (startResult == StartTriggerResult::OpenFailed)
{
log.LogWin32Error(L"DVC fuer START oeffnen", startError);
return static_cast<int>(ExitCode::ChannelOpenFailed);
}
if (startResult == StartTriggerResult::WriteFailed)
{
log.LogWin32Error(L"START-Trigger schreiben", startError);
return static_cast<int>(ExitCode::StartTriggerWriteFailed);
}
if (startResult == StartTriggerResult::AckReadFailed)
{
log.LogWin32Error(L"START-Quittung lesen", startError);
return static_cast<int>(ExitCode::StartAckReadFailed);
}
if (startResult == StartTriggerResult::Rejected)
{
log.Log(L"Client hat START mit START_FAILED beantwortet.");
return static_cast<int>(ExitCode::StartRejected);
}
log.LogWin32Error(L"Ungueltige START-Quittung", startError);
return static_cast<int>(ExitCode::InvalidStartAck);
}
log.Log(L"START_OK vom Client empfangen.");
log.Log(L"=== Erfolgreich beendet ===");
return static_cast<int>(ExitCode::Success);
}