// 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 pluginutil implements some common functions shared by multiple plugins.
package pluginutil
import (
"errors"
"io"
"io/ioutil"
"os"
"strconv"
"strings"
"github.com/aws/amazon-ssm-agent/agent/appconfig"
"github.com/aws/amazon-ssm-agent/agent/context"
"github.com/aws/amazon-ssm-agent/agent/contracts"
"github.com/aws/amazon-ssm-agent/agent/fileutil"
"github.com/aws/amazon-ssm-agent/agent/fileutil/artifact"
"github.com/aws/amazon-ssm-agent/agent/framework/processor/executer/iohandler"
"github.com/aws/amazon-ssm-agent/agent/jsonutil"
"github.com/aws/amazon-ssm-agent/agent/log"
)
const (
defaultExecutionTimeoutInSeconds = 3600
maxExecutionTimeoutInSeconds = 172800
minExecutionTimeoutInSeconds = 5
)
// StringPrefix returns the beginning part of a string, truncated to the given limit.
func StringPrefix(input string, maxLength int, truncatedSuffix string) string {
// no need to truncate
if len(input) < maxLength {
return input
}
// truncate and add suffix
if maxLength > len(truncatedSuffix) {
pos := maxLength - len(truncatedSuffix)
return string(input[:pos]) + truncatedSuffix
}
// suffix longer than maxLength - return beginning of suffix
return truncatedSuffix[:maxLength]
}
// ReadPrefix returns the beginning data from a given Reader, truncated to the given limit.
func ReadPrefix(input io.Reader, maxLength int, truncatedSuffix string) (out string, err error) {
// read up to maxLength bytes from input
data, err := ioutil.ReadAll(io.LimitReader(input, int64(maxLength)))
if err != nil {
return
}
out = StringPrefix(string(data), maxLength, truncatedSuffix)
return
}
// ReadAll returns all data from a given Reader.
func ReadAll(input io.Reader, maxLength int, truncatedSuffix string) (out string, err error) {
// read up to maxLength bytes from input
data, err := ioutil.ReadAll(io.LimitReader(input, int64(maxLength)))
if err != nil {
return "", err
}
return string(data), nil
}
// CreateScriptFile creates a script containing the given commands.
func CreateScriptFile(log log.T, scriptPath string, runCommand []string, byteOrderMark fileutil.ByteOrderMark) (err error) {
// write source commands to file
_, err = fileutil.WriteIntoFileWithPermissionsExtended(scriptPath, strings.Join(runCommand, "\n")+"\n", appconfig.ReadWriteExecuteAccess, byteOrderMark)
if err != nil {
log.Errorf("failed to write runcommand scripts to file %v, err %v", scriptPath, err)
return
}
return
}
// DownloadFileFromSource downloads file from source
func DownloadFileFromSource(context context.T, source string, sourceHash string, sourceHashType string) (artifact.DownloadOutput, error) {
// download source and verify its integrity
downloadInput := artifact.DownloadInput{
SourceURL: source,
DestinationDirectory: appconfig.DownloadRoot,
SourceChecksums: map[string]string{
sourceHashType: sourceHash,
},
}
context.Log().Debug("Downloading file")
return artifact.Download(context, downloadInput)
}
// LoadParametersAsList returns properties as a list and appropriate PluginResult if error is encountered
func LoadParametersAsList(log log.T, prop interface{}, res *contracts.PluginResult) (properties []interface{}) {
switch prop := prop.(type) {
case []interface{}:
if err := jsonutil.Remarshal(prop, &properties); err != nil {
log.Errorf("unable to parse plugin configuration")
res.Output = "Execution failed because agent is unable to parse plugin configuration"
res.Code = 1
res.Status = contracts.ResultStatusFailed
}
default:
properties = append(properties, prop)
}
return
}
// LoadParametersAsMap returns properties as a map and appropriate PluginResult if error is encountered
func LoadParametersAsMap(log log.T, prop interface{}, out iohandler.IOHandler) (properties map[string]interface{}) {
if err := jsonutil.Remarshal(prop, &properties); err != nil {
log.Errorf("unable to parse plugin configuration")
out.AppendError("Execution failed because agent is unable to parse plugin configuration")
out.SetExitCode(1)
out.SetStatus(contracts.ResultStatusFailed)
}
return
}
// ValidateExecutionTimeout validates the supplied input interface and converts it into a valid int value.
func ValidateExecutionTimeout(log log.T, input interface{}) int {
var num int
switch input.(type) {
case string:
num = extractIntFromString(log, input.(string))
case int:
num = input.(int)
case float64:
f := input.(float64)
num = int(f)
log.Infof("Unexpected 'TimeoutSeconds' float value %v received. Applying 'TimeoutSeconds' as %v", f, num)
default:
log.Infof("Unexpected 'TimeoutSeconds' value %v received. Setting 'TimeoutSeconds' to default value %v", input, defaultExecutionTimeoutInSeconds)
}
if num < minExecutionTimeoutInSeconds || num > maxExecutionTimeoutInSeconds {
log.Infof("'TimeoutSeconds' value should be between %v and %v. Setting 'TimeoutSeconds' to default value %v", minExecutionTimeoutInSeconds, maxExecutionTimeoutInSeconds, defaultExecutionTimeoutInSeconds)
num = defaultExecutionTimeoutInSeconds
}
return num
}
// ParseRunCommand checks the command type and convert it to the string array
func ParseRunCommand(input interface{}, output []string) []string {
switch value := input.(type) {
case string:
output = append(output, value)
case []interface{}:
for _, element := range value {
output = ParseRunCommand(element, output)
}
}
return output
}
// extractIntFromString extracts a valid int value from a string.
func extractIntFromString(log log.T, input string) int {
var iNum int
var fNum float64
var err error
iNum, err = strconv.Atoi(input)
if err == nil {
return iNum
}
fNum, err = strconv.ParseFloat(input, 64)
if err == nil {
iNum = int(fNum)
log.Infof("Unexpected 'TimeoutSeconds' float value %v received. Applying 'TimeoutSeconds' as %v", fNum, iNum)
} else {
log.Errorf("Unexpected 'TimeoutSeconds' string value %v received. Setting 'TimeoutSeconds' to default value %v", input, defaultExecutionTimeoutInSeconds)
iNum = defaultExecutionTimeoutInSeconds
}
return iNum
}
// GetProxySetting returns proxy setting from registry entries
func GetProxySetting(proxyValue []string) (string, string) {
var url string
var noProxy string
for _, proxy := range proxyValue {
tmp := strings.TrimSpace(proxy)
parts := strings.Split(tmp, "=")
switch parts[0] {
case "http_proxy":
url = parts[1]
case "no_proxy":
noProxy = parts[1]
}
}
return url, noProxy
}
// ReplaceMarkedFields finds substrings delimited by the start and end markers,
// removes the markers, and replaces the text between the markers with the result
// of calling the fieldReplacer function on that text substring. For example, if
// the input string is: "a string with text marked"
// the startMarker is: ""
// the end marker is: ""
// and fieldReplacer is: strings.ToUpper
// then the output will be: "a string with TEXT marked"
func ReplaceMarkedFields(str, startMarker, endMarker string, fieldReplacer func(string) string) (newStr string, err error) {
startIndex := strings.Index(str, startMarker)
newStr = ""
for startIndex >= 0 {
newStr += str[:startIndex]
fieldStart := str[startIndex+len(startMarker):]
endIndex := strings.Index(fieldStart, endMarker)
if endIndex < 0 {
err = errors.New("Found startMarker without endMarker!")
return
}
field := fieldStart[:endIndex]
transformedField := fieldReplacer(field)
newStr += transformedField
str = fieldStart[endIndex+len(endMarker):]
startIndex = strings.Index(str, startMarker)
}
newStr += str
return newStr, nil
}
// CleanupNewLines removes all newlines from the given input
func CleanupNewLines(s string) string {
return strings.Replace(strings.Replace(s, "\n", "", -1), "\r", "", -1)
}
// CleanupJSONField converts a text to a json friendly text as follows:
// - converts multi-line fields to single line by removing all but the first line
// - escapes special characters
// - truncates remaining line to length no more than maxSummaryLength
func CleanupJSONField(field string) string {
res := field
endOfLinePos := strings.Index(res, "\n")
if endOfLinePos >= 0 {
res = res[0:endOfLinePos]
}
res = strings.Replace(res, `\`, `\\`, -1)
res = strings.Replace(res, `"`, `\"`, -1)
res = strings.Replace(res, "\t", `\t`, -1)
return res
}
// Deletes file if it exists
func CleanupFile(log log.T, file string) {
if _, err := os.Stat(file); err == nil || os.IsExist(err) {
if err = os.RemoveAll(file); err != nil {
log.Debugf("failed to delete file %v, %v", file, err.Error())
} else {
log.Debugf("deleted file %v", file)
}
} else {
log.Debugf("failed to get file info: %v", file)
}
}
// AddSingleQuotesToStringArray put single quote around each string in array
// - add escape if value has single quote in it
func AddSingleQuotesToStringArray(stringValues []string) []string {
for index := range stringValues {
stringValues[index] = addSingleQuotesToStringValue(stringValues[index])
}
return stringValues
}
func addSingleQuotesToStringValue(stringValue string) string {
stringValue = strings.Replace(stringValue, "'", "''", -1)
stringValue = "'" + stringValue + "'"
return stringValue
}