mirror of
https://github.com/adnanh/webhook.git
synced 2026-07-27 01:29:16 +08:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 032c74451f | |||
| 1c50853d8d | |||
| b5ed4cbea7 | |||
| b6d176705e | |||
| 421fc2cbcd | |||
| 10d65dd2fd | |||
| 54a9dbe1d6 | |||
| 30baec91df | |||
| 3bcf6d5e2b | |||
| 67343e281d | |||
| 18b0573bc4 | |||
| ec42679305 | |||
| e85e0592dd | |||
| 4d20af8027 | |||
| d4e772c815 | |||
| 04a2b2a680 | |||
| 4914a4131f | |||
| 37698e63b6 | |||
| 80aa9800bf | |||
| f620cb056b | |||
| e55e7efe14 | |||
| 9fa02f7341 | |||
| f59f0a5c84 | |||
| 5594a62f8f | |||
| 642516d46e | |||
| 9cef8ed882 |
@@ -0,0 +1,4 @@
|
|||||||
|
.idea
|
||||||
|
.cover
|
||||||
|
coverage
|
||||||
|
webhook
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[](https://gitter.im/adnanh/webhook?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://flattr.com/submit/auto?user_id=adnanh&url=https%3A%2F%2Fwww.github.com%2Fadnanh%2Fwebhook)
|
[](https://ghit.me/repo/adnanh/webhook)[](https://gitter.im/adnanh/webhook?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://flattr.com/submit/auto?user_id=adnanh&url=https%3A%2F%2Fwww.github.com%2Fadnanh%2Fwebhook)
|
||||||
|
|
||||||
# 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.
|
||||||
@@ -59,6 +59,12 @@ However, hook defined like that could pose a security threat to your system, bec
|
|||||||
# Using HTTPS
|
# 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.
|
[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
|
# Examples
|
||||||
Check out [Hook examples page](https://github.com/adnanh/webhook/wiki/Hook-Examples) for more complex examples of hooks.
|
Check out [Hook examples page](https://github.com/adnanh/webhook/wiki/Hook-Examples) for more complex examples of hooks.
|
||||||
|
|
||||||
|
|||||||
+66
-38
@@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -92,7 +93,7 @@ func CheckPayloadSignature(payload []byte, secret string, signature string) (str
|
|||||||
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
||||||
|
|
||||||
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
|
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
|
||||||
return expectedMAC, &SignatureError{expectedMAC}
|
return expectedMAC, &SignatureError{signature}
|
||||||
}
|
}
|
||||||
return expectedMAC, err
|
return expectedMAC, err
|
||||||
}
|
}
|
||||||
@@ -195,8 +196,9 @@ func ExtractParameterAsString(s string, params interface{}) (string, bool) {
|
|||||||
// Argument type specifies the parameter key name and the source it should
|
// Argument type specifies the parameter key name and the source it should
|
||||||
// be extracted from
|
// be extracted from
|
||||||
type Argument struct {
|
type Argument struct {
|
||||||
Source string `json:"source"`
|
Source string `json:"source,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name,omitempty"`
|
||||||
|
EnvName string `json:"envname,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Argument method returns the value for the Argument's key name
|
// Get Argument method returns the value for the Argument's key name
|
||||||
@@ -246,17 +248,54 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Header is a structure containing header name and it's value
|
||||||
|
type Header struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseHeaders is a slice of Header objects
|
||||||
|
type ResponseHeaders []Header
|
||||||
|
|
||||||
|
func (h *ResponseHeaders) String() string {
|
||||||
|
// a 'hack' to display name=value in flag usage listing
|
||||||
|
if len(*h) == 0 {
|
||||||
|
return "name=value"
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]string, len(*h))
|
||||||
|
|
||||||
|
for idx, responseHeader := range *h {
|
||||||
|
result[idx] = fmt.Sprintf("%s=%s", responseHeader.Name, responseHeader.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprint(strings.Join(result, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set method appends new Header object from header=value notation
|
||||||
|
func (h *ResponseHeaders) Set(value string) error {
|
||||||
|
splitResult := strings.SplitN(value, "=", 2)
|
||||||
|
|
||||||
|
if len(splitResult) != 2 {
|
||||||
|
return errors.New("header flag must be in name=value format")
|
||||||
|
}
|
||||||
|
|
||||||
|
*h = append(*h, Header{Name: splitResult[0], Value: splitResult[1]})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Hook type is a structure containing details for a single hook
|
// Hook type is a structure containing details for a single hook
|
||||||
type Hook struct {
|
type Hook struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id,omitempty"`
|
||||||
ExecuteCommand string `json:"execute-command"`
|
ExecuteCommand string `json:"execute-command,omitempty"`
|
||||||
CommandWorkingDirectory string `json:"command-working-directory"`
|
CommandWorkingDirectory string `json:"command-working-directory,omitempty"`
|
||||||
ResponseMessage string `json:"response-message"`
|
ResponseMessage string `json:"response-message,omitempty"`
|
||||||
CaptureCommandOutput bool `json:"include-command-output-in-response"`
|
ResponseHeaders ResponseHeaders `json:"response-headers,omitempty"`
|
||||||
PassEnvironmentToCommand []Argument `json:"pass-environment-to-command"`
|
CaptureCommandOutput bool `json:"include-command-output-in-response,omitempty"`
|
||||||
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command"`
|
PassEnvironmentToCommand []Argument `json:"pass-environment-to-command,omitempty"`
|
||||||
JSONStringParameters []Argument `json:"parse-parameters-as-json"`
|
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command,omitempty"`
|
||||||
TriggerRule *Rules `json:"trigger-rule"`
|
JSONStringParameters []Argument `json:"parse-parameters-as-json,omitempty"`
|
||||||
|
TriggerRule *Rules `json:"trigger-rule,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseJSONParameters decodes specified arguments to JSON objects and replaces the
|
// ParseJSONParameters decodes specified arguments to JSON objects and replaces the
|
||||||
@@ -326,7 +365,13 @@ func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string
|
|||||||
|
|
||||||
for i := range h.PassEnvironmentToCommand {
|
for i := range h.PassEnvironmentToCommand {
|
||||||
if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok {
|
if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok {
|
||||||
|
if h.PassEnvironmentToCommand[i].EnvName != "" {
|
||||||
|
// first try to use the EnvName if specified
|
||||||
|
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].EnvName+"="+arg)
|
||||||
|
} else {
|
||||||
|
// then fallback on the name
|
||||||
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg)
|
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return args, &ArgumentError{h.PassEnvironmentToCommand[i]}
|
return args, &ArgumentError{h.PassEnvironmentToCommand[i]}
|
||||||
}
|
}
|
||||||
@@ -367,29 +412,12 @@ func (h *Hooks) Match(id string) *Hook {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchAll iterates through Hooks and returns all of the hooks that match the
|
|
||||||
// given ID, if no hook matches the given ID, nil is returned
|
|
||||||
func (h *Hooks) MatchAll(id string) []*Hook {
|
|
||||||
var matchedHooks []*Hook
|
|
||||||
for i := range *h {
|
|
||||||
if (*h)[i].ID == id {
|
|
||||||
matchedHooks = append(matchedHooks, &(*h)[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(matchedHooks) > 0 {
|
|
||||||
return matchedHooks
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rules is a structure that contains one of the valid rule types
|
// Rules is a structure that contains one of the valid rule types
|
||||||
type Rules struct {
|
type Rules struct {
|
||||||
And *AndRule `json:"and"`
|
And *AndRule `json:"and,omitempty"`
|
||||||
Or *OrRule `json:"or"`
|
Or *OrRule `json:"or,omitempty"`
|
||||||
Not *NotRule `json:"not"`
|
Not *NotRule `json:"not,omitempty"`
|
||||||
Match *MatchRule `json:"match"`
|
Match *MatchRule `json:"match,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -464,11 +492,11 @@ func (r NotRule) Evaluate(headers, query, payload *map[string]interface{}, body
|
|||||||
|
|
||||||
// MatchRule will evaluate to true based on the type
|
// MatchRule will evaluate to true based on the type
|
||||||
type MatchRule struct {
|
type MatchRule struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type,omitempty"`
|
||||||
Regex string `json:"regex"`
|
Regex string `json:"regex,omitempty"`
|
||||||
Secret string `json:"secret"`
|
Secret string `json:"secret,omitempty"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value,omitempty"`
|
||||||
Parameter Argument `json:"parameter"`
|
Parameter Argument `json:"parameter,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constants for the MatchRule type
|
// Constants for the MatchRule type
|
||||||
|
|||||||
+104
-38
@@ -2,6 +2,7 @@ package hook
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,6 +26,10 @@ func TestCheckPayloadSignature(t *testing.T) {
|
|||||||
if (err == nil) != tt.ok || mac != tt.mac {
|
if (err == nil) != tt.ok || mac != tt.mac {
|
||||||
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err != nil && strings.Contains(err.Error(), tt.mac) {
|
||||||
|
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +85,7 @@ var argumentGetTests = []struct {
|
|||||||
|
|
||||||
func TestArgumentGet(t *testing.T) {
|
func TestArgumentGet(t *testing.T) {
|
||||||
for _, tt := range argumentGetTests {
|
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)
|
value, ok := a.Get(tt.headers, tt.query, tt.payload)
|
||||||
if ok != tt.ok || value != tt.value {
|
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)
|
t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, ok:%#v}", tt.source, tt.name, tt.value, tt.ok, value, ok)
|
||||||
@@ -94,14 +99,14 @@ var hookParseJSONParametersTests = []struct {
|
|||||||
rheaders, rquery, rpayload *map[string]interface{}
|
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) {
|
||||||
@@ -121,9 +126,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) {
|
||||||
@@ -136,6 +141,67 @@ func TestHookExtractCommandArguments(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Here we test the extraction of env variables when the user defined a hook
|
||||||
|
// with the "pass-environment-to-command" directive
|
||||||
|
// we test both cases where the name of the data is used as the name of the
|
||||||
|
// env key & the case where the hook definition sets the env var name to a
|
||||||
|
// fixed value using the envname construct like so::
|
||||||
|
// [
|
||||||
|
// {
|
||||||
|
// "id": "push",
|
||||||
|
// "execute-command": "bb2mm",
|
||||||
|
// "command-working-directory": "/tmp",
|
||||||
|
// "pass-environment-to-command":
|
||||||
|
// [
|
||||||
|
// {
|
||||||
|
// "source": "entire-payload",
|
||||||
|
// "envname": "PAYLOAD"
|
||||||
|
// },
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
var hookExtractCommandArgumentsForEnvTests = []struct {
|
||||||
|
exec string
|
||||||
|
args []Argument
|
||||||
|
headers, query, payload *map[string]interface{}
|
||||||
|
value []string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
// successes
|
||||||
|
{
|
||||||
|
"test",
|
||||||
|
[]Argument{Argument{"header", "a", ""}},
|
||||||
|
&map[string]interface{}{"a": "z"}, nil, nil,
|
||||||
|
[]string{"HOOK_a=z"},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test",
|
||||||
|
[]Argument{Argument{"header", "a", "MYKEY"}},
|
||||||
|
&map[string]interface{}{"a": "z"}, nil, nil,
|
||||||
|
[]string{"HOOK_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 {
|
var hooksLoadFromFileTests = []struct {
|
||||||
path string
|
path string
|
||||||
ok bool
|
ok bool
|
||||||
@@ -182,16 +248,16 @@ var matchRuleTests = []struct {
|
|||||||
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},
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMatchRule(t *testing.T) {
|
func TestMatchRule(t *testing.T) {
|
||||||
@@ -215,8 +281,8 @@ 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,
|
||||||
@@ -224,8 +290,8 @@ 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{},
|
||||||
false, false,
|
false, false,
|
||||||
@@ -234,22 +300,22 @@ var andRuleTests = []struct {
|
|||||||
{
|
{
|
||||||
"(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", ""}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -260,7 +326,7 @@ var andRuleTests = []struct {
|
|||||||
// 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,
|
||||||
},
|
},
|
||||||
@@ -286,8 +352,8 @@ 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,
|
||||||
@@ -295,8 +361,8 @@ var orRuleTests = []struct {
|
|||||||
{
|
{
|
||||||
"(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,
|
||||||
@@ -304,8 +370,8 @@ var orRuleTests = []struct {
|
|||||||
{
|
{
|
||||||
"(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,
|
||||||
@@ -314,7 +380,7 @@ var orRuleTests = []struct {
|
|||||||
{
|
{
|
||||||
"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,
|
||||||
@@ -338,8 +404,8 @@ 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) {
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||||
"command-working-directory": "/home/adnan/go",
|
"command-working-directory": "/home/adnan/go",
|
||||||
"response-message": "I got the payload!",
|
"response-message": "I got the payload!",
|
||||||
|
"response-headers":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Access-Control-Allow-Origin",
|
||||||
|
"value": "*"
|
||||||
|
}
|
||||||
|
],
|
||||||
"pass-arguments-to-command":
|
"pass-arguments-to-command":
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|||||||
+51
-31
@@ -21,11 +21,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
version = "2.3.6"
|
version = "2.4.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ip = flag.String("ip", "", "ip the webhook should serve hooks on")
|
ip = flag.String("ip", "0.0.0.0", "ip the webhook should serve hooks on")
|
||||||
port = flag.Int("port", 9000, "port the webhook should serve hooks on")
|
port = flag.Int("port", 9000, "port the webhook should serve hooks on")
|
||||||
verbose = flag.Bool("verbose", false, "show verbose output")
|
verbose = flag.Bool("verbose", false, "show verbose output")
|
||||||
noPanic = flag.Bool("nopanic", false, "do not panic if hooks cannot be loaded when webhook is not running in verbose mode")
|
noPanic = flag.Bool("nopanic", false, "do not panic if hooks cannot be loaded when webhook is not running in verbose mode")
|
||||||
@@ -36,6 +36,8 @@ var (
|
|||||||
cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file")
|
cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file")
|
||||||
key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file")
|
key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file")
|
||||||
|
|
||||||
|
responseHeaders hook.ResponseHeaders
|
||||||
|
|
||||||
watcher *fsnotify.Watcher
|
watcher *fsnotify.Watcher
|
||||||
signals chan os.Signal
|
signals chan os.Signal
|
||||||
|
|
||||||
@@ -45,6 +47,8 @@ var (
|
|||||||
func main() {
|
func main() {
|
||||||
hooks = hook.Hooks{}
|
hooks = hook.Hooks{}
|
||||||
|
|
||||||
|
flag.Var(&responseHeaders, "header", "response header to return, specified in format name=value, use multiple times to set multiple headers")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
log.SetPrefix("[webhook] ")
|
log.SetPrefix("[webhook] ")
|
||||||
@@ -72,10 +76,16 @@ func main() {
|
|||||||
|
|
||||||
log.Printf("couldn't load hooks from file! %+v\n", err)
|
log.Printf("couldn't load hooks from file! %+v\n", err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("loaded %d hook(s) from file\n", len(hooks))
|
seenHooksIds := make(map[string]bool)
|
||||||
|
|
||||||
|
log.Printf("found %d hook(s) in file\n", len(hooks))
|
||||||
|
|
||||||
for _, hook := range hooks {
|
for _, hook := range hooks {
|
||||||
log.Printf("\t> %s\n", hook.ID)
|
if seenHooksIds[hook.ID] == true {
|
||||||
|
log.Fatalf("error: hook with the id %s has already been loaded!\nplease check your hooks file for duplicate hooks ids!\n", hook.ID)
|
||||||
|
}
|
||||||
|
seenHooksIds[hook.ID] = true
|
||||||
|
log.Printf("\tloaded: %s\n", hook.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,10 +111,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
l := negroni.NewLogger()
|
l := negroni.NewLogger()
|
||||||
l.Logger = log.New(os.Stderr, "[webhook] ", log.Ldate|log.Ltime)
|
l.ALogger = log.New(os.Stderr, "[webhook] ", log.Ldate|log.Ltime)
|
||||||
|
|
||||||
negroniRecovery := &negroni.Recovery{
|
negroniRecovery := &negroni.Recovery{
|
||||||
Logger: l.Logger,
|
Logger: l.ALogger,
|
||||||
PrintStack: true,
|
PrintStack: true,
|
||||||
StackAll: false,
|
StackAll: false,
|
||||||
StackSize: 1024 * 8,
|
StackSize: 1024 * 8,
|
||||||
@@ -127,22 +137,24 @@ func main() {
|
|||||||
n.UseHandler(router)
|
n.UseHandler(router)
|
||||||
|
|
||||||
if *secure {
|
if *secure {
|
||||||
log.Printf("starting secure (https) webhook on %s:%d", *ip, *port)
|
log.Printf("serving hooks on https://%s:%d%s", *ip, *port, hooksURL)
|
||||||
log.Fatal(http.ListenAndServeTLS(fmt.Sprintf("%s:%d", *ip, *port), *cert, *key, n))
|
log.Fatal(http.ListenAndServeTLS(fmt.Sprintf("%s:%d", *ip, *port), *cert, *key, n))
|
||||||
} else {
|
} else {
|
||||||
log.Printf("starting insecure (http) webhook on %s:%d", *ip, *port)
|
log.Printf("serving hooks on http://%s:%d%s", *ip, *port, hooksURL)
|
||||||
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *ip, *port), n))
|
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *ip, *port), n))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func hookHandler(w http.ResponseWriter, r *http.Request) {
|
func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
for _, responseHeader := range responseHeaders {
|
||||||
|
w.Header().Set(responseHeader.Name, responseHeader.Value)
|
||||||
|
}
|
||||||
|
|
||||||
id := mux.Vars(r)["id"]
|
id := mux.Vars(r)["id"]
|
||||||
|
|
||||||
matchedHooks := hooks.MatchAll(id)
|
if matchedHook := hooks.Match(id); matchedHook != nil {
|
||||||
|
log.Printf("%s got matched\n", id)
|
||||||
if matchedHooks != nil {
|
|
||||||
log.Printf("%s got matched (%d time(s))\n", id, len(matchedHooks))
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -179,48 +191,48 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle hook
|
// handle hook
|
||||||
for _, h := range matchedHooks {
|
if err = matchedHook.ParseJSONParameters(&headers, &query, &payload); err != nil {
|
||||||
err := h.ParseJSONParameters(&headers, &query, &payload)
|
msg := fmt.Sprintf("error parsing JSON parameters: %s", err)
|
||||||
if err != nil {
|
|
||||||
msg := fmt.Sprintf("error parsing JSON: %s", err)
|
|
||||||
log.Printf(msg)
|
log.Printf(msg)
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
fmt.Fprintf(w, msg)
|
fmt.Fprintf(w, "Unable to parse JSON parameters.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var ok bool
|
var ok bool
|
||||||
|
|
||||||
if h.TriggerRule == nil {
|
if matchedHook.TriggerRule == nil {
|
||||||
ok = true
|
ok = true
|
||||||
} else {
|
} else {
|
||||||
ok, err = h.TriggerRule.Evaluate(&headers, &query, &payload, &body)
|
ok, err = matchedHook.TriggerRule.Evaluate(&headers, &query, &payload, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := fmt.Sprintf("error evaluating hook: %s", err)
|
msg := fmt.Sprintf("error evaluating hook: %s", err)
|
||||||
log.Printf(msg)
|
log.Printf(msg)
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
fmt.Fprintf(w, msg)
|
fmt.Fprintf(w, "Error occurred while evaluating hook rules.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
log.Printf("%s hook triggered successfully\n", h.ID)
|
log.Printf("%s hook triggered successfully\n", matchedHook.ID)
|
||||||
|
|
||||||
if h.CaptureCommandOutput {
|
for _, responseHeader := range matchedHook.ResponseHeaders {
|
||||||
response := handleHook(h, &headers, &query, &payload, &body)
|
w.Header().Set(responseHeader.Name, responseHeader.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchedHook.CaptureCommandOutput {
|
||||||
|
response := handleHook(matchedHook, &headers, &query, &payload, &body)
|
||||||
fmt.Fprintf(w, response)
|
fmt.Fprintf(w, response)
|
||||||
} else {
|
} else {
|
||||||
go handleHook(h, &headers, &query, &payload, &body)
|
go handleHook(matchedHook, &headers, &query, &payload, &body)
|
||||||
fmt.Fprintf(w, h.ResponseMessage)
|
fmt.Fprintf(w, matchedHook.ResponseMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// if none of the hooks got triggered
|
// if none of the hooks got triggered
|
||||||
log.Printf("%s got matched (%d time(s)), but didn't get triggered because the trigger rules were not satisfied\n", matchedHooks[0].ID, len(matchedHooks))
|
log.Printf("%s got matched, but didn't get triggered because the trigger rules were not satisfied\n", matchedHook.ID)
|
||||||
|
|
||||||
fmt.Fprintf(w, "Hook rules were not satisfied.")
|
fmt.Fprintf(w, "Hook rules were not satisfied.")
|
||||||
} else {
|
} else {
|
||||||
@@ -284,10 +296,18 @@ func reloadHooks() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("couldn't load hooks from file! %+v\n", err)
|
log.Printf("couldn't load hooks from file! %+v\n", err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("loaded %d hook(s) from file\n", len(hooks))
|
seenHooksIds := make(map[string]bool)
|
||||||
|
|
||||||
for _, hook := range hooks {
|
log.Printf("found %d hook(s) in file\n", len(newHooks))
|
||||||
log.Printf("\t> %s\n", hook.ID)
|
|
||||||
|
for _, hook := range newHooks {
|
||||||
|
if seenHooksIds[hook.ID] == true {
|
||||||
|
log.Printf("error: hook with the id %s has already been loaded!\nplease check your hooks file for duplicate hooks ids!", hook.ID)
|
||||||
|
log.Println("reverting hooks back to the previous configuration")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seenHooksIds[hook.ID] = true
|
||||||
|
log.Printf("\tloaded: %s\n", hook.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
hooks = newHooks
|
hooks = newHooks
|
||||||
|
|||||||
Reference in New Issue
Block a user