mirror of
https://github.com/adnanh/webhook.git
synced 2025-06-28 15:28:31 +00:00
Make hook package internal
The hook package API is not meant for public consumption.
This commit is contained in:
parent
c872aae7e8
commit
40d9dcd6d4
4 changed files with 6 additions and 6 deletions
816
internal/hook/hook.go
Normal file
816
internal/hook/hook.go
Normal file
|
@ -0,0 +1,816 @@
|
|||
package hook
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
)
|
||||
|
||||
// Constants used to specify the parameter source
|
||||
const (
|
||||
SourceHeader string = "header"
|
||||
SourceQuery string = "url"
|
||||
SourceQueryAlias string = "query"
|
||||
SourcePayload string = "payload"
|
||||
SourceString string = "string"
|
||||
SourceEntirePayload string = "entire-payload"
|
||||
SourceEntireQuery string = "entire-query"
|
||||
SourceEntireHeaders string = "entire-headers"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnvNamespace is the prefix used for passing arguments into the command
|
||||
// environment.
|
||||
EnvNamespace string = "HOOK_"
|
||||
)
|
||||
|
||||
// SignatureError describes an invalid payload signature passed to Hook.
|
||||
type SignatureError struct {
|
||||
Signature string
|
||||
}
|
||||
|
||||
func (e *SignatureError) Error() string {
|
||||
if e == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprintf("invalid payload signature %s", e.Signature)
|
||||
}
|
||||
|
||||
// ArgumentError describes an invalid argument passed to Hook.
|
||||
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)
|
||||
}
|
||||
|
||||
// SourceError describes an invalid source passed to Hook.
|
||||
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)
|
||||
}
|
||||
|
||||
// ParseError describes an error parsing user input.
|
||||
type ParseError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *ParseError) Error() string {
|
||||
if e == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
|
||||
func CheckPayloadSignature(payload []byte, secret string, signature string) (string, error) {
|
||||
if secret == "" {
|
||||
return "", errors.New("signature validation secret can not be empty")
|
||||
}
|
||||
|
||||
signature = strings.TrimPrefix(signature, "sha1=")
|
||||
|
||||
mac := hmac.New(sha1.New, []byte(secret))
|
||||
_, err := mac.Write(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
|
||||
return expectedMAC, &SignatureError{signature}
|
||||
}
|
||||
return expectedMAC, err
|
||||
}
|
||||
|
||||
// CheckPayloadSignature256 calculates and verifies SHA256 signature of the given payload
|
||||
func CheckPayloadSignature256(payload []byte, secret string, signature string) (string, error) {
|
||||
if secret == "" {
|
||||
return "", errors.New("signature validation secret can not be empty")
|
||||
}
|
||||
|
||||
signature = strings.TrimPrefix(signature, "sha256=")
|
||||
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
_, err := mac.Write(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
|
||||
return expectedMAC, &SignatureError{signature}
|
||||
}
|
||||
return expectedMAC, err
|
||||
}
|
||||
|
||||
// CheckPayloadSignature512 calculates and verifies SHA512 signature of the given payload
|
||||
func CheckPayloadSignature512(payload []byte, secret string, signature string) (string, error) {
|
||||
if secret == "" {
|
||||
return "", errors.New("signature validation secret can not be empty")
|
||||
}
|
||||
|
||||
signature = strings.TrimPrefix(signature, "sha512=")
|
||||
|
||||
mac := hmac.New(sha512.New, []byte(secret))
|
||||
_, err := mac.Write(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
|
||||
return expectedMAC, &SignatureError{signature}
|
||||
}
|
||||
return expectedMAC, err
|
||||
}
|
||||
|
||||
func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey string, checkDate bool) (bool, error) {
|
||||
// Check for the signature and date headers
|
||||
if _, ok := headers["X-Signature"]; !ok {
|
||||
return false, nil
|
||||
}
|
||||
if _, ok := headers["Date"]; !ok {
|
||||
return false, nil
|
||||
}
|
||||
if signingKey == "" {
|
||||
return false, errors.New("signature validation signing key can not be empty")
|
||||
}
|
||||
|
||||
providedSignature := headers["X-Signature"].(string)
|
||||
dateHeader := headers["Date"].(string)
|
||||
mac := hmac.New(sha1.New, []byte(signingKey))
|
||||
mac.Write(body)
|
||||
mac.Write([]byte(dateHeader))
|
||||
expectedSignature := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
if !hmac.Equal([]byte(providedSignature), []byte(expectedSignature)) {
|
||||
return false, &SignatureError{providedSignature}
|
||||
}
|
||||
|
||||
if !checkDate {
|
||||
return true, nil
|
||||
}
|
||||
// Example format: Fri 08 Sep 2017 11:24:32 UTC
|
||||
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{"outdated"}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CheckIPWhitelist makes sure the provided remote address (of the form IP:port) falls within the provided IP range
|
||||
// (in CIDR form or a single IP address).
|
||||
func CheckIPWhitelist(remoteAddr string, ipRange string) (bool, error) {
|
||||
// Extract IP address from remote address.
|
||||
|
||||
// IPv6 addresses will likely be surrounded by [].
|
||||
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) {
|
||||
// Extract IP range in CIDR form. If a single IP address is provided, turn it into CIDR form.
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ReplaceParameter replaces parameter value with the passed value in the passed map
|
||||
// (please note you should pass pointer to the map, because we're modifying it)
|
||||
// based on the passed string
|
||||
func ReplaceParameter(s string, params interface{}, 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
|
||||
}
|
||||
|
||||
// GetParameter extracts interface{} value based on the passed string
|
||||
func GetParameter(s string, params interface{}) (interface{}, bool) {
|
||||
if params == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
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, false
|
||||
}
|
||||
|
||||
return GetParameter(p[1], params.([]interface{})[index])
|
||||
}
|
||||
|
||||
index, err := strconv.ParseUint(s, 10, 64)
|
||||
|
||||
if err != nil || paramsValueSliceLength <= int(index) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return params.([]interface{})[index], true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
|
||||
case reflect.Map:
|
||||
// Check for raw key
|
||||
if v, ok := params.(map[string]interface{})[s]; ok {
|
||||
return v, true
|
||||
}
|
||||
|
||||
// Checked for dotted references
|
||||
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, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// ExtractParameterAsString extracts value from interface{} as string based on the passed string
|
||||
func ExtractParameterAsString(s string, params interface{}) (string, bool) {
|
||||
if pValue, ok := GetParameter(s, params); ok {
|
||||
return fmt.Sprintf("%v", pValue), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Argument type specifies the parameter key name and the source it should
|
||||
// be extracted from
|
||||
type Argument struct {
|
||||
Source string `json:"source,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
EnvName string `json:"envname,omitempty"`
|
||||
Base64Decode bool `json:"base64decode,omitempty"`
|
||||
}
|
||||
|
||||
// Get Argument method returns the value for the Argument's key name
|
||||
// based on the Argument's source
|
||||
func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string, bool) {
|
||||
var source *map[string]interface{}
|
||||
key := ha.Name
|
||||
|
||||
switch ha.Source {
|
||||
case SourceHeader:
|
||||
source = headers
|
||||
key = textproto.CanonicalMIMEHeaderKey(ha.Name)
|
||||
case SourceQuery, SourceQueryAlias:
|
||||
source = query
|
||||
case SourcePayload:
|
||||
source = payload
|
||||
case SourceString:
|
||||
return ha.Name, true
|
||||
case SourceEntirePayload:
|
||||
r, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return string(r), true
|
||||
case SourceEntireHeaders:
|
||||
r, err := json.Marshal(headers)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return string(r), true
|
||||
case SourceEntireQuery:
|
||||
r, err := json.Marshal(query)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return string(r), true
|
||||
}
|
||||
|
||||
if source != nil {
|
||||
return ExtractParameterAsString(key, *source)
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Header is a structure containing header name and it's value
|
||||
type Header struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// ResponseHeaders is a slice of Header objects
|
||||
type ResponseHeaders []Header
|
||||
|
||||
func (h *ResponseHeaders) String() string {
|
||||
// a 'hack' to display name=value in flag usage listing
|
||||
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, ", ")
|
||||
}
|
||||
|
||||
// Set method appends new Header object from header=value notation
|
||||
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
|
||||
}
|
||||
|
||||
// HooksFiles is a slice of String
|
||||
type HooksFiles []string
|
||||
|
||||
func (h *HooksFiles) String() string {
|
||||
if len(*h) == 0 {
|
||||
return "hooks.json"
|
||||
}
|
||||
|
||||
return strings.Join(*h, ", ")
|
||||
}
|
||||
|
||||
// Set method appends new string
|
||||
func (h *HooksFiles) Set(value string) error {
|
||||
*h = append(*h, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Hook type is a structure containing details for a single hook
|
||||
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"`
|
||||
IncomingPayloadContentType string `json:"incoming-payload-content-type,omitempty"`
|
||||
SuccessHttpResponseCode int `json:"success-http-response-code,omitempty"`
|
||||
}
|
||||
|
||||
// ParseJSONParameters decodes specified arguments to JSON objects and replaces the
|
||||
// string with the newly created object
|
||||
func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface{}) []error {
|
||||
errors := make([]error, 0)
|
||||
|
||||
for i := range h.JSONStringParameters {
|
||||
if arg, ok := h.JSONStringParameters[i].Get(headers, query, payload); ok {
|
||||
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 = headers
|
||||
case SourcePayload:
|
||||
source = payload
|
||||
case SourceQuery, SourceQueryAlias:
|
||||
source = 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]})
|
||||
}
|
||||
} else {
|
||||
errors = append(errors, &ArgumentError{h.JSONStringParameters[i]})
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return errors
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExtractCommandArguments creates a list of arguments, based on the
|
||||
// PassArgumentsToCommand property that is ready to be used with exec.Command()
|
||||
func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) ([]string, []error) {
|
||||
args := make([]string, 0)
|
||||
errors := make([]error, 0)
|
||||
|
||||
args = append(args, h.ExecuteCommand)
|
||||
|
||||
for i := range h.PassArgumentsToCommand {
|
||||
if arg, ok := h.PassArgumentsToCommand[i].Get(headers, query, payload); ok {
|
||||
args = append(args, arg)
|
||||
} else {
|
||||
args = append(args, "")
|
||||
errors = append(errors, &ArgumentError{h.PassArgumentsToCommand[i]})
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return args, errors
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// ExtractCommandArgumentsForEnv creates a list of arguments in key=value
|
||||
// format, based on the PassEnvironmentToCommand property that is ready to be used
|
||||
// with exec.Command().
|
||||
func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string]interface{}) ([]string, []error) {
|
||||
args := make([]string, 0)
|
||||
errors := make([]error, 0)
|
||||
for i := range h.PassEnvironmentToCommand {
|
||||
if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok {
|
||||
if h.PassEnvironmentToCommand[i].EnvName != "" {
|
||||
// first try to use the EnvName if specified
|
||||
args = append(args, h.PassEnvironmentToCommand[i].EnvName+"="+arg)
|
||||
} else {
|
||||
// then fallback on the name
|
||||
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg)
|
||||
}
|
||||
} else {
|
||||
errors = append(errors, &ArgumentError{h.PassEnvironmentToCommand[i]})
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return args, errors
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// FileParameter describes a pass-file-to-command instance to be stored as file
|
||||
type FileParameter struct {
|
||||
File *os.File
|
||||
EnvName string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// ExtractCommandArgumentsForFile creates a list of arguments in key=value
|
||||
// format, based on the PassFileToCommand property that is ready to be used
|
||||
// with exec.Command().
|
||||
func (h *Hook) ExtractCommandArgumentsForFile(headers, query, payload *map[string]interface{}) ([]FileParameter, []error) {
|
||||
args := make([]FileParameter, 0)
|
||||
errors := make([]error, 0)
|
||||
for i := range h.PassFileToCommand {
|
||||
if arg, ok := h.PassFileToCommand[i].Get(headers, query, payload); ok {
|
||||
|
||||
if h.PassFileToCommand[i].EnvName == "" {
|
||||
// if no environment-variable name is set, fall-back on the name
|
||||
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})
|
||||
|
||||
} else {
|
||||
errors = append(errors, &ArgumentError{h.PassFileToCommand[i]})
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return args, errors
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// Hooks is an array of Hook objects
|
||||
type Hooks []Hook
|
||||
|
||||
// LoadFromFile attempts to load hooks from the specified file, which
|
||||
// can be either JSON or YAML. The asTemplate parameter causes the file
|
||||
// contents to be parsed as a Go text/template prior to unmarshalling.
|
||||
func (h *Hooks) LoadFromFile(path string, asTemplate bool) error {
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// parse hook file for hooks
|
||||
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()
|
||||
}
|
||||
|
||||
e = yaml.Unmarshal(file, h)
|
||||
return e
|
||||
}
|
||||
|
||||
// Append appends hooks unless the new hooks contain a hook with an ID that already exists
|
||||
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
|
||||
}
|
||||
|
||||
// Match iterates through Hooks and returns first one that matches the given ID,
|
||||
// if no hook matches the given ID, nil is returned
|
||||
func (h *Hooks) Match(id string) *Hook {
|
||||
for i := range *h {
|
||||
if (*h)[i].ID == id {
|
||||
return &(*h)[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rules is a structure that contains one of the valid rule types
|
||||
type Rules struct {
|
||||
And *AndRule `json:"and,omitempty"`
|
||||
Or *OrRule `json:"or,omitempty"`
|
||||
Not *NotRule `json:"not,omitempty"`
|
||||
Match *MatchRule `json:"match,omitempty"`
|
||||
}
|
||||
|
||||
// Evaluate finds the first rule property that is not nil and returns the value
|
||||
// it evaluates to
|
||||
func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
|
||||
switch {
|
||||
case r.And != nil:
|
||||
return r.And.Evaluate(headers, query, payload, body, remoteAddr)
|
||||
case r.Or != nil:
|
||||
return r.Or.Evaluate(headers, query, payload, body, remoteAddr)
|
||||
case r.Not != nil:
|
||||
return r.Not.Evaluate(headers, query, payload, body, remoteAddr)
|
||||
case r.Match != nil:
|
||||
return r.Match.Evaluate(headers, query, payload, body, remoteAddr)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// AndRule will evaluate to true if and only if all of the ChildRules evaluate to true
|
||||
type AndRule []Rules
|
||||
|
||||
// Evaluate AndRule will return true if and only if all of ChildRules evaluate to true
|
||||
func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
|
||||
res := true
|
||||
|
||||
for _, v := range r {
|
||||
rv, err := v.Evaluate(headers, query, payload, body, remoteAddr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
res = res && rv
|
||||
if !res {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// OrRule will evaluate to true if any of the ChildRules evaluate to true
|
||||
type OrRule []Rules
|
||||
|
||||
// Evaluate OrRule will return true if any of ChildRules evaluate to true
|
||||
func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
|
||||
res := false
|
||||
|
||||
for _, v := range r {
|
||||
rv, err := v.Evaluate(headers, query, payload, body, remoteAddr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
res = res || rv
|
||||
if res {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// NotRule will evaluate to true if any and only if the ChildRule evaluates to false
|
||||
type NotRule Rules
|
||||
|
||||
// Evaluate NotRule will return true if and only if ChildRule evaluates to false
|
||||
func (r NotRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
|
||||
rv, err := Rules(r).Evaluate(headers, query, payload, body, remoteAddr)
|
||||
return !rv, err
|
||||
}
|
||||
|
||||
// MatchRule will evaluate to true based on the type
|
||||
type MatchRule struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Regex string `json:"regex,omitempty"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Parameter Argument `json:"parameter,omitempty"`
|
||||
IPRange string `json:"ip-range,omitempty"`
|
||||
}
|
||||
|
||||
// Constants for the MatchRule type
|
||||
const (
|
||||
MatchValue string = "value"
|
||||
MatchRegex string = "regex"
|
||||
MatchHashSHA1 string = "payload-hash-sha1"
|
||||
MatchHashSHA256 string = "payload-hash-sha256"
|
||||
MatchHashSHA512 string = "payload-hash-sha512"
|
||||
IPWhitelist string = "ip-whitelist"
|
||||
ScalrSignature string = "scalr-signature"
|
||||
)
|
||||
|
||||
// Evaluate MatchRule will return based on the type
|
||||
func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
|
||||
if r.Type == IPWhitelist {
|
||||
return CheckIPWhitelist(remoteAddr, r.IPRange)
|
||||
}
|
||||
if r.Type == ScalrSignature {
|
||||
return CheckScalrSignature(*headers, *body, r.Secret, true)
|
||||
}
|
||||
|
||||
if arg, ok := r.Parameter.Get(headers, query, payload); ok {
|
||||
switch r.Type {
|
||||
case MatchValue:
|
||||
return compare(arg, r.Value), nil
|
||||
case MatchRegex:
|
||||
return regexp.MatchString(r.Regex, arg)
|
||||
case MatchHashSHA1:
|
||||
_, err := CheckPayloadSignature(*body, r.Secret, arg)
|
||||
return err == nil, err
|
||||
case MatchHashSHA256:
|
||||
_, err := CheckPayloadSignature256(*body, r.Secret, arg)
|
||||
return err == nil, err
|
||||
case MatchHashSHA512:
|
||||
_, err := CheckPayloadSignature512(*body, r.Secret, arg)
|
||||
return err == nil, err
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// compare is a helper function for constant time string comparisons.
|
||||
func compare(a, b string) bool {
|
||||
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||
}
|
||||
|
||||
// getenv provides a template function to retrieve OS environment variables.
|
||||
func getenv(s string) string {
|
||||
return os.Getenv(s)
|
||||
}
|
644
internal/hook/hook_test.go
Normal file
644
internal/hook/hook_test.go
Normal file
|
@ -0,0 +1,644 @@
|
|||
package hook
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetParameter(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
key string
|
||||
val interface{}
|
||||
expect interface{}
|
||||
ok bool
|
||||
}{
|
||||
// True
|
||||
{"a", map[string]interface{}{"a": "1"}, "1", true},
|
||||
{"a.b", map[string]interface{}{"a.b": "1"}, "1", true},
|
||||
{"a.c", map[string]interface{}{"a": map[string]interface{}{"c": 2}}, 2, true},
|
||||
{"a.1", map[string]interface{}{"a": map[string]interface{}{"1": 3}}, 3, true},
|
||||
{"a.1", map[string]interface{}{"a": []interface{}{"a", "b"}}, "b", true},
|
||||
{"0", []interface{}{"a", "b"}, "a", true},
|
||||
|
||||
// False
|
||||
{"z", map[string]interface{}{"a": "1"}, nil, false},
|
||||
{"a.z", map[string]interface{}{"a": map[string]interface{}{"b": 2}}, nil, false},
|
||||
{"z.b", map[string]interface{}{"a": map[string]interface{}{"z": 2}}, nil, false},
|
||||
{"a.2", map[string]interface{}{"a": []interface{}{"a", "b"}}, nil, false},
|
||||
} {
|
||||
res, ok := GetParameter(test.key, test.val)
|
||||
if ok != test.ok {
|
||||
t.Errorf("unexpected result given {%q, %q}: %t\n", test.key, test.val, ok)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(res, test.expect) {
|
||||
t.Errorf("failed given {%q, %q}:\nexpected {%#v}\ngot {%#v}\n", test.key, test.val, test.expect, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkPayloadSignatureTests = []struct {
|
||||
payload []byte
|
||||
secret string
|
||||
signature string
|
||||
mac string
|
||||
ok bool
|
||||
}{
|
||||
{[]byte(`{"a": "z"}`), "secret", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
|
||||
{[]byte(`{"a": "z"}`), "secreX", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "900225703e9342328db7307692736e2f7cc7b36e", false},
|
||||
{[]byte(`{"a": "z"}`), "", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "", false},
|
||||
}
|
||||
|
||||
func TestCheckPayloadSignature(t *testing.T) {
|
||||
for _, tt := range checkPayloadSignatureTests {
|
||||
mac, err := CheckPayloadSignature(tt.payload, tt.secret, tt.signature)
|
||||
if (err == nil) != tt.ok || mac != tt.mac {
|
||||
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
||||
}
|
||||
|
||||
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
|
||||
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkPayloadSignature256Tests = []struct {
|
||||
payload []byte
|
||||
secret string
|
||||
signature string
|
||||
mac string
|
||||
ok bool
|
||||
}{
|
||||
{[]byte(`{"a": "z"}`), "secret", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
|
||||
{[]byte(`{"a": "z"}`), "", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "", false},
|
||||
}
|
||||
|
||||
func TestCheckPayloadSignature256(t *testing.T) {
|
||||
for _, tt := range checkPayloadSignature256Tests {
|
||||
mac, err := CheckPayloadSignature256(tt.payload, tt.secret, tt.signature)
|
||||
if (err == nil) != tt.ok || mac != tt.mac {
|
||||
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
||||
}
|
||||
|
||||
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
|
||||
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkPayloadSignature512Tests = []struct {
|
||||
payload []byte
|
||||
secret string
|
||||
signature string
|
||||
mac string
|
||||
ok bool
|
||||
}{
|
||||
{[]byte(`{"a": "z"}`), "secret", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha512=4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true},
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", false},
|
||||
{[]byte(`{"a": "z"}`), "", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "", false},
|
||||
}
|
||||
|
||||
func TestCheckPayloadSignature512(t *testing.T) {
|
||||
for _, tt := range checkPayloadSignature512Tests {
|
||||
mac, err := CheckPayloadSignature512(tt.payload, tt.secret, tt.signature)
|
||||
if (err == nil) != tt.ok || mac != tt.mac {
|
||||
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
||||
}
|
||||
|
||||
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
|
||||
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkScalrSignatureTests = []struct {
|
||||
description string
|
||||
headers map[string]interface{}
|
||||
payload []byte
|
||||
secret string
|
||||
expectedSignature string
|
||||
ok bool
|
||||
}{
|
||||
{
|
||||
"Valid signature",
|
||||
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "48e395e38ac48988929167df531eb2da00063a7d"},
|
||||
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", true,
|
||||
},
|
||||
{
|
||||
"Wrong signature",
|
||||
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "999395e38ac48988929167df531eb2da00063a7d"},
|
||||
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
||||
},
|
||||
{
|
||||
"Missing Date header",
|
||||
map[string]interface{}{"X-Signature": "999395e38ac48988929167df531eb2da00063a7d"},
|
||||
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
||||
},
|
||||
{
|
||||
"Missing X-Signature header",
|
||||
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC"},
|
||||
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
||||
},
|
||||
{
|
||||
"Missing signing key",
|
||||
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "48e395e38ac48988929167df531eb2da00063a7d"},
|
||||
[]byte(`{"a": "b"}`), "",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestCheckScalrSignature(t *testing.T) {
|
||||
for _, testCase := range checkScalrSignatureTests {
|
||||
valid, err := CheckScalrSignature(testCase.headers, testCase.payload, testCase.secret, false)
|
||||
if valid != testCase.ok {
|
||||
t.Errorf("failed to check scalr signature fot test case: %s\nexpected ok:%#v, got ok:%#v}",
|
||||
testCase.description, testCase.ok, valid)
|
||||
}
|
||||
|
||||
if err != nil && testCase.secret != "" && strings.Contains(err.Error(), testCase.expectedSignature) {
|
||||
t.Errorf("error message should not disclose expected mac: %s on test case %s", err, testCase.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkIPWhitelistTests = []struct {
|
||||
addr string
|
||||
ipRange string
|
||||
expect bool
|
||||
ok bool
|
||||
}{
|
||||
{"[ 10.0.0.1:1234 ] ", " 10.0.0.1 ", true, true},
|
||||
{"[ 10.0.0.1:1234 ] ", " 10.0.0.0 ", false, true},
|
||||
{"[ 10.0.0.1:1234 ] ", " 10.0.0.1 10.0.0.1 ", true, true},
|
||||
{"[ 10.0.0.1:1234 ] ", " 10.0.0.0/31 ", true, true},
|
||||
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/48 ", true, true},
|
||||
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/48 2001:db8:1::/64", true, true},
|
||||
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/64 ", false, true},
|
||||
}
|
||||
|
||||
func TestCheckIPWhitelist(t *testing.T) {
|
||||
for _, tt := range checkIPWhitelistTests {
|
||||
result, err := CheckIPWhitelist(tt.addr, tt.ipRange)
|
||||
if (err == nil) != tt.ok || result != tt.expect {
|
||||
t.Errorf("ip whitelist test failed {%q, %q}:\nwant {expect:%#v, ok:%#v},\ngot {result:%#v, ok:%#v}", tt.addr, tt.ipRange, tt.expect, tt.ok, result, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var extractParameterTests = []struct {
|
||||
s string
|
||||
params interface{}
|
||||
value string
|
||||
ok bool
|
||||
}{
|
||||
{"a", map[string]interface{}{"a": "z"}, "z", true},
|
||||
{"a.b", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "z", true},
|
||||
{"a.b.c", map[string]interface{}{"a": map[string]interface{}{"b": map[string]interface{}{"c": "z"}}}, "z", true},
|
||||
{"a.b.0", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "x", true},
|
||||
{"a.1.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "z", true},
|
||||
{"a.1.b.c", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": map[string]interface{}{"c": "y"}}, map[string]interface{}{"b": map[string]interface{}{"c": "z"}}}}, "z", true},
|
||||
// failures
|
||||
{"check_nil", nil, "", false},
|
||||
{"a.X", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "", false}, // non-existent parameter reference
|
||||
{"a.X.c", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-integer slice index
|
||||
{"a.-1.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // negative slice index
|
||||
{"a.500.b", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "", false}, // non-existent slice
|
||||
{"a.501.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-existent slice index
|
||||
{"a.502.b", map[string]interface{}{"a": []interface{}{}}, "", false}, // non-existent slice index
|
||||
{"a.b.503", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "", false}, // trailing, non-existent slice index
|
||||
{"a.b", interface{}("a"), "", false}, // non-map, non-slice input
|
||||
}
|
||||
|
||||
func TestExtractParameter(t *testing.T) {
|
||||
for _, tt := range extractParameterTests {
|
||||
value, ok := ExtractParameterAsString(tt.s, tt.params)
|
||||
if ok != tt.ok || value != tt.value {
|
||||
t.Errorf("failed to extract parameter %q:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, ok:%#v}", tt.s, tt.value, tt.ok, value, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var argumentGetTests = []struct {
|
||||
source, name string
|
||||
headers, query, payload *map[string]interface{}
|
||||
value string
|
||||
ok bool
|
||||
}{
|
||||
{"header", "a", &map[string]interface{}{"A": "z"}, nil, nil, "z", true},
|
||||
{"url", "a", nil, &map[string]interface{}{"a": "z"}, nil, "z", true},
|
||||
{"payload", "a", nil, nil, &map[string]interface{}{"a": "z"}, "z", true},
|
||||
{"string", "a", nil, nil, &map[string]interface{}{"a": "z"}, "a", true},
|
||||
// failures
|
||||
{"header", "a", nil, &map[string]interface{}{"a": "z"}, &map[string]interface{}{"a": "z"}, "", false}, // nil headers
|
||||
{"url", "a", &map[string]interface{}{"A": "z"}, nil, &map[string]interface{}{"a": "z"}, "", false}, // nil query
|
||||
{"payload", "a", &map[string]interface{}{"A": "z"}, &map[string]interface{}{"a": "z"}, nil, "", false}, // nil payload
|
||||
{"foo", "a", &map[string]interface{}{"A": "z"}, nil, nil, "", false}, // invalid source
|
||||
}
|
||||
|
||||
func TestArgumentGet(t *testing.T) {
|
||||
for _, tt := range argumentGetTests {
|
||||
a := Argument{tt.source, tt.name, "", false}
|
||||
value, ok := a.Get(tt.headers, tt.query, tt.payload)
|
||||
if ok != tt.ok || value != tt.value {
|
||||
t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, ok:%#v}", tt.source, tt.name, tt.value, tt.ok, value, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hookParseJSONParametersTests = []struct {
|
||||
params []Argument
|
||||
headers, query, payload *map[string]interface{}
|
||||
rheaders, rquery, rpayload *map[string]interface{}
|
||||
ok bool
|
||||
}{
|
||||
{[]Argument{Argument{"header", "a", "", false}}, &map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, true},
|
||||
{[]Argument{Argument{"url", "a", "", false}}, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
|
||||
{[]Argument{Argument{"payload", "a", "", false}}, nil, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
|
||||
{[]Argument{Argument{"header", "z", "", false}}, &map[string]interface{}{"Z": `{}`}, nil, nil, &map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true},
|
||||
// failures
|
||||
{[]Argument{Argument{"header", "z", "", false}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // empty string
|
||||
{[]Argument{Argument{"header", "y", "", false}}, &map[string]interface{}{"X": `{}`}, nil, nil, &map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter
|
||||
{[]Argument{Argument{"string", "z", "", false}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source
|
||||
}
|
||||
|
||||
func TestHookParseJSONParameters(t *testing.T) {
|
||||
for _, tt := range hookParseJSONParametersTests {
|
||||
h := &Hook{JSONStringParameters: tt.params}
|
||||
err := h.ParseJSONParameters(tt.headers, tt.query, tt.payload)
|
||||
if (err == nil) != tt.ok || !reflect.DeepEqual(tt.headers, tt.rheaders) {
|
||||
t.Errorf("failed to parse %v:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.params, *tt.rheaders, tt.ok, *tt.headers, (err == nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hookExtractCommandArgumentsTests = []struct {
|
||||
exec string
|
||||
args []Argument
|
||||
headers, query, payload *map[string]interface{}
|
||||
value []string
|
||||
ok bool
|
||||
}{
|
||||
{"test", []Argument{Argument{"header", "a", "", false}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"test", "z"}, true},
|
||||
// failures
|
||||
{"fail", []Argument{Argument{"payload", "a", "", false}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"fail", ""}, false},
|
||||
}
|
||||
|
||||
func TestHookExtractCommandArguments(t *testing.T) {
|
||||
for _, tt := range hookExtractCommandArgumentsTests {
|
||||
h := &Hook{ExecuteCommand: tt.exec, PassArgumentsToCommand: tt.args}
|
||||
value, err := h.ExtractCommandArguments(tt.headers, tt.query, tt.payload)
|
||||
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
|
||||
t.Errorf("failed to extract args {cmd=%q, args=%v}:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.exec, tt.args, tt.value, tt.ok, value, (err == nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Here we test the extraction of env variables when the user defined a hook
|
||||
// with the "pass-environment-to-command" directive
|
||||
// we test both cases where the name of the data is used as the name of the
|
||||
// env key & the case where the hook definition sets the env var name to a
|
||||
// fixed value using the envname construct like so::
|
||||
// [
|
||||
// {
|
||||
// "id": "push",
|
||||
// "execute-command": "bb2mm",
|
||||
// "command-working-directory": "/tmp",
|
||||
// "pass-environment-to-command":
|
||||
// [
|
||||
// {
|
||||
// "source": "entire-payload",
|
||||
// "envname": "PAYLOAD"
|
||||
// },
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
var hookExtractCommandArgumentsForEnvTests = []struct {
|
||||
exec string
|
||||
args []Argument
|
||||
headers, query, payload *map[string]interface{}
|
||||
value []string
|
||||
ok bool
|
||||
}{
|
||||
// successes
|
||||
{
|
||||
"test",
|
||||
[]Argument{Argument{"header", "a", "", false}},
|
||||
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{"HOOK_a=z"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"test",
|
||||
[]Argument{Argument{"header", "a", "MYKEY", false}},
|
||||
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{"MYKEY=z"},
|
||||
true,
|
||||
},
|
||||
// failures
|
||||
{
|
||||
"fail",
|
||||
[]Argument{Argument{"payload", "a", "", false}},
|
||||
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestHookExtractCommandArgumentsForEnv(t *testing.T) {
|
||||
for _, tt := range hookExtractCommandArgumentsForEnvTests {
|
||||
h := &Hook{ExecuteCommand: tt.exec, PassEnvironmentToCommand: tt.args}
|
||||
value, err := h.ExtractCommandArgumentsForEnv(tt.headers, tt.query, tt.payload)
|
||||
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
|
||||
t.Errorf("failed to extract args for env {cmd=%q, args=%v}:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.exec, tt.args, tt.value, tt.ok, value, (err == nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hooksLoadFromFileTests = []struct {
|
||||
path string
|
||||
asTemplate bool
|
||||
ok bool
|
||||
}{
|
||||
{"../../hooks.json.example", false, true},
|
||||
{"../../hooks.yaml.example", false, true},
|
||||
{"../../hooks.json.tmpl.example", true, true},
|
||||
{"../../hooks.yaml.tmpl.example", true, true},
|
||||
{"", false, true},
|
||||
// failures
|
||||
{"missing.json", false, false},
|
||||
}
|
||||
|
||||
func TestHooksLoadFromFile(t *testing.T) {
|
||||
secret := `foo"123`
|
||||
os.Setenv("XXXTEST_SECRET", secret)
|
||||
|
||||
for _, tt := range hooksLoadFromFileTests {
|
||||
h := &Hooks{}
|
||||
err := h.LoadFromFile(tt.path, tt.asTemplate)
|
||||
if (err == nil) != tt.ok {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHooksTemplateLoadFromFile(t *testing.T) {
|
||||
secret := `foo"123`
|
||||
os.Setenv("XXXTEST_SECRET", secret)
|
||||
|
||||
for _, tt := range hooksLoadFromFileTests {
|
||||
if !tt.asTemplate {
|
||||
continue
|
||||
}
|
||||
|
||||
h := &Hooks{}
|
||||
err := h.LoadFromFile(tt.path, tt.asTemplate)
|
||||
if (err == nil) != tt.ok {
|
||||
t.Errorf(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
s := (*h.Match("webhook").TriggerRule.And)[0].Match.Secret
|
||||
if s != secret {
|
||||
t.Errorf("Expected secret of %q, got %q", secret, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hooksMatchTests = []struct {
|
||||
id string
|
||||
hooks Hooks
|
||||
value *Hook
|
||||
}{
|
||||
{"a", Hooks{Hook{ID: "a"}}, &Hook{ID: "a"}},
|
||||
{"X", Hooks{Hook{ID: "a"}}, new(Hook)},
|
||||
}
|
||||
|
||||
func TestHooksMatch(t *testing.T) {
|
||||
for _, tt := range hooksMatchTests {
|
||||
value := tt.hooks.Match(tt.id)
|
||||
if reflect.DeepEqual(reflect.ValueOf(value), reflect.ValueOf(tt.value)) {
|
||||
t.Errorf("failed to match %q:\nexpected %#v,\ngot %#v", tt.id, tt.value, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var matchRuleTests = []struct {
|
||||
typ, regex, secret, value, ipRange string
|
||||
param Argument
|
||||
headers, query, payload *map[string]interface{}
|
||||
body []byte
|
||||
remoteAddr string
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{"value", "", "", "z", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
|
||||
{"regex", "^z", "", "z", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
|
||||
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
||||
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
||||
// failures
|
||||
{"value", "", "", "X", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
|
||||
{"regex", "^X", "", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
|
||||
{"value", "", "2", "X", "", Argument{"header", "a", "", false}, &map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, false}, // reference invalid header
|
||||
// errors
|
||||
{"regex", "*", "", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex
|
||||
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
// IP whitelisting, valid cases
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1", Argument{}, nil, nil, nil, []byte{}, "192.168.0.1:9000", true, false}, // valid IPv4, no range
|
||||
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, with range
|
||||
{"ip-whitelist", "", "", "", "::1", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, no range
|
||||
// IP whitelisting, invalid cases
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/a", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, with range
|
||||
{"ip-whitelist", "", "", "", "192.168.0.a", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, no range
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.a:9000", false, true}, // invalid IPv4 address
|
||||
{"ip-whitelist", "", "", "", "::1/a", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, with range
|
||||
{"ip-whitelist", "", "", "", "::z", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, no range
|
||||
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, []byte{}, "[::z]:9000", false, true}, // invalid IPv6 address
|
||||
}
|
||||
|
||||
func TestMatchRule(t *testing.T) {
|
||||
for i, tt := range matchRuleTests {
|
||||
r := MatchRule{tt.typ, tt.regex, tt.secret, tt.value, tt.param, tt.ipRange}
|
||||
ok, err := r.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, tt.remoteAddr)
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("%d failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", i, r, tt.ok, tt.err, ok, (err != nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var andRuleTests = []struct {
|
||||
desc string // description of the test case
|
||||
rule AndRule
|
||||
headers, query, payload *map[string]interface{}
|
||||
body []byte
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
"(a=z, b=y): a=z && b=y",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"A": "z", "B": "y"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=z, b=Y): a=z && b=y",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"A": "z", "B": "Y"}, nil, nil, []byte{},
|
||||
false, false,
|
||||
},
|
||||
// Complex test to cover Rules.Evaluate
|
||||
{
|
||||
"(a=z, b=y, c=x, d=w=, e=X, f=X): a=z && (b=y && c=x) && (d=w || e=v) && !f=u",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{
|
||||
And: &AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", "", false}, ""}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Or: &OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", "", false}, ""}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Not: &NotRule{
|
||||
Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", "", false}, ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
&map[string]interface{}{"A": "z", "B": "y", "C": "x", "D": "w", "E": "X", "F": "X"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
},
|
||||
{"empty rule", AndRule{{}}, nil, nil, nil, nil, false, false},
|
||||
// failures
|
||||
{
|
||||
"invalid rule",
|
||||
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}},
|
||||
&map[string]interface{}{"Y": "z"}, nil, nil, nil,
|
||||
false, false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestAndRule(t *testing.T) {
|
||||
for _, tt := range andRuleTests {
|
||||
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, "")
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.desc, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var orRuleTests = []struct {
|
||||
desc string // description of the test case
|
||||
rule OrRule
|
||||
headers, query, payload *map[string]interface{}
|
||||
body []byte
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
"(a=z, b=X): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"A": "z", "B": "X"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=X, b=y): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"A": "X", "B": "y"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=Z, b=Y): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil, []byte{},
|
||||
false, false,
|
||||
},
|
||||
// failures
|
||||
{
|
||||
"invalid rule",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"Y": "Z"}, nil, nil, []byte{},
|
||||
false, false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestOrRule(t *testing.T) {
|
||||
for _, tt := range orRuleTests {
|
||||
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, "")
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("%#v:\nexpected ok: %#v, err: %v\ngot ok: %#v err: %v", tt.desc, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var notRuleTests = []struct {
|
||||
desc string // description of the test case
|
||||
rule NotRule
|
||||
headers, query, payload *map[string]interface{}
|
||||
body []byte
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
|
||||
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
|
||||
}
|
||||
|
||||
func TestNotRule(t *testing.T) {
|
||||
for _, tt := range notRuleTests {
|
||||
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, "")
|
||||
if ok != tt.ok || (err != nil) != tt.err {
|
||||
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.rule, tt.ok, tt.err, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompare(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
a, b string
|
||||
ok bool
|
||||
}{
|
||||
{"abcd", "abcd", true},
|
||||
{"zyxw", "abcd", false},
|
||||
} {
|
||||
if ok := compare(tt.a, tt.b); ok != tt.ok {
|
||||
t.Errorf("compare failed for %q and %q: got %v\n", tt.a, tt.b, ok)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue