// 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.

//go:build freebsd || linux || netbsd || openbsd
// +build freebsd linux netbsd openbsd

// Package appconfig manages the configuration of the agent.
package appconfig

import (
	"fmt"
	"os"
	"path/filepath"
	"syscall"

	"github.com/aws/amazon-ssm-agent/agent/network/certreader"
)

var (

	// AgentExtensions specified the root folder for various kinds of downloaded content
	AgentData = "/var/lib/amazon/ssm/"

	// PackageRoot specifies the directory under which packages will be downloaded and installed
	PackageRoot = AgentData + "packages"

	// PackageLockRoot specifies the directory under which package lock files will reside
	PackageLockRoot = AgentData + "locks/packages"

	// PackagePlatform is the platform name to use when looking for packages
	PackagePlatform = "linux"

	// DaemonRoot specifies the directory where daemon registration information is stored
	DaemonRoot = AgentData + "daemons"

	// LocalCommandRoot specifies the directory where users can submit command documents offline
	LocalCommandRoot = AgentData + "localcommands"

	// LocalCommandRootSubmitted is the directory where locally submitted command documents
	// are moved when they have been picked up
	LocalCommandRootSubmitted = AgentData + "localcommands/submitted"
	LocalCommandRootCompleted = AgentData + "localcommands/completed"

	// LocalCommandRootInvalid is the directory where locally submitted command documents
	// are moved if the service cannot validate the document (generally impossible via cli)
	LocalCommandRootInvalid = AgentData + "localcommands/invalid"

	// DownloadRoot specifies the directory under which files will be downloaded
	DownloadRoot = AgentData + "download/"

	// DefaultDataStorePath represents the directory for storing system data
	DefaultDataStorePath = AgentData

	// EC2ConfigDataStorePath represents the directory for storing ec2 config data
	EC2ConfigDataStorePath = "/var/lib/amazon/ec2config/"

	// EC2ConfigSettingPath represents the directory for storing ec2 config settings
	EC2ConfigSettingPath = "/var/lib/amazon/ec2configservice/"

	// UpdaterArtifactsRoot represents the directory for storing update related information
	UpdaterArtifactsRoot = AgentData + "update/"

	// UpdaterPidLockfile represents the location of the updater lockfile
	UpdaterPidLockfile = AgentData + "update.lock"

	// DefaultPluginPath represents the directory for storing plugins in SSM
	DefaultPluginPath = AgentData + "plugins"

	// ManifestCacheDirectory represents the directory for storing all downloaded manifest files
	ManifestCacheDirectory = AgentData + "manifests"

	// List all plugin names, unfortunately golang doesn't support const arrays of strings

	// RebootExitCode that would trigger a Soft Reboot
	RebootExitCode = 194

	// Default Custom Inventory Inventory Folder
	DefaultCustomInventoryFolder = AgentData + "inventory/custom"

	// Default Session files Folder
	SessionFilesPath = AgentData + "session"

	// PowerShellPluginCommandArgs is the arguments of powershell.exe to be used by the runPowerShellScript plugin
	PowerShellPluginCommandArgs = "-f"

	// Exit Code for a command that exits before completion (generally due to timeout or cancel)
	CommandStoppedPreemptivelyExitCode = 137 // Fatal error (128) + signal for SIGKILL (9) = 137

	// RunCommandScriptName is the script name where all downloaded or provided commands will be stored
	RunCommandScriptName = "_script.sh"

	NecessaryAgentBinaryPermissionMask  os.FileMode = 0511 // Require read/execute for root, execute for all
	DisallowedAgentBinaryPermissionMask os.FileMode = 0022 // Disallow write for group and user

	// customCertificateFileName is the name of the custom certificate
	customCertificateFileName = "amazon-ssm-agent.crt"

	// SSM Agent Update download legacy path
	LegacyUpdateDownloadFolder = "/var/log/amazon/ssm/download"

	// DefaultEC2SharedCredentialsFilePath represents the filepath for storing credentials for ec2 identity
	DefaultEC2SharedCredentialsFilePath = DefaultDataStorePath + "credentials"

	// TelemetryDataStorePath specifies the directory where telemetry data is collected before sending
	TelemetryDataStorePath  = DefaultDataStorePath + "telemetry"
	DynamicConfigFolderPath = AgentData + "dynamicconfig"
)

// PowerShellPluginCommandName is the path of the powershell.exe to be used by the runPowerShellScript plugin
var PowerShellPluginCommandName string

// DefaultProgramFolder is the default folder for SSM
var DefaultProgramFolder = "/etc/amazon/ssm/"

var defaultWorkerPath = "/usr/bin/"
var DefaultSSMAgentBinaryPath = defaultWorkerPath + "amazon-ssm-agent"
var DefaultSSMAgentWorker = defaultWorkerPath + "ssm-agent-worker"
var DefaultDocumentWorker = defaultWorkerPath + "ssm-document-worker"
var DefaultSessionWorker = defaultWorkerPath + "ssm-session-worker"
var DefaultSessionLogger = defaultWorkerPath + "ssm-session-logger"

// AppConfigPath is the path of the AppConfig
var AppConfigPath = DefaultProgramFolder + AppConfigFileName

// CustomCertificatePath is the path of the custom certificate
var CustomCertificatePath = ""

// SeelogFilePath specifies the path to the seelog
var SeelogFilePath = DefaultProgramFolder + SeelogConfigFileName

var RuntimeConfigFolderPath = AgentData + "runtimeconfig"

func init() {
	/*
	   Powershell command used to be powershell in alpha versions, now it's pwsh in prod versions
	*/
	PowerShellPluginCommandName = "/usr/bin/powershell"
	if _, err := os.Stat(PowerShellPluginCommandName); err != nil {
		PowerShellPluginCommandName = "/usr/bin/pwsh"
	}

	// curdir is amazon-ssm-agent current directory path
	curdir, err := filepath.Abs(filepath.Dir(os.Args[0]))
	if err != nil {
		return
	}

	// if curdir is not default worker dir, update paths of other binaries
	if curdir != defaultWorkerPath[:len(defaultWorkerPath)-1] {
		if validateAgentBinary("ssm-session-worker", curdir) &&
			validateAgentBinary("ssm-agent-worker", curdir) {

			DefaultSessionWorker = filepath.Join(curdir, "ssm-session-worker")
			DefaultSSMAgentWorker = filepath.Join(curdir, "ssm-agent-worker")
			DefaultProgramFolder = curdir

			if validateAgentBinary("ssm-document-worker", curdir) {
				DefaultDocumentWorker = filepath.Join(curdir, "ssm-document-worker")
			}

			if validateAgentBinary("ssm-session-logger", curdir) {
				DefaultSessionLogger = filepath.Join(curdir, "ssm-session-logger")
			}

			// Check if config is available in relative path
			const relativeConfigFolder = "configuration"
			if validateRelativeConfigFile(filepath.Join(curdir, relativeConfigFolder, AppConfigFileName)) {
				AppConfigPath = filepath.Join(curdir, relativeConfigFolder, AppConfigFileName)
			}

			// Check if seelog.xml is available in relative path
			if validateRelativeConfigFile(filepath.Join(curdir, relativeConfigFolder, SeelogConfigFileName)) {
				SeelogFilePath = filepath.Join(curdir, relativeConfigFolder, SeelogConfigFileName)
			}

			// Check if certificate is available in relative path
			const relativeCertsFolder = "certs"
			if _, err := certreader.ReadCertificate(filepath.Join(curdir, relativeCertsFolder, customCertificateFileName)); err == nil {
				CustomCertificatePath = filepath.Join(curdir, relativeCertsFolder, customCertificateFileName)
			}
		}
	}
}

func validateAgentBinary(filename, curdir string) bool {
	//  binaries exist in the directory
	if info, err := os.Stat(filepath.Join(curdir, filename)); err == nil {
		mode := info.Mode()
		fileSys := info.Sys()

		if (mode.Perm() & NecessaryAgentBinaryPermissionMask) != NecessaryAgentBinaryPermissionMask {
			// Some necessary permissions are not set
			fmt.Println("Warning: Some necessary permissions are not set for: ", filename)
			return false
		}

		if (mode.Perm() & DisallowedAgentBinaryPermissionMask) != 0 {
			// Some disallowed permissions are set
			fmt.Println("Warning: Some disallowed permissions are set for: ", filename)
			return false
		}

		//binary ownership is root
		if fileSys.(*syscall.Stat_t).Uid == 0 &&
			fileSys.(*syscall.Stat_t).Gid == 0 {
			return true
		}
	}
	return false
}

func validateRelativeConfigFile(filePath string) bool {
	// Get folder info
	info, err := os.Stat(filepath.Dir(filePath))
	if err != nil {
		return false
	}

	// Get folder resource information
	folderSys := info.Sys()
	if folderSys.(*syscall.Stat_t).Uid != 0 ||
		folderSys.(*syscall.Stat_t).Gid != 0 {
		return false
	}

	// Get file info
	info, err = os.Stat(filePath)
	if err != nil {
		return false
	}

	// Check if is file
	if !info.Mode().IsRegular() {
		return false
	}

	// check if file is owned by root
	fileSys := info.Sys()
	if fileSys.(*syscall.Stat_t).Uid != 0 ||
		fileSys.(*syscall.Stat_t).Gid != 0 {
		return false
	}

	return true
}
