func (h *Hooks) Match(id string) *Hook {
	for i := range *h {
		if (*h)[i].ID == id {
			return &(*h)[i]
		}
	}
	return nil
}
Match iterates through Hooks and returns first one that matches the given ID,
if no hook matches the given ID, nil is returned
package hook
import (
	"bytes"
	"crypto/hmac"
	"crypto/sha1"
	"crypto/sha256"
	"crypto/sha512"
	"crypto/subtle"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"hash"
	"io/ioutil"
	"log"
	"math"
	"net"
	"net/textproto"
	"os"
	"reflect"
	"regexp"
	"strconv"
	"strings"
	"text/template"
	"time"
	"github.com/ghodss/yaml"
)
const (
	SourceHeader         string = "header"
	SourceQuery          string = "url"
	SourceQueryAlias     string = "query"
	SourcePayload        string = "payload"
	SourceRawRequestBody string = "raw-request-body"
	SourceRequest        string = "request"
	SourceString         string = "string"
	SourceEntirePayload  string = "entire-payload"
	SourceEntireQuery    string = "entire-query"
	SourceEntireHeaders  string = "entire-headers"
)
const (
	EnvNamespace string = "HOOK_"
)
type ParameterNodeError struct {
	key string
}
func (e *ParameterNodeError) Error() string {
	if e == nil {
		return "<nil>"
	}
	return fmt.Sprintf("parameter node not found: %s", e.key)
}
func IsParameterNodeError(err error) bool {
	switch err.(type) {
	case *ParameterNodeError:
		return true
	default:
		return false
	}
}
type SignatureError struct {
	Signature  string
	Signatures []string
	emptyPayload bool
}
func (e *SignatureError) Error() string {
	if e == nil {
		return "<nil>"
	}
	var empty string
	if e.emptyPayload {
		empty = " on empty payload"
	}
	if e.Signatures != nil {
		return fmt.Sprintf("invalid payload signatures %s%s", e.Signatures, empty)
	}
	return fmt.Sprintf("invalid payload signature %s%s", e.Signature, empty)
}
func IsSignatureError(err error) bool {
	switch err.(type) {
	case *SignatureError:
		return true
	default:
		return false
	}
}
type ArgumentError struct {
	Argument Argument
}
func (e *ArgumentError) Error() string {
	if e == nil {
		return "<nil>"
	}
	return fmt.Sprintf("couldn't retrieve argument for %+v", e.Argument)
}
type SourceError struct {
	Argument Argument
}
func (e *SourceError) Error() string {
	if e == nil {
		return "<nil>"
	}
	return fmt.Sprintf("invalid source for argument %+v", e.Argument)
}
type ParseError struct {
	Err error
}
func (e *ParseError) Error() string {
	if e == nil {
		return "<nil>"
	}
	return e.Err.Error()
}
func ExtractCommaSeparatedValues(source, prefix string) []string {
	parts := strings.Split(source, ",")
	values := make([]string, 0)
	for _, part := range parts {
		if strings.HasPrefix(part, prefix) {
			values = append(values, strings.TrimPrefix(part, prefix))
		}
	}
	return values
}
func ExtractSignatures(source, prefix string) []string {
	if strings.Contains(source, ",") {
		return ExtractCommaSeparatedValues(source, prefix)
	}
	return []string{
		strings.TrimPrefix(source, prefix),
	}
}
func ValidateMAC(payload []byte, mac hash.Hash, signatures []string) (string, error) {
	_, err := mac.Write(payload)
	if err != nil {
		return "", err
	}
	actualMAC := hex.EncodeToString(mac.Sum(nil))
	for _, signature := range signatures {
		if hmac.Equal([]byte(signature), []byte(actualMAC)) {
			return actualMAC, err
		}
	}
	e := &SignatureError{Signatures: signatures}
	if len(payload) == 0 {
		e.emptyPayload = true
	}
	return actualMAC, e
}
func CheckPayloadSignature(payload []byte, secret, signature string) (string, error) {
	if secret == "" {
		return "", errors.New("signature validation secret can not be empty")
	}
	signatures := ExtractSignatures(signature, "sha1=")
	return ValidateMAC(payload, hmac.New(sha1.New, []byte(secret)), signatures)
}
func CheckPayloadSignature256(payload []byte, secret, signature string) (string, error) {
	if secret == "" {
		return "", errors.New("signature validation secret can not be empty")
	}
	signatures := ExtractSignatures(signature, "sha256=")
	return ValidateMAC(payload, hmac.New(sha256.New, []byte(secret)), signatures)
}
func CheckPayloadSignature512(payload []byte, secret, signature string) (string, error) {
	if secret == "" {
		return "", errors.New("signature validation secret can not be empty")
	}
	signatures := ExtractSignatures(signature, "sha512=")
	return ValidateMAC(payload, hmac.New(sha512.New, []byte(secret)), signatures)
}
func CheckScalrSignature(r *Request, signingKey string, checkDate bool) (bool, error) {
	if r.Headers == nil {
		return false, nil
	}
	if _, ok := r.Headers["X-Signature"]; !ok {
		return false, nil
	}
	if _, ok := r.Headers["Date"]; !ok {
		return false, nil
	}
	if signingKey == "" {
		return false, errors.New("signature validation signing key can not be empty")
	}
	providedSignature := r.Headers["X-Signature"].(string)
	dateHeader := r.Headers["Date"].(string)
	mac := hmac.New(sha1.New, []byte(signingKey))
	mac.Write(r.Body)
	mac.Write([]byte(dateHeader))
	expectedSignature := hex.EncodeToString(mac.Sum(nil))
	if !hmac.Equal([]byte(providedSignature), []byte(expectedSignature)) {
		return false, &SignatureError{Signature: providedSignature}
	}
	if !checkDate {
		return true, nil
	}
	date, err := time.Parse("Mon 02 Jan 2006 15:04:05 MST", dateHeader)
	if err != nil {
		return false, err
	}
	now := time.Now()
	delta := math.Abs(now.Sub(date).Seconds())
	if delta > 300 {
		return false, &SignatureError{Signature: "outdated"}
	}
	return true, nil
}
func CheckIPWhitelist(remoteAddr, ipRange string) (bool, error) {
	ip := strings.Trim(remoteAddr, " []")
	if i := strings.LastIndex(ip, ":"); i != -1 {
		ip = ip[:i]
		ip = strings.Trim(ip, " []")
	}
	parsedIP := net.ParseIP(ip)
	if parsedIP == nil {
		return false, fmt.Errorf("invalid IP address found in remote address '%s'", remoteAddr)
	}
	for _, r := range strings.Fields(ipRange) {
		if !strings.Contains(r, "/") {
			r = r + "/32"
		}
		_, cidr, err := net.ParseCIDR(r)
		if err != nil {
			return false, err
		}
		if cidr.Contains(parsedIP) {
			return true, nil
		}
	}
	return false, nil
}
func ReplaceParameter(s string, params, value interface{}) bool {
	if params == nil {
		return false
	}
	if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
		if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
			if p := strings.SplitN(s, ".", 2); len(p) > 1 {
				index, err := strconv.ParseUint(p[0], 10, 64)
				if err != nil || paramsValueSliceLength <= int(index) {
					return false
				}
				return ReplaceParameter(p[1], params.([]interface{})[index], value)
			}
		}
		return false
	}
	if p := strings.SplitN(s, ".", 2); len(p) > 1 {
		if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
			return ReplaceParameter(p[1], pValue, value)
		}
	} else {
		if _, ok := (*params.(*map[string]interface{}))[p[0]]; ok {
			(*params.(*map[string]interface{}))[p[0]] = value
			return true
		}
	}
	return false
}
func GetParameter(s string, params interface{}) (interface{}, error) {
	if params == nil {
		return nil, errors.New("no parameters")
	}
	paramsValue := reflect.ValueOf(params)
	switch paramsValue.Kind() {
	case reflect.Slice:
		paramsValueSliceLength := paramsValue.Len()
		if paramsValueSliceLength > 0 {
			if p := strings.SplitN(s, ".", 2); len(p) > 1 {
				index, err := strconv.ParseUint(p[0], 10, 64)
				if err != nil || paramsValueSliceLength <= int(index) {
					return nil, &ParameterNodeError{s}
				}
				return GetParameter(p[1], params.([]interface{})[index])
			}
			index, err := strconv.ParseUint(s, 10, 64)
			if err != nil || paramsValueSliceLength <= int(index) {
				return nil, &ParameterNodeError{s}
			}
			return params.([]interface{})[index], nil
		}
		return nil, &ParameterNodeError{s}
	case reflect.Map:
		if v, ok := params.(map[string]interface{})[s]; ok {
			return v, nil
		}
		p := strings.SplitN(s, ".", 2)
		if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
			if len(p) > 1 {
				return GetParameter(p[1], pValue)
			}
			return pValue, nil
		}
	}
	return nil, &ParameterNodeError{s}
}
func ExtractParameterAsString(s string, params interface{}) (string, error) {
	pValue, err := GetParameter(s, params)
	if err != nil {
		return "", err
	}
	switch v := reflect.ValueOf(pValue); v.Kind() {
	case reflect.Array, reflect.Map, reflect.Slice:
		r, err := json.Marshal(pValue)
		if err != nil {
			return "", err
		}
		return string(r), nil
	default:
		return fmt.Sprintf("%v", pValue), nil
	}
}
type Argument struct {
	Source       string `json:"source,omitempty"`
	Name         string `json:"name,omitempty"`
	EnvName      string `json:"envname,omitempty"`
	Base64Decode bool   `json:"base64decode,omitempty"`
}
func (ha *Argument) Get(r *Request) (string, error) {
	var source *map[string]interface{}
	key := ha.Name
	switch ha.Source {
	case SourceHeader:
		source = &r.Headers
		key = textproto.CanonicalMIMEHeaderKey(ha.Name)
	case SourceQuery, SourceQueryAlias:
		source = &r.Query
	case SourcePayload:
		source = &r.Payload
	case SourceString:
		return ha.Name, nil
	case SourceRawRequestBody:
		return string(r.Body), nil
	case SourceRequest:
		if r == nil || r.RawRequest == nil {
			return "", errors.New("request is nil")
		}
		switch strings.ToLower(ha.Name) {
		case "remote-addr":
			return r.RawRequest.RemoteAddr, nil
		case "method":
			return r.RawRequest.Method, nil
		default:
			return "", fmt.Errorf("unsupported request key: %q", ha.Name)
		}
	case SourceEntirePayload:
		res, err := json.Marshal(&r.Payload)
		if err != nil {
			return "", err
		}
		return string(res), nil
	case SourceEntireHeaders:
		res, err := json.Marshal(&r.Headers)
		if err != nil {
			return "", err
		}
		return string(res), nil
	case SourceEntireQuery:
		res, err := json.Marshal(&r.Query)
		if err != nil {
			return "", err
		}
		return string(res), nil
	}
	if source != nil {
		return ExtractParameterAsString(key, *source)
	}
	return "", errors.New("no source for value retrieval")
}
type Header struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}
type ResponseHeaders []Header
func (h *ResponseHeaders) String() string {
	if len(*h) == 0 {
		return "name=value"
	}
	result := make([]string, len(*h))
	for idx, responseHeader := range *h {
		result[idx] = fmt.Sprintf("%s=%s", responseHeader.Name, responseHeader.Value)
	}
	return strings.Join(result, ", ")
}
func (h *ResponseHeaders) Set(value string) error {
	splitResult := strings.SplitN(value, "=", 2)
	if len(splitResult) != 2 {
		return errors.New("header flag must be in name=value format")
	}
	*h = append(*h, Header{Name: splitResult[0], Value: splitResult[1]})
	return nil
}
type HooksFiles []string
func (h *HooksFiles) String() string {
	if len(*h) == 0 {
		return "hooks.json"
	}
	return strings.Join(*h, ", ")
}
func (h *HooksFiles) Set(value string) error {
	*h = append(*h, value)
	return nil
}
type Hook struct {
	ID                                  string          `json:"id,omitempty"`
	ExecuteCommand                      string          `json:"execute-command,omitempty"`
	CommandWorkingDirectory             string          `json:"command-working-directory,omitempty"`
	ResponseMessage                     string          `json:"response-message,omitempty"`
	ResponseHeaders                     ResponseHeaders `json:"response-headers,omitempty"`
	CaptureCommandOutput                bool            `json:"include-command-output-in-response,omitempty"`
	CaptureCommandOutputOnError         bool            `json:"include-command-output-in-response-on-error,omitempty"`
	PassEnvironmentToCommand            []Argument      `json:"pass-environment-to-command,omitempty"`
	PassArgumentsToCommand              []Argument      `json:"pass-arguments-to-command,omitempty"`
	PassFileToCommand                   []Argument      `json:"pass-file-to-command,omitempty"`
	JSONStringParameters                []Argument      `json:"parse-parameters-as-json,omitempty"`
	TriggerRule                         *Rules          `json:"trigger-rule,omitempty"`
	TriggerRuleMismatchHttpResponseCode int             `json:"trigger-rule-mismatch-http-response-code,omitempty"`
	TriggerSignatureSoftFailures        bool            `json:"trigger-signature-soft-failures,omitempty"`
	IncomingPayloadContentType          string          `json:"incoming-payload-content-type,omitempty"`
	SuccessHttpResponseCode             int             `json:"success-http-response-code,omitempty"`
	HTTPMethods                         []string        `json:"http-methods"`
}
func (h *Hook) ParseJSONParameters(r *Request) []error {
	errors := make([]error, 0)
	for i := range h.JSONStringParameters {
		arg, err := h.JSONStringParameters[i].Get(r)
		if err != nil {
			errors = append(errors, &ArgumentError{h.JSONStringParameters[i]})
		} else {
			var newArg map[string]interface{}
			decoder := json.NewDecoder(strings.NewReader(string(arg)))
			decoder.UseNumber()
			err := decoder.Decode(&newArg)
			if err != nil {
				errors = append(errors, &ParseError{err})
				continue
			}
			var source *map[string]interface{}
			switch h.JSONStringParameters[i].Source {
			case SourceHeader:
				source = &r.Headers
			case SourcePayload:
				source = &r.Payload
			case SourceQuery, SourceQueryAlias:
				source = &r.Query
			}
			if source != nil {
				key := h.JSONStringParameters[i].Name
				if h.JSONStringParameters[i].Source == SourceHeader {
					key = textproto.CanonicalMIMEHeaderKey(h.JSONStringParameters[i].Name)
				}
				ReplaceParameter(key, source, newArg)
			} else {
				errors = append(errors, &SourceError{h.JSONStringParameters[i]})
			}
		}
	}
	if len(errors) > 0 {
		return errors
	}
	return nil
}
func (h *Hook) ExtractCommandArguments(r *Request) ([]string, []error) {
	args := make([]string, 0)
	errors := make([]error, 0)
	args = append(args, h.ExecuteCommand)
	for i := range h.PassArgumentsToCommand {
		arg, err := h.PassArgumentsToCommand[i].Get(r)
		if err != nil {
			args = append(args, "")
			errors = append(errors, &ArgumentError{h.PassArgumentsToCommand[i]})
			continue
		}
		args = append(args, arg)
	}
	if len(errors) > 0 {
		return args, errors
	}
	return args, nil
}
func (h *Hook) ExtractCommandArgumentsForEnv(r *Request) ([]string, []error) {
	args := make([]string, 0)
	errors := make([]error, 0)
	for i := range h.PassEnvironmentToCommand {
		arg, err := h.PassEnvironmentToCommand[i].Get(r)
		if err != nil {
			errors = append(errors, &ArgumentError{h.PassEnvironmentToCommand[i]})
			continue
		}
		if h.PassEnvironmentToCommand[i].EnvName != "" {
			args = append(args, h.PassEnvironmentToCommand[i].EnvName+"="+arg)
		} else {
			args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg)
		}
	}
	if len(errors) > 0 {
		return args, errors
	}
	return args, nil
}
type FileParameter struct {
	File    *os.File
	EnvName string
	Data    []byte
}
func (h *Hook) ExtractCommandArgumentsForFile(r *Request) ([]FileParameter, []error) {
	args := make([]FileParameter, 0)
	errors := make([]error, 0)
	for i := range h.PassFileToCommand {
		arg, err := h.PassFileToCommand[i].Get(r)
		if err != nil {
			errors = append(errors, &ArgumentError{h.PassFileToCommand[i]})
			continue
		}
		if h.PassFileToCommand[i].EnvName == "" {
			log.Printf("no ENVVAR name specified, falling back to [%s]", EnvNamespace+strings.ToUpper(h.PassFileToCommand[i].Name))
			h.PassFileToCommand[i].EnvName = EnvNamespace + strings.ToUpper(h.PassFileToCommand[i].Name)
		}
		var fileContent []byte
		if h.PassFileToCommand[i].Base64Decode {
			dec, err := base64.StdEncoding.DecodeString(arg)
			if err != nil {
				log.Printf("error decoding string [%s]", err)
			}
			fileContent = []byte(dec)
		} else {
			fileContent = []byte(arg)
		}
		args = append(args, FileParameter{EnvName: h.PassFileToCommand[i].EnvName, Data: fileContent})
	}
	if len(errors) > 0 {
		return args, errors
	}
	return args, nil
}
type Hooks []Hook
func (h *Hooks) LoadFromFile(path string, asTemplate bool) error {
	if path == "" {
		return nil
	}
	file, e := ioutil.ReadFile(path)
	if e != nil {
		return e
	}
	if asTemplate {
		funcMap := template.FuncMap{"getenv": getenv}
		tmpl, err := template.New("hooks").Funcs(funcMap).Parse(string(file))
		if err != nil {
			return err
		}
		var buf bytes.Buffer
		err = tmpl.Execute(&buf, nil)
		if err != nil {
			return err
		}
		file = buf.Bytes()
	}
	return yaml.Unmarshal(file, h)
}
func (h *Hooks) Append(other *Hooks) error {
	for _, hook := range *other {
		if h.Match(hook.ID) != nil {
			return fmt.Errorf("hook with ID %s is already defined", hook.ID)
		}
		*h = append(*h, hook)
	}
	return nil
}