#!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"; elements = "str"; no_log = $false }
        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()

