1"""
2Various functions to be used by windows during start up and to monkey patch
3missing functions in other modules.
4"""
5
6import ctypes
7import platform
8import re
9
10from salt.exceptions import CommandExecutionError
11
12try:
13    import psutil
14    import pywintypes
15    import win32api
16    import win32net
17    import win32security
18    from win32con import HWND_BROADCAST, WM_SETTINGCHANGE, SMTO_ABORTIFHUNG
19
20    HAS_WIN32 = True
21except ImportError:
22    HAS_WIN32 = False
23
24
25# Although utils are often directly imported, it is also possible to use the
26# loader.
27def __virtual__():
28    """
29    Only load if Win32 Libraries are installed
30    """
31    if not HAS_WIN32:
32        return False, "This utility requires pywin32"
33
34    return "win_functions"
35
36
37def get_parent_pid():
38    """
39    This is a monkey patch for os.getppid. Used in:
40    - salt.utils.parsers
41
42    Returns:
43        int: The parent process id
44    """
45    return psutil.Process().ppid()
46
47
48def is_admin(name):
49    """
50    Is the passed user a member of the Administrators group
51
52    Args:
53        name (str): The name to check
54
55    Returns:
56        bool: True if user is a member of the Administrators group, False
57        otherwise
58    """
59    groups = get_user_groups(name, True)
60
61    for group in groups:
62        if group in ("S-1-5-32-544", "S-1-5-18"):
63            return True
64
65    return False
66
67
68def get_user_groups(name, sid=False):
69    """
70    Get the groups to which a user belongs
71
72    Args:
73        name (str): The user name to query
74        sid (bool): True will return a list of SIDs, False will return a list of
75        group names
76
77    Returns:
78        list: A list of group names or sids
79    """
80    groups = []
81    if name.upper() == "SYSTEM":
82        # 'win32net.NetUserGetLocalGroups' will fail if you pass in 'SYSTEM'.
83        groups = ["SYSTEM"]
84    else:
85        try:
86            groups = win32net.NetUserGetLocalGroups(None, name)
87        except (win32net.error, pywintypes.error) as exc:
88            # ERROR_ACCESS_DENIED, NERR_DCNotFound, RPC_S_SERVER_UNAVAILABLE
89            if exc.winerror in (5, 1722, 2453, 1927, 1355):
90                # Try without LG_INCLUDE_INDIRECT flag, because the user might
91                # not have permissions for it or something is wrong with DC
92                groups = win32net.NetUserGetLocalGroups(None, name, 0)
93            else:
94                # If this fails, try once more but instead with global groups.
95                try:
96                    groups = win32net.NetUserGetGroups(None, name)
97                except win32net.error as exc:
98                    if exc.winerror in (5, 1722, 2453, 1927, 1355):
99                        # Try without LG_INCLUDE_INDIRECT flag, because the user might
100                        # not have permissions for it or something is wrong with DC
101                        groups = win32net.NetUserGetLocalGroups(None, name, 0)
102                except pywintypes.error:
103                    if exc.winerror in (5, 1722, 2453, 1927, 1355):
104                        # Try with LG_INCLUDE_INDIRECT flag, because the user might
105                        # not have permissions for it or something is wrong with DC
106                        groups = win32net.NetUserGetLocalGroups(None, name, 1)
107                    else:
108                        raise
109
110    if not sid:
111        return groups
112
113    ret_groups = []
114    for group in groups:
115        ret_groups.append(get_sid_from_name(group))
116
117    return ret_groups
118
119
120def get_sid_from_name(name):
121    """
122    This is a tool for getting a sid from a name. The name can be any object.
123    Usually a user or a group
124
125    Args:
126        name (str): The name of the user or group for which to get the sid
127
128    Returns:
129        str: The corresponding SID
130    """
131    # If None is passed, use the Universal Well-known SID "Null SID"
132    if name is None:
133        name = "NULL SID"
134
135    try:
136        sid = win32security.LookupAccountName(None, name)[0]
137    except pywintypes.error as exc:
138        raise CommandExecutionError("User {} not found: {}".format(name, exc))
139
140    return win32security.ConvertSidToStringSid(sid)
141
142
143def get_current_user(with_domain=True):
144    """
145    Gets the user executing the process
146
147    Args:
148
149        with_domain (bool):
150            ``True`` will prepend the user name with the machine name or domain
151            separated by a backslash
152
153    Returns:
154        str: The user name
155    """
156    try:
157        user_name = win32api.GetUserNameEx(win32api.NameSamCompatible)
158        if user_name[-1] == "$":
159            # Make the system account easier to identify.
160            # Fetch sid so as to handle other language than english
161            test_user = win32api.GetUserName()
162            if test_user == "SYSTEM":
163                user_name = "SYSTEM"
164            elif get_sid_from_name(test_user) == "S-1-5-18":
165                user_name = "SYSTEM"
166        elif not with_domain:
167            user_name = win32api.GetUserName()
168    except pywintypes.error as exc:
169        raise CommandExecutionError("Failed to get current user: {}".format(exc))
170
171    if not user_name:
172        return False
173
174    return user_name
175
176
177def get_sam_name(username):
178    r"""
179    Gets the SAM name for a user. It basically prefixes a username without a
180    backslash with the computer name. If the user does not exist, a SAM
181    compatible name will be returned using the local hostname as the domain.
182
183    i.e. salt.utils.get_same_name('Administrator') would return 'DOMAIN.COM\Administrator'
184
185    .. note:: Long computer names are truncated to 15 characters
186    """
187    try:
188        sid_obj = win32security.LookupAccountName(None, username)[0]
189    except pywintypes.error:
190        return "\\".join([platform.node()[:15].upper(), username])
191    username, domain, _ = win32security.LookupAccountSid(None, sid_obj)
192    return "\\".join([domain, username])
193
194
195def enable_ctrl_logoff_handler():
196    """
197    Set the control handler on the console
198    """
199    if HAS_WIN32:
200        ctrl_logoff_event = 5
201        win32api.SetConsoleCtrlHandler(
202            lambda event: True if event == ctrl_logoff_event else False, 1
203        )
204
205
206def escape_argument(arg, escape=True):
207    """
208    Escape the argument for the cmd.exe shell.
209    See http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
210
211    First we escape the quote chars to produce a argument suitable for
212    CommandLineToArgvW. We don't need to do this for simple arguments.
213
214    Args:
215        arg (str): a single command line argument to escape for the cmd.exe shell
216
217    Kwargs:
218        escape (bool): True will call the escape_for_cmd_exe() function
219                       which escapes the characters '()%!^"<>&|'. False
220                       will not call the function and only quotes the cmd
221
222    Returns:
223        str: an escaped string suitable to be passed as a program argument to the cmd.exe shell
224    """
225    if not arg or re.search(r'(["\s])', arg):
226        arg = '"' + arg.replace('"', r"\"") + '"'
227
228    if not escape:
229        return arg
230    return escape_for_cmd_exe(arg)
231
232
233def escape_for_cmd_exe(arg):
234    """
235    Escape an argument string to be suitable to be passed to
236    cmd.exe on Windows
237
238    This method takes an argument that is expected to already be properly
239    escaped for the receiving program to be properly parsed. This argument
240    will be further escaped to pass the interpolation performed by cmd.exe
241    unchanged.
242
243    Any meta-characters will be escaped, removing the ability to e.g. use
244    redirects or variables.
245
246    Args:
247        arg (str): a single command line argument to escape for cmd.exe
248
249    Returns:
250        str: an escaped string suitable to be passed as a program argument to cmd.exe
251    """
252    meta_chars = '()%!^"<>&|'
253    meta_re = re.compile(
254        "(" + "|".join(re.escape(char) for char in list(meta_chars)) + ")"
255    )
256    meta_map = {char: "^{}".format(char) for char in meta_chars}
257
258    def escape_meta_chars(m):
259        char = m.group(1)
260        return meta_map[char]
261
262    return meta_re.sub(escape_meta_chars, arg)
263
264
265def broadcast_setting_change(message="Environment"):
266    """
267    Send a WM_SETTINGCHANGE Broadcast to all Windows
268
269    Args:
270
271        message (str):
272            A string value representing the portion of the system that has been
273            updated and needs to be refreshed. Default is ``Environment``. These
274            are some common values:
275
276            - "Environment" : to effect a change in the environment variables
277            - "intl" : to effect a change in locale settings
278            - "Policy" : to effect a change in Group Policy Settings
279            - a leaf node in the registry
280            - the name of a section in the ``Win.ini`` file
281
282            See lParam within msdn docs for
283            `WM_SETTINGCHANGE <https://msdn.microsoft.com/en-us/library/ms725497%28VS.85%29.aspx>`_
284            for more information on Broadcasting Messages.
285
286            See GWL_WNDPROC within msdn docs for
287            `SetWindowLong <https://msdn.microsoft.com/en-us/library/windows/desktop/ms633591(v=vs.85).aspx>`_
288            for information on how to retrieve those messages.
289
290    .. note::
291        This will only affect new processes that aren't launched by services. To
292        apply changes to the path or registry to services, the host must be
293        restarted. The ``salt-minion``, if running as a service, will not see
294        changes to the environment until the system is restarted. Services
295        inherit their environment from ``services.exe`` which does not respond
296        to messaging events. See
297        `MSDN Documentation <https://support.microsoft.com/en-us/help/821761/changes-that-you-make-to-environment-variables-do-not-affect-services>`_
298        for more information.
299
300    CLI Example:
301
302    .. code-block:: python
303
304        import salt.utils.win_functions
305        salt.utils.win_functions.broadcast_setting_change('Environment')
306    """
307    # Listen for messages sent by this would involve working with the
308    # SetWindowLong function. This can be accessed via win32gui or through
309    # ctypes. You can find examples on how to do this by searching for
310    # `Accessing WGL_WNDPROC` on the internet. Here are some examples of how
311    # this might work:
312    #
313    # # using win32gui
314    # import win32con
315    # import win32gui
316    # old_function = win32gui.SetWindowLong(window_handle, win32con.GWL_WNDPROC, new_function)
317    #
318    # # using ctypes
319    # import ctypes
320    # import win32con
321    # from ctypes import c_long, c_int
322    # user32 = ctypes.WinDLL('user32', use_last_error=True)
323    # WndProcType = ctypes.WINFUNCTYPE(c_int, c_long, c_int, c_int)
324    # new_function = WndProcType
325    # old_function = user32.SetWindowLongW(window_handle, win32con.GWL_WNDPROC, new_function)
326    broadcast_message = ctypes.create_unicode_buffer(message)
327    user32 = ctypes.WinDLL("user32", use_last_error=True)
328    result = user32.SendMessageTimeoutW(
329        HWND_BROADCAST,
330        WM_SETTINGCHANGE,
331        0,
332        broadcast_message,
333        SMTO_ABORTIFHUNG,
334        5000,
335        0,
336    )
337    return result == 1
338
339
340def guid_to_squid(guid):
341    """
342    Converts a GUID   to a compressed guid (SQUID)
343
344    Each Guid has 5 parts separated by '-'. For the first three each one will be
345    totally reversed, and for the remaining two each one will be reversed by
346    every other character. Then the final compressed Guid will be constructed by
347    concatenating all the reversed parts without '-'.
348
349    .. Example::
350
351        Input:                  2BE0FA87-5B36-43CF-95C8-C68D6673FB94
352        Reversed:               78AF0EB2-63B5-FC34-598C-6CD86637BF49
353        Final Compressed Guid:  78AF0EB263B5FC34598C6CD86637BF49
354
355    Args:
356
357        guid (str): A valid GUID
358
359    Returns:
360        str: A valid compressed GUID (SQUID)
361    """
362    guid_pattern = re.compile(
363        r"^\{(\w{8})-(\w{4})-(\w{4})-(\w\w)(\w\w)-(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)\}$"
364    )
365    guid_match = guid_pattern.match(guid)
366    squid = ""
367    if guid_match is not None:
368        for index in range(1, 12):
369            squid += guid_match.group(index)[::-1]
370    return squid
371
372
373def squid_to_guid(squid):
374    """
375    Converts a compressed GUID (SQUID) back into a GUID
376
377    Args:
378
379        squid (str): A valid compressed GUID
380
381    Returns:
382        str: A valid GUID
383    """
384    squid_pattern = re.compile(
385        r"^(\w{8})(\w{4})(\w{4})(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)$"
386    )
387    squid_match = squid_pattern.match(squid)
388    guid = ""
389    if squid_match is not None:
390        guid = (
391            "{"
392            + squid_match.group(1)[::-1]
393            + "-"
394            + squid_match.group(2)[::-1]
395            + "-"
396            + squid_match.group(3)[::-1]
397            + "-"
398            + squid_match.group(4)[::-1]
399            + squid_match.group(5)[::-1]
400            + "-"
401        )
402        for index in range(6, 12):
403            guid += squid_match.group(index)[::-1]
404        guid += "}"
405    return guid
406