mirror of
https://github.com/adnanh/webhook.git
synced 2025-05-14 01:24:54 +00:00
refactoring
This commit is contained in:
parent
a5d79fddd9
commit
f1c4415fc8
5 changed files with 130 additions and 74 deletions
|
@ -20,6 +20,10 @@ Hooks are defined using JSON format. The _hooks file_ must contain an array of J
|
||||||
{
|
{
|
||||||
"id": "hook-1",
|
"id": "hook-1",
|
||||||
"command": "OS command to be executed when the hook gets triggered",
|
"command": "OS command to be executed when the hook gets triggered",
|
||||||
|
"args": [
|
||||||
|
"ref",
|
||||||
|
"repository.owner.name"
|
||||||
|
],
|
||||||
"cwd": "current working directory under which the specified command will be executed (optional, defaults to the directory where the binary resides)",
|
"cwd": "current working directory under which the specified command will be executed (optional, defaults to the directory where the binary resides)",
|
||||||
"secret": "secret key used to compute the hash of the payload (optional)",
|
"secret": "secret key used to compute the hash of the payload (optional)",
|
||||||
"trigger-rule":
|
"trigger-rule":
|
||||||
|
@ -138,7 +142,6 @@ All hooks are served under the `http://ip:port/hook/:id`, where the `:id` corres
|
||||||
Visiting `http://ip:port` will show version, uptime and number of hooks the webhook is serving.
|
Visiting `http://ip:port` will show version, uptime and number of hooks the webhook is serving.
|
||||||
|
|
||||||
## Todo
|
## Todo
|
||||||
* Add support for passing parameters from payload to the command that gets executed as part of the hook
|
|
||||||
* Add support for ip white/black listing
|
* Add support for ip white/black listing
|
||||||
* Add "match-regex" rule
|
* Add "match-regex" rule
|
||||||
* ???
|
* ???
|
||||||
|
|
70
helpers/helpers.go
Normal file
70
helpers/helpers.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
|
||||||
|
func CheckPayloadSignature(payload []byte, secret string, signature string) (string, bool) {
|
||||||
|
mac := hmac.New(sha1.New, []byte(secret))
|
||||||
|
mac.Write(payload)
|
||||||
|
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
||||||
|
|
||||||
|
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{} {
|
||||||
|
ret := make(map[string]interface{})
|
||||||
|
|
||||||
|
for key, value := range formValues {
|
||||||
|
if len(value) > 0 {
|
||||||
|
ret[key] = value[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractJSONParameter extracts value from payload based on the passed string
|
||||||
|
func ExtractJSONParameter(s string, params interface{}) (string, bool) {
|
||||||
|
var p []string
|
||||||
|
|
||||||
|
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
|
||||||
|
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
|
||||||
|
|
||||||
|
if p = strings.SplitN(s, ".", 3); len(p) > 3 {
|
||||||
|
index, err := strconv.ParseInt(p[1], 10, 64)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
} else if paramsValueSliceLength <= int(index) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExtractJSONParameter(p[2], params.([]map[string]interface{})[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if p = strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||||
|
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||||
|
return ExtractJSONParameter(p[1], pValue)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||||
|
return fmt.Sprintf("%v", pValue), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
|
@ -3,7 +3,9 @@ package hooks
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/adnanh/webhook/helpers"
|
||||||
"github.com/adnanh/webhook/rules"
|
"github.com/adnanh/webhook/rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,6 +27,40 @@ type Hooks struct {
|
||||||
list []Hook
|
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, len(h.Args))
|
||||||
|
|
||||||
|
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, len(h.Args))
|
||||||
|
|
||||||
|
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
|
// UnmarshalJSON implementation for a single hook
|
||||||
func (h *Hook) UnmarshalJSON(j []byte) error {
|
func (h *Hook) UnmarshalJSON(j []byte) error {
|
||||||
m := make(map[string]interface{})
|
m := make(map[string]interface{})
|
||||||
|
|
|
@ -2,10 +2,8 @@ package rules
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"github.com/adnanh/webhook/helpers"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Rule interface
|
// Rule interface
|
||||||
|
@ -79,45 +77,10 @@ func (r NotRule) Evaluate(params interface{}) bool {
|
||||||
return !r.SubRule.Evaluate(params)
|
return !r.SubRule.Evaluate(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
func extract(s string, params interface{}) (string, bool) {
|
|
||||||
var p []string
|
|
||||||
|
|
||||||
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
|
|
||||||
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
|
|
||||||
|
|
||||||
if p = strings.SplitN(s, ".", 3); len(p) > 3 {
|
|
||||||
index, err := strconv.ParseInt(p[1], 10, 64)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", false
|
|
||||||
} else if paramsValueSliceLength <= int(index) {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
return extract(p[2], params.([]map[string]interface{})[index])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
if p = strings.SplitN(s, ".", 2); len(p) > 1 {
|
|
||||||
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
|
||||||
return extract(p[1], pValue)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
|
||||||
return fmt.Sprintf("%v", pValue), true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate MatchRule will return true if and only if the MatchParameter.Parameter
|
// Evaluate MatchRule will return true if and only if the MatchParameter.Parameter
|
||||||
// named property value in supplied params matches the MatchParameter.Value
|
// named property value in supplied params matches the MatchParameter.Value
|
||||||
func (r MatchRule) Evaluate(params interface{}) bool {
|
func (r MatchRule) Evaluate(params interface{}) bool {
|
||||||
if v, ok := extract(r.MatchParameter.Parameter, params); ok {
|
if v, ok := helpers.ExtractJSONParameter(r.MatchParameter.Parameter, params); ok {
|
||||||
return v == r.MatchParameter.Value
|
return v == r.MatchParameter.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
50
webhook.go
50
webhook.go
|
@ -1,7 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -12,13 +11,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/adnanh/webhook/helpers"
|
||||||
"github.com/adnanh/webhook/hooks"
|
"github.com/adnanh/webhook/hooks"
|
||||||
|
|
||||||
"github.com/go-martini/martini"
|
"github.com/go-martini/martini"
|
||||||
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha1"
|
|
||||||
|
|
||||||
l4g "code.google.com/p/log4go"
|
l4g "code.google.com/p/log4go"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -71,48 +68,35 @@ func rootHandler() string {
|
||||||
return fmt.Sprintf("webhook %s running for %s serving %d hook(s)\n", version, time.Since(appStart).String(), webhooks.Count())
|
return fmt.Sprintf("webhook %s running for %s serving %d hook(s)\n", version, time.Since(appStart).String(), webhooks.Count())
|
||||||
}
|
}
|
||||||
|
|
||||||
func githubHandler(id string, body []byte, signature string, params interface{}) {
|
func jsonHandler(id string, body []byte, signature string, payload interface{}) {
|
||||||
if hook := webhooks.Match(id, params); hook != nil {
|
if hook := webhooks.Match(id, payload); hook != nil {
|
||||||
if hook.Secret != "" {
|
if hook.Secret != "" {
|
||||||
if signature == "" {
|
if signature == "" {
|
||||||
l4g.Error("Hook %s got matched and contains the secret, but the request didn't contain any signature.", hook.ID)
|
l4g.Error("Hook %s got matched and contains the secret, but the request didn't contain any signature.", hook.ID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mac := hmac.New(sha1.New, []byte(hook.Secret))
|
if expectedMAC, ok := helpers.CheckPayloadSignature(body, hook.Secret, signature); ok {
|
||||||
mac.Write(body)
|
|
||||||
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
|
||||||
|
|
||||||
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
|
|
||||||
l4g.Error("Hook %s got matched and contains the secret, but the request contained invalid signature. Expected %s, got %s.", hook.ID, expectedMAC, signature)
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(hook.Command)
|
cmd := exec.Command(hook.Command)
|
||||||
|
cmd.Args = hook.ParseJSONArgs(payload)
|
||||||
cmd.Dir = hook.Cwd
|
cmd.Dir = hook.Cwd
|
||||||
out, err := cmd.Output()
|
out, err := cmd.Output()
|
||||||
l4g.Info("Hook %s triggered successfully! Command output:\n%s\nError: %+v", hook.ID, out, err)
|
l4g.Info("Hook %s triggered successfully! Command output:\n%s\n%+v", hook.ID, out, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultPostHookHandler(id string, formValues url.Values) {
|
func formHandler(id string, formValues url.Values) {
|
||||||
if hook := webhooks.Match(id, make(map[string]interface{})); hook != nil {
|
if hook := webhooks.Match(id, helpers.FormValuesToMap(formValues)); hook != nil {
|
||||||
args := make([]string, 0)
|
|
||||||
args = append(args, hook.Command)
|
|
||||||
for i := range hook.Args {
|
|
||||||
if arg := formValues[hook.Args[i]]; len(arg) > 0 {
|
|
||||||
args = append(args, arg[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(hook.Command)
|
cmd := exec.Command(hook.Command)
|
||||||
cmd.Args = args
|
cmd.Args = hook.ParseFormArgs(formValues)
|
||||||
cmd.Dir = hook.Cwd
|
cmd.Dir = hook.Cwd
|
||||||
|
|
||||||
out, err := cmd.Output()
|
out, err := cmd.Output()
|
||||||
|
l4g.Info("Hook %s triggered successfully! Command output:\n%s\n%+v", hook.ID, out, err)
|
||||||
l4g.Info("Hook %s triggered successfully! Command output:\n%s\nError: %+v", hook.ID, out, err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,18 +120,18 @@ func hookHandler(req *http.Request, params martini.Params) string {
|
||||||
l4g.Warn("Error occurred while trying to parse the payload as JSON: %s", err)
|
l4g.Warn("Error occurred while trying to parse the payload as JSON: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
githubSignature := ""
|
payloadSignature := ""
|
||||||
|
|
||||||
if len(req.Header.Get("X-Hub-Signature")) > 5 {
|
|
||||||
githubSignature = req.Header.Get("X-Hub-Signature")[5:]
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(req.Header.Get("User-Agent"), "Github") {
|
if strings.Contains(req.Header.Get("User-Agent"), "Github") {
|
||||||
go githubHandler(params["id"], body, githubSignature, payloadJSON)
|
if len(req.Header.Get("X-Hub-Signature")) > 5 {
|
||||||
|
payloadSignature = req.Header.Get("X-Hub-Signature")[5:]
|
||||||
|
}
|
||||||
|
|
||||||
|
go jsonHandler(params["id"], body, payloadSignature, payloadJSON)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
req.ParseForm()
|
req.ParseForm()
|
||||||
go defaultPostHookHandler(params["id"], req.Form)
|
go formHandler(params["id"], req.Form)
|
||||||
}
|
}
|
||||||
|
|
||||||
return "Got it, thanks. :-)"
|
return "Got it, thanks. :-)"
|
||||||
|
|
Loading…
Add table
Reference in a new issue