mirror of
https://github.com/adnanh/webhook.git
synced 2026-07-27 01:29:16 +08:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c51971fd37 | |||
| 93e5fe7712 | |||
| 058f820cbd | |||
| ddb1f2441a | |||
| 8226d5e50f | |||
| e149c99724 | |||
| 6ea2d68b44 | |||
| 5789362186 | |||
| 07f166616c | |||
| b66216675a | |||
| ecbcf11153 | |||
| 7d525cf317 | |||
| e83d7029ff | |||
| 36c2c692d6 | |||
| 86cef3e421 | |||
| 75cf8952be | |||
| c53596df59 | |||
| cc0d9b2cba | |||
| c6530b17e7 | |||
| 1943c5311f | |||
| 3b59539a33 | |||
| 923b0c6daa | |||
| 8530255ae6 | |||
| 032c74451f | |||
| 1c50853d8d | |||
| b5ed4cbea7 | |||
| b6d176705e | |||
| 421fc2cbcd | |||
| 10d65dd2fd | |||
| 54a9dbe1d6 | |||
| 30baec91df | |||
| 3bcf6d5e2b | |||
| 67343e281d | |||
| 18b0573bc4 | |||
| ec42679305 | |||
| e85e0592dd | |||
| 4d20af8027 | |||
| d4e772c815 | |||
| 04a2b2a680 | |||
| 4914a4131f | |||
| 37698e63b6 | |||
| 80aa9800bf | |||
| f620cb056b | |||
| e55e7efe14 | |||
| 9fa02f7341 |
@@ -0,0 +1,4 @@
|
||||
.idea
|
||||
.cover
|
||||
coverage
|
||||
webhook
|
||||
|
||||
@@ -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
|
||||
@@ -1,5 +1,4 @@
|
||||
[](https://gitter.im/adnanh/webhook?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://flattr.com/submit/auto?user_id=adnanh&url=https%3A%2F%2Fwww.github.com%2Fadnanh%2Fwebhook)
|
||||
|
||||
[](https://ghit.me/repo/adnanh/webhook) [](https://gitter.im/adnanh/webhook?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://flattr.com/submit/auto?user_id=adnanh&url=https%3A%2F%2Fwww.github.com%2Fadnanh%2Fwebhook) [Donate via PayPal](https://paypal.me/hookdoo) | [Patreon page](https://www.patreon.com/webhook)
|
||||
# What is webhook?
|
||||
[webhook](https://github.com/adnanh/webhook/) is a lightweight configurable tool written in Go, that allows you to easily create HTTP endpoints (hooks) on your server, which you can use to execute configured commands. You can also pass data from the HTTP request (such as headers, payload or query variables) to your commands. [webhook](https://github.com/adnanh/webhook/) also allows you to specify rules which have to be satisfied in order for the hook to be triggered.
|
||||
|
||||
@@ -19,12 +18,19 @@ Everything else is the responsibility of the command's author.
|
||||
---
|
||||
|
||||
# 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
|
||||
```bash
|
||||
$ go get 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.
|
||||
|
||||
Let's define a simple hook named `redeploy-webhook` that will run a redeploy script located in `/var/scripts/redeploy.sh`.
|
||||
@@ -59,6 +65,12 @@ However, hook defined like that could pose a security threat to your system, bec
|
||||
# Using HTTPS
|
||||
[webhook](https://github.com/adnanh/webhook/) by default serves hooks using http. If you want [webhook](https://github.com/adnanh/webhook/) to serve secure content using https, you can use the `-secure` flag while starting [webhook](https://github.com/adnanh/webhook/). Files containing a certificate and matching private key for the server must be provided using the `-cert /path/to/cert.pem` and `-key /path/to/key.pem` flags. If the certificate is signed by a certificate authority, the cert file should be the concatenation of the server's certificate followed by the CA's certificate.
|
||||
|
||||
# CORS Headers
|
||||
If you want to set CORS headers, you can use the `-header name=value` flag while starting [webhook](https://github.com/adnanh/webhook/) to set the appropriate CORS headers that will be returned with each response.
|
||||
|
||||
# Interested in running webhook inside of a Docker container?
|
||||
You can use [almir/webhook](https://hub.docker.com/r/almir/webhook/) docker image, or create your own (please read [this discussion](https://github.com/adnanh/webhook/issues/63)).
|
||||
|
||||
# Examples
|
||||
Check out [Hook examples page](https://github.com/adnanh/webhook/wiki/Hook-Examples) for more complex examples of hooks.
|
||||
|
||||
|
||||
+98
-50
@@ -5,8 +5,10 @@ import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/textproto"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -17,6 +19,7 @@ import (
|
||||
const (
|
||||
SourceHeader string = "header"
|
||||
SourceQuery string = "url"
|
||||
SourceQueryAlias string = "query"
|
||||
SourcePayload string = "payload"
|
||||
SourceString string = "string"
|
||||
SourceEntirePayload string = "entire-payload"
|
||||
@@ -92,7 +95,7 @@ func CheckPayloadSignature(payload []byte, secret string, signature string) (str
|
||||
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
|
||||
return expectedMAC, &SignatureError{expectedMAC}
|
||||
return expectedMAC, &SignatureError{signature}
|
||||
}
|
||||
return expectedMAC, err
|
||||
}
|
||||
@@ -195,19 +198,22 @@ func ExtractParameterAsString(s string, params interface{}) (string, bool) {
|
||||
// Argument type specifies the parameter key name and the source it should
|
||||
// be extracted from
|
||||
type Argument struct {
|
||||
Source string `json:"source,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
EnvName string `json:"envname,omitempty"`
|
||||
}
|
||||
|
||||
// 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{}
|
||||
key := ha.Name
|
||||
|
||||
switch ha.Source {
|
||||
case SourceHeader:
|
||||
source = headers
|
||||
case SourceQuery:
|
||||
key = textproto.CanonicalMIMEHeaderKey(ha.Name)
|
||||
case SourceQuery, SourceQueryAlias:
|
||||
source = query
|
||||
case SourcePayload:
|
||||
source = payload
|
||||
@@ -240,28 +246,68 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
|
||||
}
|
||||
|
||||
if source != nil {
|
||||
return ExtractParameterAsString(ha.Name, *source)
|
||||
return ExtractParameterAsString(key, *source)
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Header is a structure containing header name and it's value
|
||||
type Header struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// ResponseHeaders is a slice of Header objects
|
||||
type ResponseHeaders []Header
|
||||
|
||||
func (h *ResponseHeaders) String() string {
|
||||
// a 'hack' to display name=value in flag usage listing
|
||||
if len(*h) == 0 {
|
||||
return "name=value"
|
||||
}
|
||||
|
||||
result := make([]string, len(*h))
|
||||
|
||||
for idx, responseHeader := range *h {
|
||||
result[idx] = fmt.Sprintf("%s=%s", responseHeader.Name, responseHeader.Value)
|
||||
}
|
||||
|
||||
return fmt.Sprint(strings.Join(result, ", "))
|
||||
}
|
||||
|
||||
// Set method appends new Header object from header=value notation
|
||||
func (h *ResponseHeaders) Set(value string) error {
|
||||
splitResult := strings.SplitN(value, "=", 2)
|
||||
|
||||
if len(splitResult) != 2 {
|
||||
return errors.New("header flag must be in name=value format")
|
||||
}
|
||||
|
||||
*h = append(*h, Header{Name: splitResult[0], Value: splitResult[1]})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Hook type is a structure containing details for a single hook
|
||||
type Hook struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
ExecuteCommand string `json:"execute-command,omitempty"`
|
||||
CommandWorkingDirectory string `json:"command-working-directory,omitempty"`
|
||||
ResponseMessage string `json:"response-message,omitempty"`
|
||||
CaptureCommandOutput bool `json:"include-command-output-in-response,omitempty"`
|
||||
PassEnvironmentToCommand []Argument `json:"pass-environment-to-command,omitempty"`
|
||||
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command,omitempty"`
|
||||
JSONStringParameters []Argument `json:"parse-parameters-as-json,omitempty"`
|
||||
TriggerRule *Rules `json:"trigger-rule,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
ExecuteCommand string `json:"execute-command,omitempty"`
|
||||
CommandWorkingDirectory string `json:"command-working-directory,omitempty"`
|
||||
ResponseMessage string `json:"response-message,omitempty"`
|
||||
ResponseHeaders ResponseHeaders `json:"response-headers,omitempty"`
|
||||
CaptureCommandOutput bool `json:"include-command-output-in-response,omitempty"`
|
||||
PassEnvironmentToCommand []Argument `json:"pass-environment-to-command,omitempty"`
|
||||
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command,omitempty"`
|
||||
JSONStringParameters []Argument `json:"parse-parameters-as-json,omitempty"`
|
||||
TriggerRule *Rules `json:"trigger-rule,omitempty"`
|
||||
TriggerRuleMismatchHttpResponseCode int `json:"trigger-rule-mismatch-http-response-code,omitempty"`
|
||||
}
|
||||
|
||||
// ParseJSONParameters decodes specified arguments to JSON objects and replaces the
|
||||
// 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 {
|
||||
if arg, ok := h.JSONStringParameters[i].Get(headers, query, payload); ok {
|
||||
var newArg map[string]interface{}
|
||||
@@ -272,7 +318,8 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
|
||||
err := decoder.Decode(&newArg)
|
||||
|
||||
if err != nil {
|
||||
return &ParseError{err}
|
||||
errors = append(errors, &ParseError{err})
|
||||
continue
|
||||
}
|
||||
|
||||
var source *map[string]interface{}
|
||||
@@ -282,27 +329,38 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
|
||||
source = headers
|
||||
case SourcePayload:
|
||||
source = payload
|
||||
case SourceQuery:
|
||||
case SourceQuery, SourceQueryAlias:
|
||||
source = query
|
||||
}
|
||||
|
||||
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 {
|
||||
return &SourceError{h.JSONStringParameters[i]}
|
||||
errors = append(errors, &SourceError{h.JSONStringParameters[i]})
|
||||
}
|
||||
} else {
|
||||
return &ArgumentError{h.JSONStringParameters[i]}
|
||||
errors = append(errors, &ArgumentError{h.JSONStringParameters[i]})
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return errors
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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, error) {
|
||||
func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) ([]string, []error) {
|
||||
var args = make([]string, 0)
|
||||
var errors = make([]error, 0)
|
||||
|
||||
args = append(args, h.ExecuteCommand)
|
||||
|
||||
@@ -311,27 +369,41 @@ func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]inter
|
||||
args = append(args, arg)
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
// ExtractCommandArgumentsForEnv creates a list of arguments in key=value
|
||||
// format, based on the PassEnvironmentToCommand property that is ready to be used
|
||||
// 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 errors = make([]error, 0)
|
||||
for i := range h.PassEnvironmentToCommand {
|
||||
if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok {
|
||||
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg)
|
||||
if h.PassEnvironmentToCommand[i].EnvName != "" {
|
||||
// first try to use the EnvName if specified
|
||||
args = append(args, h.PassEnvironmentToCommand[i].EnvName+"="+arg)
|
||||
} else {
|
||||
// then fallback on the name
|
||||
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg)
|
||||
}
|
||||
} else {
|
||||
return args, &ArgumentError{h.PassEnvironmentToCommand[i]}
|
||||
errors = append(errors, &ArgumentError{h.PassEnvironmentToCommand[i]})
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return args, errors
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
@@ -367,23 +439,6 @@ func (h *Hooks) Match(id string) *Hook {
|
||||
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
|
||||
type Rules struct {
|
||||
And *AndRule `json:"and,omitempty"`
|
||||
@@ -493,10 +548,3 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod
|
||||
}
|
||||
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"`
|
||||
}
|
||||
|
||||
+116
-50
@@ -2,6 +2,7 @@ package hook
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -25,6 +26,10 @@ func TestCheckPayloadSignature(t *testing.T) {
|
||||
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))
|
||||
}
|
||||
|
||||
if err != nil && strings.Contains(err.Error(), tt.mac) {
|
||||
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,20 +72,20 @@ var argumentGetTests = []struct {
|
||||
value string
|
||||
ok bool
|
||||
}{
|
||||
{"header", "a", &map[string]interface{}{"a": "z"}, nil, nil, "z", true},
|
||||
{"header", "a", &map[string]interface{}{"A": "z"}, nil, nil, "z", true},
|
||||
{"url", "a", nil, &map[string]interface{}{"a": "z"}, nil, "z", true},
|
||||
{"payload", "a", nil, nil, &map[string]interface{}{"a": "z"}, "z", true},
|
||||
{"string", "a", nil, nil, &map[string]interface{}{"a": "z"}, "a", true},
|
||||
// failures
|
||||
{"header", "a", nil, &map[string]interface{}{"a": "z"}, &map[string]interface{}{"a": "z"}, "", false}, // nil headers
|
||||
{"url", "a", &map[string]interface{}{"a": "z"}, nil, &map[string]interface{}{"a": "z"}, "", false}, // nil query
|
||||
{"payload", "a", &map[string]interface{}{"a": "z"}, &map[string]interface{}{"a": "z"}, nil, "", false}, // nil payload
|
||||
{"foo", "a", &map[string]interface{}{"a": "z"}, nil, nil, "", false}, // invalid source
|
||||
{"url", "a", &map[string]interface{}{"A": "z"}, nil, &map[string]interface{}{"a": "z"}, "", false}, // nil query
|
||||
{"payload", "a", &map[string]interface{}{"A": "z"}, &map[string]interface{}{"a": "z"}, nil, "", false}, // nil payload
|
||||
{"foo", "a", &map[string]interface{}{"A": "z"}, nil, nil, "", false}, // invalid source
|
||||
}
|
||||
|
||||
func TestArgumentGet(t *testing.T) {
|
||||
for _, tt := range argumentGetTests {
|
||||
a := Argument{tt.source, tt.name}
|
||||
a := Argument{tt.source, tt.name, ""}
|
||||
value, ok := a.Get(tt.headers, tt.query, tt.payload)
|
||||
if ok != tt.ok || value != tt.value {
|
||||
t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, ok:%#v}", tt.source, tt.name, tt.value, tt.ok, value, ok)
|
||||
@@ -94,14 +99,14 @@ var hookParseJSONParametersTests = []struct {
|
||||
rheaders, rquery, rpayload *map[string]interface{}
|
||||
ok bool
|
||||
}{
|
||||
{[]Argument{Argument{"header", "a"}}, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, nil, true},
|
||||
{[]Argument{Argument{"url", "a"}}, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
|
||||
{[]Argument{Argument{"payload", "a"}}, nil, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
|
||||
{[]Argument{Argument{"header", "z"}}, &map[string]interface{}{"z": `{}`}, nil, nil, &map[string]interface{}{"z": map[string]interface{}{}}, nil, nil, true},
|
||||
{[]Argument{Argument{"header", "a", ""}}, &map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, true},
|
||||
{[]Argument{Argument{"url", "a", ""}}, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
|
||||
{[]Argument{Argument{"payload", "a", ""}}, nil, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
|
||||
{[]Argument{Argument{"header", "z", ""}}, &map[string]interface{}{"Z": `{}`}, nil, nil, &map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true},
|
||||
// failures
|
||||
{[]Argument{Argument{"header", "z"}}, &map[string]interface{}{"z": ``}, nil, nil, &map[string]interface{}{"z": ``}, nil, nil, false}, // empty string
|
||||
{[]Argument{Argument{"header", "y"}}, &map[string]interface{}{"X": `{}`}, nil, nil, &map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter
|
||||
{[]Argument{Argument{"string", "z"}}, &map[string]interface{}{"z": ``}, nil, nil, &map[string]interface{}{"z": ``}, nil, nil, false}, // invalid argument source
|
||||
{[]Argument{Argument{"header", "z", ""}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // empty string
|
||||
{[]Argument{Argument{"header", "y", ""}}, &map[string]interface{}{"X": `{}`}, nil, nil, &map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter
|
||||
{[]Argument{Argument{"string", "z", ""}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source
|
||||
}
|
||||
|
||||
func TestHookParseJSONParameters(t *testing.T) {
|
||||
@@ -121,9 +126,9 @@ var hookExtractCommandArgumentsTests = []struct {
|
||||
value []string
|
||||
ok bool
|
||||
}{
|
||||
{"test", []Argument{Argument{"header", "a"}}, &map[string]interface{}{"a": "z"}, nil, nil, []string{"test", "z"}, true},
|
||||
{"test", []Argument{Argument{"header", "a", ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"test", "z"}, true},
|
||||
// failures
|
||||
{"fail", []Argument{Argument{"payload", "a"}}, &map[string]interface{}{"a": "z"}, nil, nil, []string{"fail", ""}, false},
|
||||
{"fail", []Argument{Argument{"payload", "a", ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"fail", ""}, false},
|
||||
}
|
||||
|
||||
func TestHookExtractCommandArguments(t *testing.T) {
|
||||
@@ -136,6 +141,67 @@ func TestHookExtractCommandArguments(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Here we test the extraction of env variables when the user defined a hook
|
||||
// with the "pass-environment-to-command" directive
|
||||
// we test both cases where the name of the data is used as the name of the
|
||||
// env key & the case where the hook definition sets the env var name to a
|
||||
// fixed value using the envname construct like so::
|
||||
// [
|
||||
// {
|
||||
// "id": "push",
|
||||
// "execute-command": "bb2mm",
|
||||
// "command-working-directory": "/tmp",
|
||||
// "pass-environment-to-command":
|
||||
// [
|
||||
// {
|
||||
// "source": "entire-payload",
|
||||
// "envname": "PAYLOAD"
|
||||
// },
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
var hookExtractCommandArgumentsForEnvTests = []struct {
|
||||
exec string
|
||||
args []Argument
|
||||
headers, query, payload *map[string]interface{}
|
||||
value []string
|
||||
ok bool
|
||||
}{
|
||||
// successes
|
||||
{
|
||||
"test",
|
||||
[]Argument{Argument{"header", "a", ""}},
|
||||
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{"HOOK_a=z"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"test",
|
||||
[]Argument{Argument{"header", "a", "MYKEY"}},
|
||||
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{"MYKEY=z"},
|
||||
true,
|
||||
},
|
||||
// failures
|
||||
{
|
||||
"fail",
|
||||
[]Argument{Argument{"payload", "a", ""}},
|
||||
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestHookExtractCommandArgumentsForEnv(t *testing.T) {
|
||||
for _, tt := range hookExtractCommandArgumentsForEnvTests {
|
||||
h := &Hook{ExecuteCommand: tt.exec, PassEnvironmentToCommand: tt.args}
|
||||
value, err := h.ExtractCommandArgumentsForEnv(tt.headers, tt.query, tt.payload)
|
||||
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
|
||||
t.Errorf("failed to extract args for env {cmd=%q, args=%v}:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.exec, tt.args, tt.value, tt.ok, value, (err == nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hooksLoadFromFileTests = []struct {
|
||||
path string
|
||||
ok bool
|
||||
@@ -182,16 +248,16 @@ var matchRuleTests = []struct {
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{"value", "", "", "z", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, true, false},
|
||||
{"regex", "^z", "", "z", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, true, false},
|
||||
{"payload-hash-sha1", "", "secret", "", Argument{"header", "a"}, &map[string]interface{}{"a": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), true, false},
|
||||
{"value", "", "", "z", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
|
||||
{"regex", "^z", "", "z", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
|
||||
{"payload-hash-sha1", "", "secret", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), true, false},
|
||||
// failures
|
||||
{"value", "", "", "X", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, false},
|
||||
{"regex", "^X", "", "", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, false},
|
||||
{"value", "", "2", "X", Argument{"header", "a"}, &map[string]interface{}{"y": "z"}, nil, nil, []byte{}, false, false}, // reference invalid header
|
||||
{"value", "", "", "X", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
|
||||
{"regex", "^X", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
|
||||
{"value", "", "2", "X", Argument{"header", "a", ""}, &map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, false, false}, // reference invalid header
|
||||
// errors
|
||||
{"regex", "*", "", "", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, true}, // invalid regex
|
||||
{"payload-hash-sha1", "", "secret", "", Argument{"header", "a"}, &map[string]interface{}{"a": ""}, nil, nil, []byte{}, false, true}, // invalid hmac
|
||||
{"regex", "*", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, true}, // invalid regex
|
||||
{"payload-hash-sha1", "", "secret", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac
|
||||
}
|
||||
|
||||
func TestMatchRule(t *testing.T) {
|
||||
@@ -215,53 +281,53 @@ var andRuleTests = []struct {
|
||||
{
|
||||
"(a=z, b=y): a=z && b=y",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
||||
},
|
||||
&map[string]interface{}{"a": "z", "b": "y"}, nil, nil, []byte{},
|
||||
&map[string]interface{}{"A": "z", "B": "y"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=z, b=Y): a=z && b=y",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
||||
},
|
||||
&map[string]interface{}{"a": "z", "b": "Y"}, nil, nil, []byte{},
|
||||
&map[string]interface{}{"A": "z", "B": "Y"}, nil, nil, []byte{},
|
||||
false, false,
|
||||
},
|
||||
// Complex test to cover Rules.Evaluate
|
||||
{
|
||||
"(a=z, b=y, c=x, d=w=, e=X, f=X): a=z && (b=y && c=x) && (d=w || e=v) && !f=u",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
||||
{
|
||||
And: &AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c"}}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
||||
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", ""}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Or: &OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d"}}},
|
||||
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e"}}},
|
||||
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", ""}}},
|
||||
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", ""}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Not: &NotRule{
|
||||
Match: &MatchRule{"value", "", "", "u", Argument{"header", "f"}},
|
||||
Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", ""}},
|
||||
},
|
||||
},
|
||||
},
|
||||
&map[string]interface{}{"a": "z", "b": "y", "c": "x", "d": "w", "e": "X", "f": "X"}, nil, nil, []byte{},
|
||||
&map[string]interface{}{"A": "z", "B": "y", "C": "x", "D": "w", "E": "X", "F": "X"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
},
|
||||
{"empty rule", AndRule{{}}, nil, nil, nil, nil, false, false},
|
||||
// failures
|
||||
{
|
||||
"invalid rule",
|
||||
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a"}}}},
|
||||
&map[string]interface{}{"y": "z"}, nil, nil, nil,
|
||||
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}}}},
|
||||
&map[string]interface{}{"Y": "z"}, nil, nil, nil,
|
||||
false, false,
|
||||
},
|
||||
}
|
||||
@@ -286,37 +352,37 @@ var orRuleTests = []struct {
|
||||
{
|
||||
"(a=z, b=X): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
||||
},
|
||||
&map[string]interface{}{"a": "z", "b": "X"}, nil, nil, []byte{},
|
||||
&map[string]interface{}{"A": "z", "B": "X"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=X, b=y): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
||||
},
|
||||
&map[string]interface{}{"a": "X", "b": "y"}, nil, nil, []byte{},
|
||||
&map[string]interface{}{"A": "X", "B": "y"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
},
|
||||
{
|
||||
"(a=Z, b=Y): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
||||
},
|
||||
&map[string]interface{}{"a": "Z", "b": "Y"}, nil, nil, []byte{},
|
||||
&map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil, []byte{},
|
||||
false, false,
|
||||
},
|
||||
// failures
|
||||
{
|
||||
"invalid rule",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
||||
},
|
||||
&map[string]interface{}{"y": "Z"}, nil, nil, []byte{},
|
||||
&map[string]interface{}{"Y": "Z"}, nil, nil, []byte{},
|
||||
false, false,
|
||||
},
|
||||
}
|
||||
@@ -338,8 +404,8 @@ var notRuleTests = []struct {
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a"}}}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, true, false},
|
||||
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, false},
|
||||
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
|
||||
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
|
||||
}
|
||||
|
||||
func TestNotRule(t *testing.T) {
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"response-message": "I got the payload!",
|
||||
"response-headers":
|
||||
[
|
||||
{
|
||||
"name": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
],
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"include-command-output-in-response": true,
|
||||
"trigger-rule-mismatch-http-response-code": 400,
|
||||
"pass-environment-to-command":
|
||||
[
|
||||
{
|
||||
@@ -57,8 +58,9 @@
|
||||
"id": "bitbucket",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"include-command-output-in-response": true,
|
||||
"include-command-output-in-response": false,
|
||||
"response-message": "success",
|
||||
"trigger-rule-mismatch-http-response-code": 999,
|
||||
"parse-parameters-as-json": [
|
||||
{
|
||||
"source": "payload",
|
||||
@@ -136,4 +138,3 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
+117
-80
@@ -21,20 +21,23 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
version = "2.3.7"
|
||||
version = "2.6.1"
|
||||
)
|
||||
|
||||
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")
|
||||
noPanic = flag.Bool("nopanic", false, "do not panic if hooks cannot be loaded when webhook is not running in verbose mode")
|
||||
hotReload = flag.Bool("hotreload", false, "watch hooks file for changes and reload them automatically")
|
||||
hooksFilePath = flag.String("hooks", "hooks.json", "path to the json file containing defined hooks the webhook should serve")
|
||||
hooksURLPrefix = flag.String("urlprefix", "hooks", "url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id)")
|
||||
secure = flag.Bool("secure", false, "use HTTPS instead of HTTP")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
hotReload = flag.Bool("hotreload", false, "watch hooks file for changes and reload them automatically")
|
||||
hooksFilePath = flag.String("hooks", "hooks.json", "path to the json file containing defined hooks the webhook should serve")
|
||||
hooksURLPrefix = flag.String("urlprefix", "hooks", "url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id)")
|
||||
secure = flag.Bool("secure", false, "use HTTPS instead of HTTP")
|
||||
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")
|
||||
justDisplayVersion = flag.Bool("version", false, "display webhook version and quit")
|
||||
|
||||
responseHeaders hook.ResponseHeaders
|
||||
|
||||
watcher *fsnotify.Watcher
|
||||
signals chan os.Signal
|
||||
@@ -45,8 +48,15 @@ var (
|
||||
func main() {
|
||||
hooks = hook.Hooks{}
|
||||
|
||||
flag.Var(&responseHeaders, "header", "response header to return, specified in format name=value, use multiple times to set multiple headers")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *justDisplayVersion {
|
||||
fmt.Println("webhook version " + version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
log.SetPrefix("[webhook] ")
|
||||
log.SetFlags(log.Ldate | log.Ltime)
|
||||
|
||||
@@ -72,10 +82,16 @@ func main() {
|
||||
|
||||
log.Printf("couldn't load hooks from file! %+v\n", err)
|
||||
} 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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,10 +117,10 @@ func main() {
|
||||
}
|
||||
|
||||
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{
|
||||
Logger: l.Logger,
|
||||
Logger: l.ALogger,
|
||||
PrintStack: true,
|
||||
StackAll: false,
|
||||
StackSize: 1024 * 8,
|
||||
@@ -127,22 +143,24 @@ func main() {
|
||||
n.UseHandler(router)
|
||||
|
||||
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))
|
||||
} 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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
for _, responseHeader := range responseHeaders {
|
||||
w.Header().Set(responseHeader.Name, responseHeader.Value)
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
matchedHooks := hooks.MatchAll(id)
|
||||
|
||||
if matchedHooks != nil {
|
||||
log.Printf("%s got matched (%d time(s))\n", id, len(matchedHooks))
|
||||
if matchedHook := hooks.Match(id); matchedHook != nil {
|
||||
log.Printf("%s got matched\n", id)
|
||||
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
@@ -179,48 +197,64 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// handle hook
|
||||
for _, h := range matchedHooks {
|
||||
err := h.ParseJSONParameters(&headers, &query, &payload)
|
||||
if errors := matchedHook.ParseJSONParameters(&headers, &query, &payload); errors != nil {
|
||||
for _, err := range errors {
|
||||
log.Printf("error parsing JSON parameters: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
var ok bool
|
||||
|
||||
if matchedHook.TriggerRule == nil {
|
||||
ok = true
|
||||
} else {
|
||||
ok, err = matchedHook.TriggerRule.Evaluate(&headers, &query, &payload, &body)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("error parsing JSON: %s", err)
|
||||
msg := fmt.Sprintf("error evaluating hook: %s", err)
|
||||
log.Printf(msg)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, msg)
|
||||
return
|
||||
}
|
||||
|
||||
var ok bool
|
||||
|
||||
if h.TriggerRule == nil {
|
||||
ok = true
|
||||
} else {
|
||||
ok, err = h.TriggerRule.Evaluate(&headers, &query, &payload, &body)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("error evaluating hook: %s", err)
|
||||
log.Printf(msg)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, msg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ok {
|
||||
log.Printf("%s hook triggered successfully\n", h.ID)
|
||||
|
||||
if h.CaptureCommandOutput {
|
||||
response := handleHook(h, &headers, &query, &payload, &body)
|
||||
fmt.Fprintf(w, response)
|
||||
} else {
|
||||
go handleHook(h, &headers, &query, &payload, &body)
|
||||
fmt.Fprintf(w, h.ResponseMessage)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Error occurred while evaluating hook rules.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ok {
|
||||
log.Printf("%s hook triggered successfully\n", matchedHook.ID)
|
||||
|
||||
for _, responseHeader := range matchedHook.ResponseHeaders {
|
||||
w.Header().Set(responseHeader.Name, responseHeader.Value)
|
||||
}
|
||||
|
||||
if matchedHook.CaptureCommandOutput {
|
||||
response, err := handleHook(matchedHook, &headers, &query, &payload, &body)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Check if a return code is configured for the hook
|
||||
if matchedHook.TriggerRuleMismatchHttpResponseCode != 0 {
|
||||
// Check if the configured return code is supported by the http package
|
||||
// by testing if there is a StatusText for this code.
|
||||
if len(http.StatusText(matchedHook.TriggerRuleMismatchHttpResponseCode)) > 0 {
|
||||
w.WriteHeader(matchedHook.TriggerRuleMismatchHttpResponseCode)
|
||||
} else {
|
||||
log.Printf("%s got matched, but the configured return code %d is unknown - defaulting to 200\n", matchedHook.ID, matchedHook.TriggerRuleMismatchHttpResponseCode)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.")
|
||||
} else {
|
||||
@@ -229,48 +263,43 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) string {
|
||||
var err error
|
||||
func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) (string, error) {
|
||||
var errors []error
|
||||
|
||||
cmd := exec.Command(h.ExecuteCommand)
|
||||
cmd.Dir = h.CommandWorkingDirectory
|
||||
|
||||
cmd.Args, err = h.ExtractCommandArguments(headers, query, payload)
|
||||
if err != nil {
|
||||
log.Printf("error extracting command arguments: %s", err)
|
||||
cmd.Args, errors = h.ExtractCommandArguments(headers, query, payload)
|
||||
if errors != nil {
|
||||
for _, err := range errors {
|
||||
log.Printf("error extracting command arguments: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
var envs []string
|
||||
envs, err = h.ExtractCommandArgumentsForEnv(headers, query, payload)
|
||||
if err != nil {
|
||||
log.Printf("error extracting command arguments for environment: %s", err)
|
||||
envs, errors = h.ExtractCommandArgumentsForEnv(headers, query, payload)
|
||||
|
||||
if errors != nil {
|
||||
for _, err := range errors {
|
||||
log.Printf("error extracting command arguments for environment: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
out, err := cmd.Output()
|
||||
|
||||
log.Printf("command output: %s\n", out)
|
||||
|
||||
var errorResponse string
|
||||
|
||||
if err != nil {
|
||||
log.Printf("error occurred: %+v\n", err)
|
||||
errorResponse = fmt.Sprintf("%+v", err)
|
||||
}
|
||||
|
||||
log.Printf("finished handling %s\n", h.ID)
|
||||
|
||||
var response []byte
|
||||
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)
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
func reloadHooks() {
|
||||
@@ -284,10 +313,18 @@ func reloadHooks() {
|
||||
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))
|
||||
seenHooksIds := make(map[string]bool)
|
||||
|
||||
for _, hook := range hooks {
|
||||
log.Printf("\t> %s\n", hook.ID)
|
||||
log.Printf("found %d hook(s) in file\n", len(newHooks))
|
||||
|
||||
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
|
||||
|
||||
+17
-6
@@ -372,7 +372,9 @@ var hookHandlerTests = []struct {
|
||||
}`,
|
||||
false,
|
||||
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.
|
||||
@@ -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"}`,
|
||||
true,
|
||||
http.StatusOK,
|
||||
`{"message":"success"}`,
|
||||
`success`,
|
||||
},
|
||||
{
|
||||
"gitlab",
|
||||
@@ -431,7 +433,8 @@ var hookHandlerTests = []struct {
|
||||
}`,
|
||||
false,
|
||||
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,
|
||||
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,8 +511,14 @@ var hookHandlerTests = []struct {
|
||||
}`,
|
||||
false,
|
||||
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.`},
|
||||
// test with custom return code
|
||||
{"empty payload", "github", nil, `{}`, false, http.StatusBadRequest, `Hook rules were not satisfied.`},
|
||||
// test with custom invalid http code, should default to 200 OK
|
||||
{"empty payload", "bitbucket", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`},
|
||||
// test with no configured http return code, should default to 200 OK
|
||||
{"empty payload", "gitlab", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user