webhook/hook/hook.go
Cameron Moore 7dd55f5232 Refactor to remove helpers package
This commit removes the "helpers" package by moving functions from the
package into the other packages that use them.

CheckPayloadSignature() and ExtractParamater() are simply moved to the
"hook" package.  I'm not sure of the usefulness of having these
functions exported, but I left them allow for now.

ValuesToMap() is moved to the "main" webhook package and renamed to
valuesToMap().

Tests were moved into the "hook" package since we only test
ExtractParameter() right now.

This commit closes adnanh/webhook#12.
2015-03-20 08:55:42 -05:00

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.ParseInt(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
}