tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

wintz.cpp (16418B)


      1 // © 2016 and later: Unicode, Inc. and others.
      2 // License & terms of use: http://www.unicode.org/copyright.html
      3 /*
      4 ********************************************************************************
      5 *   Copyright (C) 2005-2015, International Business Machines
      6 *   Corporation and others.  All Rights Reserved.
      7 ********************************************************************************
      8 *
      9 * File WINTZ.CPP
     10 *
     11 ********************************************************************************
     12 */
     13 
     14 #include "unicode/utypes.h"
     15 
     16 #if U_PLATFORM_USES_ONLY_WIN32_API
     17 
     18 #include "wintz.h"
     19 #include "charstr.h"
     20 #include "cmemory.h"
     21 #include "cstring.h"
     22 
     23 #include "unicode/ures.h"
     24 #include "unicode/unistr.h"
     25 #include "uresimp.h"
     26 
     27 #ifndef WIN32_LEAN_AND_MEAN
     28 #   define WIN32_LEAN_AND_MEAN
     29 #endif
     30 #   define VC_EXTRALEAN
     31 #   define NOUSER
     32 #   define NOSERVICE
     33 #   define NOIME
     34 #   define NOMCX
     35 #include <windows.h>
     36 
     37 U_NAMESPACE_BEGIN
     38 
     39 // Note these constants and the struct are only used when dealing with the fallback path for RDP sessions.
     40 
     41 // This is the location of the time zones in the registry on Vista+ systems.
     42 // See: https://docs.microsoft.com/windows/win32/api/timezoneapi/ns-timezoneapi-dynamic_time_zone_information
     43 #define WINDOWS_TIMEZONES_REG_KEY_PATH L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones"
     44 
     45 // Max length for a registry key is 255. +1 for null.
     46 // See: https://docs.microsoft.com/windows/win32/sysinfo/registry-element-size-limits
     47 #define WINDOWS_MAX_REG_KEY_LENGTH 256
     48 
     49 #if U_PLATFORM_HAS_WINUWP_API == 0
     50 
     51 // This is the layout of the TZI binary value in the registry.
     52 // See: https://docs.microsoft.com/windows/win32/api/timezoneapi/ns-timezoneapi-time_zone_information
     53 typedef struct _REG_TZI_FORMAT {
     54    LONG Bias;
     55    LONG StandardBias;
     56    LONG DaylightBias;
     57    SYSTEMTIME StandardDate;
     58    SYSTEMTIME DaylightDate;
     59 } REG_TZI_FORMAT;
     60 
     61 #endif // U_PLATFORM_HAS_WINUWP_API
     62 
     63 /**
     64 * This is main Windows time zone detection function.
     65 * 
     66 * It returns the Windows time zone converted to an ICU time zone as a heap-allocated buffer, or nullptr upon failure.
     67 *
     68 * We use the Win32 API GetDynamicTimeZoneInformation (which is available since Vista) to get the current time zone info,
     69 * as this API returns a non-localized time zone name which can be then mapped to an ICU time zone.
     70 * 
     71 * However, in some RDP/terminal services situations, this struct isn't always fully complete, and the TimeZoneKeyName
     72 * field of the struct might be nullptr. This can happen with some 3rd party RDP clients, and also when using older versions
     73 * of the RDP protocol, which don't send the newer TimeZoneKeyNamei information and only send the StandardName and DaylightName.
     74 * 
     75 * Since these 3rd party clients and older RDP clients only send the pre-Vista time zone information to the server, this means that we 
     76 * need to fallback on using the pre-Vista methods to determine the time zone. This unfortunately requires examining the registry directly
     77 * in order to try and determine the current time zone.
     78 * 
     79 * Note that this can however still fail in some cases though if the client and server are using different languages, as the StandardName
     80 * 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
     81 * are localized in registry using the server's language. Despite that, this is the best we can do.
     82 * 
     83 * Note: This fallback method won't work for the UWP version though, as we can't use the registry APIs in UWP.
     84 * 
     85 * Once we have the current Windows time zone, then we can then map it to an ICU time zone ID (~ Olsen ID).
     86 */
     87 U_CAPI const char* U_EXPORT2
     88 uprv_detectWindowsTimeZone()
     89 {
     90    // We first try to obtain the time zone directly by using the TimeZoneKeyName field of the DYNAMIC_TIME_ZONE_INFORMATION struct.
     91    DYNAMIC_TIME_ZONE_INFORMATION dynamicTZI;
     92    uprv_memset(&dynamicTZI, 0, sizeof(dynamicTZI));
     93    SYSTEMTIME systemTimeAllZero;
     94    uprv_memset(&systemTimeAllZero, 0, sizeof(systemTimeAllZero));
     95 
     96    if (GetDynamicTimeZoneInformation(&dynamicTZI) == TIME_ZONE_ID_INVALID) {
     97        return nullptr;
     98    }
     99 
    100    // If the DST setting has been turned off in the Control Panel, then return "Etc/GMT<offset>".
    101    //
    102    // Note: This logic is based on how the Control Panel itself determines if DST is 'off' on Windows.
    103    // The code is somewhat convoluted; in a sort of pseudo-code it looks like this:
    104    // 
    105    //   IF (GetDynamicTimeZoneInformation != TIME_ZONE_ID_INVALID) && (DynamicDaylightTimeDisabled != 0) &&
    106    //      (StandardDate == DaylightDate) &&
    107    //      (
    108    //       (TimeZoneKeyName != Empty && StandardDate == 0) ||
    109    //       (TimeZoneKeyName == Empty && StandardDate != 0)
    110    //      )
    111    //   THEN
    112    //     DST setting is "Disabled".
    113    //
    114    if (dynamicTZI.DynamicDaylightTimeDisabled != 0 &&
    115        uprv_memcmp(&dynamicTZI.StandardDate, &dynamicTZI.DaylightDate, sizeof(dynamicTZI.StandardDate)) == 0 &&
    116        ((dynamicTZI.TimeZoneKeyName[0] != L'\0' && uprv_memcmp(&dynamicTZI.StandardDate, &systemTimeAllZero, sizeof(systemTimeAllZero)) == 0) ||
    117         (dynamicTZI.TimeZoneKeyName[0] == L'\0' && uprv_memcmp(&dynamicTZI.StandardDate, &systemTimeAllZero, sizeof(systemTimeAllZero)) != 0)))
    118    {
    119        LONG utcOffsetMins = dynamicTZI.Bias;
    120        if (utcOffsetMins == 0) {
    121            return uprv_strdup("Etc/UTC");
    122        }
    123 
    124        // No way to support when DST is turned off and the offset in minutes is not a multiple of 60.
    125        if (utcOffsetMins % 60 == 0) {
    126            char gmtOffsetTz[11] = {}; // "Etc/GMT+dd" is 11-char long with a terminal null.
    127            // Important note on the sign convention for zones:
    128            //
    129            // From https://en.wikipedia.org/wiki/Tz_database#Area
    130            //   "In order to conform with the POSIX style, those zone names beginning with "Etc/GMT" have their sign reversed
    131            //   from the standard ISO 8601 convention. In the "Etc" area, zones west of GMT have a positive sign and those
    132            //   east have a negative sign in their name (e.g "Etc/GMT-14" is 14 hours ahead of GMT)."
    133            //
    134            // Regarding the POSIX style, from https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
    135            //   "The offset specifies the time value you must add to the local time to get a Coordinated Universal Time value."
    136            //
    137            // However, the Bias value in DYNAMIC_TIME_ZONE_INFORMATION *already* follows the POSIX convention.
    138            // 
    139            // From https://docs.microsoft.com/en-us/windows/win32/api/timezoneapi/ns-timezoneapi-dynamic_time_zone_information
    140            //   "The bias is the difference, in minutes, between Coordinated Universal Time (UTC) and
    141            //   local time. All translations between UTC and local time are based on the following formula:
    142            //      UTC = local time + bias"
    143            //
    144            // For example, a time zone that is 3 hours ahead of UTC (UTC+03:00) would have a Bias value of -180, and the
    145            // corresponding time zone ID would be "Etc/GMT-3". (So there is no need to negate utcOffsetMins below.)
    146            int ret = snprintf(gmtOffsetTz, sizeof(gmtOffsetTz), "Etc/GMT%+ld", utcOffsetMins / 60);
    147            if (ret > 0 && ret < UPRV_LENGTHOF(gmtOffsetTz)) {
    148                return uprv_strdup(gmtOffsetTz);
    149            }
    150        }
    151    }
    152 
    153    // If DST is NOT disabled, but the TimeZoneKeyName field of the struct is nullptr, then we may be dealing with a
    154    // RDP/terminal services session where the 'Time Zone Redirection' feature is enabled. However, either the RDP
    155    // client sent the server incomplete info (some 3rd party RDP clients only send the StandardName and  DaylightName,
    156    // but do not send the important TimeZoneKeyName), or if the RDP server has not appropriately populated the struct correctly.
    157    //
    158    // In this case we unfortunately have no choice but to fallback to using the pre-Vista method of determining the
    159    // time zone, which requires examining the registry directly.
    160    //
    161    // Note that this can however still fail though if the client and server are using different languages, as the StandardName
    162    // that is sent by client is *localized* in the client's language. However, we must compare this to the names that are
    163    // on the server, which are *localized* in registry using the server's language.
    164    //
    165    // One other note is that this fallback method doesn't work for the UWP version, as we can't use the registry APIs.
    166 
    167    // windowsTimeZoneName will point at timezoneSubKeyName if we had to fallback to using the registry, and we found a match.
    168    WCHAR timezoneSubKeyName[WINDOWS_MAX_REG_KEY_LENGTH];
    169    WCHAR *windowsTimeZoneName = dynamicTZI.TimeZoneKeyName;
    170 
    171    if (dynamicTZI.TimeZoneKeyName[0] == 0) {
    172 
    173 // We can't use the registry APIs in the UWP version.
    174 #if U_PLATFORM_HAS_WINUWP_API == 1
    175        (void)timezoneSubKeyName; // suppress unused variable warnings.
    176        return nullptr;
    177 #else
    178        // Open the path to the time zones in the Windows registry.
    179        LONG ret;
    180        HKEY hKeyAllTimeZones = nullptr;
    181        ret = RegOpenKeyExW(HKEY_LOCAL_MACHINE, WINDOWS_TIMEZONES_REG_KEY_PATH, 0, KEY_READ,
    182                            reinterpret_cast<PHKEY>(&hKeyAllTimeZones));
    183        
    184        if (ret != ERROR_SUCCESS) {
    185            // If we can't open the key, then we can't do much, so fail.
    186            return nullptr;
    187        }
    188 
    189        // Read the number of subkeys under the time zone registry path.
    190        DWORD numTimeZoneSubKeys;
    191        ret = RegQueryInfoKeyW(hKeyAllTimeZones, nullptr, nullptr, nullptr, &numTimeZoneSubKeys,
    192                               nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
    193        
    194        if (ret != ERROR_SUCCESS) {
    195            RegCloseKey(hKeyAllTimeZones);
    196            return nullptr;
    197        }
    198 
    199        // Examine each of the subkeys to try and find a match for the localized standard name ("Std").
    200        //
    201        // Note: The name of the time zone subkey itself is not localized, but the "Std" name is localized. This means
    202        // that we could fail to find a match if the RDP client and RDP server are using different languages, but unfortunately
    203        // there isn't much we can do about it.
    204        HKEY hKeyTimeZoneSubKey = nullptr;
    205        ULONG registryValueType;
    206        WCHAR registryStandardName[WINDOWS_MAX_REG_KEY_LENGTH];
    207 
    208        for (DWORD i = 0; i < numTimeZoneSubKeys; i++) {
    209            // Note: RegEnumKeyExW wants the size of the buffer in characters.
    210            DWORD size = UPRV_LENGTHOF(timezoneSubKeyName);
    211            ret = RegEnumKeyExW(hKeyAllTimeZones, i, timezoneSubKeyName, &size, nullptr, nullptr, nullptr, nullptr);
    212 
    213            if (ret != ERROR_SUCCESS) {
    214                RegCloseKey(hKeyAllTimeZones);
    215                return nullptr;
    216            }
    217            
    218            ret = RegOpenKeyExW(hKeyAllTimeZones, timezoneSubKeyName, 0, KEY_READ,
    219                                reinterpret_cast<PHKEY>(&hKeyTimeZoneSubKey));
    220            
    221            if (ret != ERROR_SUCCESS) {
    222                RegCloseKey(hKeyAllTimeZones);
    223                return nullptr;
    224            }
    225 
    226            // Note: RegQueryValueExW wants the size of the buffer in bytes.
    227            size = sizeof(registryStandardName);
    228            ret = RegQueryValueExW(hKeyTimeZoneSubKey, L"Std", nullptr, &registryValueType,
    229                                   reinterpret_cast<LPBYTE>(registryStandardName), &size);
    230            
    231            if (ret != ERROR_SUCCESS || registryValueType != REG_SZ) {
    232                RegCloseKey(hKeyTimeZoneSubKey);
    233                RegCloseKey(hKeyAllTimeZones);
    234                return nullptr;
    235            }
    236 
    237            // Note: wcscmp does an ordinal (byte) comparison.
    238            if (wcscmp(reinterpret_cast<WCHAR *>(registryStandardName), dynamicTZI.StandardName) == 0) {
    239                // Since we are comparing the *localized* time zone name, it's possible that some languages might use
    240                // the same string for more than one time zone. Thus we need to examine the TZI data in the registry to
    241                // compare the GMT offset (the bias), and the DST transition dates, to ensure it's the same time zone
    242                // as the currently reported one.
    243                REG_TZI_FORMAT registryTziValue;
    244                uprv_memset(&registryTziValue, 0, sizeof(registryTziValue));
    245 
    246                // Note: RegQueryValueExW wants the size of the buffer in bytes.
    247                DWORD timezoneTziValueSize = sizeof(registryTziValue);
    248                ret = RegQueryValueExW(hKeyTimeZoneSubKey, L"TZI", nullptr, &registryValueType,
    249                                     reinterpret_cast<LPBYTE>(&registryTziValue), &timezoneTziValueSize);
    250 
    251                if (ret == ERROR_SUCCESS) {
    252                    if ((dynamicTZI.Bias == registryTziValue.Bias) &&
    253                        (memcmp((const void *)&dynamicTZI.StandardDate, (const void *)&registryTziValue.StandardDate, sizeof(SYSTEMTIME)) == 0) &&
    254                        (memcmp((const void *)&dynamicTZI.DaylightDate, (const void *)&registryTziValue.DaylightDate, sizeof(SYSTEMTIME)) == 0))
    255                    {
    256                        // We found a matching time zone.
    257                        windowsTimeZoneName = timezoneSubKeyName;
    258                        break;
    259                    }
    260                }
    261            }
    262            RegCloseKey(hKeyTimeZoneSubKey);
    263            hKeyTimeZoneSubKey = nullptr;
    264        }
    265 
    266        if (hKeyTimeZoneSubKey != nullptr) {
    267            RegCloseKey(hKeyTimeZoneSubKey);
    268        }
    269        if (hKeyAllTimeZones != nullptr) {
    270            RegCloseKey(hKeyAllTimeZones);
    271        }
    272 #endif // U_PLATFORM_HAS_WINUWP_API
    273    }
    274 
    275    CharString winTZ;
    276    UErrorCode status = U_ZERO_ERROR;
    277    winTZ.appendInvariantChars(UnicodeString(true, windowsTimeZoneName, -1), status);
    278 
    279    // Map Windows Timezone name (non-localized) to ICU timezone ID (~ Olson timezone id).
    280    StackUResourceBundle winTZBundle;
    281    ures_openDirectFillIn(winTZBundle.getAlias(), nullptr, "windowsZones", &status);
    282    ures_getByKey(winTZBundle.getAlias(), "mapTimezones", winTZBundle.getAlias(), &status);
    283    ures_getByKey(winTZBundle.getAlias(), winTZ.data(), winTZBundle.getAlias(), &status);
    284 
    285    if (U_FAILURE(status)) {
    286        return nullptr;
    287    }
    288    
    289    // Note: Since the ISO 3166 country/region codes are all invariant ASCII chars, we can
    290    // directly downcast from wchar_t to do the conversion.
    291    // We could call the A version of the GetGeoInfo API, but that would be slightly slower than calling the W API,
    292    // as the A version of the API will end up calling MultiByteToWideChar anyways internally.
    293    wchar_t regionCodeW[3] = {};
    294    char regionCode[3] = {}; // 2 letter ISO 3166 country/region code made entirely of invariant chars.
    295    int geoId = GetUserGeoID(GEOCLASS_NATION);
    296    int regionCodeLen = GetGeoInfoW(geoId, GEO_ISO2, regionCodeW, UPRV_LENGTHOF(regionCodeW), 0);
    297 
    298    const char16_t *icuTZ16 = nullptr;
    299    int32_t tzListLen = 0;
    300 
    301    if (regionCodeLen != 0) {
    302        for (int i = 0; i < UPRV_LENGTHOF(regionCodeW); i++) {
    303            regionCode[i] = static_cast<char>(regionCodeW[i]);
    304        }
    305        icuTZ16 = ures_getStringByKey(winTZBundle.getAlias(), regionCode, &tzListLen, &status);
    306    }
    307    if (regionCodeLen == 0 || U_FAILURE(status)) {
    308        // fallback to default "001" (world)
    309        status = U_ZERO_ERROR;
    310        icuTZ16 = ures_getStringByKey(winTZBundle.getAlias(), "001", &tzListLen, &status);
    311    }
    312 
    313    // Note: We want the first entry in the string returned by ures_getStringByKey.
    314    // However this string can be a space delimited list of timezones:
    315    //  Ex: "America/New_York America/Detroit America/Indiana/Petersburg ..."
    316    // We need to stop at the first space, so we pass tzLen (instead of tzListLen) to appendInvariantChars below.
    317    int32_t tzLen = 0;
    318    if (tzListLen > 0) {
    319        while (!(icuTZ16[tzLen] == u'\0' || icuTZ16[tzLen] == u' ')) {
    320            tzLen++;
    321        }
    322    }
    323 
    324    // Note: cloneData returns nullptr if the status is a failure, so this
    325    // will return nullptr if the above look-up fails.
    326    CharString icuTZStr;
    327    return icuTZStr.appendInvariantChars(icuTZ16, tzLen, status).cloneData(status);
    328 }
    329 
    330 U_NAMESPACE_END
    331 #endif /* U_PLATFORM_USES_ONLY_WIN32_API  */