1 #!powershell 2 3 # Copyright: (c) 2019, Ansible Project 4 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 6 #AnsibleRequires -CSharpUtil Ansible.Basic 7 #Requires -Module Ansible.ModuleUtils.AddType 8 9 $spec = @{ 10 options = @{ 11 auto_detect = @{ type = "bool"; default = $true } 12 auto_config_url = @{ type = "str" } 13 proxy = @{ type = "raw" } 14 bypass = @{ type = "list"; elements = "str"; no_log = $false } 15 connection = @{ type = "str" } 16 } 17 required_by = @{ 18 bypass = @("proxy") 19 } 20 supports_check_mode = $true 21 } 22 23 $module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) 24 25 $auto_detect = $module.Params.auto_detect 26 $auto_config_url = $module.Params.auto_config_url 27 $proxy = $module.Params.proxy 28 $bypass = $module.Params.bypass 29 $connection = $module.Params.connection 30 31 # Parse the raw value, it should be a Dictionary or String 32 if ($proxy -is [System.Collections.IDictionary]) { 33 $valid_keys = [System.Collections.Generic.List`1[String]]@("http", "https", "ftp", "socks") 34 # Check to make sure we don't have any invalid keys in the dict 35 $invalid_keys = [System.Collections.Generic.List`1[String]]@() 36 foreach ($k in $proxy.Keys) { 37 if ($k -notin $valid_keys) { 38 $invalid_keys.Add($k) 39 } 40 } 41 42 if ($invalid_keys.Count -gt 0) { 43 $invalid_keys = $invalid_keys | Sort-Object # So our test assertion doesn't fail due to random ordering 44 $module.FailJson("Invalid keys found in proxy: $($invalid_keys -join ', '). Valid keys are $($valid_keys -join ', ').") 45 } 46 47 # Build the proxy string in the form 'protocol=host;', the order of valid_keys is also important 48 $proxy_list = [System.Collections.Generic.List`1[String]]@() 49 foreach ($k in $valid_keys) { 50 if ($proxy.ContainsKey($k)) { 51 $proxy_list.Add("$k=$($proxy.$k)") 52 } 53 } 54 $proxy = $proxy_list -join ";" 55 } elseif ($null -ne $proxy) { 56 $proxy = $proxy.ToString() 57 } 58 59 if ($bypass) { 60 if ([System.String]::IsNullOrEmpty($proxy)) { 61 $module.FailJson("missing parameter(s) required by ''bypass'': proxy") 62 } 63 $bypass = $bypass -join ';' 64 } 65 66 $win_inet_invoke = @' 67 using Microsoft.Win32.SafeHandles; 68 using System; 69 using System.Collections.Generic; 70 using System.Runtime.ConstrainedExecution; 71 using System.Runtime.InteropServices; 72 73 namespace Ansible.WinINetProxy 74 { 75 internal class NativeHelpers 76 { 77 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 78 public class INTERNET_PER_CONN_OPTION_LISTW : IDisposable 79 { 80 public UInt32 dwSize; 81 public IntPtr pszConnection; 82 public UInt32 dwOptionCount; 83 public UInt32 dwOptionError; 84 public IntPtr pOptions; 85 86 public INTERNET_PER_CONN_OPTION_LISTW() 87 { 88 dwSize = (UInt32)Marshal.SizeOf(this); 89 } 90 91 public void Dispose() 92 { 93 if (pszConnection != IntPtr.Zero) 94 Marshal.FreeHGlobal(pszConnection); 95 if (pOptions != IntPtr.Zero) 96 Marshal.FreeHGlobal(pOptions); 97 GC.SuppressFinalize(this); 98 } 99 ~INTERNET_PER_CONN_OPTION_LISTW() { this.Dispose(); } 100 } 101 102 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 103 public class INTERNET_PER_CONN_OPTIONW : IDisposable 104 { 105 public INTERNET_PER_CONN_OPTION dwOption; 106 public ValueUnion Value; 107 108 [StructLayout(LayoutKind.Explicit)] 109 public class ValueUnion 110 { 111 [FieldOffset(0)] 112 public UInt32 dwValue; 113 114 [FieldOffset(0)] 115 public IntPtr pszValue; 116 117 [FieldOffset(0)] 118 public System.Runtime.InteropServices.ComTypes.FILETIME ftValue; 119 } 120 121 public void Dispose() 122 { 123 // We can't just check if Value.pszValue is not IntPtr.Zero as the union means it could be set even 124 // when the value is a UInt32 or FILETIME. We check against a known string option type and only free 125 // the value in those cases. 126 List<INTERNET_PER_CONN_OPTION> stringOptions = new List<INTERNET_PER_CONN_OPTION> 127 { 128 { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_AUTOCONFIG_URL }, 129 { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_BYPASS }, 130 { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_SERVER } 131 }; 132 if (Value != null && Value.pszValue != IntPtr.Zero && stringOptions.Contains(dwOption)) 133 Marshal.FreeHGlobal(Value.pszValue); 134 GC.SuppressFinalize(this); 135 } 136 ~INTERNET_PER_CONN_OPTIONW() { this.Dispose(); } 137 } 138 139 public enum INTERNET_OPTION : uint 140 { 141 INTERNET_OPTION_PER_CONNECTION_OPTION = 75, 142 INTERNET_OPTION_PROXY_SETTINGS_CHANGED = 95, 143 } 144 145 public enum INTERNET_PER_CONN_OPTION : uint 146 { 147 INTERNET_PER_CONN_FLAGS = 1, 148 INTERNET_PER_CONN_PROXY_SERVER = 2, 149 INTERNET_PER_CONN_PROXY_BYPASS = 3, 150 INTERNET_PER_CONN_AUTOCONFIG_URL = 4, 151 INTERNET_PER_CONN_AUTODISCOVERY_FLAGS = 5, 152 INTERNET_PER_CONN_FLAGS_UI = 10, // IE8+ - Included with Windows 7 and Server 2008 R2 153 } 154 155 [Flags] 156 public enum PER_CONN_FLAGS : uint 157 { 158 PROXY_TYPE_DIRECT = 0x00000001, 159 PROXY_TYPE_PROXY = 0x00000002, 160 PROXY_TYPE_AUTO_PROXY_URL = 0x00000004, 161 PROXY_TYPE_AUTO_DETECT = 0x00000008, 162 } 163 } 164 165 internal class NativeMethods 166 { 167 [DllImport("Wininet.dll", SetLastError = true, CharSet = CharSet.Unicode)] 168 public static extern bool InternetQueryOptionW( 169 IntPtr hInternet, 170 NativeHelpers.INTERNET_OPTION dwOption, 171 SafeMemoryBuffer lpBuffer, 172 ref UInt32 lpdwBufferLength); 173 174 [DllImport("Wininet.dll", SetLastError = true, CharSet = CharSet.Unicode)] 175 public static extern bool InternetSetOptionW( 176 IntPtr hInternet, 177 NativeHelpers.INTERNET_OPTION dwOption, 178 SafeMemoryBuffer lpBuffer, 179 UInt32 dwBufferLength); 180 181 [DllImport("Rasapi32.dll", CharSet = CharSet.Unicode)] 182 public static extern UInt32 RasValidateEntryNameW( 183 string lpszPhonebook, 184 string lpszEntry); 185 } 186 187 internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid 188 { 189 public SafeMemoryBuffer() : base(true) { } 190 public SafeMemoryBuffer(int cb) : base(true) 191 { 192 base.SetHandle(Marshal.AllocHGlobal(cb)); 193 } 194 public SafeMemoryBuffer(IntPtr handle) : base(true) 195 { 196 base.SetHandle(handle); 197 } 198 199 [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] 200 protected override bool ReleaseHandle() 201 { 202 Marshal.FreeHGlobal(handle); 203 return true; 204 } 205 } 206 207 public class Win32Exception : System.ComponentModel.Win32Exception 208 { 209 private string _msg; 210 211 public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } 212 public Win32Exception(int errorCode, string message) : base(errorCode) 213 { 214 _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode); 215 } 216 217 public override string Message { get { return _msg; } } 218 public static explicit operator Win32Exception(string message) { return new Win32Exception(message); } 219 } 220 221 public class WinINetProxy 222 { 223 private string Connection; 224 225 public string AutoConfigUrl; 226 public bool AutoDetect; 227 public string Proxy; 228 public string ProxyBypass; 229 230 public WinINetProxy(string connection) 231 { 232 Connection = connection; 233 Refresh(); 234 } 235 236 public static bool IsValidConnection(string name) 237 { 238 // RasValidateEntryName is used to verify is a name can be a valid phonebook entry. It returns 0 if no 239 // entry exists and 183 if it already exists. We just need to check if it returns 183 to verify the 240 // connection name. 241 return NativeMethods.RasValidateEntryNameW(null, name) == 183; 242 } 243 244 public void Refresh() 245 { 246 using (var connFlags = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_FLAGS_UI)) 247 using (var autoConfigUrl = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_AUTOCONFIG_URL)) 248 using (var server = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_SERVER)) 249 using (var bypass = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_BYPASS)) 250 { 251 NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options = new NativeHelpers.INTERNET_PER_CONN_OPTIONW[] 252 { 253 connFlags, autoConfigUrl, server, bypass 254 }; 255 256 try 257 { 258 QueryOption(options, Connection); 259 } 260 catch (Win32Exception e) 261 { 262 if (e.NativeErrorCode == 87) // ERROR_INVALID_PARAMETER 263 { 264 // INTERNET_PER_CONN_FLAGS_UI only works for IE8+, try the fallback in case we are still working 265 // with an ancient version. 266 connFlags.dwOption = NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_FLAGS; 267 QueryOption(options, Connection); 268 } 269 else 270 throw; 271 } 272 273 NativeHelpers.PER_CONN_FLAGS flags = (NativeHelpers.PER_CONN_FLAGS)connFlags.Value.dwValue; 274 275 AutoConfigUrl = flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_PROXY_URL) 276 ? Marshal.PtrToStringUni(autoConfigUrl.Value.pszValue) : null; 277 AutoDetect = flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_DETECT); 278 if (flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_PROXY)) 279 { 280 Proxy = Marshal.PtrToStringUni(server.Value.pszValue); 281 ProxyBypass = Marshal.PtrToStringUni(bypass.Value.pszValue); 282 } 283 else 284 { 285 Proxy = null; 286 ProxyBypass = null; 287 } 288 } 289 } 290 291 public void Set() 292 { 293 using (var connFlags = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_FLAGS_UI)) 294 using (var autoConfigUrl = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_AUTOCONFIG_URL)) 295 using (var server = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_SERVER)) 296 using (var bypass = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_BYPASS)) 297 { 298 List<NativeHelpers.INTERNET_PER_CONN_OPTIONW> options = new List<NativeHelpers.INTERNET_PER_CONN_OPTIONW>(); 299 300 // PROXY_TYPE_DIRECT seems to always be set, need to verify 301 NativeHelpers.PER_CONN_FLAGS flags = NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_DIRECT; 302 if (AutoDetect) 303 flags |= NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_DETECT; 304 305 if (!String.IsNullOrEmpty(AutoConfigUrl)) 306 { 307 flags |= NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_PROXY_URL; 308 autoConfigUrl.Value.pszValue = Marshal.StringToHGlobalUni(AutoConfigUrl); 309 } 310 options.Add(autoConfigUrl); 311 312 if (!String.IsNullOrEmpty(Proxy)) 313 { 314 flags |= NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_PROXY; 315 server.Value.pszValue = Marshal.StringToHGlobalUni(Proxy); 316 } 317 options.Add(server); 318 319 if (!String.IsNullOrEmpty(ProxyBypass)) 320 bypass.Value.pszValue = Marshal.StringToHGlobalUni(ProxyBypass); 321 options.Add(bypass); 322 323 connFlags.Value.dwValue = (UInt32)flags; 324 options.Add(connFlags); 325 326 SetOption(options.ToArray(), Connection); 327 328 // Tell IE that the proxy settings have been changed. 329 if (!NativeMethods.InternetSetOptionW( 330 IntPtr.Zero, 331 NativeHelpers.INTERNET_OPTION.INTERNET_OPTION_PROXY_SETTINGS_CHANGED, 332 new SafeMemoryBuffer(IntPtr.Zero), 333 0)) 334 { 335 throw new Win32Exception("InternetSetOptionW(INTERNET_OPTION_PROXY_SETTINGS_CHANGED) failed"); 336 } 337 } 338 } 339 340 internal static NativeHelpers.INTERNET_PER_CONN_OPTIONW CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION option) 341 { 342 return new NativeHelpers.INTERNET_PER_CONN_OPTIONW 343 { 344 dwOption = option, 345 Value = new NativeHelpers.INTERNET_PER_CONN_OPTIONW.ValueUnion(), 346 }; 347 } 348 349 internal static void QueryOption(NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options, string connection = null) 350 { 351 using (NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW optionList = new NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW()) 352 using (SafeMemoryBuffer optionListPtr = MarshalOptionList(optionList, options, connection)) 353 { 354 UInt32 bufferSize = optionList.dwSize; 355 if (!NativeMethods.InternetQueryOptionW( 356 IntPtr.Zero, 357 NativeHelpers.INTERNET_OPTION.INTERNET_OPTION_PER_CONNECTION_OPTION, 358 optionListPtr, 359 ref bufferSize)) 360 { 361 throw new Win32Exception("InternetQueryOptionW(INTERNET_OPTION_PER_CONNECTION_OPTION) failed"); 362 } 363 364 for (int i = 0; i < options.Length; i++) 365 { 366 IntPtr opt = IntPtr.Add(optionList.pOptions, i * Marshal.SizeOf(typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW))); 367 NativeHelpers.INTERNET_PER_CONN_OPTIONW option = (NativeHelpers.INTERNET_PER_CONN_OPTIONW)Marshal.PtrToStructure(opt, 368 typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW)); 369 options[i].Value = option.Value; 370 option.Value = null; // Stops the GC from freeing the same memory twice 371 } 372 } 373 } 374 375 internal static void SetOption(NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options, string connection = null) 376 { 377 using (NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW optionList = new NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW()) 378 using (SafeMemoryBuffer optionListPtr = MarshalOptionList(optionList, options, connection)) 379 { 380 if (!NativeMethods.InternetSetOptionW( 381 IntPtr.Zero, 382 NativeHelpers.INTERNET_OPTION.INTERNET_OPTION_PER_CONNECTION_OPTION, 383 optionListPtr, 384 optionList.dwSize)) 385 { 386 throw new Win32Exception("InternetSetOptionW(INTERNET_OPTION_PER_CONNECTION_OPTION) failed"); 387 } 388 } 389 } 390 391 internal static SafeMemoryBuffer MarshalOptionList(NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW optionList, 392 NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options, string connection) 393 { 394 optionList.pszConnection = Marshal.StringToHGlobalUni(connection); 395 optionList.dwOptionCount = (UInt32)options.Length; 396 397 int optionSize = Marshal.SizeOf(typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW)); 398 optionList.pOptions = Marshal.AllocHGlobal(optionSize * options.Length); 399 for (int i = 0; i < options.Length; i++) 400 { 401 IntPtr option = IntPtr.Add(optionList.pOptions, i * optionSize); 402 Marshal.StructureToPtr(options[i], option, false); 403 } 404 405 SafeMemoryBuffer optionListPtr = new SafeMemoryBuffer((int)optionList.dwSize); 406 Marshal.StructureToPtr(optionList, optionListPtr.DangerousGetHandle(), false); 407 return optionListPtr; 408 } 409 } 410 } 411 '@ 412 Add-CSharpType -References $win_inet_invoke -AnsibleModule $module 413 414 # We need to validate the connection because WinINet will just silently continue even if the connection does not 415 # already exist. 416 if ($null -ne $connection -and -not [Ansible.WinINetProxy.WinINetProxy]::IsValidConnection($connection)) { 417 $module.FailJson("The connection '$connection' does not exist.") 418 } 419 420 $actual_proxy = New-Object -TypeName Ansible.WinINetProxy.WinINetProxy -ArgumentList @(,$connection) 421 $module.Diff.before = @{ 422 auto_config_url = $actual_proxy.AutoConfigUrl 423 auto_detect = $actual_proxy.AutoDetect 424 bypass = $actual_proxy.ProxyBypass 425 server = $actual_proxy.Proxy 426 } 427 428 # Make sure an empty string is converted to $null for easier comparisons 429 if ([String]::IsNullOrEmpty($auto_config_url)) { 430 $auto_config_url = $null 431 } 432 if ([String]::IsNullOrEmpty($proxy)) { 433 $proxy = $null 434 } 435 if ([String]::IsNullOrEmpty($bypass)) { 436 $bypass = $null 437 } 438 439 # Record the original values in case we need to revert on a failure 440 $previous_auto_config_url = $actual_proxy.AutoConfigUrl 441 $previous_auto_detect = $actual_proxy.AutoDetect 442 $previous_proxy = $actual_proxy.Proxy 443 $previous_bypass = $actual_proxy.ProxyBypass 444 445 $changed = $false 446 if ($auto_config_url -ne $previous_auto_config_url) { 447 $actual_proxy.AutoConfigUrl = $auto_config_url 448 $changed = $true 449 } 450 451 if ($auto_detect -ne $previous_auto_detect) { 452 $actual_proxy.AutoDetect = $auto_detect 453 $changed = $true 454 } 455 456 if ($proxy -ne $previous_proxy) { 457 $actual_proxy.Proxy = $proxy 458 $changed = $true 459 } 460 461 if ($bypass -ne $previous_bypass) { 462 $actual_proxy.ProxyBypass = $bypass 463 $changed = $true 464 } 465 466 if ($changed -and -not $module.CheckMode) { 467 $actual_proxy.Set() 468 469 # Validate that the change was made correctly and revert if it wasn't. THe Set() method won't fail on invalid 470 # values so we need to check again to make sure all was good 471 $actual_proxy.Refresh() 472 if ($actual_proxy.AutoConfigUrl -ne $auto_config_url -or 473 $actual_proxy.AutoDetect -ne $auto_detect -or 474 $actual_proxy.Proxy -ne $proxy -or 475 $actual_proxy.ProxyBypass -ne $bypass) { 476 477 $actual_proxy.AutoConfigUrl = $previous_auto_config_url 478 $actual_proxy.AutoDetect = $previous_auto_detect 479 $actual_proxy.Proxy = $previous_proxy 480 $actual_proxy.ProxyBypass = $previous_bypass 481 $actual_proxy.Set() 482 483 $module.FailJson("Unknown error when trying to set auto_config_url '$auto_config_url', proxy '$proxy', or bypass '$bypass'") 484 } 485 } 486 $module.Result.changed = $changed 487 488 $module.Diff.after = @{ 489 auto_config_url = $auto_config_url 490 auto_detect = $auto_detect 491 bypass = $bypass 492 proxy = $proxy 493 } 494 495 $module.ExitJson() 496