diff --git a/hook/hook.go b/hook/hook.go index 5315b64..16f4a47 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -16,10 +16,13 @@ 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" + SourceEntireQuery string = "entire-query" + SourceEntireHeaders string = "entire-headers" ) // CheckPayloadSignature calculates and verifies SHA1 signature of the given payload @@ -151,6 +154,30 @@ 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 + 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 } if source != nil { @@ -166,6 +193,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"` @@ -260,6 +288,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"` @@ -368,3 +413,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 9c9ccb6..fc45fa1 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 { @@ -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) @@ -183,40 +183,64 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { } } - hook.ParseJSONParameters(&headers, &query, &payload) - // handle hook - go handleHook(hook, &headers, &query, &payload, &body) + 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) - // send the hook defined response message - fmt.Fprintf(w, hook.ResponseMessage) + 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 + } + } + + // 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.") } } -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) +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 - 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", h.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.CombinedOutput() - out, err := cmd.Output() + log.Printf("command output: %s\n", out) - log.Printf("stdout: %s\n", out) + var errorResponse string - 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("error occurred: %+v\n", err) + errorResponse = fmt.Sprintf("%+v", err) } + + 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 f42e8bf..08700e9 100644 --- a/webhook_windows.go +++ b/webhook_windows.go @@ -1,4 +1,4 @@ -//+build windows +//+build !windows package main @@ -23,7 +23,7 @@ import ( ) const ( - version = "2.3.3" + version = "2.3.4" ) var ( @@ -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 ) @@ -133,10 +134,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 { @@ -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) @@ -172,40 +173,64 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { } } - hook.ParseJSONParameters(&headers, &query, &payload) - // handle hook - go handleHook(hook, &headers, &query, &payload, &body) + 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) - // send the hook defined response message - fmt.Fprintf(w, hook.ResponseMessage) + 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 + } + } + + // 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.") } } -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) +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 - 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", h.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.CombinedOutput() - out, err := cmd.Output() + log.Printf("command output: %s\n", out) - log.Printf("stdout: %s\n", out) + var errorResponse string - 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("error occurred: %+v\n", err) + errorResponse = fmt.Sprintf("%+v", err) } + + 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() {