From f1ebc440a4bcdd30b5af9889961211d72063c754 Mon Sep 17 00:00:00 2001 From: Adnan Hajdarevic Date: Wed, 27 May 2015 09:16:26 +0200 Subject: [PATCH 1/4] match all hooks with the same id --- hook/hook.go | 17 ++++++++++++++ webhook.go | 56 ++++++++++++++++++++++++++------------------- webhook_windows.go | 57 +++++++++++++++++++++++++++------------------- 3 files changed, 82 insertions(+), 48 deletions(-) diff --git a/hook/hook.go b/hook/hook.go index 5315b64..f3cb78b 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -260,6 +260,23 @@ func (h *Hooks) Match(id string) *Hook { return nil } +// MatchAll iterates through Hooks and returns all of the hooks that match the +// given ID, if no hook matches the given ID, nil is returned +func (h *Hooks) MatchAll(id string) []*Hook { + matchedHooks := make([]*Hook, 0) + for i := range *h { + if (*h)[i].ID == id { + matchedHooks = append(matchedHooks, &(*h)[i]) + } + } + + if len(matchedHooks) > 0 { + return matchedHooks + } + + return nil +} + // Rules is a structure that contains one of the valid rule types type Rules struct { And *AndRule `json:"and"` diff --git a/webhook.go b/webhook.go index 9c9ccb6..7279e34 100644 --- a/webhook.go +++ b/webhook.go @@ -25,7 +25,7 @@ import ( ) const ( - version = "2.3.3" + version = "2.3.4" ) var ( @@ -144,10 +144,10 @@ func main() { func hookHandler(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - hook := hooks.Match(id) + matchedHooks := hooks.MatchAll(id) - if hook != nil { - log.Printf("%s got matched\n", id) + if matchedHooks != nil { + log.Printf("%s got matched (%d time(s))\n", id, len(matchedHooks)) body, err := ioutil.ReadAll(r.Body) if err != nil { @@ -183,13 +183,26 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { } } - hook.ParseJSONParameters(&headers, &query, &payload) - // handle hook - go handleHook(hook, &headers, &query, &payload, &body) + for _, hook := range matchedHooks { + hook.ParseJSONParameters(&headers, &query, &payload) + if hook.TriggerRule == nil || hook.TriggerRule != nil && hook.TriggerRule.Evaluate(&headers, &query, &payload, &body) { + log.Printf("%s hook triggered successfully\n", hook.ID) - // send the hook defined response message - fmt.Fprintf(w, hook.ResponseMessage) + go handleHook(hook, &headers, &query, &payload, &body) + + // send the hook defined response message + fmt.Fprintf(w, hook.ResponseMessage) + + return + } + } + + // if none of the hooks got triggered + log.Printf("%s got matched (%d time(s)), but didn't get triggered because the trigger rules were not satisfied\n", matchedHooks[0].ID, len(matchedHooks)) + + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Hook rules were not satisfied.") } else { w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, "Hook not found.") @@ -197,26 +210,21 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { } func handleHook(hook *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) { - if hook.TriggerRule == nil || hook.TriggerRule != nil && hook.TriggerRule.Evaluate(headers, query, payload, body) { - log.Printf("%s hook triggered successfully\n", hook.ID) + cmd := exec.Command(hook.ExecuteCommand) + cmd.Args = hook.ExtractCommandArguments(headers, query, payload) + cmd.Dir = hook.CommandWorkingDirectory - cmd := exec.Command(hook.ExecuteCommand) - cmd.Args = hook.ExtractCommandArguments(headers, query, payload) - cmd.Dir = hook.CommandWorkingDirectory + log.Printf("executing %s (%s) with arguments %s using %s as cwd\n", hook.ExecuteCommand, cmd.Path, cmd.Args, cmd.Dir) - log.Printf("executing %s (%s) with arguments %s using %s as cwd\n", hook.ExecuteCommand, cmd.Path, cmd.Args, cmd.Dir) + out, err := cmd.Output() - out, err := cmd.Output() + log.Printf("stdout: %s\n", out) - log.Printf("stdout: %s\n", out) - - if err != nil { - log.Printf("stderr: %+v\n", err) - } - log.Printf("finished handling %s\n", hook.ID) - } else { - log.Printf("%s hook did not get triggered\n", hook.ID) + if err != nil { + log.Printf("stderr: %+v\n", err) } + + log.Printf("finished handling %s\n", hook.ID) } func reloadHooks() { diff --git a/webhook_windows.go b/webhook_windows.go index f42e8bf..1c26828 100644 --- a/webhook_windows.go +++ b/webhook_windows.go @@ -23,7 +23,7 @@ import ( ) const ( - version = "2.3.3" + version = "2.3.4" ) var ( @@ -133,10 +133,10 @@ func main() { func hookHandler(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - hook := hooks.Match(id) + matchedHooks := hooks.MatchAll(id) - if hook != nil { - log.Printf("%s got matched\n", id) + if matchedHooks != nil { + log.Printf("%s got matched (%d time(s))\n", id, len(matchedHooks)) body, err := ioutil.ReadAll(r.Body) if err != nil { @@ -172,13 +172,27 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { } } - hook.ParseJSONParameters(&headers, &query, &payload) - // handle hook - go handleHook(hook, &headers, &query, &payload, &body) + for _, hook := range matchedHooks { + hook.ParseJSONParameters(&headers, &query, &payload) - // send the hook defined response message - fmt.Fprintf(w, hook.ResponseMessage) + if hook.TriggerRule == nil || hook.TriggerRule != nil && hook.TriggerRule.Evaluate(&headers, &query, &payload, &body) { + log.Printf("%s hook triggered successfully\n", hook.ID) + + go handleHook(hook, &headers, &query, &payload, &body) + + // send the hook defined response message + fmt.Fprintf(w, hook.ResponseMessage) + + return + } + } + + // if none of the hooks got triggered + log.Printf("%s got matched (%d time(s)), but didn't get triggered because the trigger rules were not satisfied\n", matchedHooks[0].ID, len(matchedHooks)) + + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Hook rules were not satisfied.") } else { w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, "Hook not found.") @@ -186,26 +200,21 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { } func handleHook(hook *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) { - if hook.TriggerRule == nil || hook.TriggerRule != nil && hook.TriggerRule.Evaluate(headers, query, payload, body) { - log.Printf("%s hook triggered successfully\n", hook.ID) + cmd := exec.Command(hook.ExecuteCommand) + cmd.Args = hook.ExtractCommandArguments(headers, query, payload) + cmd.Dir = hook.CommandWorkingDirectory - cmd := exec.Command(hook.ExecuteCommand) - cmd.Args = hook.ExtractCommandArguments(headers, query, payload) - cmd.Dir = hook.CommandWorkingDirectory + log.Printf("executing %s (%s) with arguments %s using %s as cwd\n", hook.ExecuteCommand, cmd.Path, cmd.Args, cmd.Dir) - log.Printf("executing %s (%s) with arguments %s using %s as cwd\n", hook.ExecuteCommand, cmd.Path, cmd.Args, cmd.Dir) + out, err := cmd.Output() - out, err := cmd.Output() + log.Printf("stdout: %s\n", out) - log.Printf("stdout: %s\n", out) - - if err != nil { - log.Printf("stderr: %+v\n", err) - } - log.Printf("finished handling %s\n", hook.ID) - } else { - log.Printf("%s hook did not get triggered\n", hook.ID) + if err != nil { + log.Printf("stderr: %+v\n", err) } + + log.Printf("finished handling %s\n", hook.ID) } func reloadHooks() { From 9c545a745f4d8b778a7ec3f055cf55645f8cd52c Mon Sep 17 00:00:00 2001 From: Adnan Hajdarevic Date: Sat, 6 Jun 2015 14:19:52 +0200 Subject: [PATCH 2/4] return command output, pass whole payload as json to the command --- hook/hook.go | 25 ++++++++++++++++---- hooks.json.example | 1 + webhook.go | 54 +++++++++++++++++++++++++++--------------- webhook_windows.go | 58 +++++++++++++++++++++++++++++----------------- 4 files changed, 94 insertions(+), 44 deletions(-) diff --git a/hook/hook.go b/hook/hook.go index f3cb78b..3cb5b58 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -16,10 +16,11 @@ import ( // Constants used to specify the parameter source const ( - SourceHeader string = "header" - SourceQuery string = "url" - SourcePayload string = "payload" - SourceString string = "string" + SourceHeader string = "header" + SourceQuery string = "url" + SourcePayload string = "payload" + SourceString string = "string" + SourceEntirePayload string = "entire-payload" ) // CheckPayloadSignature calculates and verifies SHA1 signature of the given payload @@ -151,6 +152,14 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string source = payload case SourceString: return ha.Name, true + case SourceEntirePayload: + r, err := json.Marshal(payload) + + if err != nil { + return "", false + } + + return string(r), true } if source != nil { @@ -166,6 +175,7 @@ type Hook struct { ExecuteCommand string `json:"execute-command"` CommandWorkingDirectory string `json:"command-working-directory"` ResponseMessage string `json:"response-message"` + CaptureCommandOutput bool `json:"include-command-output-in-response"` PassArgumentsToCommand []Argument `json:"pass-arguments-to-command"` JSONStringParameters []Argument `json:"parse-parameters-as-json"` TriggerRule *Rules `json:"trigger-rule"` @@ -385,3 +395,10 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod } return false } + +// CommandStatusResponse type encapsulates the executed command exit code, message, stdout and stderr +type CommandStatusResponse struct { + ResponseMessage string `json:"message"` + Output string `json:"output"` + Error string `json:"error"` +} diff --git a/hooks.json.example b/hooks.json.example index 7c6c040..36ad7a3 100644 --- a/hooks.json.example +++ b/hooks.json.example @@ -3,6 +3,7 @@ "id": "webhook", "execute-command": "/home/adnan/redeploy-go-webhook.sh", "command-working-directory": "/home/adnan/go", + "response-message": "I got the payload!", "pass-arguments-to-command": [ { diff --git a/webhook.go b/webhook.go index 7279e34..207a25a 100644 --- a/webhook.go +++ b/webhook.go @@ -165,7 +165,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { contentType := r.Header.Get("Content-Type") - if strings.HasPrefix(contentType, "application/json") { + if strings.Contains(contentType, "json") { decoder := json.NewDecoder(strings.NewReader(string(body))) decoder.UseNumber() @@ -174,7 +174,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("error parsing JSON payload %+v\n", err) } - } else if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") { + } else if strings.Contains(contentType, "form") { fd, err := url.ParseQuery(string(body)) if err != nil { log.Printf("error parsing form payload %+v\n", err) @@ -184,15 +184,18 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { } // handle hook - for _, hook := range matchedHooks { - hook.ParseJSONParameters(&headers, &query, &payload) - if hook.TriggerRule == nil || hook.TriggerRule != nil && hook.TriggerRule.Evaluate(&headers, &query, &payload, &body) { - log.Printf("%s hook triggered successfully\n", hook.ID) + for _, h := range matchedHooks { + h.ParseJSONParameters(&headers, &query, &payload) + if h.TriggerRule == nil || h.TriggerRule != nil && h.TriggerRule.Evaluate(&headers, &query, &payload, &body) { + log.Printf("%s hook triggered successfully\n", h.ID) - go handleHook(hook, &headers, &query, &payload, &body) - - // send the hook defined response message - fmt.Fprintf(w, hook.ResponseMessage) + if !h.CaptureCommandOutput { + go handleHook(h, &headers, &query, &payload, &body) + fmt.Fprintf(w, h.ResponseMessage) + } else { + response := handleHook(h, &headers, &query, &payload, &body) + fmt.Fprintf(w, response) + } return } @@ -209,22 +212,35 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { } } -func handleHook(hook *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) { - cmd := exec.Command(hook.ExecuteCommand) - cmd.Args = hook.ExtractCommandArguments(headers, query, payload) - cmd.Dir = hook.CommandWorkingDirectory +func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) string { + cmd := exec.Command(h.ExecuteCommand) + cmd.Args = h.ExtractCommandArguments(headers, query, payload) + cmd.Dir = h.CommandWorkingDirectory - log.Printf("executing %s (%s) with arguments %s using %s as cwd\n", hook.ExecuteCommand, cmd.Path, cmd.Args, cmd.Dir) + log.Printf("executing %s (%s) with arguments %s using %s as cwd\n", h.ExecuteCommand, cmd.Path, cmd.Args, cmd.Dir) - out, err := cmd.Output() + out, err := cmd.CombinedOutput() - log.Printf("stdout: %s\n", out) + log.Printf("command output: %s\n", out) + + var errorResponse string if err != nil { - log.Printf("stderr: %+v\n", err) + log.Printf("error occurred: %+v\n", err) + errorResponse = fmt.Sprintf("%+v", err) } - log.Printf("finished handling %s\n", hook.ID) + log.Printf("finished handling %s\n", h.ID) + + var response []byte + response, err = json.Marshal(&hook.CommandStatusResponse{ResponseMessage: h.ResponseMessage, Output: string(out), Error: errorResponse}) + + if err != nil { + log.Printf("error marshalling response: %+v", err) + return h.ResponseMessage + } + + return string(response) } func reloadHooks() { diff --git a/webhook_windows.go b/webhook_windows.go index 1c26828..7dc684a 100644 --- a/webhook_windows.go +++ b/webhook_windows.go @@ -1,4 +1,4 @@ -//+build windows +//+build !windows package main @@ -38,6 +38,7 @@ var ( key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file") watcher *fsnotify.Watcher + signals chan os.Signal hooks hook.Hooks ) @@ -154,7 +155,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { contentType := r.Header.Get("Content-Type") - if strings.HasPrefix(contentType, "application/json") { + if strings.Contains(contentType, "json") { decoder := json.NewDecoder(strings.NewReader(string(body))) decoder.UseNumber() @@ -163,7 +164,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("error parsing JSON payload %+v\n", err) } - } else if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") { + } else if strings.Contains(contentType, "form") { fd, err := url.ParseQuery(string(body)) if err != nil { log.Printf("error parsing form payload %+v\n", err) @@ -173,16 +174,18 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { } // handle hook - for _, hook := range matchedHooks { - hook.ParseJSONParameters(&headers, &query, &payload) + for _, h := range matchedHooks { + h.ParseJSONParameters(&headers, &query, &payload) + if h.TriggerRule == nil || h.TriggerRule != nil && h.TriggerRule.Evaluate(&headers, &query, &payload, &body) { + log.Printf("%s hook triggered successfully\n", h.ID) - if hook.TriggerRule == nil || hook.TriggerRule != nil && hook.TriggerRule.Evaluate(&headers, &query, &payload, &body) { - log.Printf("%s hook triggered successfully\n", hook.ID) - - go handleHook(hook, &headers, &query, &payload, &body) - - // send the hook defined response message - fmt.Fprintf(w, hook.ResponseMessage) + if !h.CaptureCommandOutput { + go handleHook(h, &headers, &query, &payload, &body) + fmt.Fprintf(w, h.ResponseMessage) + } else { + response := handleHook(h, &headers, &query, &payload, &body) + fmt.Fprintf(w, response) + } return } @@ -199,22 +202,35 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { } } -func handleHook(hook *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) { - cmd := exec.Command(hook.ExecuteCommand) - cmd.Args = hook.ExtractCommandArguments(headers, query, payload) - cmd.Dir = hook.CommandWorkingDirectory +func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) string { + cmd := exec.Command(h.ExecuteCommand) + cmd.Args = h.ExtractCommandArguments(headers, query, payload) + cmd.Dir = h.CommandWorkingDirectory - log.Printf("executing %s (%s) with arguments %s using %s as cwd\n", hook.ExecuteCommand, cmd.Path, cmd.Args, cmd.Dir) + log.Printf("executing %s (%s) with arguments %s using %s as cwd\n", h.ExecuteCommand, cmd.Path, cmd.Args, cmd.Dir) - out, err := cmd.Output() + out, err := cmd.CombinedOutput() - log.Printf("stdout: %s\n", out) + log.Printf("command output: %s\n", out) + + var errorResponse string if err != nil { - log.Printf("stderr: %+v\n", err) + log.Printf("error occurred: %+v\n", err) + errorResponse = fmt.Sprintf("%+v", err) } - log.Printf("finished handling %s\n", hook.ID) + log.Printf("finished handling %s\n", h.ID) + + var response []byte + response, err = json.Marshal(&hook.CommandStatusResponse{ResponseMessage: h.ResponseMessage, Output: string(out), Error: errorResponse}) + + if err != nil { + log.Printf("error marshalling response: %+v", err) + return h.ResponseMessage + } + + return string(response) } func reloadHooks() { From cbe2440cda71312dc7ba6c4da6abefc856f9539b Mon Sep 17 00:00:00 2001 From: Adnan Hajdarevic Date: Sat, 6 Jun 2015 14:25:32 +0200 Subject: [PATCH 3/4] add entire query and headers as well --- hook/hook.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/hook/hook.go b/hook/hook.go index 3cb5b58..16f4a47 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -21,6 +21,8 @@ const ( SourcePayload string = "payload" SourceString string = "string" SourceEntirePayload string = "entire-payload" + SourceEntireQuery string = "entire-query" + SourceEntireHeaders string = "entire-headers" ) // CheckPayloadSignature calculates and verifies SHA1 signature of the given payload @@ -159,6 +161,22 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string return "", false } + return string(r), true + case SourceEntireHeaders: + r, err := json.Marshal(headers) + + if err != nil { + return "", false + } + + return string(r), true + case SourceEntireQuery: + r, err := json.Marshal(query) + + if err != nil { + return "", false + } + return string(r), true } From 9977fa8c615303b031c2140aa60e7fc6c2de869c Mon Sep 17 00:00:00 2001 From: Adnan Hajdarevic Date: Sat, 6 Jun 2015 14:28:00 +0200 Subject: [PATCH 4/4] refactor --- webhook.go | 8 ++++---- webhook_windows.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/webhook.go b/webhook.go index 207a25a..fc45fa1 100644 --- a/webhook.go +++ b/webhook.go @@ -189,12 +189,12 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { if h.TriggerRule == nil || h.TriggerRule != nil && h.TriggerRule.Evaluate(&headers, &query, &payload, &body) { log.Printf("%s hook triggered successfully\n", h.ID) - if !h.CaptureCommandOutput { - go handleHook(h, &headers, &query, &payload, &body) - fmt.Fprintf(w, h.ResponseMessage) - } else { + if h.CaptureCommandOutput { response := handleHook(h, &headers, &query, &payload, &body) fmt.Fprintf(w, response) + } else { + go handleHook(h, &headers, &query, &payload, &body) + fmt.Fprintf(w, h.ResponseMessage) } return diff --git a/webhook_windows.go b/webhook_windows.go index 7dc684a..08700e9 100644 --- a/webhook_windows.go +++ b/webhook_windows.go @@ -179,12 +179,12 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { if h.TriggerRule == nil || h.TriggerRule != nil && h.TriggerRule.Evaluate(&headers, &query, &payload, &body) { log.Printf("%s hook triggered successfully\n", h.ID) - if !h.CaptureCommandOutput { - go handleHook(h, &headers, &query, &payload, &body) - fmt.Fprintf(w, h.ResponseMessage) - } else { + if h.CaptureCommandOutput { response := handleHook(h, &headers, &query, &payload, &body) fmt.Fprintf(w, response) + } else { + go handleHook(h, &headers, &query, &payload, &body) + fmt.Fprintf(w, h.ResponseMessage) } return