Compare commits

...

63 Commits

Author SHA1 Message Date
Adnan Hajdarević 3978b6687d Merge pull request #125 from adnanh/development
webhook 2.6.3
2017-04-07 11:00:37 +02:00
Adnan Hajdarevic 81b1bd7c7b bump up the version to 2.6.3 2017-04-07 10:59:40 +02:00
Adnan Hajdarevic 45cf312b8d Merge branch 'master' into development 2017-04-07 10:59:15 +02:00
Adnan Hajdarević 2cfc1ce2ff Merge pull request #124 from raphink/sha256
Add sha256 payload check
2017-04-07 10:55:16 +02:00
Raphaël Pinson 6b1021925b Add sha256 payload check 2017-04-06 17:49:31 +02:00
Adnan Hajdarević af22498d1e Update README.md 2017-04-06 17:01:08 +02:00
Adnan Hajdarević 5b567d1631 Update README.md 2017-04-06 11:09:17 +02:00
Adnan Hajdarević 4aa81b727f Merge pull request #116 from handcraftedbits/development
Add IP whitelist match rule
2017-02-21 12:50:48 +01:00
HandcraftedBits 173273b466 Add IP whitelist match rule. 2017-02-20 22:00:39 -05:00
Adnan Hajdarević 8803239e6d Merge pull request #115 from adnanh/development
Support loading hooks from multiple files
2017-02-11 23:01:01 +01:00
Adnan Hajdarevic c8a83349d2 remove hooks after the underlying file is removed 2017-02-11 22:58:49 +01:00
Adnan Hajdarevic 1da40d4634 Merge branch 'master' into development 2017-02-11 12:15:04 +01:00
Adnan Hajdarević c51971fd37 Bump version 2017-02-10 19:19:53 +01:00
Adnan Hajdarević 93e5fe7712 Merge pull request #113 from DG-i/master
make http return code for mismatch rules configurable
2017-02-10 19:19:33 +01:00
Mathias Merscher 058f820cbd rename trigger rule http response code config option
Signed-off-by: Mathias Merscher <Mathias.Merscher@dg-i.net>
2017-02-10 18:42:02 +01:00
Mathias Merscher ddb1f2441a make http return code for mismatched rules configurable
Signed-off-by: Mathias Merscher <Mathias.Merscher@dg-i.net>
2017-02-10 12:32:11 +01:00
Adnan Hajdarević 8226d5e50f Update README.md 2017-02-09 05:22:59 +01:00
Adnan Hajdarević e149c99724 Merge pull request #111 from moorereason/fix106tests
Fix failing header tests
2016-12-22 20:44:57 +01:00
Cameron Moore 6ea2d68b44 Fix failing header tests
Now that we use textproto.CanonicalMIMEHeaderKey, all header field test
data needs to be title-cased.
2016-12-22 10:23:55 -06:00
Adnan Hajdarevic 79453b7a22 Use hooks.json as default file if no hooks file have been specified 2016-12-05 21:28:38 +01:00
Adnan Hajdarevic 8207c6cf12 Started work on multi file hooks loading 2016-12-05 21:22:34 +01:00
Adnan Hajdarević 5789362186 Merge pull request #108 from adnanh/development
Update README.md
2016-11-22 15:23:23 +01:00
Adnan Hajdarević 07f166616c Update README.md 2016-11-22 15:23:09 +01:00
Adnan Hajdarević b66216675a Merge pull request #107 from adnanh/development
2.6.0
2016-11-01 20:11:07 +01:00
Adnan Hajdarevic ecbcf11153 fix #106 2016-11-01 20:09:41 +01:00
Adnan Hajdarević 7d525cf317 Merge pull request #105 from adnanh/development
Fixes #103
2016-10-28 16:07:07 +02:00
Adnan Hajdarević e83d7029ff Merge pull request #104 from adnanh/master
Backmerge
2016-10-28 16:06:33 +02:00
Adnan Hajdarević 36c2c692d6 Add instructions for debian 2016-10-28 16:05:39 +02:00
Adnan Hajdarević 86cef3e421 Merge pull request #101 from adnanh/development
webhook 2.5.0
2016-09-29 20:11:46 +02:00
Adnan Hajdarevic 75cf8952be remove \n 2016-09-29 20:11:20 +02:00
Adnan Hajdarevic c53596df59 override content type header when returning error message 2016-09-29 20:08:47 +02:00
Adnan Hajdarevic cc0d9b2cba fix tests, return raw output, return 500 if the command did not execute properly - fixes #87
return raw stdout instead of json wrapped message - fixes #88
2016-09-29 19:57:06 +02:00
Adnan Hajdarević c6530b17e7 Merge pull request #100 from adnanh/env-names
Do not prefix EnvName with HOOK_
2016-09-29 19:20:06 +02:00
Adnan Hajdarevic 1943c5311f bump version to 2.5.0 2016-09-29 19:16:47 +02:00
Adnan Hajdarevic 3b59539a33 do not prefix specified environment variable name with HOOK_ (fixes #98) 2016-09-29 19:15:51 +02:00
Adnan Hajdarević 923b0c6daa Merge pull request #95 from denji/Makefile
Makefile cross-platform binary
2016-09-13 09:20:11 +02:00
Denis Denisov 8530255ae6 Makefile build cross-binary 2016-09-12 23:09:05 +03:00
Adnan Hajdarević 032c74451f Merge pull request #93 from adnanh/development
Development
2016-09-02 18:30:31 +02:00
Adnan Hajdarević 1c50853d8d Merge pull request #92 from moorereason/iss91
Update negroni Logger usage
2016-09-02 18:29:27 +02:00
Cameron Moore b5ed4cbea7 Update negroni Logger usage
negroni made a breaking change to the Logger struct.

Fixes #91
2016-09-02 08:57:46 -05:00
Adnan Hajdarević b6d176705e Merge pull request #90 from adnanh/development
Development
2016-08-25 23:45:27 +02:00
Adnan Hajdarević 421fc2cbcd Hotfix backmerge (#89)
* fixes #76, fixes #78, fixes #82, fixes #83 (#84)

* Never disclose expected payload signature (#86)

Fixes #85
2016-08-25 23:42:33 +02:00
Cameron Moore 10d65dd2fd Never disclose expected payload signature (#86)
Fixes #85
2016-08-25 23:41:05 +02:00
Adnan Hajdarević 54a9dbe1d6 fixes #76, fixes #78, fixes #82, fixes #83 (#84) 2016-06-27 22:15:37 +02:00
Adnan Hajdarevic 30baec91df fixes #76, fixes #78, fixes #82, fixes #83 2016-06-27 22:13:00 +02:00
Adnan Hajdarevic 3bcf6d5e2b bump version to 2.3.9 2016-06-18 15:33:37 +02:00
Adnan Hajdarevic 67343e281d Merge branch 'master' into development 2016-06-18 15:31:13 +02:00
Florent Aide 18b0573bc4 Add support for naming env variables (#75)
* Adding ignore patterns

* Adding support for env var naming

* Fixed typo in docstring

* Adding tests for the env var extraction w & w/o explicit naming

* remove coverage script from ignore patterns

* Adding the coverage script to help see which code is tested and which is not

* remove coverage script from sources

* Ignore coverage script from sources tree
2016-05-26 23:33:56 +02:00
Adnan Hajdarević ec42679305 Merge pull request #69 from adnanh/development
version 2.3.8
2016-03-24 16:55:34 +01:00
Adnan Hajdarevic e85e0592dd Merge branch 'master' into development 2016-03-24 16:53:37 +01:00
Adnan Hajdarević 4d20af8027 Update webhook.go 2016-03-24 16:43:42 +01:00
Adnan Hajdarević d4e772c815 Add almir's docker image to the readme. 2016-03-15 19:09:06 +01:00
Adnan Hajdarević 04a2b2a680 Update README.md 2016-03-09 14:12:37 +01:00
Adnan Hajdarevic 4914a4131f Merge branch 'master' into development 2016-02-27 22:16:28 +01:00
Adnan Hajdarevic 37698e63b6 add support for setting global response headers using -header flag
add support for setting response headers for a successfuly triggered hook
2016-02-27 22:13:09 +01:00
Adnan Hajdarević 80aa9800bf Merge pull request #64 from adnanh/development
Update README.md to include Dockerfile discussion
2016-02-04 15:13:07 +01:00
Adnan Hajdarević f620cb056b Update README.md 2016-02-04 15:12:24 +01:00
Adnan Hajdarević e55e7efe14 Merge pull request #62 from adnanh/development
Update README.md
2015-12-29 20:39:41 +01:00
Adnan Hajdarević 9fa02f7341 Update README.md 2015-12-29 20:38:36 +01:00
Adnan Hajdarevic f59f0a5c84 - added omitempty to json fields 2015-12-27 20:05:52 +01:00
Adnan Hajdarević 5594a62f8f Merge pull request #61 from adnanh/development
- added omitempty to json fields
2015-12-27 20:04:03 +01:00
Adnan Hajdarevic 642516d46e Merge branch 'master' into development 2015-11-27 09:57:44 +01:00
Adnan Hajdarevic 9cef8ed882 add omitempty to json fields 2015-11-21 17:06:02 +01:00
10 changed files with 714 additions and 255 deletions
+4
View File
@@ -0,0 +1,4 @@
.idea
.cover
coverage
webhook
+29
View File
@@ -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
+21 -1
View File
@@ -1,4 +1,11 @@
[![Join the chat at https://gitter.im/adnanh/webhook](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/adnanh/webhook?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Flattr this](https://button.flattr.com/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=adnanh&url=https%3A%2F%2Fwww.github.com%2Fadnanh%2Fwebhook)
[![ghit.me](https://ghit.me/badge.svg?repo=adnanh/webhook)](https://ghit.me/repo/adnanh/webhook) [![Join the chat at https://gitter.im/adnanh/webhook](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/adnanh/webhook?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Flattr this](https://button.flattr.com/flattr-badge-large.png)](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)
# Hookdoo
<a href="https://www.hookdoo.com/?github"><img src="https://my.hookdoo.com/logo/logo-dark-96.png" alt="hookdoo" align="left" /></a>
If you don't have time to waste configuring, hosting, debugging and maintaining your webhook instance, we offer a __SaaS__ solution that has all of the capabilities webhook provides, plus a lot more, and all that packaged in a nice friendly web interface. If you are interested, find out more at [hookdoo website](https://www.hookdoo.com/). If you have any questions, you can contact us at info@hookdoo.com
# 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 +26,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 +73,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.
+225 -74
View File
@@ -3,10 +3,14 @@ package hook
import (
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/textproto"
"reflect"
"regexp"
"strconv"
@@ -17,6 +21,7 @@ import (
const (
SourceHeader string = "header"
SourceQuery string = "url"
SourceQueryAlias string = "query"
SourcePayload string = "payload"
SourceString string = "string"
SourceEntirePayload string = "entire-payload"
@@ -92,11 +97,72 @@ 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
}
// CheckPayloadSignature256 calculates and verifies SHA256 signature of the given payload
func CheckPayloadSignature256(payload []byte, secret string, signature string) (string, error) {
if strings.HasPrefix(signature, "sha256=") {
signature = signature[7:]
}
mac := hmac.New(sha256.New, []byte(secret))
_, err := mac.Write(payload)
if err != nil {
return "", err
}
expectedMAC := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
return expectedMAC, &SignatureError{signature}
}
return expectedMAC, err
}
// CheckIPWhitelist makes sure the provided remote address (of the form IP:port) falls within the provided IP range
// (in CIDR form or a single IP address).
func CheckIPWhitelist(remoteAddr string, ipRange string) (bool, error) {
// Extract IP address from remote address.
ip := remoteAddr
if strings.LastIndex(remoteAddr, ":") != -1 {
ip = remoteAddr[0:strings.LastIndex(remoteAddr, ":")]
}
ip = strings.TrimSpace(ip)
// IPv6 addresses will likely be surrounded by [], so don't forget to remove those.
if strings.HasPrefix(ip, "[") && strings.HasSuffix(ip, "]") {
ip = ip[1 : len(ip)-1]
}
parsedIP := net.ParseIP(strings.TrimSpace(ip))
if parsedIP == nil {
return false, fmt.Errorf("invalid IP address found in remote address '%s'", remoteAddr)
}
// Extract IP range in CIDR form. If a single IP address is provided, turn it into CIDR form.
ipRange = strings.TrimSpace(ipRange)
if strings.Index(ipRange, "/") == -1 {
ipRange = ipRange + "/32"
}
_, cidr, err := net.ParseCIDR(ipRange)
if err != nil {
return false, err
}
return cidr.Contains(parsedIP), nil
}
// ReplaceParameter replaces parameter value with the passed value in the passed map
// (please note you should pass pointer to the map, because we're modifying it)
// based on the passed string
@@ -195,19 +261,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"`
Name string `json:"name"`
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 +309,85 @@ 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 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
}
// HooksFiles is a slice of String
type HooksFiles []string
func (h *HooksFiles) String() string {
if len(*h) == 0 {
return "hooks.json"
}
return strings.Join(*h, ", ")
}
// Set method appends new string
func (h *HooksFiles) Set(value string) error {
*h = append(*h, value)
return nil
}
// 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"`
ResponseMessage string `json:"response-message"`
CaptureCommandOutput bool `json:"include-command-output-in-response"`
PassEnvironmentToCommand []Argument `json:"pass-environment-to-command"`
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command"`
JSONStringParameters []Argument `json:"parse-parameters-as-json"`
TriggerRule *Rules `json:"trigger-rule"`
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 +398,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 +409,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 +449,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
}
@@ -355,6 +507,19 @@ func (h *Hooks) LoadFromFile(path string) error {
return e
}
// Append appends hooks unless the new hooks contain a hook with an ID that already exists
func (h *Hooks) Append(other *Hooks) error {
for _, hook := range *other {
if h.Match(hook.ID) != nil {
return fmt.Errorf("hook with ID %s is already defined", hook.ID)
}
*h = append(*h, hook)
}
return nil
}
// 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 {
@@ -367,43 +532,26 @@ 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"`
Or *OrRule `json:"or"`
Not *NotRule `json:"not"`
Match *MatchRule `json:"match"`
And *AndRule `json:"and,omitempty"`
Or *OrRule `json:"or,omitempty"`
Not *NotRule `json:"not,omitempty"`
Match *MatchRule `json:"match,omitempty"`
}
// 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, error) {
func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
switch {
case r.And != nil:
return r.And.Evaluate(headers, query, payload, body)
return r.And.Evaluate(headers, query, payload, body, remoteAddr)
case r.Or != nil:
return r.Or.Evaluate(headers, query, payload, body)
return r.Or.Evaluate(headers, query, payload, body, remoteAddr)
case r.Not != nil:
return r.Not.Evaluate(headers, query, payload, body)
return r.Not.Evaluate(headers, query, payload, body, remoteAddr)
case r.Match != nil:
return r.Match.Evaluate(headers, query, payload, body)
return r.Match.Evaluate(headers, query, payload, body, remoteAddr)
}
return false, nil
@@ -413,11 +561,11 @@ func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, body *[
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, error) {
func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
res := true
for _, v := range r {
rv, err := v.Evaluate(headers, query, payload, body)
rv, err := v.Evaluate(headers, query, payload, body, remoteAddr)
if err != nil {
return false, err
}
@@ -435,11 +583,11 @@ func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, body
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, error) {
func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
res := false
for _, v := range r {
rv, err := v.Evaluate(headers, query, payload, body)
rv, err := v.Evaluate(headers, query, payload, body, remoteAddr)
if err != nil {
return false, err
}
@@ -457,29 +605,36 @@ func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, body *
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, error) {
rv, err := Rules(r).Evaluate(headers, query, payload, body)
func (r NotRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
rv, err := Rules(r).Evaluate(headers, query, payload, body, remoteAddr)
return !rv, err
}
// 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"`
Type string `json:"type,omitempty"`
Regex string `json:"regex,omitempty"`
Secret string `json:"secret,omitempty"`
Value string `json:"value,omitempty"`
Parameter Argument `json:"parameter,omitempty"`
IPRange string `json:"ip-range,omitempty"`
}
// Constants for the MatchRule type
const (
MatchValue string = "value"
MatchRegex string = "regex"
MatchHashSHA1 string = "payload-hash-sha1"
MatchValue string = "value"
MatchRegex string = "regex"
MatchHashSHA1 string = "payload-hash-sha1"
MatchHashSHA256 string = "payload-hash-sha256"
IPWhitelist string = "ip-whitelist"
)
// Evaluate MatchRule will return based on the type
func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) (bool, error) {
func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
if r.Type == IPWhitelist {
return CheckIPWhitelist(remoteAddr, r.IPRange)
}
if arg, ok := r.Parameter.Get(headers, query, payload); ok {
switch r.Type {
case MatchValue:
@@ -489,14 +644,10 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod
case MatchHashSHA1:
_, err := CheckPayloadSignature(*body, r.Secret, arg)
return err == nil, err
case MatchHashSHA256:
_, err := CheckPayloadSignature256(*body, r.Secret, arg)
return err == nil, err
}
}
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"`
}
+169 -61
View File
@@ -2,6 +2,7 @@ package hook
import (
"reflect"
"strings"
"testing"
)
@@ -25,6 +26,36 @@ 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)
}
}
}
var checkPayloadSignature256Tests = []struct {
payload []byte
secret string
signature string
mac string
ok bool
}{
{[]byte(`{"a": "z"}`), "secret", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
{[]byte(`{"a": "z"}`), "secret", "sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
// failures
{[]byte(`{"a": "z"}`), "secret", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
}
func TestCheckPayloadSignature256(t *testing.T) {
for _, tt := range checkPayloadSignature256Tests {
mac, err := CheckPayloadSignature256(tt.payload, tt.secret, tt.signature)
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 +98,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 +125,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 +152,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 +167,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
@@ -175,29 +267,45 @@ func TestHooksMatch(t *testing.T) {
}
var matchRuleTests = []struct {
typ, regex, secret, value string
param Argument
headers, query, payload *map[string]interface{}
body []byte
ok bool
err bool
typ, regex, secret, value, ipRange string
param Argument
headers, query, payload *map[string]interface{}
body []byte
remoteAddr string
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},
{"payload-hash-sha256", "", "secret", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, 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
{"payload-hash-sha256", "", "secret", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac
// IP whitelisting, valid cases
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
{"ip-whitelist", "", "", "", "192.168.0.1", Argument{}, nil, nil, nil, []byte{}, "192.168.0.1:9000", true, false}, // valid IPv4, no range
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, with range
{"ip-whitelist", "", "", "", "::1", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, no range
// IP whitelisting, invalid cases
{"ip-whitelist", "", "", "", "192.168.0.1/a", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, with range
{"ip-whitelist", "", "", "", "192.168.0.a", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, no range
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.a:9000", false, true}, // invalid IPv4 address
{"ip-whitelist", "", "", "", "::1/a", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, with range
{"ip-whitelist", "", "", "", "::z", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, no range
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, []byte{}, "[::z]:9000", false, true}, // invalid IPv6 address
}
func TestMatchRule(t *testing.T) {
for i, tt := range matchRuleTests {
r := MatchRule{tt.typ, tt.regex, tt.secret, tt.value, tt.param}
ok, err := r.Evaluate(tt.headers, tt.query, tt.payload, &tt.body)
r := MatchRule{tt.typ, tt.regex, tt.secret, tt.value, tt.param, tt.ipRange}
ok, err := r.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, tt.remoteAddr)
if ok != tt.ok || (err != nil) != tt.err {
t.Errorf("%d failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", i, r, tt.ok, tt.err, ok, (err != nil))
}
@@ -215,60 +323,60 @@ 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,
},
}
func TestAndRule(t *testing.T) {
for _, tt := range andRuleTests {
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body)
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, "")
if ok != tt.ok || (err != nil) != tt.err {
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.desc, tt.ok, tt.err, ok, err)
}
@@ -286,44 +394,44 @@ 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,
},
}
func TestOrRule(t *testing.T) {
for _, tt := range orRuleTests {
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body)
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, "")
if ok != tt.ok || (err != nil) != tt.err {
t.Errorf("%#v:\nexpected ok: %#v, err: %v\ngot ok: %#v err: %v", tt.desc, tt.ok, tt.err, ok, err)
}
@@ -338,13 +446,13 @@ 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) {
for _, tt := range notRuleTests {
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body)
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, "")
if ok != tt.ok || (err != nil) != tt.err {
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.rule, tt.ok, tt.err, ok, err)
}
+7
View File
@@ -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":
[
{
+1 -1
View File
@@ -26,7 +26,7 @@ func watchForSignals() {
if sig == syscall.SIGUSR1 {
log.Println("caught USR1 signal")
reloadHooks()
reloadAllHooks()
} else {
log.Printf("caught unhandled signal %+v\n", sig)
}
+3 -2
View File
@@ -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 @@
}
}
]
+238 -110
View File
@@ -21,32 +21,64 @@ import (
)
const (
version = "2.3.6"
version = "2.6.3"
)
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")
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
hooksFiles hook.HooksFiles
loadedHooksFromFiles = make(map[string]hook.Hooks)
watcher *fsnotify.Watcher
signals chan os.Signal
hooks hook.Hooks
)
func matchLoadedHook(id string) *hook.Hook {
for _, hooks := range loadedHooksFromFiles {
if hook := hooks.Match(id); hook != nil {
return hook
}
}
return nil
}
func lenLoadedHooks() int {
sum := 0
for _, hooks := range loadedHooksFromFiles {
sum += len(hooks)
}
return sum
}
func main() {
hooks = hook.Hooks{}
flag.Var(&hooksFiles, "hooks", "path to the json file containing defined hooks the webhook should serve, use multiple times to load from different files")
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)
}
if len(hooksFiles) == 0 {
hooksFiles = append(hooksFiles, "hooks.json")
}
log.SetPrefix("[webhook] ")
log.SetFlags(log.Ldate | log.Ltime)
@@ -60,51 +92,70 @@ func main() {
setupSignals()
// load and parse hooks
log.Printf("attempting to load hooks from %s\n", *hooksFilePath)
for _, hooksFilePath := range hooksFiles {
log.Printf("attempting to load hooks from %s\n", hooksFilePath)
err := hooks.LoadFromFile(*hooksFilePath)
newHooks := hook.Hooks{}
if err != nil {
if !*verbose && !*noPanic {
log.SetOutput(os.Stdout)
log.Fatalf("couldn't load any hooks from file! %+v\naborting webhook execution since the -verbose flag is set to false.\nIf, for some reason, you want webhook to start without the hooks, either use -verbose flag, or -nopanic", err)
}
err := newHooks.LoadFromFile(hooksFilePath)
log.Printf("couldn't load hooks from file! %+v\n", err)
} else {
log.Printf("loaded %d hook(s) from file\n", len(hooks))
if err != nil {
log.Printf("couldn't load hooks from file! %+v\n", err)
} else {
log.Printf("found %d hook(s) in file\n", len(newHooks))
for _, hook := range hooks {
log.Printf("\t> %s\n", hook.ID)
for _, hook := range newHooks {
if matchLoadedHook(hook.ID) != nil {
log.Fatalf("error: hook with the id %s has already been loaded!\nplease check your hooks file for duplicate hooks ids!\n", hook.ID)
}
log.Printf("\tloaded: %s\n", hook.ID)
}
loadedHooksFromFiles[hooksFilePath] = newHooks
}
}
if *hotReload {
// set up file watcher
log.Printf("setting up file watcher for %s\n", *hooksFilePath)
newHooksFiles := hooksFiles[:0]
for _, filePath := range hooksFiles {
if _, ok := loadedHooksFromFiles[filePath]; ok == true {
newHooksFiles = append(newHooksFiles, filePath)
}
}
hooksFiles = newHooksFiles
if !*verbose && !*noPanic && lenLoadedHooks() == 0 {
log.SetOutput(os.Stdout)
log.Fatalln("couldn't load any hooks from file!\naborting webhook execution since the -verbose flag is set to false.\nIf, for some reason, you want webhook to start without the hooks, either use -verbose flag, or -nopanic")
}
if *hotReload {
var err error
watcher, err = fsnotify.NewWatcher()
if err != nil {
log.Fatal("error creating file watcher instance", err)
log.Fatal("error creating file watcher instance\n", err)
}
defer watcher.Close()
go watchForFileChange()
for _, hooksFilePath := range hooksFiles {
// set up file watcher
log.Printf("setting up file watcher for %s\n", hooksFilePath)
err = watcher.Add(*hooksFilePath)
if err != nil {
log.Fatal("error adding hooks file to the watcher", err)
err = watcher.Add(hooksFilePath)
if err != nil {
log.Fatal("error adding hooks file to the watcher\n", err)
}
}
go watchForFileChange()
}
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 +178,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 := matchLoadedHook(id); matchedHook != nil {
log.Printf("%s got matched\n", id)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
@@ -179,48 +232,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, r.RemoteAddr)
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,68 +298,113 @@ 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() {
newHooks := hook.Hooks{}
func reloadHooks(hooksFilePath string) {
hooksInFile := hook.Hooks{}
// parse and swap
log.Printf("attempting to reload hooks from %s\n", *hooksFilePath)
log.Printf("attempting to reload hooks from %s\n", hooksFilePath)
err := newHooks.LoadFromFile(*hooksFilePath)
err := hooksInFile.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))
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(hooksInFile))
for _, hook := range hooksInFile {
wasHookIDAlreadyLoaded := false
for _, loadedHook := range loadedHooksFromFiles[hooksFilePath] {
if loadedHook.ID == hook.ID {
wasHookIDAlreadyLoaded = true
break
}
}
if (matchLoadedHook(hook.ID) != nil && !wasHookIDAlreadyLoaded) || 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
loadedHooksFromFiles[hooksFilePath] = hooksInFile
}
}
func reloadAllHooks() {
for _, hooksFilePath := range hooksFiles {
reloadHooks(hooksFilePath)
}
}
func removeHooks(hooksFilePath string) {
for _, hook := range loadedHooksFromFiles[hooksFilePath] {
log.Printf("\tremoving: %s\n", hook.ID)
}
newHooksFiles := hooksFiles[:0]
for _, filePath := range hooksFiles {
if filePath != hooksFilePath {
newHooksFiles = append(newHooksFiles, filePath)
}
}
hooksFiles = newHooksFiles
removedHooksCount := len(loadedHooksFromFiles[hooksFilePath])
delete(loadedHooksFromFiles, hooksFilePath)
log.Printf("removed %d hook(s) that were loaded from file %s\n", removedHooksCount, hooksFilePath)
if !*verbose && !*noPanic && lenLoadedHooks() == 0 {
log.SetOutput(os.Stdout)
log.Fatalln("couldn't load any hooks from file!\naborting webhook execution since the -verbose flag is set to false.\nIf, for some reason, you want webhook to run without the hooks, either use -verbose flag, or -nopanic")
}
}
@@ -299,9 +413,23 @@ func watchForFileChange() {
select {
case event := <-(*watcher).Events:
if event.Op&fsnotify.Write == fsnotify.Write {
log.Println("hooks file modified")
reloadHooks()
log.Printf("hooks file %s modified\n", event.Name)
reloadHooks(event.Name)
} else if event.Op&fsnotify.Remove == fsnotify.Remove {
log.Printf("hooks file %s removed, no longer watching this file for changes, removing hooks that were loaded from it\n", event.Name)
(*watcher).Remove(event.Name)
removeHooks(event.Name)
} else if event.Op&fsnotify.Rename == fsnotify.Rename {
if _, err := os.Stat(event.Name); os.IsNotExist(err) {
// file was removed
log.Printf("hooks file %s removed, no longer watching this file for changes, and removing hooks that were loaded from it\n", event.Name)
(*watcher).Remove(event.Name)
removeHooks(event.Name)
} else {
// file was overwritten
log.Printf("hooks file %s overwritten\n", event.Name)
reloadHooks(event.Name)
}
}
case err := <-(*watcher).Errors:
log.Println("watcher error:", err)
+17 -6
View File
@@ -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.`},
}