From 173273b466a064ab345a7204881f874ce4032f4c Mon Sep 17 00:00:00 2001 From: HandcraftedBits Date: Mon, 20 Feb 2017 22:00:39 -0500 Subject: [PATCH 1/4] Add IP whitelist match rule. --- hook/hook.go | 73 ++++++++++++++++++++++++++++++------- hook/hook_test.go | 92 +++++++++++++++++++++++++++-------------------- webhook.go | 2 +- 3 files changed, 115 insertions(+), 52 deletions(-) diff --git a/hook/hook.go b/hook/hook.go index 02859c6..b653537 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io/ioutil" + "net" "net/textproto" "reflect" "regexp" @@ -100,6 +101,48 @@ func CheckPayloadSignature(payload []byte, secret string, signature string) (str return expectedMAC, err } +// 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. + + ip := remoteAddr + + if strings.LastIndex(remoteAddr, ":") != -1 { + ip = remoteAddr[0:strings.LastIndex(remoteAddr, ":")] + } + + ip = strings.TrimSpace(ip) + + // IPv6 addresses will likely be surrounded by [], so don't forget to remove those. + + if strings.HasPrefix(ip, "[") && strings.HasSuffix(ip, "]") { + ip = ip[1 : len(ip)-1] + } + + parsedIP := net.ParseIP(strings.TrimSpace(ip)) + + if parsedIP == nil { + return false, fmt.Errorf("invalid IP address found in remote address '%s'", remoteAddr) + } + + // Extract IP range in CIDR form. If a single IP address is provided, turn it into CIDR form. + + ipRange = strings.TrimSpace(ipRange) + + if strings.Index(ipRange, "/") == -1 { + ipRange = ipRange + "/32" + } + + _, cidr, err := net.ParseCIDR(ipRange) + + if err != nil { + return false, err + } + + return cidr.Contains(parsedIP), 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 @@ -479,16 +522,16 @@ type Rules struct { // 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, error) { +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) + return r.And.Evaluate(headers, query, payload, body, remoteAddr) case r.Or != nil: - return r.Or.Evaluate(headers, query, payload, body) + return r.Or.Evaluate(headers, query, payload, body, remoteAddr) case r.Not != nil: - return r.Not.Evaluate(headers, query, payload, body) + return r.Not.Evaluate(headers, query, payload, body, remoteAddr) case r.Match != nil: - return r.Match.Evaluate(headers, query, payload, body) + return r.Match.Evaluate(headers, query, payload, body, remoteAddr) } return false, nil @@ -498,11 +541,11 @@ func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, body *[ 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, error) { +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) + rv, err := v.Evaluate(headers, query, payload, body, remoteAddr) if err != nil { return false, err } @@ -520,11 +563,11 @@ func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, body 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, error) { +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) + rv, err := v.Evaluate(headers, query, payload, body, remoteAddr) if err != nil { return false, err } @@ -542,8 +585,8 @@ func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, body * 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, error) { - rv, err := Rules(r).Evaluate(headers, query, payload, body) +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 } @@ -554,6 +597,7 @@ type MatchRule struct { 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 @@ -561,10 +605,15 @@ const ( MatchValue string = "value" MatchRegex string = "regex" MatchHashSHA1 string = "payload-hash-sha1" + IPWhitelist string = "ip-whitelist" ) // Evaluate MatchRule will return based on the type -func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) (bool, error) { +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 arg, ok := r.Parameter.Get(headers, query, payload); ok { switch r.Type { case MatchValue: diff --git a/hook/hook_test.go b/hook/hook_test.go index 39622e9..a274196 100644 --- a/hook/hook_test.go +++ b/hook/hook_test.go @@ -241,29 +241,43 @@ func TestHooksMatch(t *testing.T) { } var matchRuleTests = []struct { - typ, regex, secret, value string - param Argument - headers, query, payload *map[string]interface{} - body []byte - ok bool - err bool + 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", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false}, - {"regex", "^z", "", "z", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false}, - {"payload-hash-sha1", "", "secret", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), true, false}, + {"value", "", "", "z", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false}, + {"regex", "^z", "", "z", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false}, + {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false}, // failures - {"value", "", "", "X", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false}, - {"regex", "^X", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false}, - {"value", "", "2", "X", Argument{"header", "a", ""}, &map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, false, false}, // reference invalid header + {"value", "", "", "X", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false}, + {"regex", "^X", "", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false}, + {"value", "", "2", "X", "", Argument{"header", "a", ""}, &map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, false}, // reference invalid header // errors - {"regex", "*", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, true}, // invalid regex - {"payload-hash-sha1", "", "secret", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac + {"regex", "*", "", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex + {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", ""}, &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} - ok, err := r.Evaluate(tt.headers, tt.query, tt.payload, &tt.body) + 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)) } @@ -281,8 +295,8 @@ var andRuleTests = []struct { { "(a=z, b=y): a=z && b=y", AndRule{ - {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, - {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}}, + {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, false, @@ -290,8 +304,8 @@ var andRuleTests = []struct { { "(a=z, b=Y): a=z && b=y", AndRule{ - {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, - {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}}, + {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, false, @@ -300,22 +314,22 @@ var andRuleTests = []struct { { "(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", ""}}}, + {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}}, { And: &AndRule{ - {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}}, - {Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", ""}}}, + {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", ""}}}, + {Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", ""}, ""}}, + {Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", ""}, ""}}, }, }, { Not: &NotRule{ - Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", ""}}, + Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", ""}, ""}, }, }, }, @@ -326,7 +340,7 @@ var andRuleTests = []struct { // failures { "invalid rule", - AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}}}}, + AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}, ""}}}, &map[string]interface{}{"Y": "z"}, nil, nil, nil, false, false, }, @@ -334,7 +348,7 @@ var andRuleTests = []struct { func TestAndRule(t *testing.T) { for _, tt := range andRuleTests { - ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body) + 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) } @@ -352,8 +366,8 @@ var orRuleTests = []struct { { "(a=z, b=X): a=z || b=y", OrRule{ - {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, - {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}}, + {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, false, @@ -361,8 +375,8 @@ var orRuleTests = []struct { { "(a=X, b=y): a=z || b=y", OrRule{ - {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, - {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}}, + {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, false, @@ -370,8 +384,8 @@ var orRuleTests = []struct { { "(a=Z, b=Y): a=z || b=y", OrRule{ - {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, - {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}}, + {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, false, @@ -380,7 +394,7 @@ var orRuleTests = []struct { { "invalid rule", OrRule{ - {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, + {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}}, }, &map[string]interface{}{"Y": "Z"}, nil, nil, []byte{}, false, false, @@ -389,7 +403,7 @@ var orRuleTests = []struct { func TestOrRule(t *testing.T) { for _, tt := range orRuleTests { - ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body) + 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) } @@ -404,13 +418,13 @@ var notRuleTests = []struct { ok bool err bool }{ - {"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false}, - {"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false}, + {"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}, ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false}, + {"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}}, &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) + 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) } diff --git a/webhook.go b/webhook.go index 8d79633..6914734 100644 --- a/webhook.go +++ b/webhook.go @@ -243,7 +243,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { if matchedHook.TriggerRule == nil { ok = true } else { - ok, err = matchedHook.TriggerRule.Evaluate(&headers, &query, &payload, &body) + ok, err = matchedHook.TriggerRule.Evaluate(&headers, &query, &payload, &body, r.RemoteAddr) if err != nil { msg := fmt.Sprintf("error evaluating hook: %s", err) log.Printf(msg) From af22498d1e49a2bd733d56071af5becdebe2477f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adnan=20Hajdarevi=C4=87?= Date: Thu, 6 Apr 2017 11:09:17 +0200 Subject: [PATCH 2/4] Update README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 0be08c8..c8ae1a3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,12 @@ [![ghit.me](https://ghit.me/badge.svg?repo=adnanh/webhook)](https://ghit.me/repo/adnanh/webhook) [![Join the chat at https://gitter.im/adnanh/webhook](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/adnanh/webhook?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Flattr this](https://button.flattr.com/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=adnanh&url=https%3A%2F%2Fwww.github.com%2Fadnanh%2Fwebhook) [Donate via PayPal](https://paypal.me/hookdoo) | [Patreon page](https://www.patreon.com/webhook) + + +# Hookdoo +hookdoo + +If you don't have time to waste configuring, hosting, debugging and maintaining your webhook instance, we offer a __SaaS__ solution that has all of the capabilities webhook provides, plus a lot more, and all that packaged in a nice friendly web interface. If you are interested, find out more at [hookdoo website](https://www.hookdoo.com/). If you have any questions, you can contact us at info@hookdoo.com + + # What is webhook? [webhook](https://github.com/adnanh/webhook/) is a lightweight configurable tool written in Go, that allows you to easily create HTTP endpoints (hooks) on your server, which you can use to execute configured commands. You can also pass data from the HTTP request (such as headers, payload or query variables) to your commands. [webhook](https://github.com/adnanh/webhook/) also allows you to specify rules which have to be satisfied in order for the hook to be triggered. From 6b1021925bab608e24d38d5889c1722c87214b42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Pinson?= Date: Thu, 6 Apr 2017 15:36:35 +0200 Subject: [PATCH 3/4] Add sha256 payload check --- README.md | 8 -------- hook/hook.go | 32 ++++++++++++++++++++++++++++---- hook/hook_test.go | 34 +++++++++++++++++++++++++++++++--- 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c8ae1a3..0be08c8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,4 @@ [![ghit.me](https://ghit.me/badge.svg?repo=adnanh/webhook)](https://ghit.me/repo/adnanh/webhook) [![Join the chat at https://gitter.im/adnanh/webhook](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/adnanh/webhook?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Flattr this](https://button.flattr.com/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=adnanh&url=https%3A%2F%2Fwww.github.com%2Fadnanh%2Fwebhook) [Donate via PayPal](https://paypal.me/hookdoo) | [Patreon page](https://www.patreon.com/webhook) - - -# Hookdoo -hookdoo - -If you don't have time to waste configuring, hosting, debugging and maintaining your webhook instance, we offer a __SaaS__ solution that has all of the capabilities webhook provides, plus a lot more, and all that packaged in a nice friendly web interface. If you are interested, find out more at [hookdoo website](https://www.hookdoo.com/). If you have any questions, you can contact us at info@hookdoo.com - - # What is webhook? [webhook](https://github.com/adnanh/webhook/) is a lightweight configurable tool written in Go, that allows you to easily create HTTP endpoints (hooks) on your server, which you can use to execute configured commands. You can also pass data from the HTTP request (such as headers, payload or query variables) to your commands. [webhook](https://github.com/adnanh/webhook/) also allows you to specify rules which have to be satisfied in order for the hook to be triggered. diff --git a/hook/hook.go b/hook/hook.go index b653537..b336e54 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -3,6 +3,7 @@ package hook import ( "crypto/hmac" "crypto/sha1" + "crypto/sha256" "encoding/hex" "encoding/json" "errors" @@ -101,6 +102,25 @@ func CheckPayloadSignature(payload []byte, secret string, signature string) (str return expectedMAC, err } +// CheckPayloadSignature256 calculates and verifies SHA256 signature of the given payload +func CheckPayloadSignature256(payload []byte, secret string, signature string) (string, error) { + if strings.HasPrefix(signature, "sha256=") { + signature = signature[7:] + } + + 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 +} + // 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) { @@ -602,10 +622,11 @@ type MatchRule struct { // Constants for the MatchRule type const ( - MatchValue string = "value" - MatchRegex string = "regex" - MatchHashSHA1 string = "payload-hash-sha1" - IPWhitelist string = "ip-whitelist" + MatchValue string = "value" + MatchRegex string = "regex" + MatchHashSHA1 string = "payload-hash-sha1" + MatchHashSHA256 string = "payload-hash-sha256" + IPWhitelist string = "ip-whitelist" ) // Evaluate MatchRule will return based on the type @@ -623,6 +644,9 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod case MatchHashSHA1: _, err := CheckPayloadSignature(*body, r.Secret, arg) return err == nil, err + case MatchHashSHA256: + _, err := CheckPayloadSignature256(*body, r.Secret, arg) + return err == nil, err } } return false, nil diff --git a/hook/hook_test.go b/hook/hook_test.go index a274196..7828e00 100644 --- a/hook/hook_test.go +++ b/hook/hook_test.go @@ -33,6 +33,32 @@ func TestCheckPayloadSignature(t *testing.T) { } } +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}, +} + +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 && strings.Contains(err.Error(), tt.mac) { + t.Errorf("error message should not disclose expected mac: %s", err) + } + } +} + var extractParameterTests = []struct { s string params interface{} @@ -249,9 +275,10 @@ var matchRuleTests = []struct { ok bool err bool }{ - {"value", "", "", "z", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false}, - {"regex", "^z", "", "z", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false}, - {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false}, + {"value", "", "", "z", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false}, + {"regex", "^z", "", "z", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false}, + {"payload-hash-sha1", "", "secret", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), true, false}, + {"payload-hash-sha256", "", "secret", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), true, false}, // failures {"value", "", "", "X", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false}, {"regex", "^X", "", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false}, @@ -259,6 +286,7 @@ var matchRuleTests = []struct { // errors {"regex", "*", "", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac + {"payload-hash-sha256", "", "secret", "", Argument{"header", "a", ""}, &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 From 81b1bd7c7b4a281939f006dfe3daca6416d5ad91 Mon Sep 17 00:00:00 2001 From: Adnan Hajdarevic Date: Fri, 7 Apr 2017 10:59:40 +0200 Subject: [PATCH 4/4] bump up the version to 2.6.3 --- webhook.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webhook.go b/webhook.go index 6914734..e07b04d 100644 --- a/webhook.go +++ b/webhook.go @@ -21,7 +21,7 @@ import ( ) const ( - version = "2.6.2" + version = "2.6.3" ) var (