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