Compare commits

...

123 Commits

Author SHA1 Message Date
Adnan Hajdarevic 34ae132930 Bump webhook version to 2.6.10 2019-09-24 19:45:40 +02:00
Adnan Hajdarevic f993aaa11c Merge branch 'master' into development 2019-09-24 19:35:31 +02:00
Adnan Hajdarević d82e838554 Merge pull request #342 from moorereason/doc-content-type
Document incoming-payload-content-type hook setting
2019-09-21 11:50:46 +02:00
Cameron Moore 9c35aa070c Document incoming-payload-content-type hook setting
As implemented in PR #206
2019-09-20 08:50:44 -05:00
Adnan Hajdarević 14ee68a06e Create FUNDING.yml 2019-09-20 01:48:22 +02:00
Adnan Hajdarević e0e1dd8ade Delete appveyor.yml 2019-09-20 01:42:52 +02:00
Adnan Hajdarević 9852f0f0a5 Merge pull request #341 from moorereason/wintrav
Add Windows to Travis CI
2019-09-20 01:41:01 +02:00
Cameron Moore 2cf3f4e1a8 Add Windows to Travis CI 2019-09-19 08:42:33 -05:00
Adnan Hajdarević 1cf531b1c3 Merge pull request #339 from moorereason/StatusOK
Fix TestWebhook tests
2019-09-19 10:52:44 +02:00
Cameron Moore 74e55e3089 Update go versions in Travis CI
Test against the latest three minor releases.
2019-09-18 17:00:22 -05:00
Cameron Moore 6c77ff0a2c Fix TestWebhook tests
PR #266 appears to have changed the default response code to StatusOK.
waitForServerReady() was expected a StatusNotFound response, which was
preventing all TestWebhook tests from running.
2019-09-18 16:59:11 -05:00
Adnan Hajdarević 002c332b68 Merge pull request #327 from dexpota/master
Fix issue with relative paths and command execution
2019-09-18 18:07:07 +02:00
Adnan Hajdarević ffba396523 Merge pull request #337 from moorereason/iss333
Update GetParameter to support keys with dots
2019-09-18 18:02:25 +02:00
Cameron Moore b016e99ea6 Update GetParameter to support keys with dots
Fixes #333
2019-09-13 13:10:14 -05:00
Cameron Moore e47f9afb11 Fix failing ip-whitelist tests 2019-09-13 13:09:44 -05:00
Fabrizio Destro d3fd9bddd9 Fix issue with relative paths and command execution 2019-08-25 20:08:39 +02:00
Adnan Hajdarević 42b72b028b Merge pull request #319 from Htbaa/issue-313
Replaced fmt.Frpintf calls with fmt.Fprint when there's no formatting…
2019-06-21 22:04:12 +02:00
Christiaan Kras c6939d57dc Replaced fmt.Frpintf calls with fmt.Fprint when there's no formatting used
This fixes #313
2019-04-24 14:59:38 +02:00
Adnan Hajdarević 90f751a61d Merge pull request #266 from aioobe/development
Added SuccessHttpResponseCode hook setting
2019-04-17 18:13:25 +02:00
Adnan Hajdarević e86c2cf610 Merge branch 'development' into development 2019-04-17 18:11:12 +02:00
Adnan Hajdarević 0aa7395e21 Merge pull request #304 from johnpmitsch/travis
Add travis CI webhook example
2019-02-13 09:53:03 +01:00
John Mitsch 3f834f5c3d Add travis webhook example 2019-02-12 20:51:29 -05:00
Adnan Hajdarević 5bca86cdb2 Add snap store link to the README.md 2019-02-12 10:29:39 +01:00
Adnan Hajdarević 896d1608ca Merge pull request #297 from moorereason/iss207
Return errors on empty secrets during signature validations
2019-01-08 09:30:08 +01:00
Adnan Hajdarević e5c18aa87e Merge pull request #298 from moorereason/iss290
Allow multiple values for ip-whitelist
2019-01-08 09:29:32 +01:00
Cameron Moore f056f94305 Allow multiple values for ip-whitelist
Allow the value of ip-whitelist to consist of multiple space-separated
addresses or CIDRs.

Updates #290
2019-01-02 16:50:23 -06:00
Cameron Moore 1a17dc83fe Return errors on empty secrets during signature validations
Fixes #207
2019-01-02 16:09:27 -06:00
Adnan Hajdarević 753734428f Merge pull request #283 from adnanh/healthcheck-route
Add `/` route handler to return 200 OK which can be used as a healthcheck endpoint
2018-11-17 19:03:20 +01:00
Adnan Hajdarevic f76426e9b0 add handler for the route to be used as a healtcheck endpoint, fixes #233 2018-11-17 19:01:26 +01:00
Adnan Hajdarević 5803d5e849 Merge pull request #282 from adnanh/master
Backmerge
2018-11-16 18:59:25 +01:00
Adnan Hajdarević 385898b21f Merge pull request #281 from ruliezz/patch-1
Update docs with a Gitea example
2018-11-16 18:58:58 +01:00
ruliezz ce7f8d5d28 Updated with a Gitea example 2018-11-16 15:23:39 +01:00
Adnan Hajdarević b2899d1d3e Update README.md 2018-11-16 10:46:35 +01:00
Adnan Hajdarević e8628cd662 Add more guides to the README 2018-11-16 10:45:37 +01:00
Adnan Hajdarević 4e1719d966 Merge pull request #278 from adnanh/add-exe-extension-to-windows-build
Fix Makefile to include .exe extension for windows builds
2018-11-13 21:27:24 +01:00
Adnan Hajdarevic 98f86cf044 Fix Makefile to include .exe extension for windows builds 2018-11-13 21:12:20 +01:00
Adnan Hajdarevic fc0544e4a2 Bump version to 2.6.9 2018-11-13 21:01:42 +01:00
Adnan Hajdarević 537f5c21bc Merge pull request #277 from ZachCheung/master
docs: fix link
2018-11-13 18:03:14 +01:00
Zach Cheung a0880ab82d docs: fix link 2018-11-13 23:30:47 +08:00
Adnan Hajdarević 5636ead921 Merge pull request #269 from alyssais/hook-def-links
Fix links in Hook Definition docs
2018-10-05 21:37:46 +02:00
Alyssa Ross 01e0c9e972 Fix links in Hook Definition docs 2018-10-05 11:24:36 +01:00
Andreas Lundblad 54a7190113 Forgot a rename in previous refactoring. 2018-09-17 20:41:51 +02:00
Andreas Lundblad b65bdbbb24 Removed trailing tab 2018-09-17 20:35:51 +02:00
Andreas Lundblad ef3f43f89f Added SuccessHttpResponseCode handling for case when capture output is set to true. 2018-09-15 16:06:18 +02:00
Andreas Lundblad 22073d8847 Renamed http-response-code to success-http-response-code 2018-09-15 16:00:42 +02:00
Andreas Lundblad c05ca8c528 Added HttpResponseCode hook setting 2018-09-15 15:55:28 +02:00
Adnan Hajdarević f9e799fea0 Merge pull request #206 from dcj/feature/incoming-payload-content-type
added support for incoming-payload-content-type
2018-09-14 11:51:06 +02:00
Adnan Hajdarević 9b99452b60 Merge pull request #256 from vkovalchuk/master
Fixed links to other .md files in Hook-Definition.md doc
2018-09-14 11:43:31 +02:00
Adnan Hajdarević b93cdc346e Update README.md 2018-08-02 11:29:49 +02:00
Adnan Hajdarević d59f6228ad Merge pull request #262 from Awea/master
Add an entry to guide list
2018-07-23 10:09:42 +02:00
Awea 78c8c61bf2 📝 Update README 2018-07-23 09:54:52 +02:00
Vladimir Kovalchuk 7ed5d4af9b Fixed links to other .md files in Hook-Definition.md doc 2018-06-13 17:32:46 +03:00
Adnan Hajdarević 7905c74687 Merge pull request #237 from kirecek/fix/links-in-docs
Add .md  suffix for links to related doc pages in "Hook-Definition"
2018-06-05 09:38:19 +02:00
Adnan Hajdarević 2fb08ab579 Merge pull request #255 from 464bb26bac556e85b6fd6b524347b103/patch-1
Include shebang info from wiki/home.md in readme
2018-06-05 09:29:43 +02:00
md5(donics) 681e8b6459 Include shebang info wiki/home.md in readme
A change was made in response to #60 (fork/exec: exec format error) on the wiki that would be useful to also propagate to the readme.
2018-06-04 14:44:04 -04:00
Erik Jankovic 8a3770db29 fix: links for related doc pages
Signed-off-by: Erik Jankovic <erik.jankovic@vnet.eu>
2018-03-01 15:27:36 +01:00
Adnan Hajdarević ae54669c02 Merge pull request #228 from moorereason/iss225
Fix some tests for Windows
2018-02-19 11:00:34 +01:00
Adnan Hajdarević b449793825 Merge pull request #229 from moorereason/tidyup
Minor Housecleaning PR
2018-02-19 11:00:17 +01:00
Cameron Moore 66a9e48e39 Fix unnecessary nil check around range 2018-02-16 20:36:42 -06:00
Cameron Moore d85ee5e068 Use strings.TrimPrefix 2018-02-16 20:33:17 -06:00
Cameron Moore 7da4d8ba9f Use strings.Contains 2018-02-16 20:31:23 -06:00
Cameron Moore 8d260c6a7e Apply gofmt 2018-02-16 20:26:33 -06:00
Cameron Moore 48061f1508 Simplify boolean some comparisons 2018-02-16 20:23:25 -06:00
Cameron Moore cfed5cfe4b Fix unnecessary use of printf 2018-02-16 20:18:40 -06:00
Cameron Moore 471c849c50 Fix another race condition in TestWebhook
There's the potential for a race condition where we try to read the logs
buffer before the logs have been flushed by the webhook process. Kill
the process to flush the logs before testing against the log buffer.
2018-02-16 14:36:08 -06:00
Cameron Moore 337621998e Fix race in TestWebhook
Previous commit misused a bytes.Buffer. Protect the buffer with a
mutex.
2018-02-15 19:53:28 -06:00
Cameron Moore 0feeb945fc Fix some tests for Windows
This commit incorporates some tests into the main TestWebhook framework.  New features to TestWebhook:

- Check log output against Regexp
- Add Testing sub-tests

Updates #225
2018-02-15 19:20:39 -06:00
Adnan Hajdarević 4f9ed434c5 Merge pull request #227 from moorereason/iss226
Add Travis CI and Appveyor configurations
2018-02-15 20:24:46 +01:00
Cameron Moore 0934b9414c Add Travis CI and Appveyor configurations
Fixes #226
2018-02-14 16:35:54 -06:00
Adnan Hajdarević 356870358d Merge pull request #218 from hassanbabaie/development
Document updates for new scalr-signature feature
2018-02-14 15:39:42 +01:00
Hass_SEA 6dc331726d Updated Examples document with scalr-signature
Updated the Examples document with an example of how you would use the scalr-signature match rule
2018-01-18 12:43:12 -08:00
Hass_SEA 3f8dbf09dc Correct typos - Rules Document with scalr-signature
Correct typos - Rules Document with scalr-signature
2018-01-18 12:40:05 -08:00
Hass_SEA dcda096b5d Update Rules Document with scalr-signature
Updated Rules Document with scalr-signature information
2018-01-18 12:36:16 -08:00
Hass_SEA 7079128eca Merge pull request #1 from adnanh/development
Merge latest Adnanh/webhook Development into fork
2018-01-18 11:42:28 -08:00
Adnan Hajdarević 6e3ec89ce1 Merge pull request #210 from hassanbabaie/master
Add support for Scalr webhook signature verification (new Match Rule) #200 - Updated
2018-01-16 09:26:26 +01:00
Adnan Hajdarević 10396a5434 Update README.md 2018-01-11 10:34:55 +01:00
Adnan Hajdarević d009919755 Update README.md 2018-01-11 10:34:07 +01:00
Adnan Hajdarevic a811db410b check before removing 2017-12-21 13:25:19 +01:00
Adnan Hajdarević 357c471667 Merge pull request #212 from adnanh/fix-file-panic
Fix nilpointer dereference when file cannot be created
2017-12-21 13:15:18 +01:00
Adnan Hajdarevic 85889fe378 Fix nilpointer dereference when file cannot be created 2017-12-21 13:14:07 +01:00
Hass_SEA b595694658 Update to support Scalr Signature Verification
Add a new match rule type that checks for a Scalr webhook signature. Tracking ticket #200

The signature algorithm is described here:
https://scalr-wiki.atlassian.net/wiki/spaces/docs/pages/6193247/Webhook+Security+and+Authentication

An example match rule ifor a Scalr webhook will look like:

"match": {
"type": "scalr-signature",
"secret": ""
}
2017-12-19 12:48:10 -08:00
Adnan Hajdarevic ecbba514e5 Bump version 2017-12-13 20:46:15 +01:00
Adnan Hajdarevic 13555e1ef1 Merge 2017-12-13 20:45:22 +01:00
Adnan Hajdarević ddec431ae9 Update README.md 2017-11-29 11:17:48 +01:00
Donald Clark Jackson f84edae99d added support for incoming-payload-content-type 2017-11-27 14:02:57 -08:00
Adnan Hajdarević 514388e39e Merge pull request #166 from gdubicki/add_CaptureCommandOutputOnError
Add option to capture output of failed commands
2017-11-13 19:24:53 +01:00
Adnan Hajdarević a49364ae93 Update README.md 2017-11-13 08:44:31 +01:00
Adnan Hajdarević 6c4e52d87e Update README.md 2017-11-13 08:39:33 +01:00
Adnan Hajdarević b161de13f2 Add DigitalOcean to sponsors 2017-11-13 08:38:55 +01:00
Greg Dubicki e2f6e4eb37 Add tests for capturing command output
and fix running tests on macOS, where there is no
/bin/true...
2017-11-11 21:40:34 +01:00
Greg Dubicki 0d3d29055b Allow hookecho to exit with codes other than 0 2017-11-11 21:05:36 +01:00
Greg Dubicki a40fba5e29 Add CaptureCommandOutputOnError
to include stdout & stderror in failed executions,
with docs.
2017-11-11 18:46:55 +01:00
Adnan Hajdarević 137ded093d Update README.md 2017-11-11 12:39:18 +01:00
Adnan Hajdarevic 24ec77cb6a crisp 2017-11-11 12:29:39 +01:00
Adnan Hajdarevic 7f3529e302 Add project logo 2017-11-11 12:18:20 +01:00
Adnan Hajdarević df781925f1 Merge pull request #195 from moorereason/iss193
Add Template docs
2017-11-11 01:33:48 +01:00
Cameron Moore 44d19e34a0 Add Template docs
In addition to the Template docs, README is updated to use the "w" URL
shortcut.

Fixes #193
2017-11-10 16:11:29 -06:00
Adnan Hajdarević b1e0c0bf01 Merge pull request #194 from moorereason/iss192
Add docs
2017-11-10 21:05:50 +01:00
Cameron Moore bab32db017 Add docs 2017-11-10 12:54:15 -06:00
Cameron Moore 5a69eab2b3 Convert README.md to unix line endings 2017-11-10 12:48:19 -06:00
Adnan Hajdarević 01aa178bb4 Merge pull request #161 from moorereason/templates
Allow hooks file to be parsed as a template
2017-11-09 12:00:14 +01:00
Cameron Moore f5f04ddaa2 Allow hooks file to be parsed as a template
Add a -template command line option that instructs webhook to parse the
hooks files as Go text templates.

Includes a `getenv` template func for retrieving environment variables.
2017-11-08 09:14:58 -06:00
Adnan Hajdarević ba0adb117a Merge pull request #174 from col-panic/master
Pass "big" binary files #162
2017-11-08 10:56:49 +01:00
Marco Descher c107bb4539 Merge branch 'master' of https://github.com/adnanh/webhook into colpanic/master 2017-11-08 08:33:22 +01:00
Adnan Hajdarevic 36c5a52539 Merge branch 'master' of github.com:adnanh/webhook 2017-11-07 19:22:41 +01:00
Adnan Hajdarevic 027f2d7363 Fix tests 2017-11-07 19:22:32 +01:00
Marco Descher c9abc252e8 Delete hook files by default 2017-11-07 16:38:12 +01:00
Marco Descher 6145634670 Adapt to #173 2017-11-07 16:19:22 +01:00
Marco Descher 41b75acc70 Merge branch 'master' into master 2017-11-07 16:14:47 +01:00
Adnan Hajdarević 83e688efd3 Update README.md 2017-11-05 16:54:05 +01:00
Adnan Hajdarević b1851784b5 Merge pull request #188 from adnanh/development
Merge pull request #187 from adnanh/master
2017-11-05 16:50:31 +01:00
Adnan Hajdarević 3c0508a8d3 Merge pull request #187 from adnanh/master
Backmerge
2017-11-05 16:50:09 +01:00
Adnan Hajdarević 7a91168f23 Update README.md 2017-11-05 16:49:36 +01:00
Adnan Hajdarević 2adf724295 Update README.md 2017-11-05 16:23:10 +01:00
Adnan Hajdarević 2f7eb31e8d Update README.md 2017-11-05 16:09:31 +01:00
Adnan Hajdarević 717ad779a3 Merge pull request #185 from cavneb/patch-2
Remove Code Sponsor
2017-11-05 16:08:27 +01:00
Eric Berry 1e67afc3de Remove Code Sponsor 2017-11-05 06:14:08 -08:00
Adnan Hajdarević c094740905 Update README.md 2017-11-04 21:52:26 +01:00
Adnan Hajdarević de494d4cc6 Add guides featuring webhook 2017-11-04 21:00:33 +01:00
Marco Descher 213e4529e8 #162 do use temporary files, provide env variable 2017-09-15 13:30:44 +02:00
Marco Descher 78aa85e0c1 Revert to original hook import 2017-08-30 14:23:29 +02:00
Marco Descher 395fb41d23 Provide means to transfer files #162 add deleteOnExit 2017-08-29 14:42:14 +02:00
Marco Descher 34c4b1c166 Provide means to transfer files #162 2017-08-25 15:01:57 +02:00
22 changed files with 1845 additions and 334 deletions
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
open_collective: webhook
+1
View File
@@ -2,3 +2,4 @@
.cover
coverage
webhook
/test/hookecho
+26
View File
@@ -0,0 +1,26 @@
language: go
go:
- 1.11.x
- 1.12.x
- 1.13.x
- tip
os:
- linux
- osx
- windows
matrix:
fast_finish: true
allow_failures:
- go: tip
exclude:
- os: windows
go: tip
install:
- go get -d -v -t ./...
script:
- go test -v -race ./...
+11 -2
View File
@@ -1,7 +1,7 @@
OS = darwin freebsd linux openbsd windows
OS = darwin freebsd linux openbsd
ARCHS = 386 arm amd64 arm64
all: build release
all: build release release-windows
build: deps
go build
@@ -18,6 +18,15 @@ release: clean deps
done \
done
release-windows: clean deps
@for arch in $(ARCHS);\
do \
echo "Building windows-$$arch"; \
mkdir -p build/webhook-windows-$$arch/; \
GOOS=windows GOARCH=$$arch go build -o build/webhook-windows-$$arch/webhook.exe; \
tar cz -C build -f build/webhook-windows-$$arch.tar.gz webhook-windows-$$arch; \
done
test: deps
go test ./...
+83 -62
View File
@@ -1,29 +1,14 @@
[![ghit.me](https://ghit.me/badge.svg?repo=adnanh/webhook)](https://ghit.me/repo/adnanh/webhook) [![Join the chat at https://gitter.im/adnanh/webhook](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/adnanh/webhook?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
# Hookdoo
<a href="https://www.hookdoo.com/?github"><img src="https://my.hookdoo.com/logo/logo-dark-96.png" alt="hookdoo" align="left" /></a>
If you don't have time to waste configuring, hosting, debugging and maintaining your webhook instance, we offer a __SaaS__ solution that has all of the capabilities webhook provides, plus a lot more, and all that packaged in a nice friendly web interface. If you are interested, find out more at [hookdoo website](https://www.hookdoo.com/?ref=github-webhook-readme). If you have any questions, you can contact us at info@hookdoo.com
# Ensure active development
[![Backers on Open Collective](https://opencollective.com/webhook/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/webhook/sponsors/badge.svg)](#sponsors)
[![Flattr this](https://button.flattr.com/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=adnanh&url=https%3A%2F%2Fwww.github.com%2Fadnanh%2Fwebhook)
[Donate via PayPal](https://paypal.me/hookdoo)
[Patreon page](https://www.patreon.com/webhook)
[![Faircode](https://faircode.io/product/webhook/badge)](https://faircode.io/product/webhook?utm_source=badge&utm_medium=badgelarge&utm_campaign=webhook)
# What is webhook?
[webhook](https://github.com/adnanh/webhook/) is a lightweight configurable tool written in Go, that allows you to easily create HTTP endpoints (hooks) on your server, which you can use to execute configured commands. You can also pass data from the HTTP request (such as headers, payload or query variables) to your commands. [webhook](https://github.com/adnanh/webhook/) also allows you to specify rules which have to be satisfied in order for the hook to be triggered.
For example, if you're using Github or Bitbucket, you can use [webhook](https://github.com/adnanh/webhook/) to set up a hook that runs a redeploy script for your project on your staging server, whenever you push changes to the master branch of your project.
<img src="https://github.com/adnanh/webhook/raw/development/docs/logo/logo-128x128.png" alt="Webhook" align="left" />
[webhook][w] is a lightweight configurable tool written in Go, that allows you to easily create HTTP endpoints (hooks) on your server, which you can use to execute configured commands. You can also pass data from the HTTP request (such as headers, payload or query variables) to your commands. [webhook][w] also allows you to specify rules which have to be satisfied in order for the hook to be triggered.
For example, if you're using Github or Bitbucket, you can use [webhook][w] to set up a hook that runs a redeploy script for your project on your staging server, whenever you push changes to the master branch of your project.
If you use Mattermost or Slack, you can set up an "Outgoing webhook integration" or "Slash command" to run various commands on your server, which can then report back directly to you or your channels using the "Incoming webhook integrations", or the appropriate response body.
[webhook](https://github.com/adnanh/webhook/) aims to do nothing more than it should do, and that is:
[webhook][w] aims to do nothing more than it should do, and that is:
1. receive the request,
2. parse the headers, payload and query variables,
3. check if the specified rules for the hook are satisfied,
@@ -32,7 +17,11 @@ If you use Mattermost or Slack, you can set up an "Outgoing webhook integration"
Everything else is the responsibility of the command's author.
---
# Hookdoo
<a href="https://www.hookdoo.com/?github"><img src="https://www.hookdoo.com/logo/logo.svg" height="96" alt="hookdoo" align="left" /></a>
If you don't have time to waste configuring, hosting, debugging and maintaining your webhook instance, we offer a __SaaS__ solution that has all of the capabilities webhook provides, plus a lot more, and all that packaged in a nice friendly web interface. If you are interested, find out more at [hookdoo website](https://www.hookdoo.com/?ref=github-webhook-readme). If you have any questions, you can contact us at info@hookdoo.com
# Getting started
## Installation
@@ -41,19 +30,25 @@ To get started, first make sure you've properly set up your [Golang](http://gola
```bash
$ go get github.com/adnanh/webhook
```
to get the latest version of the [webhook](https://github.com/adnanh/webhook/).
to get the latest version of the [webhook][w].
### Using package manager
#### Snap store
[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-white.svg)](https://snapcraft.io/webhook)
#### Ubuntu
If you are using Ubuntu linux (17.04 or later), you can install webhook using `sudo apt-get install webhook` which will install community packaged version.
#### Debian
If you are using Debian linux ("stretch" or later), you can install webhook using `apt-get install webhook` which will install community packaged version (thanks [@freeekanayaka](https://github.com/freeekanayaka)) from https://packages.debian.org/sid/webhook
If you are using Debian linux ("stretch" or later), you can install webhook using `sudo apt-get install webhook` which will install community packaged version (thanks [@freeekanayaka](https://github.com/freeekanayaka)) from https://packages.debian.org/sid/webhook
### Download prebuilt binaries
Prebuilt binaries for different architectures are available at [GitHub Releases](https://github.com/adnanh/webhook/releases).
## Configuration
Next step is to define some hooks you want [webhook](https://github.com/adnanh/webhook/) to serve. Begin by creating an empty file named `hooks.json`. This file will contain an array of hooks the [webhook](https://github.com/adnanh/webhook/) will serve. Check [Hook definition page](https://github.com/adnanh/webhook/wiki/Hook-Definition) to see the detailed description of what properties a hook can contain, and how to use them.
Next step is to define some hooks you want [webhook][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.
Let's define a simple hook named `redeploy-webhook` that will run a redeploy script located in `/var/scripts/redeploy.sh`.
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.
Our `hooks.json` file will now look like this:
```json
@@ -66,7 +61,7 @@ Our `hooks.json` file will now look like this:
]
```
You can now run [webhook](https://github.com/adnanh/webhook/) using
You can now run [webhook][w] using
```bash
$ /path/to/webhook -hooks hooks.json -verbose
```
@@ -76,56 +71,64 @@ It will start up on default port 9000 and will provide you with one HTTP endpoin
http://yourserver:9000/hooks/redeploy-webhook
```
Check [webhook parameters page](https://github.com/adnanh/webhook/wiki/Webhook-Parameters) to see how to override the ip, port and other settings such as hook hotreload, verbose output, etc, when starting the [webhook](https://github.com/adnanh/webhook/).
Check [webhook parameters page](docs/Webhook-Parameters.md) to see how to override the ip, port and other settings such as hook hotreload, verbose output, etc, when starting the [webhook][w].
By performing a simple HTTP GET or POST request to that endpoint, your specified redeploy script would be executed. Neat!
However, hook defined like that could pose a security threat to your system, because anyone who knows your endpoint, can send a request and execute your command. To prevent that, you can use the `"trigger-rule"` property for your hook, to specify the exact circumstances under which the hook would be triggered. For example, you can use them to add a secret that you must supply as a parameter in order to successfully trigger the hook. Please check out the [Hook rules page](https://github.com/adnanh/webhook/wiki/Hook-Rules) for detailed list of available rules and their usage.
However, hook defined like that could pose a security threat to your system, because anyone who knows your endpoint, can send a request and execute your command. To prevent that, you can use the `"trigger-rule"` property for your hook, to specify the exact circumstances under which the hook would be triggered. For example, you can use them to add a secret that you must supply as a parameter in order to successfully trigger the hook. Please check out the [Hook rules page](docs/Hook-Rules.md) for detailed list of available rules and their usage.
# Using HTTPS
[webhook](https://github.com/adnanh/webhook/) by default serves hooks using http. If you want [webhook](https://github.com/adnanh/webhook/) to serve secure content using https, you can use the `-secure` flag while starting [webhook](https://github.com/adnanh/webhook/). Files containing a certificate and matching private key for the server must be provided using the `-cert /path/to/cert.pem` and `-key /path/to/key.pem` flags. If the certificate is signed by a certificate authority, the cert file should be the concatenation of the server's certificate followed by the CA's certificate.
## 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.
# CORS Headers
If you want to set CORS headers, you can use the `-header name=value` flag while starting [webhook](https://github.com/adnanh/webhook/) to set the appropriate CORS headers that will be returned with each response.
## 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.
# Interested in running webhook inside of a Docker container?
## CORS Headers
If you want to set CORS headers, you can use the `-header name=value` flag while starting [webhook][w] to set the appropriate CORS headers that will be returned with each response.
## Interested in running webhook inside of a Docker container?
You can use [almir/webhook](https://hub.docker.com/r/almir/webhook/) docker image, or create your own (please read [this discussion](https://github.com/adnanh/webhook/issues/63)).
# Examples
Check out [Hook examples page](https://github.com/adnanh/webhook/wiki/Hook-Examples) for more complex examples of hooks.
## Examples
Check out [Hook examples page](docs/Hook-Examples.md) for more complex examples of hooks.
# Contributing
Any form of contribution is welcome and highly appreciated.
Big thanks to [all the current contributors](https://github.com/adnanh/webhook/graphs/contributors) for their contributions!
# Community Contributions
### Guides featuring webhook
- [Webhook & JIRA](https://sites.google.com/site/mrxpalmeiras/notes/jira-webhooks) by [@perfecto25](https://github.com/perfecto25)
- [Trigger Ansible AWX job runs on SCM (e.g. git) commit](http://jpmens.net/2017/10/23/trigger-awx-job-runs-on-scm-commit/) by [@jpmens](http://mens.de/)
- [Deploy using GitHub webhooks](https://davidauthier.wearemd.com/blog/deploy-using-github-webhooks.html) by [@awea](https://davidauthier.wearemd.com)
- [Setting up Automatic Deployment and Builds Using Webhooks](https://willbrowning.me/setting-up-automatic-deployment-and-builds-using-webhooks/) by [Will Browning](https://willbrowning.me/about/)
- [Auto deploy your Node.js app on push to GitHub in 3 simple steps](https://webhookrelay.com/blog/2018/07/17/auto-deploy-on-git-push/) by Karolis Rusenas
- [Automate Static Site Deployments with Salt, Git, and Webhooks](https://www.linode.com/docs/applications/configuration-management/automate-a-static-site-deployment-with-salt/) by [Linode](https://www.linode.com)
- [Using Prometheus to Automatically Scale WebLogic Clusters on Kubernetes](https://blogs.oracle.com/weblogicserver/using-prometheus-to-automatically-scale-weblogic-clusters-on-kubernetes-v5) by [Marina Kogan](https://blogs.oracle.com/author/9a4fe754-1cc2-4c64-95fc-360642b62927)
- [Github Pages and Jekyll - A New Platform for LACNIC Labs](https://labs.lacnic.net/a-new-platform-for-lacniclabs/) by [Carlos Martínez Cagnazzo](https://twitter.com/carlosm3011)
- [How to Deploy React Apps Using Webhooks and Integrating Slack on Ubuntu](https://www.alibabacloud.com/blog/how-to-deploy-react-apps-using-webhooks-and-integrating-slack-on-ubuntu_594116) by Arslan Ud Din Shafiq
- [Private webhooks](https://ihateithe.re/2018/01/private-webhooks/) by [Thomas](https://ihateithe.re/colophon/)
- [Adventures in webhooks](https://medium.com/@draketech/adventures-in-webhooks-2d6584501c62) by [Drake](https://medium.com/@draketech)
- [GitHub pro tips](http://notes.spencerlyon.com/2016/01/04/github-pro-tips/) by [Spencer Lyon](http://notes.spencerlyon.com/)
- [XiaoMi Vacuum + Amazon Button = Dash Cleaning](https://www.instructables.com/id/XiaoMi-Vacuum-Amazon-Button-Dash-Cleaning/) by [c0mmensal](https://www.instructables.com/member/c0mmensal/)
- VIDEO: [Gitlab CI/CD configuration using Docker and adnanh/webhook to deploy on VPS - Tutorial #1](https://www.youtube.com/watch?v=Qhn-lXjyrZA&feature=youtu.be) by [Yes! Let's Learn Software Engineering](https://www.youtube.com/channel/UCH4XJf2BZ_52fbf8fOBMF3w)
- ...
- Want to add your own? Open an Issue or create a PR :-)
## Community Contributions
See the [webhook-contrib][wc] repository for a collections of tools and helpers related to [webhook][w] that have been contributed by the [webhook][w] community.
# Sponsors
## Need help?
Check out [existing issues](https://github.com/adnanh/webhook/issues) to see if someone else also had the same problem, or [open a new one](https://github.com/adnanh/webhook/issues/new).
# Support active development
## Sponsors
## <a href="https://www.digitalocean.com/?ref=webhook"><img src="http://www.hajdarevic.net/DO_Logo_Horizontal_Blue.png" alt="DigitalOcean" width="250"/></a>
[DigitalOcean](https://www.digitalocean.com/?ref=webhook) is a simple and robust cloud computing platform, designed for developers.
## <a href="https://www.browserstack.com/?ref=webhook"><img src="http://www.hajdarevic.net/browserstack.svg" alt="BrowserStack" width="250"/></a>
[BrowserStack](https://www.browserstack.com/?ref=webhook) is a cloud-based cross-browser testing tool that enables developers to test their websites across various browsers on different operating systems and mobile devices, without requiring users to install virtual machines, devices or emulators.
<a target='_blank' rel='nofollow' href='https://app.codesponsor.io/link/2AxDfLX6qJBgxEXxcCPGjJyD/adnanh/webhook'>
<img alt='Sponsor' width='888' height='68' src='https://app.codesponsor.io/embed/2AxDfLX6qJBgxEXxcCPGjJyD/adnanh/webhook.svg' />
</a>
---
## Contributors
This project exists thanks to all the people who contribute. [[Contribute]](CONTRIBUTING.md).
<a href="graphs/contributors"><img src="https://opencollective.com/webhook/contributors.svg?width=890" /></a>
## Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/webhook#backer)]
<a href="https://opencollective.com/webhook#backers" target="_blank"><img src="https://opencollective.com/webhook/backers.svg?width=890"></a>
## Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/webhook#sponsor)]
Support this project by becoming a sponsor. Your logo will show up here with a link to your website.
<a href="https://opencollective.com/webhook/sponsor/0/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/webhook/sponsor/1/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/1/avatar.svg"></a>
@@ -138,7 +141,25 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
<a href="https://opencollective.com/webhook/sponsor/8/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/webhook/sponsor/9/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/9/avatar.svg"></a>
## By contributing
This project exists thanks to all the people who contribute. [Contribute!](CONTRIBUTING.md).
<a href="graphs/contributors"><img src="https://opencollective.com/webhook/contributors.svg?width=890" /></a>
## By giving money
- [OpenCollective Backer](https://opencollective.com/webhook#backer)
- [OpenCollective Sponsor](https://opencollective.com/webhook#sponsor)
- [PayPal](https://paypal.me/hookdoo)
- [Patreon](https://www.patreon.com/webhook)
- [Faircode](https://faircode.io/product/webhook?utm_source=badge&utm_medium=badgelarge&utm_campaign=webhook)
- [Flattr](https://flattr.com/submit/auto?user_id=adnanh&url=https%3A%2F%2Fwww.github.com%2Fadnanh%2Fwebhook)
---
Thank you to all our backers!
<a href="https://opencollective.com/webhook#backers" target="_blank"><img src="https://opencollective.com/webhook/backers.svg?width=890"></a>
# License
+25
View File
@@ -0,0 +1,25 @@
# 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.
## Properties (keys)
* `id` - specifies the ID of your hook. This value is used to create the HTTP endpoint (http://yourserver:port/hooks/your-hook-id)
* `execute-command` - specifies the command that should be executed when the hook is triggered
* `command-working-directory` - specifies the working directory that will be used for the script when it's executed
* `response-message` - specifies the string that will be returned to the hook initiator
* `response-headers` - specifies the list of headers in format `{"name": "X-Example-Header", "value": "it works"}` that will be returned in HTTP response for the hook
* `success-http-response-code` - specifies the HTTP status code to be returned upon success
* `incoming-payload-content-type` - sets the `Content-Type` of the incoming HTTP request (ie. `application/json`); useful when the request lacks a `Content-Type` or sends an erroneous value
* `include-command-output-in-response` - boolean whether webhook should wait for the command to finish and return the raw output as a response to the hook initiator. If the command fails to execute or encounters any errors while executing the response will result in 500 Internal Server Error HTTP status code, otherwise the 200 OK status code will be returned.
* `include-command-output-in-response-on-error` - boolean whether webhook should include command stdout & stderror as a response in failed executions. It only works if `include-command-output-in-response` is set to `true`.
* `parse-parameters-as-json` - specifies the list of arguments that contain JSON strings. These parameters will be decoded by webhook and you can access them like regular objects in rules and `pass-arguments-to-command`.
* `pass-arguments-to-command` - specifies the list of arguments that will be passed to the command. Check [Referencing request values page](Referencing-Request-Values.md) to see how to reference the values from the request. If you want to pass a static string value to your command you can specify it as
`{ "source": "string", "name": "argumentvalue" }`
* `pass-environment-to-command` - specifies the list of arguments that will be passed to the command as environment variables. If you do not specify the `"envname"` field in the referenced value, the hook will be in format "HOOK_argumentname", otherwise "envname" field will be used as it's name. Check [Referencing request values page](Referencing-Request-Values.md) to see how to reference the values from the request. If you want to pass a static string value to your command you can specify it as
`{ "source": "string", "envname": "SOMETHING", "name": "argumentvalue" }`
* `pass-file-to-command` - specifies a list of entries that will be serialized as a file. Incoming [data](Referencing-Request-Values.md) will be serialized in a request-temporary-file (otherwise parallel calls of the hook would lead to concurrent overwritings of the file). The filename to be addressed within the subsequent script is provided via an environment variable. Use `envname` to specify the name of the environment variable. If `envname` is not provided `HOOK_` and the name used to reference the request value are used. Defining `command-working-directory` will store the file relative to this location, if not provided, the systems temporary file directory will be used. If `base64decode` is true, the incoming binary data will be base 64 decoded prior to storing it into the file. By default the corresponding file will be removed after the webhook exited.
* `trigger-rule` - specifies the rule that will be evaluated in order to determine should the hook be triggered. Check [Hook rules page](Hook-Rules.md) to see the list of valid rules and their usage
* `trigger-rule-mismatch-http-response-code` - specifies the HTTP status code to be returned when the trigger rule is not satisfied
## Examples
Check out [Hook examples page](Hook-Examples.md) for more complex examples of hooks.
+426
View File
@@ -0,0 +1,426 @@
# Hook examples
This page is still work in progress. Feel free to contribute!
## Incoming Github webhook
```json
[
{
"id": "webhook",
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
"command-working-directory": "/home/adnan/go",
"pass-arguments-to-command":
[
{
"source": "payload",
"name": "head_commit.id"
},
{
"source": "payload",
"name": "pusher.name"
},
{
"source": "payload",
"name": "pusher.email"
}
],
"trigger-rule":
{
"and":
[
{
"match":
{
"type": "payload-hash-sha1",
"secret": "mysecret",
"parameter":
{
"source": "header",
"name": "X-Hub-Signature"
}
}
},
{
"match":
{
"type": "value",
"value": "refs/heads/master",
"parameter":
{
"source": "payload",
"name": "ref"
}
}
}
]
}
}
]
```
## Incoming Bitbucket webhook
Bitbucket does not pass any secrets back to the webhook. [Per their documentation](https://confluence.atlassian.com/bitbucket/manage-webhooks-735643732.html#Managewebhooks-trigger_webhookTriggeringwebhooks), in order to verify that the webhook came from Bitbucket you must whitelist the IP range `104.192.143.0/24`:
```json
[
{
"id": "webhook",
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
"command-working-directory": "/home/adnan/go",
"pass-arguments-to-command":
[
{
"source": "payload",
"name": "actor.username"
}
],
"trigger-rule":
{
"match":
{
"type": "ip-whitelist",
"ip-range": "104.192.143.0/24"
}
}
}
]
```
## Incoming Gitlab Webhook
Gitlab provides webhooks for many kinds of events.
Refer to this URL for example request body content: [gitlab-ce/integrations/webhooks](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/project/integrations/webhooks.md)
Values in the request body can be accessed in the command or to the match rule by referencing 'payload' as the source:
```json
[
{
"id": "redeploy-webhook",
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
"command-working-directory": "/home/adnan/go",
"pass-arguments-to-command":
[
{
"source": "payload",
"name": "user_name"
}
],
"response-message": "Executing redeploy script",
"trigger-rule":
{
"match":
{
"type": "value",
"value": "<YOUR-GENERATED-TOKEN>",
"parameter":
{
"source": "header",
"name": "X-Gitlab-Token"
}
}
}
}
]
```
## Incoming Gogs webhook
```json
[
{
"id": "webhook",
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
"command-working-directory": "/home/adnan/go",
"pass-arguments-to-command":
[
{
"source": "payload",
"name": "head_commit.id"
},
{
"source": "payload",
"name": "pusher.name"
},
{
"source": "payload",
"name": "pusher.email"
}
],
"trigger-rule":
{
"and":
[
{
"match":
{
"type": "payload-hash-sha256",
"secret": "mysecret",
"parameter":
{
"source": "header",
"name": "X-Gogs-Signature"
}
}
},
{
"match":
{
"type": "value",
"value": "refs/heads/master",
"parameter":
{
"source": "payload",
"name": "ref"
}
}
}
]
}
}
]
```
## Incoming Gitea webhook
```json
[
{
"id": "webhook",
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
"command-working-directory": "/home/adnan/go",
"pass-arguments-to-command":
[
{
"source": "payload",
"name": "head_commit.id"
},
{
"source": "payload",
"name": "pusher.name"
},
{
"source": "payload",
"name": "pusher.email"
}
],
"trigger-rule":
{
"and":
[
{
"match":
{
"type": "value",
"value": "mysecret",
"parameter":
{
"source": "payload",
"name": "secret"
}
}
},
{
"match":
{
"type": "value",
"value": "refs/heads/master",
"parameter":
{
"source": "payload",
"name": "ref"
}
}
}
]
}
}
]
```
## Slack slash command
```json
[
{
"id": "redeploy-webhook",
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
"command-working-directory": "/home/adnan/go",
"response-message": "Executing redeploy script",
"trigger-rule":
{
"match":
{
"type": "value",
"value": "<YOUR-GENERATED-TOKEN>",
"parameter":
{
"source": "payload",
"name": "token"
}
}
}
}
]
```
## A simple webhook with a secret key in GET query
__Not recommended in production due to low security__
`example.com:9000/hooks/simple-one` - won't work
`example.com:9000/hooks/simple-one?token=42` - will work
```json
[
{
"id": "simple-one",
"execute-command": "/path/to/command.sh",
"response-message": "Executing simple webhook...",
"trigger-rule":
{
"match":
{
"type": "value",
"value": "42",
"parameter":
{
"source": "url",
"name": "token"
}
}
}
}
]
```
# JIRA Webhooks
[Guide by @perfecto25](https://sites.google.com/site/mrxpalmeiras/notes/jira-webhooks)
# Pass File-to-command sample
## Webhook configuration
<pre>
[
{
"id": "test-file-webhook",
"execute-command": "/bin/ls",
"command-working-directory": "/tmp",
"pass-file-to-command":
[
{
"source": "payload",
"name": "binary",
"envname": "ENV_VARIABLE", // to use $ENV_VARIABLE in execute-command
// if not defined, $HOOK_BINARY will be provided
"base64decode": true, // defaults to false
}
],
"include-command-output-in-response": true
}
]
</pre>
## Sample client usage
Store the following file as `testRequest.json`.
<pre>
{"binary":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2lpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wUmlnaHRzPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvcmlnaHRzLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcFJpZ2h0czpNYXJrZWQ9IkZhbHNlIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjEzMTA4RDI0QzMxQjExRTBCMzYzRjY1QUQ1Njc4QzFBIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjEzMTA4RDIzQzMxQjExRTBCMzYzRjY1QUQ1Njc4QzFBIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzMgV2luZG93cyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ1dWlkOkFDMUYyRTgzMzI0QURGMTFBQUI4QzUzOTBEODVCNUIzIiBzdFJlZjpkb2N1bWVudElEPSJ1dWlkOkM5RDM0OTY2NEEzQ0REMTFCMDhBQkJCQ0ZGMTcyMTU2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+IBFgEwAAAmJJREFUeNqkk89rE1EQx2d/NNq0xcYYayPYJDWC9ODBsKIgAREjBmvEg2cvHnr05KHQ9iB49SL+/BMEfxBQKHgwCEbTNNIYaqgaoanFJi+rcXezye4689jYkIMIDnx47837zrx583YFx3Hgf0xA6/dJyAkkgUy4vgryAnmNWH9L4EVmotFoKplMHgoGg6PkrFarjXQ6/bFcLj/G5W1E+3NaX4KZeDx+dX5+7kg4HBlmrC6JoiDFYrGhROLM/mp1Y6JSqdCd3/SW0GUqEAjkl5ZyHTSHKBQKnO6a9khD2m5cr91IJBJ1VVWdiM/n6LruNJtNDs3JR3ukIW03SHTHi8iVsbG9I51OG1bW16HVasHQZopDc/JZVgdIQ1o3BmTkEnJXURS/KIpgGAYPkCQJPi0u8uzDKQN0XQPbtgE1MmrHs9nsfSqAEjxCNtHxZHLy4G4smUQgyzL4LzOegDGGp1ucVqsNqKVrpJCM7F4hg6iaZvhqtZrg8XjA4xnAU3XeKLqWaRImoIZeQXVjQO5pYp4xNVirsR1erxer2O4yfa227WCwhtWoJmn7m0h270NxmemFW4706zMm8GCgxBGEASCfhnukIW03iFdQnOPz0LNKp3362JqQzSw4u2LXBe+Bs3xD+/oc1NxN55RiC9fOme0LEQiRf2rBzaKEeJJ37ZWTVunBeGN2WmQjg/DeLTVP89nzAive2dMwlo9bpFVC2xWMZr+A720FVn88fAUb3wDMOjyN7YNc6TvUSHQ4AH6TOUdLL7em68UtWPsJqxgTpgeiLu1EBt1R+Me/mF7CQPTfAgwAGxY2vOTrR3oAAAAASUVORK5CYII="}
</pre>
use then the curl tool to execute a request to the webhook.
<pre>
#!/bin/bash
curl -H "Content-Type:application/json" -X POST -d @testRequest.json \
http://localhost:9000/hooks/test-file-webhook
</pre>
or in a single line, using https://github.com/jpmens/jo to generate the JSON code
<pre>
jo binary=%filename.zip | curl -H "Content-Type:application/json" -X POST -d @- \
http://localhost:9000/hooks/test-file-webhook
</pre>
## Incoming Scalr Webhook
[Guide by @hassanbabaie]
Scalr makes webhook calls based on an event to a configured webhook endpoint (for example Host Down, Host Up). Webhook endpoints are URLs where Scalr will deliver Webhook notifications.
Scalr assigns a unique signing key for every configured webhook endpoint.
Refer to this URL for information on how to setup the webhook call on the Scalr side: [Scalr Wiki Webhooks](https://scalr-wiki.atlassian.net/wiki/spaces/docs/pages/6193173/Webhooks)
In order to leverage the Signing Key for addtional authentication/security you must configure the trigger rule with a match type of "scalr-signature".
```json
[
{
"id": "redeploy-webhook",
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
"command-working-directory": "/home/adnan/go",
"include-command-output-in-response": true,
"trigger-rule":
{
"match":
{
"type": "scalr-signature",
"secret": "Scalr-provided signing key"
}
},
"pass-environment-to-command":
[
{
"envname": "EVENT_NAME",
"source": "payload",
"name": "eventName"
},
{
"envname": "SERVER_HOSTNAME",
"source": "payload",
"name": "data.SCALR_SERVER_HOSTNAME"
}
]
}
]
```
## Travis CI webhook
Travis sends webhooks as `payload=<JSON_STRING>`, so the payload needs to be parsed as JSON. Here is an example to run on successful builds of the master branch.
```json
[
{
"id": "deploy",
"execute-command": "/root/my-server/deployment.sh",
"command-working-directory": "/root/my-server",
"parse-parameters-as-json": [
{
"source": "payload",
"name": "payload"
}
],
"trigger-rule":
{
"and":
[
{
"match":
{
"type": "value",
"value": "passed",
"parameter": {
"name": "payload.state",
"source": "payload"
}
}
},
{
"match":
{
"type": "value",
"value": "master",
"parameter": {
"name": "payload.branch",
"source": "payload"
}
}
}
]
}
}
]
```
+217
View File
@@ -0,0 +1,217 @@
# Hook rules
## And
*And rule* will evaluate to _true_, if and only if all of the sub rules evaluate to _true_.
```json
{
"and":
[
{
"match":
{
"type": "value",
"value": "refs/heads/master",
"parameter":
{
"source": "payload",
"name": "ref"
}
}
},
{
"match":
{
"type": "regex",
"regex": ".*",
"parameter":
{
"source": "payload",
"name": "repository.owner.name"
}
}
}
]
}
```
## Or
*Or rule* will evaluate to _true_, if any of the sub rules evaluate to _true_.
```json
{
"or":
[
{
"match":
{
"type": "value",
"value": "refs/heads/master",
"parameter":
{
"source": "payload",
"name": "ref"
}
}
},
{
"match":
{
"type": "value",
"value": "refs/heads/development",
"parameter":
{
"source": "payload",
"name": "ref"
}
}
}
]
}
```
## Not
*Not rule* will evaluate to _true_, if and only if the sub rule evaluates to _false_.
```json
{
"not":
{
"match":
{
"type": "value",
"value": "refs/heads/development",
"parameter":
{
"source": "payload",
"name": "ref"
}
}
}
}
```
## Multi-level
```json
{
"and": [
{
"match": {
"parameter": {
"source": "header",
"name": "X-Hub-Signature"
},
"type": "payload-hash-sha1",
"secret": "mysecret"
}
},
{
"or": [
{
"match":
{
"parameter":
{
"source": "payload",
"name": "ref"
},
"type": "value",
"value": "refs/heads/master"
}
},
{
"match":
{
"parameter":
{
"source": "header",
"name": "X-GitHub-Event"
},
"type": "value",
"value": "ping"
}
}
]
}
]
}
```
## Match
*Match rule* will evaluate to _true_, if and only if the referenced value in the `parameter` field satisfies the `type`-specific rule.
*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
```json
{
"match":
{
"type": "value",
"value": "refs/heads/development",
"parameter":
{
"source": "payload",
"name": "ref"
}
}
}
```
### 2. Match regex
For the regex syntax, check out <http://golang.org/pkg/regexp/syntax/>
```json
{
"match":
{
"type": "regex",
"regex": ".*",
"parameter":
{
"source": "payload",
"name": "ref"
}
}
}
```
### 3. Match payload-hash-sha1
```json
{
"match":
{
"type": "payload-hash-sha1",
"secret": "yoursecret",
"parameter":
{
"source": "header",
"name": "X-Hub-Signature"
}
}
}
```
### 4. 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`.
```json
{
"match":
{
"type": "ip-whitelist",
"ip-range": "192.168.0.1/24"
}
}
```
### 5. 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.
Given the time check make sure that NTP is enabled on both your Scalr and webhook server to prevent any issues
```json
{
"match":
{
"type": "scalr-signature",
"secret": "Scalr-provided signing key"
}
}
```
+90
View File
@@ -0,0 +1,90 @@
# Referencing request values
There are three types of request values:
1. HTTP Request Header values
```json
{
"source": "header",
"name": "Header-Name"
}
```
2. HTTP Query parameters
```json
{
"source": "url",
"name": "parameter-name"
}
```
3. Payload (JSON or form-value encoded)
```json
{
"source": "payload",
"name": "parameter-name"
}
```
*Note:* For JSON encoded payload, you can reference nested values using the dot-notation.
For example, if you have following JSON payload
```json
{
"commits": [
{
"commit": {
"id": 1
}
}, {
"commit": {
"id": 2
}
}
]
}
```
You can reference the first commit id as
```json
{
"source": "payload",
"name": "commits.0.commit.id"
}
```
If the payload contains a key with the specified name "commits.0.commit.id", then the value of that key has priority over the dot-notation referencing.
If you are referencing values for environment, you can use `envname` property to set the name of the environment variable like so
```json
{
"source": "url",
"name": "q",
"envname": "QUERY"
}
```
to get the QUERY environment variable set to the `q` parameter passed in the query string.
# Special cases
If you want to pass the entire payload as JSON string to your command you can use
```json
{
"source": "entire-payload"
}
```
for headers you can use
```json
{
"source": "entire-headers"
}
```
and for query variables you can use
```json
{
"source": "entire-query"
}
```
+76
View File
@@ -0,0 +1,76 @@
# 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).
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.
## 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.
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.
```
{
"id": "webhook",
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
"command-working-directory": "/home/adnan/go",
"response-message": "I got the payload!",
"response-headers":
[
{
"name": "Access-Control-Allow-Origin",
"value": "*"
}
],
"pass-arguments-to-command":
[
{
"source": "payload",
"name": "head_commit.id"
},
{
"source": "payload",
"name": "pusher.name"
},
{
"source": "payload",
"name": "pusher.email"
}
],
"trigger-rule":
{
"and":
[
{
"match":
{
"type": "payload-hash-sha1",
"secret": "{{ getenv "XXXTEST_SECRET" | js }}",
"parameter":
{
"source": "header",
"name": "X-Hub-Signature"
}
}
},
{
"match":
{
"type": "value",
"value": "refs/heads/master",
"parameter":
{
"source": "payload",
"name": "ref"
}
}
}
]
}
}
]
```
[w]: https://github.com/adnanh/webhook
[tt]: https://golang.org/pkg/text/template/
+38
View File
@@ -0,0 +1,38 @@
# Webhook parameters
```
Usage of webhook:
-cert string
path to the HTTPS certificate pem file (default "cert.pem")
-header value
response header to return, specified in format name=value, use multiple times to set multiple headers
-hooks value
path to the json file containing defined hooks the webhook should serve, use multiple times to load from different files
-hotreload
watch hooks file for changes and reload them automatically
-ip string
ip the webhook should serve hooks on (default "0.0.0.0")
-key string
path to the HTTPS certificate private key pem file (default "key.pem")
-nopanic
do not panic if hooks cannot be loaded when webhook is not running in verbose mode
-port int
port the webhook should serve hooks on (default 9000)
-secure
use HTTPS instead of HTTP
-template
parse hooks file as a Go template
-urlprefix string
url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id) (default "hooks")
-verbose
show verbose output
-version
display webhook version and quit
```
Use any of the above specified flags to override their default behavior.
# Live reloading hooks
If you are running an OS that supports USR1 signal, you can use it to trigger hooks reload from hooks file, without restarting the webhook instance.
```bash
kill -USR1 webhookpid
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+185 -56
View File
@@ -1,20 +1,27 @@
package hook
import (
"bytes"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"math"
"net"
"net/textproto"
"os"
"reflect"
"regexp"
"strconv"
"strings"
"text/template"
"time"
"github.com/ghodss/yaml"
)
@@ -87,10 +94,12 @@ func (e *ParseError) Error() string {
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
func CheckPayloadSignature(payload []byte, secret string, signature string) (string, error) {
if strings.HasPrefix(signature, "sha1=") {
signature = signature[5:]
if secret == "" {
return "", errors.New("signature validation secret can not be empty")
}
signature = strings.TrimPrefix(signature, "sha1=")
mac := hmac.New(sha1.New, []byte(secret))
_, err := mac.Write(payload)
if err != nil {
@@ -106,10 +115,12 @@ 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) {
if strings.HasPrefix(signature, "sha256=") {
signature = signature[7:]
if secret == "" {
return "", errors.New("signature validation secret can not be empty")
}
signature = strings.TrimPrefix(signature, "sha256=")
mac := hmac.New(sha256.New, []byte(secret))
_, err := mac.Write(payload)
if err != nil {
@@ -123,46 +134,82 @@ func CheckPayloadSignature256(payload []byte, secret string, signature string) (
return expectedMAC, err
}
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 {
return false, nil
}
if _, ok := 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)
mac := hmac.New(sha1.New, []byte(signingKey))
mac.Write(body)
mac.Write([]byte(dateHeader))
expectedSignature := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(providedSignature), []byte(expectedSignature)) {
return false, &SignatureError{providedSignature}
}
if !checkDate {
return true, nil
}
// Example format: Fri 08 Sep 2017 11:24:32 UTC
date, err := time.Parse("Mon 02 Jan 2006 15:04:05 MST", dateHeader)
if err != nil {
return false, err
}
now := time.Now()
delta := math.Abs(now.Sub(date).Seconds())
if delta > 300 {
return false, &SignatureError{"outdated"}
}
return true, nil
}
// CheckIPWhitelist makes sure the provided remote address (of the form IP:port) falls within the provided IP range
// (in CIDR form or a single IP address).
func CheckIPWhitelist(remoteAddr string, ipRange string) (bool, error) {
// Extract IP address from remote address.
ip := remoteAddr
// IPv6 addresses will likely be surrounded by [].
ip := strings.Trim(remoteAddr, " []")
if strings.LastIndex(remoteAddr, ":") != -1 {
ip = remoteAddr[0:strings.LastIndex(remoteAddr, ":")]
if i := strings.LastIndex(ip, ":"); i != -1 {
ip = ip[:i]
ip = strings.Trim(ip, " []")
}
ip = strings.TrimSpace(ip)
// IPv6 addresses will likely be surrounded by [], so don't forget to remove those.
if strings.HasPrefix(ip, "[") && strings.HasSuffix(ip, "]") {
ip = ip[1 : len(ip)-1]
}
parsedIP := net.ParseIP(strings.TrimSpace(ip))
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
return false, fmt.Errorf("invalid IP address found in remote address '%s'", remoteAddr)
}
// Extract IP range in CIDR form. If a single IP address is provided, turn it into CIDR form.
for _, r := range strings.Fields(ipRange) {
// Extract IP range in CIDR form. If a single IP address is provided, turn it into CIDR form.
ipRange = strings.TrimSpace(ipRange)
if !strings.Contains(r, "/") {
r = r + "/32"
}
if strings.Index(ipRange, "/") == -1 {
ipRange = ipRange + "/32"
_, cidr, err := net.ParseCIDR(r)
if err != nil {
return false, err
}
if cidr.Contains(parsedIP) {
return true, nil
}
}
_, cidr, err := net.ParseCIDR(ipRange)
if err != nil {
return false, err
}
return cidr.Contains(parsedIP), nil
return false, nil
}
// ReplaceParameter replaces parameter value with the passed value in the passed map
@@ -175,7 +222,6 @@ func ReplaceParameter(s string, params interface{}, value interface{}) bool {
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
index, err := strconv.ParseUint(p[0], 10, 64)
@@ -210,8 +256,12 @@ func GetParameter(s string, params interface{}) (interface{}, bool) {
return nil, false
}
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
paramsValue := reflect.ValueOf(params)
switch paramsValue.Kind() {
case reflect.Slice:
paramsValueSliceLength := paramsValue.Len()
if paramsValueSliceLength > 0 {
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
index, err := strconv.ParseUint(p[0], 10, 64)
@@ -233,18 +283,20 @@ func GetParameter(s string, params interface{}) (interface{}, bool) {
}
return nil, false
}
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Map {
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
case reflect.Map:
// Check for raw key
if v, ok := params.(map[string]interface{})[s]; ok {
return v, true
}
// Checked for dotted references
p := strings.SplitN(s, ".", 2)
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
if len(p) > 1 {
return GetParameter(p[1], pValue)
}
} else {
return nil, false
}
} else {
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
return pValue, true
}
}
@@ -263,9 +315,10 @@ func ExtractParameterAsString(s string, params interface{}) (string, bool) {
// Argument type specifies the parameter key name and the source it should
// be extracted from
type Argument struct {
Source string `json:"source,omitempty"`
Name string `json:"name,omitempty"`
EnvName string `json:"envname,omitempty"`
Source string `json:"source,omitempty"`
Name string `json:"name,omitempty"`
EnvName string `json:"envname,omitempty"`
Base64Decode bool `json:"base64decode,omitempty"`
}
// Get Argument method returns the value for the Argument's key name
@@ -286,7 +339,6 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
return ha.Name, true
case SourceEntirePayload:
r, err := json.Marshal(payload)
if err != nil {
return "", false
}
@@ -294,7 +346,6 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
return string(r), true
case SourceEntireHeaders:
r, err := json.Marshal(headers)
if err != nil {
return "", false
}
@@ -302,7 +353,6 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
return string(r), true
case SourceEntireQuery:
r, err := json.Marshal(query)
if err != nil {
return "", false
}
@@ -378,17 +428,21 @@ type Hook struct {
ResponseMessage string `json:"response-message,omitempty"`
ResponseHeaders ResponseHeaders `json:"response-headers,omitempty"`
CaptureCommandOutput bool `json:"include-command-output-in-response,omitempty"`
CaptureCommandOutputOnError bool `json:"include-command-output-in-response-on-error,omitempty"`
PassEnvironmentToCommand []Argument `json:"pass-environment-to-command,omitempty"`
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command,omitempty"`
PassFileToCommand []Argument `json:"pass-file-to-command,omitempty"`
JSONStringParameters []Argument `json:"parse-parameters-as-json,omitempty"`
TriggerRule *Rules `json:"trigger-rule,omitempty"`
TriggerRuleMismatchHttpResponseCode int `json:"trigger-rule-mismatch-http-response-code,omitempty"`
IncomingPayloadContentType string `json:"incoming-payload-content-type,omitempty"`
SuccessHttpResponseCode int `json:"success-http-response-code,omitempty"`
}
// ParseJSONParameters decodes specified arguments to JSON objects and replaces the
// string with the newly created object
func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface{}) []error {
var errors = make([]error, 0)
errors := make([]error, 0)
for i := range h.JSONStringParameters {
if arg, ok := h.JSONStringParameters[i].Get(headers, query, payload); ok {
@@ -398,7 +452,6 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
decoder.UseNumber()
err := decoder.Decode(&newArg)
if err != nil {
errors = append(errors, &ParseError{err})
continue
@@ -441,8 +494,8 @@ 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{}) ([]string, []error) {
var args = make([]string, 0)
var errors = make([]error, 0)
args := make([]string, 0)
errors := make([]error, 0)
args = append(args, h.ExecuteCommand)
@@ -466,8 +519,8 @@ func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]inter
// format, based on the PassEnvironmentToCommand property that is ready to be used
// with exec.Command().
func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string]interface{}) ([]string, []error) {
var args = make([]string, 0)
var errors = make([]error, 0)
args := make([]string, 0)
errors := make([]error, 0)
for i := range h.PassEnvironmentToCommand {
if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok {
if h.PassEnvironmentToCommand[i].EnvName != "" {
@@ -489,11 +542,60 @@ func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string
return args, nil
}
// FileParameter describes a pass-file-to-command instance to be stored as file
type FileParameter struct {
File *os.File
EnvName string
Data []byte
}
// 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{}) ([]FileParameter, []error) {
args := make([]FileParameter, 0)
errors := make([]error, 0)
for i := range h.PassFileToCommand {
if arg, ok := h.PassFileToCommand[i].Get(headers, query, payload); ok {
if h.PassFileToCommand[i].EnvName == "" {
// if no environment-variable name is set, fall-back on the name
log.Printf("no ENVVAR name specified, falling back to [%s]", EnvNamespace+strings.ToUpper(h.PassFileToCommand[i].Name))
h.PassFileToCommand[i].EnvName = EnvNamespace + strings.ToUpper(h.PassFileToCommand[i].Name)
}
var fileContent []byte
if h.PassFileToCommand[i].Base64Decode {
dec, err := base64.StdEncoding.DecodeString(arg)
if err != nil {
log.Printf("error decoding string [%s]", err)
}
fileContent = []byte(dec)
} else {
fileContent = []byte(arg)
}
args = append(args, FileParameter{EnvName: h.PassFileToCommand[i].EnvName, Data: fileContent})
} else {
errors = append(errors, &ArgumentError{h.PassFileToCommand[i]})
}
}
if len(errors) > 0 {
return args, errors
}
return args, nil
}
// Hooks is an array of Hook objects
type Hooks []Hook
// LoadFromFile attempts to load hooks from specified JSON file
func (h *Hooks) LoadFromFile(path string) error {
// LoadFromFile attempts to load hooks from the specified file, which
// can be either JSON or YAML. The asTemplate parameter causes the file
// contents to be parsed as a Go text/template prior to unmarshalling.
func (h *Hooks) LoadFromFile(path string, asTemplate bool) error {
if path == "" {
return nil
}
@@ -505,6 +607,24 @@ func (h *Hooks) LoadFromFile(path string) error {
return e
}
if asTemplate {
funcMap := template.FuncMap{"getenv": getenv}
tmpl, err := template.New("hooks").Funcs(funcMap).Parse(string(file))
if err != nil {
return err
}
var buf bytes.Buffer
err = tmpl.Execute(&buf, nil)
if err != nil {
return err
}
file = buf.Bytes()
}
e = yaml.Unmarshal(file, h)
return e
}
@@ -573,7 +693,7 @@ func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, body
}
res = res && rv
if res == false {
if !res {
return res, nil
}
}
@@ -595,7 +715,7 @@ func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, body *
}
res = res || rv
if res == true {
if res {
return res, nil
}
}
@@ -629,6 +749,7 @@ const (
MatchHashSHA1 string = "payload-hash-sha1"
MatchHashSHA256 string = "payload-hash-sha256"
IPWhitelist string = "ip-whitelist"
ScalrSignature string = "scalr-signature"
)
// Evaluate MatchRule will return based on the type
@@ -636,6 +757,9 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod
if r.Type == IPWhitelist {
return CheckIPWhitelist(remoteAddr, r.IPRange)
}
if r.Type == ScalrSignature {
return CheckScalrSignature(*headers, *body, r.Secret, true)
}
if arg, ok := r.Parameter.Get(headers, query, payload); ok {
switch r.Type {
@@ -653,3 +777,8 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod
}
return false, nil
}
// getenv provides a template function to retrieve OS environment variables.
func getenv(s string) string {
return os.Getenv(s)
}
+195 -53
View File
@@ -1,11 +1,44 @@
package hook
import (
"os"
"reflect"
"strings"
"testing"
)
func TestGetParameter(t *testing.T) {
for _, test := range []struct {
key string
val interface{}
expect interface{}
ok bool
}{
// True
{"a", map[string]interface{}{"a": "1"}, "1", true},
{"a.b", map[string]interface{}{"a.b": "1"}, "1", true},
{"a.c", map[string]interface{}{"a": map[string]interface{}{"c": 2}}, 2, true},
{"a.1", map[string]interface{}{"a": map[string]interface{}{"1": 3}}, 3, true},
{"a.1", map[string]interface{}{"a": []interface{}{"a", "b"}}, "b", true},
{"0", []interface{}{"a", "b"}, "a", true},
// False
{"z", map[string]interface{}{"a": "1"}, nil, false},
{"a.z", map[string]interface{}{"a": map[string]interface{}{"b": 2}}, nil, false},
{"z.b", map[string]interface{}{"a": map[string]interface{}{"z": 2}}, nil, false},
{"a.2", map[string]interface{}{"a": []interface{}{"a", "b"}}, nil, false},
} {
res, ok := GetParameter(test.key, test.val)
if ok != test.ok {
t.Errorf("unexpected result given {%q, %q}: %t\n", test.key, test.val, ok)
}
if !reflect.DeepEqual(res, test.expect) {
t.Errorf("failed given {%q, %q}:\nexpected {%#v}\ngot {%#v}\n", test.key, test.val, test.expect, res)
}
}
}
var checkPayloadSignatureTests = []struct {
payload []byte
secret string
@@ -18,6 +51,7 @@ var checkPayloadSignatureTests = []struct {
// failures
{[]byte(`{"a": "z"}`), "secret", "XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
{[]byte(`{"a": "z"}`), "secreX", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "900225703e9342328db7307692736e2f7cc7b36e", false},
{[]byte(`{"a": "z"}`), "", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "", false},
}
func TestCheckPayloadSignature(t *testing.T) {
@@ -27,7 +61,7 @@ func TestCheckPayloadSignature(t *testing.T) {
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
}
if err != nil && strings.Contains(err.Error(), tt.mac) {
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
t.Errorf("error message should not disclose expected mac: %s", err)
}
}
@@ -44,6 +78,7 @@ var checkPayloadSignature256Tests = []struct {
{[]byte(`{"a": "z"}`), "secret", "sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
// failures
{[]byte(`{"a": "z"}`), "secret", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
{[]byte(`{"a": "z"}`), "", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "", false},
}
func TestCheckPayloadSignature256(t *testing.T) {
@@ -53,12 +88,90 @@ func TestCheckPayloadSignature256(t *testing.T) {
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
}
if err != nil && strings.Contains(err.Error(), tt.mac) {
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
t.Errorf("error message should not disclose expected mac: %s", err)
}
}
}
var checkScalrSignatureTests = []struct {
description string
headers map[string]interface{}
payload []byte
secret string
expectedSignature string
ok bool
}{
{
"Valid signature",
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "48e395e38ac48988929167df531eb2da00063a7d"},
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
"48e395e38ac48988929167df531eb2da00063a7d", true,
},
{
"Wrong signature",
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "999395e38ac48988929167df531eb2da00063a7d"},
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
"48e395e38ac48988929167df531eb2da00063a7d", false,
},
{
"Missing Date header",
map[string]interface{}{"X-Signature": "999395e38ac48988929167df531eb2da00063a7d"},
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
"48e395e38ac48988929167df531eb2da00063a7d", false,
},
{
"Missing X-Signature header",
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC"},
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
"48e395e38ac48988929167df531eb2da00063a7d", false,
},
{
"Missing signing key",
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "48e395e38ac48988929167df531eb2da00063a7d"},
[]byte(`{"a": "b"}`), "",
"48e395e38ac48988929167df531eb2da00063a7d", false,
},
}
func TestCheckScalrSignature(t *testing.T) {
for _, testCase := range checkScalrSignatureTests {
valid, err := CheckScalrSignature(testCase.headers, testCase.payload, 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)
}
if err != nil && testCase.secret != "" && strings.Contains(err.Error(), testCase.expectedSignature) {
t.Errorf("error message should not disclose expected mac: %s on test case %s", err, testCase.description)
}
}
}
var checkIPWhitelistTests = []struct {
addr string
ipRange string
expect bool
ok bool
}{
{"[ 10.0.0.1:1234 ] ", " 10.0.0.1 ", true, true},
{"[ 10.0.0.1:1234 ] ", " 10.0.0.0 ", false, true},
{"[ 10.0.0.1:1234 ] ", " 10.0.0.1 10.0.0.1 ", true, true},
{"[ 10.0.0.1:1234 ] ", " 10.0.0.0/31 ", true, true},
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/48 ", true, true},
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/48 2001:db8:1::/64", true, true},
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/64 ", false, true},
}
func TestCheckIPWhitelist(t *testing.T) {
for _, tt := range checkIPWhitelistTests {
result, err := CheckIPWhitelist(tt.addr, tt.ipRange)
if (err == nil) != tt.ok || result != tt.expect {
t.Errorf("ip whitelist test failed {%q, %q}:\nwant {expect:%#v, ok:%#v},\ngot {result:%#v, ok:%#v}", tt.addr, tt.ipRange, tt.expect, tt.ok, result, err)
}
}
}
var extractParameterTests = []struct {
s string
params interface{}
@@ -80,7 +193,7 @@ var extractParameterTests = []struct {
{"a.501.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-existent slice index
{"a.502.b", map[string]interface{}{"a": []interface{}{}}, "", false}, // non-existent slice index
{"a.b.503", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "", false}, // trailing, non-existent slice index
{"a.b", interface{}("a"), "", false}, // non-map, non-slice input
{"a.b", interface{}("a"), "", false}, // non-map, non-slice input
}
func TestExtractParameter(t *testing.T) {
@@ -111,7 +224,7 @@ var argumentGetTests = []struct {
func TestArgumentGet(t *testing.T) {
for _, tt := range argumentGetTests {
a := Argument{tt.source, tt.name, ""}
a := Argument{tt.source, tt.name, "", false}
value, ok := a.Get(tt.headers, tt.query, tt.payload)
if ok != tt.ok || value != tt.value {
t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, ok:%#v}", tt.source, tt.name, tt.value, tt.ok, value, ok)
@@ -125,14 +238,14 @@ var hookParseJSONParametersTests = []struct {
rheaders, rquery, rpayload *map[string]interface{}
ok bool
}{
{[]Argument{Argument{"header", "a", ""}}, &map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, true},
{[]Argument{Argument{"url", "a", ""}}, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
{[]Argument{Argument{"payload", "a", ""}}, nil, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
{[]Argument{Argument{"header", "z", ""}}, &map[string]interface{}{"Z": `{}`}, nil, nil, &map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true},
{[]Argument{Argument{"header", "a", "", false}}, &map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, true},
{[]Argument{Argument{"url", "a", "", false}}, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
{[]Argument{Argument{"payload", "a", "", false}}, nil, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
{[]Argument{Argument{"header", "z", "", false}}, &map[string]interface{}{"Z": `{}`}, nil, nil, &map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true},
// failures
{[]Argument{Argument{"header", "z", ""}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // empty string
{[]Argument{Argument{"header", "y", ""}}, &map[string]interface{}{"X": `{}`}, nil, nil, &map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter
{[]Argument{Argument{"string", "z", ""}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source
{[]Argument{Argument{"header", "z", "", false}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // empty string
{[]Argument{Argument{"header", "y", "", false}}, &map[string]interface{}{"X": `{}`}, nil, nil, &map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter
{[]Argument{Argument{"string", "z", "", false}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source
}
func TestHookParseJSONParameters(t *testing.T) {
@@ -152,9 +265,9 @@ var hookExtractCommandArgumentsTests = []struct {
value []string
ok bool
}{
{"test", []Argument{Argument{"header", "a", ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"test", "z"}, true},
{"test", []Argument{Argument{"header", "a", "", false}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"test", "z"}, true},
// failures
{"fail", []Argument{Argument{"payload", "a", ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"fail", ""}, false},
{"fail", []Argument{Argument{"payload", "a", "", false}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"fail", ""}, false},
}
func TestHookExtractCommandArguments(t *testing.T) {
@@ -196,14 +309,14 @@ var hookExtractCommandArgumentsForEnvTests = []struct {
// successes
{
"test",
[]Argument{Argument{"header", "a", ""}},
[]Argument{Argument{"header", "a", "", false}},
&map[string]interface{}{"A": "z"}, nil, nil,
[]string{"HOOK_a=z"},
true,
},
{
"test",
[]Argument{Argument{"header", "a", "MYKEY"}},
[]Argument{Argument{"header", "a", "MYKEY", false}},
&map[string]interface{}{"A": "z"}, nil, nil,
[]string{"MYKEY=z"},
true,
@@ -211,7 +324,7 @@ var hookExtractCommandArgumentsForEnvTests = []struct {
// failures
{
"fail",
[]Argument{Argument{"payload", "a", ""}},
[]Argument{Argument{"payload", "a", "", false}},
&map[string]interface{}{"A": "z"}, nil, nil,
[]string{},
false,
@@ -229,26 +342,55 @@ func TestHookExtractCommandArgumentsForEnv(t *testing.T) {
}
var hooksLoadFromFileTests = []struct {
path string
ok bool
path string
asTemplate bool
ok bool
}{
{"../hooks.json.example", true},
{"../hooks.yaml.example", true},
{"", true},
{"../hooks.json.example", false, true},
{"../hooks.yaml.example", false, true},
{"../hooks.json.tmpl.example", true, true},
{"../hooks.yaml.tmpl.example", true, true},
{"", false, true},
// failures
{"missing.json", false},
{"missing.json", false, false},
}
func TestHooksLoadFromFile(t *testing.T) {
secret := `foo"123`
os.Setenv("XXXTEST_SECRET", secret)
for _, tt := range hooksLoadFromFileTests {
h := &Hooks{}
err := h.LoadFromFile(tt.path)
err := h.LoadFromFile(tt.path, tt.asTemplate)
if (err == nil) != tt.ok {
t.Errorf(err.Error())
}
}
}
func TestHooksTemplateLoadFromFile(t *testing.T) {
secret := `foo"123`
os.Setenv("XXXTEST_SECRET", secret)
for _, tt := range hooksLoadFromFileTests {
if !tt.asTemplate {
continue
}
h := &Hooks{}
err := h.LoadFromFile(tt.path, tt.asTemplate)
if (err == nil) != tt.ok {
t.Errorf(err.Error())
continue
}
s := (*h.Match("webhook").TriggerRule.And)[0].Match.Secret
if s != secret {
t.Errorf("Expected secret of %q, got %q", secret, s)
}
}
}
var hooksMatchTests = []struct {
id string
hooks Hooks
@@ -276,18 +418,18 @@ var matchRuleTests = []struct {
ok bool
err bool
}{
{"value", "", "", "z", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
{"regex", "^z", "", "z", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
{"value", "", "", "z", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
{"regex", "^z", "", "z", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
// failures
{"value", "", "", "X", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
{"regex", "^X", "", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
{"value", "", "2", "X", "", Argument{"header", "a", ""}, &map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, false}, // reference invalid header
{"value", "", "", "X", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
{"regex", "^X", "", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
{"value", "", "2", "X", "", Argument{"header", "a", "", false}, &map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, false}, // reference invalid header
// errors
{"regex", "*", "", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"regex", "*", "", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
// IP whitelisting, valid cases
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
@@ -324,8 +466,8 @@ var andRuleTests = []struct {
{
"(a=z, b=y): a=z && b=y",
AndRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
{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, []byte{},
true, false,
@@ -333,8 +475,8 @@ var andRuleTests = []struct {
{
"(a=z, b=Y): a=z && b=y",
AndRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
{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, []byte{},
false, false,
@@ -343,22 +485,22 @@ var andRuleTests = []struct {
{
"(a=z, b=y, c=x, d=w=, e=X, f=X): a=z && (b=y && c=x) && (d=w || e=v) && !f=u",
AndRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{
And: &AndRule{
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", ""}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", "", false}, ""}},
},
},
{
Or: &OrRule{
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", ""}, ""}},
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", ""}, ""}},
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", "", false}, ""}},
},
},
{
Not: &NotRule{
Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", ""}, ""},
Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", "", false}, ""},
},
},
},
@@ -369,7 +511,7 @@ var andRuleTests = []struct {
// failures
{
"invalid rule",
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}, ""}}},
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}},
&map[string]interface{}{"Y": "z"}, nil, nil, nil,
false, false,
},
@@ -395,8 +537,8 @@ var orRuleTests = []struct {
{
"(a=z, b=X): a=z || b=y",
OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
{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, []byte{},
true, false,
@@ -404,8 +546,8 @@ var orRuleTests = []struct {
{
"(a=X, b=y): a=z || b=y",
OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
{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, []byte{},
true, false,
@@ -413,8 +555,8 @@ var orRuleTests = []struct {
{
"(a=Z, b=Y): a=z || b=y",
OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
{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, []byte{},
false, false,
@@ -423,7 +565,7 @@ var orRuleTests = []struct {
{
"invalid rule",
OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
},
&map[string]interface{}{"Y": "Z"}, nil, nil, []byte{},
false, false,
@@ -447,8 +589,8 @@ var notRuleTests = []struct {
ok bool
err bool
}{
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}, ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}, &map[string]interface{}{"A": "z"}, 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, []byte{}, false, false},
}
func TestNotRule(t *testing.T) {
+60
View File
@@ -0,0 +1,60 @@
[
{
"id": "webhook",
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
"command-working-directory": "/home/adnan/go",
"response-message": "I got the payload!",
"response-headers":
[
{
"name": "Access-Control-Allow-Origin",
"value": "*"
}
],
"pass-arguments-to-command":
[
{
"source": "payload",
"name": "head_commit.id"
},
{
"source": "payload",
"name": "pusher.name"
},
{
"source": "payload",
"name": "pusher.email"
}
],
"trigger-rule":
{
"and":
[
{
"match":
{
"type": "payload-hash-sha1",
"secret": "{{ getenv "XXXTEST_SECRET" | js }}",
"parameter":
{
"source": "header",
"name": "X-Hub-Signature"
}
}
},
{
"match":
{
"type": "value",
"value": "refs/heads/master",
"parameter":
{
"source": "payload",
"name": "ref"
}
}
}
]
}
}
]
+28
View File
@@ -0,0 +1,28 @@
- id: webhook
execute-command: /home/adnan/redeploy-go-webhook.sh
command-working-directory: /home/adnan/go
response-message: I got the payload!
response-headers:
- name: Access-Control-Allow-Origin
value: '*'
pass-arguments-to-command:
- source: payload
name: head_commit.id
- source: payload
name: pusher.name
- source: payload
name: pusher.email
trigger-rule:
and:
- match:
type: payload-hash-sha1
secret: "{{ getenv "XXXTEST_SECRET" | js }}"
parameter:
source: header
name: X-Hub-Signature
- match:
type: value
value: refs/heads/master
parameter:
source: payload
name: ref
+11
View File
@@ -5,6 +5,7 @@ package main
import (
"fmt"
"os"
"strconv"
"strings"
)
@@ -23,4 +24,14 @@ func main() {
if len(env) > 0 {
fmt.Printf("env: %s\n", strings.Join(env, " "))
}
if (len(os.Args) > 1) && (strings.HasPrefix(os.Args[1], "exit=")) {
exit_code_str := os.Args[1][5:]
exit_code, err := strconv.Atoi(exit_code_str)
if err != nil {
fmt.Printf("Exit code %s not an int!", exit_code_str)
os.Exit(-1)
}
os.Exit(exit_code)
}
}
+68
View File
@@ -136,5 +136,73 @@
}
}
}
},
{
"id": "capture-command-output-on-success-not-by-default",
"pass-arguments-to-command": [
{
"source": "string",
"name": "exit=0"
}
],
"execute-command": "{{ .Hookecho }}"
},
{
"id": "capture-command-output-on-success-yes-with-flag",
"pass-arguments-to-command": [
{
"source": "string",
"name": "exit=0"
}
],
"execute-command": "{{ .Hookecho }}",
"include-command-output-in-response": true
},
{
"id": "capture-command-output-on-error-not-by-default",
"pass-arguments-to-command": [
{
"source": "string",
"name": "exit=1"
}
],
"execute-command": "{{ .Hookecho }}",
"include-command-output-in-response": true
},
{
"id": "capture-command-output-on-error-yes-with-extra-flag",
"pass-arguments-to-command": [
{
"source": "string",
"name": "exit=1"
}
],
"execute-command": "{{ .Hookecho }}",
"include-command-output-in-response": true,
"include-command-output-in-response-on-error": true
},
{
"id": "static-params-ok",
"execute-command": "{{ .Hookecho }}",
"response-message": "success",
"include-command-output-in-response": true,
"pass-arguments-to-command": [
{
"source": "string",
"name": "passed"
}
],
},
{
"id": "warn-on-space",
"execute-command": "{{ .Hookecho }} foo",
"response-message": "success",
"include-command-output-in-response": true,
"pass-arguments-to-command": [
{
"source": "string",
"name": "passed"
}
],
}
]
+47 -6
View File
@@ -1,4 +1,5 @@
- trigger-rule:
- id: github
trigger-rule:
and:
- match:
parameter:
@@ -23,9 +24,10 @@
pass-environment-to-command:
- source: payload
name: head_commit.timestamp
id: github
command-working-directory: /
- trigger-rule:
- id: bitbucket
trigger-rule:
and:
- match:
parameter:
@@ -52,9 +54,10 @@
execute-command: '{{ .Hookecho }}'
response-message: success
include-command-output-in-response: false
id: bitbucket
command-working-directory: /
- trigger-rule:
- id: gitlab
trigger-rule:
match:
parameter:
source: payload
@@ -71,5 +74,43 @@
execute-command: '{{ .Hookecho }}'
response-message: success
include-command-output-in-response: true
id: gitlab
command-working-directory: /
- id: capture-command-output-on-success-not-by-default
pass-arguments-to-command:
- source: string
name: exit=0
execute-command: '{{ .Hookecho }}'
- id: capture-command-output-on-success-yes-with-flag
pass-arguments-to-command:
- source: string
name: exit=0
execute-command: '{{ .Hookecho }}'
include-command-output-in-response: true
- id: capture-command-output-on-error-not-by-default
pass-arguments-to-command:
- source: string
name: exit=1
execute-command: '{{ .Hookecho }}'
include-command-output-in-response: true
- id: capture-command-output-on-error-yes-with-extra-flag
pass-arguments-to-command:
- source: string
name: exit=1
execute-command: '{{ .Hookecho }}'
include-command-output-in-response: true
include-command-output-in-response-on-error: true
- id: static-params-ok
execute-command: '{{ .Hookecho }}'
include-command-output-in-response: true
pass-arguments-to-command:
- source: string
name: passed
- id: warn-on-space
execute-command: '{{ .Hookecho }} foo'
include-command-output-in-response: true
+97 -31
View File
@@ -10,6 +10,7 @@ import (
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
@@ -23,7 +24,7 @@ import (
)
const (
version = "2.6.6"
version = "2.6.10"
)
var (
@@ -34,6 +35,7 @@ var (
hotReload = flag.Bool("hotreload", false, "watch hooks file for changes and reload them automatically")
hooksURLPrefix = flag.String("urlprefix", "hooks", "url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id)")
secure = flag.Bool("secure", false, "use HTTPS instead of HTTP")
asTemplate = flag.Bool("template", false, "parse hooks file as a Go template")
cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file")
key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file")
justDisplayVersion = flag.Bool("version", false, "display webhook version and quit")
@@ -99,7 +101,7 @@ func main() {
newHooks := hook.Hooks{}
err := newHooks.LoadFromFile(hooksFilePath)
err := newHooks.LoadFromFile(hooksFilePath, *asTemplate)
if err != nil {
log.Printf("couldn't load hooks from file! %+v\n", err)
@@ -119,7 +121,7 @@ func main() {
newHooksFiles := hooksFiles[:0]
for _, filePath := range hooksFiles {
if _, ok := loadedHooksFromFiles[filePath]; ok == true {
if _, ok := loadedHooksFromFiles[filePath]; ok {
newHooksFiles = append(newHooksFiles, filePath)
}
}
@@ -184,6 +186,10 @@ func main() {
hooksURL = "/" + *hooksURLPrefix + "/{id}"
}
router.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprint(w, "OK")
})
router.HandleFunc(hooksURL, hookHandler)
n.UseHandler(router)
@@ -228,7 +234,11 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
// parse body
var payload map[string]interface{}
// set contentType to IncomingPayloadContentType or header value
contentType := r.Header.Get("Content-Type")
if len(matchedHook.IncomingPayloadContentType) != 0 {
contentType = matchedHook.IncomingPayloadContentType
}
if strings.Contains(contentType, "json") {
decoder := json.NewDecoder(strings.NewReader(string(body)))
@@ -249,10 +259,9 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
}
// handle hook
if errors := matchedHook.ParseJSONParameters(&headers, &query, &payload); errors != nil {
for _, err := range errors {
log.Printf("[%s] error parsing JSON parameters: %s\n", rid, err)
}
errors := matchedHook.ParseJSONParameters(&headers, &query, &payload)
for _, err := range errors {
log.Printf("[%s] error parsing JSON parameters: %s\n", rid, err)
}
var ok bool
@@ -263,9 +272,9 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
ok, err = matchedHook.TriggerRule.Evaluate(&headers, &query, &payload, &body, r.RemoteAddr)
if err != nil {
msg := fmt.Sprintf("[%s] error evaluating hook: %s", rid, err)
log.Printf(msg)
log.Print(msg)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Error occurred while evaluating hook rules.")
fmt.Fprint(w, "Error occurred while evaluating hook rules.")
return
}
}
@@ -281,37 +290,45 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
response, err := handleHook(matchedHook, rid, &headers, &query, &payload, &body)
if err != nil {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Error occurred while executing the hook's command. Please check your logs for more details.")
if matchedHook.CaptureCommandOutputOnError {
fmt.Fprint(w, response)
} else {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprint(w, "Error occurred while executing the hook's command. Please check your logs for more details.")
}
} else {
fmt.Fprintf(w, response)
// Check if a success return code is configured for the hook
if matchedHook.SuccessHttpResponseCode != 0 {
writeHttpResponseCode(w, rid, matchedHook.ID, matchedHook.SuccessHttpResponseCode)
}
fmt.Fprint(w, response)
}
} else {
go handleHook(matchedHook, rid, &headers, &query, &payload, &body)
fmt.Fprintf(w, matchedHook.ResponseMessage)
// Check if a success return code is configured for the hook
if matchedHook.SuccessHttpResponseCode != 0 {
writeHttpResponseCode(w, rid, matchedHook.ID, matchedHook.SuccessHttpResponseCode)
}
fmt.Fprint(w, matchedHook.ResponseMessage)
}
return
}
// Check if a return code is configured for the hook
if matchedHook.TriggerRuleMismatchHttpResponseCode != 0 {
// Check if the configured return code is supported by the http package
// by testing if there is a StatusText for this code.
if len(http.StatusText(matchedHook.TriggerRuleMismatchHttpResponseCode)) > 0 {
w.WriteHeader(matchedHook.TriggerRuleMismatchHttpResponseCode)
} else {
log.Printf("[%s] %s got matched, but the configured return code %d is unknown - defaulting to 200\n", rid, matchedHook.ID, matchedHook.TriggerRuleMismatchHttpResponseCode)
}
writeHttpResponseCode(w, rid, 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)
fmt.Fprintf(w, "Hook rules were not satisfied.")
fmt.Fprint(w, "Hook rules were not satisfied.")
} else {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Hook not found.")
fmt.Fprint(w, "Hook not found.")
}
}
@@ -320,6 +337,13 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in
// 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)
}
if err != nil {
log.Printf("unable to locate command: '%s'", h.ExecuteCommand)
@@ -336,19 +360,41 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in
cmd.Dir = h.CommandWorkingDirectory
cmd.Args, errors = h.ExtractCommandArguments(headers, query, payload)
if errors != nil {
for _, err := range errors {
log.Printf("[%s] error extracting command arguments: %s\n", rid, err)
}
for _, err := range errors {
log.Printf("[%s] error extracting command arguments: %s\n", rid, err)
}
var envs []string
envs, errors = h.ExtractCommandArgumentsForEnv(headers, query, payload)
if errors != nil {
for _, err := range errors {
log.Printf("[%s] error extracting command arguments for environment: %s\n", rid, err)
for _, err := range errors {
log.Printf("[%s] error extracting command arguments for environment: %s\n", rid, err)
}
files, errors := h.ExtractCommandArgumentsForFile(headers, query, payload)
for _, err := range errors {
log.Printf("[%s] error extracting command arguments for file: %s\n", rid, 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]", rid, err)
continue
}
log.Printf("[%s] writing env %s file %s", rid, files[i].EnvName, tmpfile.Name())
if _, err := tmpfile.Write(files[i].Data); err != nil {
log.Printf("[%s] error writing file %s [%s]", rid, tmpfile.Name(), err)
continue
}
if err := tmpfile.Close(); err != nil {
log.Printf("[%s] error closing file %s [%s]", rid, tmpfile.Name(), err)
continue
}
files[i].File = tmpfile
envs = append(envs, files[i].EnvName+"="+tmpfile.Name())
}
cmd.Env = append(os.Environ(), envs...)
@@ -363,18 +409,38 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in
log.Printf("[%s] error occurred: %+v\n", rid, err)
}
for i := range files {
if files[i].File != nil {
log.Printf("[%s] removing file %s\n", rid, files[i].File.Name())
err := os.Remove(files[i].File.Name())
if err != nil {
log.Printf("[%s] error removing file %s [%s]", rid, files[i].File.Name(), err)
}
}
}
log.Printf("[%s] finished handling %s\n", rid, h.ID)
return string(out), err
}
func writeHttpResponseCode(w http.ResponseWriter, rid string, 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 {
w.WriteHeader(responseCode)
} else {
log.Printf("[%s] %s got matched, but the configured return code %d is unknown - defaulting to 200\n", rid, hookId, responseCode)
}
}
func reloadHooks(hooksFilePath string) {
hooksInFile := hook.Hooks{}
// parse and swap
log.Printf("attempting to reload hooks from %s\n", hooksFilePath)
err := hooksInFile.LoadFromFile(hooksFilePath)
err := hooksInFile.LoadFromFile(hooksFilePath, *asTemplate)
if err != nil {
log.Printf("couldn't load hooks from file! %+v\n", err)
@@ -393,7 +459,7 @@ func reloadHooks(hooksFilePath string) {
}
}
if (matchLoadedHook(hook.ID) != nil && !wasHookIDAlreadyLoaded) || seenHooksIds[hook.ID] == true {
if (matchLoadedHook(hook.ID) != nil && !wasHookIDAlreadyLoaded) || seenHooksIds[hook.ID] {
log.Printf("error: hook with the id %s has already been loaded!\nplease check your hooks file for duplicate hooks ids!", hook.ID)
log.Println("reverting hooks back to the previous configuration")
return
+158 -124
View File
@@ -13,6 +13,7 @@ import (
"regexp"
"runtime"
"strings"
"sync"
"testing"
"text/template"
"time"
@@ -21,15 +22,26 @@ import (
)
func TestStaticParams(t *testing.T) {
// FIXME(moorereason): incorporate this test into TestWebhook.
// Need to be able to execute a binary with a space in the filename.
if runtime.GOOS == "windows" {
t.Skip("Skipping on Windows")
}
spHeaders := make(map[string]interface{})
spHeaders["User-Agent"] = "curl/7.54.0"
spHeaders["Accept"] = "*/*"
// case 1: correct entry
// case 2: binary with spaces in its name
err := os.Symlink("/bin/echo", "/tmp/with space")
if err != nil {
t.Fatalf("%v", err)
}
defer os.Remove("/tmp/with space")
spHook := &hook.Hook{
ID: "static-params-ok",
ExecuteCommand: "/bin/echo",
ID: "static-params-name-space",
ExecuteCommand: "/tmp/with space",
CommandWorkingDirectory: "/tmp",
ResponseMessage: "success",
CaptureCommandOutput: true,
@@ -41,159 +53,125 @@ func TestStaticParams(t *testing.T) {
b := &bytes.Buffer{}
log.SetOutput(b)
s, err := handleHook(spHook, &spHeaders, &map[string]interface{}{}, &map[string]interface{}{}, &[]byte{})
_, err = handleHook(spHook, "test", &spHeaders, &map[string]interface{}{}, &map[string]interface{}{}, &[]byte{})
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
matched, _ := regexp.MatchString("(?s).*command output: passed.*static-params-ok", b.String())
if !matched {
t.Fatalf("Unexpected log output:\n%s", b)
}
// case 2: binary with spaces in its name
err = os.Symlink("/bin/true", "/tmp/with space")
if err != nil {
t.Fatalf("%v", err)
}
defer os.Remove("/tmp/with space")
spHook = &hook.Hook{
ID: "static-params-name-space",
ExecuteCommand: "/tmp/with space",
CommandWorkingDirectory: "/tmp",
ResponseMessage: "success",
CaptureCommandOutput: true,
PassArgumentsToCommand: []hook.Argument{
hook.Argument{Source: "string", Name: "passed"},
},
}
b = &bytes.Buffer{}
log.SetOutput(b)
s, err = handleHook(spHook, &spHeaders, &map[string]interface{}{}, &map[string]interface{}{}, &[]byte{})
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
matched, _ = regexp.MatchString("(?s)command output: .*static-params-name-space", b.String())
matched, _ := regexp.MatchString("(?s)command output: .*static-params-name-space", b.String())
if !matched {
t.Fatalf("Unexpected log output:\n%sn", b)
}
// case 3: parameters specified in execute-command
spHook = &hook.Hook{
ID: "static-params-bad",
ExecuteCommand: "/bin/echo success",
CommandWorkingDirectory: "/tmp",
ResponseMessage: "success",
CaptureCommandOutput: true,
PassArgumentsToCommand: []hook.Argument{
hook.Argument{Source: "string", Name: "failed"},
},
}
b = &bytes.Buffer{}
log.SetOutput(b)
s, err = handleHook(spHook, &spHeaders, &map[string]interface{}{}, &map[string]interface{}{}, &[]byte{})
if err == nil {
t.Fatalf("Error expected, but none returned: %s\n", s)
}
matched, _ = regexp.MatchString("(?s)unable to locate command: ..bin.echo success.*use 'pass-arguments-to-command'", b.String())
if !matched {
t.Fatalf("Unexpected log output:\n%s\n", b)
}
}
func TestWebhook(t *testing.T) {
hookecho, cleanupHookecho := buildHookecho(t)
defer cleanupHookecho()
webhook, cleanupWebhookFn := buildWebhook(t)
defer cleanupWebhookFn()
for _, hookTmpl := range []string{"test/hooks.json.tmpl", "test/hooks.yaml.tmpl"} {
config, cleanupConfig := genConfig(t, hookecho, hookTmpl)
defer cleanupConfig()
webhook, cleanupWebhook := buildWebhook(t)
defer cleanupWebhook()
ip, port := serverAddress(t)
args := []string{fmt.Sprintf("-hooks=%s", config), fmt.Sprintf("-ip=%s", ip), fmt.Sprintf("-port=%s", port), "-verbose"}
cmd := exec.Command(webhook, args...)
//cmd.Stderr = os.Stderr // uncomment to see verbose output
cmd.Env = webhookEnv()
cmd.Args[0] = "webhook"
if err := cmd.Start(); err != nil {
t.Fatalf("failed to start webhook: %s", err)
}
defer killAndWait(cmd)
waitForServerReady(t, ip, port)
configPath, cleanupConfigFn := genConfig(t, hookecho, hookTmpl)
defer cleanupConfigFn()
for _, tt := range hookHandlerTests {
url := fmt.Sprintf("http://%s:%s/hooks/%s", ip, port, tt.id)
t.Run(tt.desc, 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"}
req, err := http.NewRequest("POST", url, ioutil.NopCloser(strings.NewReader(tt.body)))
if err != nil {
t.Errorf("New request failed: %s", err)
}
// Setup a buffer for capturing webhook logs for later evaluation
b := &buffer{}
for k, v := range tt.headers {
req.Header.Add(k, v)
}
cmd := exec.Command(webhook, args...)
cmd.Stderr = b
cmd.Env = webhookEnv()
cmd.Args[0] = "webhook"
if err := cmd.Start(); err != nil {
t.Fatalf("failed to start webhook: %s", err)
}
defer killAndWait(cmd)
var res *http.Response
waitForServerReady(t, ip, port)
if tt.urlencoded == true {
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
} else {
req.Header.Add("Content-Type", "application/json")
}
url := fmt.Sprintf("http://%s:%s/hooks/%s", ip, port, tt.id)
client := &http.Client{}
res, err = client.Do(req)
if err != nil {
t.Errorf("client.Do failed: %s", err)
}
req, err := http.NewRequest("POST", url, ioutil.NopCloser(strings.NewReader(tt.body)))
if err != nil {
t.Errorf("New request failed: %s", err)
}
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Errorf("POST %q: failed to ready body: %s", tt.desc, err)
}
for k, v := range tt.headers {
req.Header.Add(k, v)
}
if res.StatusCode != tt.respStatus || string(body) != tt.respBody {
t.Errorf("failed %q (id: %s):\nexpected status: %#v, response: %s\ngot status: %#v, response: %s", tt.desc, tt.id, tt.respStatus, tt.respBody, res.StatusCode, body)
}
var res *http.Response
if tt.urlencoded == true {
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
} else {
req.Header.Add("Content-Type", "application/json")
}
client := &http.Client{}
res, err = client.Do(req)
if err != nil {
t.Errorf("client.Do failed: %s", err)
}
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Errorf("POST %q: failed to ready body: %s", tt.desc, err)
}
if res.StatusCode != tt.respStatus || string(body) != tt.respBody {
t.Errorf("failed %q (id: %s):\nexpected status: %#v, response: %s\ngot status: %#v, response: %s", tt.desc, tt.id, tt.respStatus, tt.respBody, res.StatusCode, body)
}
if tt.logMatch == "" {
return
}
// There's the potential for a race condition below where we
// try to read the logs buffer b before the logs have been
// flushed by the webhook process. Kill the process to flush
// the logs.
killAndWait(cmd)
matched, _ := regexp.MatchString(tt.logMatch, b.String())
if !matched {
t.Errorf("failed log match for %q (id: %s):\nmatch pattern: %q\ngot:\n%s", tt.desc, tt.id, tt.logMatch, b)
}
})
}
}
}
func buildHookecho(t *testing.T) (bin string, cleanup func()) {
func buildHookecho(t *testing.T) (binPath string, cleanupFn func()) {
tmp, err := ioutil.TempDir("", "hookecho-test-")
if err != nil {
t.Fatal(err)
}
defer func() {
if cleanup == nil {
if cleanupFn == nil {
os.RemoveAll(tmp)
}
}()
bin = filepath.Join(tmp, "hookecho")
binPath = filepath.Join(tmp, "hookecho")
if runtime.GOOS == "windows" {
bin += ".exe"
binPath += ".exe"
}
cmd := exec.Command("go", "build", "-o", bin, "test/hookecho.go")
cmd := exec.Command("go", "build", "-o", binPath, "test/hookecho.go")
if err := cmd.Run(); err != nil {
t.Fatalf("Building hookecho: %v", err)
}
return bin, func() { os.RemoveAll(tmp) }
return binPath, func() { os.RemoveAll(tmp) }
}
func genConfig(t *testing.T, bin string, hookTemplate string) (config string, cleanup func()) {
func genConfig(t *testing.T, bin string, hookTemplate string) (configPath string, cleanupFn func()) {
tmpl := template.Must(template.ParseFiles(hookTemplate))
tmp, err := ioutil.TempDir("", "webhook-config-")
@@ -201,7 +179,7 @@ func genConfig(t *testing.T, bin string, hookTemplate string) (config string, cl
t.Fatal(err)
}
defer func() {
if cleanup == nil {
if cleanupFn == nil {
os.RemoveAll(tmp)
}
}()
@@ -215,7 +193,11 @@ func genConfig(t *testing.T, bin string, hookTemplate string) (config string, cl
}
defer file.Close()
data := struct{ Hookecho string }{filepath.ToSlash(bin)}
data := struct{ Hookecho string }{filepath.FromSlash(bin)}
if runtime.GOOS == "windows" {
// Simulate escaped backslashes on Windows.
data.Hookecho = strings.Replace(data.Hookecho, `\`, `\\`, -1)
}
if err := tmpl.Execute(file, data); err != nil {
t.Fatalf("Executing template: %v", err)
}
@@ -223,28 +205,28 @@ func genConfig(t *testing.T, bin string, hookTemplate string) (config string, cl
return path, func() { os.RemoveAll(tmp) }
}
func buildWebhook(t *testing.T) (bin string, cleanup func()) {
func buildWebhook(t *testing.T) (binPath string, cleanupFn func()) {
tmp, err := ioutil.TempDir("", "webhook-test-")
if err != nil {
t.Fatal(err)
}
defer func() {
if cleanup == nil {
if cleanupFn == nil {
os.RemoveAll(tmp)
}
}()
bin = filepath.Join(tmp, "webhook")
binPath = filepath.Join(tmp, "webhook")
if runtime.GOOS == "windows" {
bin += ".exe"
binPath += ".exe"
}
cmd := exec.Command("go", "build", "-o", bin)
cmd := exec.Command("go", "build", "-o", binPath)
if err := cmd.Run(); err != nil {
t.Fatalf("Building webhook: %v", err)
}
return bin, func() { os.RemoveAll(tmp) }
return binPath, func() { os.RemoveAll(tmp) }
}
func serverAddress(t *testing.T) (string, string) {
@@ -266,7 +248,7 @@ func serverAddress(t *testing.T) (string, string) {
func waitForServerReady(t *testing.T, ip, port string) {
waitForServer(t,
fmt.Sprintf("http://%v:%v/", ip, port),
http.StatusNotFound,
http.StatusOK,
5*time.Second)
}
@@ -288,6 +270,10 @@ func waitForServer(t *testing.T, url string, status int, timeout time.Duration)
}
func killAndWait(cmd *exec.Cmd) {
if cmd == nil || cmd.ProcessState != nil && cmd.ProcessState.Exited() {
return
}
cmd.Process.Kill()
cmd.Wait()
}
@@ -313,6 +299,7 @@ var hookHandlerTests = []struct {
respStatus int
respBody string
logMatch string
}{
{
"github",
@@ -467,6 +454,7 @@ var hookHandlerTests = []struct {
`arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
`,
``,
},
{
"bitbucket", // bitbucket sends their payload using uriencoded params.
@@ -476,6 +464,7 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
true,
http.StatusOK,
`success`,
``,
},
{
"gitlab",
@@ -527,6 +516,7 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
http.StatusOK,
`arg: b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327 John Smith john@example.com
`,
``,
},
{
@@ -567,6 +557,7 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
`arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
`,
``,
},
{
@@ -605,12 +596,55 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
http.StatusOK,
`arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
`,
``,
},
// test with custom return code
{"empty payload", "github", nil, `{}`, false, http.StatusBadRequest, `Hook rules were not satisfied.`},
{"empty payload", "github", nil, `{}`, false, http.StatusBadRequest, `Hook rules were not satisfied.`, ``},
// test with custom invalid http code, should default to 200 OK
{"empty payload", "bitbucket", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`},
{"empty payload", "bitbucket", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`, ``},
// test with no configured http return code, should default to 200 OK
{"empty payload", "gitlab", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`},
{"empty payload", "gitlab", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`, ``},
// test capturing command output
{"don't capture output on success by default", "capture-command-output-on-success-not-by-default", nil, `{}`, false, http.StatusOK, ``, ``},
{"capture output on success with flag set", "capture-command-output-on-success-yes-with-flag", nil, `{}`, false, http.StatusOK, `arg: exit=0
`, ``},
{"don't capture output on error by default", "capture-command-output-on-error-not-by-default", nil, `{}`, false, http.StatusInternalServerError, `Error occurred while executing the hook's command. Please check your logs for more details.`, ``},
{"capture output on error with extra flag set", "capture-command-output-on-error-yes-with-extra-flag", nil, `{}`, false, http.StatusInternalServerError, `arg: exit=1
`, ``},
// Check logs
{"static params should pass", "static-params-ok", nil, `{}`, false, http.StatusOK, "arg: passed\n", `(?s)command output: arg: passed`},
{"command with space logs warning", "warn-on-space", nil, `{}`, false, 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`},
}
// buffer provides a concurrency-safe bytes.Buffer to tests above.
type buffer struct {
b bytes.Buffer
m sync.Mutex
}
func (b *buffer) Read(p []byte) (n int, err error) {
b.m.Lock()
defer b.m.Unlock()
return b.b.Read(p)
}
func (b *buffer) Write(p []byte) (n int, err error) {
b.m.Lock()
defer b.m.Unlock()
return b.b.Write(p)
}
func (b *buffer) String() string {
b.m.Lock()
defer b.m.Unlock()
return b.b.String()
}
func (b *buffer) Reset() {
b.m.Lock()
defer b.m.Unlock()
b.b.Reset()
}