From 4f1089495dbda946cc83748a0585b9e7a7cb3650 Mon Sep 17 00:00:00 2001 From: Adnan Hajdarevic Date: Thu, 19 Nov 2020 21:22:13 +0100 Subject: [PATCH] wip --- docs/Hook-Definition.md | 2 +- docs/Referencing-Request-Values.md | 4 +- hooks.json | 13 ++++ internal/hook/hook.go | 10 +-- internal/hook/hook_test.go | 42 +++++----- internal/hook/request.go | 4 +- webhook.go | 121 +++++++++++++++++++---------- 7 files changed, 123 insertions(+), 73 deletions(-) create mode 100644 hooks.json diff --git a/docs/Hook-Definition.md b/docs/Hook-Definition.md index 41faa13..25668f5 100644 --- a/docs/Hook-Definition.md +++ b/docs/Hook-Definition.md @@ -32,7 +32,7 @@ Hooks are defined as objects in the JSON or YAML hooks configuration file. Pleas * `query` - object with query parameters and their respective values * `headers` - object with headers and their respective values * `base64EncodedBody` - base64 encoded request body - * Output of this command __MUST__ be valid JSON string which will be parsed by the webhook and accessible using the `context` as source when referencing values. + * Output of this command __MUST__ be valid JSON string which will be parsed by the webhook and accessible using the `pre-hook` as source when referencing values. ## Examples Check out [Hook examples page](Hook-Examples.md) for more complex examples of hooks. diff --git a/docs/Referencing-Request-Values.md b/docs/Referencing-Request-Values.md index 9731722..9a36e34 100644 --- a/docs/Referencing-Request-Values.md +++ b/docs/Referencing-Request-Values.md @@ -1,12 +1,12 @@ # Referencing request values There are four types of request values: -1. Context values +1. Pre-hook values These are the values provided by the `pre-hook-command` output. ```json { - "source": "context", + "source": "pre-hook", "name": "parameter-name" } ``` diff --git a/hooks.json b/hooks.json new file mode 100644 index 0000000..2514e08 --- /dev/null +++ b/hooks.json @@ -0,0 +1,13 @@ +[ + { + "id": "test", + "pre-hook-command": "/home/adnan/test.sh", + "execute-command": "/bin/echo", + "pass-arguments-to-command": [ + { + "source": "payload", + "name": "root" + } + ] + } +] diff --git a/internal/hook/hook.go b/internal/hook/hook.go index 9344691..bb7033b 100644 --- a/internal/hook/hook.go +++ b/internal/hook/hook.go @@ -35,7 +35,7 @@ const ( SourceQuery string = "url" SourceQueryAlias string = "query" SourcePayload string = "payload" - SourceContext string = "context" + SourcePreHook string = "pre-hook" SourceString string = "string" SourceEntirePayload string = "entire-payload" SourceEntireQuery string = "entire-query" @@ -443,8 +443,8 @@ func (ha *Argument) Get(r *Request) (string, error) { source = &r.Query case SourcePayload: source = &r.Payload - case SourceContext: - source = &r.Context + case SourcePreHook: + source = &r.PreHook case SourceString: return ha.Name, nil case SourceEntirePayload: @@ -591,8 +591,8 @@ func (h *Hook) ParseJSONParameters(r *Request) []error { source = &r.Headers case SourcePayload: source = &r.Payload - case SourceContext: - source = &r.Context + case SourcePreHook: + source = &r.PreHook case SourceQuery, SourceQueryAlias: source = &r.Query } diff --git a/internal/hook/hook_test.go b/internal/hook/hook_test.go index fa912ef..1a8056c 100644 --- a/internal/hook/hook_test.go +++ b/internal/hook/hook_test.go @@ -254,21 +254,21 @@ func TestExtractParameter(t *testing.T) { var argumentGetTests = []struct { source, name string - headers, query, payload, context map[string]interface{} + headers, query, payload, prehook map[string]interface{} value string ok bool }{ {"header", "a", map[string]interface{}{"A": "z"}, nil, nil, nil, "z", true}, {"url", "a", nil, map[string]interface{}{"a": "z"}, nil, nil, "z", true}, {"payload", "a", nil, nil, map[string]interface{}{"a": "z"}, nil, "z", true}, - {"context", "a", nil, nil, nil, map[string]interface{}{"a": "z"}, "z", true}, + {"prehook", "a", nil, nil, nil, map[string]interface{}{"a": "z"}, "z", true}, {"string", "a", nil, nil, nil, nil, "a", true}, // failures {"header", "a", nil, map[string]interface{}{"a": "z"}, map[string]interface{}{"a": "z"}, nil, "", false}, // nil headers {"url", "a", map[string]interface{}{"A": "z"}, nil, map[string]interface{}{"a": "z"}, nil, "", false}, // nil query {"payload", "a", map[string]interface{}{"A": "z"}, map[string]interface{}{"a": "z"}, nil, nil, "", false}, // nil payload - {"context", "a", nil, nil, nil, nil, "", false}, // nil context - {"foo", "a", map[string]interface{}{"A": "z"}, nil, nil, nil, "", false}, // invalid source + {"prehook", "a", nil, nil, nil, nil, "", false}, // nil prehook + {"foo", "a", map[string]interface{}{"A": "z"}, nil, nil, nil, "", false}, // invalid source } func TestArgumentGet(t *testing.T) { @@ -278,7 +278,7 @@ func TestArgumentGet(t *testing.T) { Headers: tt.headers, Query: tt.query, Payload: tt.payload, - Context: tt.context, + PreHook: tt.prehook, } value, err := a.Get(r) if (err == nil) != tt.ok || value != tt.value { @@ -289,14 +289,14 @@ func TestArgumentGet(t *testing.T) { var hookParseJSONParametersTests = []struct { params []Argument - headers, query, payload, context map[string]interface{} - rheaders, rquery, rpayload, rcontext map[string]interface{} + headers, query, payload, prehook map[string]interface{} + rheaders, rquery, rpayload, rprehook map[string]interface{} ok bool }{ {[]Argument{Argument{"header", "a", "", false}}, map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, nil, map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, nil, true}, {[]Argument{Argument{"url", "a", "", false}}, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, nil, true}, {[]Argument{Argument{"payload", "a", "", false}}, nil, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true}, - {[]Argument{Argument{"context", "a", "", false}}, nil, nil, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true}, + {[]Argument{Argument{"prehook", "a", "", false}}, nil, nil, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true}, {[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": `{}`}, nil, nil, nil, map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, nil, true}, // failures {[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, nil, false}, // empty string @@ -311,7 +311,7 @@ func TestHookParseJSONParameters(t *testing.T) { Headers: tt.headers, Query: tt.query, Payload: tt.payload, - Context: tt.context, + PreHook: tt.prehook, } err := h.ParseJSONParameters(r) if (err == nil) != tt.ok || !reflect.DeepEqual(tt.headers, tt.rheaders) { @@ -323,7 +323,7 @@ func TestHookParseJSONParameters(t *testing.T) { var hookExtractCommandArgumentsTests = []struct { exec string args []Argument - headers, query, payload, context map[string]interface{} + headers, query, payload, prehook map[string]interface{} value []string ok bool }{ @@ -339,7 +339,7 @@ func TestHookExtractCommandArguments(t *testing.T) { Headers: tt.headers, Query: tt.query, Payload: tt.payload, - Context: tt.context, + PreHook: tt.prehook, } value, err := h.ExtractCommandArguments(r) if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) { @@ -370,7 +370,7 @@ func TestHookExtractCommandArguments(t *testing.T) { var hookExtractCommandArgumentsForEnvTests = []struct { exec string args []Argument - headers, query, payload, context map[string]interface{} + headers, query, payload, prehook map[string]interface{} value []string ok bool }{ @@ -406,7 +406,7 @@ func TestHookExtractCommandArgumentsForEnv(t *testing.T) { Headers: tt.headers, Query: tt.query, Payload: tt.payload, - Context: tt.context, + PreHook: tt.prehook, } value, err := h.ExtractCommandArgumentsForEnv(r) if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) { @@ -486,7 +486,7 @@ func TestHooksMatch(t *testing.T) { var matchRuleTests = []struct { typ, regex, secret, value, ipRange string param Argument - headers, query, payload, context map[string]interface{} + headers, query, payload, prehook map[string]interface{} body []byte remoteAddr string ok bool @@ -533,7 +533,7 @@ func TestMatchRule(t *testing.T) { Headers: tt.headers, Query: tt.query, Payload: tt.payload, - Context: tt.context, + PreHook: tt.prehook, Body: tt.body, RawRequest: &http.Request{ RemoteAddr: tt.remoteAddr, @@ -549,7 +549,7 @@ func TestMatchRule(t *testing.T) { var andRuleTests = []struct { desc string // description of the test case rule AndRule - headers, query, payload, context map[string]interface{} + headers, query, payload, prehook map[string]interface{} body []byte ok bool err bool @@ -614,7 +614,7 @@ func TestAndRule(t *testing.T) { Headers: tt.headers, Query: tt.query, Payload: tt.payload, - Context: tt.context, + PreHook: tt.prehook, Body: tt.body, } ok, err := tt.rule.Evaluate(r) @@ -627,7 +627,7 @@ func TestAndRule(t *testing.T) { var orRuleTests = []struct { desc string // description of the test case rule OrRule - headers, query, payload, context map[string]interface{} + headers, query, payload, prehook map[string]interface{} body []byte ok bool err bool @@ -676,7 +676,7 @@ func TestOrRule(t *testing.T) { Headers: tt.headers, Query: tt.query, Payload: tt.payload, - Context: tt.context, + PreHook: tt.prehook, Body: tt.body, } ok, err := tt.rule.Evaluate(r) @@ -689,7 +689,7 @@ func TestOrRule(t *testing.T) { var notRuleTests = []struct { desc string // description of the test case rule NotRule - headers, query, payload, context map[string]interface{} + headers, query, payload, prehook map[string]interface{} body []byte ok bool err bool @@ -704,7 +704,7 @@ func TestNotRule(t *testing.T) { Headers: tt.headers, Query: tt.query, Payload: tt.payload, - Context: tt.context, + PreHook: tt.prehook, Body: tt.body, } ok, err := tt.rule.Evaluate(r) diff --git a/internal/hook/request.go b/internal/hook/request.go index 7fa9714..dd3ca95 100644 --- a/internal/hook/request.go +++ b/internal/hook/request.go @@ -31,8 +31,8 @@ type Request struct { // Payload is a map of the parsed payload. Payload map[string]interface{} - // Context is a map of the parsed pre-hook command result - Context map[string]interface{} + // PreHook is a map of the parsed pre-hook command result + PreHook map[string]interface{} // The underlying HTTP request. RawRequest *http.Request diff --git a/webhook.go b/webhook.go index 9949b5e..ae79504 100644 --- a/webhook.go +++ b/webhook.go @@ -473,14 +473,14 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { if matchedHook.PreHookCommand != "" { // check the command exists - var lookpath string + var searchPath string if filepath.IsAbs(matchedHook.PreHookCommand) || matchedHook.CommandWorkingDirectory == "" { - lookpath = matchedHook.PreHookCommand + searchPath = matchedHook.PreHookCommand } else { - lookpath = filepath.Join(matchedHook.CommandWorkingDirectory, matchedHook.PreHookCommand) + searchPath = filepath.Join(matchedHook.CommandWorkingDirectory, matchedHook.PreHookCommand) } - preHookCommandPath, err := exec.LookPath(lookpath) + preHookCommandPath, err := exec.LookPath(searchPath) if err != nil { log.Printf("[%s] unable to locate pre-hook command: '%s', %+v\n", req.ID, matchedHook.PreHookCommand, err) @@ -489,48 +489,85 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { s := strings.Fields(matchedHook.PreHookCommand)[0] log.Printf("[%s] please use a wrapper script to provide arguments to pre-hook command for '%s'\n", req.ID, s) } - } else { - preHookCommandStdin := hook.PreHookContext{ - HookID: matchedHook.ID, - Method: r.Method, - Base64EncodedBody: base64.StdEncoding.EncodeToString(req.Body), - RemoteAddr: r.RemoteAddr, - URI: r.RequestURI, - Host: r.Host, - Headers: r.Header, - Query: r.URL.Query(), - } - if preHookCommandStdinJSONString, err := json.Marshal(preHookCommandStdin); err != nil { - log.Printf("[%s] unable to encode pre-hook context as JSON string for the pre-hook command: %+v\n", req.ID, err) - } else { - preHookCommand := exec.Command(preHookCommandPath) - preHookCommand.Dir = matchedHook.CommandWorkingDirectory - preHookCommand.Env = append(os.Environ()) + writeHttpResponseCode(w, req.ID, matchedHook.ID, http.StatusInternalServerError) + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + fmt.Fprint(w, "Error occurred while executing the hook's pre-hook command. Please check your logs for more details.") + return + } - if preHookCommandStdinPipe, err := preHookCommand.StdinPipe(); err != nil { - log.Printf("[%s] unable to acquire stdin pipe for the pre-hook command: %+v\n", req.ID, err) - } else { - _, err := io.WriteString(preHookCommandStdinPipe, string(preHookCommandStdinJSONString)) - preHookCommandStdinPipe.Close() - if err != nil { - log.Printf("[%s] unable to write to pre-hook command stdin: %+v\n", req.ID, err) - } else { - log.Printf("[%s] executing pre-hook command %s (%s) using %s as cwd\n", req.ID, matchedHook.PreHookCommand, preHookCommand.Path, preHookCommand.Dir) + preHookCommandStdin := hook.PreHookContext{ + HookID: matchedHook.ID, + Method: r.Method, + Base64EncodedBody: base64.StdEncoding.EncodeToString(req.Body), + RemoteAddr: r.RemoteAddr, + URI: r.RequestURI, + Host: r.Host, + Headers: r.Header, + Query: r.URL.Query(), + } - if preHookCommandOutput, err := preHookCommand.CombinedOutput(); err != nil { - log.Printf("[%s] unable to execute pre-hook command: %+v\n", req.ID, err) - } else { - JSONDecoder := json.NewDecoder(strings.NewReader(string(preHookCommandOutput))) - JSONDecoder.UseNumber() + preHookCommandStdinJSONString, err := json.Marshal(preHookCommandStdin) - if err := JSONDecoder.Decode(&req.Context); err != nil { - log.Printf("[%s] unable to parse pre-hook command output: %+v\npre-hook command output was: %+v\n", req.ID, err, string(preHookCommandOutput)) - } - } - } - } - } + if err != nil { + log.Printf("[%s] unable to encode pre-hook context as JSON string for the pre-hook command: %+v\n", req.ID, err) + + writeHttpResponseCode(w, req.ID, matchedHook.ID, http.StatusInternalServerError) + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + fmt.Fprint(w, "Error occurred while executing the hook's pre-hook command. Please check your logs for more details.") + return + } + preHookCommand := exec.Command(preHookCommandPath) + preHookCommand.Dir = matchedHook.CommandWorkingDirectory + preHookCommand.Env = append(os.Environ()) + + preHookCommandStdinPipe, err := preHookCommand.StdinPipe() + + if err != nil { + log.Printf("[%s] unable to acquire stdin pipe for the pre-hook command: %+v\n", req.ID, err) + + writeHttpResponseCode(w, req.ID, matchedHook.ID, http.StatusInternalServerError) + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + fmt.Fprint(w, "Error occurred while executing the hook's pre-hook command. Please check your logs for more details.") + return + } + + _, err = io.WriteString(preHookCommandStdinPipe, string(preHookCommandStdinJSONString)) + + preHookCommandStdinPipe.Close() + + if err != nil { + log.Printf("[%s] unable to write to pre-hook command stdin: %+v\n", req.ID, err) + + writeHttpResponseCode(w, req.ID, matchedHook.ID, http.StatusInternalServerError) + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + fmt.Fprint(w, "Error occurred while executing the hook's pre-hook command. Please check your logs for more details.") + return + } + + log.Printf("[%s] executing pre-hook command %s (%s) using %s as cwd\n", req.ID, matchedHook.PreHookCommand, preHookCommand.Path, preHookCommand.Dir) + + preHookCommandOutput, err := preHookCommand.CombinedOutput() + + if err != nil { + log.Printf("[%s] unable to execute pre-hook command: %+v\n", req.ID, err) + + writeHttpResponseCode(w, req.ID, matchedHook.ID, http.StatusInternalServerError) + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + fmt.Fprint(w, "Error occurred while executing the hook's pre-hook command. Please check your logs for more details.") + return + } + + JSONDecoder := json.NewDecoder(strings.NewReader(string(preHookCommandOutput))) + JSONDecoder.UseNumber() + + if err := JSONDecoder.Decode(&req.PreHook); err != nil { + log.Printf("[%s] unable to parse pre-hook command output: %+v\npre-hook command output was: %+v\n", req.ID, err, string(preHookCommandOutput)) + + writeHttpResponseCode(w, req.ID, matchedHook.ID, http.StatusInternalServerError) + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + fmt.Fprint(w, "Error occurred while executing the hook's pre-hook command. Please check your logs for more details.") + return } }