diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..75f347d
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,23 @@
+name: build
+on: [push, pull_request]
+jobs:
+ build:
+ strategy:
+ matrix:
+ go-version: [1.14.x, 1.15.x]
+ os: [ubuntu-latest, macos-latest, windows-latest]
+
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-go@v2
+ with:
+ go-version: ${{ matrix.go-version }}
+ id: go
+
+ - name: Build
+ run: go build -v
+
+ - name: Test
+ run: go test -v ./...
diff --git a/.travis.yml b/.travis.yml
index 86f29d5..54e4652 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,7 @@
language: go
go:
- - 1.13.x
+ - 1.14.x
- master
os:
diff --git a/Makefile b/Makefile
index b341c1f..1a06482 100644
--- a/Makefile
+++ b/Makefile
@@ -1,12 +1,18 @@
OS = darwin freebsd linux openbsd
ARCHS = 386 arm amd64 arm64
+.DEFAULT_GOAL := help
+
+.PHONY: help
+help:
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-16s\033[0m %s\n", $$1, $$2}'
+
all: build release release-windows
-build: deps
+build: deps ## Build the project
go build
-release: clean deps
+release: clean deps ## Generate releases for unix systems
@for arch in $(ARCHS);\
do \
for os in $(OS);\
@@ -18,7 +24,7 @@ release: clean deps
done \
done
-release-windows: clean deps
+release-windows: clean deps ## Generate release for windows
@for arch in $(ARCHS);\
do \
echo "Building windows-$$arch"; \
@@ -27,12 +33,12 @@ release-windows: clean deps
tar cz -C build -f build/webhook-windows-$$arch.tar.gz webhook-windows-$$arch; \
done
-test: deps
+test: deps ## Execute tests
go test ./...
-deps:
+deps: ## Install dependencies using go get
go get -d -v -t ./...
-clean:
+clean: ## Remove building artifacts
rm -rf build
rm -f webhook
diff --git a/README.md b/README.md
index 083c36f..f653672 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# What is webhook?
+# What is webhook? ![build-status][badge]
@@ -26,11 +26,11 @@ If you don't have time to waste configuring, hosting, debugging and maintaining
# Getting started
## Installation
### Building from source
-To get started, first make sure you've properly set up your [Go](http://golang.org/doc/install) 1.12 or newer environment and then run
+To get started, first make sure you've properly set up your [Go](http://golang.org/doc/install) 1.14 or newer environment and then run
```bash
-$ go get github.com/adnanh/webhook
+$ go build github.com/adnanh/webhook
```
-to get the latest version of the [webhook][w].
+to build the latest version of the [webhook][w].
### Using package manager
#### Snap store
@@ -46,7 +46,9 @@ If you are using Debian linux ("stretch" or later), you can install webhook usin
Prebuilt binaries for different architectures are available at [GitHub Releases](https://github.com/adnanh/webhook/releases).
## Configuration
-Next step is to define some hooks you want [webhook][w] to serve. Begin by creating an empty file named `hooks.json`. This file will contain an array of hooks the [webhook][w] will serve. Check [Hook definition page](docs/Hook-Definition.md) to see the detailed description of what properties a hook can contain, and how to use them.
+Next step is to define some hooks you want [webhook][w] to serve.
+[webhook][w] supports JSON or YAML configuration files, but we'll focus primarily on JSON in the following example.
+Begin by creating an empty file named `hooks.json`. This file will contain an array of hooks the [webhook][w] will serve. Check [Hook definition page](docs/Hook-Definition.md) to see the detailed description of what properties a hook can contain, and how to use them.
Let's define a simple hook named `redeploy-webhook` that will run a redeploy script located in `/var/scripts/redeploy.sh`. Make sure that your bash script has `#!/bin/sh` shebang on top.
@@ -61,6 +63,13 @@ Our `hooks.json` file will now look like this:
]
```
+**NOTE:** If you prefer YAML, the equivalent `hooks.yaml` file would be:
+```yaml
+- id: redeploy-webhook
+ execute-command: "/var/scripts/redeploy.sh"
+ command-working-directory: "/var/webhook"
+```
+
You can now run [webhook][w] using
```bash
$ /path/to/webhook -hooks hooks.json -verbose
@@ -90,7 +99,7 @@ All files are ignored unless they match one of the following criteria:
In either case, the given file part will be parsed as JSON and added to the `payload` map.
## Templates
-[webhook][w] can parse the `hooks.json` input file as a Go template when given the `-template` [CLI parameter](docs/Webhook-Parameters.md). See the [Templates page](docs/Templates.md) for more details on template usage.
+[webhook][w] can parse the hooks configuration file as a Go template when given the `-template` [CLI parameter](docs/Webhook-Parameters.md). See the [Templates page](docs/Templates.md) for more details on template usage.
## Using HTTPS
[webhook][w] by default serves hooks using http. If you want [webhook][w] to serve secure content using https, you can use the `-secure` flag while starting [webhook][w]. 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.
@@ -202,3 +211,4 @@ THE SOFTWARE.
[w]: https://github.com/adnanh/webhook
[wc]: https://github.com/adnanh/webhook-contrib
+[badge]: https://github.com/adnanh/webhook/workflows/build/badge.svg
diff --git a/cipher_suites.go b/cipher_suites.go
deleted file mode 100644
index 81db51f..0000000
--- a/cipher_suites.go
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright 2010 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// Copied from Go 1.14 tip src/crypto/tls/cipher_suites.go
-
-package main
-
-import (
- "crypto/tls"
- "fmt"
-)
-
-// CipherSuite is a TLS cipher suite. Note that most functions in this package
-// accept and expose cipher suite IDs instead of this type.
-type CipherSuite struct {
- ID uint16
- Name string
-
- // Supported versions is the list of TLS protocol versions that can
- // negotiate this cipher suite.
- SupportedVersions []uint16
-
- // Insecure is true if the cipher suite has known security issues
- // due to its primitives, design, or implementation.
- Insecure bool
-}
-
-var (
- supportedUpToTLS12 = []uint16{tls.VersionTLS10, tls.VersionTLS11, tls.VersionTLS12}
- supportedOnlyTLS12 = []uint16{tls.VersionTLS12}
- supportedOnlyTLS13 = []uint16{tls.VersionTLS13}
-)
-
-// CipherSuites returns a list of cipher suites currently implemented by this
-// package, excluding those with security issues, which are returned by
-// InsecureCipherSuites.
-//
-// The list is sorted by ID. Note that the default cipher suites selected by
-// this package might depend on logic that can't be captured by a static list.
-func CipherSuites() []*CipherSuite {
- return []*CipherSuite{
- {tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, "TLS_RSA_WITH_3DES_EDE_CBC_SHA", supportedUpToTLS12, false},
- {tls.TLS_RSA_WITH_AES_128_CBC_SHA, "TLS_RSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
- {tls.TLS_RSA_WITH_AES_256_CBC_SHA, "TLS_RSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
- {tls.TLS_RSA_WITH_AES_128_GCM_SHA256, "TLS_RSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
- {tls.TLS_RSA_WITH_AES_256_GCM_SHA384, "TLS_RSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
-
- {tls.TLS_AES_128_GCM_SHA256, "TLS_AES_128_GCM_SHA256", supportedOnlyTLS13, false},
- {tls.TLS_AES_256_GCM_SHA384, "TLS_AES_256_GCM_SHA384", supportedOnlyTLS13, false},
- {tls.TLS_CHACHA20_POLY1305_SHA256, "TLS_CHACHA20_POLY1305_SHA256", supportedOnlyTLS13, false},
-
- {tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
- {tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
- {tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", supportedUpToTLS12, false},
- {tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
- {tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
- {tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
- {tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
- {tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
- {tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
-
- // go1.14
- // {tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", supportedOnlyTLS12, false},
- // {tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", supportedOnlyTLS12, false},
- }
-}
-
-// InsecureCipherSuites returns a list of cipher suites currently implemented by
-// this package and which have security issues.
-//
-// Most applications should not use the cipher suites in this list, and should
-// only use those returned by CipherSuites.
-func InsecureCipherSuites() []*CipherSuite {
- // RC4 suites are broken because RC4 is.
- // CBC-SHA256 suites have no Lucky13 countermeasures.
- return []*CipherSuite{
- {tls.TLS_RSA_WITH_RC4_128_SHA, "TLS_RSA_WITH_RC4_128_SHA", supportedUpToTLS12, true},
- {tls.TLS_RSA_WITH_AES_128_CBC_SHA256, "TLS_RSA_WITH_AES_128_CBC_SHA256", supportedOnlyTLS12, true},
- {tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", supportedUpToTLS12, true},
- {tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, "TLS_ECDHE_RSA_WITH_RC4_128_SHA", supportedUpToTLS12, true},
- {tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", supportedOnlyTLS12, true},
- {tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", supportedOnlyTLS12, true},
- }
-}
-
-// CipherSuiteName returns the standard name for the passed cipher suite ID
-// (e.g. "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"), or a fallback representation
-// of the ID value if the cipher suite is not implemented by this package.
-func CipherSuiteName(id uint16) string {
- for _, c := range CipherSuites() {
- if c.ID == id {
- return c.Name
- }
- }
- for _, c := range InsecureCipherSuites() {
- if c.ID == id {
- return c.Name
- }
- }
- return fmt.Sprintf("0x%04X", id)
-}
diff --git a/docs/Hook-Definition.md b/docs/Hook-Definition.md
index a963ef3..41faa13 100644
--- a/docs/Hook-Definition.md
+++ b/docs/Hook-Definition.md
@@ -1,5 +1,6 @@
# Hook definition
-Hooks are defined as JSON objects. Please note that in order to be considered valid, a hook object must contain the `id` and `execute-command` properties. All other properties are considered optional.
+
+Hooks are defined as objects in the JSON or YAML hooks configuration file. Please note that in order to be considered valid, a hook object must contain the `id` and `execute-command` properties. All other properties are considered optional.
## Properties (keys)
diff --git a/docs/Hook-Examples.md b/docs/Hook-Examples.md
index 23896a7..ba581eb 100644
--- a/docs/Hook-Examples.md
+++ b/docs/Hook-Examples.md
@@ -1,5 +1,25 @@
-# Hook examples
-This page is still work in progress. Feel free to contribute!
+# Hook Examples
+
+Hooks are defined in a hooks configuration file in either JSON or YAML format,
+although the examples on this page all use the JSON format.
+
+🌱 This page is still a work in progress. Feel free to contribute!
+
+### Table of Contents
+
+* [Incoming Github webhook](#incoming-github-webhook)
+* [Incoming Bitbucket webhook](#incoming-bitbucket-webhook)
+* [Incoming Gitlab webhook](#incoming-gitlab-webhook)
+* [Incoming Gogs webhook](#incoming-gogs-webhook)
+* [Incoming Gitea webhook](#incoming-gitea-webhook)
+* [Slack slash command](#slack-slash-command)
+* [A simple webhook with a secret key in GET query](#a-simple-webhook-with-a-secret-key-in-get-query)
+* [JIRA Webhooks](#jira-webhooks)
+* [Pass File-to-command sample](#pass-file-to-command-sample)
+* [Incoming Scalr Webhook](#incoming-scalr-webhook)
+* [Travis CI webhook](#travis-ci-webhook)
+* [XML Payload](#xml-payload)
+* [Multipart Form Data](#multipart-form-data)
## Incoming Github webhook
```json
@@ -30,7 +50,7 @@ This page is still work in progress. Feel free to contribute!
{
"match":
{
- "type": "payload-hash-sha1",
+ "type": "payload-hmac-sha1",
"secret": "mysecret",
"parameter":
{
@@ -150,7 +170,7 @@ Values in the request body can be accessed in the command or to the match rule b
{
"match":
{
- "type": "payload-hash-sha256",
+ "type": "payload-hmac-sha256",
"secret": "mysecret",
"parameter":
{
@@ -425,6 +445,57 @@ Travis sends webhooks as `payload=`, so the payload needs to be par
]
```
+## JSON Array Payload
+
+If the JSON payload is an array instead of an object, `webhook` will process the payload and place it into a "root" object.
+Therefore, references to payload values must begin with `root.`.
+
+For example, given the following payload (taken from the Sendgrid Event Webhook documentation):
+```json
+[
+ {
+ "email": "example@test.com",
+ "timestamp": 1513299569,
+ "smtp-id": "<14c5d75ce93.dfd.64b469@ismtpd-555>",
+ "event": "processed",
+ "category": "cat facts",
+ "sg_event_id": "sg_event_id",
+ "sg_message_id": "sg_message_id"
+ },
+ {
+ "email": "example@test.com",
+ "timestamp": 1513299569,
+ "smtp-id": "<14c5d75ce93.dfd.64b469@ismtpd-555>",
+ "event": "deferred",
+ "category": "cat facts",
+ "sg_event_id": "sg_event_id",
+ "sg_message_id": "sg_message_id",
+ "response": "400 try again later",
+ "attempt": "5"
+ }
+]
+```
+
+A reference to the second item in the array would look like this:
+```json
+[
+ {
+ "id": "sendgrid",
+ "execute-command": "{{ .Hookecho }}",
+ "trigger-rule": {
+ "match": {
+ "type": "value",
+ "parameter": {
+ "source": "payload",
+ "name": "root.1.event"
+ },
+ "value": "deferred"
+ }
+ }
+ }
+]
+```
+
## XML Payload
Given the following payload:
diff --git a/docs/Hook-Rules.md b/docs/Hook-Rules.md
index e1f92f6..2e5550b 100644
--- a/docs/Hook-Rules.md
+++ b/docs/Hook-Rules.md
@@ -1,5 +1,20 @@
# Hook rules
+### Table of Contents
+
+* [And](#and)
+* [Or](#or)
+* [Not](#not)
+* [Multi-level](#multi-level)
+* [Match](#match)
+ * [Match value](#match-value)
+ * [Match regex](#match-regex)
+ * [Match payload-hmac-sha1](#match-payload-hmac-sha1)
+ * [Match payload-hmac-sha256](#match-payload-hmac-sha256)
+ * [Match payload-hmac-sha512](#match-payload-hmac-sha512)
+ * [Match Whitelisted IP range](#match-whitelisted-ip-range)
+ * [Match scalr-signature](#match-scalr-signature)
+
## And
*And rule* will evaluate to _true_, if and only if all of the sub rules evaluate to _true_.
```json
@@ -95,7 +110,7 @@
"source": "header",
"name": "X-Hub-Signature"
},
- "type": "payload-hash-sha1",
+ "type": "payload-hmac-sha1",
"secret": "mysecret"
}
},
@@ -135,9 +150,7 @@
*Please note:* Due to technical reasons, _number_ and _boolean_ values in the _match rule_ must be wrapped around with a pair of quotes.
-There are three different match rules:
-
-### 1. Match value
+### Match value
```json
{
"match":
@@ -153,7 +166,7 @@ There are three different match rules:
}
```
-### 2. Match regex
+### Match regex
For the regex syntax, check out
```json
{
@@ -170,12 +183,13 @@ For the regex syntax, check out
}
```
-### 3. Match payload-hash-sha1
+### Match payload-hmac-sha1
+Validate the HMAC of the payload using the SHA1 hash and the given *secret*.
```json
{
"match":
{
- "type": "payload-hash-sha1",
+ "type": "payload-hmac-sha1",
"secret": "yoursecret",
"parameter":
{
@@ -193,12 +207,13 @@ will be tried unless a match is found. For example:
X-Hub-Signature: sha1=the-first-signature,sha1=the-second-signature
```
-### 4. Match payload-hash-sha256
+### Match payload-hmac-sha256
+Validate the HMAC of the payload using the SHA256 hash and the given *secret*.
```json
{
"match":
{
- "type": "payload-hash-sha256",
+ "type": "payload-hmac-sha256",
"secret": "yoursecret",
"parameter":
{
@@ -216,12 +231,13 @@ will be tried unless a match is found. For example:
X-Hub-Signature: sha256=the-first-signature,sha256=the-second-signature
```
-### 5. Match payload-hash-sha512
+### Match payload-hmac-sha512
+Validate the HMAC of the payload using the SHA512 hash and the given *secret*.
```json
{
"match":
{
- "type": "payload-hash-sha512",
+ "type": "payload-hmac-sha512",
"secret": "yoursecret",
"parameter":
{
@@ -239,7 +255,7 @@ will be tried unless a match is found. For example:
X-Hub-Signature: sha512=the-first-signature,sha512=the-second-signature
```
-### 6. Match Whitelisted IP range
+### Match Whitelisted IP range
The IP can be IPv4- or IPv6-formatted, using [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_blocks). To match a single IP address only, use `/32`.
@@ -253,7 +269,7 @@ The IP can be IPv4- or IPv6-formatted, using [CIDR notation](https://en.wikipedi
}
```
-### 7. Match scalr-signature
+### Match scalr-signature
The trigger rule checks the scalr signature and also checks that the request was signed less than 5 minutes before it was received.
A unqiue signing key is generated for each webhook endpoint URL you register in Scalr.
@@ -267,4 +283,4 @@ Given the time check make sure that NTP is enabled on both your Scalr and webhoo
"secret": "Scalr-provided signing key"
}
}
-```
\ No newline at end of file
+```
diff --git a/docs/Templates.md b/docs/Templates.md
index 2adeab3..12798f4 100644
--- a/docs/Templates.md
+++ b/docs/Templates.md
@@ -1,12 +1,12 @@
# Templates in Webhook
-[`webhook`][w] can parse the `hooks.json` input file as a Go template when given the `-template` [CLI parameter](Webhook-Parameters.md).
+[`webhook`][w] can parse a hooks configuration file as a Go template when given the `-template` [CLI parameter](Webhook-Parameters.md).
-In additional to the [built-in Go template functions and features][tt], `webhook` provides a `getenv` template function for inserting environment variables into a `hooks.json` file.
+In additional to the [built-in Go template functions and features][tt], `webhook` provides a `getenv` template function for inserting environment variables into a templated configuration file.
## Example Usage
-In the example `hooks.json` file below, the `payload-hash-sha1` matching rule looks up the secret hash from the environment using the `getenv` template function.
+In the example JSON template file below (YAML is also supported), the `payload-hmac-sha1` matching rule looks up the HMAC secret from the environment using the `getenv` template function.
Additionally, the result is piped through the built-in Go template function `js` to ensure that the result is a well-formed Javascript/JSON string.
```
@@ -44,7 +44,7 @@ Additionally, the result is piped through the built-in Go template function `js`
{
"match":
{
- "type": "payload-hash-sha1",
+ "type": "payload-hmac-sha1",
"secret": "{{ getenv "XXXTEST_SECRET" | js }}",
"parameter":
{
diff --git a/go.mod b/go.mod
index bc7efb7..48f350c 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/adnanh/webhook
-go 1.13
+go 1.14
require (
github.com/clbanning/mxj v1.8.4
diff --git a/hooks.json.example b/hooks.json.example
index 00f6ff3..90cd275 100644
--- a/hooks.json.example
+++ b/hooks.json.example
@@ -33,7 +33,7 @@
{
"match":
{
- "type": "payload-hash-sha1",
+ "type": "payload-hmac-sha1",
"secret": "mysecret",
"parameter":
{
diff --git a/hooks.json.tmpl.example b/hooks.json.tmpl.example
index 70e05c8..5c00643 100644
--- a/hooks.json.tmpl.example
+++ b/hooks.json.tmpl.example
@@ -33,7 +33,7 @@
{
"match":
{
- "type": "payload-hash-sha1",
+ "type": "payload-hmac-sha1",
"secret": "{{ getenv "XXXTEST_SECRET" | js }}",
"parameter":
{
diff --git a/hooks.yaml.example b/hooks.yaml.example
index 74713e0..ba9d0b7 100644
--- a/hooks.yaml.example
+++ b/hooks.yaml.example
@@ -15,7 +15,7 @@
trigger-rule:
and:
- match:
- type: payload-hash-sha1
+ type: payload-hmac-sha1
secret: mysecret
parameter:
source: header
diff --git a/hooks.yaml.tmpl.example b/hooks.yaml.tmpl.example
index 2bcfbd6..672c8d0 100644
--- a/hooks.yaml.tmpl.example
+++ b/hooks.yaml.tmpl.example
@@ -15,7 +15,7 @@
trigger-rule:
and:
- match:
- type: payload-hash-sha1
+ type: payload-hmac-sha1
secret: "{{ getenv "XXXTEST_SECRET" | js }}"
parameter:
source: header
diff --git a/internal/hook/hook.go b/internal/hook/hook.go
index 0573a3f..d38415a 100644
--- a/internal/hook/hook.go
+++ b/internal/hook/hook.go
@@ -50,6 +50,33 @@ const (
EnvNamespace string = "HOOK_"
)
+// Request represents a webhook request.
+type Request struct {
+ // The request ID set by the RequestID middleware.
+ ID string
+
+ // The Content-Type of the request.
+ ContentType string
+
+ // The raw request body.
+ Body []byte
+
+ // Headers is a map of the parsed headers.
+ Headers map[string]interface{}
+
+ // Query is a map of the parsed URL query values.
+ Query map[string]interface{}
+
+ // Payload is a map of the parsed payload.
+ Payload map[string]interface{}
+
+ // Context is a map of the parsed pre-hook command result
+ Context map[string]interface{}
+
+ // The underlying HTTP request.
+ RawRequest *http.Request
+}
+
// ParameterNodeError describes an error walking a parameter node.
type ParameterNodeError struct {
key string
@@ -76,6 +103,8 @@ func IsParameterNodeError(err error) bool {
type SignatureError struct {
Signature string
Signatures []string
+
+ emptyPayload bool
}
func (e *SignatureError) Error() string {
@@ -83,11 +112,16 @@ func (e *SignatureError) Error() string {
return ""
}
- if e.Signatures != nil {
- return fmt.Sprintf("invalid payload signatures %s", e.Signatures)
+ var empty string
+ if e.emptyPayload {
+ empty = " on empty payload"
}
- return fmt.Sprintf("invalid payload signature %s", e.Signature)
+ if e.Signatures != nil {
+ return fmt.Sprintf("invalid payload signatures %s%s", e.Signatures, empty)
+ }
+
+ return fmt.Sprintf("invalid payload signature %s%s", e.Signature, empty)
}
// ArgumentError describes an invalid argument passed to Hook.
@@ -163,21 +197,24 @@ func ValidateMAC(payload []byte, mac hash.Hash, signatures []string) (string, er
return "", err
}
- expectedMAC := hex.EncodeToString(mac.Sum(nil))
+ actualMAC := hex.EncodeToString(mac.Sum(nil))
for _, signature := range signatures {
- if hmac.Equal([]byte(signature), []byte(expectedMAC)) {
- return expectedMAC, err
+ if hmac.Equal([]byte(signature), []byte(actualMAC)) {
+ return actualMAC, err
}
}
- return expectedMAC, &SignatureError{
- Signatures: signatures,
+ e := &SignatureError{Signatures: signatures}
+ if len(payload) == 0 {
+ e.emptyPayload = true
}
+
+ return actualMAC, e
}
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
-func CheckPayloadSignature(payload []byte, secret string, signature string) (string, error) {
+func CheckPayloadSignature(payload []byte, secret, signature string) (string, error) {
if secret == "" {
return "", errors.New("signature validation secret can not be empty")
}
@@ -190,7 +227,7 @@ func CheckPayloadSignature(payload []byte, secret string, signature string) (str
}
// CheckPayloadSignature256 calculates and verifies SHA256 signature of the given payload
-func CheckPayloadSignature256(payload []byte, secret string, signature string) (string, error) {
+func CheckPayloadSignature256(payload []byte, secret, signature string) (string, error) {
if secret == "" {
return "", errors.New("signature validation secret can not be empty")
}
@@ -203,7 +240,7 @@ func CheckPayloadSignature256(payload []byte, secret string, signature string) (
}
// CheckPayloadSignature512 calculates and verifies SHA512 signature of the given payload
-func CheckPayloadSignature512(payload []byte, secret string, signature string) (string, error) {
+func CheckPayloadSignature512(payload []byte, secret, signature string) (string, error) {
if secret == "" {
return "", errors.New("signature validation secret can not be empty")
}
@@ -215,22 +252,26 @@ func CheckPayloadSignature512(payload []byte, secret string, signature string) (
return ValidateMAC(payload, hmac.New(sha512.New, []byte(secret)), signatures)
}
-func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey string, checkDate bool) (bool, error) {
- // Check for the signature and date headers
- if _, ok := headers["X-Signature"]; !ok {
+func CheckScalrSignature(r *Request, signingKey string, checkDate bool) (bool, error) {
+ if r.Headers == nil {
return false, nil
}
- if _, ok := headers["Date"]; !ok {
+
+ // Check for the signature and date headers
+ if _, ok := r.Headers["X-Signature"]; !ok {
+ return false, nil
+ }
+ if _, ok := r.Headers["Date"]; !ok {
return false, nil
}
if signingKey == "" {
return false, errors.New("signature validation signing key can not be empty")
}
- providedSignature := headers["X-Signature"].(string)
- dateHeader := headers["Date"].(string)
+ providedSignature := r.Headers["X-Signature"].(string)
+ dateHeader := r.Headers["Date"].(string)
mac := hmac.New(sha1.New, []byte(signingKey))
- mac.Write(body)
+ mac.Write(r.Body)
mac.Write([]byte(dateHeader))
expectedSignature := hex.EncodeToString(mac.Sum(nil))
@@ -257,7 +298,7 @@ func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey
// CheckIPWhitelist makes sure the provided remote address (of the form IP:port) falls within the provided IP range
// (in CIDR form or a single IP address).
-func CheckIPWhitelist(remoteAddr string, ipRange string) (bool, error) {
+func CheckIPWhitelist(remoteAddr, ipRange string) (bool, error) {
// Extract IP address from remote address.
// IPv6 addresses will likely be surrounded by [].
@@ -296,7 +337,7 @@ func CheckIPWhitelist(remoteAddr string, ipRange string) (bool, error) {
// 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 {
+func ReplaceParameter(s string, params, value interface{}) bool {
if params == nil {
return false
}
@@ -385,14 +426,27 @@ func GetParameter(s string, params interface{}) (interface{}, error) {
return nil, &ParameterNodeError{s}
}
-// ExtractParameterAsString extracts value from interface{} as string based on the passed string
+// ExtractParameterAsString extracts value from interface{} as string based on
+// the passed string. Complex data types are rendered as JSON instead of the Go
+// Stringer format.
func ExtractParameterAsString(s string, params interface{}) (string, error) {
pValue, err := GetParameter(s, params)
if err != nil {
return "", err
}
- return fmt.Sprintf("%v", pValue), nil
+ switch v := reflect.ValueOf(pValue); v.Kind() {
+ case reflect.Array, reflect.Map, reflect.Slice:
+ r, err := json.Marshal(pValue)
+ if err != nil {
+ return "", err
+ }
+
+ return string(r), nil
+
+ default:
+ return fmt.Sprintf("%v", pValue), nil
+ }
}
// Argument type specifies the parameter key name and the source it should
@@ -406,43 +460,43 @@ type Argument struct {
// Get Argument method returns the value for the Argument's key name
// based on the Argument's source
-func (ha *Argument) Get(headers, query, payload *map[string]interface{}, context *map[string]interface{}) (string, error) {
+func (ha *Argument) Get(r *Request) (string, error) {
var source *map[string]interface{}
key := ha.Name
switch ha.Source {
case SourceHeader:
- source = headers
+ source = &r.Headers
key = textproto.CanonicalMIMEHeaderKey(ha.Name)
case SourceQuery, SourceQueryAlias:
- source = query
+ source = &r.Query
case SourcePayload:
- source = payload
+ source = &r.Payload
case SourceContext:
- source = context
+ source = &r.Context
case SourceString:
return ha.Name, nil
case SourceEntirePayload:
- r, err := json.Marshal(payload)
+ res, err := json.Marshal(&r.Payload)
if err != nil {
return "", err
}
- return string(r), nil
+ return string(res), nil
case SourceEntireHeaders:
- r, err := json.Marshal(headers)
+ res, err := json.Marshal(&r.Headers)
if err != nil {
return "", err
}
- return string(r), nil
+ return string(res), nil
case SourceEntireQuery:
- r, err := json.Marshal(query)
+ res, err := json.Marshal(&r.Query)
if err != nil {
return "", err
}
- return string(r), nil
+ return string(res), nil
}
if source != nil {
@@ -540,11 +594,11 @@ type Hook struct {
// 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{}, context *map[string]interface{}) []error {
+func (h *Hook) ParseJSONParameters(r *Request) []error {
errors := make([]error, 0)
for i := range h.JSONStringParameters {
- arg, err := h.JSONStringParameters[i].Get(headers, query, payload, context)
+ arg, err := h.JSONStringParameters[i].Get(r)
if err != nil {
errors = append(errors, &ArgumentError{h.JSONStringParameters[i]})
} else {
@@ -563,13 +617,13 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
switch h.JSONStringParameters[i].Source {
case SourceHeader:
- source = headers
+ source = &r.Headers
case SourcePayload:
- source = payload
+ source = &r.Payload
case SourceContext:
- source = context
+ source = &r.Context
case SourceQuery, SourceQueryAlias:
- source = query
+ source = &r.Query
}
if source != nil {
@@ -595,14 +649,14 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
// ExtractCommandArguments creates a list of arguments, based on the
// PassArgumentsToCommand property that is ready to be used with exec.Command()
-func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}, context *map[string]interface{}) ([]string, []error) {
+func (h *Hook) ExtractCommandArguments(r *Request) ([]string, []error) {
args := make([]string, 0)
errors := make([]error, 0)
args = append(args, h.ExecuteCommand)
for i := range h.PassArgumentsToCommand {
- arg, err := h.PassArgumentsToCommand[i].Get(headers, query, payload, context)
+ arg, err := h.PassArgumentsToCommand[i].Get(r)
if err != nil {
args = append(args, "")
errors = append(errors, &ArgumentError{h.PassArgumentsToCommand[i]})
@@ -622,11 +676,11 @@ func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]inter
// 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{}, context *map[string]interface{}) ([]string, []error) {
+func (h *Hook) ExtractCommandArgumentsForEnv(r *Request) ([]string, []error) {
args := make([]string, 0)
errors := make([]error, 0)
for i := range h.PassEnvironmentToCommand {
- arg, err := h.PassEnvironmentToCommand[i].Get(headers, query, payload, context)
+ arg, err := h.PassEnvironmentToCommand[i].Get(r)
if err != nil {
errors = append(errors, &ArgumentError{h.PassEnvironmentToCommand[i]})
continue
@@ -658,11 +712,11 @@ type FileParameter struct {
// ExtractCommandArgumentsForFile creates a list of arguments in key=value
// format, based on the PassFileToCommand property that is ready to be used
// with exec.Command().
-func (h *Hook) ExtractCommandArgumentsForFile(headers, query, payload *map[string]interface{}, context *map[string]interface{}) ([]FileParameter, []error) {
+func (h *Hook) ExtractCommandArgumentsForFile(r *Request) ([]FileParameter, []error) {
args := make([]FileParameter, 0)
errors := make([]error, 0)
for i := range h.PassFileToCommand {
- arg, err := h.PassFileToCommand[i].Get(headers, query, payload, context)
+ arg, err := h.PassFileToCommand[i].Get(r)
if err != nil {
errors = append(errors, &ArgumentError{h.PassFileToCommand[i]})
continue
@@ -769,16 +823,16 @@ type Rules struct {
// Evaluate finds the first rule property that is not nil and returns the value
// it evaluates to
-func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, context *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
+func (r Rules) Evaluate(req *Request) (bool, error) {
switch {
case r.And != nil:
- return r.And.Evaluate(headers, query, payload, context, body, remoteAddr)
+ return r.And.Evaluate(req)
case r.Or != nil:
- return r.Or.Evaluate(headers, query, payload, context, body, remoteAddr)
+ return r.Or.Evaluate(req)
case r.Not != nil:
- return r.Not.Evaluate(headers, query, payload, context, body, remoteAddr)
+ return r.Not.Evaluate(req)
case r.Match != nil:
- return r.Match.Evaluate(headers, query, payload, context, body, remoteAddr)
+ return r.Match.Evaluate(req)
}
return false, nil
@@ -788,11 +842,11 @@ func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, context
type AndRule []Rules
// Evaluate AndRule will return true if and only if all of ChildRules evaluate to true
-func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, context *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
+func (r AndRule) Evaluate(req *Request) (bool, error) {
res := true
for _, v := range r {
- rv, err := v.Evaluate(headers, query, payload, context, body, remoteAddr)
+ rv, err := v.Evaluate(req)
if err != nil {
return false, err
}
@@ -810,11 +864,11 @@ func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, conte
type OrRule []Rules
// Evaluate OrRule will return true if any of ChildRules evaluate to true
-func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, context *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
+func (r OrRule) Evaluate(req *Request) (bool, error) {
res := false
for _, v := range r {
- rv, err := v.Evaluate(headers, query, payload, context, body, remoteAddr)
+ rv, err := v.Evaluate(req)
if err != nil {
return false, err
}
@@ -832,8 +886,8 @@ func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, contex
type NotRule Rules
// Evaluate NotRule will return true if and only if ChildRule evaluates to false
-func (r NotRule) Evaluate(headers, query, payload *map[string]interface{}, context *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
- rv, err := Rules(r).Evaluate(headers, query, payload, context, body, remoteAddr)
+func (r NotRule) Evaluate(req *Request) (bool, error) {
+ rv, err := Rules(r).Evaluate(req)
return !rv, err
}
@@ -851,6 +905,9 @@ type MatchRule struct {
const (
MatchValue string = "value"
MatchRegex string = "regex"
+ MatchHMACSHA1 string = "payload-hmac-sha1"
+ MatchHMACSHA256 string = "payload-hmac-sha256"
+ MatchHMACSHA512 string = "payload-hmac-sha512"
MatchHashSHA1 string = "payload-hash-sha1"
MatchHashSHA256 string = "payload-hash-sha256"
MatchHashSHA512 string = "payload-hash-sha512"
@@ -859,16 +916,16 @@ const (
)
// Evaluate MatchRule will return based on the type
-func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, context *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
+func (r MatchRule) Evaluate(req *Request) (bool, error) {
if r.Type == IPWhitelist {
- return CheckIPWhitelist(remoteAddr, r.IPRange)
+ return CheckIPWhitelist(req.RawRequest.RemoteAddr, r.IPRange)
}
if r.Type == ScalrSignature {
- return CheckScalrSignature(*headers, *body, r.Secret, true)
+ return CheckScalrSignature(req, r.Secret, true)
}
- arg, err := r.Parameter.Get(headers, query, payload, context)
+ arg, err := r.Parameter.Get(req)
if err == nil {
switch r.Type {
case MatchValue:
@@ -876,13 +933,22 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, con
case MatchRegex:
return regexp.MatchString(r.Regex, arg)
case MatchHashSHA1:
- _, err := CheckPayloadSignature(*body, r.Secret, arg)
+ log.Print(`warn: use of deprecated option payload-hash-sha1; use payload-hmac-sha1 instead`)
+ fallthrough
+ case MatchHMACSHA1:
+ _, err := CheckPayloadSignature(req.Body, r.Secret, arg)
return err == nil, err
case MatchHashSHA256:
- _, err := CheckPayloadSignature256(*body, r.Secret, arg)
+ log.Print(`warn: use of deprecated option payload-hash-sha256: use payload-hmac-sha256 instead`)
+ fallthrough
+ case MatchHMACSHA256:
+ _, err := CheckPayloadSignature256(req.Body, r.Secret, arg)
return err == nil, err
case MatchHashSHA512:
- _, err := CheckPayloadSignature512(*body, r.Secret, arg)
+ log.Print(`warn: use of deprecated option payload-hash-sha512: use payload-hmac-sha512 instead`)
+ fallthrough
+ case MatchHMACSHA512:
+ _, err := CheckPayloadSignature512(req.Body, r.Secret, arg)
return err == nil, err
}
}
diff --git a/internal/hook/hook_test.go b/internal/hook/hook_test.go
index 7d84acf..84ad248 100644
--- a/internal/hook/hook_test.go
+++ b/internal/hook/hook_test.go
@@ -1,6 +1,7 @@
package hook
import (
+ "net/http"
"os"
"reflect"
"strings"
@@ -49,12 +50,14 @@ var checkPayloadSignatureTests = []struct {
{[]byte(`{"a": "z"}`), "secret", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
{[]byte(`{"a": "z"}`), "secret", "sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e,sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true},
+ {[]byte(``), "secret", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", true},
// failures
{[]byte(`{"a": "z"}`), "secret", "XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
{[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e,sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
{[]byte(`{"a": "z"}`), "secreX", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "900225703e9342328db7307692736e2f7cc7b36e", false},
{[]byte(`{"a": "z"}`), "", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "", false},
+ {[]byte(``), "secret", "XXXf6174a0fcecc4d346680a72b7ce644b9a88e8", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", false},
}
func TestCheckPayloadSignature(t *testing.T) {
@@ -80,11 +83,13 @@ var checkPayloadSignature256Tests = []struct {
{[]byte(`{"a": "z"}`), "secret", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
{[]byte(`{"a": "z"}`), "secret", "sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89,sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
+ {[]byte(``), "secret", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", true},
// failures
{[]byte(`{"a": "z"}`), "secret", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
{[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89,sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
{[]byte(`{"a": "z"}`), "", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "", false},
+ {[]byte(``), "secret", "XXX66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", false},
}
func TestCheckPayloadSignature256(t *testing.T) {
@@ -109,9 +114,11 @@ var checkPayloadSignature512Tests = []struct {
}{
{[]byte(`{"a": "z"}`), "secret", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true},
{[]byte(`{"a": "z"}`), "secret", "sha512=4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true},
+ {[]byte(``), "secret", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", true},
// failures
{[]byte(`{"a": "z"}`), "secret", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", false},
{[]byte(`{"a": "z"}`), "", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "", false},
+ {[]byte(``), "secret", "XXX9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", false},
}
func TestCheckPayloadSignature512(t *testing.T) {
@@ -130,7 +137,7 @@ func TestCheckPayloadSignature512(t *testing.T) {
var checkScalrSignatureTests = []struct {
description string
headers map[string]interface{}
- payload []byte
+ body []byte
secret string
expectedSignature string
ok bool
@@ -169,7 +176,11 @@ var checkScalrSignatureTests = []struct {
func TestCheckScalrSignature(t *testing.T) {
for _, testCase := range checkScalrSignatureTests {
- valid, err := CheckScalrSignature(testCase.headers, testCase.payload, testCase.secret, false)
+ r := &Request{
+ Headers: testCase.headers,
+ Body: testCase.body,
+ }
+ valid, err := CheckScalrSignature(r, testCase.secret, false)
if valid != testCase.ok {
t.Errorf("failed to check scalr signature fot test case: %s\nexpected ok:%#v, got ok:%#v}",
testCase.description, testCase.ok, valid)
@@ -217,6 +228,9 @@ var extractParameterTests = []struct {
{"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},
+ {"b", map[string]interface{}{"b": map[string]interface{}{"z": 1}}, `{"z":1}`, true},
+ {"c", map[string]interface{}{"c": []interface{}{"y", "z"}}, `["y","z"]`, true},
+ {"d", map[string]interface{}{"d": [2]interface{}{"y", "z"}}, `["y","z"]`, true},
// failures
{"check_nil", nil, "", false},
{"a.X", map[string]interface{}{"a": map[string]interface{}{"b": "z"}}, "", false}, // non-existent parameter reference
@@ -233,63 +247,75 @@ func TestExtractParameter(t *testing.T) {
for _, tt := range extractParameterTests {
value, err := ExtractParameterAsString(tt.s, tt.params)
if (err == nil) != tt.ok || value != tt.value {
- t.Errorf("failed to extract parameter %q:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, err:%s}", tt.s, tt.value, tt.ok, value, err)
+ t.Errorf("failed to extract parameter %q:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, err:%v}", tt.s, tt.value, tt.ok, value, err)
}
}
}
var argumentGetTests = []struct {
source, name string
- headers, query, payload, context *map[string]interface{}
+ headers, query, payload, context map[string]interface{}
value string
ok bool
}{
- {"header", "a", &map[string]interface{}{"A": "z"}, nil, nil, nil, "z", true},
- {"url", "a", nil, &map[string]interface{}{"a": "z"}, nil, nil, "z", true},
- {"payload", "a", nil, nil, &map[string]interface{}{"a": "z"}, nil, "z", true},
- {"context", "a", nil, nil, nil, &map[string]interface{}{"a": "z"}, "z", true},
+ {"header", "a", map[string]interface{}{"A": "z"}, nil, nil, nil, "z", true},
+ {"url", "a", nil, map[string]interface{}{"a": "z"}, nil, nil, "z", true},
+ {"payload", "a", nil, nil, map[string]interface{}{"a": "z"}, nil, "z", true},
+ {"context", "a", nil, nil, nil, map[string]interface{}{"a": "z"}, "z", true},
{"string", "a", nil, nil, nil, nil, "a", true},
// failures
- {"header", "a", nil, &map[string]interface{}{"a": "z"}, &map[string]interface{}{"a": "z"}, nil, "", false}, // nil headers
- {"url", "a", &map[string]interface{}{"A": "z"}, nil, &map[string]interface{}{"a": "z"}, nil, "", false}, // nil query
- {"payload", "a", &map[string]interface{}{"A": "z"}, &map[string]interface{}{"a": "z"}, nil, nil, "", false}, // nil payload
+ {"header", "a", nil, map[string]interface{}{"a": "z"}, map[string]interface{}{"a": "z"}, nil, "", false}, // nil headers
+ {"url", "a", map[string]interface{}{"A": "z"}, nil, map[string]interface{}{"a": "z"}, nil, "", false}, // nil query
+ {"payload", "a", map[string]interface{}{"A": "z"}, map[string]interface{}{"a": "z"}, nil, nil, "", false}, // nil payload
{"context", "a", nil, nil, nil, nil, "", false}, // nil context
- {"foo", "a", &map[string]interface{}{"A": "z"}, nil, nil, nil, "", false}, // invalid source
+ {"foo", "a", map[string]interface{}{"A": "z"}, nil, nil, nil, "", false}, // invalid source
}
func TestArgumentGet(t *testing.T) {
for _, tt := range argumentGetTests {
a := Argument{tt.source, tt.name, "", false}
- value, err := a.Get(tt.headers, tt.query, tt.payload, tt.context)
+ r := &Request{
+ Headers: tt.headers,
+ Query: tt.query,
+ Payload: tt.payload,
+ Context: tt.context,
+ }
+ value, err := a.Get(r)
if (err == nil) != tt.ok || value != tt.value {
- t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, err:%s}", tt.source, tt.name, tt.value, tt.ok, value, err)
+ t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, err:%v}", tt.source, tt.name, tt.value, tt.ok, value, err)
}
}
}
var hookParseJSONParametersTests = []struct {
params []Argument
- headers, query, payload, context *map[string]interface{}
- rheaders, rquery, rpayload, rcontext *map[string]interface{}
+ headers, query, payload, context map[string]interface{}
+ rheaders, rquery, rpayload, rcontext map[string]interface{}
ok bool
}{
- {[]Argument{Argument{"header", "a", "", false}}, &map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, nil, &map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, nil, true},
- {[]Argument{Argument{"url", "a", "", false}}, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, nil, true},
- {[]Argument{Argument{"payload", "a", "", false}}, nil, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
- {[]Argument{Argument{"context", "a", "", false}}, nil, nil, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
- {[]Argument{Argument{"header", "z", "", false}}, &map[string]interface{}{"Z": `{}`}, nil, nil, nil, &map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, nil, true},
+ {[]Argument{Argument{"header", "a", "", false}}, map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, nil, map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, nil, true},
+ {[]Argument{Argument{"url", "a", "", false}}, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, nil, true},
+ {[]Argument{Argument{"payload", "a", "", false}}, nil, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
+ {[]Argument{Argument{"context", "a", "", false}}, nil, nil, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
+ {[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": `{}`}, nil, nil, nil, map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, nil, true},
// failures
- {[]Argument{Argument{"header", "z", "", false}}, &map[string]interface{}{"Z": ``}, nil, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, nil, false}, // empty string
- {[]Argument{Argument{"header", "y", "", false}}, &map[string]interface{}{"X": `{}`}, nil, nil, nil, &map[string]interface{}{"X": `{}`}, nil, nil, nil, false}, // missing parameter
- {[]Argument{Argument{"string", "z", "", false}}, &map[string]interface{}{"Z": ``}, nil, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, nil, false}, // invalid argument source
+ {[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, nil, false}, // empty string
+ {[]Argument{Argument{"header", "y", "", false}}, map[string]interface{}{"X": `{}`}, nil, nil, nil, map[string]interface{}{"X": `{}`}, nil, nil, nil, false}, // missing parameter
+ {[]Argument{Argument{"string", "z", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, nil, map[string]interface{}{"Z": ``}, nil, 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, tt.context)
+ r := &Request{
+ Headers: tt.headers,
+ Query: tt.query,
+ Payload: tt.payload,
+ Context: tt.context,
+ }
+ err := h.ParseJSONParameters(r)
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))
+ t.Errorf("failed to parse %v:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.params, tt.rheaders, tt.ok, tt.headers, (err == nil))
}
}
}
@@ -297,19 +323,25 @@ func TestHookParseJSONParameters(t *testing.T) {
var hookExtractCommandArgumentsTests = []struct {
exec string
args []Argument
- headers, query, payload, context *map[string]interface{}
+ headers, query, payload, context map[string]interface{}
value []string
ok bool
}{
- {"test", []Argument{Argument{"header", "a", "", false}}, &map[string]interface{}{"A": "z"}, nil, nil, nil, []string{"test", "z"}, true},
+ {"test", []Argument{Argument{"header", "a", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, nil, []string{"test", "z"}, true},
// failures
- {"fail", []Argument{Argument{"payload", "a", "", false}}, &map[string]interface{}{"A": "z"}, nil, nil, nil, []string{"fail", ""}, false},
+ {"fail", []Argument{Argument{"payload", "a", "", false}}, map[string]interface{}{"A": "z"}, nil, 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, tt.context)
+ r := &Request{
+ Headers: tt.headers,
+ Query: tt.query,
+ Payload: tt.payload,
+ Context: tt.context,
+ }
+ value, err := h.ExtractCommandArguments(r)
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))
}
@@ -338,7 +370,7 @@ func TestHookExtractCommandArguments(t *testing.T) {
var hookExtractCommandArgumentsForEnvTests = []struct {
exec string
args []Argument
- headers, query, payload, context *map[string]interface{}
+ headers, query, payload, context map[string]interface{}
value []string
ok bool
}{
@@ -346,14 +378,14 @@ var hookExtractCommandArgumentsForEnvTests = []struct {
{
"test",
[]Argument{Argument{"header", "a", "", false}},
- &map[string]interface{}{"A": "z"}, nil, nil, nil,
+ map[string]interface{}{"A": "z"}, nil, nil, nil,
[]string{"HOOK_a=z"},
true,
},
{
"test",
[]Argument{Argument{"header", "a", "MYKEY", false}},
- &map[string]interface{}{"A": "z"}, nil, nil, nil,
+ map[string]interface{}{"A": "z"}, nil, nil, nil,
[]string{"MYKEY=z"},
true,
},
@@ -361,7 +393,7 @@ var hookExtractCommandArgumentsForEnvTests = []struct {
{
"fail",
[]Argument{Argument{"payload", "a", "", false}},
- &map[string]interface{}{"A": "z"}, nil, nil, nil,
+ map[string]interface{}{"A": "z"}, nil, nil, nil,
[]string{},
false,
},
@@ -370,7 +402,13 @@ var hookExtractCommandArgumentsForEnvTests = []struct {
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, tt.context)
+ r := &Request{
+ Headers: tt.headers,
+ Query: tt.query,
+ Payload: tt.payload,
+ Context: tt.context,
+ }
+ value, err := h.ExtractCommandArgumentsForEnv(r)
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))
}
@@ -448,24 +486,31 @@ func TestHooksMatch(t *testing.T) {
var matchRuleTests = []struct {
typ, regex, secret, value, ipRange string
param Argument
- headers, query, payload, context *map[string]interface{}
+ headers, query, payload, context map[string]interface{}
body []byte
remoteAddr string
ok bool
err bool
}{
- {"value", "", "", "z", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, "", true, false},
- {"regex", "^z", "", "z", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, "", true, false},
- {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, nil, []byte(`{"a": "z"}`), "", true, false},
- {"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, nil, []byte(`{"a": "z"}`), "", true, false},
+ {"value", "", "", "z", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, "", true, false},
+ {"regex", "^z", "", "z", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, "", true, false},
+ {"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, nil, []byte(`{"a": "z"}`), "", true, false},
+ {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, nil, []byte(`{"a": "z"}`), "", true, false},
+ {"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, nil, []byte(`{"a": "z"}`), "", true, false},
+ {"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, nil, []byte(`{"a": "z"}`), "", true, false},
// failures
- {"value", "", "", "X", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, "", false, false},
- {"regex", "^X", "", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, "", false, false},
- {"value", "", "2", "X", "", Argument{"header", "a", "", false}, &map[string]interface{}{"Y": "z"}, nil, nil, nil, []byte{}, "", false, true}, // reference invalid header
+ {"value", "", "", "X", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, "", false, false},
+ {"regex", "^X", "", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, "", false, false},
+ {"value", "", "2", "X", "", Argument{"header", "a", "", false}, map[string]interface{}{"Y": "z"}, nil, nil, nil, []byte{}, "", false, true}, // reference invalid header
// errors
- {"regex", "*", "", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, "", false, true}, // invalid regex
- {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": ""}, nil, nil, nil, []byte{}, "", false, true}, // invalid hmac
- {"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": ""}, nil, nil, nil, []byte{}, "", false, true}, // invalid hmac
+ {"regex", "*", "", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, "", false, true}, // invalid regex
+ {"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, nil, []byte{}, "", false, true}, // invalid hmac
+ {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, nil, []byte{}, "", false, true}, // invalid hmac
+ {"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, nil, []byte{}, "", false, true}, // invalid hmac
+ {"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, nil, []byte{}, "", false, true}, // invalid hmac
+ {"payload-hmac-sha512", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, nil, []byte{}, "", false, true}, // invalid hmac
+ {"payload-hash-sha512", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, nil, []byte{}, "", false, true}, // invalid hmac
+
// IP whitelisting, valid cases
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
@@ -484,7 +529,17 @@ var matchRuleTests = []struct {
func TestMatchRule(t *testing.T) {
for i, tt := range matchRuleTests {
r := MatchRule{tt.typ, tt.regex, tt.secret, tt.value, tt.param, tt.ipRange}
- ok, err := r.Evaluate(tt.headers, tt.query, tt.payload, tt.context, &tt.body, tt.remoteAddr)
+ req := &Request{
+ Headers: tt.headers,
+ Query: tt.query,
+ Payload: tt.payload,
+ Context: tt.context,
+ Body: tt.body,
+ RawRequest: &http.Request{
+ RemoteAddr: tt.remoteAddr,
+ },
+ }
+ ok, err := r.Evaluate(req)
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)
}
@@ -494,7 +549,7 @@ func TestMatchRule(t *testing.T) {
var andRuleTests = []struct {
desc string // description of the test case
rule AndRule
- headers, query, payload, context *map[string]interface{}
+ headers, query, payload, context map[string]interface{}
body []byte
ok bool
err bool
@@ -505,7 +560,7 @@ var andRuleTests = []struct {
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
},
- &map[string]interface{}{"A": "z", "B": "y"}, nil, nil, nil, []byte{},
+ map[string]interface{}{"A": "z", "B": "y"}, nil, nil, nil, []byte{},
true, false,
},
{
@@ -514,7 +569,7 @@ var andRuleTests = []struct {
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
},
- &map[string]interface{}{"A": "z", "B": "Y"}, nil, nil, nil, []byte{},
+ map[string]interface{}{"A": "z", "B": "Y"}, nil, nil, nil, []byte{},
false, false,
},
// Complex test to cover Rules.Evaluate
@@ -540,7 +595,7 @@ var andRuleTests = []struct {
},
},
},
- &map[string]interface{}{"A": "z", "B": "y", "C": "x", "D": "w", "E": "X", "F": "X"}, nil, nil, nil, []byte{},
+ map[string]interface{}{"A": "z", "B": "y", "C": "x", "D": "w", "E": "X", "F": "X"}, nil, nil, nil, []byte{},
true, false,
},
{"empty rule", AndRule{{}}, nil, nil, nil, nil, nil, false, false},
@@ -548,14 +603,21 @@ var andRuleTests = []struct {
{
"invalid rule",
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}},
- &map[string]interface{}{"Y": "z"}, nil, nil, nil, nil,
+ map[string]interface{}{"Y": "z"}, nil, nil, nil, nil,
false, true,
},
}
func TestAndRule(t *testing.T) {
for _, tt := range andRuleTests {
- ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, tt.context, &tt.body, "")
+ r := &Request{
+ Headers: tt.headers,
+ Query: tt.query,
+ Payload: tt.payload,
+ Context: tt.context,
+ Body: tt.body,
+ }
+ ok, err := tt.rule.Evaluate(r)
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)
}
@@ -565,7 +627,7 @@ func TestAndRule(t *testing.T) {
var orRuleTests = []struct {
desc string // description of the test case
rule OrRule
- headers, query, payload, context *map[string]interface{}
+ headers, query, payload, context map[string]interface{}
body []byte
ok bool
err bool
@@ -576,7 +638,7 @@ var orRuleTests = []struct {
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
},
- &map[string]interface{}{"A": "z", "B": "X"}, nil, nil, nil, []byte{},
+ map[string]interface{}{"A": "z", "B": "X"}, nil, nil, nil, []byte{},
true, false,
},
{
@@ -585,7 +647,7 @@ var orRuleTests = []struct {
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
},
- &map[string]interface{}{"A": "X", "B": "y"}, nil, nil, nil, []byte{},
+ map[string]interface{}{"A": "X", "B": "y"}, nil, nil, nil, []byte{},
true, false,
},
{
@@ -594,7 +656,7 @@ var orRuleTests = []struct {
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
},
- &map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil, nil, []byte{},
+ map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil, nil, []byte{},
false, false,
},
// failures
@@ -603,14 +665,21 @@ var orRuleTests = []struct {
OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
},
- &map[string]interface{}{"Y": "Z"}, nil, nil, nil, []byte{},
+ map[string]interface{}{"Y": "Z"}, nil, nil, nil, []byte{},
false, true,
},
}
func TestOrRule(t *testing.T) {
for _, tt := range orRuleTests {
- ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, tt.context, &tt.body, "")
+ r := &Request{
+ Headers: tt.headers,
+ Query: tt.query,
+ Payload: tt.payload,
+ Context: tt.context,
+ Body: tt.body,
+ }
+ ok, err := tt.rule.Evaluate(r)
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)
}
@@ -620,18 +689,25 @@ func TestOrRule(t *testing.T) {
var notRuleTests = []struct {
desc string // description of the test case
rule NotRule
- headers, query, payload, context *map[string]interface{}
+ headers, query, payload, context map[string]interface{}
body []byte
ok bool
err bool
}{
- {"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}, &map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, true, false},
- {"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, &map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, false, false},
+ {"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, true, false},
+ {"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, 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.context, &tt.body, "")
+ r := &Request{
+ Headers: tt.headers,
+ Query: tt.query,
+ Payload: tt.payload,
+ Context: tt.context,
+ Body: tt.body,
+ }
+ ok, err := tt.rule.Evaluate(r)
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)
}
diff --git a/internal/middleware/dumper.go b/internal/middleware/dumper.go
index 582c163..ae0e110 100644
--- a/internal/middleware/dumper.go
+++ b/internal/middleware/dumper.go
@@ -8,13 +8,11 @@ import (
"bytes"
"fmt"
"io"
- "io/ioutil"
"net"
"net/http"
+ "net/http/httputil"
"sort"
"strings"
-
- "github.com/gorilla/mux"
)
// responseDupper tees the response to a buffer and a response writer.
@@ -34,70 +32,53 @@ func Dumper(w io.Writer) func(http.Handler) http.Handler {
// Request ID
rid := r.Context().Value(RequestIDKey)
- // Request URL
- buf.WriteString(fmt.Sprintf("> [%s] %s %s", rid, r.Method, r.URL.String()))
+ // Dump request
- // Request Headers
- keys := make([]string, len(r.Header))
- i := 0
- for k := range r.Header {
- keys[i] = k
- i++
- }
- sort.Strings(keys)
- for _, k := range keys {
- buf.WriteString(fmt.Sprintf("\n> [%s] %s: %s", rid, k, strings.Join(r.Header[k], ", ")))
- }
-
- // Request parameters
- params := mux.Vars(r)
- keys = make([]string, len(params))
- i = 0
- for k := range params {
- keys[i] = k
- i++
- }
- sort.Strings(keys)
- for _, k := range keys {
- buf.WriteString(fmt.Sprintf("\n> [%s] %s: %s", rid, k, strings.Join(r.Header[k], ", ")))
- }
-
- // Request body
- b, err := ioutil.ReadAll(r.Body)
+ bd, err := httputil.DumpRequest(r, true)
if err != nil {
- b = []byte("failed to read body: " + err.Error())
+ buf.WriteString(fmt.Sprintf("[%s] Error dumping request for debugging: %s\n", rid, err))
}
- if len(b) > 0 {
- buf.WriteByte('\n')
- lines := strings.Split(string(b), "\n")
- for _, line := range lines {
- buf.WriteString(fmt.Sprintf("> [%s] %s\n", rid, line))
- }
+
+ sc := bufio.NewScanner(bytes.NewBuffer(bd))
+ sc.Split(bufio.ScanLines)
+ for sc.Scan() {
+ buf.WriteString(fmt.Sprintf("> [%s] ", rid))
+ buf.WriteString(sc.Text() + "\n")
}
- r.Body = ioutil.NopCloser(bytes.NewBuffer(b))
+
+ w.Write(buf.Bytes())
+ buf.Reset()
+
+ // Dump Response
dupper := &responseDupper{ResponseWriter: rw, Buffer: &bytes.Buffer{}}
h.ServeHTTP(dupper, r)
- buf.WriteString(fmt.Sprintf("\n< [%s] %s", rid, http.StatusText(dupper.Status)))
- keys = make([]string, len(dupper.Header()))
- i = 0
+ // Response Status
+ buf.WriteString(fmt.Sprintf("< [%s] %d %s\n", rid, dupper.Status, http.StatusText(dupper.Status)))
+
+ // Response Headers
+ keys := make([]string, len(dupper.Header()))
+ i := 0
for k := range dupper.Header() {
keys[i] = k
i++
}
sort.Strings(keys)
for _, k := range keys {
- buf.WriteString(fmt.Sprintf("\n< [%s] %s: %s", rid, k, strings.Join(dupper.Header()[k], ", ")))
+ buf.WriteString(fmt.Sprintf("< [%s] %s: %s\n", rid, k, strings.Join(dupper.Header()[k], ", ")))
}
+
+ // Response Body
if dupper.Buffer.Len() > 0 {
- buf.WriteByte('\n')
- lines := strings.Split(dupper.Buffer.String(), "\n")
- for _, line := range lines {
- buf.WriteString(fmt.Sprintf("< [%s] %s\n", rid, line))
+ buf.WriteString(fmt.Sprintf("< [%s]\n", rid))
+ sc = bufio.NewScanner(dupper.Buffer)
+ sc.Split(bufio.ScanLines)
+ for sc.Scan() {
+ buf.WriteString(fmt.Sprintf("< [%s] ", rid))
+ buf.WriteString(sc.Text() + "\n")
}
}
- buf.WriteByte('\n')
w.Write(buf.Bytes())
})
}
diff --git a/internal/middleware/logger.go b/internal/middleware/logger.go
index 50c6a44..899dd1c 100644
--- a/internal/middleware/logger.go
+++ b/internal/middleware/logger.go
@@ -39,18 +39,18 @@ type LogEntry struct {
}
// Write constructs and writes the final log entry.
-func (l *LogEntry) Write(status, bytes int, elapsed time.Duration) {
+func (l *LogEntry) Write(status, totalBytes int, elapsed time.Duration) {
rid := GetReqID(l.req.Context())
if rid != "" {
fmt.Fprintf(l.buf, "[%s] ", rid)
}
- fmt.Fprintf(l.buf, "%03d | %s | %s | ", status, humanize.IBytes(uint64(bytes)), elapsed)
+ fmt.Fprintf(l.buf, "%03d | %s | %s | ", status, humanize.IBytes(uint64(totalBytes)), elapsed)
l.buf.WriteString(l.req.Host + " | " + l.req.Method + " " + l.req.RequestURI)
log.Print(l.buf.String())
}
-/// Panic prints the call stack for a panic.
+// Panic prints the call stack for a panic.
func (l *LogEntry) Panic(v interface{}, stack []byte) {
e := l.NewLogEntry(l.req).(*LogEntry)
fmt.Fprintf(e.buf, "panic: %#v", v)
diff --git a/internal/pidfile/pidfile.go b/internal/pidfile/pidfile.go
index c88c159..e602dcb 100644
--- a/internal/pidfile/pidfile.go
+++ b/internal/pidfile/pidfile.go
@@ -35,10 +35,10 @@ func New(path string) (*PIDFile, error) {
return nil, err
}
// Note MkdirAll returns nil if a directory already exists
- if err := MkdirAll(filepath.Dir(path), os.FileMode(0755)); err != nil {
+ if err := MkdirAll(filepath.Dir(path), os.FileMode(0o755)); err != nil {
return nil, err
}
- if err := ioutil.WriteFile(path, []byte(fmt.Sprintf("%d", os.Getpid())), 0644); err != nil {
+ if err := ioutil.WriteFile(path, []byte(fmt.Sprintf("%d", os.Getpid())), 0o600); err != nil {
return nil, err
}
diff --git a/test/hooks.json.tmpl b/test/hooks.json.tmpl
index 0c7fc0c..3075762 100644
--- a/test/hooks.json.tmpl
+++ b/test/hooks.json.tmpl
@@ -31,7 +31,7 @@
{
"match":
{
- "type": "payload-hash-sha1",
+ "type": "payload-hmac-sha1",
"secret": "mysecret",
"parameter":
{
@@ -168,6 +168,22 @@
],
}
},
+ {
+ "id": "sendgrid",
+ "execute-command": "{{ .Hookecho }}",
+ "command-working-directory": "/",
+ "response-message": "success",
+ "trigger-rule": {
+ "match": {
+ "type": "value",
+ "parameter": {
+ "source": "payload",
+ "name": "root.0.event"
+ },
+ "value": "processed"
+ }
+ }
+ },
{
"id": "plex",
"execute-command": "{{ .Hookecho }}",
@@ -259,5 +275,29 @@
"name": "passed"
}
],
+ },
+ {
+ "id": "empty-payload-signature",
+ "execute-command": "{{ .Hookecho }}",
+ "command-working-directory": "/",
+ "include-command-output-in-response": true,
+ "trigger-rule":
+ {
+ "and":
+ [
+ {
+ "match":
+ {
+ "type": "payload-hmac-sha1",
+ "secret": "mysecret",
+ "parameter":
+ {
+ "source": "header",
+ "name": "X-Hub-Signature"
+ }
+ }
+ }
+ ]
+ }
}
]
diff --git a/test/hooks.yaml.tmpl b/test/hooks.yaml.tmpl
index f1e1204..16aa8c1 100644
--- a/test/hooks.yaml.tmpl
+++ b/test/hooks.yaml.tmpl
@@ -8,7 +8,7 @@
source: header
name: X-Hub-Signature
secret: mysecret
- type: payload-hash-sha1
+ type: payload-hmac-sha1
- match:
parameter:
source: payload
@@ -97,6 +97,18 @@
name: "app.messages.message.#text"
value: "Hello!!"
+- id: sendgrid
+ execute-command: '{{ .Hookecho }}'
+ command-working-directory: /
+ response-message: success
+ trigger-rule:
+ match:
+ type: value
+ parameter:
+ source: payload
+ name: root.0.event
+ value: processed
+
- id: plex
trigger-rule:
match:
@@ -150,3 +162,16 @@
- id: warn-on-space
execute-command: '{{ .Hookecho }} foo'
include-command-output-in-response: true
+
+- id: empty-payload-signature
+ include-command-output-in-response: true
+ execute-command: '{{ .Hookecho }}'
+ command-working-directory: /
+ trigger-rule:
+ and:
+ - match:
+ parameter:
+ source: header
+ name: X-Hub-Signature
+ secret: mysecret
+ type: payload-hmac-sha1
diff --git a/tls.go b/tls.go
index 8e6cb73..526fd36 100644
--- a/tls.go
+++ b/tls.go
@@ -8,7 +8,7 @@ import (
)
func writeTLSSupportedCipherStrings(w io.Writer, min uint16) error {
- for _, c := range CipherSuites() {
+ for _, c := range tls.CipherSuites() {
var found bool
for _, v := range c.SupportedVersions {
@@ -50,7 +50,7 @@ func getTLSMinVersion(v string) uint16 {
// getTLSCipherSuites converts a comma separated list of cipher suites into a
// slice of TLS cipher suite IDs.
func getTLSCipherSuites(v string) []uint16 {
- supported := CipherSuites()
+ supported := tls.CipherSuites()
if v == "" {
suites := make([]uint16, len(supported))
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 06d8a6a..8c121cf 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -1,20 +1,37 @@
# github.com/clbanning/mxj v1.8.4
+## explicit
github.com/clbanning/mxj
# github.com/dustin/go-humanize v1.0.0
+## explicit
github.com/dustin/go-humanize
+# github.com/fsnotify/fsnotify v1.4.7
+## explicit
# github.com/ghodss/yaml v1.0.0
+## explicit
github.com/ghodss/yaml
# github.com/go-chi/chi v4.0.2+incompatible
+## explicit
github.com/go-chi/chi
github.com/go-chi/chi/middleware
# github.com/gofrs/uuid v3.2.0+incompatible
+## explicit
github.com/gofrs/uuid
# github.com/gorilla/mux v1.7.3
+## explicit
github.com/gorilla/mux
+# github.com/kr/pretty v0.1.0
+## explicit
+# golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553
+## explicit
# golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8
+## explicit
golang.org/x/sys/unix
golang.org/x/sys/windows
+# gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15
+## explicit
# gopkg.in/fsnotify.v1 v1.4.2
+## explicit
gopkg.in/fsnotify.v1
# gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7
+## explicit
gopkg.in/yaml.v2
diff --git a/webhook.go b/webhook.go
index fb0116a..422e966 100644
--- a/webhook.go
+++ b/webhook.go
@@ -18,6 +18,7 @@ import (
"path/filepath"
"strings"
"time"
+ "unicode"
"github.com/adnanh/webhook/internal/hook"
"github.com/adnanh/webhook/internal/middleware"
@@ -30,7 +31,7 @@ import (
)
const (
- version = "2.6.11"
+ version = "2.7.0"
)
var (
@@ -143,7 +144,7 @@ func main() {
}
if *logPath != "" {
- file, err := os.OpenFile(*logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
+ file, err := os.OpenFile(*logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
if err != nil {
logQueue = append(logQueue, fmt.Sprintf("error opening log file %q: %v", *logPath, err))
// we'll bail out below
@@ -303,10 +304,14 @@ func main() {
}
func hookHandler(w http.ResponseWriter, r *http.Request) {
- rid := middleware.GetReqID(r.Context())
+ req := &hook.Request{
+ ID: middleware.GetReqID(r.Context()),
+ RawRequest: r,
+ }
- log.Printf("[%s] incoming HTTP %s request from %s\n", rid, r.Method, r.RemoteAddr)
+ log.Printf("[%s] incoming HTTP %s request from %s\n", req.ID, r.Method, r.RemoteAddr)
+ // TODO: rename this to avoid confusion with Request.ID
id := mux.Vars(r)["id"]
matchedHook := matchLoadedHook(id)
@@ -342,138 +347,89 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
if !allowedMethod {
w.WriteHeader(http.StatusMethodNotAllowed)
- log.Printf("[%s] HTTP %s method not allowed for hook %q", rid, r.Method, id)
+ log.Printf("[%s] HTTP %s method not allowed for hook %q", req.ID, r.Method, id)
return
}
- log.Printf("[%s] %s got matched\n", rid, id)
+ log.Printf("[%s] %s got matched\n", req.ID, id)
for _, responseHeader := range responseHeaders {
w.Header().Set(responseHeader.Name, responseHeader.Value)
}
- var (
- body []byte
- err error
- )
+ var err error
// set contentType to IncomingPayloadContentType or header value
- contentType := r.Header.Get("Content-Type")
+ req.ContentType = r.Header.Get("Content-Type")
if len(matchedHook.IncomingPayloadContentType) != 0 {
- contentType = matchedHook.IncomingPayloadContentType
+ req.ContentType = matchedHook.IncomingPayloadContentType
}
- isMultipart := strings.HasPrefix(contentType, "multipart/form-data;")
+ isMultipart := strings.HasPrefix(req.ContentType, "multipart/form-data;")
if !isMultipart {
- body, err = ioutil.ReadAll(r.Body)
+ req.Body, err = ioutil.ReadAll(r.Body)
if err != nil {
- log.Printf("[%s] error reading the request body: %+v\n", rid, err)
+ log.Printf("[%s] error reading the request body: %+v\n", req.ID, err)
}
}
// parse headers
- headers := valuesToMap(r.Header)
+ req.Headers = valuesToMap(r.Header)
// parse query variables
- query := valuesToMap(r.URL.Query())
+ req.Query = valuesToMap(r.URL.Query())
// parse body
- var payload map[string]interface{}
- // parse context
- var context map[string]interface{}
-
- if matchedHook.PreHookCommand != "" {
- // check the command exists
- preHookCommandPath, err := exec.LookPath(matchedHook.PreHookCommand)
- if err != nil {
- // give a last chance, maybe it's a relative path
- preHookCommandPathRelativeToCurrentWorkingDirectory := filepath.Join(matchedHook.CommandWorkingDirectory, matchedHook.PreHookCommand)
- // check the command exists
- preHookCommandPath, err = exec.LookPath(preHookCommandPathRelativeToCurrentWorkingDirectory)
- }
-
- if err != nil {
- log.Printf("[%s] unable to locate pre-hook command: '%s', %+v\n", rid, matchedHook.PreHookCommand, err)
- // check if parameters specified in pre-hook command by mistake
- if strings.IndexByte(matchedHook.PreHookCommand, ' ') != -1 {
- s := strings.Fields(matchedHook.PreHookCommand)[0]
- log.Printf("[%s] please use a wrapper script to provide arguments to pre-hook command for '%s'\n", rid, s)
- }
- } else {
- preHookCommandStdin := hook.PreHookContext{
- HookID: matchedHook.ID,
- Method: r.Method,
- Base64EncodedBody: base64.StdEncoding.EncodeToString(body),
- RemoteAddr: r.RemoteAddr,
- URI: r.RequestURI,
- Host: r.Host,
- Headers: r.Header,
- Query: r.URL.Query(),
- }
-
- if preHookCommandStdinJSONString, err := json.Marshal(preHookCommandStdin); err != nil {
- log.Printf("[%s] unable to encode pre-hook context as JSON string for the pre-hook command: %+v\n", rid, err)
- } else {
- preHookCommand := exec.Command(preHookCommandPath)
- preHookCommand.Dir = matchedHook.CommandWorkingDirectory
- preHookCommand.Env = append(os.Environ())
-
- if preHookCommandStdinPipe, err := preHookCommand.StdinPipe(); err != nil {
- log.Printf("[%s] unable to acquire stdin pipe for the pre-hook command: %+v\n", rid, err)
- } else {
- _, err := io.WriteString(preHookCommandStdinPipe, string(preHookCommandStdinJSONString))
- preHookCommandStdinPipe.Close()
- if err != nil {
- log.Printf("[%s] unable to write to pre-hook command stdin: %+v\n", rid, err)
- } else {
- log.Printf("[%s] executing pre-hook command %s (%s) using %s as cwd\n", rid, matchedHook.PreHookCommand, preHookCommand.Path, preHookCommand.Dir)
-
- if preHookCommandOutput, err := preHookCommand.CombinedOutput(); err != nil {
- log.Printf("[%s] unable to execute pre-hook command: %+v\n", rid, err)
- } else {
- JSONDecoder := json.NewDecoder(strings.NewReader(string(preHookCommandOutput)))
- JSONDecoder.UseNumber()
-
- if err := JSONDecoder.Decode(&context); err != nil {
- log.Printf("[%s] unable to parse pre-hook command output: %+v\npre-hook command output was: %+v\n", rid, err, string(preHookCommandOutput))
- }
- }
- }
- }
- }
- }
- }
-
switch {
- case strings.Contains(contentType, "json"):
- decoder := json.NewDecoder(bytes.NewReader(body))
+ case strings.Contains(req.ContentType, "json"):
+ decoder := json.NewDecoder(bytes.NewReader(req.Body))
decoder.UseNumber()
- err := decoder.Decode(&payload)
- if err != nil {
- log.Printf("[%s] error parsing JSON payload %+v\n", rid, err)
+ var firstChar byte
+ for i := 0; i < len(req.Body); i++ {
+ if unicode.IsSpace(rune(req.Body[i])) {
+ continue
+ }
+ firstChar = req.Body[i]
+ break
}
- case strings.Contains(contentType, "x-www-form-urlencoded"):
- fd, err := url.ParseQuery(string(body))
- if err != nil {
- log.Printf("[%s] error parsing form payload %+v\n", rid, err)
+ if firstChar == byte('[') {
+ var arrayPayload interface{}
+ err := decoder.Decode(&arrayPayload)
+ if err != nil {
+ log.Printf("[%s] error parsing JSON array payload %+v\n", req.ID, err)
+ }
+
+ req.Payload = make(map[string]interface{}, 1)
+ req.Payload["root"] = arrayPayload
} else {
- payload = valuesToMap(fd)
+ err := decoder.Decode(&req.Payload)
+ if err != nil {
+ log.Printf("[%s] error parsing JSON payload %+v\n", req.ID, err)
+ }
}
- case strings.Contains(contentType, "xml"):
- payload, err = mxj.NewMapXmlReader(bytes.NewReader(body))
+ case strings.Contains(req.ContentType, "x-www-form-urlencoded"):
+ fd, err := url.ParseQuery(string(req.Body))
if err != nil {
- log.Printf("[%s] error parsing XML payload: %+v\n", rid, err)
+ log.Printf("[%s] error parsing form payload %+v\n", req.ID, err)
+ } else {
+ req.Payload = valuesToMap(fd)
+ }
+
+ case strings.Contains(req.ContentType, "xml"):
+ req.Payload, err = mxj.NewMapXmlReader(bytes.NewReader(req.Body))
+ if err != nil {
+ log.Printf("[%s] error parsing XML payload: %+v\n", req.ID, err)
}
case isMultipart:
err = r.ParseMultipartForm(*maxMultipartMem)
if err != nil {
- msg := fmt.Sprintf("[%s] error parsing multipart form: %+v\n", rid, err)
+ msg := fmt.Sprintf("[%s] error parsing multipart form: %+v\n", req.ID, err)
log.Println(msg)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "Error occurred while parsing multipart form.")
@@ -481,14 +437,14 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
}
for k, v := range r.MultipartForm.Value {
- log.Printf("[%s] found multipart form value %q", rid, k)
+ log.Printf("[%s] found multipart form value %q", req.ID, k)
- if payload == nil {
- payload = make(map[string]interface{})
+ if req.Payload == nil {
+ req.Payload = make(map[string]interface{})
}
// TODO(moorereason): support duplicate, named values
- payload[k] = v[0]
+ req.Payload[k] = v[0]
}
for k, v := range r.MultipartForm.File {
@@ -517,11 +473,11 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
}
if parseAsJSON {
- log.Printf("[%s] parsing multipart form file %q as JSON\n", rid, k)
+ log.Printf("[%s] parsing multipart form file %q as JSON\n", req.ID, k)
f, err := v[0].Open()
if err != nil {
- msg := fmt.Sprintf("[%s] error parsing multipart form file: %+v\n", rid, err)
+ msg := fmt.Sprintf("[%s] error parsing multipart form file: %+v\n", req.ID, err)
log.Println(msg)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "Error occurred while parsing multipart form file.")
@@ -534,24 +490,87 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
var part map[string]interface{}
err = decoder.Decode(&part)
if err != nil {
- log.Printf("[%s] error parsing JSON payload file: %+v\n", rid, err)
+ log.Printf("[%s] error parsing JSON payload file: %+v\n", req.ID, err)
}
- if payload == nil {
- payload = make(map[string]interface{})
+ if req.Payload == nil {
+ req.Payload = make(map[string]interface{})
}
- payload[k] = part
+ req.Payload[k] = part
}
}
default:
- log.Printf("[%s] error parsing body payload due to unsupported content type header: %s\n", rid, contentType)
+ log.Printf("[%s] error parsing body payload due to unsupported content type header: %s\n", req.ID, req.ContentType)
+ }
+
+ if matchedHook.PreHookCommand != "" {
+ // check the command exists
+ var lookpath string
+ if filepath.IsAbs(matchedHook.PreHookCommand) || matchedHook.CommandWorkingDirectory == "" {
+ lookpath = matchedHook.PreHookCommand
+ } else {
+ lookpath = filepath.Join(matchedHook.CommandWorkingDirectory, matchedHook.PreHookCommand)
+ }
+
+ preHookCommandPath, err := exec.LookPath(lookpath)
+
+ if err != nil {
+ log.Printf("[%s] unable to locate pre-hook command: '%s', %+v\n", req.ID, matchedHook.PreHookCommand, err)
+ // check if parameters specified in pre-hook command by mistake
+ if strings.IndexByte(matchedHook.PreHookCommand, ' ') != -1 {
+ s := strings.Fields(matchedHook.PreHookCommand)[0]
+ log.Printf("[%s] please use a wrapper script to provide arguments to pre-hook command for '%s'\n", req.ID, s)
+ }
+ } else {
+ preHookCommandStdin := hook.PreHookContext{
+ HookID: matchedHook.ID,
+ Method: r.Method,
+ Base64EncodedBody: base64.StdEncoding.EncodeToString(req.Body),
+ RemoteAddr: r.RemoteAddr,
+ URI: r.RequestURI,
+ Host: r.Host,
+ Headers: r.Header,
+ Query: r.URL.Query(),
+ }
+
+ if preHookCommandStdinJSONString, err := json.Marshal(preHookCommandStdin); err != nil {
+ log.Printf("[%s] unable to encode pre-hook context as JSON string for the pre-hook command: %+v\n", req.ID, err)
+ } else {
+ preHookCommand := exec.Command(preHookCommandPath)
+ preHookCommand.Dir = matchedHook.CommandWorkingDirectory
+ preHookCommand.Env = append(os.Environ())
+
+ if preHookCommandStdinPipe, err := preHookCommand.StdinPipe(); err != nil {
+ log.Printf("[%s] unable to acquire stdin pipe for the pre-hook command: %+v\n", req.ID, err)
+ } else {
+ _, err := io.WriteString(preHookCommandStdinPipe, string(preHookCommandStdinJSONString))
+ preHookCommandStdinPipe.Close()
+ if err != nil {
+ log.Printf("[%s] unable to write to pre-hook command stdin: %+v\n", req.ID, err)
+ } else {
+ log.Printf("[%s] executing pre-hook command %s (%s) using %s as cwd\n", req.ID, matchedHook.PreHookCommand, preHookCommand.Path, preHookCommand.Dir)
+
+ if preHookCommandOutput, err := preHookCommand.CombinedOutput(); err != nil {
+ log.Printf("[%s] unable to execute pre-hook command: %+v\n", req.ID, err)
+ } else {
+ JSONDecoder := json.NewDecoder(strings.NewReader(string(preHookCommandOutput)))
+ JSONDecoder.UseNumber()
+
+ if err := JSONDecoder.Decode(&req.Context); err != nil {
+ log.Printf("[%s] unable to parse pre-hook command output: %+v\npre-hook command output was: %+v\n", req.ID, err, string(preHookCommandOutput))
+ }
+ }
+ }
+ }
+ }
+ }
}
// handle hook
- errors := matchedHook.ParseJSONParameters(&headers, &query, &payload, &context)
+ errors := matchedHook.ParseJSONParameters(req)
for _, err := range errors {
- log.Printf("[%s] error parsing JSON parameters: %s\n", rid, err)
+ log.Printf("[%s] error parsing JSON parameters: %s\n", req.ID, err)
}
var ok bool
@@ -559,29 +578,29 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
if matchedHook.TriggerRule == nil {
ok = true
} else {
- ok, err = matchedHook.TriggerRule.Evaluate(&headers, &query, &payload, &context, &body, r.RemoteAddr)
+ ok, err = matchedHook.TriggerRule.Evaluate(req)
if err != nil {
if !hook.IsParameterNodeError(err) {
- msg := fmt.Sprintf("[%s] error evaluating hook: %s", rid, err)
+ msg := fmt.Sprintf("[%s] error evaluating hook: %s", req.ID, err)
log.Println(msg)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "Error occurred while evaluating hook rules.")
return
}
- log.Printf("[%s] %v", rid, err)
+ log.Printf("[%s] %v", req.ID, err)
}
}
if ok {
- log.Printf("[%s] %s hook triggered successfully\n", rid, matchedHook.ID)
+ log.Printf("[%s] %s hook triggered successfully\n", req.ID, matchedHook.ID)
for _, responseHeader := range matchedHook.ResponseHeaders {
w.Header().Set(responseHeader.Name, responseHeader.Value)
}
if matchedHook.CaptureCommandOutput {
- response, err := handleHook(matchedHook, rid, &headers, &query, &payload, &context, &body)
+ response, err := handleHook(matchedHook, req)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
@@ -594,16 +613,16 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
} else {
// Check if a success return code is configured for the hook
if matchedHook.SuccessHttpResponseCode != 0 {
- writeHttpResponseCode(w, rid, matchedHook.ID, matchedHook.SuccessHttpResponseCode)
+ writeHttpResponseCode(w, req.ID, matchedHook.ID, matchedHook.SuccessHttpResponseCode)
}
fmt.Fprint(w, response)
}
} else {
- go handleHook(matchedHook, rid, &headers, &query, &payload, &context, &body)
+ go handleHook(matchedHook, req)
// Check if a success return code is configured for the hook
if matchedHook.SuccessHttpResponseCode != 0 {
- writeHttpResponseCode(w, rid, matchedHook.ID, matchedHook.SuccessHttpResponseCode)
+ writeHttpResponseCode(w, req.ID, matchedHook.ID, matchedHook.SuccessHttpResponseCode)
}
fmt.Fprint(w, matchedHook.ResponseMessage)
@@ -613,34 +632,34 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
// Check if a return code is configured for the hook
if matchedHook.TriggerRuleMismatchHttpResponseCode != 0 {
- writeHttpResponseCode(w, rid, matchedHook.ID, matchedHook.TriggerRuleMismatchHttpResponseCode)
+ writeHttpResponseCode(w, req.ID, matchedHook.ID, matchedHook.TriggerRuleMismatchHttpResponseCode)
}
// if none of the hooks got triggered
- log.Printf("[%s] %s got matched, but didn't get triggered because the trigger rules were not satisfied\n", rid, matchedHook.ID)
+ log.Printf("[%s] %s got matched, but didn't get triggered because the trigger rules were not satisfied\n", req.ID, matchedHook.ID)
fmt.Fprint(w, "Hook rules were not satisfied.")
}
-func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]interface{}, context *map[string]interface{}, body *[]byte) (string, error) {
+func handleHook(h *hook.Hook, r *hook.Request) (string, error) {
var errors []error
// check the command exists
- cmdPath, err := exec.LookPath(h.ExecuteCommand)
- if err != nil {
- // give a last chance, maybe is a relative path
- relativeToCwd := filepath.Join(h.CommandWorkingDirectory, h.ExecuteCommand)
- // check the command exists
- cmdPath, err = exec.LookPath(relativeToCwd)
+ var lookpath string
+ if filepath.IsAbs(h.ExecuteCommand) || h.CommandWorkingDirectory == "" {
+ lookpath = h.ExecuteCommand
+ } else {
+ lookpath = filepath.Join(h.CommandWorkingDirectory, h.ExecuteCommand)
}
+ cmdPath, err := exec.LookPath(lookpath)
if err != nil {
- log.Printf("[%s] unable to locate command: '%s'\n", rid, h.ExecuteCommand)
+ log.Printf("[%s] error in %s", r.ID, err)
// check if parameters specified in execute-command by mistake
if strings.IndexByte(h.ExecuteCommand, ' ') != -1 {
s := strings.Fields(h.ExecuteCommand)[0]
- log.Printf("[%s] please use 'pass-arguments-to-command' to specify args for '%s'\n", rid, s)
+ log.Printf("[%s] use 'pass-arguments-to-command' to specify args for '%s'", r.ID, s)
}
return "", err
@@ -649,37 +668,37 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in
cmd := exec.Command(cmdPath)
cmd.Dir = h.CommandWorkingDirectory
- cmd.Args, errors = h.ExtractCommandArguments(headers, query, payload, context)
+ cmd.Args, errors = h.ExtractCommandArguments(r)
for _, err := range errors {
- log.Printf("[%s] error extracting command arguments: %s\n", rid, err)
+ log.Printf("[%s] error extracting command arguments: %s\n", r.ID, err)
}
var envs []string
- envs, errors = h.ExtractCommandArgumentsForEnv(headers, query, payload, context)
+ envs, errors = h.ExtractCommandArgumentsForEnv(r)
for _, err := range errors {
- log.Printf("[%s] error extracting command arguments for environment: %s\n", rid, err)
+ log.Printf("[%s] error extracting command arguments for environment: %s\n", r.ID, err)
}
- files, errors := h.ExtractCommandArgumentsForFile(headers, query, payload, context)
+ files, errors := h.ExtractCommandArgumentsForFile(r)
for _, err := range errors {
- log.Printf("[%s] error extracting command arguments for file: %s\n", rid, err)
+ log.Printf("[%s] error extracting command arguments for file: %s\n", r.ID, err)
}
for i := range files {
tmpfile, err := ioutil.TempFile(h.CommandWorkingDirectory, files[i].EnvName)
if err != nil {
- log.Printf("[%s] error creating temp file [%s]\n", rid, err)
+ log.Printf("[%s] error creating temp file [%s]", r.ID, err)
continue
}
- log.Printf("[%s] writing env %s file %s", rid, files[i].EnvName, tmpfile.Name())
+ log.Printf("[%s] writing env %s file %s", r.ID, files[i].EnvName, tmpfile.Name())
if _, err := tmpfile.Write(files[i].Data); err != nil {
- log.Printf("[%s] error writing file %s [%s]\n", rid, tmpfile.Name(), err)
+ log.Printf("[%s] error writing file %s [%s]", r.ID, tmpfile.Name(), err)
continue
}
if err := tmpfile.Close(); err != nil {
- log.Printf("[%s] error closing file %s [%s]\n", rid, tmpfile.Name(), err)
+ log.Printf("[%s] error closing file %s [%s]", r.ID, tmpfile.Name(), err)
continue
}
@@ -689,32 +708,32 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in
cmd.Env = append(os.Environ(), envs...)
- log.Printf("[%s] executing %s (%s) with arguments %q and environment %s using %s as cwd\n", rid, h.ExecuteCommand, cmd.Path, cmd.Args, envs, cmd.Dir)
+ log.Printf("[%s] executing %s (%s) with arguments %q and environment %s using %s as cwd\n", r.ID, h.ExecuteCommand, cmd.Path, cmd.Args, envs, cmd.Dir)
out, err := cmd.CombinedOutput()
- log.Printf("[%s] command output: %s\n", rid, out)
+ log.Printf("[%s] command output: %s\n", r.ID, out)
if err != nil {
- log.Printf("[%s] error occurred: %+v\n", rid, err)
+ log.Printf("[%s] error occurred: %+v\n", r.ID, err)
}
for i := range files {
if files[i].File != nil {
- log.Printf("[%s] removing file %s\n", rid, files[i].File.Name())
+ log.Printf("[%s] removing file %s\n", r.ID, files[i].File.Name())
err := os.Remove(files[i].File.Name())
if err != nil {
- log.Printf("[%s] error removing file %s [%s]\n", rid, files[i].File.Name(), err)
+ log.Printf("[%s] error removing file %s [%s]", r.ID, files[i].File.Name(), err)
}
}
}
- log.Printf("[%s] finished handling %s\n", rid, h.ID)
+ log.Printf("[%s] finished handling %s\n", r.ID, h.ID)
return string(out), err
}
-func writeHttpResponseCode(w http.ResponseWriter, rid string, hookId string, responseCode int) {
+func writeHttpResponseCode(w http.ResponseWriter, rid, hookId string, responseCode int) {
// Check if the given return code is supported by the http package
// by testing if there is a StatusText for this code.
if len(http.StatusText(responseCode)) > 0 {
diff --git a/webhook_test.go b/webhook_test.go
index 3b10060..c8a46cc 100644
--- a/webhook_test.go
+++ b/webhook_test.go
@@ -53,7 +53,11 @@ func TestStaticParams(t *testing.T) {
b := &bytes.Buffer{}
log.SetOutput(b)
- _, err = handleHook(spHook, "test", &spHeaders, &map[string]interface{}{}, &map[string]interface{}{}, &map[string]interface{}{}, &[]byte{})
+ r := &hook.Request{
+ ID: "test",
+ Headers: spHeaders,
+ }
+ _, err = handleHook(spHook, r)
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
@@ -77,7 +81,7 @@ func TestWebhook(t *testing.T) {
for _, tt := range hookHandlerTests {
t.Run(tt.desc+"@"+hookTmpl, func(t *testing.T) {
ip, port := serverAddress(t)
- args := []string{fmt.Sprintf("-hooks=%s", configPath), fmt.Sprintf("-ip=%s", ip), fmt.Sprintf("-port=%s", port), "-verbose"}
+ args := []string{fmt.Sprintf("-hooks=%s", configPath), fmt.Sprintf("-ip=%s", ip), fmt.Sprintf("-port=%s", port), "-debug"}
if len(tt.cliMethods) != 0 {
args = append(args, "-http-methods="+strings.Join(tt.cliMethods, ","))
@@ -111,6 +115,7 @@ func TestWebhook(t *testing.T) {
var res *http.Response
req.Header.Add("Content-Type", tt.contentType)
+ req.ContentLength = int64(len(tt.body))
client := &http.Client{}
res, err = client.Do(req)
@@ -171,7 +176,7 @@ func buildHookecho(t *testing.T) (binPath string, cleanupFn func()) {
return binPath, func() { os.RemoveAll(tmp) }
}
-func genConfig(t *testing.T, bin string, hookTemplate string) (configPath string, cleanupFn func()) {
+func genConfig(t *testing.T, bin, hookTemplate string) (configPath string, cleanupFn func()) {
tmpl := template.Must(template.ParseFiles(hookTemplate))
tmp, err := ioutil.TempDir("", "webhook-config-")
@@ -546,6 +551,28 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
`success`,
``,
},
+ {
+ "payload-json-array",
+ "sendgrid",
+ nil,
+ "POST",
+ nil,
+ "application/json",
+ `[
+ {
+ "email": "example@test.com",
+ "timestamp": 1513299569,
+ "smtp-id": "<14c5d75ce93.dfd.64b469@ismtpd-555>",
+ "event": "processed",
+ "category": "cat facts",
+ "sg_event_id": "sg_event_id",
+ "sg_message_id": "sg_message_id"
+ }
+]`,
+ http.StatusOK,
+ `success`,
+ ``,
+ },
{
"multipart",
"plex",
@@ -663,6 +690,19 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
``,
},
+ {
+ "empty-payload-signature", // allow empty payload signature validation
+ "empty-payload-signature",
+ nil,
+ "POST",
+ map[string]string{"X-Hub-Signature": "33f9d709782f62b8b4a0178586c65ab098a39fe2"},
+ "application/json",
+ ``,
+ http.StatusOK,
+ ``,
+ ``,
+ },
+
// test with disallowed global HTTP method
{"global disallowed method", "bitbucket", []string{"Post "}, "GET", nil, `{}`, "application/json", http.StatusMethodNotAllowed, ``, ``},
// test with disallowed HTTP method
@@ -684,7 +724,7 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
// Check logs
{"static params should pass", "static-params-ok", nil, "POST", nil, "application/json", `{}`, http.StatusOK, "arg: passed\n", `(?s)command output: arg: passed`},
- {"command with space logs warning", "warn-on-space", nil, "POST", nil, "application/json", `{}`, http.StatusInternalServerError, "Error occurred while executing the hook's command. Please check your logs for more details.", `(?s)unable to locate command.*use 'pass[-]arguments[-]to[-]command' to specify args`},
+ {"command with space logs warning", "warn-on-space", nil, "POST", nil, "application/json", `{}`, http.StatusInternalServerError, "Error occurred while executing the hook's command. Please check your logs for more details.", `(?s)error in exec:.*use 'pass[-]arguments[-]to[-]command' to specify args`},
{"unsupported content type error", "github", nil, "POST", map[string]string{"Content-Type": "nonexistent/format"}, "application/json", `{}`, http.StatusBadRequest, `Hook rules were not satisfied.`, `(?s)error parsing body payload due to unsupported content type header:`},
}