webhook 2.0.0

This commit is contained in:
Adnan Hajdarevic 2015-03-13 01:31:49 +01:00
parent 489750a710
commit 90528b2ed9
7 changed files with 377 additions and 630 deletions

View file

@ -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 {

214
hook/hook.go Normal file
View file

@ -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
}

View file

@ -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"
}
}
}
]
}
}
]

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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. :-)"
}

View file

@ -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)
}