mirror of
https://github.com/adnanh/webhook.git
synced 2026-07-27 01:29:16 +08:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b66216675a | |||
| ecbcf11153 | |||
| 7d525cf317 | |||
| e83d7029ff | |||
| 36c2c692d6 | |||
| 86cef3e421 | |||
| 75cf8952be | |||
| c53596df59 | |||
| cc0d9b2cba | |||
| c6530b17e7 | |||
| 1943c5311f | |||
| 3b59539a33 | |||
| 923b0c6daa | |||
| 8530255ae6 | |||
| 032c74451f | |||
| 1c50853d8d | |||
| b5ed4cbea7 | |||
| b6d176705e | |||
| 421fc2cbcd | |||
| 10d65dd2fd | |||
| 54a9dbe1d6 | |||
| 30baec91df |
@@ -0,0 +1,29 @@
|
|||||||
|
OS = darwin freebsd linux openbsd windows
|
||||||
|
ARCHS = 386 arm amd64 arm64
|
||||||
|
|
||||||
|
all: build release
|
||||||
|
|
||||||
|
build: deps
|
||||||
|
go build
|
||||||
|
|
||||||
|
release: clean deps
|
||||||
|
@for arch in $(ARCHS);\
|
||||||
|
do \
|
||||||
|
for os in $(OS);\
|
||||||
|
do \
|
||||||
|
echo "Building $$os-$$arch"; \
|
||||||
|
mkdir -p build/webhook-$$os-$$arch/; \
|
||||||
|
GOOS=$$os GOARCH=$$arch go build -o build/webhook-$$os-$$arch/webhook; \
|
||||||
|
tar cz -C build -f build/webhook-$$os-$$arch.tar.gz webhook-$$os-$$arch; \
|
||||||
|
done \
|
||||||
|
done
|
||||||
|
|
||||||
|
test: deps
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
deps:
|
||||||
|
go get -d -v -t ./...
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf build
|
||||||
|
rm -f webhook
|
||||||
@@ -19,12 +19,19 @@ Everything else is the responsibility of the command's author.
|
|||||||
---
|
---
|
||||||
|
|
||||||
# Getting started
|
# Getting started
|
||||||
|
## Installation
|
||||||
|
### Building from source
|
||||||
To get started, first make sure you've properly set up your [Golang](http://golang.org/doc/install) environment and then run the
|
To get started, first make sure you've properly set up your [Golang](http://golang.org/doc/install) environment and then run the
|
||||||
```bash
|
```bash
|
||||||
$ go get github.com/adnanh/webhook
|
$ go get github.com/adnanh/webhook
|
||||||
```
|
```
|
||||||
to get the latest version of the [webhook](https://github.com/adnanh/webhook/).
|
to get the latest version of the [webhook](https://github.com/adnanh/webhook/).
|
||||||
|
|
||||||
|
### Using package manager
|
||||||
|
#### Debian "sid"
|
||||||
|
If you are using unstable version of Debian linux ("sid"), you can install webhook using `apt-get install webhook` which will install community packaged version (thanks [@freeekanayaka](https://github.com/freeekanayaka)) from https://packages.debian.org/sid/webhook
|
||||||
|
|
||||||
|
## Configuration
|
||||||
Next step is to define some hooks you want [webhook](https://github.com/adnanh/webhook/) to serve. Begin by creating an empty file named `hooks.json`. This file will contain an array of hooks the [webhook](https://github.com/adnanh/webhook/) will serve. Check [Hook definition page](https://github.com/adnanh/webhook/wiki/Hook-Definition) to see the detailed description of what properties a hook can contain, and how to use them.
|
Next step is to define some hooks you want [webhook](https://github.com/adnanh/webhook/) to serve. Begin by creating an empty file named `hooks.json`. This file will contain an array of hooks the [webhook](https://github.com/adnanh/webhook/) will serve. Check [Hook definition page](https://github.com/adnanh/webhook/wiki/Hook-Definition) to see the detailed description of what properties a hook can contain, and how to use them.
|
||||||
|
|
||||||
Let's define a simple hook named `redeploy-webhook` that will run a redeploy script located in `/var/scripts/redeploy.sh`.
|
Let's define a simple hook named `redeploy-webhook` that will run a redeploy script located in `/var/scripts/redeploy.sh`.
|
||||||
|
|||||||
+41
-39
@@ -8,6 +8,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/textproto"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -18,6 +19,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
SourceHeader string = "header"
|
SourceHeader string = "header"
|
||||||
SourceQuery string = "url"
|
SourceQuery string = "url"
|
||||||
|
SourceQueryAlias string = "query"
|
||||||
SourcePayload string = "payload"
|
SourcePayload string = "payload"
|
||||||
SourceString string = "string"
|
SourceString string = "string"
|
||||||
SourceEntirePayload string = "entire-payload"
|
SourceEntirePayload string = "entire-payload"
|
||||||
@@ -93,7 +95,7 @@ func CheckPayloadSignature(payload []byte, secret string, signature string) (str
|
|||||||
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
||||||
|
|
||||||
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
|
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
|
||||||
return expectedMAC, &SignatureError{expectedMAC}
|
return expectedMAC, &SignatureError{signature}
|
||||||
}
|
}
|
||||||
return expectedMAC, err
|
return expectedMAC, err
|
||||||
}
|
}
|
||||||
@@ -205,11 +207,13 @@ type Argument struct {
|
|||||||
// based on the Argument's source
|
// based on the Argument's source
|
||||||
func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string, bool) {
|
func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string, bool) {
|
||||||
var source *map[string]interface{}
|
var source *map[string]interface{}
|
||||||
|
key := ha.Name
|
||||||
|
|
||||||
switch ha.Source {
|
switch ha.Source {
|
||||||
case SourceHeader:
|
case SourceHeader:
|
||||||
source = headers
|
source = headers
|
||||||
case SourceQuery:
|
key = textproto.CanonicalMIMEHeaderKey(ha.Name)
|
||||||
|
case SourceQuery, SourceQueryAlias:
|
||||||
source = query
|
source = query
|
||||||
case SourcePayload:
|
case SourcePayload:
|
||||||
source = payload
|
source = payload
|
||||||
@@ -242,7 +246,7 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if source != nil {
|
if source != nil {
|
||||||
return ExtractParameterAsString(ha.Name, *source)
|
return ExtractParameterAsString(key, *source)
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", false
|
return "", false
|
||||||
@@ -300,7 +304,9 @@ type Hook struct {
|
|||||||
|
|
||||||
// ParseJSONParameters decodes specified arguments to JSON objects and replaces the
|
// ParseJSONParameters decodes specified arguments to JSON objects and replaces the
|
||||||
// string with the newly created object
|
// string with the newly created object
|
||||||
func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface{}) error {
|
func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface{}) []error {
|
||||||
|
var errors = make([]error, 0)
|
||||||
|
|
||||||
for i := range h.JSONStringParameters {
|
for i := range h.JSONStringParameters {
|
||||||
if arg, ok := h.JSONStringParameters[i].Get(headers, query, payload); ok {
|
if arg, ok := h.JSONStringParameters[i].Get(headers, query, payload); ok {
|
||||||
var newArg map[string]interface{}
|
var newArg map[string]interface{}
|
||||||
@@ -311,7 +317,8 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
|
|||||||
err := decoder.Decode(&newArg)
|
err := decoder.Decode(&newArg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &ParseError{err}
|
errors = append(errors, &ParseError{err})
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var source *map[string]interface{}
|
var source *map[string]interface{}
|
||||||
@@ -321,27 +328,38 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
|
|||||||
source = headers
|
source = headers
|
||||||
case SourcePayload:
|
case SourcePayload:
|
||||||
source = payload
|
source = payload
|
||||||
case SourceQuery:
|
case SourceQuery, SourceQueryAlias:
|
||||||
source = query
|
source = query
|
||||||
}
|
}
|
||||||
|
|
||||||
if source != nil {
|
if source != nil {
|
||||||
ReplaceParameter(h.JSONStringParameters[i].Name, source, newArg)
|
key := h.JSONStringParameters[i].Name
|
||||||
|
|
||||||
|
if h.JSONStringParameters[i].Source == SourceHeader {
|
||||||
|
key = textproto.CanonicalMIMEHeaderKey(h.JSONStringParameters[i].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
ReplaceParameter(key, source, newArg)
|
||||||
} else {
|
} else {
|
||||||
return &SourceError{h.JSONStringParameters[i]}
|
errors = append(errors, &SourceError{h.JSONStringParameters[i]})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return &ArgumentError{h.JSONStringParameters[i]}
|
errors = append(errors, &ArgumentError{h.JSONStringParameters[i]})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractCommandArguments creates a list of arguments, based on the
|
// ExtractCommandArguments creates a list of arguments, based on the
|
||||||
// PassArgumentsToCommand property that is ready to be used with exec.Command()
|
// PassArgumentsToCommand property that is ready to be used with exec.Command()
|
||||||
func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) ([]string, error) {
|
func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) ([]string, []error) {
|
||||||
var args = make([]string, 0)
|
var args = make([]string, 0)
|
||||||
|
var errors = make([]error, 0)
|
||||||
|
|
||||||
args = append(args, h.ExecuteCommand)
|
args = append(args, h.ExecuteCommand)
|
||||||
|
|
||||||
@@ -350,33 +368,41 @@ func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]inter
|
|||||||
args = append(args, arg)
|
args = append(args, arg)
|
||||||
} else {
|
} else {
|
||||||
args = append(args, "")
|
args = append(args, "")
|
||||||
return args, &ArgumentError{h.PassArgumentsToCommand[i]}
|
errors = append(errors, &ArgumentError{h.PassArgumentsToCommand[i]})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
return args, errors
|
||||||
|
}
|
||||||
|
|
||||||
return args, nil
|
return args, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractCommandArgumentsForEnv creates a list of arguments in key=value
|
// ExtractCommandArgumentsForEnv creates a list of arguments in key=value
|
||||||
// format, based on the PassEnvironmentToCommand property that is ready to be used
|
// format, based on the PassEnvironmentToCommand property that is ready to be used
|
||||||
// with exec.Command().
|
// with exec.Command().
|
||||||
func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string]interface{}) ([]string, error) {
|
func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string]interface{}) ([]string, []error) {
|
||||||
var args = make([]string, 0)
|
var args = make([]string, 0)
|
||||||
|
var errors = make([]error, 0)
|
||||||
for i := range h.PassEnvironmentToCommand {
|
for i := range h.PassEnvironmentToCommand {
|
||||||
if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok {
|
if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok {
|
||||||
if h.PassEnvironmentToCommand[i].EnvName != "" {
|
if h.PassEnvironmentToCommand[i].EnvName != "" {
|
||||||
// first try to use the EnvName if specified
|
// first try to use the EnvName if specified
|
||||||
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].EnvName+"="+arg)
|
args = append(args, h.PassEnvironmentToCommand[i].EnvName+"="+arg)
|
||||||
} else {
|
} else {
|
||||||
// then fallback on the name
|
// then fallback on the name
|
||||||
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg)
|
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return args, &ArgumentError{h.PassEnvironmentToCommand[i]}
|
errors = append(errors, &ArgumentError{h.PassEnvironmentToCommand[i]})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
return args, errors
|
||||||
|
}
|
||||||
|
|
||||||
return args, nil
|
return args, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,23 +438,6 @@ func (h *Hooks) Match(id string) *Hook {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchAll iterates through Hooks and returns all of the hooks that match the
|
|
||||||
// given ID, if no hook matches the given ID, nil is returned
|
|
||||||
func (h *Hooks) MatchAll(id string) []*Hook {
|
|
||||||
var matchedHooks []*Hook
|
|
||||||
for i := range *h {
|
|
||||||
if (*h)[i].ID == id {
|
|
||||||
matchedHooks = append(matchedHooks, &(*h)[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(matchedHooks) > 0 {
|
|
||||||
return matchedHooks
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rules is a structure that contains one of the valid rule types
|
// Rules is a structure that contains one of the valid rule types
|
||||||
type Rules struct {
|
type Rules struct {
|
||||||
And *AndRule `json:"and,omitempty"`
|
And *AndRule `json:"and,omitempty"`
|
||||||
@@ -538,10 +547,3 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod
|
|||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommandStatusResponse type encapsulates the executed command exit code, message, stdout and stderr
|
|
||||||
type CommandStatusResponse struct {
|
|
||||||
ResponseMessage string `json:"message,omitempty"`
|
|
||||||
Output string `json:"output,omitempty"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|||||||
+6
-1
@@ -2,6 +2,7 @@ package hook
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,6 +26,10 @@ func TestCheckPayloadSignature(t *testing.T) {
|
|||||||
if (err == nil) != tt.ok || mac != tt.mac {
|
if (err == nil) != tt.ok || mac != tt.mac {
|
||||||
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err != nil && strings.Contains(err.Error(), tt.mac) {
|
||||||
|
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +179,7 @@ var hookExtractCommandArgumentsForEnvTests = []struct {
|
|||||||
"test",
|
"test",
|
||||||
[]Argument{Argument{"header", "a", "MYKEY"}},
|
[]Argument{Argument{"header", "a", "MYKEY"}},
|
||||||
&map[string]interface{}{"a": "z"}, nil, nil,
|
&map[string]interface{}{"a": "z"}, nil, nil,
|
||||||
[]string{"HOOK_MYKEY=z"},
|
[]string{"MYKEY=z"},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
// failures
|
// failures
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
"id": "bitbucket",
|
"id": "bitbucket",
|
||||||
"execute-command": "{{ .Hookecho }}",
|
"execute-command": "{{ .Hookecho }}",
|
||||||
"command-working-directory": "/",
|
"command-working-directory": "/",
|
||||||
"include-command-output-in-response": true,
|
"include-command-output-in-response": false,
|
||||||
"response-message": "success",
|
"response-message": "success",
|
||||||
"parse-parameters-as-json": [
|
"parse-parameters-as-json": [
|
||||||
{
|
{
|
||||||
@@ -136,4 +136,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+72
-59
@@ -21,11 +21,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
version = "2.3.9"
|
version = "2.6.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ip = flag.String("ip", "", "ip the webhook should serve hooks on")
|
ip = flag.String("ip", "0.0.0.0", "ip the webhook should serve hooks on")
|
||||||
port = flag.Int("port", 9000, "port 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")
|
verbose = flag.Bool("verbose", false, "show verbose output")
|
||||||
noPanic = flag.Bool("nopanic", false, "do not panic if hooks cannot be loaded when webhook is not running in verbose mode")
|
noPanic = flag.Bool("nopanic", false, "do not panic if hooks cannot be loaded when webhook is not running in verbose mode")
|
||||||
@@ -35,6 +35,7 @@ var (
|
|||||||
secure = flag.Bool("secure", false, "use HTTPS instead of HTTP")
|
secure = flag.Bool("secure", false, "use HTTPS instead of HTTP")
|
||||||
cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file")
|
cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file")
|
||||||
key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file")
|
key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file")
|
||||||
|
justDisplayVersion = flag.Bool("version", false, "display webhook version and quit")
|
||||||
|
|
||||||
responseHeaders hook.ResponseHeaders
|
responseHeaders hook.ResponseHeaders
|
||||||
|
|
||||||
@@ -51,6 +52,11 @@ func main() {
|
|||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if *justDisplayVersion {
|
||||||
|
fmt.Println("webhook version " + version)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
log.SetPrefix("[webhook] ")
|
log.SetPrefix("[webhook] ")
|
||||||
log.SetFlags(log.Ldate | log.Ltime)
|
log.SetFlags(log.Ldate | log.Ltime)
|
||||||
|
|
||||||
@@ -76,10 +82,16 @@ func main() {
|
|||||||
|
|
||||||
log.Printf("couldn't load hooks from file! %+v\n", err)
|
log.Printf("couldn't load hooks from file! %+v\n", err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("loaded %d hook(s) from file\n", len(hooks))
|
seenHooksIds := make(map[string]bool)
|
||||||
|
|
||||||
|
log.Printf("found %d hook(s) in file\n", len(hooks))
|
||||||
|
|
||||||
for _, hook := range hooks {
|
for _, hook := range hooks {
|
||||||
log.Printf("\t> %s\n", hook.ID)
|
if seenHooksIds[hook.ID] == true {
|
||||||
|
log.Fatalf("error: hook with the id %s has already been loaded!\nplease check your hooks file for duplicate hooks ids!\n", hook.ID)
|
||||||
|
}
|
||||||
|
seenHooksIds[hook.ID] = true
|
||||||
|
log.Printf("\tloaded: %s\n", hook.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,10 +117,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
l := negroni.NewLogger()
|
l := negroni.NewLogger()
|
||||||
l.Logger = log.New(os.Stderr, "[webhook] ", log.Ldate|log.Ltime)
|
l.ALogger = log.New(os.Stderr, "[webhook] ", log.Ldate|log.Ltime)
|
||||||
|
|
||||||
negroniRecovery := &negroni.Recovery{
|
negroniRecovery := &negroni.Recovery{
|
||||||
Logger: l.Logger,
|
Logger: l.ALogger,
|
||||||
PrintStack: true,
|
PrintStack: true,
|
||||||
StackAll: false,
|
StackAll: false,
|
||||||
StackSize: 1024 * 8,
|
StackSize: 1024 * 8,
|
||||||
@@ -131,10 +143,10 @@ func main() {
|
|||||||
n.UseHandler(router)
|
n.UseHandler(router)
|
||||||
|
|
||||||
if *secure {
|
if *secure {
|
||||||
log.Printf("starting secure (https) webhook on %s:%d", *ip, *port)
|
log.Printf("serving hooks on https://%s:%d%s", *ip, *port, hooksURL)
|
||||||
log.Fatal(http.ListenAndServeTLS(fmt.Sprintf("%s:%d", *ip, *port), *cert, *key, n))
|
log.Fatal(http.ListenAndServeTLS(fmt.Sprintf("%s:%d", *ip, *port), *cert, *key, n))
|
||||||
} else {
|
} else {
|
||||||
log.Printf("starting insecure (http) webhook on %s:%d", *ip, *port)
|
log.Printf("serving hooks on http://%s:%d%s", *ip, *port, hooksURL)
|
||||||
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *ip, *port), n))
|
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *ip, *port), n))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,10 +159,8 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
id := mux.Vars(r)["id"]
|
id := mux.Vars(r)["id"]
|
||||||
|
|
||||||
matchedHooks := hooks.MatchAll(id)
|
if matchedHook := hooks.Match(id); matchedHook != nil {
|
||||||
|
log.Printf("%s got matched\n", id)
|
||||||
if matchedHooks != nil {
|
|
||||||
log.Printf("%s got matched (%d time(s))\n", id, len(matchedHooks))
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -187,53 +197,53 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle hook
|
// handle hook
|
||||||
for _, h := range matchedHooks {
|
if errors := matchedHook.ParseJSONParameters(&headers, &query, &payload); errors != nil {
|
||||||
|
for _, err := range errors {
|
||||||
err := h.ParseJSONParameters(&headers, &query, &payload)
|
log.Printf("error parsing JSON parameters: %s\n", err)
|
||||||
if err != nil {
|
}
|
||||||
msg := fmt.Sprintf("error parsing JSON: %s", err)
|
|
||||||
log.Printf(msg)
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
fmt.Fprintf(w, msg)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var ok bool
|
var ok bool
|
||||||
|
|
||||||
if h.TriggerRule == nil {
|
if matchedHook.TriggerRule == nil {
|
||||||
ok = true
|
ok = true
|
||||||
} else {
|
} else {
|
||||||
ok, err = h.TriggerRule.Evaluate(&headers, &query, &payload, &body)
|
ok, err = matchedHook.TriggerRule.Evaluate(&headers, &query, &payload, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := fmt.Sprintf("error evaluating hook: %s", err)
|
msg := fmt.Sprintf("error evaluating hook: %s", err)
|
||||||
log.Printf(msg)
|
log.Printf(msg)
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
fmt.Fprintf(w, msg)
|
fmt.Fprintf(w, "Error occurred while evaluating hook rules.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
log.Printf("%s hook triggered successfully\n", h.ID)
|
log.Printf("%s hook triggered successfully\n", matchedHook.ID)
|
||||||
|
|
||||||
for _, responseHeader := range h.ResponseHeaders {
|
for _, responseHeader := range matchedHook.ResponseHeaders {
|
||||||
w.Header().Set(responseHeader.Name, responseHeader.Value)
|
w.Header().Set(responseHeader.Name, responseHeader.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.CaptureCommandOutput {
|
if matchedHook.CaptureCommandOutput {
|
||||||
response := handleHook(h, &headers, &query, &payload, &body)
|
response, err := handleHook(matchedHook, &headers, &query, &payload, &body)
|
||||||
fmt.Fprintf(w, response)
|
|
||||||
} else {
|
|
||||||
go handleHook(h, &headers, &query, &payload, &body)
|
|
||||||
fmt.Fprintf(w, h.ResponseMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
if err != nil {
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprintf(w, "Error occurred while executing the hook's command. Please check your logs for more details.")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, response)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
go handleHook(matchedHook, &headers, &query, &payload, &body)
|
||||||
|
fmt.Fprintf(w, matchedHook.ResponseMessage)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// if none of the hooks got triggered
|
// if none of the hooks got triggered
|
||||||
log.Printf("%s got matched (%d time(s)), but didn't get triggered because the trigger rules were not satisfied\n", matchedHooks[0].ID, len(matchedHooks))
|
log.Printf("%s got matched, but didn't get triggered because the trigger rules were not satisfied\n", matchedHook.ID)
|
||||||
|
|
||||||
fmt.Fprintf(w, "Hook rules were not satisfied.")
|
fmt.Fprintf(w, "Hook rules were not satisfied.")
|
||||||
} else {
|
} else {
|
||||||
@@ -242,48 +252,43 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) string {
|
func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) (string, error) {
|
||||||
var err error
|
var errors []error
|
||||||
|
|
||||||
cmd := exec.Command(h.ExecuteCommand)
|
cmd := exec.Command(h.ExecuteCommand)
|
||||||
cmd.Dir = h.CommandWorkingDirectory
|
cmd.Dir = h.CommandWorkingDirectory
|
||||||
|
|
||||||
cmd.Args, err = h.ExtractCommandArguments(headers, query, payload)
|
cmd.Args, errors = h.ExtractCommandArguments(headers, query, payload)
|
||||||
if err != nil {
|
if errors != nil {
|
||||||
log.Printf("error extracting command arguments: %s", err)
|
for _, err := range errors {
|
||||||
|
log.Printf("error extracting command arguments: %s\n", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var envs []string
|
var envs []string
|
||||||
envs, err = h.ExtractCommandArgumentsForEnv(headers, query, payload)
|
envs, errors = h.ExtractCommandArgumentsForEnv(headers, query, payload)
|
||||||
if err != nil {
|
|
||||||
log.Printf("error extracting command arguments for environment: %s", err)
|
if errors != nil {
|
||||||
|
for _, err := range errors {
|
||||||
|
log.Printf("error extracting command arguments for environment: %s\n", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cmd.Env = append(os.Environ(), envs...)
|
cmd.Env = append(os.Environ(), envs...)
|
||||||
|
|
||||||
log.Printf("executing %s (%s) with arguments %q and environment %s using %s as cwd\n", h.ExecuteCommand, cmd.Path, cmd.Args, envs, cmd.Dir)
|
log.Printf("executing %s (%s) with arguments %q and environment %s using %s as cwd\n", h.ExecuteCommand, cmd.Path, cmd.Args, envs, cmd.Dir)
|
||||||
|
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.Output()
|
||||||
|
|
||||||
log.Printf("command output: %s\n", out)
|
log.Printf("command output: %s\n", out)
|
||||||
|
|
||||||
var errorResponse string
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error occurred: %+v\n", err)
|
log.Printf("error occurred: %+v\n", err)
|
||||||
errorResponse = fmt.Sprintf("%+v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("finished handling %s\n", h.ID)
|
log.Printf("finished handling %s\n", h.ID)
|
||||||
|
|
||||||
var response []byte
|
return string(out), err
|
||||||
response, err = json.Marshal(&hook.CommandStatusResponse{ResponseMessage: h.ResponseMessage, Output: string(out), Error: errorResponse})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error marshalling response: %+v", err)
|
|
||||||
return h.ResponseMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func reloadHooks() {
|
func reloadHooks() {
|
||||||
@@ -297,10 +302,18 @@ func reloadHooks() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("couldn't load hooks from file! %+v\n", err)
|
log.Printf("couldn't load hooks from file! %+v\n", err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("loaded %d hook(s) from file\n", len(hooks))
|
seenHooksIds := make(map[string]bool)
|
||||||
|
|
||||||
for _, hook := range hooks {
|
log.Printf("found %d hook(s) in file\n", len(newHooks))
|
||||||
log.Printf("\t> %s\n", hook.ID)
|
|
||||||
|
for _, hook := range newHooks {
|
||||||
|
if seenHooksIds[hook.ID] == true {
|
||||||
|
log.Printf("error: hook with the id %s has already been loaded!\nplease check your hooks file for duplicate hooks ids!", hook.ID)
|
||||||
|
log.Println("reverting hooks back to the previous configuration")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seenHooksIds[hook.ID] = true
|
||||||
|
log.Printf("\tloaded: %s\n", hook.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
hooks = newHooks
|
hooks = newHooks
|
||||||
|
|||||||
+11
-5
@@ -372,7 +372,9 @@ var hookHandlerTests = []struct {
|
|||||||
}`,
|
}`,
|
||||||
false,
|
false,
|
||||||
http.StatusOK,
|
http.StatusOK,
|
||||||
`{"output":"arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz\nenv: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00\n"}`,
|
`arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
|
||||||
|
env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
|
||||||
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bitbucket", // bitbucket sends their payload using uriencoded params.
|
"bitbucket", // bitbucket sends their payload using uriencoded params.
|
||||||
@@ -381,7 +383,7 @@ var hookHandlerTests = []struct {
|
|||||||
`payload={"canon_url": "https://bitbucket.org","commits": [{"author": "marcus","branch": "master","files": [{"file": "somefile.py","type": "modified"}],"message": "Added some more things to somefile.py\n","node": "620ade18607a","parents": ["702c70160afc"],"raw_author": "Marcus Bertrand <marcus@somedomain.com>","raw_node": "620ade18607ac42d872b568bb92acaa9a28620e9","revision": null,"size": -1,"timestamp": "2012-05-30 05:58:56","utctimestamp": "2014-11-07 15:19:02+00:00"}],"repository": {"absolute_url": "/webhook/testing/","fork": false,"is_private": true,"name": "Project X","owner": "marcus","scm": "git","slug": "project-x","website": "https://atlassian.com/"},"user": "marcus"}`,
|
`payload={"canon_url": "https://bitbucket.org","commits": [{"author": "marcus","branch": "master","files": [{"file": "somefile.py","type": "modified"}],"message": "Added some more things to somefile.py\n","node": "620ade18607a","parents": ["702c70160afc"],"raw_author": "Marcus Bertrand <marcus@somedomain.com>","raw_node": "620ade18607ac42d872b568bb92acaa9a28620e9","revision": null,"size": -1,"timestamp": "2012-05-30 05:58:56","utctimestamp": "2014-11-07 15:19:02+00:00"}],"repository": {"absolute_url": "/webhook/testing/","fork": false,"is_private": true,"name": "Project X","owner": "marcus","scm": "git","slug": "project-x","website": "https://atlassian.com/"},"user": "marcus"}`,
|
||||||
true,
|
true,
|
||||||
http.StatusOK,
|
http.StatusOK,
|
||||||
`{"message":"success"}`,
|
`success`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"gitlab",
|
"gitlab",
|
||||||
@@ -431,7 +433,8 @@ var hookHandlerTests = []struct {
|
|||||||
}`,
|
}`,
|
||||||
false,
|
false,
|
||||||
http.StatusOK,
|
http.StatusOK,
|
||||||
`{"message":"success","output":"arg: b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327 John Smith john@example.com\n"}`,
|
`arg: b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327 John Smith john@example.com
|
||||||
|
`,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -469,7 +472,9 @@ var hookHandlerTests = []struct {
|
|||||||
}`,
|
}`,
|
||||||
false,
|
false,
|
||||||
http.StatusOK,
|
http.StatusOK,
|
||||||
`{"output":"arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz\nenv: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00\n"}`,
|
`arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
|
||||||
|
env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
|
||||||
|
`,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -506,7 +511,8 @@ var hookHandlerTests = []struct {
|
|||||||
}`,
|
}`,
|
||||||
false,
|
false,
|
||||||
http.StatusOK,
|
http.StatusOK,
|
||||||
`{"output":"arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz\n"}`,
|
`arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
|
||||||
|
`,
|
||||||
},
|
},
|
||||||
|
|
||||||
{"empty payload", "github", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`},
|
{"empty payload", "github", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`},
|
||||||
|
|||||||
Reference in New Issue
Block a user