diff --git a/README.md b/README.md index 379f910..0be08c8 100644 --- a/README.md +++ b/README.md @@ -1,5 +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) - +[![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) # 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 f70bd8d..02859c6 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -307,16 +307,17 @@ func (h *HooksFiles) Set(value string) error { // 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"` - PassEnvironmentToCommand []Argument `json:"pass-environment-to-command,omitempty"` - PassArgumentsToCommand []Argument `json:"pass-arguments-to-command,omitempty"` - JSONStringParameters []Argument `json:"parse-parameters-as-json,omitempty"` - TriggerRule *Rules `json:"trigger-rule,omitempty"` + 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"` + PassEnvironmentToCommand []Argument `json:"pass-environment-to-command,omitempty"` + PassArgumentsToCommand []Argument `json:"pass-arguments-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"` } // ParseJSONParameters decodes specified arguments to JSON objects and replaces the diff --git a/hook/hook_test.go b/hook/hook_test.go index 1fc9a88..39622e9 100644 --- a/hook/hook_test.go +++ b/hook/hook_test.go @@ -72,15 +72,15 @@ var argumentGetTests = []struct { value string ok bool }{ - {"header", "a", &map[string]interface{}{"a": "z"}, nil, nil, "z", true}, + {"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 + {"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) { @@ -99,14 +99,14 @@ var hookParseJSONParametersTests = []struct { rheaders, rquery, rpayload *map[string]interface{} ok bool }{ - {[]Argument{Argument{"header", "a", ""}}, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, nil, true}, + {[]Argument{Argument{"header", "a", ""}}, &map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, true}, {[]Argument{Argument{"url", "a", ""}}, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true}, {[]Argument{Argument{"payload", "a", ""}}, nil, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true}, - {[]Argument{Argument{"header", "z", ""}}, &map[string]interface{}{"z": `{}`}, nil, nil, &map[string]interface{}{"z": map[string]interface{}{}}, nil, nil, true}, + {[]Argument{Argument{"header", "z", ""}}, &map[string]interface{}{"Z": `{}`}, nil, nil, &map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true}, // failures - {[]Argument{Argument{"header", "z", ""}}, &map[string]interface{}{"z": ``}, nil, nil, &map[string]interface{}{"z": ``}, nil, nil, false}, // empty string + {[]Argument{Argument{"header", "z", ""}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // empty string {[]Argument{Argument{"header", "y", ""}}, &map[string]interface{}{"X": `{}`}, nil, nil, &map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter - {[]Argument{Argument{"string", "z", ""}}, &map[string]interface{}{"z": ``}, nil, nil, &map[string]interface{}{"z": ``}, nil, nil, false}, // invalid argument source + {[]Argument{Argument{"string", "z", ""}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source } func TestHookParseJSONParameters(t *testing.T) { @@ -126,9 +126,9 @@ var hookExtractCommandArgumentsTests = []struct { value []string ok bool }{ - {"test", []Argument{Argument{"header", "a", ""}}, &map[string]interface{}{"a": "z"}, nil, nil, []string{"test", "z"}, true}, + {"test", []Argument{Argument{"header", "a", ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"test", "z"}, true}, // failures - {"fail", []Argument{Argument{"payload", "a", ""}}, &map[string]interface{}{"a": "z"}, nil, nil, []string{"fail", ""}, false}, + {"fail", []Argument{Argument{"payload", "a", ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"fail", ""}, false}, } func TestHookExtractCommandArguments(t *testing.T) { @@ -171,14 +171,14 @@ var hookExtractCommandArgumentsForEnvTests = []struct { { "test", []Argument{Argument{"header", "a", ""}}, - &map[string]interface{}{"a": "z"}, nil, nil, + &map[string]interface{}{"A": "z"}, nil, nil, []string{"HOOK_a=z"}, true, }, { "test", []Argument{Argument{"header", "a", "MYKEY"}}, - &map[string]interface{}{"a": "z"}, nil, nil, + &map[string]interface{}{"A": "z"}, nil, nil, []string{"MYKEY=z"}, true, }, @@ -186,7 +186,7 @@ var hookExtractCommandArgumentsForEnvTests = []struct { { "fail", []Argument{Argument{"payload", "a", ""}}, - &map[string]interface{}{"a": "z"}, nil, nil, + &map[string]interface{}{"A": "z"}, nil, nil, []string{}, false, }, @@ -248,16 +248,16 @@ 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}, // 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 } func TestMatchRule(t *testing.T) { @@ -284,7 +284,7 @@ var andRuleTests = []struct { {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}}, }, - &map[string]interface{}{"a": "z", "b": "y"}, nil, nil, []byte{}, + &map[string]interface{}{"A": "z", "B": "y"}, nil, nil, []byte{}, true, false, }, { @@ -293,7 +293,7 @@ var andRuleTests = []struct { {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}}, }, - &map[string]interface{}{"a": "z", "b": "Y"}, nil, nil, []byte{}, + &map[string]interface{}{"A": "z", "B": "Y"}, nil, nil, []byte{}, false, false, }, // Complex test to cover Rules.Evaluate @@ -319,7 +319,7 @@ var andRuleTests = []struct { }, }, }, - &map[string]interface{}{"a": "z", "b": "y", "c": "x", "d": "w", "e": "X", "f": "X"}, nil, nil, []byte{}, + &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}, @@ -327,7 +327,7 @@ var andRuleTests = []struct { { "invalid rule", AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}}}}, - &map[string]interface{}{"y": "z"}, nil, nil, nil, + &map[string]interface{}{"Y": "z"}, nil, nil, nil, false, false, }, } @@ -355,7 +355,7 @@ var orRuleTests = []struct { {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}}, }, - &map[string]interface{}{"a": "z", "b": "X"}, nil, nil, []byte{}, + &map[string]interface{}{"A": "z", "B": "X"}, nil, nil, []byte{}, true, false, }, { @@ -364,7 +364,7 @@ var orRuleTests = []struct { {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}}, }, - &map[string]interface{}{"a": "X", "b": "y"}, nil, nil, []byte{}, + &map[string]interface{}{"A": "X", "B": "y"}, nil, nil, []byte{}, true, false, }, { @@ -373,7 +373,7 @@ var orRuleTests = []struct { {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}}, }, - &map[string]interface{}{"a": "Z", "b": "Y"}, nil, nil, []byte{}, + &map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil, []byte{}, false, false, }, // failures @@ -382,7 +382,7 @@ var orRuleTests = []struct { OrRule{ {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, }, - &map[string]interface{}{"y": "Z"}, nil, nil, []byte{}, + &map[string]interface{}{"Y": "Z"}, nil, nil, []byte{}, false, false, }, } @@ -404,8 +404,8 @@ 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) { diff --git a/test/hooks.json.tmpl b/test/hooks.json.tmpl index 4d94449..0a8aead 100644 --- a/test/hooks.json.tmpl +++ b/test/hooks.json.tmpl @@ -4,6 +4,7 @@ "execute-command": "{{ .Hookecho }}", "command-working-directory": "/", "include-command-output-in-response": true, + "trigger-rule-mismatch-http-response-code": 400, "pass-environment-to-command": [ { @@ -59,6 +60,7 @@ "command-working-directory": "/", "include-command-output-in-response": false, "response-message": "success", + "trigger-rule-mismatch-http-response-code": 999, "parse-parameters-as-json": [ { "source": "payload", diff --git a/webhook.go b/webhook.go index 8b79b6e..a2fd752 100644 --- a/webhook.go +++ b/webhook.go @@ -268,6 +268,17 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { return } + // Check if a return code is configured for the hook + if matchedHook.TriggerRuleMismatchHttpResponseCode != 0 { + // Check if the configured return code is supported by the http package + // by testing if there is a StatusText for this code. + if len(http.StatusText(matchedHook.TriggerRuleMismatchHttpResponseCode)) > 0 { + w.WriteHeader(matchedHook.TriggerRuleMismatchHttpResponseCode) + } else { + log.Printf("%s got matched, but the configured return code %d is unknown - defaulting to 200\n", matchedHook.ID, matchedHook.TriggerRuleMismatchHttpResponseCode) + } + } + // if none of the hooks got triggered log.Printf("%s got matched, but didn't get triggered because the trigger rules were not satisfied\n", matchedHook.ID) diff --git a/webhook_test.go b/webhook_test.go index 0b25de9..eef1e0a 100644 --- a/webhook_test.go +++ b/webhook_test.go @@ -515,5 +515,10 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00 `, }, - {"empty payload", "github", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`}, + // test with custom return code + {"empty payload", "github", nil, `{}`, false, http.StatusBadRequest, `Hook rules were not satisfied.`}, + // test with custom invalid http code, should default to 200 OK + {"empty payload", "bitbucket", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`}, + // test with no configured http return code, should default to 200 OK + {"empty payload", "gitlab", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`}, }