// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may not
// use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific language governing
// permissions and limitations under the License.

// Package proxyconfig to handle set/get proxy settings
package proxyconfig

import (
	"errors"
	"net/url"
	"os"
	"strings"
	"syscall"
	"unicode/utf16"
	"unsafe"

	"golang.org/x/sys/windows"

	"github.com/aws/amazon-ssm-agent/agent/log"
)

// WinHttpIEProxyConfig represents the Internet Explorer proxy configuration information
//
//	fAutoDetect: If TRUE, indicates that the Internet Explorer proxy configuration for the current user specifies "automatically detect settings".
//	lpszAutoConfigUrl: Pointer to a null-terminated Unicode string that contains the auto-configuration URL if the Internet Explorer proxy configuration for the current user specifies "Use automatic proxy configuration".
//	lpszProxy: Pointer to a null-terminated Unicode string that contains the proxy URL if the Internet Explorer proxy configuration for the current user specifies "use a proxy server".
//	lpszProxyBypass: Pointer to a null-terminated Unicode string that contains the optional proxy by-pass server list.
type WinHttpIEProxyConfig struct {
	fAutoDetect       bool
	lpszAutoConfigUrl *uint16
	lpszProxy         *uint16
	lpszProxyBypass   *uint16
}

// WinHttpProxyInfo represents the WinHTTP machine proxy configuration.
//
//	lpszProxy: Pointer to a string value that contains the proxy server list.
//	lpszProxyBypass: Pointer to a string value that contains the proxy bypass list.
type WinHttpProxyInfo struct {
	dwAccessType    uint32
	lpszProxy       *uint16
	lpszProxyBypass *uint16
}

// HttpIEProxyConfig represents the Internet Explorer proxy configuration.
//
//	auto: indicates if the 'Automatically detect settings' option in IE is enabled
//	enabled: indicates if the 'Use proxy settings for your LAN' option in IE is enabled
//	proxy: specifies the proxy addresses to use.
//	bypass: specifies addresses that should be excluded from proxy
type HttpIEProxyConfig struct {
	proxy   string
	bypass  string
	config  string
	auto    bool
	enabled bool
}

// HttpDefaultProxyConfig represents the WinHTTP machine proxy configuration.
//
//	proxy: specifies the proxy addresses to use.
//	bypass: specifies addresses that should be excluded from proxy
type HttpDefaultProxyConfig struct {
	proxy  string
	bypass string
}

// ProxySettings represents the proxy settings for https_proxy and http_proxy
type ProxySettings struct {
	HttpsProxy *url.URL
	HttpProxy  *url.URL
}

// StringFromUTF16Ptr converts a *uint16 C string to a Go String
// https://github.com/mattn/go-ieproxy/blob/master/utils.go
func StringFromUTF16Ptr(s *uint16) string {
	if s == nil {
		return ""
	}

	p := (*[1<<30 - 1]uint16)(unsafe.Pointer(s))

	// find the string length
	sz := 0
	for p[sz] != 0 {
		sz++
	}

	return string(utf16.Decode(p[:sz:sz]))
}

// For HTTP requests the agent gets the proxy address from the
// environment variables http_proxy, https_proxy and no_proxy
// https_proxy takes precedence over http_proxy for https requests.
// SetProxySettings() overwrites the environment variables using the
// Windows proxy configuration if no settings are provided in the
// registry HKLM:\SYSTEM\CurrentControlSet\Services\AmazonSSMAgent\Environment

func SetProxyConfig(log log.T) (proxySettings map[string]string) {
	var err error
	var ie HttpIEProxyConfig
	var df HttpDefaultProxyConfig
	var proxy string
	var bypass string
	var v = []string{}

	defer func() {
		if err := recover(); err != nil {
			log.Errorf("Failed while setting proxy settings: %v", err)
		}
	}()

	for _, value := range ProxyEnvVariables {
		v = append(v, value+":"+os.Getenv(value))
	}
	log.Debugf("Current proxy environment variables: %v", strings.Join(v, ";"))
	v = nil

	// IE current user proxy settings have precedence over WinHTTP machine proxy settings
	if ie, err = GetIEProxySettings(log); ie.enabled && err == nil {
		proxy = ie.proxy
		bypass = ie.bypass

		if ie.auto {
			log.Debugf("IE option 'Automatically  detect settings' is not supported")
		}

		if len(ie.config) > 0 {
			log.Debugf("IE option 'Use automatic configuration script' is not supported")
		}
	} else {
		if df, err = GetDefaultProxySettings(log); len(df.proxy) > 0 && err == nil {
			proxy = df.proxy
			bypass = df.bypass
		}
	}

	// Current registry environment variables http_proxy, https_proxy and no_proxy
	// have precedence over IE and WinHTTP machine proxy settings
	for _, value := range ProxyEnvVariables {
		if v := os.Getenv(value); len(v) > 0 {
			switch value {
			case "https_proxy", "http_proxy":
				proxy = ""
			case "no_proxy":
				bypass = ""
			}
		}
	}

	settings := ParseProxySettings(log, proxy)

	if settings.HttpsProxy != nil {
		os.Setenv("https_proxy", settings.HttpsProxy.String())
	}
	if settings.HttpProxy != nil {
		os.Setenv("http_proxy", settings.HttpProxy.String())
	}

	bypassList := ParseProxyBypass(log, bypass)
	if len(bypassList) > 0 {
		os.Setenv("no_proxy", strings.Join(bypassList, ","))
	}

	return GetProxyConfig()
}

func ParseProxyBypass(log log.T, bypass string) []string {
	var bypassList []string
	for _, f := range strings.Fields(bypass) {
		for _, s := range strings.Split(f, ";") {
			if len(s) == 0 {
				continue
			}
			parsedUrl, err := ValidateHost(s)
			if err == nil {
				bypassList = append(bypassList, parsedUrl.Host)
			} else {
				log.Warnf("SetProxySettings invalid URL or host for no_proxy: %v\n", err.Error())
			}
		}
	}

	return bypassList
}

// GetDefaultProxySettings returns the machine WinHTTP proxy configuration
func GetDefaultProxySettings(log log.T) (p HttpDefaultProxyConfig, err error) {
	winhttp := syscall.Handle(windows.NewLazySystemDLL("Winhttp.dll").Handle())

	defer syscall.FreeLibrary(winhttp)

	getDefaultProxy, err := syscall.GetProcAddress(winhttp, "WinHttpGetDefaultProxyConfiguration")
	if err != nil {
		log.Errorf("Failed to get default machine WinHTTP proxy configuration: %v", err.Error())
		return p, err
	}

	settings := new(WinHttpProxyInfo)

	ret, _, err := syscall.Syscall(uintptr(getDefaultProxy), 1, uintptr(unsafe.Pointer(settings)), 0, 0)
	if ret != 1 {
		log.Errorf("Failed to get default machine WinHTTP proxy configuration: %v", err.Error())
		return p, err
	} else {
		log.Infof("Getting WinHTTP proxy default configuration: %v", err.Error())
	}
	err = nil

	result := HttpDefaultProxyConfig{
		proxy:  StringFromUTF16Ptr(settings.lpszProxy),
		bypass: StringFromUTF16Ptr(settings.lpszProxyBypass),
	}

	log.Debugf("WinHTTP proxy default configuration: proxy:%v,bypass:%v",
		result.proxy,
		result.bypass,
	)

	return result, nil
}

// GetIEProxySettings returns the Internet Explorer proxy configuration for the current user
func GetIEProxySettings(log log.T) (p HttpIEProxyConfig, err error) {
	p.auto = false
	p.enabled = false
	winhttp := syscall.Handle(windows.NewLazySystemDLL("Winhttp.dll").Handle())

	defer syscall.FreeLibrary(winhttp)

	getIEProxy, err := syscall.GetProcAddress(winhttp, "WinHttpGetIEProxyConfigForCurrentUser")
	if err != nil {
		log.Error("Failed to get IE proxy configuration for current user: ", err.Error())
		return p, err
	}

	settings := new(WinHttpIEProxyConfig)
	ret, _, err := syscall.Syscall(uintptr(getIEProxy), 1, uintptr(unsafe.Pointer(settings)), 0, 0)
	if ret != 1 {
		log.Error("Failed to get IE proxy configuration for current user: ", err.Error())
		return p, err
	} else {
		log.Info("Getting IE proxy configuration for current user: ", err.Error())
	}
	err = nil

	result := HttpIEProxyConfig{
		proxy:   StringFromUTF16Ptr(settings.lpszProxy),
		bypass:  StringFromUTF16Ptr(settings.lpszProxyBypass),
		auto:    settings.fAutoDetect,
		enabled: settings.lpszProxy != nil,
		config:  StringFromUTF16Ptr(settings.lpszAutoConfigUrl),
	}

	log.Debugf("IE proxy configuration for current user: proxy:%v,bypass:%v,enabled:%v,automatically detect proxy settings:%v,automatic configuration script:%v",
		result.proxy,
		result.bypass,
		result.enabled,
		result.auto,
		result.config,
	)

	return result, nil
}

// ParseProxySettings parses the proxy-list string
// The Windows proxy server list contains one or more of the following strings
// ([<scheme>=][<scheme>"://"]<server>[":"<port>])
// Internet Explorer and WinHTTP support 4 proxy types for [<scheme>=]:
// http=, https=, ftp=, or socks=
func ParseProxySettings(log log.T, proxy string) ProxySettings {
	// Parse http and https proxy settings allowing only valid URL or host[:port] values
	var http, https, other *url.URL
	var err error = nil

	for _, f := range strings.Fields(proxy) {
		for _, s := range strings.Split(f, ";") {
			if len(s) == 0 {
				continue
			}

			split := strings.SplitN(s, "=", 2)
			if len(split) > 1 {
				switch split[0] {
				case "https":
					https, err = ValidateHost(split[1])
				case "http":
					http, err = ValidateHost(split[1])
				default:
					continue
				}
			} else {
				other, err = ValidateHost(split[0])
			}

			if err != nil {
				log.Warnf("ParseProxySettings, invalid URL or host for proxy: %v", err.Error())
			}
		}
	}

	result := ProxySettings{
		HttpProxy:  http,
		HttpsProxy: https,
	}

	// If no [<scheme>=] is provided http is the default option
	if https == nil && http == nil {
		result.HttpProxy = other
	} else if https != nil && http == nil {
		result.HttpProxy = other
	} else if https == nil && http != nil {
		result.HttpsProxy = other
	}

	log.Debugf("ParseProxySettings result: http_proxy:%v,https_proxy:%v",
		result.HttpProxy,
		result.HttpsProxy,
	)

	return result
}

// ValidateHost tries to parse the http_proxy and https_proxy addresses
func ValidateHost(s string) (*url.URL, error) {

	if s == "<-loopback>" || s == "<local>" {
		return nil, errors.New(s + " host not supported, skipped")
	}

	// Helps url.Parse to validate an IP for example 127.0.0.1
	if strings.Index(s, "//") == 0 {
		s = "http:" + s
	}

	// Forces http when the schema is missing
	if strings.Index(s, "://") == -1 {
		s = "http://" + s
	}

	u, err := url.Parse(s)
	if err != nil {
		return nil, errors.New(err.Error() + s + ", skipped")
	}

	return u, nil
}
