mirror of
https://github.com/adnanh/webhook.git
synced 2026-07-28 01:59:17 +08:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 147c95dd8b | |||
| cfd138c96d | |||
| c19e514ee9 | |||
| c7ec25f378 | |||
| 6916f89a2b | |||
| ed251c3cc7 | |||
| 9db13ef649 | |||
| 98512463d8 | |||
| 53ec2e7acc | |||
| c581e0532b | |||
| 3a75bbf4db | |||
| 2f1ea098c0 | |||
| ccd28908db | |||
| 69a4aec1f3 | |||
| a38875213a | |||
| 249962f002 | |||
| ced9e01895 | |||
| 5561fa3308 | |||
| 47b82c7c4a | |||
| 3978b6687d | |||
| 81b1bd7c7b | |||
| 45cf312b8d | |||
| 2cfc1ce2ff | |||
| 6b1021925b | |||
| af22498d1e | |||
| 5b567d1631 | |||
| 4aa81b727f | |||
| 173273b466 | |||
| 8803239e6d | |||
| c8a83349d2 | |||
| 1da40d4634 | |||
| c51971fd37 | |||
| 93e5fe7712 | |||
| 058f820cbd | |||
| ddb1f2441a | |||
| 8226d5e50f | |||
| e149c99724 | |||
| 6ea2d68b44 | |||
| 79453b7a22 | |||
| 8207c6cf12 | |||
| 5789362186 | |||
| 07f166616c |
@@ -1,11 +1,18 @@
|
|||||||
[](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)
|
[](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)
|
||||||
|
|
||||||
|
|
||||||
|
# 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/?ref=github-webhook-readme). If you have any questions, you can contact us at info@hookdoo.com
|
||||||
|
|
||||||
|
|
||||||
# What is 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.
|
[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.
|
||||||
|
|
||||||
For example, if you're using Github or Bitbucket, you can use [webhook](https://github.com/adnanh/webhook/) to set up a hook that runs a redeploy script for your project on your staging server, whenever you push changes to the master branch of your project.
|
For example, if you're using Github or Bitbucket, you can use [webhook](https://github.com/adnanh/webhook/) to set up a hook that runs a redeploy script for your project on your staging server, whenever you push changes to the master branch of your project.
|
||||||
|
|
||||||
If you use Slack, you can set up an "Outgoing webhook integration" to run various commands on your server, which can then report back directly to your Slack channels using the "Incoming webhook integrations".
|
If you use Mattermost or Slack, you can set up an "Outgoing webhook integration" or "Slash command" to run various commands on your server, which can then report back directly to you or your channels using the "Incoming webhook integrations", or the appropriate response body.
|
||||||
|
|
||||||
[webhook](https://github.com/adnanh/webhook/) aims to do nothing more than it should do, and that is:
|
[webhook](https://github.com/adnanh/webhook/) aims to do nothing more than it should do, and that is:
|
||||||
1. receive the request,
|
1. receive the request,
|
||||||
@@ -28,8 +35,11 @@ $ go get github.com/adnanh/webhook
|
|||||||
to get the latest version of the [webhook](https://github.com/adnanh/webhook/).
|
to get the latest version of the [webhook](https://github.com/adnanh/webhook/).
|
||||||
|
|
||||||
### Using package manager
|
### Using package manager
|
||||||
#### Debian "sid"
|
#### Debian
|
||||||
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
|
If you are using Debian linux ("stretch" or later), 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
|
||||||
|
|
||||||
|
### Download prebuilt binaries
|
||||||
|
Prebuilt binaries for different architectures are available at [GitHub Releases](https://github.com/adnanh/webhook/releases).
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
Next step is to define some hooks you want [webhook](https://github.com/adnanh/webhook/) to serve. Begin by creating an empty file named `hooks.json`. This file will contain an array of hooks the [webhook](https://github.com/adnanh/webhook/) will serve. Check [Hook definition page](https://github.com/adnanh/webhook/wiki/Hook-Definition) to see the detailed description of what properties a hook can contain, and how to use them.
|
Next step is to define some hooks you want [webhook](https://github.com/adnanh/webhook/) to serve. Begin by creating an empty file named `hooks.json`. This file will contain an array of hooks the [webhook](https://github.com/adnanh/webhook/) will serve. Check [Hook definition page](https://github.com/adnanh/webhook/wiki/Hook-Definition) to see the detailed description of what properties a hook can contain, and how to use them.
|
||||||
@@ -83,6 +93,11 @@ Big thanks to [all the current contributors](https://github.com/adnanh/webhook/g
|
|||||||
# Community Contributions
|
# Community Contributions
|
||||||
See the [webhook-contrib][wc] repository for a collections of tools and helpers related to [webhook][w] that have been contributed by the [webhook][w] community.
|
See the [webhook-contrib][wc] repository for a collections of tools and helpers related to [webhook][w] that have been contributed by the [webhook][w] community.
|
||||||
|
|
||||||
|
# Sponsors
|
||||||
|
## <a href="https://www.browserstack.com/?ref=webhook"><img src="http://www.hajdarevic.net/browserstack.svg" alt="BrowserStack" width="250"/></a>
|
||||||
|
[BrowserStack](https://www.browserstack.com/?ref=webhook) is a cloud-based cross-browser testing tool that enables developers to test their websites across various browsers on different operating systems and mobile devices, without requiring users to install virtual machines, devices or emulators.
|
||||||
|
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|||||||
+120
-14
@@ -3,16 +3,20 @@ package hook
|
|||||||
import (
|
import (
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ghodss/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Constants used to specify the parameter source
|
// Constants used to specify the parameter source
|
||||||
@@ -100,6 +104,67 @@ func CheckPayloadSignature(payload []byte, secret string, signature string) (str
|
|||||||
return expectedMAC, err
|
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
|
// 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)
|
// (please note you should pass pointer to the map, because we're modifying it)
|
||||||
// based on the passed string
|
// based on the passed string
|
||||||
@@ -273,7 +338,7 @@ func (h *ResponseHeaders) String() string {
|
|||||||
result[idx] = fmt.Sprintf("%s=%s", responseHeader.Name, responseHeader.Value)
|
result[idx] = fmt.Sprintf("%s=%s", responseHeader.Name, responseHeader.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprint(strings.Join(result, ", "))
|
return strings.Join(result, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set method appends new Header object from header=value notation
|
// Set method appends new Header object from header=value notation
|
||||||
@@ -288,6 +353,23 @@ func (h *ResponseHeaders) Set(value string) error {
|
|||||||
return nil
|
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
|
// Hook type is a structure containing details for a single hook
|
||||||
type Hook struct {
|
type Hook struct {
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
@@ -300,6 +382,7 @@ type Hook struct {
|
|||||||
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command,omitempty"`
|
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command,omitempty"`
|
||||||
JSONStringParameters []Argument `json:"parse-parameters-as-json,omitempty"`
|
JSONStringParameters []Argument `json:"parse-parameters-as-json,omitempty"`
|
||||||
TriggerRule *Rules `json:"trigger-rule,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
|
// ParseJSONParameters decodes specified arguments to JSON objects and replaces the
|
||||||
@@ -422,10 +505,23 @@ func (h *Hooks) LoadFromFile(path string) error {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
e = json.Unmarshal(file, h)
|
e = yaml.Unmarshal(file, h)
|
||||||
return e
|
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,
|
// Match iterates through Hooks and returns first one that matches the given ID,
|
||||||
// if no hook matches the given ID, nil is returned
|
// if no hook matches the given ID, nil is returned
|
||||||
func (h *Hooks) Match(id string) *Hook {
|
func (h *Hooks) Match(id string) *Hook {
|
||||||
@@ -448,16 +544,16 @@ type Rules struct {
|
|||||||
|
|
||||||
// Evaluate finds the first rule property that is not nil and returns the value
|
// Evaluate finds the first rule property that is not nil and returns the value
|
||||||
// it evaluates to
|
// 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 {
|
switch {
|
||||||
case r.And != nil:
|
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:
|
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:
|
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:
|
case r.Match != nil:
|
||||||
return r.Match.Evaluate(headers, query, payload, body)
|
return r.Match.Evaluate(headers, query, payload, body, remoteAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
@@ -467,11 +563,11 @@ func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, body *[
|
|||||||
type AndRule []Rules
|
type AndRule []Rules
|
||||||
|
|
||||||
// Evaluate AndRule will return true if and only if all of ChildRules evaluate to true
|
// 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
|
res := true
|
||||||
|
|
||||||
for _, v := range r {
|
for _, v := range r {
|
||||||
rv, err := v.Evaluate(headers, query, payload, body)
|
rv, err := v.Evaluate(headers, query, payload, body, remoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -489,11 +585,11 @@ func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, body
|
|||||||
type OrRule []Rules
|
type OrRule []Rules
|
||||||
|
|
||||||
// Evaluate OrRule will return true if any of ChildRules evaluate to true
|
// 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
|
res := false
|
||||||
|
|
||||||
for _, v := range r {
|
for _, v := range r {
|
||||||
rv, err := v.Evaluate(headers, query, payload, body)
|
rv, err := v.Evaluate(headers, query, payload, body, remoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -511,8 +607,8 @@ func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, body *
|
|||||||
type NotRule Rules
|
type NotRule Rules
|
||||||
|
|
||||||
// Evaluate NotRule will return true if and only if ChildRule evaluates to false
|
// 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) {
|
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)
|
rv, err := Rules(r).Evaluate(headers, query, payload, body, remoteAddr)
|
||||||
return !rv, err
|
return !rv, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,6 +619,7 @@ type MatchRule struct {
|
|||||||
Secret string `json:"secret,omitempty"`
|
Secret string `json:"secret,omitempty"`
|
||||||
Value string `json:"value,omitempty"`
|
Value string `json:"value,omitempty"`
|
||||||
Parameter Argument `json:"parameter,omitempty"`
|
Parameter Argument `json:"parameter,omitempty"`
|
||||||
|
IPRange string `json:"ip-range,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constants for the MatchRule type
|
// Constants for the MatchRule type
|
||||||
@@ -530,10 +627,16 @@ const (
|
|||||||
MatchValue string = "value"
|
MatchValue string = "value"
|
||||||
MatchRegex string = "regex"
|
MatchRegex string = "regex"
|
||||||
MatchHashSHA1 string = "payload-hash-sha1"
|
MatchHashSHA1 string = "payload-hash-sha1"
|
||||||
|
MatchHashSHA256 string = "payload-hash-sha256"
|
||||||
|
IPWhitelist string = "ip-whitelist"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Evaluate MatchRule will return based on the type
|
// 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 {
|
if arg, ok := r.Parameter.Get(headers, query, payload); ok {
|
||||||
switch r.Type {
|
switch r.Type {
|
||||||
case MatchValue:
|
case MatchValue:
|
||||||
@@ -543,6 +646,9 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod
|
|||||||
case MatchHashSHA1:
|
case MatchHashSHA1:
|
||||||
_, err := CheckPayloadSignature(*body, r.Secret, arg)
|
_, err := CheckPayloadSignature(*body, r.Secret, arg)
|
||||||
return err == nil, err
|
return err == nil, err
|
||||||
|
case MatchHashSHA256:
|
||||||
|
_, err := CheckPayloadSignature256(*body, r.Secret, arg)
|
||||||
|
return err == nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|||||||
+98
-55
@@ -33,6 +33,32 @@ func TestCheckPayloadSignature(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var extractParameterTests = []struct {
|
var extractParameterTests = []struct {
|
||||||
s string
|
s string
|
||||||
params interface{}
|
params interface{}
|
||||||
@@ -72,15 +98,15 @@ var argumentGetTests = []struct {
|
|||||||
value string
|
value string
|
||||||
ok bool
|
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},
|
{"url", "a", nil, &map[string]interface{}{"a": "z"}, nil, "z", true},
|
||||||
{"payload", "a", nil, nil, &map[string]interface{}{"a": "z"}, "z", true},
|
{"payload", "a", nil, nil, &map[string]interface{}{"a": "z"}, "z", true},
|
||||||
{"string", "a", nil, nil, &map[string]interface{}{"a": "z"}, "a", true},
|
{"string", "a", nil, nil, &map[string]interface{}{"a": "z"}, "a", true},
|
||||||
// failures
|
// failures
|
||||||
{"header", "a", nil, &map[string]interface{}{"a": "z"}, &map[string]interface{}{"a": "z"}, "", false}, // nil headers
|
{"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
|
{"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
|
{"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
|
{"foo", "a", &map[string]interface{}{"A": "z"}, nil, nil, "", false}, // invalid source
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestArgumentGet(t *testing.T) {
|
func TestArgumentGet(t *testing.T) {
|
||||||
@@ -99,14 +125,14 @@ var hookParseJSONParametersTests = []struct {
|
|||||||
rheaders, rquery, rpayload *map[string]interface{}
|
rheaders, rquery, rpayload *map[string]interface{}
|
||||||
ok bool
|
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{"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{"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{"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", "z", ""}}, &map[string]interface{}{"Z": `{}`}, nil, nil, &map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true},
|
||||||
// failures
|
// failures
|
||||||
{[]Argument{Argument{"header", "z", ""}}, &map[string]interface{}{"z": ``}, nil, nil, &map[string]interface{}{"z": ``}, nil, nil, false}, // empty string
|
{[]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{"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{"string", "z", ""}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHookParseJSONParameters(t *testing.T) {
|
func TestHookParseJSONParameters(t *testing.T) {
|
||||||
@@ -126,9 +152,9 @@ var hookExtractCommandArgumentsTests = []struct {
|
|||||||
value []string
|
value []string
|
||||||
ok bool
|
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
|
// 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) {
|
func TestHookExtractCommandArguments(t *testing.T) {
|
||||||
@@ -171,14 +197,14 @@ var hookExtractCommandArgumentsForEnvTests = []struct {
|
|||||||
{
|
{
|
||||||
"test",
|
"test",
|
||||||
[]Argument{Argument{"header", "a", ""}},
|
[]Argument{Argument{"header", "a", ""}},
|
||||||
&map[string]interface{}{"a": "z"}, nil, nil,
|
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||||
[]string{"HOOK_a=z"},
|
[]string{"HOOK_a=z"},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"test",
|
"test",
|
||||||
[]Argument{Argument{"header", "a", "MYKEY"}},
|
[]Argument{Argument{"header", "a", "MYKEY"}},
|
||||||
&map[string]interface{}{"a": "z"}, nil, nil,
|
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||||
[]string{"MYKEY=z"},
|
[]string{"MYKEY=z"},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
@@ -186,7 +212,7 @@ var hookExtractCommandArgumentsForEnvTests = []struct {
|
|||||||
{
|
{
|
||||||
"fail",
|
"fail",
|
||||||
[]Argument{Argument{"payload", "a", ""}},
|
[]Argument{Argument{"payload", "a", ""}},
|
||||||
&map[string]interface{}{"a": "z"}, nil, nil,
|
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||||
[]string{},
|
[]string{},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
@@ -207,6 +233,7 @@ var hooksLoadFromFileTests = []struct {
|
|||||||
ok bool
|
ok bool
|
||||||
}{
|
}{
|
||||||
{"../hooks.json.example", true},
|
{"../hooks.json.example", true},
|
||||||
|
{"../hooks.yaml.example", true},
|
||||||
{"", true},
|
{"", true},
|
||||||
// failures
|
// failures
|
||||||
{"missing.json", false},
|
{"missing.json", false},
|
||||||
@@ -241,29 +268,45 @@ func TestHooksMatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var matchRuleTests = []struct {
|
var matchRuleTests = []struct {
|
||||||
typ, regex, secret, value string
|
typ, regex, secret, value, ipRange string
|
||||||
param Argument
|
param Argument
|
||||||
headers, query, payload *map[string]interface{}
|
headers, query, payload *map[string]interface{}
|
||||||
body []byte
|
body []byte
|
||||||
|
remoteAddr string
|
||||||
ok bool
|
ok bool
|
||||||
err bool
|
err bool
|
||||||
}{
|
}{
|
||||||
{"value", "", "", "z", Argument{"header", "a", ""}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, 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},
|
{"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-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
|
// failures
|
||||||
{"value", "", "", "X", Argument{"header", "a", ""}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, false},
|
{"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},
|
{"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", "", "2", "X", "", Argument{"header", "a", ""}, &map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, false}, // reference invalid header
|
||||||
// errors
|
// errors
|
||||||
{"regex", "*", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, true}, // invalid regex
|
{"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-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) {
|
func TestMatchRule(t *testing.T) {
|
||||||
for i, tt := range matchRuleTests {
|
for i, tt := range matchRuleTests {
|
||||||
r := MatchRule{tt.typ, tt.regex, tt.secret, tt.value, tt.param}
|
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)
|
ok, err := r.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, tt.remoteAddr)
|
||||||
if ok != tt.ok || (err != nil) != tt.err {
|
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))
|
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))
|
||||||
}
|
}
|
||||||
@@ -281,60 +324,60 @@ var andRuleTests = []struct {
|
|||||||
{
|
{
|
||||||
"(a=z, b=y): a=z && b=y",
|
"(a=z, b=y): a=z && b=y",
|
||||||
AndRule{
|
AndRule{
|
||||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
|
||||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
{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,
|
true, false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"(a=z, b=Y): a=z && b=y",
|
"(a=z, b=Y): a=z && b=y",
|
||||||
AndRule{
|
AndRule{
|
||||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
|
||||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
{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,
|
false, false,
|
||||||
},
|
},
|
||||||
// Complex test to cover Rules.Evaluate
|
// 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",
|
"(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{
|
AndRule{
|
||||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
|
||||||
{
|
{
|
||||||
And: &AndRule{
|
And: &AndRule{
|
||||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
|
||||||
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", ""}}},
|
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", ""}, ""}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Or: &OrRule{
|
Or: &OrRule{
|
||||||
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", ""}}},
|
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", ""}, ""}},
|
||||||
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", ""}}},
|
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", ""}, ""}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Not: &NotRule{
|
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,
|
true, false,
|
||||||
},
|
},
|
||||||
{"empty rule", AndRule{{}}, nil, nil, nil, nil, false, false},
|
{"empty rule", AndRule{{}}, nil, nil, nil, nil, false, false},
|
||||||
// failures
|
// failures
|
||||||
{
|
{
|
||||||
"invalid rule",
|
"invalid rule",
|
||||||
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}}}},
|
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}, ""}}},
|
||||||
&map[string]interface{}{"y": "z"}, nil, nil, nil,
|
&map[string]interface{}{"Y": "z"}, nil, nil, nil,
|
||||||
false, false,
|
false, false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAndRule(t *testing.T) {
|
func TestAndRule(t *testing.T) {
|
||||||
for _, tt := range andRuleTests {
|
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 {
|
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)
|
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.desc, tt.ok, tt.err, ok, err)
|
||||||
}
|
}
|
||||||
@@ -352,44 +395,44 @@ var orRuleTests = []struct {
|
|||||||
{
|
{
|
||||||
"(a=z, b=X): a=z || b=y",
|
"(a=z, b=X): a=z || b=y",
|
||||||
OrRule{
|
OrRule{
|
||||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
|
||||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
{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,
|
true, false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"(a=X, b=y): a=z || b=y",
|
"(a=X, b=y): a=z || b=y",
|
||||||
OrRule{
|
OrRule{
|
||||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
|
||||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
{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,
|
true, false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"(a=Z, b=Y): a=z || b=y",
|
"(a=Z, b=Y): a=z || b=y",
|
||||||
OrRule{
|
OrRule{
|
||||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}},
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
|
||||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}},
|
{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,
|
false, false,
|
||||||
},
|
},
|
||||||
// failures
|
// failures
|
||||||
{
|
{
|
||||||
"invalid rule",
|
"invalid rule",
|
||||||
OrRule{
|
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,
|
false, false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOrRule(t *testing.T) {
|
func TestOrRule(t *testing.T) {
|
||||||
for _, tt := range orRuleTests {
|
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 {
|
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)
|
t.Errorf("%#v:\nexpected ok: %#v, err: %v\ngot ok: %#v err: %v", tt.desc, tt.ok, tt.err, ok, err)
|
||||||
}
|
}
|
||||||
@@ -404,13 +447,13 @@ var notRuleTests = []struct {
|
|||||||
ok bool
|
ok bool
|
||||||
err 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=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=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNotRule(t *testing.T) {
|
func TestNotRule(t *testing.T) {
|
||||||
for _, tt := range notRuleTests {
|
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 {
|
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)
|
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.rule, tt.ok, tt.err, ok, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
- id: webhook
|
||||||
|
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:
|
||||||
|
- source: payload
|
||||||
|
name: head_commit.id
|
||||||
|
- source: payload
|
||||||
|
name: pusher.name
|
||||||
|
- source: payload
|
||||||
|
name: pusher.email
|
||||||
|
trigger-rule:
|
||||||
|
and:
|
||||||
|
- match:
|
||||||
|
type: payload-hash-sha1
|
||||||
|
secret: mysecret
|
||||||
|
parameter:
|
||||||
|
source: header
|
||||||
|
name: X-Hub-Signature
|
||||||
|
- match:
|
||||||
|
type: value
|
||||||
|
value: refs/heads/master
|
||||||
|
parameter:
|
||||||
|
source: payload
|
||||||
|
name: ref
|
||||||
+1
-1
@@ -26,7 +26,7 @@ func watchForSignals() {
|
|||||||
if sig == syscall.SIGUSR1 {
|
if sig == syscall.SIGUSR1 {
|
||||||
log.Println("caught USR1 signal")
|
log.Println("caught USR1 signal")
|
||||||
|
|
||||||
reloadHooks()
|
reloadAllHooks()
|
||||||
} else {
|
} else {
|
||||||
log.Printf("caught unhandled signal %+v\n", sig)
|
log.Printf("caught unhandled signal %+v\n", sig)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"execute-command": "{{ .Hookecho }}",
|
"execute-command": "{{ .Hookecho }}",
|
||||||
"command-working-directory": "/",
|
"command-working-directory": "/",
|
||||||
"include-command-output-in-response": true,
|
"include-command-output-in-response": true,
|
||||||
|
"trigger-rule-mismatch-http-response-code": 400,
|
||||||
"pass-environment-to-command":
|
"pass-environment-to-command":
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
"command-working-directory": "/",
|
"command-working-directory": "/",
|
||||||
"include-command-output-in-response": false,
|
"include-command-output-in-response": false,
|
||||||
"response-message": "success",
|
"response-message": "success",
|
||||||
|
"trigger-rule-mismatch-http-response-code": 999,
|
||||||
"parse-parameters-as-json": [
|
"parse-parameters-as-json": [
|
||||||
{
|
{
|
||||||
"source": "payload",
|
"source": "payload",
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
- trigger-rule:
|
||||||
|
and:
|
||||||
|
- match:
|
||||||
|
parameter:
|
||||||
|
source: header
|
||||||
|
name: X-Hub-Signature
|
||||||
|
secret: mysecret
|
||||||
|
type: payload-hash-sha1
|
||||||
|
- match:
|
||||||
|
parameter:
|
||||||
|
source: payload
|
||||||
|
name: ref
|
||||||
|
type: value
|
||||||
|
value: refs/heads/master
|
||||||
|
include-command-output-in-response: true
|
||||||
|
trigger-rule-mismatch-http-response-code: 400
|
||||||
|
execute-command: '{{ .Hookecho }}'
|
||||||
|
pass-arguments-to-command:
|
||||||
|
- source: payload
|
||||||
|
name: head_commit.id
|
||||||
|
- source: payload
|
||||||
|
name: head_commit.author.email
|
||||||
|
pass-environment-to-command:
|
||||||
|
- source: payload
|
||||||
|
name: head_commit.timestamp
|
||||||
|
id: github
|
||||||
|
command-working-directory: /
|
||||||
|
- trigger-rule:
|
||||||
|
and:
|
||||||
|
- match:
|
||||||
|
parameter:
|
||||||
|
source: payload
|
||||||
|
name: payload.canon_url
|
||||||
|
type: value
|
||||||
|
value: https://bitbucket.org
|
||||||
|
- match:
|
||||||
|
parameter:
|
||||||
|
source: payload
|
||||||
|
name: payload.repository.absolute_url
|
||||||
|
type: value
|
||||||
|
value: /webhook/testing/
|
||||||
|
- match:
|
||||||
|
parameter:
|
||||||
|
source: payload
|
||||||
|
name: payload.commits.0.branch
|
||||||
|
type: value
|
||||||
|
value: master
|
||||||
|
parse-parameters-as-json:
|
||||||
|
- source: payload
|
||||||
|
name: payload
|
||||||
|
trigger-rule-mismatch-http-response-code: 999
|
||||||
|
execute-command: '{{ .Hookecho }}'
|
||||||
|
response-message: success
|
||||||
|
include-command-output-in-response: false
|
||||||
|
id: bitbucket
|
||||||
|
command-working-directory: /
|
||||||
|
- trigger-rule:
|
||||||
|
match:
|
||||||
|
parameter:
|
||||||
|
source: payload
|
||||||
|
name: ref
|
||||||
|
type: value
|
||||||
|
value: refs/heads/master
|
||||||
|
pass-arguments-to-command:
|
||||||
|
- source: payload
|
||||||
|
name: commits.0.id
|
||||||
|
- source: payload
|
||||||
|
name: user_name
|
||||||
|
- source: payload
|
||||||
|
name: user_email
|
||||||
|
execute-command: '{{ .Hookecho }}'
|
||||||
|
response-message: success
|
||||||
|
include-command-output-in-response: true
|
||||||
|
id: gitlab
|
||||||
|
command-working-directory: /
|
||||||
+155
-42
@@ -21,7 +21,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
version = "2.6.0"
|
version = "2.6.5"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -30,7 +30,6 @@ var (
|
|||||||
verbose = flag.Bool("verbose", false, "show verbose output")
|
verbose = flag.Bool("verbose", false, "show verbose output")
|
||||||
noPanic = flag.Bool("nopanic", false, "do not panic if hooks cannot be loaded when webhook is not running in verbose mode")
|
noPanic = flag.Bool("nopanic", false, "do not panic if hooks cannot be loaded when webhook is not running in verbose mode")
|
||||||
hotReload = flag.Bool("hotreload", false, "watch hooks file for changes and reload them automatically")
|
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)")
|
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")
|
secure = flag.Bool("secure", false, "use HTTPS instead of HTTP")
|
||||||
cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file")
|
cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file")
|
||||||
@@ -38,16 +37,35 @@ var (
|
|||||||
justDisplayVersion = flag.Bool("version", false, "display webhook version and quit")
|
justDisplayVersion = flag.Bool("version", false, "display webhook version and quit")
|
||||||
|
|
||||||
responseHeaders hook.ResponseHeaders
|
responseHeaders hook.ResponseHeaders
|
||||||
|
hooksFiles hook.HooksFiles
|
||||||
|
|
||||||
|
loadedHooksFromFiles = make(map[string]hook.Hooks)
|
||||||
|
|
||||||
watcher *fsnotify.Watcher
|
watcher *fsnotify.Watcher
|
||||||
signals chan os.Signal
|
signals chan os.Signal
|
||||||
|
|
||||||
hooks hook.Hooks
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func matchLoadedHook(id string) *hook.Hook {
|
||||||
hooks = hook.Hooks{}
|
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() {
|
||||||
|
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.Var(&responseHeaders, "header", "response header to return, specified in format name=value, use multiple times to set multiple headers")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@@ -57,6 +75,10 @@ func main() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(hooksFiles) == 0 {
|
||||||
|
hooksFiles = append(hooksFiles, "hooks.json")
|
||||||
|
}
|
||||||
|
|
||||||
log.SetPrefix("[webhook] ")
|
log.SetPrefix("[webhook] ")
|
||||||
log.SetFlags(log.Ldate | log.Ltime)
|
log.SetFlags(log.Ldate | log.Ltime)
|
||||||
|
|
||||||
@@ -70,54 +92,76 @@ func main() {
|
|||||||
setupSignals()
|
setupSignals()
|
||||||
|
|
||||||
// load and parse hooks
|
// 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{}
|
||||||
|
|
||||||
|
err := newHooks.LoadFromFile(hooksFilePath)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("couldn't load hooks from file! %+v\n", err)
|
log.Printf("couldn't load hooks from file! %+v\n", err)
|
||||||
} else {
|
} else {
|
||||||
seenHooksIds := make(map[string]bool)
|
log.Printf("found %d hook(s) in file\n", len(newHooks))
|
||||||
|
|
||||||
log.Printf("found %d hook(s) in file\n", len(hooks))
|
for _, hook := range newHooks {
|
||||||
|
if matchLoadedHook(hook.ID) != nil {
|
||||||
for _, hook := range hooks {
|
|
||||||
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)
|
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)
|
log.Printf("\tloaded: %s\n", hook.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadedHooksFromFiles[hooksFilePath] = newHooks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
if *hotReload {
|
||||||
// set up file watcher
|
|
||||||
log.Printf("setting up file watcher for %s\n", *hooksFilePath)
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
watcher, err = fsnotify.NewWatcher()
|
watcher, err = fsnotify.NewWatcher()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("error creating file watcher instance", err)
|
log.Fatal("error creating file watcher instance\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer watcher.Close()
|
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)
|
err = watcher.Add(hooksFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("error adding hooks file to the watcher", err)
|
log.Fatal("error adding hooks file to the watcher\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go watchForFileChange()
|
||||||
|
}
|
||||||
|
|
||||||
l := negroni.NewLogger()
|
l := negroni.NewLogger()
|
||||||
l.ALogger = log.New(os.Stderr, "[webhook] ", log.Ldate|log.Ltime)
|
|
||||||
|
l.SetFormat("{{.Status}} | {{.Duration}} | {{.Hostname}} | {{.Method}} {{.Path}} \n")
|
||||||
|
|
||||||
|
standardLogger := log.New(os.Stdout, "[webhook] ", log.Ldate|log.Ltime)
|
||||||
|
|
||||||
|
if !*verbose {
|
||||||
|
standardLogger.SetOutput(ioutil.Discard)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.ALogger = standardLogger
|
||||||
|
|
||||||
negroniRecovery := &negroni.Recovery{
|
negroniRecovery := &negroni.Recovery{
|
||||||
Logger: l.ALogger,
|
Logger: l.ALogger,
|
||||||
@@ -153,13 +197,15 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func hookHandler(w http.ResponseWriter, r *http.Request) {
|
func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Printf("incoming HTTP request from %s\n", r.RemoteAddr)
|
||||||
|
|
||||||
for _, responseHeader := range responseHeaders {
|
for _, responseHeader := range responseHeaders {
|
||||||
w.Header().Set(responseHeader.Name, responseHeader.Value)
|
w.Header().Set(responseHeader.Name, responseHeader.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
id := mux.Vars(r)["id"]
|
id := mux.Vars(r)["id"]
|
||||||
|
|
||||||
if matchedHook := hooks.Match(id); matchedHook != nil {
|
if matchedHook := matchLoadedHook(id); matchedHook != nil {
|
||||||
log.Printf("%s got matched\n", id)
|
log.Printf("%s got matched\n", id)
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
@@ -208,7 +254,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if matchedHook.TriggerRule == nil {
|
if matchedHook.TriggerRule == nil {
|
||||||
ok = true
|
ok = true
|
||||||
} else {
|
} else {
|
||||||
ok, err = matchedHook.TriggerRule.Evaluate(&headers, &query, &payload, &body)
|
ok, err = matchedHook.TriggerRule.Evaluate(&headers, &query, &payload, &body, r.RemoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := fmt.Sprintf("error evaluating hook: %s", err)
|
msg := fmt.Sprintf("error evaluating hook: %s", err)
|
||||||
log.Printf(msg)
|
log.Printf(msg)
|
||||||
@@ -242,6 +288,17 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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
|
// if none of the hooks got triggered
|
||||||
log.Printf("%s got matched, but didn't get triggered because the trigger rules were not satisfied\n", matchedHook.ID)
|
log.Printf("%s got matched, but didn't get triggered because the trigger rules were not satisfied\n", matchedHook.ID)
|
||||||
|
|
||||||
@@ -278,7 +335,7 @@ func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, b
|
|||||||
|
|
||||||
log.Printf("executing %s (%s) with arguments %q and environment %s using %s as cwd\n", h.ExecuteCommand, cmd.Path, cmd.Args, envs, cmd.Dir)
|
log.Printf("executing %s (%s) with arguments %q and environment %s using %s as cwd\n", h.ExecuteCommand, cmd.Path, cmd.Args, envs, cmd.Dir)
|
||||||
|
|
||||||
out, err := cmd.Output()
|
out, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
log.Printf("command output: %s\n", out)
|
log.Printf("command output: %s\n", out)
|
||||||
|
|
||||||
@@ -291,32 +348,74 @@ func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, b
|
|||||||
return string(out), err
|
return string(out), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func reloadHooks() {
|
func reloadHooks(hooksFilePath string) {
|
||||||
newHooks := hook.Hooks{}
|
hooksInFile := hook.Hooks{}
|
||||||
|
|
||||||
// parse and swap
|
// 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 {
|
if err != nil {
|
||||||
log.Printf("couldn't load hooks from file! %+v\n", err)
|
log.Printf("couldn't load hooks from file! %+v\n", err)
|
||||||
} else {
|
} else {
|
||||||
seenHooksIds := make(map[string]bool)
|
seenHooksIds := make(map[string]bool)
|
||||||
|
|
||||||
log.Printf("found %d hook(s) in file\n", len(newHooks))
|
log.Printf("found %d hook(s) in file\n", len(hooksInFile))
|
||||||
|
|
||||||
for _, hook := range newHooks {
|
for _, hook := range hooksInFile {
|
||||||
if seenHooksIds[hook.ID] == true {
|
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.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")
|
log.Println("reverting hooks back to the previous configuration")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
seenHooksIds[hook.ID] = true
|
seenHooksIds[hook.ID] = true
|
||||||
log.Printf("\tloaded: %s\n", hook.ID)
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,9 +424,23 @@ func watchForFileChange() {
|
|||||||
select {
|
select {
|
||||||
case event := <-(*watcher).Events:
|
case event := <-(*watcher).Events:
|
||||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||||
log.Println("hooks file modified")
|
log.Printf("hooks file %s modified\n", event.Name)
|
||||||
|
reloadHooks(event.Name)
|
||||||
reloadHooks()
|
} 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:
|
case err := <-(*watcher).Errors:
|
||||||
log.Println("watcher error:", err)
|
log.Println("watcher error:", err)
|
||||||
|
|||||||
+14
-5
@@ -21,7 +21,8 @@ func TestWebhook(t *testing.T) {
|
|||||||
hookecho, cleanupHookecho := buildHookecho(t)
|
hookecho, cleanupHookecho := buildHookecho(t)
|
||||||
defer cleanupHookecho()
|
defer cleanupHookecho()
|
||||||
|
|
||||||
config, cleanupConfig := genConfig(t, hookecho)
|
for _, hookTmpl := range []string{"test/hooks.json.tmpl", "test/hooks.yaml.tmpl"} {
|
||||||
|
config, cleanupConfig := genConfig(t, hookecho, hookTmpl)
|
||||||
defer cleanupConfig()
|
defer cleanupConfig()
|
||||||
|
|
||||||
webhook, cleanupWebhook := buildWebhook(t)
|
webhook, cleanupWebhook := buildWebhook(t)
|
||||||
@@ -77,6 +78,7 @@ func TestWebhook(t *testing.T) {
|
|||||||
t.Errorf("failed %q (id: %s):\nexpected status: %#v, response: %s\ngot status: %#v, response: %s", tt.desc, tt.id, tt.respStatus, tt.respBody, res.StatusCode, body)
|
t.Errorf("failed %q (id: %s):\nexpected status: %#v, response: %s\ngot status: %#v, response: %s", tt.desc, tt.id, tt.respStatus, tt.respBody, res.StatusCode, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildHookecho(t *testing.T) (bin string, cleanup func()) {
|
func buildHookecho(t *testing.T) (bin string, cleanup func()) {
|
||||||
@@ -103,8 +105,8 @@ func buildHookecho(t *testing.T) (bin string, cleanup func()) {
|
|||||||
return bin, func() { os.RemoveAll(tmp) }
|
return bin, func() { os.RemoveAll(tmp) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func genConfig(t *testing.T, bin string) (config string, cleanup func()) {
|
func genConfig(t *testing.T, bin string, hookTemplate string) (config string, cleanup func()) {
|
||||||
tmpl := template.Must(template.ParseFiles("test/hooks.json.tmpl"))
|
tmpl := template.Must(template.ParseFiles(hookTemplate))
|
||||||
|
|
||||||
tmp, err := ioutil.TempDir("", "webhook-config-")
|
tmp, err := ioutil.TempDir("", "webhook-config-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -116,7 +118,9 @@ func genConfig(t *testing.T, bin string) (config string, cleanup func()) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
path := filepath.Join(tmp, "hooks.json")
|
outputBaseName := filepath.Ext(filepath.Ext(hookTemplate))
|
||||||
|
|
||||||
|
path := filepath.Join(tmp, outputBaseName)
|
||||||
file, err := os.Create(path)
|
file, err := os.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Creating config template: %v", err)
|
t.Fatalf("Creating config template: %v", err)
|
||||||
@@ -515,5 +519,10 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
|
|||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
|
||||||
{"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