Merge branch 'development' into feature/context-provider-command
This commit is contained in:
commit
eece0137ef
23
.github/workflows/build.yml
vendored
Normal file
23
.github/workflows/build.yml
vendored
Normal file
@ -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 ./...
|
@ -1,7 +1,7 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.13.x
|
||||
- 1.14.x
|
||||
- master
|
||||
|
||||
os:
|
||||
|
18
Makefile
18
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
|
||||
|
22
README.md
22
README.md
@ -1,4 +1,4 @@
|
||||
# What is webhook?
|
||||
# What is webhook? ![build-status][badge]
|
||||
|
||||
<img src="https://github.com/adnanh/webhook/raw/development/docs/logo/logo-128x128.png" alt="Webhook" align="left" />
|
||||
|
||||
@ -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
|
||||
|
102
cipher_suites.go
102
cipher_suites.go
@ -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)
|
||||
}
|
@ -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)
|
||||
|
||||
|
@ -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=<JSON_STRING>`, 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:
|
||||
|
@ -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 <http://golang.org/pkg/regexp/syntax/>
|
||||
```json
|
||||
{
|
||||
@ -170,12 +183,13 @@ For the regex syntax, check out <http://golang.org/pkg/regexp/syntax/>
|
||||
}
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
@ -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":
|
||||
{
|
||||
|
2
go.mod
2
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
|
||||
|
@ -33,7 +33,7 @@
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha1",
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
|
@ -33,7 +33,7 @@
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha1",
|
||||
"type": "payload-hmac-sha1",
|
||||
"secret": "{{ getenv "XXXTEST_SECRET" | js }}",
|
||||
"parameter":
|
||||
{
|
||||
|
@ -15,7 +15,7 @@
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
type: payload-hash-sha1
|
||||
type: payload-hmac-sha1
|
||||
secret: mysecret
|
||||
parameter:
|
||||
source: header
|
||||
|
@ -15,7 +15,7 @@
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
type: payload-hash-sha1
|
||||
type: payload-hmac-sha1
|
||||
secret: "{{ getenv "XXXTEST_SECRET" | js }}"
|
||||
parameter:
|
||||
source: header
|
||||
|
@ -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 "<nil>"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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())
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -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
|
||||
|
4
tls.go
4
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))
|
||||
|
17
vendor/modules.txt
vendored
17
vendor/modules.txt
vendored
@ -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
|
||||
|
309
webhook.go
309
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 firstChar == byte('[') {
|
||||
var arrayPayload interface{}
|
||||
err := decoder.Decode(&arrayPayload)
|
||||
if err != nil {
|
||||
log.Printf("[%s] error parsing form payload %+v\n", rid, err)
|
||||
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 {
|
||||
|
@ -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:`},
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user