diff --git a/helpers/helpers.go b/helpers/helpers.go index df9d070..4c8c87d 100644 --- a/helpers/helpers.go +++ b/helpers/helpers.go @@ -5,7 +5,6 @@ import ( "crypto/sha1" "encoding/hex" "fmt" - "net/url" "reflect" "strconv" "strings" @@ -20,11 +19,11 @@ func CheckPayloadSignature(payload []byte, secret string, signature string) (str return expectedMAC, hmac.Equal([]byte(signature), []byte(expectedMAC)) } -// FormValuesToMap converts url.Values to a map[string]interface{} object -func FormValuesToMap(formValues url.Values) map[string]interface{} { +// ValuesToMap converts map[string][]string to a map[string]string object +func ValuesToMap(values map[string][]string) map[string]interface{} { ret := make(map[string]interface{}) - for key, value := range formValues { + for key, value := range values { if len(value) > 0 { ret[key] = value[0] } @@ -33,8 +32,12 @@ func FormValuesToMap(formValues url.Values) map[string]interface{} { return ret } -// ExtractJSONParameter extracts value from payload based on the passed string -func ExtractJSONParameter(s string, params interface{}) (string, bool) { +// ExtractParameter extracts value from interface{} based on the passed string +func ExtractParameter(s string, params interface{}) (string, bool) { + if params == nil { + return "", false + } + var p []string if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice { @@ -49,7 +52,7 @@ func ExtractJSONParameter(s string, params interface{}) (string, bool) { return "", false } - return ExtractJSONParameter(p[2], params.([]map[string]interface{})[index]) + return ExtractParameter(p[2], params.([]map[string]interface{})[index]) } } @@ -58,7 +61,7 @@ func ExtractJSONParameter(s string, params interface{}) (string, bool) { if p = strings.SplitN(s, ".", 2); len(p) > 1 { if pValue, ok := params.(map[string]interface{})[p[0]]; ok { - return ExtractJSONParameter(p[1], pValue) + return ExtractParameter(p[1], pValue) } } else { if pValue, ok := params.(map[string]interface{})[p[0]]; ok { diff --git a/hook/hook.go b/hook/hook.go new file mode 100644 index 0000000..576d0e5 --- /dev/null +++ b/hook/hook.go @@ -0,0 +1,214 @@ +package hook + +import ( + "encoding/json" + "io/ioutil" + "log" + "regexp" + + "github.com/adnanh/webhook/helpers" +) + +// Constants used to specify the parameter source +const ( + SourceHeader string = "header" + SourceQuery string = "url" + SourcePayload string = "payload" +) + +// Argument type specifies the parameter key name and the source it should +// be extracted from +type Argument struct { + Source string `json:"source"` + Name string `json:"name"` +} + +// Get Argument method returns the value for the Argument's key name +// based on the Argument's source +func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string, bool) { + var source *map[string]interface{} + + switch ha.Source { + case SourceHeader: + source = headers + case SourceQuery: + source = query + case SourcePayload: + source = payload + } + + if source != nil { + return helpers.ExtractParameter(ha.Name, *source) + } + + return "", false +} + +// Hook type is a structure containing details for a single hook +type Hook struct { + ID string `json:"id"` + ExecuteCommand string `json:"execute-command"` + CommandWorkingDirectory string `json:"command-working-directory"` + PassArgumentsToCommand []Argument `json:"pass-arguments-to-command"` + TriggerRule *Rules `json:"trigger-rule"` +} + +// ExtractCommandArguments creates a list of arguments, based on the +// PassArgumentsToCommand property that is ready to be used with exec.Command() +func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) []string { + var args = make([]string, 0) + + args = append(args, h.ExecuteCommand) + + for i := range h.PassArgumentsToCommand { + if arg, ok := h.PassArgumentsToCommand[i].Get(headers, query, payload); ok { + args = append(args, arg) + } else { + args = append(args, "") + log.Printf("couldn't retrieve argument for %+v\n", h.PassArgumentsToCommand[i]) + } + } + + return args +} + +// Hooks is an array of Hook objects +type Hooks []Hook + +// LoadFromFile attempts to load hooks from specified JSON file +func (h *Hooks) LoadFromFile(path string) error { + if path == "" { + return nil + } + + // parse hook file for hooks + file, e := ioutil.ReadFile(path) + + if e != nil { + return e + } + + e = json.Unmarshal(file, h) + return e +} + +// Match iterates through Hooks and returns first one that matches the given ID, +// if no hook matches the given ID, nil is returned +func (h *Hooks) Match(id string) *Hook { + for i := range *h { + if (*h)[i].ID == id { + return &(*h)[i] + } + } + + return nil +} + +// Rules is a structure that contains one of the valid rule types +type Rules struct { + And *AndRule `json:"and"` + Or *OrRule `json:"or"` + Not *NotRule `json:"not"` + Match *MatchRule `json:"match"` +} + +// 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 { + switch { + case r.And != nil: + return r.And.Evaluate(headers, query, payload, body) + case r.Or != nil: + return r.Or.Evaluate(headers, query, payload, body) + case r.Not != nil: + return r.Not.Evaluate(headers, query, payload, body) + case r.Match != nil: + return r.Match.Evaluate(headers, query, payload, body) + } + + return false +} + +// AndRule will evaluate to true if and only if all of the ChildRules evaluate to true +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 { + res := true + + for _, v := range r { + res = res && v.Evaluate(headers, query, payload, body) + if res == false { + return res + } + } + + return res +} + +// OrRule will evaluate to true if any of the ChildRules evaluate to true +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 { + res := false + + for _, v := range r { + res = res || v.Evaluate(headers, query, payload, body) + if res == true { + return res + } + } + + return res +} + +// NotRule will evaluate to true if any and only if the ChildRule evaluates to false +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 { + return !r.Evaluate(headers, query, payload, body) +} + +// MatchRule will evaluate to true based on the type +type MatchRule struct { + Type string `json:"type"` + Regex string `json:"regex"` + Secret string `json:"secret"` + Value string `json:"value"` + Parameter Argument `json:"parameter"` +} + +// Constants for the MatchRule type +const ( + MatchValue string = "value" + MatchRegex string = "regex" + MatchHashSHA1 string = "payload-hash-sha1" +) + +// Evaluate MatchRule will return based on the type +func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) bool { + if arg, ok := r.Parameter.Get(headers, query, payload); ok { + switch r.Type { + case MatchValue: + return arg == r.Value + case MatchRegex: + ok, err := regexp.MatchString(r.Regex, arg) + if err != nil { + log.Printf("error while trying to evaluate regex: %+v", err) + } + return ok + case MatchHashSHA1: + expected, ok := helpers.CheckPayloadSignature(*body, r.Secret, arg) + if !ok { + log.Printf("payload signature mismatch, expected %s got %s", expected, arg) + } + + return ok + } + } else { + log.Printf("couldn't retrieve argument for %+v\n", r.Parameter) + } + return false +} diff --git a/hooks.json.example b/hooks.json.example index 7a1bc49..edd79ae 100644 --- a/hooks.json.example +++ b/hooks.json.example @@ -1,18 +1,52 @@ [ { "id": "webhook", - "command": "/home/adnan/redeploy-go-webhook.sh", - "args": [ - "head" + "execute-command": "/home/adnan/redeploy-go-webhook.sh", + "command-working-directory": "/home/adnan/go", + "pass-arguments-to-command": + [ + { + "source": "payload", + "name": "head" + }, + { + "source": "payload", + "name": "pusher.name" + }, + { + "source": "payload", + "name": "pusher.email" + } ], - "cwd": "/home/adnan/go", "trigger-rule": { - "match": - { - "parameter": "ref", - "value": "refs/heads/master" - } + "and": + [ + { + "match": + { + "type": "payload-hash-sha1", + "secret": "mysecret", + "parameter": + { + "source": "header", + "name": "X-Hub-Signature" + } + } + }, + { + "match": + { + "type": "value", + "value": "refs/heads/master", + "parameter": + { + "source": "payload", + "name": "ref" + } + } + } + ] } } ] diff --git a/hooks/hooks.go b/hooks/hooks.go deleted file mode 100644 index f00ae5e..0000000 --- a/hooks/hooks.go +++ /dev/null @@ -1,203 +0,0 @@ -package hooks - -import ( - "encoding/json" - "io/ioutil" - "net/url" - - "github.com/adnanh/webhook/helpers" - "github.com/adnanh/webhook/rules" -) - -// Hook is a structure that contains command to be executed -// and the current working directory name where that command should be executed -type Hook struct { - ID string `json:"id"` - Command string `json:"command"` - Cwd string `json:"cwd"` - Secret string `json:"secret"` - Args []string `json:"args"` - Rule rules.Rule `json:"trigger-rule"` -} - -// Hooks represents structure that contains list of Hook objects -// and the name of file which is correspondingly mapped to it -type Hooks struct { - fileName string - list []Hook -} - -// ParseFormArgs gets arguments from the Form payload that should be passed to the command -func (h *Hook) ParseFormArgs(form url.Values) []string { - var args = make([]string, 0) - - args = append(args, h.Command) - - for i := range h.Args { - if arg := form[h.Args[i]]; len(arg) > 0 { - args = append(args, arg[0]) - } else { - args = append(args, "") - } - } - - return args -} - -// ParseJSONArgs gets arguments from the JSON payload that should be passed to the command -func (h *Hook) ParseJSONArgs(payload interface{}) []string { - var args = make([]string, 0) - - args = append(args, h.Command) - - for i := range h.Args { - if arg, ok := helpers.ExtractJSONParameter(h.Args[i], payload); ok { - args = append(args, arg) - } else { - args = append(args, "") - } - } - - return args -} - -// UnmarshalJSON implementation for a single hook -func (h *Hook) UnmarshalJSON(j []byte) error { - m := make(map[string]interface{}) - err := json.Unmarshal(j, &m) - - if err != nil { - return err - } - - if v, ok := m["id"]; ok { - h.ID = v.(string) - } - - if v, ok := m["command"]; ok { - h.Command = v.(string) - } - - if v, ok := m["cwd"]; ok { - h.Cwd = v.(string) - } - - if v, ok := m["secret"]; ok { - h.Secret = v.(string) - } - - if v, ok := m["args"]; ok { - h.Args = make([]string, 0) - - for i := range v.([]interface{}) { - h.Args = append(h.Args, v.([]interface{})[i].(string)) - } - } - - if v, ok := m["trigger-rule"]; ok { - rule := v.(map[string]interface{}) - - if ruleValue, ok := rule["match"]; ok { - ruleString, _ := json.Marshal(ruleValue) - rulePtr := new(rules.MatchRule) - - err = json.Unmarshal(ruleString, rulePtr) - if err != nil { - return err - } - - h.Rule = *rulePtr - - } else if ruleValue, ok := rule["not"]; ok { - ruleString, _ := json.Marshal(ruleValue) - rulePtr := new(rules.NotRule) - - err = json.Unmarshal(ruleString, rulePtr) - if err != nil { - return err - } - - h.Rule = *rulePtr - - } else if ruleValue, ok := rule["and"]; ok { - ruleString, _ := json.Marshal(ruleValue) - rulePtr := new(rules.AndRule) - - err = json.Unmarshal(ruleString, rulePtr) - if err != nil { - return err - } - - h.Rule = *rulePtr - - } else if ruleValue, ok := rule["or"]; ok { - ruleString, _ := json.Marshal(ruleValue) - rulePtr := new(rules.OrRule) - - err = json.Unmarshal(ruleString, rulePtr) - if err != nil { - return err - } - - h.Rule = *rulePtr - } - - } - - return nil -} - -// New creates an instance of Hooks, tries to unmarshal contents of hookFile -// and returns a pointer to the newly created instance -func New(hookFile string) (*Hooks, error) { - h := &Hooks{fileName: hookFile} - - if hookFile == "" { - return h, nil - } - - // parse hook file for hooks - file, e := ioutil.ReadFile(hookFile) - - if e != nil { - return h, e - } - - e = json.Unmarshal(file, &(h.list)) - - h.SetDefaults() - - return h, e -} - -// Match looks for the hook with the given id in the list of hooks -// and returns the pointer to the hook if it exists, or nil if it doesn't exist -func (h *Hooks) Match(id string, params interface{}) *Hook { - for i := range h.list { - if h.list[i].ID == id { - if h.list[i].Rule == nil || (h.list[i].Rule != nil && h.list[i].Rule.Evaluate(params)) { - return &h.list[i] - } - } - } - - return nil -} - -// Count returns number of hooks in the list -func (h *Hooks) Count() int { - return len(h.list) -} - -// SetDefaults sets default values that were ommited for hooks in JSON file -func (h *Hooks) SetDefaults() { - for i := range h.list { - if h.list[i].Cwd == "" { - h.list[i].Cwd = "." - } - - if h.list[i].Args == nil { - h.list[i].Args = make([]string, 1) - } - } -} diff --git a/rules/rules.go b/rules/rules.go deleted file mode 100644 index 5e84b71..0000000 --- a/rules/rules.go +++ /dev/null @@ -1,262 +0,0 @@ -package rules - -import ( - "encoding/json" - - "github.com/adnanh/webhook/helpers" -) - -// Rule interface -type Rule interface { - Evaluate(params interface{}) bool -} - -// AndRule type is a structure that contains list of rules (SubRules) that will be evaluated, -// and the AndRule's Evaluate method will evaluate to true if and only if all -// of the SubRules evaluate to true -type AndRule struct { - SubRules []Rule `json:"and"` -} - -// OrRule type is a structure that contains list of rules (SubRules) that will be evaluated, -// and the OrRule's Evaluate method will evaluate to true if any of the SubRules -// evaluate to true -type OrRule struct { - SubRules []Rule `json:"or"` -} - -// NotRule type is a structure that contains a single rule (SubRule) that will be evaluated, -// and the OrRule's Evaluate method will evaluate to true if any and only if -// the SubRule evaluates to false -type NotRule struct { - SubRule Rule `json:"not"` -} - -// MatchRule type is a structure that contains MatchParameter structure -type MatchRule struct { - MatchParameter MatchParameter `json:"match"` -} - -// MatchParameter type is a structure that contains Parameter and Value which are used in -// Match -type MatchParameter struct { - Parameter string `json:"parameter"` - Value string `json:"value"` -} - -// Evaluate AndRule will return true if and only if all of SubRules evaluate to true -func (r AndRule) Evaluate(params interface{}) bool { - res := true - - for _, v := range r.SubRules { - res = res && v.Evaluate(params) - if res == false { - return res - } - } - - return res -} - -// Evaluate OrRule will return true if any of SubRules evaluate to true -func (r OrRule) Evaluate(params interface{}) bool { - res := false - - for _, v := range r.SubRules { - res = res || v.Evaluate(params) - if res == true { - return res - } - } - - return res -} - -// Evaluate NotRule will return true if and only if SubRule evaluates to false -func (r NotRule) Evaluate(params interface{}) bool { - return !r.SubRule.Evaluate(params) -} - -// Evaluate MatchRule will return true if and only if the MatchParameter.Parameter -// named property value in supplied params matches the MatchParameter.Value -func (r MatchRule) Evaluate(params interface{}) bool { - if v, ok := helpers.ExtractJSONParameter(r.MatchParameter.Parameter, params); ok { - return v == r.MatchParameter.Value - } - - return false -} - -// UnmarshalJSON implementation for the MatchRule type -func (r *MatchRule) UnmarshalJSON(j []byte) error { - err := json.Unmarshal(j, &r.MatchParameter) - return err -} - -// UnmarshalJSON implementation for the NotRule type -func (r *NotRule) UnmarshalJSON(j []byte) error { - m := make(map[string]interface{}) - err := json.Unmarshal(j, &m) - - if ruleValue, ok := m["match"]; ok { - ruleString, _ := json.Marshal(ruleValue) - rulePtr := new(MatchRule) - - err = json.Unmarshal(ruleString, &rulePtr.MatchParameter) - if err != nil { - return err - } - - r.SubRule = *rulePtr - - } else if ruleValue, ok := m["not"]; ok { - ruleString, _ := json.Marshal(ruleValue) - rulePtr := new(NotRule) - - err = json.Unmarshal(ruleString, rulePtr) - if err != nil { - return err - } - - r.SubRule = *rulePtr - - } else if ruleValue, ok := m["and"]; ok { - ruleString, _ := json.Marshal(ruleValue) - rulePtr := new(AndRule) - - err = json.Unmarshal(ruleString, rulePtr) - if err != nil { - return err - } - - r.SubRule = *rulePtr - - } else if ruleValue, ok := m["or"]; ok { - ruleString, _ := json.Marshal(ruleValue) - rulePtr := new(OrRule) - - err = json.Unmarshal(ruleString, rulePtr) - if err != nil { - return err - } - - r.SubRule = *rulePtr - - } - - return err -} - -// UnmarshalJSON implementation for the AndRule type -func (r *AndRule) UnmarshalJSON(j []byte) error { - rules := new([]interface{}) - err := json.Unmarshal(j, &rules) - - for _, rulesValue := range *rules { - m := rulesValue.(map[string]interface{}) - - if ruleValue, ok := m["match"]; ok { - ruleString, _ := json.Marshal(ruleValue) - rulePtr := new(MatchRule) - - err = json.Unmarshal(ruleString, &rulePtr.MatchParameter) - if err != nil { - return err - } - - r.SubRules = append(r.SubRules, *rulePtr) - - } else if ruleValue, ok := m["not"]; ok { - ruleString, _ := json.Marshal(ruleValue) - rulePtr := new(NotRule) - - err = json.Unmarshal(ruleString, rulePtr) - if err != nil { - return err - } - - r.SubRules = append(r.SubRules, *rulePtr) - - } else if ruleValue, ok := m["and"]; ok { - ruleString, _ := json.Marshal(ruleValue) - rulePtr := new(AndRule) - - err = json.Unmarshal(ruleString, rulePtr) - if err != nil { - return err - } - - r.SubRules = append(r.SubRules, *rulePtr) - - } else if ruleValue, ok := m["or"]; ok { - ruleString, _ := json.Marshal(ruleValue) - rulePtr := new(OrRule) - - err = json.Unmarshal(ruleString, rulePtr) - if err != nil { - return err - } - - r.SubRules = append(r.SubRules, *rulePtr) - } - } - - return err -} - -// UnmarshalJSON implementation for the OrRule type -func (r *OrRule) UnmarshalJSON(j []byte) error { - rules := new([]interface{}) - err := json.Unmarshal(j, &rules) - - for _, rulesValue := range *rules { - m := rulesValue.(map[string]interface{}) - - if ruleValue, ok := m["match"]; ok { - ruleString, _ := json.Marshal(ruleValue) - rulePtr := new(MatchRule) - - err = json.Unmarshal(ruleString, &rulePtr.MatchParameter) - if err != nil { - return err - } - - r.SubRules = append(r.SubRules, *rulePtr) - - } else if ruleValue, ok := m["not"]; ok { - ruleString, _ := json.Marshal(ruleValue) - rulePtr := new(NotRule) - - err = json.Unmarshal(ruleString, rulePtr) - if err != nil { - return err - } - - r.SubRules = append(r.SubRules, *rulePtr) - - } else if ruleValue, ok := m["and"]; ok { - ruleString, _ := json.Marshal(ruleValue) - rulePtr := new(AndRule) - - err = json.Unmarshal(ruleString, rulePtr) - if err != nil { - return err - } - - r.SubRules = append(r.SubRules, *rulePtr) - - } else if ruleValue, ok := m["or"]; ok { - ruleString, _ := json.Marshal(ruleValue) - rulePtr := new(OrRule) - - err = json.Unmarshal(ruleString, rulePtr) - if err != nil { - return err - } - - r.SubRules = append(r.SubRules, *rulePtr) - } - } - - return err -} diff --git a/webhook.go b/webhook.go index e8e4f5f..1803280 100644 --- a/webhook.go +++ b/webhook.go @@ -5,134 +5,160 @@ import ( "flag" "fmt" "io/ioutil" + "log" "net/http" "net/url" + "os" "os/exec" "strings" - "time" "github.com/adnanh/webhook/helpers" - "github.com/adnanh/webhook/hooks" + "github.com/adnanh/webhook/hook" - "github.com/go-martini/martini" - - l4g "code.google.com/p/log4go" + "github.com/codegangsta/negroni" + "github.com/gorilla/mux" ) const ( - version string = "1.0.4" + version = "2.0.0" ) var ( - webhooks *hooks.Hooks - appStart time.Time - ip = flag.String("ip", "", "ip the webhook server should listen on") - port = flag.Int("port", 9000, "port the webhook server should listen on") - hooksFilename = flag.String("hooks", "hooks.json", "path to the json file containing defined hooks the webhook should serve") - logFilename = flag.String("log", "webhook.log", "path to the log file") + ip = flag.String("ip", "", "ip the webhook should serve hooks on") + port = flag.Int("port", 9000, "port the webhook should serve hooks on") + verbose = flag.Bool("verbose", false, "show verbose output") + hooksFilePath = flag.String("hooks", "hooks.json", "path to the json file containing defined hooks the webhook should serve") + + hooks hook.Hooks ) func init() { + hooks = hook.Hooks{} + flag.Parse() - fileLogWriter := l4g.NewFileLogWriter(*logFilename, false) - fileLogWriter.SetRotateDaily(false) + log.SetPrefix("[webhook] ") + log.SetFlags(log.Ldate | log.Ltime) - martini.Env = "production" + if !*verbose { + log.SetOutput(ioutil.Discard) + } - l4g.AddFilter("file", l4g.FINE, fileLogWriter) + log.Println("version " + version + " starting") + + // load and parse hooks + log.Printf("attempting to load hooks from %s\n", *hooksFilePath) + + err := hooks.LoadFromFile(*hooksFilePath) + + if err != nil { + log.Printf("couldn't load hooks from file! %+v\n", err) + } else { + log.Printf("loaded %d hook(s) from file\n", len(hooks)) + + for _, hook := range hooks { + log.Printf("\t> %s\n", hook.ID) + } + } + // set up file watcher + //log.Printf("setting up file watcher for %s\n", *hooksFilePath) } func main() { - appStart = time.Now() - var e error + l := log.New(os.Stdout, "[webhook] ", log.Ldate|log.Ltime) - webhooks, e = hooks.New(*hooksFilename) + negroniLogger := &negroni.Logger{l} - if e != nil { - l4g.Warn("Error occurred while loading hooks from %s: %s", *hooksFilename, e) + negroniRecovery := &negroni.Recovery{ + Logger: l, + PrintStack: true, + StackAll: false, + StackSize: 1024 * 8, } - web := martini.Classic() + n := negroni.New(negroniRecovery, negroniLogger) - web.Get("/", rootHandler) - web.Get("/hook/:id", hookHandler) - web.Post("/hook/:id", hookHandler) + router := mux.NewRouter() + router.HandleFunc("/hooks/{id}", hookHandler) - l4g.Info("Starting webhook %s with %d hook(s) on %s:%d", version, webhooks.Count(), *ip, *port) + n.UseHandler(router) - web.RunOnAddr(fmt.Sprintf("%s:%d", *ip, *port)) + log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *ip, *port), n)) + + log.Printf("listening on %s:%d", *ip, *port) } -func rootHandler() string { - return fmt.Sprintf("webhook %s running for %s serving %d hook(s)\n", version, time.Since(appStart).String(), webhooks.Count()) -} +func hookHandler(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] -func jsonHandler(id string, body []byte, signature string, payload interface{}) { - if hook := webhooks.Match(id, payload); hook != nil { - if hook.Secret != "" { - if signature == "" { - l4g.Error("Hook %s got matched and contains the secret, but the request didn't contain any signature.", hook.ID) - return - } + hook := hooks.Match(id) - if expectedMAC, ok := helpers.CheckPayloadSignature(body, hook.Secret, signature); !ok { - l4g.Error("Hook %s got matched and contains the secret, but the request contained invalid signature. Expected %s, got %s.", hook.ID, expectedMAC, signature) - return - } - } + if hook != nil { + log.Printf("%s got matched\n", id) - cmd := exec.Command(hook.Command) - cmd.Args = hook.ParseJSONArgs(payload) - cmd.Dir = hook.Cwd - out, err := cmd.Output() - l4g.Info("Hook %s triggered successfully! Command output:\n%s\n%+v", hook.ID, out, err) - } -} - -func formHandler(id string, formValues url.Values) { - if hook := webhooks.Match(id, helpers.FormValuesToMap(formValues)); hook != nil { - cmd := exec.Command(hook.Command) - cmd.Args = hook.ParseFormArgs(formValues) - cmd.Dir = hook.Cwd - out, err := cmd.Output() - l4g.Info("Hook %s triggered successfully! Command output:\n%s\n%+v", hook.ID, out, err) - } -} - -func hookHandler(req *http.Request, params martini.Params) string { - if req.Header.Get("Content-Type") == "application/json" { - defer req.Body.Close() - - body, err := ioutil.ReadAll(req.Body) + body, err := ioutil.ReadAll(r.Body) if err != nil { - l4g.Warn("Error occurred while trying to read the request body: %s", err) + log.Printf("error reading the request body. %+v\n", err) } - payloadJSON := make(map[string]interface{}) + // parse headers + headers := helpers.ValuesToMap(r.Header) - decoder := json.NewDecoder(strings.NewReader(string(body))) - decoder.UseNumber() + // parse query variables + query := helpers.ValuesToMap(r.URL.Query()) - err = decoder.Decode(&payloadJSON) + // parse body + var payload map[string]interface{} - if err != nil { - l4g.Warn("Error occurred while trying to parse the payload as JSON: %s", err) - } + contentType := r.Header.Get("Content-Type") - payloadSignature := "" + if contentType == "application/json" { + decoder := json.NewDecoder(strings.NewReader(string(body))) + decoder.UseNumber() - if strings.Contains(req.Header.Get("User-Agent"), "GitHub-Hookshot") { - if len(req.Header.Get("X-Hub-Signature")) > 5 { - payloadSignature = req.Header.Get("X-Hub-Signature")[5:] + err := decoder.Decode(&payload) + + if err != nil { + log.Printf("error parsing JSON payload %+v\n", err) + } + } else if contentType == "application/x-www-form-urlencoded" { + fd, err := url.ParseQuery(string(body)) + if err != nil { + log.Printf("error parsing form payload %+v\n", err) + } else { + payload = helpers.ValuesToMap(fd) } } - - go jsonHandler(params["id"], body, payloadSignature, payloadJSON) + + // handle hook + go handleHook(hook, &headers, &query, &payload, &body) + + // say thanks + fmt.Fprintf(w, "Thanks.") } else { - req.ParseForm() - go formHandler(params["id"], req.Form) + 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) + + 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) + + out, err := cmd.Output() + + 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) } - - return "Got it, thanks. :-)" } diff --git a/webhook2.go b/webhook2.go deleted file mode 100644 index 99b08fd..0000000 --- a/webhook2.go +++ /dev/null @@ -1,65 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "io/ioutil" - "log" - "net/http" - - "github.com/codegangsta/negroni" - "github.com/gorilla/mux" -) - -var ( - ip = flag.String("ip", "", "ip the webhook should serve hooks on") - port = flag.Int("port", 9000, "port the webhook should serve hooks on") - verbose = flag.Bool("verbose", false, "show verbose output") - hooksFilePath = flag.String("hooks", "hooks.json", "path to the json file containing defined hooks the webhook should serve") -) - -func init() { - flag.Parse() - - log.SetPrefix("[webhook] ") - log.SetFlags(log.Ldate | log.Ltime) - - if !*verbose { - log.SetOutput(ioutil.Discard) - } - - log.Println("starting") - - // load and parse hooks - log.Printf("attempting to load hooks from %s\n", *hooksFilePath) - - // set up file watcher - log.Printf("setting up file watcher for %s\n", *hooksFilePath) -} - -func main() { - router := mux.NewRouter() - router.HandleFunc("/hooks/{id}", hookHandler) - - n := negroni.Classic() - n.UseHandler(router) - - n.Run(fmt.Sprintf("%s:%d", *ip, *port)) -} - -func hookHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - - // parse headers - - // parse body - - // find hook - - // trigger hook - - // say thanks - - fmt.Fprintf(w, "Thanks. %s %+v %+v %+v", id, vars, r.Header, r.Body) -}