- 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
1216 lines
37 KiB
C++
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);
|
|
} |