mirror of
https://github.com/adnanh/webhook.git
synced 2026-07-27 01:29:16 +08:00
Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 34ae132930 | |||
| f993aaa11c | |||
| d82e838554 | |||
| 9c35aa070c | |||
| 14ee68a06e | |||
| e0e1dd8ade | |||
| 9852f0f0a5 | |||
| 2cf3f4e1a8 | |||
| 1cf531b1c3 | |||
| 74e55e3089 | |||
| 6c77ff0a2c | |||
| 002c332b68 | |||
| ffba396523 | |||
| b016e99ea6 | |||
| e47f9afb11 | |||
| d3fd9bddd9 | |||
| 42b72b028b | |||
| c6939d57dc | |||
| 90f751a61d | |||
| e86c2cf610 | |||
| 0aa7395e21 | |||
| 3f834f5c3d | |||
| 5bca86cdb2 | |||
| 896d1608ca | |||
| e5c18aa87e | |||
| f056f94305 | |||
| 1a17dc83fe | |||
| 753734428f | |||
| f76426e9b0 | |||
| 5803d5e849 | |||
| 385898b21f | |||
| ce7f8d5d28 | |||
| b2899d1d3e | |||
| e8628cd662 | |||
| 4e1719d966 | |||
| 98f86cf044 | |||
| fc0544e4a2 | |||
| 537f5c21bc | |||
| a0880ab82d | |||
| 5636ead921 | |||
| 01e0c9e972 | |||
| 54a7190113 | |||
| b65bdbbb24 | |||
| ef3f43f89f | |||
| 22073d8847 | |||
| c05ca8c528 | |||
| f9e799fea0 | |||
| 9b99452b60 | |||
| b93cdc346e | |||
| d59f6228ad | |||
| 78c8c61bf2 | |||
| 7ed5d4af9b | |||
| 7905c74687 | |||
| 2fb08ab579 | |||
| 681e8b6459 | |||
| 8a3770db29 | |||
| ae54669c02 | |||
| b449793825 | |||
| 66a9e48e39 | |||
| d85ee5e068 | |||
| 7da4d8ba9f | |||
| 8d260c6a7e | |||
| 48061f1508 | |||
| cfed5cfe4b | |||
| 471c849c50 | |||
| 337621998e | |||
| 0feeb945fc | |||
| 4f9ed434c5 | |||
| 0934b9414c | |||
| 356870358d | |||
| 6dc331726d | |||
| 3f8dbf09dc | |||
| dcda096b5d | |||
| 7079128eca | |||
| 6e3ec89ce1 | |||
| 10396a5434 | |||
| d009919755 | |||
| a811db410b | |||
| 357c471667 | |||
| 85889fe378 | |||
| b595694658 | |||
| f84edae99d |
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
open_collective: webhook
|
||||
+26
@@ -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 ./...
|
||||
@@ -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 ./...
|
||||
|
||||
|
||||
@@ -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
|
||||
[](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.
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -5,8 +5,8 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user