mirror of
https://github.com/adnanh/webhook.git
synced 2026-07-27 01:29:16 +08:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aeacb6dac7 | |||
| 1039151a16 | |||
| db928228c8 | |||
| 6896a34aab | |||
| 5f853d8aba | |||
| 12c48f87cb | |||
| acf38c3210 | |||
| d3f368cb8f | |||
| 943bc258f7 | |||
| 231426da57 | |||
| baec1cadc5 | |||
| 688483d6d1 | |||
| bddb523b67 | |||
| b8807ed434 | |||
| 8527a9b23e | |||
| becd8935be | |||
| 230d16dd93 | |||
| 9a7dedbc09 | |||
| 84ce6f262a | |||
| 7dd55f5232 | |||
| d8a21582a3 | |||
| 8a627f7e67 | |||
| 7635cfde33 | |||
| 2a2a20dcb8 | |||
| 10755eb9d9 | |||
| e9aaeb579e | |||
| 956589fab3 |
@@ -1,77 +0,0 @@
|
|||||||
package helpers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValuesToMap converts map[string][]string to a map[string]string object
|
|
||||||
func ValuesToMap(values map[string][]string) map[string]interface{} {
|
|
||||||
ret := make(map[string]interface{})
|
|
||||||
|
|
||||||
for key, value := range values {
|
|
||||||
if len(value) > 0 {
|
|
||||||
ret[key] = value[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtractParameter extracts value from interface{} based on the passed string
|
|
||||||
func ExtractParameter(s string, params interface{}) (string, bool) {
|
|
||||||
if params == nil {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
var p []string
|
|
||||||
|
|
||||||
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
|
|
||||||
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
|
|
||||||
|
|
||||||
if p = strings.SplitN(s, ".", 3); len(p) > 3 {
|
|
||||||
index, err := strconv.ParseInt(p[1], 10, 64)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", false
|
|
||||||
} else if paramsValueSliceLength <= int(index) {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
return ExtractParameter(p[2], params.([]map[string]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
|
|
||||||
}
|
|
||||||
+153
-5
@@ -1,12 +1,17 @@
|
|||||||
package hook
|
package hook
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"github.com/adnanh/webhook/helpers"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Constants used to specify the parameter source
|
// Constants used to specify the parameter source
|
||||||
@@ -16,6 +21,114 @@ const (
|
|||||||
SourcePayload string = "payload"
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||||
|
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Map {
|
||||||
|
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||||
|
return GetParameter(p[1], pValue)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||||
|
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
|
// Argument type specifies the parameter key name and the source it should
|
||||||
// be extracted from
|
// be extracted from
|
||||||
type Argument struct {
|
type Argument struct {
|
||||||
@@ -38,7 +151,7 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if source != nil {
|
if source != nil {
|
||||||
return helpers.ExtractParameter(ha.Name, *source)
|
return ExtractParameterAsString(ha.Name, *source)
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", false
|
return "", false
|
||||||
@@ -51,9 +164,44 @@ type Hook struct {
|
|||||||
CommandWorkingDirectory string `json:"command-working-directory"`
|
CommandWorkingDirectory string `json:"command-working-directory"`
|
||||||
ResponseMessage string `json:"response-message"`
|
ResponseMessage string `json:"response-message"`
|
||||||
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command"`
|
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command"`
|
||||||
|
JSONStringParameters []Argument `json:"parse-parameters-as-json"`
|
||||||
TriggerRule *Rules `json:"trigger-rule"`
|
TriggerRule *Rules `json:"trigger-rule"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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{}) {
|
||||||
|
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 {
|
||||||
|
log.Printf("error parsing argument as JSON payload %+v\n", err)
|
||||||
|
} else {
|
||||||
|
var source *map[string]interface{}
|
||||||
|
|
||||||
|
switch h.JSONStringParameters[i].Source {
|
||||||
|
case SourceHeader:
|
||||||
|
source = headers
|
||||||
|
case SourcePayload:
|
||||||
|
source = payload
|
||||||
|
case SourceQuery:
|
||||||
|
source = query
|
||||||
|
}
|
||||||
|
|
||||||
|
ReplaceParameter(h.JSONStringParameters[i].Name, source, newArg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("couldn't retrieve argument for %+v\n", h.JSONStringParameters[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ExtractCommandArguments creates a list of arguments, based on the
|
// ExtractCommandArguments creates a list of arguments, based on the
|
||||||
// PassArgumentsToCommand property that is ready to be used with exec.Command()
|
// PassArgumentsToCommand property that is ready to be used with exec.Command()
|
||||||
func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) []string {
|
func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) []string {
|
||||||
@@ -169,7 +317,7 @@ type NotRule Rules
|
|||||||
|
|
||||||
// Evaluate NotRule will return true if and only if ChildRule evaluates to false
|
// 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 {
|
func (r NotRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) bool {
|
||||||
return !r.Evaluate(headers, query, payload, body)
|
return !Rules(r).Evaluate(headers, query, payload, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchRule will evaluate to true based on the type
|
// MatchRule will evaluate to true based on the type
|
||||||
@@ -201,7 +349,7 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod
|
|||||||
}
|
}
|
||||||
return ok
|
return ok
|
||||||
case MatchHashSHA1:
|
case MatchHashSHA1:
|
||||||
expected, ok := helpers.CheckPayloadSignature(*body, r.Secret, arg)
|
expected, ok := CheckPayloadSignature(*body, r.Secret, arg)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("payload signature mismatch, expected %s got %s", expected, arg)
|
log.Printf("payload signature mismatch, expected %s got %s", expected, arg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,327 @@
|
|||||||
|
package hook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
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},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckPayloadSignature(t *testing.T) {
|
||||||
|
for _, tt := range checkPayloadSignatureTests {
|
||||||
|
mac, ok := CheckPayloadSignature(tt.payload, tt.secret, tt.signature)
|
||||||
|
if ok != 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, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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},
|
||||||
|
// 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}
|
||||||
|
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{}
|
||||||
|
}{
|
||||||
|
{[]Argument{Argument{"header", "a"}}, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, nil},
|
||||||
|
{[]Argument{Argument{"url", "a"}}, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil},
|
||||||
|
{[]Argument{Argument{"payload", "a"}}, nil, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}},
|
||||||
|
{[]Argument{Argument{"header", "z"}}, &map[string]interface{}{"z": `{}`}, nil, nil, &map[string]interface{}{"z": map[string]interface{}{}}, nil, nil},
|
||||||
|
// failures
|
||||||
|
{[]Argument{Argument{"header", "z"}}, &map[string]interface{}{"z": ``}, nil, nil, &map[string]interface{}{"z": ``}, nil, nil}, // empty string
|
||||||
|
{[]Argument{Argument{"header", "y"}}, &map[string]interface{}{"X": `{}`}, nil, nil, &map[string]interface{}{"X": `{}`}, nil, nil}, // missing parameter
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookParseJSONParameters(t *testing.T) {
|
||||||
|
for _, tt := range hookParseJSONParametersTests {
|
||||||
|
h := &Hook{JSONStringParameters: tt.params}
|
||||||
|
h.ParseJSONParameters(tt.headers, tt.query, tt.payload)
|
||||||
|
if !reflect.DeepEqual(tt.headers, tt.rheaders) {
|
||||||
|
t.Errorf("failed to parse %v:\nexpected %#v,\ngot %#v", tt.params, *tt.rheaders, *tt.headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hookExtractCommandArgumentsTests = []struct {
|
||||||
|
exec string
|
||||||
|
args []Argument
|
||||||
|
headers, query, payload *map[string]interface{}
|
||||||
|
value []string
|
||||||
|
}{
|
||||||
|
{"test", []Argument{Argument{"header", "a"}}, &map[string]interface{}{"a": "z"}, nil, nil, []string{"test", "z"}},
|
||||||
|
// failures
|
||||||
|
{"fail", []Argument{Argument{"payload", "a"}}, &map[string]interface{}{"a": "z"}, nil, nil, []string{"fail", ""}},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookExtractCommandArguments(t *testing.T) {
|
||||||
|
for _, tt := range hookExtractCommandArgumentsTests {
|
||||||
|
h := &Hook{ExecuteCommand: tt.exec, PassArgumentsToCommand: tt.args}
|
||||||
|
value := h.ExtractCommandArguments(tt.headers, tt.query, tt.payload)
|
||||||
|
if !reflect.DeepEqual(value, tt.value) {
|
||||||
|
t.Errorf("failed to extract args {cmd=%q, args=%v}:\nexpected %#v,\ngot %#v", tt.exec, tt.args, tt.value, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hooksLoadFromFileTests = []struct {
|
||||||
|
path string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"../hooks.json.example", true},
|
||||||
|
{"", true},
|
||||||
|
// failures
|
||||||
|
{"missing.json", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHooksLoadFromFile(t *testing.T) {
|
||||||
|
for _, tt := range hooksLoadFromFileTests {
|
||||||
|
h := &Hooks{}
|
||||||
|
err := h.LoadFromFile(tt.path)
|
||||||
|
if (err == nil) != tt.ok {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 string
|
||||||
|
param Argument
|
||||||
|
headers, query, payload *map[string]interface{}
|
||||||
|
body []byte
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"value", "", "", "z", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, true},
|
||||||
|
{"regex", "^z", "", "z", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, true},
|
||||||
|
{"payload-hash-sha1", "", "secret", "", Argument{"header", "a"}, &map[string]interface{}{"a": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), true},
|
||||||
|
// failures
|
||||||
|
{"value", "", "", "X", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false},
|
||||||
|
{"value", "", "", "X", Argument{"header", "a"}, &map[string]interface{}{"y": "z"}, nil, nil, []byte{}, false},
|
||||||
|
{"regex", "^X", "", "", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false},
|
||||||
|
{"regex", "*", "", "", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false},
|
||||||
|
{"payload-hash-sha1", "", "secret", "", Argument{"header", "a"}, &map[string]interface{}{"a": ""}, nil, nil, []byte{}, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchRule(t *testing.T) {
|
||||||
|
for _, tt := range matchRuleTests {
|
||||||
|
r := MatchRule{tt.typ, tt.regex, tt.secret, tt.value, tt.param}
|
||||||
|
ok := r.Evaluate(tt.headers, tt.query, tt.payload, &tt.body)
|
||||||
|
if ok != tt.ok {
|
||||||
|
t.Errorf("failed to match %#v:\nexpected %#v,\ngot %#v", r, tt.ok, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var andRuleTests = []struct {
|
||||||
|
desc string // description of the test case
|
||||||
|
rule AndRule
|
||||||
|
headers, query, payload *map[string]interface{}
|
||||||
|
body []byte
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"(a=z, b=y): a=z && b=y",
|
||||||
|
AndRule{
|
||||||
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||||
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||||
|
},
|
||||||
|
&map[string]interface{}{"a": "z", "b": "y"}, nil, nil, []byte{},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"(a=z, b=Y): a=z && b=y",
|
||||||
|
AndRule{
|
||||||
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||||
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||||
|
},
|
||||||
|
&map[string]interface{}{"a": "z", "b": "Y"}, nil, nil, []byte{},
|
||||||
|
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"}}},
|
||||||
|
{
|
||||||
|
And: &AndRule{
|
||||||
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||||
|
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Or: &OrRule{
|
||||||
|
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d"}}},
|
||||||
|
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Not: &NotRule{
|
||||||
|
Match: &MatchRule{"value", "", "", "u", Argument{"header", "f"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&map[string]interface{}{"a": "z", "b": "y", "c": "x", "d": "w", "e": "X", "f": "X"}, nil, nil, []byte{},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{"empty rule", AndRule{{}}, nil, nil, nil, nil, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAndRule(t *testing.T) {
|
||||||
|
for _, tt := range andRuleTests {
|
||||||
|
ok := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body)
|
||||||
|
if ok != tt.ok {
|
||||||
|
t.Errorf("failed to match %#v:\nexpected %#v,\ngot %#v", tt.desc, tt.ok, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var orRuleTests = []struct {
|
||||||
|
desc string // description of the test case
|
||||||
|
rule OrRule
|
||||||
|
headers, query, payload *map[string]interface{}
|
||||||
|
body []byte
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"(a=z, b=X): a=z || b=y",
|
||||||
|
OrRule{
|
||||||
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||||
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||||
|
},
|
||||||
|
&map[string]interface{}{"a": "z", "b": "X"}, nil, nil, []byte{},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"(a=X, b=y): a=z || b=y",
|
||||||
|
OrRule{
|
||||||
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||||
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||||
|
},
|
||||||
|
&map[string]interface{}{"a": "X", "b": "y"}, nil, nil, []byte{},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"(a=Z, b=Y): a=z || b=y",
|
||||||
|
OrRule{
|
||||||
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||||
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||||
|
},
|
||||||
|
&map[string]interface{}{"a": "Z", "b": "Y"}, nil, nil, []byte{},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOrRule(t *testing.T) {
|
||||||
|
for _, tt := range orRuleTests {
|
||||||
|
ok := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body)
|
||||||
|
if ok != tt.ok {
|
||||||
|
t.Errorf("%#v:\nexpected %#v,\ngot %#v", tt.desc, tt.ok, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var notRuleTests = []struct {
|
||||||
|
desc string // description of the test case
|
||||||
|
rule NotRule
|
||||||
|
headers, query, payload *map[string]interface{}
|
||||||
|
body []byte
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a"}}}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, true},
|
||||||
|
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotRule(t *testing.T) {
|
||||||
|
for _, tt := range notRuleTests {
|
||||||
|
ok := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body)
|
||||||
|
if ok != tt.ok {
|
||||||
|
t.Errorf("failed to match %#v:\nexpected %#v,\ngot %#v", tt.rule, tt.ok, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
-30
@@ -10,9 +10,10 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/adnanh/webhook/helpers"
|
|
||||||
"github.com/adnanh/webhook/hook"
|
"github.com/adnanh/webhook/hook"
|
||||||
|
|
||||||
"github.com/codegangsta/negroni"
|
"github.com/codegangsta/negroni"
|
||||||
@@ -22,7 +23,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
version = "2.2.2"
|
version = "2.3.2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -37,6 +38,7 @@ var (
|
|||||||
key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file")
|
key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file")
|
||||||
|
|
||||||
watcher *fsnotify.Watcher
|
watcher *fsnotify.Watcher
|
||||||
|
signals chan os.Signal
|
||||||
|
|
||||||
hooks hook.Hooks
|
hooks hook.Hooks
|
||||||
)
|
)
|
||||||
@@ -55,6 +57,14 @@ func init() {
|
|||||||
|
|
||||||
log.Println("version " + version + " starting")
|
log.Println("version " + version + " starting")
|
||||||
|
|
||||||
|
// set os signal watcher
|
||||||
|
log.Printf("setting up os signal watcher\n")
|
||||||
|
|
||||||
|
signals = make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signals, syscall.Signal(0xa))
|
||||||
|
|
||||||
|
go watchForSignals()
|
||||||
|
|
||||||
// load and parse hooks
|
// load and parse hooks
|
||||||
log.Printf("attempting to load hooks from %s\n", *hooksFilePath)
|
log.Printf("attempting to load hooks from %s\n", *hooksFilePath)
|
||||||
|
|
||||||
@@ -93,18 +103,17 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
l := log.New(os.Stdout, "[webhook] ", log.Ldate|log.Ltime)
|
l := negroni.NewLogger()
|
||||||
|
l.Logger = log.New(os.Stdout, "[webhook] ", log.Ldate|log.Ltime)
|
||||||
negroniLogger := &negroni.Logger{l}
|
|
||||||
|
|
||||||
negroniRecovery := &negroni.Recovery{
|
negroniRecovery := &negroni.Recovery{
|
||||||
Logger: l,
|
Logger: l.Logger,
|
||||||
PrintStack: true,
|
PrintStack: true,
|
||||||
StackAll: false,
|
StackAll: false,
|
||||||
StackSize: 1024 * 8,
|
StackSize: 1024 * 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
n := negroni.New(negroniRecovery, negroniLogger)
|
n := negroni.New(negroniRecovery, l)
|
||||||
|
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
|
|
||||||
@@ -144,17 +153,17 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parse headers
|
// parse headers
|
||||||
headers := helpers.ValuesToMap(r.Header)
|
headers := valuesToMap(r.Header)
|
||||||
|
|
||||||
// parse query variables
|
// parse query variables
|
||||||
query := helpers.ValuesToMap(r.URL.Query())
|
query := valuesToMap(r.URL.Query())
|
||||||
|
|
||||||
// parse body
|
// parse body
|
||||||
var payload map[string]interface{}
|
var payload map[string]interface{}
|
||||||
|
|
||||||
contentType := r.Header.Get("Content-Type")
|
contentType := r.Header.Get("Content-Type")
|
||||||
|
|
||||||
if contentType == "application/json" {
|
if strings.HasPrefix(contentType, "application/json") {
|
||||||
decoder := json.NewDecoder(strings.NewReader(string(body)))
|
decoder := json.NewDecoder(strings.NewReader(string(body)))
|
||||||
decoder.UseNumber()
|
decoder.UseNumber()
|
||||||
|
|
||||||
@@ -163,15 +172,17 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error parsing JSON payload %+v\n", err)
|
log.Printf("error parsing JSON payload %+v\n", err)
|
||||||
}
|
}
|
||||||
} else if contentType == "application/x-www-form-urlencoded" {
|
} else if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
|
||||||
fd, err := url.ParseQuery(string(body))
|
fd, err := url.ParseQuery(string(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error parsing form payload %+v\n", err)
|
log.Printf("error parsing form payload %+v\n", err)
|
||||||
} else {
|
} else {
|
||||||
payload = helpers.ValuesToMap(fd)
|
payload = valuesToMap(fd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hook.ParseJSONParameters(&headers, &query, &payload)
|
||||||
|
|
||||||
// handle hook
|
// handle hook
|
||||||
go handleHook(hook, &headers, &query, &payload, &body)
|
go handleHook(hook, &headers, &query, &payload, &body)
|
||||||
|
|
||||||
@@ -206,6 +217,27 @@ func handleHook(hook *hook.Hook, headers, query, payload *map[string]interface{}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reloadHooks() {
|
||||||
|
newHooks := hook.Hooks{}
|
||||||
|
|
||||||
|
// parse and swap
|
||||||
|
log.Printf("attempting to reload hooks from %s\n", *hooksFilePath)
|
||||||
|
|
||||||
|
err := newHooks.LoadFromFile(*hooksFilePath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("couldn't load hooks from file! %+v\n", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("loaded %d hook(s) from file\n", len(hooks))
|
||||||
|
|
||||||
|
for _, hook := range hooks {
|
||||||
|
log.Printf("\t> %s\n", hook.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
hooks = newHooks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func watchForFileChange() {
|
func watchForFileChange() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -213,27 +245,38 @@ func watchForFileChange() {
|
|||||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||||
log.Println("hooks file modified")
|
log.Println("hooks file modified")
|
||||||
|
|
||||||
newHooks := hook.Hooks{}
|
reloadHooks()
|
||||||
|
|
||||||
// parse and swap
|
|
||||||
log.Printf("attempting to reload hooks from %s\n", *hooksFilePath)
|
|
||||||
|
|
||||||
err := newHooks.LoadFromFile(*hooksFilePath)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("couldn't load hooks from file! %+v\n", err)
|
|
||||||
} else {
|
|
||||||
log.Printf("loaded %d hook(s) from file\n", len(hooks))
|
|
||||||
|
|
||||||
for _, hook := range hooks {
|
|
||||||
log.Printf("\t> %s\n", hook.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
hooks = newHooks
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case err := <-(*watcher).Errors:
|
case err := <-(*watcher).Errors:
|
||||||
log.Println("watcher error:", err)
|
log.Println("watcher error:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func watchForSignals() {
|
||||||
|
log.Println("os signal watcher ready")
|
||||||
|
|
||||||
|
for {
|
||||||
|
sig := <-signals
|
||||||
|
if sig == syscall.Signal(0xa) {
|
||||||
|
log.Println("caught USR1 signal")
|
||||||
|
|
||||||
|
reloadHooks()
|
||||||
|
} else {
|
||||||
|
log.Printf("caught unhandled signal %+v\n", sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// valuesToMap converts map[string][]string to a map[string]string object
|
||||||
|
func valuesToMap(values map[string][]string) map[string]interface{} {
|
||||||
|
ret := make(map[string]interface{})
|
||||||
|
|
||||||
|
for key, value := range values {
|
||||||
|
if len(value) > 0 {
|
||||||
|
ret[key] = value[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user