mirror of
https://github.com/unicode-org/icu.git
synced 2025-04-10 15:42:14 +00:00
ICU-21352 ICU4C: Windows time zone detection can fail in RDP sessions with some 3rd party software and older RDP clients
This commit is contained in:
parent
c7024d5faa
commit
df72f8cbaf
1 changed files with 165 additions and 8 deletions
|
@ -36,17 +36,58 @@
|
|||
|
||||
U_NAMESPACE_BEGIN
|
||||
|
||||
// Note these constants and the struct are only used when dealing with the fallback path for RDP sesssions.
|
||||
|
||||
// This is the location of the time zones in the registry on Vista+ systems.
|
||||
// See: https://docs.microsoft.com/windows/win32/api/timezoneapi/ns-timezoneapi-dynamic_time_zone_information
|
||||
#define WINDOWS_TIMEZONES_REG_KEY_PATH L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones"
|
||||
|
||||
// Max length for a registry key is 255. +1 for null.
|
||||
// See: https://docs.microsoft.com/windows/win32/sysinfo/registry-element-size-limits
|
||||
#define WINDOWS_MAX_REG_KEY_LENGTH 256
|
||||
|
||||
#if U_PLATFORM_HAS_WINUWP_API == 0
|
||||
|
||||
// This is the layout of the TZI binary value in the registry.
|
||||
// See: https://docs.microsoft.com/windows/win32/api/timezoneapi/ns-timezoneapi-time_zone_information
|
||||
typedef struct _REG_TZI_FORMAT {
|
||||
LONG Bias;
|
||||
LONG StandardBias;
|
||||
LONG DaylightBias;
|
||||
SYSTEMTIME StandardDate;
|
||||
SYSTEMTIME DaylightDate;
|
||||
} REG_TZI_FORMAT;
|
||||
|
||||
#endif // U_PLATFORM_HAS_WINUWP_API
|
||||
|
||||
/**
|
||||
* Main Windows time zone detection function.
|
||||
* Returns the Windows time zone converted to an ICU time zone as a heap-allocated buffer, or nullptr upon failure.
|
||||
* This is main Windows time zone detection function.
|
||||
*
|
||||
* It returns the Windows time zone converted to an ICU time zone as a heap-allocated buffer, or nullptr upon failure.
|
||||
*
|
||||
* Note: We use the Win32 API GetDynamicTimeZoneInformation (available since Vista+) to get the current time zone info.
|
||||
* This API returns a non-localized time zone name, which is mapped to an ICU time zone ID (~ Olsen ID).
|
||||
* We use the Win32 API GetDynamicTimeZoneInformation (which is available since Vista) to get the current time zone info,
|
||||
* as this API returns a non-localized time zone name which can be then mapped to an ICU time zone.
|
||||
*
|
||||
* However, in some RDP/terminal services situations, this struct isn't always fully complete, and the TimeZoneKeyName
|
||||
* field of the struct might be NULL. This can happen with some 3rd party RDP clients, and also when using older versions
|
||||
* of the RDP protocol, which don't send the newer TimeZoneKeyNamei information and only send the StandardName and DaylightName.
|
||||
*
|
||||
* Since these 3rd party clients and older RDP clients only send the pre-Vista time zone information to the server, this means that we
|
||||
* need to fallback on using the pre-Vista methods to determine the time zone. This unfortunately requires examining the registry directly
|
||||
* in order to try and determine the current time zone.
|
||||
*
|
||||
* Note that this can however still fail in some cases though if the client and server are using different languages, as the StandardName
|
||||
* that is sent by client is localized in the client's language. However, we must compare this to the names that are on the server, which
|
||||
* are localized in registry using the server's language. Despite that, this is the best we can do.
|
||||
*
|
||||
* Note: This fallback method won't work for the UWP version though, as we can't use the registry APIs in UWP.
|
||||
*
|
||||
* Once we have the current Windows time zone, then we can then map it to an ICU time zone ID (~ Olsen ID).
|
||||
*/
|
||||
U_CAPI const char* U_EXPORT2
|
||||
uprv_detectWindowsTimeZone()
|
||||
{
|
||||
// Obtain the DYNAMIC_TIME_ZONE_INFORMATION info to get the non-localized time zone name.
|
||||
// We first try to obtain the time zone directly by using the TimeZoneKeyName field of the DYNAMIC_TIME_ZONE_INFORMATION struct.
|
||||
DYNAMIC_TIME_ZONE_INFORMATION dynamicTZI;
|
||||
uprv_memset(&dynamicTZI, 0, sizeof(dynamicTZI));
|
||||
SYSTEMTIME systemTimeAllZero;
|
||||
|
@ -93,15 +134,131 @@ uprv_detectWindowsTimeZone()
|
|||
}
|
||||
}
|
||||
|
||||
// If DST is NOT disabled, but we have an empty TimeZoneKeyName, then it is unclear
|
||||
// what we should do as this should not happen.
|
||||
// If DST is NOT disabled, but the TimeZoneKeyName field of the struct is NULL, then we may be dealing with a
|
||||
// RDP/terminal services session where the 'Time Zone Redirection' feature is enabled. However, either the RDP
|
||||
// client sent the server incomplete info (some 3rd party RDP clients only send the StandardName and DaylightName,
|
||||
// but do not send the important TimeZoneKeyName), or if the RDP server has not appropriately populated the struct correctly.
|
||||
//
|
||||
// In this case we unfortunately have no choice but to fallback to using the pre-Vista method of determining the
|
||||
// time zone, which requires examining the registry directly.
|
||||
//
|
||||
// Note that this can however still fail though if the client and server are using different languages, as the StandardName
|
||||
// that is sent by client is *localized* in the client's language. However, we must compare this to the names that are
|
||||
// on the server, which are *localized* in registry using the server's language.
|
||||
//
|
||||
// One other note is that this fallback method doesn't work for the UWP version, as we can't use the registry APIs.
|
||||
|
||||
// windowsTimeZoneName will point at timezoneSubKeyName if we had to fallback to using the registry, and we found a match.
|
||||
WCHAR timezoneSubKeyName[WINDOWS_MAX_REG_KEY_LENGTH];
|
||||
WCHAR *windowsTimeZoneName = dynamicTZI.TimeZoneKeyName;
|
||||
|
||||
if (dynamicTZI.TimeZoneKeyName[0] == 0) {
|
||||
|
||||
// We can't use the registry APIs in the UWP version.
|
||||
#if U_PLATFORM_HAS_WINUWP_API == 1
|
||||
(void)timezoneSubKeyName; // suppress unused variable warnings.
|
||||
return nullptr;
|
||||
#else
|
||||
// Open the path to the time zones in the Windows registry.
|
||||
LONG ret;
|
||||
HKEY hKeyAllTimeZones = nullptr;
|
||||
ret = RegOpenKeyExW(HKEY_LOCAL_MACHINE, WINDOWS_TIMEZONES_REG_KEY_PATH, 0, KEY_READ,
|
||||
reinterpret_cast<PHKEY>(&hKeyAllTimeZones));
|
||||
|
||||
if (ret != ERROR_SUCCESS) {
|
||||
// If we can't open the key, then we can't do much, so fail.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Read the number of subkeys under the time zone registry path.
|
||||
DWORD numTimeZoneSubKeys;
|
||||
ret = RegQueryInfoKeyW(hKeyAllTimeZones, nullptr, nullptr, nullptr, &numTimeZoneSubKeys,
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
|
||||
|
||||
if (ret != ERROR_SUCCESS) {
|
||||
RegCloseKey(hKeyAllTimeZones);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Examine each of the subkeys to try and find a match for the localized standard name ("Std").
|
||||
//
|
||||
// Note: The name of the time zone subkey itself is not localized, but the "Std" name is localized. This means
|
||||
// that we could fail to find a match if the RDP client and RDP server are using different languages, but unfortunately
|
||||
// there isn't much we can do about it.
|
||||
HKEY hKeyTimeZoneSubKey = nullptr;
|
||||
ULONG registryValueType;
|
||||
WCHAR registryStandardName[WINDOWS_MAX_REG_KEY_LENGTH];
|
||||
|
||||
for (DWORD i = 0; i < numTimeZoneSubKeys; i++) {
|
||||
// Note: RegEnumKeyExW wants the size of the buffer in characters.
|
||||
DWORD size = UPRV_LENGTHOF(timezoneSubKeyName);
|
||||
ret = RegEnumKeyExW(hKeyAllTimeZones, i, timezoneSubKeyName, &size, nullptr, nullptr, nullptr, nullptr);
|
||||
|
||||
if (ret != ERROR_SUCCESS) {
|
||||
RegCloseKey(hKeyAllTimeZones);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ret = RegOpenKeyExW(hKeyAllTimeZones, timezoneSubKeyName, 0, KEY_READ,
|
||||
reinterpret_cast<PHKEY>(&hKeyTimeZoneSubKey));
|
||||
|
||||
if (ret != ERROR_SUCCESS) {
|
||||
RegCloseKey(hKeyAllTimeZones);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Note: RegQueryValueExW wants the size of the buffer in bytes.
|
||||
size = sizeof(registryStandardName);
|
||||
ret = RegQueryValueExW(hKeyTimeZoneSubKey, L"Std", nullptr, ®istryValueType,
|
||||
reinterpret_cast<LPBYTE>(registryStandardName), &size);
|
||||
|
||||
if (ret != ERROR_SUCCESS || registryValueType != REG_SZ) {
|
||||
RegCloseKey(hKeyTimeZoneSubKey);
|
||||
RegCloseKey(hKeyAllTimeZones);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Note: wcscmp does an ordinal (byte) comparison.
|
||||
if (wcscmp(reinterpret_cast<WCHAR *>(registryStandardName), dynamicTZI.StandardName) == 0) {
|
||||
// Since we are comparing the *localized* time zone name, it's possible that some languages might use
|
||||
// the same string for more than one time zone. Thus we need to examine the TZI data in the registry to
|
||||
// compare the GMT offset (the bias), and the DST transition dates, to ensure it's the same time zone
|
||||
// as the currently reported one.
|
||||
REG_TZI_FORMAT registryTziValue;
|
||||
uprv_memset(®istryTziValue, 0, sizeof(registryTziValue));
|
||||
|
||||
// Note: RegQueryValueExW wants the size of the buffer in bytes.
|
||||
DWORD timezoneTziValueSize = sizeof(registryTziValue);
|
||||
ret = RegQueryValueExW(hKeyTimeZoneSubKey, L"TZI", nullptr, ®istryValueType,
|
||||
reinterpret_cast<LPBYTE>(®istryTziValue), &timezoneTziValueSize);
|
||||
|
||||
if (ret == ERROR_SUCCESS) {
|
||||
if ((dynamicTZI.Bias == registryTziValue.Bias) &&
|
||||
(memcmp((const void *)&dynamicTZI.StandardDate, (const void *)®istryTziValue.StandardDate, sizeof(SYSTEMTIME)) == 0) &&
|
||||
(memcmp((const void *)&dynamicTZI.DaylightDate, (const void *)®istryTziValue.DaylightDate, sizeof(SYSTEMTIME)) == 0))
|
||||
{
|
||||
// We found a matching time zone.
|
||||
windowsTimeZoneName = timezoneSubKeyName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
RegCloseKey(hKeyTimeZoneSubKey);
|
||||
hKeyTimeZoneSubKey = nullptr;
|
||||
}
|
||||
|
||||
if (hKeyTimeZoneSubKey != nullptr) {
|
||||
RegCloseKey(hKeyTimeZoneSubKey);
|
||||
}
|
||||
if (hKeyAllTimeZones != nullptr) {
|
||||
RegCloseKey(hKeyAllTimeZones);
|
||||
}
|
||||
#endif // U_PLATFORM_HAS_WINUWP_API
|
||||
}
|
||||
|
||||
CharString winTZ;
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
winTZ.appendInvariantChars(UnicodeString(TRUE, dynamicTZI.TimeZoneKeyName, -1), status);
|
||||
winTZ.appendInvariantChars(UnicodeString(TRUE, windowsTimeZoneName, -1), status);
|
||||
|
||||
// Map Windows Timezone name (non-localized) to ICU timezone ID (~ Olson timezone id).
|
||||
StackUResourceBundle winTZBundle;
|
||||
|
|
Loading…
Add table
Reference in a new issue