mirror of
https://github.com/adnanh/webhook.git
synced 2025-05-12 00:24:45 +00:00
This commit changes ExtractParameter to parse slice indexes as unsigned ints. Fixes test case "a.-1.b".
271 lines
7 KiB
Go
271 lines
7 KiB
Go
package hook
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha1"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Constants used to specify the parameter source
|
|
const (
|
|
SourceHeader string = "header"
|
|
SourceQuery string = "url"
|
|
SourcePayload string = "payload"
|
|
)
|
|
|
|
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
|
|
func CheckPayloadSignature(payload []byte, secret string, signature string) (string, bool) {
|
|
if strings.HasPrefix(signature, "sha1=") {
|
|
signature = signature[5:]
|
|
}
|
|
|
|
mac := hmac.New(sha1.New, []byte(secret))
|
|
mac.Write(payload)
|
|
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
|
|
|
return expectedMAC, hmac.Equal([]byte(signature), []byte(expectedMAC))
|
|
}
|
|
|
|
// ExtractParameter extracts value from interface{} based on the passed string
|
|
func ExtractParameter(s string, params interface{}) (string, 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 {
|
|
return "", false
|
|
} else if paramsValueSliceLength <= int(index) {
|
|
return "", false
|
|
}
|
|
|
|
return ExtractParameter(p[1], params.([]interface{})[index])
|
|
}
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
|
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
|
return ExtractParameter(p[1], pValue)
|
|
}
|
|
} else {
|
|
if pValue, ok := params.(map[string]interface{})[p[0]]; 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"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// 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{}
|
|
|
|
switch ha.Source {
|
|
case SourceHeader:
|
|
source = headers
|
|
case SourceQuery:
|
|
source = query
|
|
case SourcePayload:
|
|
source = payload
|
|
}
|
|
|
|
if source != nil {
|
|
return ExtractParameter(ha.Name, *source)
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
// Hook type is a structure containing details for a single hook
|
|
type Hook struct {
|
|
ID string `json:"id"`
|
|
ExecuteCommand string `json:"execute-command"`
|
|
CommandWorkingDirectory string `json:"command-working-directory"`
|
|
ResponseMessage string `json:"response-message"`
|
|
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command"`
|
|
TriggerRule *Rules `json:"trigger-rule"`
|
|
}
|
|
|
|
// 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 {
|
|
var args = make([]string, 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, "")
|
|
log.Printf("couldn't retrieve argument for %+v\n", h.PassArgumentsToCommand[i])
|
|
}
|
|
}
|
|
|
|
return args
|
|
}
|
|
|
|
// Hooks is an array of Hook objects
|
|
type Hooks []Hook
|
|
|
|
// LoadFromFile attempts to load hooks from specified JSON file
|
|
func (h *Hooks) LoadFromFile(path string) error {
|
|
if path == "" {
|
|
return nil
|
|
}
|
|
|
|
// parse hook file for hooks
|
|
file, e := ioutil.ReadFile(path)
|
|
|
|
if e != nil {
|
|
return e
|
|
}
|
|
|
|
e = json.Unmarshal(file, h)
|
|
return e
|
|
}
|
|
|
|
// 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"`
|
|
Or *OrRule `json:"or"`
|
|
Not *NotRule `json:"not"`
|
|
Match *MatchRule `json:"match"`
|
|
}
|
|
|
|
// 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) bool {
|
|
switch {
|
|
case r.And != nil:
|
|
return r.And.Evaluate(headers, query, payload, body)
|
|
case r.Or != nil:
|
|
return r.Or.Evaluate(headers, query, payload, body)
|
|
case r.Not != nil:
|
|
return r.Not.Evaluate(headers, query, payload, body)
|
|
case r.Match != nil:
|
|
return r.Match.Evaluate(headers, query, payload, body)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// 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) bool {
|
|
res := true
|
|
|
|
for _, v := range r {
|
|
res = res && v.Evaluate(headers, query, payload, body)
|
|
if res == false {
|
|
return res
|
|
}
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
// 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) bool {
|
|
res := false
|
|
|
|
for _, v := range r {
|
|
res = res || v.Evaluate(headers, query, payload, body)
|
|
if res == true {
|
|
return res
|
|
}
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
// 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) bool {
|
|
return !r.Evaluate(headers, query, payload, body)
|
|
}
|
|
|
|
// MatchRule will evaluate to true based on the type
|
|
type MatchRule struct {
|
|
Type string `json:"type"`
|
|
Regex string `json:"regex"`
|
|
Secret string `json:"secret"`
|
|
Value string `json:"value"`
|
|
Parameter Argument `json:"parameter"`
|
|
}
|
|
|
|
// Constants for the MatchRule type
|
|
const (
|
|
MatchValue string = "value"
|
|
MatchRegex string = "regex"
|
|
MatchHashSHA1 string = "payload-hash-sha1"
|
|
)
|
|
|
|
// Evaluate MatchRule will return based on the type
|
|
func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) bool {
|
|
if arg, ok := r.Parameter.Get(headers, query, payload); ok {
|
|
switch r.Type {
|
|
case MatchValue:
|
|
return arg == r.Value
|
|
case MatchRegex:
|
|
ok, err := regexp.MatchString(r.Regex, arg)
|
|
if err != nil {
|
|
log.Printf("error while trying to evaluate regex: %+v", err)
|
|
}
|
|
return ok
|
|
case MatchHashSHA1:
|
|
expected, ok := CheckPayloadSignature(*body, r.Secret, arg)
|
|
if !ok {
|
|
log.Printf("payload signature mismatch, expected %s got %s", expected, arg)
|
|
}
|
|
|
|
return ok
|
|
}
|
|
} else {
|
|
log.Printf("couldn't retrieve argument for %+v\n", r.Parameter)
|
|
}
|
|
return false
|
|
}
|