Compare commits

...

82 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
Donald Clark Jackson f84edae99d added support for incoming-payload-content-type 2017-11-27 14:02:57 -08:00
14 changed files with 993 additions and 521 deletions
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
open_collective: webhook
+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 ./...
+21 -4
View File
@@ -18,7 +18,7 @@ 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://my.hookdoo.com/logo/logo-dark-96.png" alt="hookdoo" align="left" /></a>
<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
@@ -33,6 +33,9 @@ $ go get 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.
@@ -45,7 +48,7 @@ Prebuilt binaries for different architectures are available at [GitHub Releases]
## Configuration
Next step is to define some hooks you want [webhook][w] to serve. Begin by creating an empty file named `hooks.json`. This file will contain an array of hooks the [webhook][w] will serve. Check [Hook definition page](docs/Hook-Definition.md) to see the detailed description of what properties a hook can contain, and how to use them.
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
@@ -92,7 +95,21 @@ Check out [Hook examples page](docs/Hook-Examples.md) for more complex examples
### 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.
@@ -102,7 +119,7 @@ Check out [existing issues](https://github.com/adnanh/webhook/issues) to see if
# Support active development
## Sponsors
## <a href="https://www.digitalocean.com/?ref=webhook"><img src="https://www.digitalocean.com/assets/media/logos-badges/png/DO_Logo_Horizontal_Blue-3db19536.png" alt="DigitalOcean" width="250"/></a>
## <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.
+5 -3
View File
@@ -8,15 +8,17 @@ Hooks are defined as JSON objects. Please note that in order to be considered va
* `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) 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
* `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) 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
* `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) to see the list of valid rules and their usage
* `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
+426 -283
View File
@@ -1,283 +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"
}
}
}
]
}
}
]
```
## 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>
# 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"
}
}
}
]
}
}
]
```
+16
View File
@@ -198,4 +198,20 @@ The IP can be IPv4- or IPv6-formatted, using [CIDR notation](https://en.wikipedi
"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"
}
}
```
+102 -53
View File
@@ -12,6 +12,7 @@ import (
"fmt"
"io/ioutil"
"log"
"math"
"net"
"net/textproto"
"os"
@@ -20,6 +21,7 @@ import (
"strconv"
"strings"
"text/template"
"time"
"github.com/ghodss/yaml"
)
@@ -92,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 {
@@ -111,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 {
@@ -128,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
@@ -180,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)
@@ -215,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)
@@ -238,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
}
}
@@ -292,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
}
@@ -300,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
}
@@ -308,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
}
@@ -391,12 +435,14 @@ type Hook struct {
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 {
@@ -406,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
@@ -449,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)
@@ -474,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 != "" {
@@ -508,8 +553,8 @@ type FileParameter struct {
// 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) {
var args = make([]FileParameter, 0)
var errors = make([]error, 0)
args := make([]FileParameter, 0)
errors := make([]error, 0)
for i := range h.PassFileToCommand {
if arg, ok := h.PassFileToCommand[i].Get(headers, query, payload); ok {
@@ -648,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
}
}
@@ -670,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
}
}
@@ -704,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
@@ -711,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 {
+115 -3
View File
@@ -7,6 +7,38 @@ import (
"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
@@ -19,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) {
@@ -28,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)
}
}
@@ -45,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) {
@@ -54,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{}
@@ -81,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) {
+1 -1
View File
@@ -5,8 +5,8 @@ package main
import (
"fmt"
"os"
"strings"
"strconv"
"strings"
)
func main() {
+24
View File
@@ -180,5 +180,29 @@
"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"
}
],
}
]
+23 -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,25 +74,28 @@
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
@@ -97,3 +103,14 @@
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
+66 -38
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.7"
version = "2.6.10"
)
var (
@@ -120,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)
}
}
@@ -185,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)
@@ -229,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)))
@@ -250,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
@@ -264,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
}
}
@@ -284,39 +292,43 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
if matchedHook.CaptureCommandOutputOnError {
fmt.Fprintf(w, response)
fmt.Fprint(w, response)
} else {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, "Error occurred while executing the hook's command. Please check your logs for more details.")
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.")
}
}
@@ -325,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)
@@ -341,40 +360,37 @@ 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)
if errors != nil {
for _, err := range errors {
log.Printf("[%s] error extracting command arguments for file: %s\n", rid, err)
}
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
@@ -394,10 +410,12 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in
}
for i := range files {
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)
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)
}
}
}
@@ -406,6 +424,16 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in
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{}
@@ -431,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
+154 -128
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, "test", &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/echo", "/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, "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: .*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, "test", &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,20 +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, ``},
{"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.`},
`, ``},
{"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()
}