// Copyright 2018 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.
//
//go:build windows
// +build windows

// utility package implements all the shared methods between clients.
package utility

import (
	"fmt"
	"net"
	"syscall"
	"unsafe"

	"golang.org/x/sys/windows"

	"github.com/aws/amazon-ssm-agent/agent/appconfig"
	"github.com/aws/amazon-ssm-agent/agent/context"
	"github.com/aws/amazon-ssm-agent/agent/log"
	"golang.org/x/sys/windows/registry"
	"golang.org/x/sys/windows/svc"
	"golang.org/x/sys/windows/svc/mgr"
)

const (
	// Success code for NetUser function calls
	nerrSuccess = 0
	// Nil is a zero value for pointer types
	nilPointerValue = 0
	// User level privilege
	userPrivUser = 1
	// Sever name for local machine
	serverNameLocalMachine = 0
	// Indicates that the logon script is executed
	ufScript = 1
	// Indicates that the user's account is disabled.
	USER_UF_ACCOUNTDISABLE = 2
	// Indicates running state of a service
	serviceRunning = 4
	// Active Directory Domain Controller service name
	addsServiceName = "NTDS"

	// Information level for domain and name of the new local group member.
	levelForLocalGroupMembersInfo3 = 3
	// Information level for password data
	levelForUserInfo1003 = 1003
	// Information level for user account attributes
	levelForUserInfo1008 = 1008
	// Level for fetching USER_INFO_1 structure data
	levelForUserInfo1 = 1

	// Windows error code for user not found
	errCodeForUserNotFound = 2221
	// Winows error code for user account already exists
	errCodeForUserAlreadyExists = 2224
	// Windows error code for account name already a member of the group
	errCodeForUserAlreadyGroupMember = 1378

	logon32LogonNetwork    = uintptr(3)
	logon32ProviderDefault = uintptr(0)
)

type USER_INFO_1003 struct {
	Usri1003_password *uint16
}

type USER_INFO_1008 struct {
	Usri1008_flags uint32
}

type USER_INFO_1 struct {
	Usri1_name         *uint16
	Usri1_password     *uint16
	Usri1_password_age uint32
	Usri1_priv         uint32
	Usri1_home_dir     *uint16
	Usri1_comment      *uint16
	Usri1_flags        uint32
	Usri1_script_path  *uint16
}

type USER1_FLAGS struct {
	UF_ACCOUNTDISABLE *uint16
}

type LOCALGROUP_MEMBERS_INFO_3 struct {
	Lgrmi3_domainandname *uint16
}

var (
	modNetapi32             = windows.NewLazySystemDLL("netapi32.dll")
	netUserSetInfo          = modNetapi32.NewProc("NetUserSetInfo")
	netUserGetInfo          = modNetapi32.NewProc("NetUserGetInfo")
	netUserAdd              = modNetapi32.NewProc("NetUserAdd")
	netApiBufferFree        = modNetapi32.NewProc("NetApiBufferFree")
	netLocalGroupAddMembers = modNetapi32.NewProc("NetLocalGroupAddMembers")
	advapi32                = windows.NewLazySystemDLL("advapi32.dll")
	userenv                 = windows.NewLazySystemDLL("userenv.dll")
	logonProc               = advapi32.NewProc("LogonUserW")
	loadUserProfileW        = userenv.NewProc("LoadUserProfileW")
	unloadUserProfile       = userenv.NewProc("UnloadUserProfile")
	getUserProfileDirectory = userenv.NewProc("GetUserProfileDirectoryW")
	duplicateToken          = advapi32.NewProc("DuplicateToken")
	impersonateProc         = advapi32.NewProc("ImpersonateLoggedOnUser")
	revertSelfProc          = advapi32.NewProc("RevertToSelf")
)

type ProfileInfo struct {
	Size        uint32
	Flags       uint32
	UserName    *uint16
	ProfilePath *uint16
	DefaultPath *uint16
	ServerName  *uint16
	PolicyPath  *uint16
	Profile     syscall.Handle
}

// AddNewUser adds new user using NetUserAdd function of netapi32.dll on local machine
func (u *SessionUtil) AddNewUser(username string, password string) (userExists bool, err error) {
	var (
		errParam uint32
		uPointer *uint16
		pPointer *uint16
	)

	if uPointer, err = syscall.UTF16PtrFromString(username); err != nil {
		return false, fmt.Errorf("Unable to encode username to UTF16")
	}

	if pPointer, err = syscall.UTF16PtrFromString(password); err != nil {
		return false, fmt.Errorf("Unable to encode password to UTF16")
	}

	uInfo := USER_INFO_1{
		Usri1_name:     uPointer,
		Usri1_password: pPointer,
		Usri1_priv:     userPrivUser,
		Usri1_flags:    ufScript,
	}

	ret, _, _ := netUserAdd.Call(
		uintptr(serverNameLocalMachine),
		uintptr(uint32(levelForUserInfo1)),
		uintptr(unsafe.Pointer(&uInfo)),
		uintptr(unsafe.Pointer(&errParam)),
	)

	if ret != nerrSuccess {
		if ret == errCodeForUserAlreadyExists {
			userExists = true
			err = nil
		} else {
			userExists = false
			err = fmt.Errorf("NetUserAdd call failed. Error Code: %d", ret)
		}
	}
	return
}

// IsInstanceADomainController returns true if Active Directory Domain Controller service is running on local instance, false otherwise
func (u *SessionUtil) IsInstanceADomainController(log log.T) (isDCServiceRunning bool) {

	var err error
	if isDCServiceRunning, err = isServiceRunning(addsServiceName); err != nil {
		log.Debugf("Instance is not running active directory domain controller service. %v", err)
		// In case of error always assume instance is not a domain controller machine and return false.
		return false
	}

	return isDCServiceRunning
}

// isServiceRunning returns if given service is running
func isServiceRunning(serviceName string) (isServiceRunning bool, err error) {
	var (
		winManager *mgr.Mgr
		service    *mgr.Service
		status     svc.Status
	)

	// connect to windows service control manager
	if winManager, err = mgr.Connect(); err != nil {
		return false, fmt.Errorf("Something went wrong while trying to connect to Service Manager - %v", err)
	}
	if winManager != nil {
		defer winManager.Disconnect()
	}

	// get handle of service
	if service, err = winManager.OpenService(serviceName); err != nil {
		return false, fmt.Errorf("Opening service failed with error %v", err)
	}
	if service != nil {
		defer service.Close()
	}

	// query to check status of service
	if status, err = service.Query(); err != nil {
		return false, fmt.Errorf("Error when trying to find out if service is running, %v", err)
	}

	if status.State == serviceRunning {
		return true, nil
	}
	return false, nil
}

// AddUserToLocalAdministratorsGroup adds user to local built in administrators group using NetLocalGroupAddMembers function of netapi32.dll
func (u *SessionUtil) AddUserToLocalAdministratorsGroup(username string) (adminGroupName string, err error) {
	var (
		uPointer *uint16
		gPointer *uint16
	)

	if adminGroupName, err = u.getBuiltInAdministratorsGroupName(); err != nil {
		return
	}

	if uPointer, err = syscall.UTF16PtrFromString(username); err != nil {
		return "", fmt.Errorf("Unable to encode username to UTF16")
	}

	if gPointer, err = syscall.UTF16PtrFromString(adminGroupName); err != nil {
		return "", fmt.Errorf("Unable to encode adminGroupName to UTF16")
	}

	localGroupMembers := make([]LOCALGROUP_MEMBERS_INFO_3, 1)
	localGroupMembers[0] = LOCALGROUP_MEMBERS_INFO_3{
		Lgrmi3_domainandname: uPointer,
	}

	ret, _, _ := netLocalGroupAddMembers.Call(
		uintptr(serverNameLocalMachine),
		uintptr(unsafe.Pointer(gPointer)),
		uintptr(uint32(levelForLocalGroupMembersInfo3)),
		uintptr(unsafe.Pointer(&localGroupMembers[0])),
		uintptr(uint32(len(localGroupMembers))),
	)

	// return error if API call failed and user is NOT a group member
	if ret != nerrSuccess && ret != errCodeForUserAlreadyGroupMember {
		err = fmt.Errorf("NetLocalGroupAddMembers call failed. Error Code: %d", ret)
	}

	return
}

// getBuiltInAdministratorsGroupName fetches builtin local administrators group name
func (u *SessionUtil) getBuiltInAdministratorsGroupName() (adminGroupName string, err error) {
	var sid *windows.SID
	if err = windows.AllocateAndInitializeSid(
		&windows.SECURITY_NT_AUTHORITY,
		2,
		windows.SECURITY_BUILTIN_DOMAIN_RID,
		windows.DOMAIN_ALIAS_RID_ADMINS,
		0,
		0,
		0,
		0,
		0,
		0,
		&sid); err != nil {
		return
	}

	// Passing system name as empty string and LookupAccountSidW will translate it to local system
	if adminGroupName, _, _, err = sid.LookupAccount(""); err != nil {
		return
	}

	return
}

// ChangePassword changes password for given user using NetUserSetInfo function of netapi32.dll on local machine
func (u *SessionUtil) ChangePassword(username string, password string) (userExists bool, err error) {
	var (
		errParam uint32
		uPointer *uint16
		pPointer *uint16
	)

	if uPointer, err = syscall.UTF16PtrFromString(username); err != nil {
		return userExists, fmt.Errorf("Unable to encode username to UTF16")
	}

	if pPointer, err = syscall.UTF16PtrFromString(password); err != nil {
		return userExists, fmt.Errorf("Unable to encode password to UTF16")
	}

	ret, _, _ := netUserSetInfo.Call(
		uintptr(serverNameLocalMachine),
		uintptr(unsafe.Pointer(uPointer)),
		uintptr(uint32(levelForUserInfo1003)),
		uintptr(unsafe.Pointer(&USER_INFO_1003{Usri1003_password: pPointer})),
		uintptr(unsafe.Pointer(&errParam)),
	)

	if ret == nerrSuccess {
		return true, nil
	} else if ret == errCodeForUserNotFound {
		return false, nil
	}
	return userExists, fmt.Errorf("NetUserSetInfo call failed. %d", ret)
}

// ResetPasswordIfDefaultUserExists resets default RunAs user password if user exists (only for agent starts)
func (u *SessionUtil) ResetPasswordIfDefaultUserExists(context context.T) (err error) {
	var userExists bool
	if userExists, err = u.doesUserExist(appconfig.DefaultRunAsUserName); err != nil {
		return fmt.Errorf("Error occured while checking if %s user exists, %v", appconfig.DefaultRunAsUserName, err)
	}

	if userExists {
		log := context.Log()
		log.Infof("%s already exists. Resetting password.", appconfig.DefaultRunAsUserName)
		newPassword, err := u.GeneratePasswordForDefaultUser()
		if err != nil {
			return err
		}
		if _, err = u.ChangePassword(appconfig.DefaultRunAsUserName, newPassword); err != nil {
			return fmt.Errorf("Error occured while changing password for %s, %v", appconfig.DefaultRunAsUserName, err)
		}
	}

	return nil
}

// doesUserExist checks if given user already exists using NetUserGetInfo function of netapi32.dll on local machine
func (u *SessionUtil) doesUserExist(username string) (bool, error) {
	var (
		uPointer         *uint16
		userInfo1Pointer uintptr
		err              error
		userExists       bool
	)

	if uPointer, err = syscall.UTF16PtrFromString(username); err != nil {
		return false, fmt.Errorf("Unable to encode username to UTF16")
	}

	ret, _, _ := netUserGetInfo.Call(
		uintptr(serverNameLocalMachine),
		uintptr(unsafe.Pointer(uPointer)),
		uintptr(uint32(levelForUserInfo1)),
		uintptr(unsafe.Pointer(&userInfo1Pointer)),
	)

	if userInfo1Pointer != nilPointerValue {
		defer netApiBufferFree.Call(uintptr(unsafe.Pointer(userInfo1Pointer)))
	}

	if ret == nerrSuccess {
		userExists = true
	} else if uint(ret) == errCodeForUserNotFound {
		userExists = false
	} else {
		userExists = false
		err = fmt.Errorf("NetUserGetInfo call failed. %d", ret)
	}

	return userExists, err
}

// createLocalAdminUser creates a local OS user on the instance with admin permissions.
func (u *SessionUtil) CreateLocalAdminUser(log log.T) (newPassword string, err error) {
	if u.IsInstanceADomainController(log) {
		return "", fmt.Errorf("Instance is running active directory domain controller service. Disable the service to continue to use session manager.")
	}

	if newPassword, err = u.GeneratePasswordForDefaultUser(); err != nil {
		return
	}

	var userExists bool
	if userExists, err = u.AddNewUser(appconfig.DefaultRunAsUserName, newPassword); err != nil {
		return "", fmt.Errorf("Failed to create %s: %v", appconfig.DefaultRunAsUserName, err)
	}

	if userExists {
		log.Infof("%s already exists.", appconfig.DefaultRunAsUserName)
		return
	}
	log.Infof("Successfully created %s", appconfig.DefaultRunAsUserName)

	var adminGroupName string
	if adminGroupName, err = u.AddUserToLocalAdministratorsGroup(appconfig.DefaultRunAsUserName); err != nil {
		return newPassword, fmt.Errorf("Failed to add %s to local admin group: %v", appconfig.DefaultRunAsUserName, err)
	}
	log.Infof("Added %s to %s group", appconfig.DefaultRunAsUserName, adminGroupName)

	return
}

// Impersonate attempts to impersonate the user.
func (u *SessionUtil) Impersonate(log log.T, user string, pass string) error {
	token, err := logonUser(user, pass)
	if err != nil {
		return err
	}
	defer mustCloseHandle(log, token)
	if rc, _, ec := syscall.Syscall(impersonateProc.Addr(), 1, uintptr(token), 0, 0); rc == 0 {
		return error(ec)
	}
	return nil
}

// LoadUserProfile loads user profile and return user handle to user's registry hive
func (u *SessionUtil) LoadUserProfile(user string, pass string) (syscall.Token, syscall.Handle, error) {
	token, err := logonUser(user, pass)
	if err != nil {
		return 0, 0, err
	}

	var ret uintptr
	var lUser, _ = syscall.UTF16PtrFromString(user)
	var profileInfo = ProfileInfo{UserName: lUser}
	var profileInfoSize = unsafe.Sizeof(profileInfo)
	profileInfo.Size = uint32(profileInfoSize)
	if ret, _, err = loadUserProfileW.Call(uintptr(token), uintptr(unsafe.Pointer(&profileInfo))); ret == 0 {
		return 0, 0, err
	}

	return token, profileInfo.Profile, nil
}

// logonUser attempts to log a user on to the local computer to generate a token.
func logonUser(user, pass string) (token syscall.Token, err error) {
	// ".\0" meaning "this computer:
	domain := [2]uint16{uint16('.'), 0}

	var pu, pp []uint16
	if pu, err = syscall.UTF16FromString(user); err != nil {
		return
	}
	if pp, err = syscall.UTF16FromString(pass); err != nil {
		return
	}

	if rc, _, ec := syscall.Syscall6(logonProc.Addr(), 6,
		uintptr(unsafe.Pointer(&pu[0])),
		uintptr(unsafe.Pointer(&domain[0])),
		uintptr(unsafe.Pointer(&pp[0])),
		logon32LogonNetwork,
		logon32ProviderDefault,
		uintptr(unsafe.Pointer(&token))); rc == 0 {
		err = error(ec)
	}
	return
}

// UnloadUserProfile attempts to unload user profile
func (u *SessionUtil) UnloadUserProfile(log log.T, token syscall.Token, profile syscall.Handle) {
	var ret uintptr
	var err error
	if ret, _, err = unloadUserProfile.Call(uintptr(token), uintptr(profile)); ret == 0 {
		log.Debugf("unloading of user profile failed: ret: %d, %v", ret, err)
	}
	defer mustCloseHandle(log, token)
}

// EnablePowerShellTranscription enabled PowerShell's transcript logging by modifying user's registry hive
func (u *SessionUtil) EnablePowerShellTranscription(transcriptFilePath string, keyHandle syscall.Handle) (err error) {
	winkey, err := registry.OpenKey(registry.Key(keyHandle), `Software\Policies\Microsoft\Windows`, registry.CREATE_SUB_KEY)
	if err != nil {
		return fmt.Errorf("error opening Windows key: %v", err)
	}
	defer winkey.Close()

	powerShellKey, alreadyExists, err := registry.CreateKey(winkey, `PowerShell`, registry.SET_VALUE)
	if err != nil && !alreadyExists {
		return fmt.Errorf("error creating Powershell key: %v", err)
	}
	defer powerShellKey.Close()

	transcriptionKey, alreadyExists, err := registry.CreateKey(powerShellKey, `Transcription`, registry.SET_VALUE)
	if err != nil && !alreadyExists {
		return fmt.Errorf("error creating Transcription key: %v", err)

	}
	defer transcriptionKey.Close()

	err = transcriptionKey.SetDWordValue("EnableTranscripting", 1)
	if err != nil {
		return fmt.Errorf("error setting value for EnableTranscripting key: %v", err)
	}

	err = transcriptionKey.SetStringValue("OutputDirectory", transcriptFilePath)
	if err != nil {
		return fmt.Errorf("error setting value for OutputDirectory key: %v", err)
	}

	return
}

// revertToSelf reverts the impersonation process.
func (u *SessionUtil) RevertToSelf() error {
	if rc, _, ec := syscall.Syscall(revertSelfProc.Addr(), 0, 0, 0, 0); rc == 0 {
		return error(ec)
	}
	return nil
}

// mustCloseHandle ensures to close the user token handle.
func mustCloseHandle(log log.T, token syscall.Token) {
	if err := token.Close(); err != nil {
		log.Error(err)
	}
}

// GetInstalledVersionOfPowerShell fetches installed version of PowerShell
func (u *SessionUtil) GetInstalledVersionOfPowerShell() (powerShellVersion string, err error) {
	powerShellEngineKey, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine`, registry.READ)
	if err != nil {
		err = fmt.Errorf("error fetching PowerShellEngine registry key: %v", err)
		return
	}
	defer powerShellEngineKey.Close()

	powerShellVersion, _, err = powerShellEngineKey.GetStringValue("PowerShellVersion")
	if err != nil {
		err = fmt.Errorf("error fetching PowerShellVersion registry value: %v", err)
		return
	}
	return
}

// IsSystemLevelPowerShellTranscriptionConfigured checks if PowerShell Transcript logging has been configured at system level
func (u *SessionUtil) IsSystemLevelPowerShellTranscriptionConfigured(log log.T) bool {
	powerShellTranscription, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription`, registry.READ)
	if err != nil {
		log.Debugf("error fetching PowerShell Transcription registry key: %v", err)
		return false
	}
	defer powerShellTranscription.Close()

	// return true as PowerShell Transcription is configured
	return true
}

func (u *SessionUtil) EnableLocalUser(log log.T) (err error) {
	if err = u.userDelFlags(log, appconfig.DefaultRunAsUserName, USER_UF_ACCOUNTDISABLE); err != nil {
		log.Errorf("error occurred enabling %s: %v", appconfig.DefaultRunAsUserName, err)
		return err
	}

	log.Infof("Successfully enabled %s", appconfig.DefaultRunAsUserName)
	return nil
}

func (u *SessionUtil) DisableLocalUser(log log.T) (err error) {
	if err = u.userAddFlags(log, appconfig.DefaultRunAsUserName, USER_UF_ACCOUNTDISABLE); err != nil {
		log.Errorf("error occurred disabling %s: %v", appconfig.DefaultRunAsUserName, err)
		return err
	}

	log.Infof("Successfully disabled %s", appconfig.DefaultRunAsUserName)
	return nil
}

func (u *SessionUtil) userAddFlags(log log.T, username string, flags uint32) error {
	eFlags, err := u.userGetFlags(log, username)
	if err != nil {
		return fmt.Errorf("Error while getting existing flags, %s.", err.Error())
	}
	eFlags |= flags // add supplied bits to mask.
	return u.userSetFlags(log, username, eFlags)
}

func (u *SessionUtil) userSetFlags(log log.T, username string, flags uint32) error {
	var errParam uint32
	uPointer, err := syscall.UTF16PtrFromString(username)
	if err != nil {
		return fmt.Errorf("Unable to encode username to UTF16")
	}
	ret, _, _ := netUserSetInfo.Call(
		uintptr(serverNameLocalMachine),
		uintptr(unsafe.Pointer(uPointer)),
		uintptr(uint32(levelForUserInfo1008)),
		uintptr(unsafe.Pointer(&USER_INFO_1008{Usri1008_flags: flags})),
		uintptr(unsafe.Pointer(&errParam)),
	)
	if ret != nerrSuccess {
		return fmt.Errorf("NetUserSetInfo call failed when trying to set flags. %d", ret)
	}
	return nil
}

func (u *SessionUtil) userDelFlags(log log.T, username string, flags uint32) error {
	eFlags, err := u.userGetFlags(log, username)
	if err != nil {
		return fmt.Errorf("Error while getting existing flags, %s.", err.Error())
	}
	eFlags &^= flags // clear bits we want to remove.
	return u.userSetFlags(log, username, eFlags)
}

func (u *SessionUtil) userGetFlags(log log.T, username string) (uint32, error) {
	var dataPointer uintptr
	uPointer, err := syscall.UTF16PtrFromString(username)
	if err != nil {
		return 0, fmt.Errorf("unable to encode username to UTF16")
	}
	_, _, _ = netUserGetInfo.Call(
		uintptr(serverNameLocalMachine),
		uintptr(unsafe.Pointer(uPointer)),
		uintptr(uint32(levelForUserInfo1)),
		uintptr(unsafe.Pointer(&dataPointer)),
	)
	defer netApiBufferFree.Call(dataPointer)

	if dataPointer == uintptr(0) {
		return 0, fmt.Errorf("unable to get data structure for user flags")
	}

	var data = (*USER_INFO_1)(unsafe.Pointer(dataPointer))
	log.Debugf("existing user flags: %d\r\n", data.Usri1_flags)
	return data.Usri1_flags, nil
}

// NewListener starts a new socket listener on the address.
// unix sockets are not supported in older windows versions, start tcp loopback server in such cases
func NewListener(log log.T, address string) (listener net.Listener, err error) {
	if listener, err = net.Listen("unix", address); err != nil {
		log.Debugf("Failed to open unix socket listener, %v. Starting TCP listener.", err)
		return net.Listen("tcp", "localhost:0")
	}
	return
}

// DeleteIpcTempFile resets file properties of ipcTempFile and tries deletion
func (u *SessionUtil) DeleteIpcTempFile(log log.T, sessionOrchestrationPath string) (bool, error) {
	// no deletion needed for windows since ipcTempFile properties are default and no issues during deletion
	return false, nil
}
