mirror of
https://github.com/adnanh/webhook.git
synced 2025-05-23 05:42:30 +00:00
webhook 2.0.0
This commit is contained in:
parent
489750a710
commit
90528b2ed9
7 changed files with 377 additions and 630 deletions
|
@ -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
214
hook/hook.go
Normal 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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
203
hooks/hooks.go
203
hooks/hooks.go
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
262
rules/rules.go
262
rules/rules.go
|
@ -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
|
||||
}
|
192
webhook.go
192
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. :-)"
|
||||
}
|
||||
|
|
65
webhook2.go
65
webhook2.go
|
@ -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)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue