From 10a9cf59ddc5304531105b1d0d73d53d653d0725 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Fri, 5 Apr 2019 11:19:30 +1000 Subject: [PATCH] Added win_http_proxy and win_inet_proxy (#54631) * Added win_http_proxy and win_inet_proxy * Fix up docs sanity issues * removed duplicate doc entry * Fix docs issues and fix for empty proxy * Removed <-loopback> for win_http_proxy * doc changes from review --- .../modules/windows/win_http_proxy.ps1 | 267 +++++++++ lib/ansible/modules/windows/win_http_proxy.py | 103 ++++ .../modules/windows/win_inet_proxy.ps1 | 495 +++++++++++++++++ lib/ansible/modules/windows/win_inet_proxy.py | 173 ++++++ .../targets/win_http_proxy/aliases | 1 + .../targets/win_http_proxy/tasks/main.yml | 14 + .../targets/win_http_proxy/tasks/tests.yml | 265 +++++++++ .../targets/win_inet_proxy/aliases | 1 + .../library/win_inet_proxy_info.ps1 | 275 +++++++++ .../library/win_phonebook_entry.ps1 | 521 ++++++++++++++++++ .../targets/win_inet_proxy/tasks/main.yml | 16 + .../targets/win_inet_proxy/tasks/tests.yml | 308 +++++++++++ 12 files changed, 2439 insertions(+) create mode 100644 lib/ansible/modules/windows/win_http_proxy.ps1 create mode 100644 lib/ansible/modules/windows/win_http_proxy.py create mode 100644 lib/ansible/modules/windows/win_inet_proxy.ps1 create mode 100644 lib/ansible/modules/windows/win_inet_proxy.py create mode 100644 test/integration/targets/win_http_proxy/aliases create mode 100644 test/integration/targets/win_http_proxy/tasks/main.yml create mode 100644 test/integration/targets/win_http_proxy/tasks/tests.yml create mode 100644 test/integration/targets/win_inet_proxy/aliases create mode 100644 test/integration/targets/win_inet_proxy/library/win_inet_proxy_info.ps1 create mode 100644 test/integration/targets/win_inet_proxy/library/win_phonebook_entry.ps1 create mode 100644 test/integration/targets/win_inet_proxy/tasks/main.yml create mode 100644 test/integration/targets/win_inet_proxy/tasks/tests.yml diff --git a/lib/ansible/modules/windows/win_http_proxy.ps1 b/lib/ansible/modules/windows/win_http_proxy.ps1 new file mode 100644 index 00000000000..04ea473b998 --- /dev/null +++ b/lib/ansible/modules/windows/win_http_proxy.ps1 @@ -0,0 +1,267 @@ +#!powershell + +# Copyright: (c) 2019, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.AddType + +$spec = @{ + options = @{ + bypass = @{ type = "list" } + proxy = @{ type = "raw" } + source = @{ type = "str"; choices = @("ie") } + } + mutually_exclusive = @( + @("proxy", "source"), + @("bypass", "source") + ) + required_by = @{ + bypass = @("proxy") + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$proxy = $module.Params.proxy +$bypass = $module.Params.bypass +$source = $module.Params.source + +# Parse the raw value, it should be a Dictionary or String +if ($proxy -is [System.Collections.IDictionary]) { + $valid_keys = [System.Collections.Generic.List`1[String]]@("http", "https", "ftp", "socks") + # Check to make sure we don't have any invalid keys in the dict + $invalid_keys = [System.Collections.Generic.List`1[String]]@() + foreach ($k in $proxy.Keys) { + if ($k -notin $valid_keys) { + $invalid_keys.Add($k) + } + } + + if ($invalid_keys.Count -gt 0) { + $invalid_keys = $invalid_keys | Sort-Object # So our test assertion doesn't fail due to random ordering + $module.FailJson("Invalid keys found in proxy: $($invalid_keys -join ', '). Valid keys are $($valid_keys -join ', ').") + } + + # Build the proxy string in the form 'protocol=host;', the order of valid_keys is also important + $proxy_list = [System.Collections.Generic.List`1[String]]@() + foreach ($k in $valid_keys) { + if ($proxy.ContainsKey($k)) { + $proxy_list.Add("$k=$($proxy.$k)") + } + } + $proxy = $proxy_list -join ";" +} elseif ($null -ne $proxy) { + $proxy = $proxy.ToString() +} + +if ($bypass) { + if ([System.String]::IsNullOrEmpty($proxy)) { + $module.FailJson("missing parameter(s) required by ''bypass'': proxy") + } + $bypass = $bypass -join ';' +} + +$win_http_invoke = @' +using System; +using System.Runtime.InteropServices; + +namespace Ansible.WinHttpProxy +{ + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public class WINHTTP_CURRENT_USER_IE_PROXY_CONFIG : IDisposable + { + public bool fAutoDetect; + public IntPtr lpszAutoConfigUrl; + public IntPtr lpszProxy; + public IntPtr lpszProxyBypass; + + public void Dispose() + { + if (lpszAutoConfigUrl != IntPtr.Zero) + Marshal.FreeHGlobal(lpszAutoConfigUrl); + if (lpszProxy != IntPtr.Zero) + Marshal.FreeHGlobal(lpszProxy); + if (lpszProxyBypass != IntPtr.Zero) + Marshal.FreeHGlobal(lpszProxyBypass); + GC.SuppressFinalize(this); + } + ~WINHTTP_CURRENT_USER_IE_PROXY_CONFIG() { this.Dispose(); } + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public class WINHTTP_PROXY_INFO : IDisposable + { + public UInt32 dwAccessType; + public IntPtr lpszProxy; + public IntPtr lpszProxyBypass; + + public void Dispose() + { + if (lpszProxy != IntPtr.Zero) + Marshal.FreeHGlobal(lpszProxy); + if (lpszProxyBypass != IntPtr.Zero) + Marshal.FreeHGlobal(lpszProxyBypass); + GC.SuppressFinalize(this); + } + ~WINHTTP_PROXY_INFO() { this.Dispose(); } + } + } + + internal class NativeMethods + { + [DllImport("Winhttp.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool WinHttpGetDefaultProxyConfiguration( + [Out] NativeHelpers.WINHTTP_PROXY_INFO pProxyInfo); + + [DllImport("Winhttp.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool WinHttpGetIEProxyConfigForCurrentUser( + [Out] NativeHelpers.WINHTTP_CURRENT_USER_IE_PROXY_CONFIG pProxyConfig); + + [DllImport("Winhttp.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool WinHttpSetDefaultProxyConfiguration( + NativeHelpers.WINHTTP_PROXY_INFO pProxyInfo); + } + + public class Win32Exception : System.ComponentModel.Win32Exception + { + private string _msg; + + public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } + public Win32Exception(int errorCode, string message) : base(errorCode) + { + _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode); + } + + public override string Message { get { return _msg; } } + public static explicit operator Win32Exception(string message) { return new Win32Exception(message); } + } + + public class WinINetProxy + { + public bool AutoDetect; + public string AutoConfigUrl; + public string Proxy; + public string ProxyBypass; + } + + public class WinHttpProxy + { + public string Proxy; + public string ProxyBypass; + + public WinHttpProxy() + { + Refresh(); + } + + public void Set() + { + using (NativeHelpers.WINHTTP_PROXY_INFO proxyInfo = new NativeHelpers.WINHTTP_PROXY_INFO()) + { + if (String.IsNullOrEmpty(Proxy)) + proxyInfo.dwAccessType = 1; // WINHTTP_ACCESS_TYPE_NO_PROXY + else + { + proxyInfo.dwAccessType = 3; // WINHTTP_ACCESS_TYPE_NAMED_PROXY + proxyInfo.lpszProxy = Marshal.StringToHGlobalUni(Proxy); + + if (!String.IsNullOrEmpty(ProxyBypass)) + proxyInfo.lpszProxyBypass = Marshal.StringToHGlobalUni(ProxyBypass); + } + + if (!NativeMethods.WinHttpSetDefaultProxyConfiguration(proxyInfo)) + throw new Win32Exception("WinHttpSetDefaultProxyConfiguration() failed"); + } + } + + public void Refresh() + { + using (NativeHelpers.WINHTTP_PROXY_INFO proxyInfo = new NativeHelpers.WINHTTP_PROXY_INFO()) + { + if (!NativeMethods.WinHttpGetDefaultProxyConfiguration(proxyInfo)) + throw new Win32Exception("WinHttpGetDefaultProxyConfiguration() failed"); + + Proxy = Marshal.PtrToStringUni(proxyInfo.lpszProxy); + ProxyBypass = Marshal.PtrToStringUni(proxyInfo.lpszProxyBypass); + } + } + + public static WinINetProxy GetIEProxyConfig() + { + using (NativeHelpers.WINHTTP_CURRENT_USER_IE_PROXY_CONFIG ieProxy = new NativeHelpers.WINHTTP_CURRENT_USER_IE_PROXY_CONFIG()) + { + if (!NativeMethods.WinHttpGetIEProxyConfigForCurrentUser(ieProxy)) + throw new Win32Exception("WinHttpGetIEProxyConfigForCurrentUser() failed"); + + return new WinINetProxy + { + AutoDetect = ieProxy.fAutoDetect, + AutoConfigUrl = Marshal.PtrToStringUni(ieProxy.lpszAutoConfigUrl), + Proxy = Marshal.PtrToStringUni(ieProxy.lpszProxy), + ProxyBypass = Marshal.PtrToStringUni(ieProxy.lpszProxyBypass), + }; + } + } + } +} +'@ +Add-CSharpType -References $win_http_invoke -AnsibleModule $module + +$actual_proxy = New-Object -TypeName Ansible.WinHttpProxy.WinHttpProxy + +$module.Diff.before = @{ + proxy = $actual_proxy.Proxy + bypass = $actual_proxy.ProxyBypass +} + +if ($source -eq "ie") { + # If source=ie we need to get the server and bypass values from the IE configuration + $ie_proxy = [Ansible.WinHttpProxy.WinHttpProxy]::GetIEProxyConfig() + $proxy = $ie_proxy.Proxy + $bypass = $ie_proxy.ProxyBypass +} + +$previous_proxy = $actual_proxy.Proxy +$previous_bypass = $actual_proxy.ProxyBypass + +# Make sure an empty string is converted to $null for easier comparisons +if ([String]::IsNullOrEmpty($proxy)) { + $proxy = $null +} +if ([String]::IsNullOrEmpty($bypass)) { + $bypass = $null +} + +if ($previous_proxy -ne $proxy -or $previous_bypass -ne $bypass) { + $actual_proxy.Proxy = $proxy + $actual_proxy.ProxyBypass = $bypass + + if (-not $module.CheckMode) { + $actual_proxy.Set() + + # Validate that the change was made correctly and revert if it wasn't. The Set() method won't fail on invalid + # values so we need to check again to make sure all was good. + $actual_proxy.Refresh() + if ($actual_proxy.Proxy -ne $proxy -or $actual_proxy.ProxyBypass -ne $bypass) { + $actual_proxy.Proxy = $previous_proxy + $actual_proxy.ProxyBypass = $previous_bypass + $actual_proxy.Set() + + $module.FailJson("Unknown error when trying to set proxy '$proxy' or bypass '$bypass'") + } + } + + $module.Result.changed = $true +} + +$module.Diff.after = @{ + proxy = $proxy + bypass = $bypass +} + +$module.ExitJson() + diff --git a/lib/ansible/modules/windows/win_http_proxy.py b/lib/ansible/modules/windows/win_http_proxy.py new file mode 100644 index 00000000000..d9abb858e25 --- /dev/null +++ b/lib/ansible/modules/windows/win_http_proxy.py @@ -0,0 +1,103 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: win_http_proxy +version_added: '2.8' +short_description: Manages proxy settings for WinHTTP +description: +- Used to set, remove, or import proxy settings for Windows HTTP Services + C(WinHTTP). +- WinHTTP is a framework used by applications or services, typically .NET + applications or non-interactive services, to make web requests. +options: + bypass: + description: + - A list of hosts that will bypass the set proxy when being accessed. + - Use C() to match hostnames that are not fully qualified domain + names. This is useful when needing to connect to intranet sites using + just the hostname. + - Omit, set to null or an empty string/list to remove the bypass list. + - If this is set then I(proxy) must also be set. + type: list + proxy: + description: + - A string or dict that specifies the proxy to be set. + - If setting a string, should be in the form C(hostname), C(hostname:port), + or C(protocol=hostname:port). + - If the port is undefined, the default port for the protocol in use is + used. + - If setting a dict, the keys should be the protocol and the values should + be the hostname and/or port for that protocol. + - Valid protocols are C(http), C(https), C(ftp), and C(socks). + - Omit, set to null or an empty string to remove the proxy settings. + source: + description: + - Instead of manually specifying the I(proxy) and/or I(bypass), set this to + import the proxy from a set source like Internet Explorer. + - Using C(ie) will import the Internet Explorer proxy settings for the + current active network connection of the current user. + - Only IE's proxy URL and bypass list will be imported into WinHTTP. + - This is like running C(netsh winhttp import proxy source=ie). + - The value is imported when the module runs and will not automatically + be updated if the IE configuration changes in the future. The module will + have to be run again to sync the latest changes. + choices: + - ie + type: str +notes: +- This is not the same as the proxy settings set in Internet Explorer, also + known as C(WinINet); use the M(win_inet_proxy) module to manage that instead. +- These settings are set system wide and not per user, it will require + Administrative privileges to run. +seealso: +- module: win_inet_proxy +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: Set a proxy to use for all protocols + win_http_proxy: + proxy: hostname + +- name: Set a proxy with a specific port with a bypass list + win_http_proxy: + proxy: hostname:8080 + bypass: + - server1 + - server2 + - + +- name: Set the proxy based on the IE proxy settings + win_http_proxy: + source: ie + +- name: Set a proxy for specific protocols + win_http_proxy: + proxy: + http: hostname:8080 + https: hostname:8443 + +- name: Set a proxy for specific protocols using a string + win_http_proxy: + proxy: http=hostname:8080;https=hostname:8443 + bypass: server1,server2, + +- name: Remove any proxy settings + win_http_proxy: + proxy: '' + bypass: '' +''' + +RETURN = r''' +# +''' diff --git a/lib/ansible/modules/windows/win_inet_proxy.ps1 b/lib/ansible/modules/windows/win_inet_proxy.ps1 new file mode 100644 index 00000000000..3b0420f6486 --- /dev/null +++ b/lib/ansible/modules/windows/win_inet_proxy.ps1 @@ -0,0 +1,495 @@ +#!powershell + +# Copyright: (c) 2019, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.AddType + +$spec = @{ + options = @{ + auto_detect = @{ type = "bool"; default = $true } + auto_config_url = @{ type = "str" } + proxy = @{ type = "raw" } + bypass = @{ type = "list" } + connection = @{ type = "str" } + } + required_by = @{ + bypass = @("proxy") + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$auto_detect = $module.Params.auto_detect +$auto_config_url = $module.Params.auto_config_url +$proxy = $module.Params.proxy +$bypass = $module.Params.bypass +$connection = $module.Params.connection + +# Parse the raw value, it should be a Dictionary or String +if ($proxy -is [System.Collections.IDictionary]) { + $valid_keys = [System.Collections.Generic.List`1[String]]@("http", "https", "ftp", "socks") + # Check to make sure we don't have any invalid keys in the dict + $invalid_keys = [System.Collections.Generic.List`1[String]]@() + foreach ($k in $proxy.Keys) { + if ($k -notin $valid_keys) { + $invalid_keys.Add($k) + } + } + + if ($invalid_keys.Count -gt 0) { + $invalid_keys = $invalid_keys | Sort-Object # So our test assertion doesn't fail due to random ordering + $module.FailJson("Invalid keys found in proxy: $($invalid_keys -join ', '). Valid keys are $($valid_keys -join ', ').") + } + + # Build the proxy string in the form 'protocol=host;', the order of valid_keys is also important + $proxy_list = [System.Collections.Generic.List`1[String]]@() + foreach ($k in $valid_keys) { + if ($proxy.ContainsKey($k)) { + $proxy_list.Add("$k=$($proxy.$k)") + } + } + $proxy = $proxy_list -join ";" +} elseif ($null -ne $proxy) { + $proxy = $proxy.ToString() +} + +if ($bypass) { + if ([System.String]::IsNullOrEmpty($proxy)) { + $module.FailJson("missing parameter(s) required by ''bypass'': proxy") + } + $bypass = $bypass -join ';' +} + +$win_inet_invoke = @' +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; + +namespace Ansible.WinINetProxy +{ + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public class INTERNET_PER_CONN_OPTION_LISTW : IDisposable + { + public UInt32 dwSize; + public IntPtr pszConnection; + public UInt32 dwOptionCount; + public UInt32 dwOptionError; + public IntPtr pOptions; + + public INTERNET_PER_CONN_OPTION_LISTW() + { + dwSize = (UInt32)Marshal.SizeOf(this); + } + + public void Dispose() + { + if (pszConnection != IntPtr.Zero) + Marshal.FreeHGlobal(pszConnection); + if (pOptions != IntPtr.Zero) + Marshal.FreeHGlobal(pOptions); + GC.SuppressFinalize(this); + } + ~INTERNET_PER_CONN_OPTION_LISTW() { this.Dispose(); } + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public class INTERNET_PER_CONN_OPTIONW : IDisposable + { + public INTERNET_PER_CONN_OPTION dwOption; + public ValueUnion Value; + + [StructLayout(LayoutKind.Explicit)] + public class ValueUnion + { + [FieldOffset(0)] + public UInt32 dwValue; + + [FieldOffset(0)] + public IntPtr pszValue; + + [FieldOffset(0)] + public System.Runtime.InteropServices.ComTypes.FILETIME ftValue; + } + + public void Dispose() + { + // We can't just check if Value.pszValue is not IntPtr.Zero as the union means it could be set even + // when the value is a UInt32 or FILETIME. We check against a known string option type and only free + // the value in those cases. + List stringOptions = new List + { + { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_AUTOCONFIG_URL }, + { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_BYPASS }, + { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_SERVER } + }; + if (Value != null && Value.pszValue != IntPtr.Zero && stringOptions.Contains(dwOption)) + Marshal.FreeHGlobal(Value.pszValue); + GC.SuppressFinalize(this); + } + ~INTERNET_PER_CONN_OPTIONW() { this.Dispose(); } + } + + public enum INTERNET_OPTION : uint + { + INTERNET_OPTION_PER_CONNECTION_OPTION = 75, + INTERNET_OPTION_PROXY_SETTINGS_CHANGED = 95, + } + + public enum INTERNET_PER_CONN_OPTION : uint + { + INTERNET_PER_CONN_FLAGS = 1, + INTERNET_PER_CONN_PROXY_SERVER = 2, + INTERNET_PER_CONN_PROXY_BYPASS = 3, + INTERNET_PER_CONN_AUTOCONFIG_URL = 4, + INTERNET_PER_CONN_AUTODISCOVERY_FLAGS = 5, + INTERNET_PER_CONN_FLAGS_UI = 10, // IE8+ - Included with Windows 7 and Server 2008 R2 + } + + [Flags] + public enum PER_CONN_FLAGS : uint + { + PROXY_TYPE_DIRECT = 0x00000001, + PROXY_TYPE_PROXY = 0x00000002, + PROXY_TYPE_AUTO_PROXY_URL = 0x00000004, + PROXY_TYPE_AUTO_DETECT = 0x00000008, + } + } + + internal class NativeMethods + { + [DllImport("Wininet.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool InternetQueryOptionW( + IntPtr hInternet, + NativeHelpers.INTERNET_OPTION dwOption, + SafeMemoryBuffer lpBuffer, + ref UInt32 lpdwBufferLength); + + [DllImport("Wininet.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool InternetSetOptionW( + IntPtr hInternet, + NativeHelpers.INTERNET_OPTION dwOption, + SafeMemoryBuffer lpBuffer, + UInt32 dwBufferLength); + + [DllImport("Rasapi32.dll", CharSet = CharSet.Unicode)] + public static extern UInt32 RasValidateEntryNameW( + string lpszPhonebook, + string lpszEntry); + } + + internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeMemoryBuffer() : base(true) { } + public SafeMemoryBuffer(int cb) : base(true) + { + base.SetHandle(Marshal.AllocHGlobal(cb)); + } + public SafeMemoryBuffer(IntPtr handle) : base(true) + { + base.SetHandle(handle); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + Marshal.FreeHGlobal(handle); + return true; + } + } + + public class Win32Exception : System.ComponentModel.Win32Exception + { + private string _msg; + + public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } + public Win32Exception(int errorCode, string message) : base(errorCode) + { + _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode); + } + + public override string Message { get { return _msg; } } + public static explicit operator Win32Exception(string message) { return new Win32Exception(message); } + } + + public class WinINetProxy + { + private string Connection; + + public string AutoConfigUrl; + public bool AutoDetect; + public string Proxy; + public string ProxyBypass; + + public WinINetProxy(string connection) + { + Connection = connection; + Refresh(); + } + + public static bool IsValidConnection(string name) + { + // RasValidateEntryName is used to verify is a name can be a valid phonebook entry. It returns 0 if no + // entry exists and 183 if it already exists. We just need to check if it returns 183 to verify the + // connection name. + return NativeMethods.RasValidateEntryNameW(null, name) == 183; + } + + public void Refresh() + { + using (var connFlags = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_FLAGS_UI)) + using (var autoConfigUrl = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_AUTOCONFIG_URL)) + using (var server = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_SERVER)) + using (var bypass = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_BYPASS)) + { + NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options = new NativeHelpers.INTERNET_PER_CONN_OPTIONW[] + { + connFlags, autoConfigUrl, server, bypass + }; + + try + { + QueryOption(options, Connection); + } + catch (Win32Exception e) + { + if (e.NativeErrorCode == 87) // ERROR_INVALID_PARAMETER + { + // INTERNET_PER_CONN_FLAGS_UI only works for IE8+, try the fallback in case we are still working + // with an ancient version. + connFlags.dwOption = NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_FLAGS; + QueryOption(options, Connection); + } + else + throw; + } + + NativeHelpers.PER_CONN_FLAGS flags = (NativeHelpers.PER_CONN_FLAGS)connFlags.Value.dwValue; + + AutoConfigUrl = flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_PROXY_URL) + ? Marshal.PtrToStringUni(autoConfigUrl.Value.pszValue) : null; + AutoDetect = flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_DETECT); + if (flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_PROXY)) + { + Proxy = Marshal.PtrToStringUni(server.Value.pszValue); + ProxyBypass = Marshal.PtrToStringUni(bypass.Value.pszValue); + } + else + { + Proxy = null; + ProxyBypass = null; + } + } + } + + public void Set() + { + using (var connFlags = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_FLAGS_UI)) + using (var autoConfigUrl = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_AUTOCONFIG_URL)) + using (var server = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_SERVER)) + using (var bypass = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_BYPASS)) + { + List options = new List(); + + // PROXY_TYPE_DIRECT seems to always be set, need to verify + NativeHelpers.PER_CONN_FLAGS flags = NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_DIRECT; + if (AutoDetect) + flags |= NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_DETECT; + + if (!String.IsNullOrEmpty(AutoConfigUrl)) + { + flags |= NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_PROXY_URL; + autoConfigUrl.Value.pszValue = Marshal.StringToHGlobalUni(AutoConfigUrl); + } + options.Add(autoConfigUrl); + + if (!String.IsNullOrEmpty(Proxy)) + { + flags |= NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_PROXY; + server.Value.pszValue = Marshal.StringToHGlobalUni(Proxy); + } + options.Add(server); + + if (!String.IsNullOrEmpty(ProxyBypass)) + bypass.Value.pszValue = Marshal.StringToHGlobalUni(ProxyBypass); + options.Add(bypass); + + connFlags.Value.dwValue = (UInt32)flags; + options.Add(connFlags); + + SetOption(options.ToArray(), Connection); + + // Tell IE that the proxy settings have been changed. + if (!NativeMethods.InternetSetOptionW( + IntPtr.Zero, + NativeHelpers.INTERNET_OPTION.INTERNET_OPTION_PROXY_SETTINGS_CHANGED, + new SafeMemoryBuffer(IntPtr.Zero), + 0)) + { + throw new Win32Exception("InternetSetOptionW(INTERNET_OPTION_PROXY_SETTINGS_CHANGED) failed"); + } + } + } + + internal static NativeHelpers.INTERNET_PER_CONN_OPTIONW CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION option) + { + return new NativeHelpers.INTERNET_PER_CONN_OPTIONW + { + dwOption = option, + Value = new NativeHelpers.INTERNET_PER_CONN_OPTIONW.ValueUnion(), + }; + } + + internal static void QueryOption(NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options, string connection = null) + { + using (NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW optionList = new NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW()) + using (SafeMemoryBuffer optionListPtr = MarshalOptionList(optionList, options, connection)) + { + UInt32 bufferSize = optionList.dwSize; + if (!NativeMethods.InternetQueryOptionW( + IntPtr.Zero, + NativeHelpers.INTERNET_OPTION.INTERNET_OPTION_PER_CONNECTION_OPTION, + optionListPtr, + ref bufferSize)) + { + throw new Win32Exception("InternetQueryOptionW(INTERNET_OPTION_PER_CONNECTION_OPTION) failed"); + } + + for (int i = 0; i < options.Length; i++) + { + IntPtr opt = IntPtr.Add(optionList.pOptions, i * Marshal.SizeOf(typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW))); + NativeHelpers.INTERNET_PER_CONN_OPTIONW option = (NativeHelpers.INTERNET_PER_CONN_OPTIONW)Marshal.PtrToStructure(opt, + typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW)); + options[i].Value = option.Value; + option.Value = null; // Stops the GC from freeing the same memory twice + } + } + } + + internal static void SetOption(NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options, string connection = null) + { + using (NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW optionList = new NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW()) + using (SafeMemoryBuffer optionListPtr = MarshalOptionList(optionList, options, connection)) + { + if (!NativeMethods.InternetSetOptionW( + IntPtr.Zero, + NativeHelpers.INTERNET_OPTION.INTERNET_OPTION_PER_CONNECTION_OPTION, + optionListPtr, + optionList.dwSize)) + { + throw new Win32Exception("InternetSetOptionW(INTERNET_OPTION_PER_CONNECTION_OPTION) failed"); + } + } + } + + internal static SafeMemoryBuffer MarshalOptionList(NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW optionList, + NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options, string connection) + { + optionList.pszConnection = Marshal.StringToHGlobalUni(connection); + optionList.dwOptionCount = (UInt32)options.Length; + + int optionSize = Marshal.SizeOf(typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW)); + optionList.pOptions = Marshal.AllocHGlobal(optionSize * options.Length); + for (int i = 0; i < options.Length; i++) + { + IntPtr option = IntPtr.Add(optionList.pOptions, i * optionSize); + Marshal.StructureToPtr(options[i], option, false); + } + + SafeMemoryBuffer optionListPtr = new SafeMemoryBuffer((int)optionList.dwSize); + Marshal.StructureToPtr(optionList, optionListPtr.DangerousGetHandle(), false); + return optionListPtr; + } + } +} +'@ +Add-CSharpType -References $win_inet_invoke -AnsibleModule $module + +# We need to validate the connection because WinINet will just silently continue even if the connection does not +# already exist. +if ($null -ne $connection -and -not [Ansible.WinINetProxy.WinINetProxy]::IsValidConnection($connection)) { + $module.FailJson("The connection '$connection' does not exist.") +} + +$actual_proxy = New-Object -TypeName Ansible.WinINetProxy.WinINetProxy -ArgumentList @(,$connection) +$module.Diff.before = @{ + auto_config_url = $actual_proxy.AutoConfigUrl + auto_detect = $actual_proxy.AutoDetect + bypass = $actual_proxy.ProxyBypass + server = $actual_proxy.Proxy +} + +# Make sure an empty string is converted to $null for easier comparisons +if ([String]::IsNullOrEmpty($auto_config_url)) { + $auto_config_url = $null +} +if ([String]::IsNullOrEmpty($proxy)) { + $proxy = $null +} +if ([String]::IsNullOrEmpty($bypass)) { + $bypass = $null +} + +# Record the original values in case we need to revert on a failure +$previous_auto_config_url = $actual_proxy.AutoConfigUrl +$previous_auto_detect = $actual_proxy.AutoDetect +$previous_proxy = $actual_proxy.Proxy +$previous_bypass = $actual_proxy.ProxyBypass + +$changed = $false +if ($auto_config_url -ne $previous_auto_config_url) { + $actual_proxy.AutoConfigUrl = $auto_config_url + $changed = $true +} + +if ($auto_detect -ne $previous_auto_detect) { + $actual_proxy.AutoDetect = $auto_detect + $changed = $true +} + +if ($proxy -ne $previous_proxy) { + $actual_proxy.Proxy = $proxy + $changed = $true +} + +if ($bypass -ne $previous_bypass) { + $actual_proxy.ProxyBypass = $bypass + $changed = $true +} + +if ($changed -and -not $module.CheckMode) { + $actual_proxy.Set() + + # Validate that the change was made correctly and revert if it wasn't. THe Set() method won't fail on invalid + # values so we need to check again to make sure all was good + $actual_proxy.Refresh() + if ($actual_proxy.AutoConfigUrl -ne $auto_config_url -or + $actual_proxy.AutoDetect -ne $auto_detect -or + $actual_proxy.Proxy -ne $proxy -or + $actual_proxy.ProxyBypass -ne $bypass) { + + $actual_proxy.AutoConfigUrl = $previous_auto_config_url + $actual_proxy.AutoDetect = $previous_auto_detect + $actual_proxy.Proxy = $previous_proxy + $actual_proxy.ProxyBypass = $previous_bypass + $actual_proxy.Set() + + $module.FailJson("Unknown error when trying to set auto_config_url '$auto_config_url', proxy '$proxy', or bypass '$bypass'") + } +} +$module.Result.changed = $changed + +$module.Diff.after = @{ + auto_config_url = $auto_config_url + auto_detect = $auto_detect + bypass = $bypass + proxy = $proxy +} + +$module.ExitJson() diff --git a/lib/ansible/modules/windows/win_inet_proxy.py b/lib/ansible/modules/windows/win_inet_proxy.py new file mode 100644 index 00000000000..417c774f9a8 --- /dev/null +++ b/lib/ansible/modules/windows/win_inet_proxy.py @@ -0,0 +1,173 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: win_inet_proxy +version_added: '2.8' +short_description: Manages proxy settings for WinINet and Internet Explorer +description: +- Used to set or remove proxy settings for Windows INet which includes Internet + Explorer. +- WinINet is a framework used by interactive applications to submit web + requests through. +- The proxy settings can also be used by other applications like Firefox, + Chrome, and others but there is no definitive list. +options: + auto_detect: + description: + - Whether to configure WinINet to automatically detect proxy settings + through Web Proxy Auto-Detection C(WPAD). + - This corresponds to the checkbox I(Automatically detect settings) in the + connection settings window. + default: yes + type: bool + auto_config_url: + description: + - The URL of a proxy configuration script. + - Proxy configuration scripts are typically JavaScript files with the + C(.pac) extension that implement the C(FindProxyForURL(url, host) + function. + - Omit, set to null or an empty string to remove the auto config URL. + - This corresponds to the checkbox I(Use automatic configuration script) in + the connection settings window. + type: str + bypass: + description: + - A list of hosts that will bypass the set proxy when being accessed. + - Use C() to match hostnames that are not fully qualified domain + names. This is useful when needing to connect to intranet sites using + just the hostname. If defined, this should be the last entry in the + bypass list. + - Use C(<-loopback>) to stop automatically bypassing the proxy when + connecting through any loopback address like C(127.0.0.1), C(localhost), + or the local hostname. + - Omit, set to null or an empty string/list to remove the bypass list. + - If this is set then I(proxy) must also be set. + type: list + connection: + description: + - The name of the IE connection to set the proxy settings for. + - These are the connections under the I(Dial-up and Virtual Private Network) + header in the IE settings. + - When ommited, the default LAN connection is used. + type: str + proxy: + description: + - A string or dict that specifies the proxy to be set. + - If setting a string, should be in the form C(hostname), C(hostname:port), + or C(protocol=hostname:port). + - If the port is undefined, the default port for the protocol in use is + used. + - If setting a dict, the keys should be the protocol and the values should + be the hostname and/or port for that protocol. + - Valid protocols are C(http), C(https), C(ftp), and C(socks). + - Omit, set to null or an empty string to remove the proxy settings. +notes: +- This is not the same as the proxy settings set in WinHTTP through the + C(netsh) command. Use the M(win_http_proxy) module to manage that instead. +- These settings are by default set per user and not system wide. A registry + property must be set independently from this module if you wish to apply the + proxy for all users. See examples for more detail. +- If per user proxy settings are desired, use I(become) to become any local + user on the host. No password is needed to be set for this to work. +- If the proxy requires authentication, set the credentials using the + M(win_credential) module. This requires I(become) to be used so the + credential store can be accessed. +seealso: +- module: win_http_proxy +- module: win_credential +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +# This should be set before running the win_inet_proxy module +- name: Configure IE proxy settings to apply to all users + win_regedit: + path: HKLM:\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings + name: ProxySettingsPerUser + data: 0 + type: dword + state: present + +# This should be set before running the win_inet_proxy module +- name: Configure IE proxy settings to apply per user + win_regedit: + path: HKLM:\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings + name: ProxySettingsPerUser + data: 1 + type: dword + state: present + +- name: Configure IE proxy to use auto detected settings without an explicit proxy + win_inet_proxy: + auto_detect: yes + +- name: Configure IE proxy to use auto detected settings with a configuration script + win_inet_proxy: + auto_detect: yes + auto_config_url: http://proxy.ansible.com/proxy.pac + +- name: Configure IE to use explicit proxy host + win_inet_proxy: + auto_detect: yes + proxy: ansible.proxy + +- name: Configure IE to use explicit proxy host with port and without auto detection + win_inet_proxy: + auto_detect: no + proxy: ansible.proxy:8080 + +- name: Configure IE to use a specific proxy per protocol + win_inet_proxy: + proxy: + http: ansible.proxy:8080 + https: ansible.proxy:8443 + +- name: Configure IE to use a specific proxy per protocol using a string + win_inet_proxy: + proxy: http=ansible.proxy:8080;https=ansible.proxy:8443 + +- name: Set a proxy with a bypass list + win_inet_proxy: + proxy: ansible.proxy + bypass: + - server1 + - server2 + - <-loopback> + - + +- name: Remove any explicit proxies that are set + win_inet_proxy: + proxy: '' + bypass: '' + +# This should be done after setting the IE proxy with win_inet_proxy +- name: Import IE proxy configuration to WinHTTP + win_http_proxy: + source: ie + +# Explicit credentials can only be set per user and require become to work +- name: Set credential to use for proxy auth + win_credential: + name: ansible.proxy # The name should be the FQDN of the proxy host + type: generic_password + username: proxyuser + secret: proxypass + state: present + become: yes + become_user: '{{ ansible_user }}' + become_method: runas +''' + +RETURN = r''' +# +''' diff --git a/test/integration/targets/win_http_proxy/aliases b/test/integration/targets/win_http_proxy/aliases new file mode 100644 index 00000000000..215e0b06920 --- /dev/null +++ b/test/integration/targets/win_http_proxy/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/test/integration/targets/win_http_proxy/tasks/main.yml b/test/integration/targets/win_http_proxy/tasks/main.yml new file mode 100644 index 00000000000..5da9aa7fecb --- /dev/null +++ b/test/integration/targets/win_http_proxy/tasks/main.yml @@ -0,0 +1,14 @@ +--- +- name: make sure we start the tests with no proxy set + win_http_proxy: + +- block: + - name: run tests + include_tasks: tests.yml + + always: + - name: remove any explicit proxy settings + win_http_proxy: + + - name: reset WinINet proxy settings + win_inet_proxy: diff --git a/test/integration/targets/win_http_proxy/tasks/tests.yml b/test/integration/targets/win_http_proxy/tasks/tests.yml new file mode 100644 index 00000000000..c68ea611b72 --- /dev/null +++ b/test/integration/targets/win_http_proxy/tasks/tests.yml @@ -0,0 +1,265 @@ +--- +- name: ensure we fail when proxy is not set with bypass + win_http_proxy: + bypass: abc + register: fail_bypass + failed_when: 'fail_bypass.msg != "missing parameter(s) required by ''bypass'': proxy"' + +- name: ensure we fail when proxy and source is set + win_http_proxy: + proxy: proxy + source: ie + register: fail_source + failed_when: 'fail_source.msg != "parameters are mutually exclusive: proxy, source"' + +- name: ensure we fail if an invalid protocol is specified + win_http_proxy: + proxy: + fail1: fail + fail2: fail + register: fail_protocol + failed_when: 'fail_protocol.msg != "Invalid keys found in proxy: fail1, fail2. Valid keys are http, https, ftp, socks."' + +# WinHTTP does not validate on set, this ensures the module checks and revert any failed attempts at setting the proxy +# FIXME: Only certain hosts seem to have a strict winhttp definition, we can't run this in CI for now +#- name: ensure we fail if invalid value is set +# win_http_proxy: +# proxy: fake=proxy +# register: fail_invalid +# failed_when: fail_invalid.msg != "Unknown error when trying to set proxy 'fake=proxy' or bypass ''" +# +#- name: check proxy is still set to Direct access +# win_command: netsh winhttp show proxy +# register: fail_invalid_actual +# failed_when: fail_invalid_actual.stdout_lines[3]|trim != "Direct access (no proxy server)." + +- name: set a proxy using a string (check) + win_http_proxy: + proxy: proxyhost + register: proxy_str_check + check_mode: True + +- name: get result of set a proxy using a string (check) + win_command: netsh winhttp show proxy + register: proxy_str_actual_check + +- name: assert set a proxy using a string (check) + assert: + that: + - proxy_str_check is changed + - proxy_str_actual_check.stdout_lines[3]|trim == "Direct access (no proxy server)." + +- name: set a proxy using a string + win_http_proxy: + proxy: proxyhost + register: proxy_str + +- name: get result of set a proxy using a string + win_command: netsh winhttp show proxy + register: proxy_str_actual + +- name: assert set a proxy using a string + assert: + that: + - proxy_str is changed + - 'proxy_str_actual.stdout_lines[3]|trim == "Proxy Server(s) : proxyhost"' + - 'proxy_str_actual.stdout_lines[4]|trim == "Bypass List : (none)"' + +- name: set a proxy using a string (idempotent) + win_http_proxy: + proxy: proxyhost + register: proxy_str_again + +- name: assert set a proxy using a string (idempotent) + assert: + that: + - not proxy_str_again is changed + +- name: change a proxy and set bypass (check) + win_http_proxy: + proxy: proxyhost:8080 + bypass: + - abc + - def + - + register: change_proxy_check + check_mode: True + +- name: get result of change a proxy and set bypass (check) + win_command: netsh winhttp show proxy + register: change_proxy_actual_check + +- name: assert change a proxy and set bypass (check) + assert: + that: + - change_proxy_check is changed + - 'change_proxy_actual_check.stdout_lines[3]|trim == "Proxy Server(s) : proxyhost"' + - 'change_proxy_actual_check.stdout_lines[4]|trim == "Bypass List : (none)"' + +- name: change a proxy and set bypass + win_http_proxy: + proxy: proxyhost:8080 + bypass: + - abc + - def + - + register: change_proxy + +- name: get result of change a proxy and set bypass + win_command: netsh winhttp show proxy + register: change_proxy_actual + +- name: assert change a proxy and set bypass + assert: + that: + - change_proxy is changed + - 'change_proxy_actual.stdout_lines[3]|trim == "Proxy Server(s) : proxyhost:8080"' + - 'change_proxy_actual.stdout_lines[4]|trim == "Bypass List : abc;def;"' + +- name: change a proxy and set bypass (idempotent) + win_http_proxy: + proxy: proxyhost:8080 + bypass: abc,def, + register: change_proxy_again + +- name: assert change a proxy and set bypass (idempotent) + assert: + that: + - not change_proxy_again is changed + +- name: change bypass list + win_http_proxy: + proxy: proxyhost:8080 + bypass: + - abc + - <-loopback> + register: change_bypass + +- name: get result of change bypass list + win_command: netsh winhttp show proxy + register: change_bypass_actual + +- name: assert change bypass list + assert: + that: + - change_bypass is changed + - 'change_bypass_actual.stdout_lines[3]|trim == "Proxy Server(s) : proxyhost:8080"' + - 'change_bypass_actual.stdout_lines[4]|trim == "Bypass List : abc;<-loopback>"' + +- name: remove proxy without options (check) + win_http_proxy: + register: remove_proxy_check + check_mode: yes + +- name: get result of remove proxy without options (check) + win_command: netsh winhttp show proxy + register: remove_proxy_actual_check + +- name: assert remove proxy without options (check) + assert: + that: + - remove_proxy_check is changed + - 'remove_proxy_actual_check.stdout_lines[3]|trim == "Proxy Server(s) : proxyhost:8080"' + - 'remove_proxy_actual_check.stdout_lines[4]|trim == "Bypass List : abc;<-loopback>"' + +- name: remove proxy without options + win_http_proxy: + register: remove_proxy + +- name: get result of remove proxy without options + win_command: netsh winhttp show proxy + register: remove_proxy_actual + +- name: assert remove proxy without options + assert: + that: + - remove_proxy is changed + - remove_proxy_actual.stdout_lines[3]|trim == "Direct access (no proxy server)." + +- name: remove proxy without options (idempotent) + win_http_proxy: + register: remove_proxy_again + +- name: assert remove proxy without options (idempotent) + assert: + that: + - not remove_proxy_again is changed + +- name: set proxy with dictionary + win_http_proxy: + proxy: + http: proxy:8080 + https: proxy:8443 + ftp: proxy:821 + socks: proxy:888 + register: set_dict + +- name: get result of set proxy with dictionary + win_command: netsh winhttp show proxy + register: set_dict_actual + +- name: assert set proxy with dictionary + assert: + that: + - set_dict is changed + - 'set_dict_actual.stdout_lines[3]|trim == "Proxy Server(s) : http=proxy:8080;https=proxy:8443;ftp=proxy:821;socks=proxy:888"' + - 'set_dict_actual.stdout_lines[4]|trim == "Bypass List : (none)"' + +- name: set proxy protocol with str + win_http_proxy: + proxy: http=proxy:8080;https=proxy:8443;ftp=proxy:821;socks=proxy:888 + register: set_str_protocol + +- name: assert set proxy protocol with str + assert: + that: + - not set_str_protocol is changed + +- name: remove proxy with empty string + win_http_proxy: + proxy: '' + register: remove_empty_str + +- name: get result of remove proxy with empty string + win_command: netsh winhttp show proxy + register: remove_empty_str_actual + +- name: assert remove proxy with empty string + assert: + that: + - remove_empty_str is changed + - remove_empty_str_actual.stdout_lines[3]|trim == "Direct access (no proxy server)." + +- name: set explicit proxy for WinINet + win_inet_proxy: + proxy: proxyhost:8080 + bypass: + - abc + - def + - + +- name: import proxy from IE + win_http_proxy: + source: ie + register: import_ie + +- name: get result of import proxy from IE + win_command: netsh winhttp show proxy + register: import_ie_actual + +- name: assert import proxy from IE + assert: + that: + - import_ie is changed + - 'import_ie_actual.stdout_lines[3]|trim == "Proxy Server(s) : proxyhost:8080"' + - 'import_ie_actual.stdout_lines[4]|trim == "Bypass List : abc;def;"' + +- name: import proxy from IE (idempotent) + win_http_proxy: + source: ie + register: import_ie_again + +- name: assert import proxy from IE (idempotent) + assert: + that: + - not import_ie_again is changed diff --git a/test/integration/targets/win_inet_proxy/aliases b/test/integration/targets/win_inet_proxy/aliases new file mode 100644 index 00000000000..215e0b06920 --- /dev/null +++ b/test/integration/targets/win_inet_proxy/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/test/integration/targets/win_inet_proxy/library/win_inet_proxy_info.ps1 b/test/integration/targets/win_inet_proxy/library/win_inet_proxy_info.ps1 new file mode 100644 index 00000000000..d52b11d3ad3 --- /dev/null +++ b/test/integration/targets/win_inet_proxy/library/win_inet_proxy_info.ps1 @@ -0,0 +1,275 @@ +#!powershell + +# Copyright: (c) 2019, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.AddType + +$spec = @{ + options = @{ + connection = @{ type = "str" } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$connection = $module.Params.connection + +$win_inet_invoke = @' +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; + +namespace Ansible.WinINetProxyInfo +{ + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public class INTERNET_PER_CONN_OPTION_LISTW : IDisposable + { + public UInt32 dwSize; + public IntPtr pszConnection; + public UInt32 dwOptionCount; + public UInt32 dwOptionError; + public IntPtr pOptions; + + public INTERNET_PER_CONN_OPTION_LISTW() + { + dwSize = (UInt32)Marshal.SizeOf(this); + } + + public void Dispose() + { + if (pszConnection != IntPtr.Zero) + Marshal.FreeHGlobal(pszConnection); + if (pOptions != IntPtr.Zero) + Marshal.FreeHGlobal(pOptions); + GC.SuppressFinalize(this); + } + ~INTERNET_PER_CONN_OPTION_LISTW() { this.Dispose(); } + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public class INTERNET_PER_CONN_OPTIONW : IDisposable + { + public INTERNET_PER_CONN_OPTION dwOption; + public ValueUnion Value; + + [StructLayout(LayoutKind.Explicit)] + public class ValueUnion + { + [FieldOffset(0)] + public UInt32 dwValue; + + [FieldOffset(0)] + public IntPtr pszValue; + + [FieldOffset(0)] + public System.Runtime.InteropServices.ComTypes.FILETIME ftValue; + } + + public void Dispose() + { + // We can't just check if Value.pszValue is not IntPtr.Zero as the union means it could be set even + // when the value is a UInt32 or FILETIME. We check against a known string option type and only free + // the value in those cases. + List stringOptions = new List + { + { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_AUTOCONFIG_URL }, + { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_BYPASS }, + { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_SERVER } + }; + if (Value != null && Value.pszValue != IntPtr.Zero && stringOptions.Contains(dwOption)) + Marshal.FreeHGlobal(Value.pszValue); + GC.SuppressFinalize(this); + } + ~INTERNET_PER_CONN_OPTIONW() { this.Dispose(); } + } + + public enum INTERNET_OPTION : uint + { + INTERNET_OPTION_PER_CONNECTION_OPTION = 75, + } + + public enum INTERNET_PER_CONN_OPTION : uint + { + INTERNET_PER_CONN_FLAGS = 1, + INTERNET_PER_CONN_PROXY_SERVER = 2, + INTERNET_PER_CONN_PROXY_BYPASS = 3, + INTERNET_PER_CONN_AUTOCONFIG_URL = 4, + INTERNET_PER_CONN_AUTODISCOVERY_FLAGS = 5, + INTERNET_PER_CONN_FLAGS_UI = 10, // IE8+ - Included with Windows 7 and Server 2008 R2 + } + + [Flags] + public enum PER_CONN_FLAGS : uint + { + PROXY_TYPE_DIRECT = 0x00000001, + PROXY_TYPE_PROXY = 0x00000002, + PROXY_TYPE_AUTO_PROXY_URL = 0x00000004, + PROXY_TYPE_AUTO_DETECT = 0x00000008, + } + } + + internal class NativeMethods + { + [DllImport("Wininet.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool InternetQueryOptionW( + IntPtr hInternet, + NativeHelpers.INTERNET_OPTION dwOption, + SafeMemoryBuffer lpBuffer, + ref UInt32 lpdwBufferLength); + } + + internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeMemoryBuffer() : base(true) { } + public SafeMemoryBuffer(int cb) : base(true) + { + base.SetHandle(Marshal.AllocHGlobal(cb)); + } + public SafeMemoryBuffer(IntPtr handle) : base(true) + { + base.SetHandle(handle); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + Marshal.FreeHGlobal(handle); + return true; + } + } + + public class WinINetProxy + { + private string Connection; + + public string AutoConfigUrl; + public bool AutoDetect; + public string Proxy; + public string ProxyBypass; + + public WinINetProxy(string connection) + { + Connection = connection; + Refresh(); + } + + public void Refresh() + { + using (var connFlags = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_FLAGS_UI)) + using (var autoConfigUrl = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_AUTOCONFIG_URL)) + using (var server = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_SERVER)) + using (var bypass = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_BYPASS)) + { + NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options = new NativeHelpers.INTERNET_PER_CONN_OPTIONW[] + { + connFlags, autoConfigUrl, server, bypass + }; + + try + { + QueryOption(options, Connection); + } + catch (Win32Exception e) + { + if (e.NativeErrorCode == 87) // ERROR_INVALID_PARAMETER + { + // INTERNET_PER_CONN_FLAGS_UI only works for IE8+, try the fallback in case we are still working + // with an ancient version. + connFlags.dwOption = NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_FLAGS; + QueryOption(options, Connection); + } + else + throw; + } + + NativeHelpers.PER_CONN_FLAGS flags = (NativeHelpers.PER_CONN_FLAGS)connFlags.Value.dwValue; + + AutoConfigUrl = flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_PROXY_URL) + ? Marshal.PtrToStringUni(autoConfigUrl.Value.pszValue) : null; + AutoDetect = flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_DETECT); + if (flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_PROXY)) + { + Proxy = Marshal.PtrToStringUni(server.Value.pszValue); + ProxyBypass = Marshal.PtrToStringUni(bypass.Value.pszValue); + } + else + { + Proxy = null; + ProxyBypass = null; + } + } + } + + internal static NativeHelpers.INTERNET_PER_CONN_OPTIONW CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION option) + { + return new NativeHelpers.INTERNET_PER_CONN_OPTIONW + { + dwOption = option, + Value = new NativeHelpers.INTERNET_PER_CONN_OPTIONW.ValueUnion(), + }; + } + + internal static void QueryOption(NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options, string connection = null) + { + using (NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW optionList = new NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW()) + using (SafeMemoryBuffer optionListPtr = MarshalOptionList(optionList, options, connection)) + { + UInt32 bufferSize = optionList.dwSize; + if (!NativeMethods.InternetQueryOptionW( + IntPtr.Zero, + NativeHelpers.INTERNET_OPTION.INTERNET_OPTION_PER_CONNECTION_OPTION, + optionListPtr, + ref bufferSize)) + { + throw new Win32Exception(); + } + + for (int i = 0; i < options.Length; i++) + { + IntPtr opt = IntPtr.Add(optionList.pOptions, i * Marshal.SizeOf(typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW))); + NativeHelpers.INTERNET_PER_CONN_OPTIONW option = (NativeHelpers.INTERNET_PER_CONN_OPTIONW)Marshal.PtrToStructure(opt, + typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW)); + options[i].Value = option.Value; + option.Value = null; // Stops the GC from freeing the same memory twice + } + } + } + + internal static SafeMemoryBuffer MarshalOptionList(NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW optionList, + NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options, string connection) + { + optionList.pszConnection = Marshal.StringToHGlobalUni(connection); + optionList.dwOptionCount = (UInt32)options.Length; + + int optionSize = Marshal.SizeOf(typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW)); + optionList.pOptions = Marshal.AllocHGlobal(optionSize * options.Length); + for (int i = 0; i < options.Length; i++) + { + IntPtr option = IntPtr.Add(optionList.pOptions, i * optionSize); + Marshal.StructureToPtr(options[i], option, false); + } + + SafeMemoryBuffer optionListPtr = new SafeMemoryBuffer((int)optionList.dwSize); + Marshal.StructureToPtr(optionList, optionListPtr.DangerousGetHandle(), false); + return optionListPtr; + } + } +} +'@ +Add-CSharpType -References $win_inet_invoke -AnsibleModule $module + +$proxy = New-Object -TypeName Ansible.WinINetProxyInfo.WinINetProxy -ArgumentList @(,$connection) +$module.Result.auto_config_url = $proxy.AutoConfigUrl +$module.Result.auto_detect = $proxy.AutoDetect +$module.Result.proxy = $proxy.Proxy +$module.Result.bypass = $proxy.ProxyBypass + +$module.ExitJson() diff --git a/test/integration/targets/win_inet_proxy/library/win_phonebook_entry.ps1 b/test/integration/targets/win_inet_proxy/library/win_phonebook_entry.ps1 new file mode 100644 index 00000000000..1746921f6d7 --- /dev/null +++ b/test/integration/targets/win_inet_proxy/library/win_phonebook_entry.ps1 @@ -0,0 +1,521 @@ +#!powershell + +# Copyright: (c) 2019, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.AddType + +# This is a very basic skeleton of a possible Windows module for managing RAS connections. It is mostly barebones +# to enable testing for win_inet_proxy but I've done a bit of extra work in the PInvoke space to possible expand +# sometime in the future. + +$spec = @{ + options = @{ + device_type = @{ + type = "str" + choices = @("atm", "framerelay", "generic", "rda", "isdn", "modem", "pad", + "parallel", "pppoe", "vpn", "serial", "sonet", "sw56", "x25") + } + device_name = @{ type = "str" } + framing_protocol = @{ type = "str"; choices = @("ppp", "ras", "slip") } + name = @{ type = "str"; required = $true } + options = @{ type = "list" } + state = @{ type = "str"; choices = @("absent", "present"); default = "present" } + type = @{ type = "str"; choices = @("broadband", "direct", "phone", "vpn")} + } + required_if = @( + ,@("state", "present", @("type", "device_name", "device_type", "framing_protocol")) + ) + supports_check_mode = $false +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$device_type = $module.Params.device_type +$device_name = $module.Params.device_name +$framing_protocol = $module.Params.framing_protocol +$name = $module.Params.name +$options = $module.Params.options +$state = $module.Params.state +$type = $module.Params.type + +$module.Result.guid = [System.Guid]::Empty + +$win_ras_invoke = @' +using System; +using System.Runtime.InteropServices; + +namespace Ansible.WinPhonebookEntry +{ + public class NativeHelpers + { + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public class RASENTRYW + { + public UInt32 dwSize; + public RasEntryOptions dwfOptions; + public UInt32 dwCountryId; + public UInt32 dwCountryCode; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 11)] public string szAreaCode; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 129)] public string szLocalPhoneNumber; + public UInt32 dwAlternateOffset; + public RASIPADDR ipaddr; + public RASIPADDR ipaddrDns; + public RASIPADDR ipaddrDnsAlt; + public RASIPADDR ipaddrWins; + public RASIPADDR ipaddrWinsAlt; + public UInt32 dwFrameSize; + public RasNetProtocols dwfNetProtocols; + public RasFramingProtocol dwFramingProtocol; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szScript; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szAutodialDll; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szAutodialFunc; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 17)] public string szDeviceType; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 129)] public string szDeviceName; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 33)] public string szX25PadType; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 201)] public string szX25Address; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 201)] public string szX25Facilities; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 201)] public string szX25UserData; + public UInt32 dwChannels; + public UInt32 dwReserved1; + public UInt32 dwReserved2; + public UInt32 dwSubEntries; + public RasDialMode dwDialMode; + public UInt32 dwDialExtraPercent; + public UInt32 dwDialExtraSampleSeconds; + public UInt32 dwHangUpExtraPercent; + public UInt32 dwHangUpExtraSampleSeconds; + public UInt32 dwIdleDisconnectSeconds; + public RasEntryTypes dwType; + public RasEntryEncryption dwEntryptionType; + public UInt32 dwCustomAuthKey; + public Guid guidId; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szCustomDialDll; + public RasVpnStrategy dwVpnStrategy; + public RasEntryOptions2 dwfOptions2; + public UInt32 dwfOptions3; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string szDnsSuffix; + public UInt32 dwTcpWindowSize; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szPrerequisitePbk; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 257)] public string szPrerequisiteEntry; + public UInt32 dwRedialCount; + public UInt32 dwRedialPause; + public RASIPV6ADDR ipv6addrDns; + public RASIPV6ADDR ipv6addrDnsAlt; + public UInt32 dwIPv4InterfaceMatrix; + public UInt32 dwIPv6InterfaceMatrix; + // Server 2008 R2 / Windows 7+ + // We cannot include these fields when running in Server 2008 as it will break the SizeOf calc of the struct +#if !LONGHORN + public RASIPV6ADDR ipv6addr; + public UInt32 dwIPv6PrefixLength; + public UInt32 dwNetworkOutageTime; +#endif + + public RASENTRYW() + { + this.dwSize = (UInt32)Marshal.SizeOf(this); + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct RASIPADDR + { + public byte a; + public byte b; + public byte c; + public byte d; + } + + [StructLayout(LayoutKind.Sequential)] + public struct RASIPV6ADDR + { + byte a; + byte b; + byte c; + byte d; + byte e; + byte f; + byte g; + byte h; + byte i; + byte j; + byte k; + byte l; + byte m; + byte n; + byte o; + byte p; + } + + public enum RasDialMode : uint + { + RASEDM_DialAll = 1, + RASEDM_DialAsNeeded = 2, + } + + public enum RasEntryEncryption : uint + { + ET_None = 0, + ET_Require = 1, + ET_RequireMax = 2, + ET_Optional = 3 + } + + [Flags] + public enum RasEntryOptions : uint + { + RASEO_UseCountryAndAreaCodes = 0x00000001, + RASEO_SpecificIpAddr = 0x00000002, + RASEO_SpecificNameServers = 0x00000004, + RASEO_IpHeaderCompression = 0x00000008, + RASEO_RemoteDefaultGateway = 0x00000010, + RASEO_DisableLcpExtensions = 0x00000020, + RASEO_TerminalBeforeDial = 0x00000040, + RASEO_TerminalAfterDial = 0x00000080, + RASEO_ModemLights = 0x00000100, + RASEO_SwCompression = 0x00000200, + RASEO_RequireEncrptedPw = 0x00000400, + RASEO_RequireMsEncrptedPw = 0x00000800, + RASEO_RequireDataEncrption = 0x00001000, + RASEO_NetworkLogon = 0x00002000, + RASEO_UseLogonCredentials = 0x00004000, + RASEO_PromoteAlternates = 0x00008000, + RASEO_SecureLocalFiles = 0x00010000, + RASEO_RequireEAP = 0x00020000, + RASEO_RequirePAP = 0x00040000, + RASEO_RequireSPAP = 0x00080000, + RASEO_Custom = 0x00100000, + RASEO_PreviewPhoneNumber = 0x00200000, + RASEO_SharedPhoneNumbers = 0x00800000, + RASEO_PreviewUserPw = 0x01000000, + RASEO_PreviewDomain = 0x02000000, + RASEO_ShowDialingProgress = 0x04000000, + RASEO_RequireCHAP = 0x08000000, + RASEO_RequireMsCHAP = 0x10000000, + RASEO_RequireMsCHAP2 = 0x20000000, + RASEO_RequireW95MSCHAP = 0x40000000, + RASEO_CustomScript = 0x80000000, + } + + [Flags] + public enum RasEntryOptions2 : uint + { + RASEO2_None = 0x00000000, + RASEO2_SecureFileAndPrint = 0x00000001, + RASEO2_SecureClientForMSNet = 0x00000002, + RASEO2_DontNegotiateMultilink = 0x00000004, + RASEO2_DontUseRasCredentials = 0x00000008, + RASEO2_UsePreSharedKey = 0x00000010, + RASEO2_Internet = 0x00000020, + RASEO2_DisableNbtOverIP = 0x00000040, + RASEO2_UseGlobalDeviceSettings = 0x00000080, + RASEO2_ReconnectIfDropped = 0x00000100, + RASEO2_SharePhoneNumbers = 0x00000200, + RASEO2_SecureRoutingCompartment = 0x00000400, + RASEO2_UseTypicalSettings = 0x00000800, + RASEO2_IPv6SpecificNameServers = 0x00001000, + RASEO2_IPv6RemoteDefaultGateway = 0x00002000, + RASEO2_RegisterIpWithDNS = 0x00004000, + RASEO2_UseDNSSuffixForRegistration = 0x00008000, + RASEO2_IPv4ExplicitMetric = 0x00010000, + RASEO2_IPv6ExplicitMetric = 0x00020000, + RASEO2_DisableIKENameEkuCheck = 0x00040000, + // Server 2008 R2 / Windows 7+ + RASEO2_DisableClassBasedStaticRoute = 0x00800000, + RASEO2_SpecificIPv6Addr = 0x01000000, + RASEO2_DisableMobility = 0x02000000, + RASEO2_RequireMachineCertificates = 0x04000000, + // Server 2012 / Windows 8+ + RASEO2_UsePreSharedKeyForIkev2Initiator = 0x00800000, + RASEO2_UsePreSharedKeyForIkev2Responder = 0x01000000, + RASEO2_CacheCredentials = 0x02000000, + // Server 2012 R2 / Windows 8.1+ + RASEO2_AutoTriggerCapable = 0x04000000, + RASEO2_IsThirdPartyProfile = 0x08000000, + RASEO2_AuthTypeIsOtp = 0x10000000, + // Server 2016 / Windows 10+ + RASEO2_IsAlwaysOn = 0x20000000, + RASEO2_IsPrivateNetwork = 0x40000000, + } + + public enum RasEntryTypes : uint + { + RASET_Phone = 1, + RASET_Vpn = 2, + RASET_Direct = 3, + RASET_Internet = 4, + RASET_Broadband = 5, + } + + public enum RasFramingProtocol : uint + { + RASFP_Ppp = 0x00000001, + RASFP_Slip = 0x00000002, + RASFP_Ras = 0x00000004 + } + + [Flags] + public enum RasNetProtocols : uint + { + RASNP_NetBEUI = 0x00000001, + RASNP_Ipx = 0x00000002, + RASNP_Ip = 0x00000004, + RASNP_Ipv6 = 0x00000008 + } + + public enum RasVpnStrategy : uint + { + VS_Default = 0, + VS_PptpOnly = 1, + VS_PptpFirst = 2, + VS_L2tpOnly = 3, + VS_L2tpFirst = 4, + VS_SstpOnly = 5, + VS_SstpFirst = 6, + VS_Ikev2Only = 7, + VS_Ikev2First = 8, + VS_GREOnly = 9, + VS_PptpSstp = 12, + VS_L2tpSstp = 13, + VS_Ikev2Sstp = 14, + } + } + + internal class NativeMethods + { + [DllImport("Rasapi32.dll", CharSet = CharSet.Unicode)] + public static extern UInt32 RasDeleteEntryW( + string lpszPhonebook, + string lpszEntry); + + [DllImport("Rasapi32.dll", CharSet = CharSet.Unicode)] + public static extern UInt32 RasGetEntryPropertiesW( + string lpszPhonebook, + string lpszEntry, + [In, Out] NativeHelpers.RASENTRYW lpRasEntry, + ref UInt32 dwEntryInfoSize, + IntPtr lpbDeviceInfo, + ref UInt32 dwDeviceInfoSize); + + [DllImport("Rasapi32.dll", CharSet = CharSet.Unicode)] + public static extern UInt32 RasSetEntryPropertiesW( + string lpszPhonebook, + string lpszEntry, + NativeHelpers.RASENTRYW lpRasEntry, + UInt32 dwEntryInfoSize, + IntPtr lpbDeviceInfo, + UInt32 dwDeviceInfoSize); + + [DllImport("Rasapi32.dll", CharSet = CharSet.Unicode)] + public static extern UInt32 RasValidateEntryNameW( + string lpszPhonebook, + string lpszEntry); + } + + public class Phonebook + { + public static void CreateEntry(string entry, NativeHelpers.RASENTRYW details) + { + UInt32 res = NativeMethods.RasSetEntryPropertiesW(null, entry, details, + details.dwSize, IntPtr.Zero, 0); + + if (res != 0) + throw new Exception(String.Format("RasSetEntryPropertiesW({0}) failed {1}", entry, res)); + } + + public static void DeleteEntry(string entry) + { + UInt32 res = NativeMethods.RasDeleteEntryW(null, entry); + if (res != 0) + throw new Exception(String.Format("RasDeleteEntryW({0}) failed {1}", entry, res)); + } + + public static NativeHelpers.RASENTRYW GetEntry(string entry) + { + NativeHelpers.RASENTRYW details = new NativeHelpers.RASENTRYW(); + UInt32 dwEntryInfoSize = details.dwSize; + UInt32 dwDeviceInfoSize = 0; + + UInt32 res = NativeMethods.RasGetEntryPropertiesW(null, entry, details, ref dwEntryInfoSize, + IntPtr.Zero, ref dwDeviceInfoSize); + + if (res != 0) + throw new Exception(String.Format("RasGetEntryPropertiesW({0}) failed {1}", entry, res)); + + return details; + } + + public static bool IsValidEntry(string entry) + { + // 183 == ENTRY_ALREADY_EXISTS + return NativeMethods.RasValidateEntryNameW(null, entry) == 183; + } + } +} +'@ + +$add_type_params = @{ + Reference = $win_ras_invoke + AnsibleModule = $module +} +# We need to set a custom compile option when running on Server 2008 due to the change in the RASENTRYW structure +$os_version = [Version](Get-Item -LiteralPath $env:SystemRoot\System32\kernel32.dll).VersionInfo.ProductVersion +if ($os_version -lt [Version]"6.1") { + $add_type_params.CompileSymbols = @("LONGHORN") +} +Add-CSharpType @add_type_params + +$exists = [Ansible.WinPhonebookEntry.Phonebook]::IsValidEntry($name) +if ($exists) { + $entry = [Ansible.WinPhonebookEntry.Phonebook]::GetEntry($name) + $module.Result.guid = $entry.guidId +} + +if ($state -eq "present") { + # Convert the input values to enum values + $expected_type = switch ($type) { + "broadband" { [Ansible.WinPhonebookEntry.NativeHelpers+RasEntryTypes]::RASET_Broadband } + "direct" { [Ansible.WinPhonebookEntry.NativeHelpers+RasEntryTypes]::RASET_Direct } + "phone" { [Ansible.WinPhonebookEntry.NativeHelpers+RasEntryTypes]::RASET_Phone } + "vpn" { [Ansible.WinPhonebookEntry.NativeHelpers+RasEntryTypes]::RASET_Vpn } + } + + $expected_framing_protocol = switch ($framing_protocol) { + "ppp" { [Ansible.WinPhonebookEntry.NativeHelpers+RasFramingProtocol]::RASFP_Ppp } + "ras" { [Ansible.WinPhonebookEntry.NativeHelpers+RasFramingProtocol]::RASFP_Ras } + "slip" { [Ansible.WinPhonebookEntry.NativeHelpers+RasFramingProtocol]::RASFP_Slip } + } + + $expected_options1 = [System.Collections.Generic.List`1[String]]@() + $expected_options2 = [System.Collections.Generic.List`1[String]]@() + $invalid_options = [System.Collections.Generic.List`1[String]]@() + foreach ($option in $options) { + # See https://msdn.microsoft.com/en-us/25c46850-4fb7-47a9-9645-139f0e869559 for more info on the options + # TODO: some of these options are set to indicate entries in RASENTRYW, we should automatically add them + # based on the input values. + switch ($option) { + # dwfOptions + "use_country_and_area_codes" { $expected_options1.Add("RASEO_UseCountryAndAreaCode") } + "specific_ip_addr" { $expected_options1.Add("RASEO_SpecificIpAddr") } + "specific_name_servers" { $expected_options1.Add("RASEO_SpecificNameServers") } + "ip_header_compression" { $expected_options1.Add("RASEO_IpHeaderCompression") } + "remote_default_gateway" { $expected_options1.Add("RASEO_RemoteDefaultGateway") } + "disable_lcp_extensions" { $expected_options1.Add("RASEO_DisableLcpExtensions") } + "terminal_before_dial" { $expected_options1.Add("RASEO_TerminalBeforeDial") } + "terminal_after_dial" { $expected_options1.Add("RASEO_TerminalAfterDial") } + "modem_lights" { $expected_options1.Add("RASEO_ModemLights") } + "sw_compression" { $expected_options1.Add("RASEO_SwCompression") } + "require_encrypted_password" { $expected_options1.Add("RASEO_RequireEncrptedPw") } + "require_ms_encrypted_password" { $expected_options1.Add("RASEO_RequireMsEncrptedPw") } + "require_data_encryption" { $expected_options1.Add("RASEO_RequireDataEncrption") } + "network_logon" { $expected_options1.Add("RASEO_NetworkLogon") } + "use_logon_credentials" { $expected_options1.Add("RASEO_UseLogonCredentials") } + "promote_alternates" { $expected_options1.Add("RASEO_PromoteAlternates") } + "secure_local_files" { $expected_options1.Add("RASEO_SecureLocalFiles") } + "require_eap" { $expected_options1.Add("RASEO_RequireEAP") } + "require_pap" { $expected_options1.Add("RASEO_RequirePAP") } + "require_spap" { $expected_options1.Add("RASEO_RequireSPAP") } + "custom" { $expected_options1.Add("RASEO_Custom") } + "preview_phone_number" { $expected_options1.Add("RASEO_PreviewPhoneNumber") } + "shared_phone_numbers" { $expected_options1.Add("RASEO_SharedPhoneNumbers") } + "preview_user_password" { $expected_options1.Add("RASEO_PreviewUserPw") } + "preview_domain" { $expected_options1.Add("RASEO_PreviewDomain") } + "show_dialing_progress" { $expected_options1.Add("RASEO_ShowDialingProgress") } + "require_chap" { $expected_options1.Add("RASEO_RequireCHAP") } + "require_ms_chap" { $expected_options1.Add("RASEO_RequireMsCHAP") } + "require_ms_chap2" { $expected_options1.Add("RASEO_RequireMsCHAP2") } + "require_w95_ms_chap" { $expected_options1.Add("RASEO_RequireW95MSCHAP") } + "custom_script" { $expected_options1.Add("RASEO_CustomScript") } + # dwfOptions2 + "secure_file_and_print" { $expected_options2.Add("RASEO2_SecureFileAndPrint") } + "secure_client_for_ms_net" { $expected_options2.Add("RASEO2_SecureClientForMSNet") } + "dont_negotiate_multilink" { $expected_options2.Add("RASEO2_DontNegotiateMultilink") } + "dont_use_ras_credential" { $expected_options2.Add("RASEO2_DontUseRasCredentials") } + "use_pre_shared_key" { $expected_options2.Add("RASEO2_UsePreSharedKey") } + "internet" { $expected_options2.Add("RASEO2_Internet") } + "disable_nbt_over_ip" { $expected_options2.Add("RASEO2_DisableNbtOverIP") } + "use_global_device_settings" { $expected_options2.Add("RASEO2_UseGlobalDeviceSettings") } + "reconnect_if_dropped" { $expected_options2.Add("RASEO2_ReconnectIfDropped") } + "share_phone_numbers" { $expected_options2.Add("RASEO2_SharePhoneNumbers") } + "secure_routing_compartment" { $expected_options2.Add("RASEO2_SecureRoutingCompartment") } + "use_typical_settings" { $expected_options2.Add("RASEO2_UseTypicalSettings") } + "ipv6_specific_name_servers" { $expected_options2.Add("RASEO2_IPv6SpecificNameServers") } + "ipv6_remote_default_gateway" { $expected_options2.Add("RASEO2_IPv6RemoteDefaultGateway") } + "register_ip_with_dns" { $expected_options2.Add("RASEO2_RegisterIpWithDNS") } + "use_dns_suffix_for_registration" { $expected_options2.Add("RASEO2_UseDNSSuffixForRegistration") } + "ipv4_explicit_metric" { $expected_options2.Add("RASEO2_IPv4ExplicitMetric") } + "ipv6_explicit_metric" { $expected_options2.Add("RASEO2_IPv6ExplicitMetric") } + "disable_ike_name_eku_check" { $expected_options2.Add("RASEO2_DisableIKENameEkuCheck") } + # TODO: Version check for the below, OS Version >= 6.1 + "disable_class_based_static_route" { $expected_options2.Add("RASEO2_DisableClassBasedStaticRoute") } + "specific_ipv6_addr" { $expected_options2.Add("RASEO2_SpecificIPv6Addr") } + "disable_mobility" { $expected_options2.Add("RASEO2_DisableMobility") } + "require_machine_certificates" { $expected_options2.Add("RASEO2_RequireMachineCertificates") } + # TODO: Version check for the below, OS Version >= 6.2 + "use_pre_shared_key_for_ikev2_initiator" { $expected_options2.Add("RASEO2_UsePreSharedKeyForIkev2Initiator") } + "use_pre_shared_key_for_ikev2_responder" { $expected_options2.Add("RASEO2_UsePreSharedKeyForIkev2Responder") } + "cache_credentials" { $expected_options2.Add("RASEO2_CacheCredentials") } + # TODO: Version check for the below, OS Version >= 6.3 + "auto_trigger_capable" { $expected_options2.Add("RASEO2_AutoTriggerCapable") } + "is_third_party_profile" { $expected_options2.Add("RASEO2_IsThirdPartyProfile") } + "auth_type_is_otp" { $expected_options2.Add("RASEO2_AuthTypeIsOtp") } + # TODO: Version check for the below, OS Version >= 10.0 + "is_always_on" { $expected_options2.Add("RASEO2_IsAlwaysOn") } + "is_private_network" { $expected_options2.Add("RASEO2_IsPrivateNetwork") } + default { $invalid_options.Add($option) } + } + } + if ($invalid_options.Count -gt 0) { + $module.FailJson("Encountered invalid options: $($invalid_options -join ", ")") + } + $expected_options1 = [Ansible.WinPhonebookEntry.NativeHelpers+RasEntryOptions]($expected_options1 -join ", ") + $expected_options2 = [Ansible.WinPhonebookEntry.NativeHelpers+RasEntryOptions2]($expected_options2 -join ", ") + + $property_map = @{ + szDeviceName = $device_name + szDeviceType = $device_type + dwFramingProtocol = $expected_framing_protocol + dwfOptions = $expected_options1 + dwfOptions2 = $expected_options2 + dwType = $expected_type + } + + if (-not $exists) { + $entry = New-Object -TypeName Ansible.WinPhonebookEntry.NativeHelpers+RASENTRYW + foreach ($kvp in $property_map.GetEnumerator()) { + $entry."$($kvp.Key)" = $kvp.Value + } + + [Ansible.WinPhonebookEntry.Phonebook]::CreateEntry($name, $entry) + $module.Result.changed = $true + + # Once created we then get the entry object again to retrieve the unique GUID ID to return + $entry = [Ansible.WinPhonebookEntry.Phonebook]::GetEntry($name) + $module.Result.guid = $entry.guidId + } else { + $entry = [Ansible.WinPhonebookEntry.Phonebook]::GetEntry($name) + $changed = $false + foreach ($kvp in $property_map.GetEnumerator()) { + $key = $kvp.Key + $actual_value = $entry.$key + if ($actual_value -ne $kvp.Value) { + $entry.$key = $kvp.Value + $changed = $true + } + } + + if ($changed) { + [Ansible.WinPhonebookEntry.Phonebook]::CreateEntry($name, $entry) + $module.Result.changed = $true + } + } +} else { + if ($exists) { + [Ansible.WinPhonebookEntry.Phonebook]::DeleteEntry($name) + $module.Result.changed = $true + } +} + +$module.ExitJson() diff --git a/test/integration/targets/win_inet_proxy/tasks/main.yml b/test/integration/targets/win_inet_proxy/tasks/main.yml new file mode 100644 index 00000000000..f92a60eab57 --- /dev/null +++ b/test/integration/targets/win_inet_proxy/tasks/main.yml @@ -0,0 +1,16 @@ +--- +- name: make sure we start the tests with the defaults + win_inet_proxy: + +- block: + - name: run tests + include_tasks: tests.yml + + always: + - name: reset proxy back to defaults + win_inet_proxy: + + - name: remove phonebook entry + win_phonebook_entry: + name: Ansible Test Dialup + state: absent diff --git a/test/integration/targets/win_inet_proxy/tasks/tests.yml b/test/integration/targets/win_inet_proxy/tasks/tests.yml new file mode 100644 index 00000000000..47ddfbe2e80 --- /dev/null +++ b/test/integration/targets/win_inet_proxy/tasks/tests.yml @@ -0,0 +1,308 @@ +--- +- name: ensure we fail when proxy is not set with bypass + win_inet_proxy: + bypass: abc + register: fail_bypass + failed_when: 'fail_bypass.msg != "missing parameter(s) required by ''bypass'': proxy"' + +- name: ensure we fail if an invalid protocol is specified + win_inet_proxy: + proxy: + fail1: fail + fail2: fail + register: fail_proxy + failed_when: 'fail_proxy.msg != "Invalid keys found in proxy: fail1, fail2. Valid keys are http, https, ftp, socks."' + +- name: ensure we fail if invalid value is set + win_inet_proxy: + proxy: fake=proxy + register: fail_invalid + failed_when: fail_invalid.msg != "Unknown error when trying to set auto_config_url '', proxy 'fake=proxy', or bypass ''" + +- name: ensure we fail if an invalid connection is set + win_inet_proxy: + connection: Fake Connection + register: fail_connection + failed_when: fail_connection.msg != "The connection 'Fake Connection' does not exist." + +- name: check proxy is still set to Direct access + win_inet_proxy_info: + register: fail_invalid_actual + failed_when: fail_invalid_actual.proxy == 'fake=proxy' + +- name: disable auto detect (check) + win_inet_proxy: + auto_detect: no + register: disable_auto_detect_check + check_mode: yes + +- name: get result of disable auto detect (check) + win_inet_proxy_info: + register: disable_auto_detect_actual_check + +- name: assert disable auto detect (check) + assert: + that: + - disable_auto_detect_check is changed + - disable_auto_detect_actual_check.auto_detect + +- name: disable auto detect + win_inet_proxy: + auto_detect: no + register: disable_auto_detect + +- name: get result of disable auto detect + win_inet_proxy_info: + register: disable_auto_detect_actual + +- name: assert disable auto detect + assert: + that: + - disable_auto_detect is changed + - not disable_auto_detect_actual.auto_detect + +- name: disable auto detect (idempotent) + win_inet_proxy: + auto_detect: no + register: disable_auto_detect_again + +- name: assert disable auto detect (idempotent) + assert: + that: + - not disable_auto_detect_again is changed + +- name: set auto config url + win_inet_proxy: + auto_config_url: http://ansible.com/proxy.pac + register: set_auto_url + +- name: get result of set auto config url + win_inet_proxy_info: + register: set_auto_url_actual + +- name: assert set auto config url + assert: + that: + - set_auto_url is changed + - set_auto_url_actual.auto_detect + - set_auto_url_actual.auto_config_url == 'http://ansible.com/proxy.pac' + +- name: set auto config url (idempotent) + win_inet_proxy: + auto_config_url: http://ansible.com/proxy.pac + register: set_auto_url_again + +- name: set auto config url (idempotent) + assert: + that: + - not set_auto_url_again is changed + +- name: set a proxy using a string + win_inet_proxy: + proxy: proxyhost + register: proxy_str + +- name: get result of set a proxy using a string + win_inet_proxy_info: + register: proxy_str_actual + +- name: assert set a proxy using a string + assert: + that: + - proxy_str is changed + - proxy_str_actual.auto_detect + - proxy_str_actual.auto_config_url == None + - proxy_str_actual.proxy == 'proxyhost' + +- name: set a proxy using a string (idempotent) + win_inet_proxy: + proxy: proxyhost + register: proxy_str_again + +- name: assert set a proxy using a string (idempotent) + assert: + that: + - not proxy_str_again is changed + +- name: change a proxy and set bypass + win_inet_proxy: + proxy: proxyhost:8080 + bypass: + - abc + - def + - + register: change_proxy + +- name: get result of change a proxy and set bypass + win_inet_proxy_info: + register: change_proxy_actual + +- name: assert change a proxy and set bypass + assert: + that: + - change_proxy is changed + - change_proxy_actual.proxy == 'proxyhost:8080' + - change_proxy_actual.bypass == 'abc;def;' + +- name: change a proxy and set bypass (idempotent) + win_inet_proxy: + proxy: proxyhost:8080 + bypass: abc,def, + register: change_proxy_again + +- name: assert change a proxy and set bypass (idempotent) + assert: + that: + - not change_proxy_again is changed + +- name: change bypass list + win_inet_proxy: + proxy: proxyhost:8080 + bypass: + - abc + - <-loopback> + register: change_bypass + +- name: get reuslt of change bypass list + win_inet_proxy_info: + register: change_bypass_actual + +- name: assert change bypass list + assert: + that: + - change_bypass is changed + - change_bypass_actual.proxy == 'proxyhost:8080' + - change_bypass_actual.bypass == 'abc;<-loopback>' + +- name: remove proxy without options + win_inet_proxy: + register: remove_proxy + +- name: get result of remove proxy without options + win_inet_proxy_info: + register: remove_proxy_actual + +- name: assert remove proxy without options + assert: + that: + - remove_proxy is changed + - remove_proxy_actual.auto_detect == True + - remove_proxy_actual.auto_config_url == None + - remove_proxy_actual.proxy == None + - remove_proxy_actual.bypass == None + +- name: remove proxy without options (idempotent) + win_inet_proxy: + register: remove_proxy_again + +- name: assert remove proxy without options (idempotent) + assert: + that: + - not remove_proxy_again is changed + +- name: set proxy with dictionary + win_inet_proxy: + proxy: + http: proxy:8080 + https: proxy:8443 + ftp: proxy:821 + socks: proxy:888 + register: set_dict + +- name: get result of set proxy with dictionary + win_inet_proxy_info: + register: set_dict_actual + +- name: assert set proxy with dictionary + assert: + that: + - set_dict is changed + - set_dict_actual.proxy == 'http=proxy:8080;https=proxy:8443;ftp=proxy:821;socks=proxy:888' + +- name: set proxy protocol with str + win_inet_proxy: + proxy: http=proxy:8080;https=proxy:8443;ftp=proxy:821;socks=proxy:888 + register: set_str_protocol + +- name: assert set proxy protocol with str + assert: + that: + - not set_str_protocol is changed + +- name: remove proxy with empty string + win_inet_proxy: + proxy: '' + register: remove_empty_str + +- name: get result of remove proxy with empty string + win_inet_proxy_info: + register: remove_empty_str_actual + +- name: assert remove proxy with empty string + assert: + that: + - remove_empty_str is changed + - remove_empty_str_actual.proxy == None + +- name: create test phonebook entry + win_phonebook_entry: + name: Ansible Test Dialup + device_type: pppoe + device_name: WAN Miniport (PPPOE) + framing_protocol: ppp + options: + - remote_default_gateway + - require_pap + - internet + type: broadband + state: present + +- name: set proxy for specific connection + win_inet_proxy: + connection: Ansible Test Dialup + auto_detect: no + auto_config_url: proxy.com + proxy: proxyhost:8080 + bypass: proxyhost + register: set_connection + +- name: get result for set proxy for specific connection + win_inet_proxy_info: + connection: Ansible Test Dialup + register: set_connection_actual + +- name: get result for LAN connection proxy + win_inet_proxy_info: + register: set_connection_lan_actual + +- name: assert set proxy for specific connection + assert: + that: + - set_connection is changed + - set_connection_actual.auto_detect == False + - set_connection_actual.auto_config_url == 'proxy.com' + - set_connection_actual.proxy == 'proxyhost:8080' + - set_connection_actual.bypass == 'proxyhost' + - set_connection_lan_actual.auto_detect == True + - set_connection_lan_actual.auto_config_url == None + - set_connection_lan_actual.proxy == None + - set_connection_lan_actual.bypass == None + +- name: remove proxy for specific connection + win_inet_proxy: + connection: Ansible Test Dialup + register: remove_connection + +- name: get result of remove proxy for specific connection + win_inet_proxy_info: + connection: Ansible Test Dialup + register: remove_connection_actual + +- name: assert remove proxy for specific connection + assert: + that: + - remove_connection is changed + - remove_connection_actual.auto_detect == True + - remove_connection_actual.auto_config_url == None + - remove_connection_actual.proxy == None + - remove_connection_actual.bypass == None