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