mirror of
https://github.com/adnanh/webhook.git
synced 2026-07-27 01:29:16 +08:00
Compare commits
97 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec42679305 | |||
| e85e0592dd | |||
| 4d20af8027 | |||
| d4e772c815 | |||
| 04a2b2a680 | |||
| 4914a4131f | |||
| 37698e63b6 | |||
| 80aa9800bf | |||
| f620cb056b | |||
| e55e7efe14 | |||
| 9fa02f7341 | |||
| f59f0a5c84 | |||
| 5594a62f8f | |||
| 642516d46e | |||
| 9cef8ed882 | |||
| 8de1a51926 | |||
| ed9d557a13 | |||
| d2e315d9c6 | |||
| a7aa7f2327 | |||
| 82bba7ba07 | |||
| 8a9ff6d06c | |||
| 3080fd115b | |||
| b314eda1f9 | |||
| 8544653787 | |||
| 8d1498e285 | |||
| 2ed15e35bc | |||
| aff3a155d7 | |||
| 5f041cb9ae | |||
| b7081f3934 | |||
| 2026328c56 | |||
| ea3dbf3438 | |||
| 6774079a57 | |||
| 334fbc814c | |||
| 802f3f572c | |||
| 6da00561bf | |||
| 2e40847eb9 | |||
| 5f7dd7211b | |||
| db25fcd06d | |||
| d1b4ef767a | |||
| 64b3256408 | |||
| bfe4c148a5 | |||
| 033764601d | |||
| 8173f4a346 | |||
| ffd05a2101 | |||
| fea31474bd | |||
| 1c319a7a08 | |||
| 2947e5e0e8 | |||
| 409b441c31 | |||
| 93505b4132 | |||
| c350784507 | |||
| 7b8cc04992 | |||
| d4810bebdb | |||
| 272546bb2b | |||
| 2afc6e6a54 | |||
| a5c92b88a6 | |||
| d12bbf4036 | |||
| d91b34a316 | |||
| 005e723b23 | |||
| 9977fa8c61 | |||
| cbe2440cda | |||
| 9c545a745f | |||
| 263c75b1b5 | |||
| 83cbffd37c | |||
| b310b79fb8 | |||
| f1ebc440a4 | |||
| 10732bd57b | |||
| 4350685330 | |||
| 6053f48b23 | |||
| 6cd8258651 | |||
| fb71ea0fae | |||
| aeacb6dac7 | |||
| 1039151a16 | |||
| db928228c8 | |||
| 6896a34aab | |||
| 5f853d8aba | |||
| 12c48f87cb | |||
| acf38c3210 | |||
| d3f368cb8f | |||
| 943bc258f7 | |||
| 231426da57 | |||
| baec1cadc5 | |||
| 688483d6d1 | |||
| bddb523b67 | |||
| b8807ed434 | |||
| 8527a9b23e | |||
| becd8935be | |||
| 230d16dd93 | |||
| 9a7dedbc09 | |||
| 84ce6f262a | |||
| 7dd55f5232 | |||
| d8a21582a3 | |||
| 8a627f7e67 | |||
| 7635cfde33 | |||
| 2a2a20dcb8 | |||
| 10755eb9d9 | |||
| e9aaeb579e | |||
| 956589fab3 |
@@ -1,3 +1,5 @@
|
|||||||
|
[](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.
|
||||||
|
|
||||||
@@ -9,7 +11,8 @@ If you use Slack, you can set up an "Outgoing webhook integration" to run variou
|
|||||||
1. receive the request,
|
1. receive the request,
|
||||||
2. parse the headers, payload and query variables,
|
2. parse the headers, payload and query variables,
|
||||||
3. check if the specified rules for the hook are satisfied,
|
3. check if the specified rules for the hook are satisfied,
|
||||||
3. and finally, pass the specified arguments to the specified command.
|
3. and finally, pass the specified arguments to the specified command via
|
||||||
|
command line arguments or via environment variables.
|
||||||
|
|
||||||
Everything else is the responsibility of the command's author.
|
Everything else is the responsibility of the command's author.
|
||||||
|
|
||||||
@@ -56,12 +59,23 @@ 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.
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
Any form of contribution is welcome and highly appreciated.
|
Any form of contribution is welcome and highly appreciated.
|
||||||
|
|
||||||
|
Big thanks to [all the current contributors](https://github.com/adnanh/webhook/graphs/contributors) for their 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.
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
@@ -85,3 +99,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
[w]: https://github.com/adnanh/webhook
|
||||||
|
[wc]: https://github.com/adnanh/webhook-contrib
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
package helpers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
|
|
||||||
func CheckPayloadSignature(payload []byte, secret string, signature string) (string, bool) {
|
|
||||||
if strings.HasPrefix(signature, "sha1=") {
|
|
||||||
signature = signature[5:]
|
|
||||||
}
|
|
||||||
|
|
||||||
mac := hmac.New(sha1.New, []byte(secret))
|
|
||||||
mac.Write(payload)
|
|
||||||
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
|
||||||
|
|
||||||
return expectedMAC, hmac.Equal([]byte(signature), []byte(expectedMAC))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValuesToMap converts map[string][]string to a map[string]string object
|
|
||||||
func ValuesToMap(values map[string][]string) map[string]interface{} {
|
|
||||||
ret := make(map[string]interface{})
|
|
||||||
|
|
||||||
for key, value := range values {
|
|
||||||
if len(value) > 0 {
|
|
||||||
ret[key] = value[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtractParameter extracts value from interface{} based on the passed string
|
|
||||||
func ExtractParameter(s string, params interface{}) (string, bool) {
|
|
||||||
if params == nil {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
var p []string
|
|
||||||
|
|
||||||
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
|
|
||||||
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
|
|
||||||
|
|
||||||
if p = strings.SplitN(s, ".", 3); len(p) > 3 {
|
|
||||||
index, err := strconv.ParseInt(p[1], 10, 64)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", false
|
|
||||||
} else if paramsValueSliceLength <= int(index) {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
return ExtractParameter(p[2], params.([]map[string]interface{})[index])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
if p = strings.SplitN(s, ".", 2); len(p) > 1 {
|
|
||||||
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
|
||||||
return ExtractParameter(p[1], pValue)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
|
||||||
return fmt.Sprintf("%v", pValue), true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
+380
-55
@@ -1,26 +1,203 @@
|
|||||||
package hook
|
package hook
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"github.com/adnanh/webhook/helpers"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Constants used to specify the parameter source
|
// Constants used to specify the parameter source
|
||||||
const (
|
const (
|
||||||
SourceHeader string = "header"
|
SourceHeader string = "header"
|
||||||
SourceQuery string = "url"
|
SourceQuery string = "url"
|
||||||
SourcePayload string = "payload"
|
SourcePayload string = "payload"
|
||||||
|
SourceString string = "string"
|
||||||
|
SourceEntirePayload string = "entire-payload"
|
||||||
|
SourceEntireQuery string = "entire-query"
|
||||||
|
SourceEntireHeaders string = "entire-headers"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// EnvNamespace is the prefix used for passing arguments into the command
|
||||||
|
// environment.
|
||||||
|
EnvNamespace string = "HOOK_"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SignatureError describes an invalid payload signature passed to Hook.
|
||||||
|
type SignatureError struct {
|
||||||
|
Signature string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SignatureError) Error() string {
|
||||||
|
if e == nil {
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("invalid payload signature %s", e.Signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArgumentError describes an invalid argument passed to Hook.
|
||||||
|
type ArgumentError struct {
|
||||||
|
Argument Argument
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ArgumentError) Error() string {
|
||||||
|
if e == nil {
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("couldn't retrieve argument for %+v", e.Argument)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SourceError describes an invalid source passed to Hook.
|
||||||
|
type SourceError struct {
|
||||||
|
Argument Argument
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SourceError) Error() string {
|
||||||
|
if e == nil {
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("invalid source for argument %+v", e.Argument)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseError describes an error parsing user input.
|
||||||
|
type ParseError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ParseError) Error() string {
|
||||||
|
if e == nil {
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
return e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
|
||||||
|
func CheckPayloadSignature(payload []byte, secret string, signature string) (string, error) {
|
||||||
|
if strings.HasPrefix(signature, "sha1=") {
|
||||||
|
signature = signature[5:]
|
||||||
|
}
|
||||||
|
|
||||||
|
mac := hmac.New(sha1.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{expectedMAC}
|
||||||
|
}
|
||||||
|
return expectedMAC, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceParameter replaces parameter value with the passed value in the passed map
|
||||||
|
// (please note you should pass pointer to the map, because we're modifying it)
|
||||||
|
// based on the passed string
|
||||||
|
func ReplaceParameter(s string, params interface{}, value interface{}) bool {
|
||||||
|
if params == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
|
||||||
|
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
|
||||||
|
|
||||||
|
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||||
|
index, err := strconv.ParseUint(p[0], 10, 64)
|
||||||
|
|
||||||
|
if err != nil || paramsValueSliceLength <= int(index) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReplaceParameter(p[1], params.([]interface{})[index], value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||||
|
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||||
|
return ReplaceParameter(p[1], pValue, value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, ok := (*params.(*map[string]interface{}))[p[0]]; ok {
|
||||||
|
(*params.(*map[string]interface{}))[p[0]] = value
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParameter extracts interface{} value based on the passed string
|
||||||
|
func GetParameter(s string, params interface{}) (interface{}, bool) {
|
||||||
|
if params == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
|
||||||
|
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
|
||||||
|
|
||||||
|
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||||
|
index, err := strconv.ParseUint(p[0], 10, 64)
|
||||||
|
|
||||||
|
if err != nil || paramsValueSliceLength <= int(index) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetParameter(p[1], params.([]interface{})[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
index, err := strconv.ParseUint(s, 10, 64)
|
||||||
|
|
||||||
|
if err != nil || paramsValueSliceLength <= int(index) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.([]interface{})[index], true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||||
|
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Map {
|
||||||
|
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||||
|
return GetParameter(p[1], pValue)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||||
|
return pValue, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractParameterAsString extracts value from interface{} as string based on the passed string
|
||||||
|
func ExtractParameterAsString(s string, params interface{}) (string, bool) {
|
||||||
|
if pValue, ok := GetParameter(s, params); ok {
|
||||||
|
return fmt.Sprintf("%v", pValue), true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
// 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Argument method returns the value for the Argument's key name
|
// Get Argument method returns the value for the Argument's key name
|
||||||
@@ -35,28 +212,134 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
|
|||||||
source = query
|
source = query
|
||||||
case SourcePayload:
|
case SourcePayload:
|
||||||
source = payload
|
source = payload
|
||||||
|
case SourceString:
|
||||||
|
return ha.Name, true
|
||||||
|
case SourceEntirePayload:
|
||||||
|
r, err := json.Marshal(payload)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(r), true
|
||||||
|
case SourceEntireHeaders:
|
||||||
|
r, err := json.Marshal(headers)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(r), true
|
||||||
|
case SourceEntireQuery:
|
||||||
|
r, err := json.Marshal(query)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(r), true
|
||||||
}
|
}
|
||||||
|
|
||||||
if source != nil {
|
if source != nil {
|
||||||
return helpers.ExtractParameter(ha.Name, *source)
|
return ExtractParameterAsString(ha.Name, *source)
|
||||||
}
|
}
|
||||||
|
|
||||||
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"`
|
||||||
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command"`
|
ResponseHeaders ResponseHeaders `json:"response-headers,omitempty"`
|
||||||
TriggerRule *Rules `json:"trigger-rule"`
|
CaptureCommandOutput bool `json:"include-command-output-in-response,omitempty"`
|
||||||
|
PassEnvironmentToCommand []Argument `json:"pass-environment-to-command,omitempty"`
|
||||||
|
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command,omitempty"`
|
||||||
|
JSONStringParameters []Argument `json:"parse-parameters-as-json,omitempty"`
|
||||||
|
TriggerRule *Rules `json:"trigger-rule,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseJSONParameters decodes specified arguments to JSON objects and replaces the
|
||||||
|
// string with the newly created object
|
||||||
|
func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface{}) error {
|
||||||
|
for i := range h.JSONStringParameters {
|
||||||
|
if arg, ok := h.JSONStringParameters[i].Get(headers, query, payload); ok {
|
||||||
|
var newArg map[string]interface{}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(strings.NewReader(string(arg)))
|
||||||
|
decoder.UseNumber()
|
||||||
|
|
||||||
|
err := decoder.Decode(&newArg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return &ParseError{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
var source *map[string]interface{}
|
||||||
|
|
||||||
|
switch h.JSONStringParameters[i].Source {
|
||||||
|
case SourceHeader:
|
||||||
|
source = headers
|
||||||
|
case SourcePayload:
|
||||||
|
source = payload
|
||||||
|
case SourceQuery:
|
||||||
|
source = query
|
||||||
|
}
|
||||||
|
|
||||||
|
if source != nil {
|
||||||
|
ReplaceParameter(h.JSONStringParameters[i].Name, source, newArg)
|
||||||
|
} else {
|
||||||
|
return &SourceError{h.JSONStringParameters[i]}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return &ArgumentError{h.JSONStringParameters[i]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractCommandArguments creates a list of arguments, based on the
|
// ExtractCommandArguments creates a list of arguments, based on the
|
||||||
// PassArgumentsToCommand property that is ready to be used with exec.Command()
|
// PassArgumentsToCommand property that is ready to be used with exec.Command()
|
||||||
func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) []string {
|
func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) ([]string, error) {
|
||||||
var args = make([]string, 0)
|
var args = make([]string, 0)
|
||||||
|
|
||||||
args = append(args, h.ExecuteCommand)
|
args = append(args, h.ExecuteCommand)
|
||||||
@@ -66,11 +349,28 @@ func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]inter
|
|||||||
args = append(args, arg)
|
args = append(args, arg)
|
||||||
} else {
|
} else {
|
||||||
args = append(args, "")
|
args = append(args, "")
|
||||||
log.Printf("couldn't retrieve argument for %+v\n", h.PassArgumentsToCommand[i])
|
return args, &ArgumentError{h.PassArgumentsToCommand[i]}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return args
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractCommandArgumentsForEnv creates a list of arguments in key=value
|
||||||
|
// format, based on the PassEnvironmentToCommand property that is ready to be used
|
||||||
|
// with exec.Command().
|
||||||
|
func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string]interface{}) ([]string, error) {
|
||||||
|
var args = make([]string, 0)
|
||||||
|
|
||||||
|
for i := range h.PassEnvironmentToCommand {
|
||||||
|
if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok {
|
||||||
|
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg)
|
||||||
|
} else {
|
||||||
|
return args, &ArgumentError{h.PassEnvironmentToCommand[i]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hooks is an array of Hook objects
|
// Hooks is an array of Hook objects
|
||||||
@@ -105,17 +405,34 @@ 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
|
||||||
// it evaluates to
|
// it evaluates to
|
||||||
func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) bool {
|
func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) (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)
|
||||||
@@ -127,58 +444,69 @@ func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, body *[
|
|||||||
return r.Match.Evaluate(headers, query, payload, body)
|
return r.Match.Evaluate(headers, query, payload, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AndRule will evaluate to true if and only if all of the ChildRules evaluate to true
|
// AndRule will evaluate to true if and only if all of the ChildRules evaluate to true
|
||||||
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 {
|
func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) (bool, error) {
|
||||||
res := true
|
res := true
|
||||||
|
|
||||||
for _, v := range r {
|
for _, v := range r {
|
||||||
res = res && v.Evaluate(headers, query, payload, body)
|
rv, err := v.Evaluate(headers, query, payload, body)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res = res && rv
|
||||||
if res == false {
|
if res == false {
|
||||||
return res
|
return res, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OrRule will evaluate to true if any of the ChildRules evaluate to true
|
// OrRule will evaluate to true if any of the ChildRules evaluate to true
|
||||||
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 {
|
func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) (bool, error) {
|
||||||
res := false
|
res := false
|
||||||
|
|
||||||
for _, v := range r {
|
for _, v := range r {
|
||||||
res = res || v.Evaluate(headers, query, payload, body)
|
rv, err := v.Evaluate(headers, query, payload, body)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res = res || rv
|
||||||
if res == true {
|
if res == true {
|
||||||
return res
|
return res, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotRule will evaluate to true if any and only if the ChildRule evaluates to false
|
// NotRule will evaluate to true if any and only if the ChildRule evaluates to false
|
||||||
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 {
|
func (r NotRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) (bool, error) {
|
||||||
return !r.Evaluate(headers, query, payload, body)
|
rv, err := Rules(r).Evaluate(headers, query, payload, body)
|
||||||
|
return !rv, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -189,27 +517,24 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 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 {
|
func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) (bool, error) {
|
||||||
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:
|
||||||
return arg == r.Value
|
return arg == r.Value, nil
|
||||||
case MatchRegex:
|
case MatchRegex:
|
||||||
ok, err := regexp.MatchString(r.Regex, arg)
|
return regexp.MatchString(r.Regex, arg)
|
||||||
if err != nil {
|
|
||||||
log.Printf("error while trying to evaluate regex: %+v", err)
|
|
||||||
}
|
|
||||||
return ok
|
|
||||||
case MatchHashSHA1:
|
case MatchHashSHA1:
|
||||||
expected, ok := helpers.CheckPayloadSignature(*body, r.Secret, arg)
|
_, err := CheckPayloadSignature(*body, r.Secret, arg)
|
||||||
if !ok {
|
return err == nil, err
|
||||||
log.Printf("payload signature mismatch, expected %s got %s", expected, arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
log.Printf("couldn't retrieve argument for %+v\n", r.Parameter)
|
|
||||||
}
|
}
|
||||||
return false
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandStatusResponse type encapsulates the executed command exit code, message, stdout and stderr
|
||||||
|
type CommandStatusResponse struct {
|
||||||
|
ResponseMessage string `json:"message,omitempty"`
|
||||||
|
Output string `json:"output,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,352 @@
|
|||||||
|
package hook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var checkPayloadSignatureTests = []struct {
|
||||||
|
payload []byte
|
||||||
|
secret string
|
||||||
|
signature string
|
||||||
|
mac string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{[]byte(`{"a": "z"}`), "secret", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
||||||
|
{[]byte(`{"a": "z"}`), "secret", "sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
|
||||||
|
// failures
|
||||||
|
{[]byte(`{"a": "z"}`), "secret", "XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
|
||||||
|
{[]byte(`{"a": "z"}`), "secreX", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "900225703e9342328db7307692736e2f7cc7b36e", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckPayloadSignature(t *testing.T) {
|
||||||
|
for _, tt := range checkPayloadSignatureTests {
|
||||||
|
mac, err := CheckPayloadSignature(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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var extractParameterTests = []struct {
|
||||||
|
s string
|
||||||
|
params interface{}
|
||||||
|
value string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"a", map[string]interface{}{"a": "z"}, "z", true},
|
||||||
|
{"a.b", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "z", true},
|
||||||
|
{"a.b.c", map[string]interface{}{"a": map[string]interface{}{"b": map[string]interface{}{"c": "z"}}}, "z", true},
|
||||||
|
{"a.b.0", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "x", true},
|
||||||
|
{"a.1.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "z", true},
|
||||||
|
{"a.1.b.c", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": map[string]interface{}{"c": "y"}}, map[string]interface{}{"b": map[string]interface{}{"c": "z"}}}}, "z", true},
|
||||||
|
// failures
|
||||||
|
{"check_nil", nil, "", false},
|
||||||
|
{"a.X", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "", false}, // non-existent parameter reference
|
||||||
|
{"a.X.c", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-integer slice index
|
||||||
|
{"a.-1.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // negative slice index
|
||||||
|
{"a.500.b", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "", false}, // non-existent slice
|
||||||
|
{"a.501.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-existent slice index
|
||||||
|
{"a.502.b", map[string]interface{}{"a": []interface{}{}}, "", false}, // non-existent slice index
|
||||||
|
{"a.b.503", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "", false}, // trailing, non-existent slice index
|
||||||
|
{"a.b", interface{}("a"), "", false}, // non-map, non-slice input
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractParameter(t *testing.T) {
|
||||||
|
for _, tt := range extractParameterTests {
|
||||||
|
value, ok := ExtractParameterAsString(tt.s, tt.params)
|
||||||
|
if ok != tt.ok || value != tt.value {
|
||||||
|
t.Errorf("failed to extract parameter %q:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, ok:%#v}", tt.s, tt.value, tt.ok, value, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var argumentGetTests = []struct {
|
||||||
|
source, name string
|
||||||
|
headers, query, payload *map[string]interface{}
|
||||||
|
value string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"header", "a", &map[string]interface{}{"a": "z"}, nil, nil, "z", true},
|
||||||
|
{"url", "a", nil, &map[string]interface{}{"a": "z"}, nil, "z", true},
|
||||||
|
{"payload", "a", nil, nil, &map[string]interface{}{"a": "z"}, "z", true},
|
||||||
|
{"string", "a", nil, nil, &map[string]interface{}{"a": "z"}, "a", true},
|
||||||
|
// failures
|
||||||
|
{"header", "a", nil, &map[string]interface{}{"a": "z"}, &map[string]interface{}{"a": "z"}, "", false}, // nil headers
|
||||||
|
{"url", "a", &map[string]interface{}{"a": "z"}, nil, &map[string]interface{}{"a": "z"}, "", false}, // nil query
|
||||||
|
{"payload", "a", &map[string]interface{}{"a": "z"}, &map[string]interface{}{"a": "z"}, nil, "", false}, // nil payload
|
||||||
|
{"foo", "a", &map[string]interface{}{"a": "z"}, nil, nil, "", false}, // invalid source
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgumentGet(t *testing.T) {
|
||||||
|
for _, tt := range argumentGetTests {
|
||||||
|
a := Argument{tt.source, tt.name}
|
||||||
|
value, ok := a.Get(tt.headers, tt.query, tt.payload)
|
||||||
|
if ok != tt.ok || value != tt.value {
|
||||||
|
t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, ok:%#v}", tt.source, tt.name, tt.value, tt.ok, value, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hookParseJSONParametersTests = []struct {
|
||||||
|
params []Argument
|
||||||
|
headers, query, payload *map[string]interface{}
|
||||||
|
rheaders, rquery, rpayload *map[string]interface{}
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{[]Argument{Argument{"header", "a"}}, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, nil, true},
|
||||||
|
{[]Argument{Argument{"url", "a"}}, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
|
||||||
|
{[]Argument{Argument{"payload", "a"}}, nil, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
|
||||||
|
{[]Argument{Argument{"header", "z"}}, &map[string]interface{}{"z": `{}`}, nil, nil, &map[string]interface{}{"z": map[string]interface{}{}}, nil, nil, true},
|
||||||
|
// failures
|
||||||
|
{[]Argument{Argument{"header", "z"}}, &map[string]interface{}{"z": ``}, nil, nil, &map[string]interface{}{"z": ``}, nil, nil, false}, // empty string
|
||||||
|
{[]Argument{Argument{"header", "y"}}, &map[string]interface{}{"X": `{}`}, nil, nil, &map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter
|
||||||
|
{[]Argument{Argument{"string", "z"}}, &map[string]interface{}{"z": ``}, nil, nil, &map[string]interface{}{"z": ``}, nil, nil, false}, // invalid argument source
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookParseJSONParameters(t *testing.T) {
|
||||||
|
for _, tt := range hookParseJSONParametersTests {
|
||||||
|
h := &Hook{JSONStringParameters: tt.params}
|
||||||
|
err := h.ParseJSONParameters(tt.headers, tt.query, tt.payload)
|
||||||
|
if (err == nil) != tt.ok || !reflect.DeepEqual(tt.headers, tt.rheaders) {
|
||||||
|
t.Errorf("failed to parse %v:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.params, *tt.rheaders, tt.ok, *tt.headers, (err == nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hookExtractCommandArgumentsTests = []struct {
|
||||||
|
exec string
|
||||||
|
args []Argument
|
||||||
|
headers, query, payload *map[string]interface{}
|
||||||
|
value []string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"test", []Argument{Argument{"header", "a"}}, &map[string]interface{}{"a": "z"}, nil, nil, []string{"test", "z"}, true},
|
||||||
|
// failures
|
||||||
|
{"fail", []Argument{Argument{"payload", "a"}}, &map[string]interface{}{"a": "z"}, nil, nil, []string{"fail", ""}, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookExtractCommandArguments(t *testing.T) {
|
||||||
|
for _, tt := range hookExtractCommandArgumentsTests {
|
||||||
|
h := &Hook{ExecuteCommand: tt.exec, PassArgumentsToCommand: tt.args}
|
||||||
|
value, err := h.ExtractCommandArguments(tt.headers, tt.query, tt.payload)
|
||||||
|
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
|
||||||
|
t.Errorf("failed to extract args {cmd=%q, args=%v}:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.exec, tt.args, tt.value, tt.ok, value, (err == nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hooksLoadFromFileTests = []struct {
|
||||||
|
path string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"../hooks.json.example", true},
|
||||||
|
{"", true},
|
||||||
|
// failures
|
||||||
|
{"missing.json", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHooksLoadFromFile(t *testing.T) {
|
||||||
|
for _, tt := range hooksLoadFromFileTests {
|
||||||
|
h := &Hooks{}
|
||||||
|
err := h.LoadFromFile(tt.path)
|
||||||
|
if (err == nil) != tt.ok {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hooksMatchTests = []struct {
|
||||||
|
id string
|
||||||
|
hooks Hooks
|
||||||
|
value *Hook
|
||||||
|
}{
|
||||||
|
{"a", Hooks{Hook{ID: "a"}}, &Hook{ID: "a"}},
|
||||||
|
{"X", Hooks{Hook{ID: "a"}}, new(Hook)},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHooksMatch(t *testing.T) {
|
||||||
|
for _, tt := range hooksMatchTests {
|
||||||
|
value := tt.hooks.Match(tt.id)
|
||||||
|
if reflect.DeepEqual(reflect.ValueOf(value), reflect.ValueOf(tt.value)) {
|
||||||
|
t.Errorf("failed to match %q:\nexpected %#v,\ngot %#v", tt.id, tt.value, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchRuleTests = []struct {
|
||||||
|
typ, regex, secret, value string
|
||||||
|
param Argument
|
||||||
|
headers, query, payload *map[string]interface{}
|
||||||
|
body []byte
|
||||||
|
ok bool
|
||||||
|
err bool
|
||||||
|
}{
|
||||||
|
{"value", "", "", "z", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, true, false},
|
||||||
|
{"regex", "^z", "", "z", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, true, false},
|
||||||
|
{"payload-hash-sha1", "", "secret", "", Argument{"header", "a"}, &map[string]interface{}{"a": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), true, false},
|
||||||
|
// failures
|
||||||
|
{"value", "", "", "X", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, false},
|
||||||
|
{"regex", "^X", "", "", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, false},
|
||||||
|
{"value", "", "2", "X", Argument{"header", "a"}, &map[string]interface{}{"y": "z"}, nil, nil, []byte{}, false, false}, // reference invalid header
|
||||||
|
// errors
|
||||||
|
{"regex", "*", "", "", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, true}, // invalid regex
|
||||||
|
{"payload-hash-sha1", "", "secret", "", Argument{"header", "a"}, &map[string]interface{}{"a": ""}, nil, nil, []byte{}, false, true}, // invalid hmac
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchRule(t *testing.T) {
|
||||||
|
for i, tt := range matchRuleTests {
|
||||||
|
r := MatchRule{tt.typ, tt.regex, tt.secret, tt.value, tt.param}
|
||||||
|
ok, err := r.Evaluate(tt.headers, tt.query, tt.payload, &tt.body)
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var andRuleTests = []struct {
|
||||||
|
desc string // description of the test case
|
||||||
|
rule AndRule
|
||||||
|
headers, query, payload *map[string]interface{}
|
||||||
|
body []byte
|
||||||
|
ok bool
|
||||||
|
err bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"(a=z, b=y): a=z && b=y",
|
||||||
|
AndRule{
|
||||||
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||||
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||||
|
},
|
||||||
|
&map[string]interface{}{"a": "z", "b": "y"}, nil, nil, []byte{},
|
||||||
|
true, false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"(a=z, b=Y): a=z && b=y",
|
||||||
|
AndRule{
|
||||||
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||||
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||||
|
},
|
||||||
|
&map[string]interface{}{"a": "z", "b": "Y"}, nil, nil, []byte{},
|
||||||
|
false, false,
|
||||||
|
},
|
||||||
|
// Complex test to cover Rules.Evaluate
|
||||||
|
{
|
||||||
|
"(a=z, b=y, c=x, d=w=, e=X, f=X): a=z && (b=y && c=x) && (d=w || e=v) && !f=u",
|
||||||
|
AndRule{
|
||||||
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||||
|
{
|
||||||
|
And: &AndRule{
|
||||||
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||||
|
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Or: &OrRule{
|
||||||
|
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d"}}},
|
||||||
|
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Not: &NotRule{
|
||||||
|
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{},
|
||||||
|
true, false,
|
||||||
|
},
|
||||||
|
{"empty rule", AndRule{{}}, nil, nil, nil, nil, false, false},
|
||||||
|
// failures
|
||||||
|
{
|
||||||
|
"invalid rule",
|
||||||
|
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a"}}}},
|
||||||
|
&map[string]interface{}{"y": "z"}, nil, nil, nil,
|
||||||
|
false, false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAndRule(t *testing.T) {
|
||||||
|
for _, tt := range andRuleTests {
|
||||||
|
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body)
|
||||||
|
if ok != tt.ok || (err != nil) != tt.err {
|
||||||
|
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.desc, tt.ok, tt.err, ok, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var orRuleTests = []struct {
|
||||||
|
desc string // description of the test case
|
||||||
|
rule OrRule
|
||||||
|
headers, query, payload *map[string]interface{}
|
||||||
|
body []byte
|
||||||
|
ok bool
|
||||||
|
err bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"(a=z, b=X): a=z || b=y",
|
||||||
|
OrRule{
|
||||||
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||||
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||||
|
},
|
||||||
|
&map[string]interface{}{"a": "z", "b": "X"}, nil, nil, []byte{},
|
||||||
|
true, false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"(a=X, b=y): a=z || b=y",
|
||||||
|
OrRule{
|
||||||
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||||
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||||
|
},
|
||||||
|
&map[string]interface{}{"a": "X", "b": "y"}, nil, nil, []byte{},
|
||||||
|
true, false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"(a=Z, b=Y): a=z || b=y",
|
||||||
|
OrRule{
|
||||||
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||||
|
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b"}}},
|
||||||
|
},
|
||||||
|
&map[string]interface{}{"a": "Z", "b": "Y"}, nil, nil, []byte{},
|
||||||
|
false, false,
|
||||||
|
},
|
||||||
|
// failures
|
||||||
|
{
|
||||||
|
"invalid rule",
|
||||||
|
OrRule{
|
||||||
|
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||||
|
},
|
||||||
|
&map[string]interface{}{"y": "Z"}, nil, nil, []byte{},
|
||||||
|
false, false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOrRule(t *testing.T) {
|
||||||
|
for _, tt := range orRuleTests {
|
||||||
|
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var notRuleTests = []struct {
|
||||||
|
desc string // description of the test case
|
||||||
|
rule NotRule
|
||||||
|
headers, query, payload *map[string]interface{}
|
||||||
|
body []byte
|
||||||
|
ok bool
|
||||||
|
err bool
|
||||||
|
}{
|
||||||
|
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a"}}}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, true, false},
|
||||||
|
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotRule(t *testing.T) {
|
||||||
|
for _, tt := range notRuleTests {
|
||||||
|
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body)
|
||||||
|
if ok != tt.ok || (err != nil) != tt.err {
|
||||||
|
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.rule, tt.ok, tt.err, ok, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,14 @@
|
|||||||
"id": "webhook",
|
"id": "webhook",
|
||||||
"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-headers":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Access-Control-Allow-Origin",
|
||||||
|
"value": "*"
|
||||||
|
}
|
||||||
|
],
|
||||||
"pass-arguments-to-command":
|
"pass-arguments-to-command":
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupSignals() {
|
||||||
|
log.Printf("setting up os signal watcher\n")
|
||||||
|
|
||||||
|
signals = make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signals, syscall.SIGUSR1)
|
||||||
|
|
||||||
|
go watchForSignals()
|
||||||
|
}
|
||||||
|
|
||||||
|
func watchForSignals() {
|
||||||
|
log.Println("os signal watcher ready")
|
||||||
|
|
||||||
|
for {
|
||||||
|
sig := <-signals
|
||||||
|
if sig == syscall.SIGUSR1 {
|
||||||
|
log.Println("caught USR1 signal")
|
||||||
|
|
||||||
|
reloadHooks()
|
||||||
|
} else {
|
||||||
|
log.Printf("caught unhandled signal %+v\n", sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func setupSignals() {
|
||||||
|
// NOOP: Windows doesn't have signals equivalent to the Unix world.
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// Hook Echo is a simply utility used for testing the Webhook package.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) > 1 {
|
||||||
|
fmt.Printf("arg: %s\n", strings.Join(os.Args[1:], " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
var env []string
|
||||||
|
for _, v := range os.Environ() {
|
||||||
|
if strings.HasPrefix(v, "HOOK_") {
|
||||||
|
env = append(env, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(env) > 0 {
|
||||||
|
fmt.Printf("env: %s\n", strings.Join(env, " "))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "github",
|
||||||
|
"execute-command": "{{ .Hookecho }}",
|
||||||
|
"command-working-directory": "/",
|
||||||
|
"include-command-output-in-response": true,
|
||||||
|
"pass-environment-to-command":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"source": "payload",
|
||||||
|
"name": "head_commit.timestamp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pass-arguments-to-command":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"source": "payload",
|
||||||
|
"name": "head_commit.id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "payload",
|
||||||
|
"name": "head_commit.author.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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bitbucket",
|
||||||
|
"execute-command": "{{ .Hookecho }}",
|
||||||
|
"command-working-directory": "/",
|
||||||
|
"include-command-output-in-response": true,
|
||||||
|
"response-message": "success",
|
||||||
|
"parse-parameters-as-json": [
|
||||||
|
{
|
||||||
|
"source": "payload",
|
||||||
|
"name": "payload"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trigger-rule": {
|
||||||
|
"and": [
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"type": "value",
|
||||||
|
"parameter": {
|
||||||
|
"source": "payload",
|
||||||
|
"name": "payload.canon_url"
|
||||||
|
},
|
||||||
|
"value": "https://bitbucket.org"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"type": "value",
|
||||||
|
"parameter": {
|
||||||
|
"source": "payload",
|
||||||
|
"name": "payload.repository.absolute_url"
|
||||||
|
},
|
||||||
|
"value": "/webhook/testing/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"type": "value",
|
||||||
|
"parameter": {
|
||||||
|
"source": "payload",
|
||||||
|
"name": "payload.commits.0.branch"
|
||||||
|
},
|
||||||
|
"value": "master"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gitlab",
|
||||||
|
"execute-command": "{{ .Hookecho }}",
|
||||||
|
"command-working-directory": "/",
|
||||||
|
"response-message": "success",
|
||||||
|
"include-command-output-in-response": true,
|
||||||
|
"pass-arguments-to-command":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"source": "payload",
|
||||||
|
"name": "commits.0.id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "payload",
|
||||||
|
"name": "user_name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "payload",
|
||||||
|
"name": "user_email"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trigger-rule":
|
||||||
|
{
|
||||||
|
"match":
|
||||||
|
{
|
||||||
|
"type": "value",
|
||||||
|
"value": "refs/heads/master",
|
||||||
|
"parameter":
|
||||||
|
{
|
||||||
|
"source": "payload",
|
||||||
|
"name": "ref"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
+150
-53
@@ -12,7 +12,6 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/adnanh/webhook/helpers"
|
|
||||||
"github.com/adnanh/webhook/hook"
|
"github.com/adnanh/webhook/hook"
|
||||||
|
|
||||||
"github.com/codegangsta/negroni"
|
"github.com/codegangsta/negroni"
|
||||||
@@ -22,13 +21,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
version = "2.2.2"
|
version = "2.3.8"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ip = flag.String("ip", "", "ip the webhook should serve hooks on")
|
ip = flag.String("ip", "", "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")
|
||||||
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")
|
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)")
|
||||||
@@ -36,14 +36,19 @@ 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
|
||||||
|
|
||||||
hooks hook.Hooks
|
hooks hook.Hooks
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
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] ")
|
||||||
@@ -55,12 +60,20 @@ func init() {
|
|||||||
|
|
||||||
log.Println("version " + version + " starting")
|
log.Println("version " + version + " starting")
|
||||||
|
|
||||||
|
// set os signal watcher
|
||||||
|
setupSignals()
|
||||||
|
|
||||||
// load and parse hooks
|
// load and parse hooks
|
||||||
log.Printf("attempting to load hooks from %s\n", *hooksFilePath)
|
log.Printf("attempting to load hooks from %s\n", *hooksFilePath)
|
||||||
|
|
||||||
err := hooks.LoadFromFile(*hooksFilePath)
|
err := hooks.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 {
|
||||||
log.Printf("loaded %d hook(s) from file\n", len(hooks))
|
log.Printf("loaded %d hook(s) from file\n", len(hooks))
|
||||||
@@ -69,9 +82,7 @@ func init() {
|
|||||||
log.Printf("\t> %s\n", hook.ID)
|
log.Printf("\t> %s\n", hook.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if *hotReload {
|
if *hotReload {
|
||||||
// set up file watcher
|
// set up file watcher
|
||||||
log.Printf("setting up file watcher for %s\n", *hooksFilePath)
|
log.Printf("setting up file watcher for %s\n", *hooksFilePath)
|
||||||
@@ -93,18 +104,17 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
l := log.New(os.Stdout, "[webhook] ", log.Ldate|log.Ltime)
|
l := negroni.NewLogger()
|
||||||
|
l.Logger = log.New(os.Stderr, "[webhook] ", log.Ldate|log.Ltime)
|
||||||
negroniLogger := &negroni.Logger{l}
|
|
||||||
|
|
||||||
negroniRecovery := &negroni.Recovery{
|
negroniRecovery := &negroni.Recovery{
|
||||||
Logger: l,
|
Logger: l.Logger,
|
||||||
PrintStack: true,
|
PrintStack: true,
|
||||||
StackAll: false,
|
StackAll: false,
|
||||||
StackSize: 1024 * 8,
|
StackSize: 1024 * 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
n := negroni.New(negroniRecovery, negroniLogger)
|
n := negroni.New(negroniRecovery, l)
|
||||||
|
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
|
|
||||||
@@ -131,12 +141,16 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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"]
|
||||||
|
|
||||||
hook := hooks.Match(id)
|
matchedHooks := hooks.MatchAll(id)
|
||||||
|
|
||||||
if hook != nil {
|
if matchedHooks != nil {
|
||||||
log.Printf("%s got matched\n", id)
|
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 {
|
||||||
@@ -144,17 +158,17 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parse headers
|
// parse headers
|
||||||
headers := helpers.ValuesToMap(r.Header)
|
headers := valuesToMap(r.Header)
|
||||||
|
|
||||||
// parse query variables
|
// parse query variables
|
||||||
query := helpers.ValuesToMap(r.URL.Query())
|
query := valuesToMap(r.URL.Query())
|
||||||
|
|
||||||
// parse body
|
// parse body
|
||||||
var payload map[string]interface{}
|
var payload map[string]interface{}
|
||||||
|
|
||||||
contentType := r.Header.Get("Content-Type")
|
contentType := r.Header.Get("Content-Type")
|
||||||
|
|
||||||
if contentType == "application/json" {
|
if strings.Contains(contentType, "json") {
|
||||||
decoder := json.NewDecoder(strings.NewReader(string(body)))
|
decoder := json.NewDecoder(strings.NewReader(string(body)))
|
||||||
decoder.UseNumber()
|
decoder.UseNumber()
|
||||||
|
|
||||||
@@ -163,46 +177,133 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error parsing JSON payload %+v\n", err)
|
log.Printf("error parsing JSON payload %+v\n", err)
|
||||||
}
|
}
|
||||||
} else if contentType == "application/x-www-form-urlencoded" {
|
} else if strings.Contains(contentType, "form") {
|
||||||
fd, err := url.ParseQuery(string(body))
|
fd, err := url.ParseQuery(string(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error parsing form payload %+v\n", err)
|
log.Printf("error parsing form payload %+v\n", err)
|
||||||
} else {
|
} else {
|
||||||
payload = helpers.ValuesToMap(fd)
|
payload = valuesToMap(fd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle hook
|
// handle hook
|
||||||
go handleHook(hook, &headers, &query, &payload, &body)
|
for _, h := range matchedHooks {
|
||||||
|
|
||||||
// send the hook defined response message
|
err := h.ParseJSONParameters(&headers, &query, &payload)
|
||||||
fmt.Fprintf(w, hook.ResponseMessage)
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("error parsing JSON: %s", err)
|
||||||
|
log.Printf(msg)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprintf(w, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
if h.TriggerRule == nil {
|
||||||
|
ok = true
|
||||||
|
} else {
|
||||||
|
ok, err = h.TriggerRule.Evaluate(&headers, &query, &payload, &body)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("error evaluating hook: %s", err)
|
||||||
|
log.Printf(msg)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprintf(w, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
log.Printf("%s hook triggered successfully\n", h.ID)
|
||||||
|
|
||||||
|
for _, responseHeader := range h.ResponseHeaders {
|
||||||
|
w.Header().Set(responseHeader.Name, responseHeader.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.CaptureCommandOutput {
|
||||||
|
response := handleHook(h, &headers, &query, &payload, &body)
|
||||||
|
fmt.Fprintf(w, response)
|
||||||
|
} else {
|
||||||
|
go handleHook(h, &headers, &query, &payload, &body)
|
||||||
|
fmt.Fprintf(w, h.ResponseMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "Hook rules were not satisfied.")
|
||||||
} else {
|
} else {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
fmt.Fprintf(w, "Hook not found.")
|
fmt.Fprintf(w, "Hook not found.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleHook(hook *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) {
|
func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) string {
|
||||||
if hook.TriggerRule == nil || hook.TriggerRule != nil && hook.TriggerRule.Evaluate(headers, query, payload, body) {
|
var err error
|
||||||
log.Printf("%s hook triggered successfully\n", hook.ID)
|
|
||||||
|
|
||||||
cmd := exec.Command(hook.ExecuteCommand)
|
cmd := exec.Command(h.ExecuteCommand)
|
||||||
cmd.Args = hook.ExtractCommandArguments(headers, query, payload)
|
cmd.Dir = h.CommandWorkingDirectory
|
||||||
cmd.Dir = hook.CommandWorkingDirectory
|
|
||||||
|
|
||||||
log.Printf("executing %s (%s) with arguments %s using %s as cwd\n", hook.ExecuteCommand, cmd.Path, cmd.Args, cmd.Dir)
|
cmd.Args, err = h.ExtractCommandArguments(headers, query, payload)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error extracting command arguments: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
out, err := cmd.Output()
|
var envs []string
|
||||||
|
envs, err = h.ExtractCommandArgumentsForEnv(headers, query, payload)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error extracting command arguments for environment: %s", err)
|
||||||
|
}
|
||||||
|
cmd.Env = append(os.Environ(), envs...)
|
||||||
|
|
||||||
log.Printf("stdout: %s\n", out)
|
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)
|
||||||
|
|
||||||
if err != nil {
|
out, err := cmd.CombinedOutput()
|
||||||
log.Printf("stderr: %+v\n", err)
|
|
||||||
}
|
log.Printf("command output: %s\n", out)
|
||||||
log.Printf("finished handling %s\n", hook.ID)
|
|
||||||
|
var errorResponse string
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error occurred: %+v\n", err)
|
||||||
|
errorResponse = fmt.Sprintf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("finished handling %s\n", h.ID)
|
||||||
|
|
||||||
|
var response []byte
|
||||||
|
response, err = json.Marshal(&hook.CommandStatusResponse{ResponseMessage: h.ResponseMessage, Output: string(out), Error: errorResponse})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error marshalling response: %+v", err)
|
||||||
|
return h.ResponseMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadHooks() {
|
||||||
|
newHooks := hook.Hooks{}
|
||||||
|
|
||||||
|
// parse and swap
|
||||||
|
log.Printf("attempting to reload hooks from %s\n", *hooksFilePath)
|
||||||
|
|
||||||
|
err := newHooks.LoadFromFile(*hooksFilePath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("couldn't load hooks from file! %+v\n", err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("%s hook did not get triggered\n", hook.ID)
|
log.Printf("loaded %d hook(s) from file\n", len(hooks))
|
||||||
|
|
||||||
|
for _, hook := range hooks {
|
||||||
|
log.Printf("\t> %s\n", hook.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
hooks = newHooks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,27 +314,23 @@ func watchForFileChange() {
|
|||||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||||
log.Println("hooks file modified")
|
log.Println("hooks file modified")
|
||||||
|
|
||||||
newHooks := hook.Hooks{}
|
reloadHooks()
|
||||||
|
|
||||||
// parse and swap
|
|
||||||
log.Printf("attempting to reload hooks from %s\n", *hooksFilePath)
|
|
||||||
|
|
||||||
err := newHooks.LoadFromFile(*hooksFilePath)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("couldn't load hooks from file! %+v\n", err)
|
|
||||||
} else {
|
|
||||||
log.Printf("loaded %d hook(s) from file\n", len(hooks))
|
|
||||||
|
|
||||||
for _, hook := range hooks {
|
|
||||||
log.Printf("\t> %s\n", hook.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
hooks = newHooks
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case err := <-(*watcher).Errors:
|
case err := <-(*watcher).Errors:
|
||||||
log.Println("watcher error:", err)
|
log.Println("watcher error:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// valuesToMap converts map[string][]string to a map[string]string object
|
||||||
|
func valuesToMap(values map[string][]string) map[string]interface{} {
|
||||||
|
ret := make(map[string]interface{})
|
||||||
|
|
||||||
|
for key, value := range values {
|
||||||
|
if len(value) > 0 {
|
||||||
|
ret[key] = value[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|||||||
+513
@@ -0,0 +1,513 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/adnanh/webhook/hook"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWebhook(t *testing.T) {
|
||||||
|
hookecho, cleanupHookecho := buildHookecho(t)
|
||||||
|
defer cleanupHookecho()
|
||||||
|
|
||||||
|
config, cleanupConfig := genConfig(t, hookecho)
|
||||||
|
defer cleanupConfig()
|
||||||
|
|
||||||
|
webhook, cleanupWebhook := buildWebhook(t)
|
||||||
|
defer cleanupWebhook()
|
||||||
|
|
||||||
|
ip, port := serverAddress(t)
|
||||||
|
args := []string{fmt.Sprintf("-hooks=%s", config), fmt.Sprintf("-ip=%s", ip), fmt.Sprintf("-port=%s", port), "-verbose"}
|
||||||
|
|
||||||
|
cmd := exec.Command(webhook, args...)
|
||||||
|
//cmd.Stderr = os.Stderr // uncomment to see verbose output
|
||||||
|
cmd.Env = webhookEnv()
|
||||||
|
cmd.Args[0] = "webhook"
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
t.Fatalf("failed to start webhook: %s", err)
|
||||||
|
}
|
||||||
|
defer killAndWait(cmd)
|
||||||
|
|
||||||
|
waitForServerReady(t, ip, port)
|
||||||
|
|
||||||
|
for _, tt := range hookHandlerTests {
|
||||||
|
url := fmt.Sprintf("http://%s:%s/hooks/%s", ip, port, tt.id)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, ioutil.NopCloser(strings.NewReader(tt.body)))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("New request failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range tt.headers {
|
||||||
|
req.Header.Add(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
var res *http.Response
|
||||||
|
|
||||||
|
if tt.urlencoded == true {
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
} else {
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
res, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("client.Do failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(res.Body)
|
||||||
|
res.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("POST %q: failed to ready body: %s", tt.desc, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode != tt.respStatus || string(body) != tt.respBody {
|
||||||
|
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()) {
|
||||||
|
tmp, err := ioutil.TempDir("", "hookecho-test-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cleanup == nil {
|
||||||
|
os.RemoveAll(tmp)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
bin = filepath.Join(tmp, "hookecho")
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
bin += ".exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("go", "build", "-o", bin, "test/hookecho.go")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
t.Fatalf("Building hookecho: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bin, func() { os.RemoveAll(tmp) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func genConfig(t *testing.T, bin string) (config string, cleanup func()) {
|
||||||
|
tmpl := template.Must(template.ParseFiles("test/hooks.json.tmpl"))
|
||||||
|
|
||||||
|
tmp, err := ioutil.TempDir("", "webhook-config-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cleanup == nil {
|
||||||
|
os.RemoveAll(tmp)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
path := filepath.Join(tmp, "hooks.json")
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Creating config template: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
data := struct{ Hookecho string }{filepath.ToSlash(bin)}
|
||||||
|
if err := tmpl.Execute(file, data); err != nil {
|
||||||
|
t.Fatalf("Executing template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, func() { os.RemoveAll(tmp) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildWebhook(t *testing.T) (bin string, cleanup func()) {
|
||||||
|
tmp, err := ioutil.TempDir("", "webhook-test-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cleanup == nil {
|
||||||
|
os.RemoveAll(tmp)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
bin = filepath.Join(tmp, "webhook")
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
bin += ".exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("go", "build", "-o", bin)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
t.Fatalf("Building webhook: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bin, func() { os.RemoveAll(tmp) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverAddress(t *testing.T) (string, string) {
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
ln, err = net.Listen("tcp6", "[::1]:0")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
host, port, err := net.SplitHostPort(ln.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to split network address: %v", err)
|
||||||
|
}
|
||||||
|
return host, port
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForServerReady(t *testing.T, ip, port string) {
|
||||||
|
waitForServer(t,
|
||||||
|
fmt.Sprintf("http://%v:%v/", ip, port),
|
||||||
|
http.StatusNotFound,
|
||||||
|
5*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pollInterval = 200 * time.Millisecond
|
||||||
|
|
||||||
|
func waitForServer(t *testing.T, url string, status int, timeout time.Duration) {
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
time.Sleep(pollInterval)
|
||||||
|
res, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if res.StatusCode == status {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("Server failed to respond in %v", timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func killAndWait(cmd *exec.Cmd) {
|
||||||
|
cmd.Process.Kill()
|
||||||
|
cmd.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// webhookEnv returns the process environment without any existing hook
|
||||||
|
// namespace variables.
|
||||||
|
func webhookEnv() (env []string) {
|
||||||
|
for _, v := range os.Environ() {
|
||||||
|
if strings.HasPrefix(v, hook.EnvNamespace) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
env = append(env, v)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var hookHandlerTests = []struct {
|
||||||
|
desc string
|
||||||
|
id string
|
||||||
|
headers map[string]string
|
||||||
|
body string
|
||||||
|
urlencoded bool
|
||||||
|
|
||||||
|
respStatus int
|
||||||
|
respBody string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"github",
|
||||||
|
"github",
|
||||||
|
map[string]string{"X-Hub-Signature": "f68df0375d7b03e3eb29b4cf9f9ec12e08f42ff8"},
|
||||||
|
`{
|
||||||
|
"after":"1481a2de7b2a7d02428ad93446ab166be7793fbb",
|
||||||
|
"before":"17c497ccc7cca9c2f735aa07e9e3813060ce9a6a",
|
||||||
|
"commits":[
|
||||||
|
{
|
||||||
|
"added":[
|
||||||
|
|
||||||
|
],
|
||||||
|
"author":{
|
||||||
|
"email":"lolwut@noway.biz",
|
||||||
|
"name":"Garen Torikian",
|
||||||
|
"username":"octokitty"
|
||||||
|
},
|
||||||
|
"committer":{
|
||||||
|
"email":"lolwut@noway.biz",
|
||||||
|
"name":"Garen Torikian",
|
||||||
|
"username":"octokitty"
|
||||||
|
},
|
||||||
|
"distinct":true,
|
||||||
|
"id":"c441029cf673f84c8b7db52d0a5944ee5c52ff89",
|
||||||
|
"message":"Test",
|
||||||
|
"modified":[
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"removed":[
|
||||||
|
|
||||||
|
],
|
||||||
|
"timestamp":"2013-02-22T13:50:07-08:00",
|
||||||
|
"url":"https://github.com/octokitty/testing/commit/c441029cf673f84c8b7db52d0a5944ee5c52ff89"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"added":[
|
||||||
|
|
||||||
|
],
|
||||||
|
"author":{
|
||||||
|
"email":"lolwut@noway.biz",
|
||||||
|
"name":"Garen Torikian",
|
||||||
|
"username":"octokitty"
|
||||||
|
},
|
||||||
|
"committer":{
|
||||||
|
"email":"lolwut@noway.biz",
|
||||||
|
"name":"Garen Torikian",
|
||||||
|
"username":"octokitty"
|
||||||
|
},
|
||||||
|
"distinct":true,
|
||||||
|
"id":"36c5f2243ed24de58284a96f2a643bed8c028658",
|
||||||
|
"message":"This is me testing the windows client.",
|
||||||
|
"modified":[
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"removed":[
|
||||||
|
|
||||||
|
],
|
||||||
|
"timestamp":"2013-02-22T14:07:13-08:00",
|
||||||
|
"url":"https://github.com/octokitty/testing/commit/36c5f2243ed24de58284a96f2a643bed8c028658"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"added":[
|
||||||
|
"words/madame-bovary.txt"
|
||||||
|
],
|
||||||
|
"author":{
|
||||||
|
"email":"lolwut@noway.biz",
|
||||||
|
"name":"Garen Torikian",
|
||||||
|
"username":"octokitty"
|
||||||
|
},
|
||||||
|
"committer":{
|
||||||
|
"email":"lolwut@noway.biz",
|
||||||
|
"name":"Garen Torikian",
|
||||||
|
"username":"octokitty"
|
||||||
|
},
|
||||||
|
"distinct":true,
|
||||||
|
"id":"1481a2de7b2a7d02428ad93446ab166be7793fbb",
|
||||||
|
"message":"Rename madame-bovary.txt to words/madame-bovary.txt",
|
||||||
|
"modified":[
|
||||||
|
|
||||||
|
],
|
||||||
|
"removed":[
|
||||||
|
"madame-bovary.txt"
|
||||||
|
],
|
||||||
|
"timestamp":"2013-03-12T08:14:29-07:00",
|
||||||
|
"url":"https://github.com/octokitty/testing/commit/1481a2de7b2a7d02428ad93446ab166be7793fbb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compare":"https://github.com/octokitty/testing/compare/17c497ccc7cc...1481a2de7b2a",
|
||||||
|
"created":false,
|
||||||
|
"deleted":false,
|
||||||
|
"forced":false,
|
||||||
|
"head_commit":{
|
||||||
|
"added":[
|
||||||
|
"words/madame-bovary.txt"
|
||||||
|
],
|
||||||
|
"author":{
|
||||||
|
"email":"lolwut@noway.biz",
|
||||||
|
"name":"Garen Torikian",
|
||||||
|
"username":"octokitty"
|
||||||
|
},
|
||||||
|
"committer":{
|
||||||
|
"email":"lolwut@noway.biz",
|
||||||
|
"name":"Garen Torikian",
|
||||||
|
"username":"octokitty"
|
||||||
|
},
|
||||||
|
"distinct":true,
|
||||||
|
"id":"1481a2de7b2a7d02428ad93446ab166be7793fbb",
|
||||||
|
"message":"Rename madame-bovary.txt to words/madame-bovary.txt",
|
||||||
|
"modified":[
|
||||||
|
|
||||||
|
],
|
||||||
|
"removed":[
|
||||||
|
"madame-bovary.txt"
|
||||||
|
],
|
||||||
|
"timestamp":"2013-03-12T08:14:29-07:00",
|
||||||
|
"url":"https://github.com/octokitty/testing/commit/1481a2de7b2a7d02428ad93446ab166be7793fbb"
|
||||||
|
},
|
||||||
|
"pusher":{
|
||||||
|
"email":"lolwut@noway.biz",
|
||||||
|
"name":"Garen Torikian"
|
||||||
|
},
|
||||||
|
"ref":"refs/heads/master",
|
||||||
|
"repository":{
|
||||||
|
"created_at":1332977768,
|
||||||
|
"description":"",
|
||||||
|
"fork":false,
|
||||||
|
"forks":0,
|
||||||
|
"has_downloads":true,
|
||||||
|
"has_issues":true,
|
||||||
|
"has_wiki":true,
|
||||||
|
"homepage":"",
|
||||||
|
"id":3860742,
|
||||||
|
"language":"Ruby",
|
||||||
|
"master_branch":"master",
|
||||||
|
"name":"testing",
|
||||||
|
"open_issues":2,
|
||||||
|
"owner":{
|
||||||
|
"email":"lolwut@noway.biz",
|
||||||
|
"name":"octokitty"
|
||||||
|
},
|
||||||
|
"private":false,
|
||||||
|
"pushed_at":1363295520,
|
||||||
|
"size":2156,
|
||||||
|
"stargazers":1,
|
||||||
|
"url":"https://github.com/octokitty/testing",
|
||||||
|
"watchers":1
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
false,
|
||||||
|
http.StatusOK,
|
||||||
|
`{"output":"arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz\nenv: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00\n"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bitbucket", // bitbucket sends their payload using uriencoded params.
|
||||||
|
"bitbucket",
|
||||||
|
nil,
|
||||||
|
`payload={"canon_url": "https://bitbucket.org","commits": [{"author": "marcus","branch": "master","files": [{"file": "somefile.py","type": "modified"}],"message": "Added some more things to somefile.py\n","node": "620ade18607a","parents": ["702c70160afc"],"raw_author": "Marcus Bertrand <marcus@somedomain.com>","raw_node": "620ade18607ac42d872b568bb92acaa9a28620e9","revision": null,"size": -1,"timestamp": "2012-05-30 05:58:56","utctimestamp": "2014-11-07 15:19:02+00:00"}],"repository": {"absolute_url": "/webhook/testing/","fork": false,"is_private": true,"name": "Project X","owner": "marcus","scm": "git","slug": "project-x","website": "https://atlassian.com/"},"user": "marcus"}`,
|
||||||
|
true,
|
||||||
|
http.StatusOK,
|
||||||
|
`{"message":"success"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"gitlab",
|
||||||
|
"gitlab",
|
||||||
|
map[string]string{"X-Gitlab-Event": "Push Hook"},
|
||||||
|
`{
|
||||||
|
"object_kind": "push",
|
||||||
|
"before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
|
||||||
|
"after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
|
||||||
|
"ref": "refs/heads/master",
|
||||||
|
"user_id": 4,
|
||||||
|
"user_name": "John Smith",
|
||||||
|
"user_email": "john@example.com",
|
||||||
|
"project_id": 15,
|
||||||
|
"repository": {
|
||||||
|
"name": "Diaspora",
|
||||||
|
"url": "git@example.com:mike/diasporadiaspora.git",
|
||||||
|
"description": "",
|
||||||
|
"homepage": "http://example.com/mike/diaspora",
|
||||||
|
"git_http_url":"http://example.com/mike/diaspora.git",
|
||||||
|
"git_ssh_url":"git@example.com:mike/diaspora.git",
|
||||||
|
"visibility_level":0
|
||||||
|
},
|
||||||
|
"commits": [
|
||||||
|
{
|
||||||
|
"id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
|
||||||
|
"message": "Update Catalan translation to e38cb41.",
|
||||||
|
"timestamp": "2011-12-12T14:27:31+02:00",
|
||||||
|
"url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
|
||||||
|
"author": {
|
||||||
|
"name": "Jordi Mallach",
|
||||||
|
"email": "jordi@softcatala.org"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
|
||||||
|
"message": "fixed readme",
|
||||||
|
"timestamp": "2012-01-03T23:36:29+02:00",
|
||||||
|
"url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
|
||||||
|
"author": {
|
||||||
|
"name": "GitLab dev user",
|
||||||
|
"email": "gitlabdev@dv6700.(none)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_commits_count": 4
|
||||||
|
}`,
|
||||||
|
false,
|
||||||
|
http.StatusOK,
|
||||||
|
`{"message":"success","output":"arg: b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327 John Smith john@example.com\n"}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"missing-cmd-arg", // missing head_commit.author.email
|
||||||
|
"github",
|
||||||
|
map[string]string{"X-Hub-Signature": "ab03955b9377f530aa298b1b6d273ae9a47e1e40"},
|
||||||
|
`{
|
||||||
|
"head_commit":{
|
||||||
|
"added":[
|
||||||
|
"words/madame-bovary.txt"
|
||||||
|
],
|
||||||
|
"author":{
|
||||||
|
"email":"lolwut@noway.biz",
|
||||||
|
"name":"Garen Torikian",
|
||||||
|
"username":"octokitty"
|
||||||
|
},
|
||||||
|
"committer":{
|
||||||
|
"email":"lolwut@noway.biz",
|
||||||
|
"name":"Garen Torikian",
|
||||||
|
"username":"octokitty"
|
||||||
|
},
|
||||||
|
"distinct":true,
|
||||||
|
"id":"1481a2de7b2a7d02428ad93446ab166be7793fbb",
|
||||||
|
"message":"Rename madame-bovary.txt to words/madame-bovary.txt",
|
||||||
|
"modified":[
|
||||||
|
|
||||||
|
],
|
||||||
|
"removed":[
|
||||||
|
"madame-bovary.txt"
|
||||||
|
],
|
||||||
|
"timestamp":"2013-03-12T08:14:29-07:00",
|
||||||
|
"url":"https://github.com/octokitty/testing/commit/1481a2de7b2a7d02428ad93446ab166be7793fbb"
|
||||||
|
},
|
||||||
|
"ref":"refs/heads/master"
|
||||||
|
}`,
|
||||||
|
false,
|
||||||
|
http.StatusOK,
|
||||||
|
`{"output":"arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz\nenv: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00\n"}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"missing-env-arg", // missing head_commit.timestamp
|
||||||
|
"github",
|
||||||
|
map[string]string{"X-Hub-Signature": "2cf8b878cb6b74a25090a140fa4a474be04b97fa"},
|
||||||
|
`{
|
||||||
|
"head_commit":{
|
||||||
|
"added":[
|
||||||
|
"words/madame-bovary.txt"
|
||||||
|
],
|
||||||
|
"author":{
|
||||||
|
"email":"lolwut@noway.biz",
|
||||||
|
"name":"Garen Torikian",
|
||||||
|
"username":"octokitty"
|
||||||
|
},
|
||||||
|
"committer":{
|
||||||
|
"email":"lolwut@noway.biz",
|
||||||
|
"name":"Garen Torikian",
|
||||||
|
"username":"octokitty"
|
||||||
|
},
|
||||||
|
"distinct":true,
|
||||||
|
"id":"1481a2de7b2a7d02428ad93446ab166be7793fbb",
|
||||||
|
"message":"Rename madame-bovary.txt to words/madame-bovary.txt",
|
||||||
|
"modified":[
|
||||||
|
|
||||||
|
],
|
||||||
|
"removed":[
|
||||||
|
"madame-bovary.txt"
|
||||||
|
],
|
||||||
|
"url":"https://github.com/octokitty/testing/commit/1481a2de7b2a7d02428ad93446ab166be7793fbb"
|
||||||
|
},
|
||||||
|
"ref":"refs/heads/master"
|
||||||
|
}`,
|
||||||
|
false,
|
||||||
|
http.StatusOK,
|
||||||
|
`{"output":"arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz\n"}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{"empty payload", "github", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user