mirror of
https://github.com/adnanh/webhook.git
synced 2026-07-27 09:39:15 +08:00
Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fe6c9a05d | |||
| 7c4e6e94fc | |||
| 31e76bcd00 | |||
| c47c06e822 | |||
| bf3d042da6 | |||
| d05911cdcb | |||
| 634ca84807 | |||
| 8c46a8343b | |||
| 13d5630e80 | |||
| f1003560f1 | |||
| 997db04b9f | |||
| 769e743563 | |||
| 43f519a712 | |||
| a617b1a6ac | |||
| 9117f4f6d6 | |||
| b53996f175 | |||
| 154177e46a | |||
| d4e98281d7 | |||
| ce186487f4 | |||
| 1110f82443 | |||
| a99abd4e6f | |||
| 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 | |||
| ecbba514e5 | |||
| 13555e1ef1 | |||
| ddec431ae9 | |||
| f84edae99d | |||
| 514388e39e | |||
| a49364ae93 | |||
| 6c4e52d87e | |||
| b161de13f2 | |||
| e2f6e4eb37 | |||
| 0d3d29055b | |||
| a40fba5e29 | |||
| 137ded093d | |||
| 24ec77cb6a | |||
| 7f3529e302 | |||
| df781925f1 | |||
| 44d19e34a0 | |||
| b1e0c0bf01 | |||
| bab32db017 | |||
| 5a69eab2b3 | |||
| 01aa178bb4 | |||
| f5f04ddaa2 | |||
| ba0adb117a | |||
| c107bb4539 | |||
| 36c5a52539 | |||
| 027f2d7363 | |||
| c9abc252e8 | |||
| 6145634670 | |||
| 41b75acc70 | |||
| 83e688efd3 | |||
| b1851784b5 | |||
| 3c0508a8d3 | |||
| 7a91168f23 | |||
| 2adf724295 | |||
| 2f7eb31e8d | |||
| 717ad779a3 | |||
| 1e67afc3de | |||
| c094740905 | |||
| de494d4cc6 | |||
| 213e4529e8 | |||
| 78aa85e0c1 | |||
| 395fb41d23 | |||
| 34c4b1c166 |
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
open_collective: webhook
|
||||
@@ -2,3 +2,4 @@
|
||||
.cover
|
||||
coverage
|
||||
webhook
|
||||
/test/hookecho
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.12.x
|
||||
- 1.13.x
|
||||
- master
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
- windows
|
||||
|
||||
arch:
|
||||
- amd64
|
||||
- arm64
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- go: master
|
||||
exclude:
|
||||
- os: windows
|
||||
go: master
|
||||
- os: windows
|
||||
arch: arm64
|
||||
- os: osx
|
||||
arch: arm64
|
||||
|
||||
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 ./...
|
||||
|
||||
|
||||
@@ -1,29 +1,14 @@
|
||||
[](https://ghit.me/repo/adnanh/webhook) [](https://gitter.im/adnanh/webhook?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
# Hookdoo
|
||||
<a href="https://www.hookdoo.com/?github"><img src="https://my.hookdoo.com/logo/logo-dark-96.png" alt="hookdoo" align="left" /></a>
|
||||
|
||||
If you don't have time to waste configuring, hosting, debugging and maintaining your webhook instance, we offer a __SaaS__ solution that has all of the capabilities webhook provides, plus a lot more, and all that packaged in a nice friendly web interface. If you are interested, find out more at [hookdoo website](https://www.hookdoo.com/?ref=github-webhook-readme). If you have any questions, you can contact us at info@hookdoo.com
|
||||
|
||||
# Ensure active development
|
||||
[](#backers) [](#sponsors)
|
||||
|
||||
[](https://flattr.com/submit/auto?user_id=adnanh&url=https%3A%2F%2Fwww.github.com%2Fadnanh%2Fwebhook)
|
||||
|
||||
[Donate via PayPal](https://paypal.me/hookdoo)
|
||||
|
||||
[Patreon page](https://www.patreon.com/webhook)
|
||||
|
||||
[](https://faircode.io/product/webhook?utm_source=badge&utm_medium=badgelarge&utm_campaign=webhook)
|
||||
|
||||
# What is webhook?
|
||||
[webhook](https://github.com/adnanh/webhook/) is a lightweight configurable tool written in Go, that allows you to easily create HTTP endpoints (hooks) on your server, which you can use to execute configured commands. You can also pass data from the HTTP request (such as headers, payload or query variables) to your commands. [webhook](https://github.com/adnanh/webhook/) also allows you to specify rules which have to be satisfied in order for the hook to be triggered.
|
||||
|
||||
For example, if you're using Github or Bitbucket, you can use [webhook](https://github.com/adnanh/webhook/) to set up a hook that runs a redeploy script for your project on your staging server, whenever you push changes to the master branch of your project.
|
||||
<img src="https://github.com/adnanh/webhook/raw/development/docs/logo/logo-128x128.png" alt="Webhook" align="left" />
|
||||
|
||||
[webhook][w] is a lightweight configurable tool written in Go, that allows you to easily create HTTP endpoints (hooks) on your server, which you can use to execute configured commands. You can also pass data from the HTTP request (such as headers, payload or query variables) to your commands. [webhook][w] also allows you to specify rules which have to be satisfied in order for the hook to be triggered.
|
||||
|
||||
For example, if you're using Github or Bitbucket, you can use [webhook][w] to set up a hook that runs a redeploy script for your project on your staging server, whenever you push changes to the master branch of your project.
|
||||
|
||||
If you use Mattermost or Slack, you can set up an "Outgoing webhook integration" or "Slash command" to run various commands on your server, which can then report back directly to you or your channels using the "Incoming webhook integrations", or the appropriate response body.
|
||||
|
||||
[webhook](https://github.com/adnanh/webhook/) aims to do nothing more than it should do, and that is:
|
||||
[webhook][w] aims to do nothing more than it should do, and that is:
|
||||
1. receive the request,
|
||||
2. parse the headers, payload and query variables,
|
||||
3. check if the specified rules for the hook are satisfied,
|
||||
@@ -32,28 +17,38 @@ If you use Mattermost or Slack, you can set up an "Outgoing webhook integration"
|
||||
|
||||
Everything else is the responsibility of the command's author.
|
||||
|
||||
---
|
||||
# Hookdoo
|
||||
<a href="https://www.hookdoo.com/?github"><img src="https://www.hookdoo.com/logo/logo.svg" height="96" alt="hookdoo" align="left" /></a>
|
||||
|
||||
If you don't have time to waste configuring, hosting, debugging and maintaining your webhook instance, we offer a __SaaS__ solution that has all of the capabilities webhook provides, plus a lot more, and all that packaged in a nice friendly web interface. If you are interested, find out more at [hookdoo website](https://www.hookdoo.com/?ref=github-webhook-readme). If you have any questions, you can contact us at info@hookdoo.com
|
||||
|
||||
|
||||
# Getting started
|
||||
## Installation
|
||||
### Building from source
|
||||
To get started, first make sure you've properly set up your [Golang](http://golang.org/doc/install) environment and then run the
|
||||
To get started, first make sure you've properly set up your [Go](http://golang.org/doc/install) 1.12 or newer environment and then run
|
||||
```bash
|
||||
$ go get github.com/adnanh/webhook
|
||||
```
|
||||
to get the latest version of the [webhook](https://github.com/adnanh/webhook/).
|
||||
to get the latest version of the [webhook][w].
|
||||
|
||||
### Using package manager
|
||||
#### Snap store
|
||||
[](https://snapcraft.io/webhook)
|
||||
|
||||
#### Ubuntu
|
||||
If you are using Ubuntu linux (17.04 or later), you can install webhook using `sudo apt-get install webhook` which will install community packaged version.
|
||||
|
||||
#### Debian
|
||||
If you are using Debian linux ("stretch" or later), you can install webhook using `apt-get install webhook` which will install community packaged version (thanks [@freeekanayaka](https://github.com/freeekanayaka)) from https://packages.debian.org/sid/webhook
|
||||
If you are using Debian linux ("stretch" or later), you can install webhook using `sudo apt-get install webhook` which will install community packaged version (thanks [@freeekanayaka](https://github.com/freeekanayaka)) from https://packages.debian.org/sid/webhook
|
||||
|
||||
### Download prebuilt binaries
|
||||
Prebuilt binaries for different architectures are available at [GitHub Releases](https://github.com/adnanh/webhook/releases).
|
||||
|
||||
## Configuration
|
||||
Next step is to define some hooks you want [webhook](https://github.com/adnanh/webhook/) to serve. Begin by creating an empty file named `hooks.json`. This file will contain an array of hooks the [webhook](https://github.com/adnanh/webhook/) will serve. Check [Hook definition page](https://github.com/adnanh/webhook/wiki/Hook-Definition) to see the detailed description of what properties a hook can contain, and how to use them.
|
||||
Next step is to define some hooks you want [webhook][w] to serve. Begin by creating an empty file named `hooks.json`. This file will contain an array of hooks the [webhook][w] will serve. Check [Hook definition page](docs/Hook-Definition.md) to see the detailed description of what properties a hook can contain, and how to use them.
|
||||
|
||||
Let's define a simple hook named `redeploy-webhook` that will run a redeploy script located in `/var/scripts/redeploy.sh`.
|
||||
Let's define a simple hook named `redeploy-webhook` that will run a redeploy script located in `/var/scripts/redeploy.sh`. Make sure that your bash script has `#!/bin/sh` shebang on top.
|
||||
|
||||
Our `hooks.json` file will now look like this:
|
||||
```json
|
||||
@@ -66,7 +61,7 @@ Our `hooks.json` file will now look like this:
|
||||
]
|
||||
```
|
||||
|
||||
You can now run [webhook](https://github.com/adnanh/webhook/) using
|
||||
You can now run [webhook][w] using
|
||||
```bash
|
||||
$ /path/to/webhook -hooks hooks.json -verbose
|
||||
```
|
||||
@@ -76,56 +71,66 @@ It will start up on default port 9000 and will provide you with one HTTP endpoin
|
||||
http://yourserver:9000/hooks/redeploy-webhook
|
||||
```
|
||||
|
||||
Check [webhook parameters page](https://github.com/adnanh/webhook/wiki/Webhook-Parameters) to see how to override the ip, port and other settings such as hook hotreload, verbose output, etc, when starting the [webhook](https://github.com/adnanh/webhook/).
|
||||
Check [webhook parameters page](docs/Webhook-Parameters.md) to see how to override the ip, port and other settings such as hook hotreload, verbose output, etc, when starting the [webhook][w].
|
||||
|
||||
By performing a simple HTTP GET or POST request to that endpoint, your specified redeploy script would be executed. Neat!
|
||||
|
||||
However, hook defined like that could pose a security threat to your system, because anyone who knows your endpoint, can send a request and execute your command. To prevent that, you can use the `"trigger-rule"` property for your hook, to specify the exact circumstances under which the hook would be triggered. For example, you can use them to add a secret that you must supply as a parameter in order to successfully trigger the hook. Please check out the [Hook rules page](https://github.com/adnanh/webhook/wiki/Hook-Rules) for detailed list of available rules and their usage.
|
||||
However, hook defined like that could pose a security threat to your system, because anyone who knows your endpoint, can send a request and execute your command. To prevent that, you can use the `"trigger-rule"` property for your hook, to specify the exact circumstances under which the hook would be triggered. For example, you can use them to add a secret that you must supply as a parameter in order to successfully trigger the hook. Please check out the [Hook rules page](docs/Hook-Rules.md) for detailed list of available rules and their usage.
|
||||
|
||||
# Using HTTPS
|
||||
[webhook](https://github.com/adnanh/webhook/) by default serves hooks using http. If you want [webhook](https://github.com/adnanh/webhook/) to serve secure content using https, you can use the `-secure` flag while starting [webhook](https://github.com/adnanh/webhook/). Files containing a certificate and matching private key for the server must be provided using the `-cert /path/to/cert.pem` and `-key /path/to/key.pem` flags. If the certificate is signed by a certificate authority, the cert file should be the concatenation of the server's certificate followed by the CA's certificate.
|
||||
## Templates
|
||||
[webhook][w] can parse the `hooks.json` input file as a Go template when given the `-template` [CLI parameter](docs/Webhook-Parameters.md). See the [Templates page](docs/Templates.md) for more details on template usage.
|
||||
|
||||
# CORS Headers
|
||||
If you want to set CORS headers, you can use the `-header name=value` flag while starting [webhook](https://github.com/adnanh/webhook/) to set the appropriate CORS headers that will be returned with each response.
|
||||
## Using HTTPS
|
||||
[webhook][w] by default serves hooks using http. If you want [webhook][w] to serve secure content using https, you can use the `-secure` flag while starting [webhook][w]. Files containing a certificate and matching private key for the server must be provided using the `-cert /path/to/cert.pem` and `-key /path/to/key.pem` flags. If the certificate is signed by a certificate authority, the cert file should be the concatenation of the server's certificate followed by the CA's certificate.
|
||||
|
||||
# Interested in running webhook inside of a Docker container?
|
||||
TLS version and cipher suite selection flags are available from the command line. To list available cipher suites, use the `-list-cipher-suites` flag. The `-tls-min-version` flag can be used with `-list-cipher-suites`.
|
||||
|
||||
## CORS Headers
|
||||
If you want to set CORS headers, you can use the `-header name=value` flag while starting [webhook][w] to set the appropriate CORS headers that will be returned with each response.
|
||||
|
||||
## Interested in running webhook inside of a Docker container?
|
||||
You can use [almir/webhook](https://hub.docker.com/r/almir/webhook/) docker image, or create your own (please read [this discussion](https://github.com/adnanh/webhook/issues/63)).
|
||||
|
||||
# Examples
|
||||
Check out [Hook examples page](https://github.com/adnanh/webhook/wiki/Hook-Examples) for more complex examples of hooks.
|
||||
## Examples
|
||||
Check out [Hook examples page](docs/Hook-Examples.md) for more complex examples of hooks.
|
||||
|
||||
# Contributing
|
||||
Any form of contribution is welcome and highly appreciated.
|
||||
|
||||
Big thanks to [all the current contributors](https://github.com/adnanh/webhook/graphs/contributors) for their contributions!
|
||||
|
||||
# Community Contributions
|
||||
### Guides featuring webhook
|
||||
- [Webhook & JIRA](https://sites.google.com/site/mrxpalmeiras/notes/jira-webhooks) by [@perfecto25](https://github.com/perfecto25)
|
||||
- [Trigger Ansible AWX job runs on SCM (e.g. git) commit](http://jpmens.net/2017/10/23/trigger-awx-job-runs-on-scm-commit/) by [@jpmens](http://mens.de/)
|
||||
- [Deploy using GitHub webhooks](https://davidauthier.wearemd.com/blog/deploy-using-github-webhooks.html) by [@awea](https://davidauthier.wearemd.com)
|
||||
- [Setting up Automatic Deployment and Builds Using Webhooks](https://willbrowning.me/setting-up-automatic-deployment-and-builds-using-webhooks/) by [Will Browning](https://willbrowning.me/about/)
|
||||
- [Auto deploy your Node.js app on push to GitHub in 3 simple steps](https://webhookrelay.com/blog/2018/07/17/auto-deploy-on-git-push/) by Karolis Rusenas
|
||||
- [Automate Static Site Deployments with Salt, Git, and Webhooks](https://www.linode.com/docs/applications/configuration-management/automate-a-static-site-deployment-with-salt/) by [Linode](https://www.linode.com)
|
||||
- [Using Prometheus to Automatically Scale WebLogic Clusters on Kubernetes](https://blogs.oracle.com/weblogicserver/using-prometheus-to-automatically-scale-weblogic-clusters-on-kubernetes-v5) by [Marina Kogan](https://blogs.oracle.com/author/9a4fe754-1cc2-4c64-95fc-360642b62927)
|
||||
- [Github Pages and Jekyll - A New Platform for LACNIC Labs](https://labs.lacnic.net/a-new-platform-for-lacniclabs/) by [Carlos Martínez Cagnazzo](https://twitter.com/carlosm3011)
|
||||
- [How to Deploy React Apps Using Webhooks and Integrating Slack on Ubuntu](https://www.alibabacloud.com/blog/how-to-deploy-react-apps-using-webhooks-and-integrating-slack-on-ubuntu_594116) by Arslan Ud Din Shafiq
|
||||
- [Private webhooks](https://ihateithe.re/2018/01/private-webhooks/) by [Thomas](https://ihateithe.re/colophon/)
|
||||
- [Adventures in webhooks](https://medium.com/@draketech/adventures-in-webhooks-2d6584501c62) by [Drake](https://medium.com/@draketech)
|
||||
- [GitHub pro tips](http://notes.spencerlyon.com/2016/01/04/github-pro-tips/) by [Spencer Lyon](http://notes.spencerlyon.com/)
|
||||
- [XiaoMi Vacuum + Amazon Button = Dash Cleaning](https://www.instructables.com/id/XiaoMi-Vacuum-Amazon-Button-Dash-Cleaning/) by [c0mmensal](https://www.instructables.com/member/c0mmensal/)
|
||||
- VIDEO: [Gitlab CI/CD configuration using Docker and adnanh/webhook to deploy on VPS - Tutorial #1](https://www.youtube.com/watch?v=Qhn-lXjyrZA&feature=youtu.be) by [Yes! Let's Learn Software Engineering](https://www.youtube.com/channel/UCH4XJf2BZ_52fbf8fOBMF3w)
|
||||
- ...
|
||||
- Want to add your own? Open an Issue or create a PR :-)
|
||||
|
||||
## Community Contributions
|
||||
See the [webhook-contrib][wc] repository for a collections of tools and helpers related to [webhook][w] that have been contributed by the [webhook][w] community.
|
||||
|
||||
# Sponsors
|
||||
## Need help?
|
||||
Check out [existing issues](https://github.com/adnanh/webhook/issues) to see if someone else also had the same problem, or [open a new one](https://github.com/adnanh/webhook/issues/new).
|
||||
|
||||
# Support active development
|
||||
|
||||
## Sponsors
|
||||
## <a href="https://www.digitalocean.com/?ref=webhook"><img src="http://www.hajdarevic.net/DO_Logo_Horizontal_Blue.png" alt="DigitalOcean" width="250"/></a>
|
||||
[DigitalOcean](https://www.digitalocean.com/?ref=webhook) is a simple and robust cloud computing platform, designed for developers.
|
||||
|
||||
|
||||
## <a href="https://www.browserstack.com/?ref=webhook"><img src="http://www.hajdarevic.net/browserstack.svg" alt="BrowserStack" width="250"/></a>
|
||||
[BrowserStack](https://www.browserstack.com/?ref=webhook) is a cloud-based cross-browser testing tool that enables developers to test their websites across various browsers on different operating systems and mobile devices, without requiring users to install virtual machines, devices or emulators.
|
||||
|
||||
<a target='_blank' rel='nofollow' href='https://app.codesponsor.io/link/2AxDfLX6qJBgxEXxcCPGjJyD/adnanh/webhook'>
|
||||
<img alt='Sponsor' width='888' height='68' src='https://app.codesponsor.io/embed/2AxDfLX6qJBgxEXxcCPGjJyD/adnanh/webhook.svg' />
|
||||
</a>
|
||||
---
|
||||
|
||||
## Contributors
|
||||
|
||||
This project exists thanks to all the people who contribute. [[Contribute]](CONTRIBUTING.md).
|
||||
<a href="graphs/contributors"><img src="https://opencollective.com/webhook/contributors.svg?width=890" /></a>
|
||||
|
||||
|
||||
## Backers
|
||||
|
||||
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/webhook#backer)]
|
||||
|
||||
<a href="https://opencollective.com/webhook#backers" target="_blank"><img src="https://opencollective.com/webhook/backers.svg?width=890"></a>
|
||||
|
||||
|
||||
## Sponsors
|
||||
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/webhook#sponsor)]
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website.
|
||||
|
||||
<a href="https://opencollective.com/webhook/sponsor/0/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/1/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/1/avatar.svg"></a>
|
||||
@@ -138,7 +143,25 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
|
||||
<a href="https://opencollective.com/webhook/sponsor/8/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/webhook/sponsor/9/website" target="_blank"><img src="https://opencollective.com/webhook/sponsor/9/avatar.svg"></a>
|
||||
|
||||
## By contributing
|
||||
|
||||
This project exists thanks to all the people who contribute. [Contribute!](CONTRIBUTING.md).
|
||||
<a href="graphs/contributors"><img src="https://opencollective.com/webhook/contributors.svg?width=890" /></a>
|
||||
|
||||
## By giving money
|
||||
|
||||
- [OpenCollective Backer](https://opencollective.com/webhook#backer)
|
||||
- [OpenCollective Sponsor](https://opencollective.com/webhook#sponsor)
|
||||
- [PayPal](https://paypal.me/hookdoo)
|
||||
- [Patreon](https://www.patreon.com/webhook)
|
||||
- [Faircode](https://faircode.io/product/webhook?utm_source=badge&utm_medium=badgelarge&utm_campaign=webhook)
|
||||
- [Flattr](https://flattr.com/submit/auto?user_id=adnanh&url=https%3A%2F%2Fwww.github.com%2Fadnanh%2Fwebhook)
|
||||
|
||||
---
|
||||
|
||||
Thank you to all our backers!
|
||||
|
||||
<a href="https://opencollective.com/webhook#backers" target="_blank"><img src="https://opencollective.com/webhook/backers.svg?width=890"></a>
|
||||
|
||||
# License
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Copied from Go 1.14 tip src/crypto/tls/cipher_suites.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// CipherSuite is a TLS cipher suite. Note that most functions in this package
|
||||
// accept and expose cipher suite IDs instead of this type.
|
||||
type CipherSuite struct {
|
||||
ID uint16
|
||||
Name string
|
||||
|
||||
// Supported versions is the list of TLS protocol versions that can
|
||||
// negotiate this cipher suite.
|
||||
SupportedVersions []uint16
|
||||
|
||||
// Insecure is true if the cipher suite has known security issues
|
||||
// due to its primitives, design, or implementation.
|
||||
Insecure bool
|
||||
}
|
||||
|
||||
var (
|
||||
supportedUpToTLS12 = []uint16{tls.VersionTLS10, tls.VersionTLS11, tls.VersionTLS12}
|
||||
supportedOnlyTLS12 = []uint16{tls.VersionTLS12}
|
||||
supportedOnlyTLS13 = []uint16{tls.VersionTLS13}
|
||||
)
|
||||
|
||||
// CipherSuites returns a list of cipher suites currently implemented by this
|
||||
// package, excluding those with security issues, which are returned by
|
||||
// InsecureCipherSuites.
|
||||
//
|
||||
// The list is sorted by ID. Note that the default cipher suites selected by
|
||||
// this package might depend on logic that can't be captured by a static list.
|
||||
func CipherSuites() []*CipherSuite {
|
||||
return []*CipherSuite{
|
||||
{tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, "TLS_RSA_WITH_3DES_EDE_CBC_SHA", supportedUpToTLS12, false},
|
||||
{tls.TLS_RSA_WITH_AES_128_CBC_SHA, "TLS_RSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
|
||||
{tls.TLS_RSA_WITH_AES_256_CBC_SHA, "TLS_RSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
|
||||
{tls.TLS_RSA_WITH_AES_128_GCM_SHA256, "TLS_RSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
|
||||
{tls.TLS_RSA_WITH_AES_256_GCM_SHA384, "TLS_RSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
|
||||
|
||||
{tls.TLS_AES_128_GCM_SHA256, "TLS_AES_128_GCM_SHA256", supportedOnlyTLS13, false},
|
||||
{tls.TLS_AES_256_GCM_SHA384, "TLS_AES_256_GCM_SHA384", supportedOnlyTLS13, false},
|
||||
{tls.TLS_CHACHA20_POLY1305_SHA256, "TLS_CHACHA20_POLY1305_SHA256", supportedOnlyTLS13, false},
|
||||
|
||||
{tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
|
||||
{tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
|
||||
{tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", supportedUpToTLS12, false},
|
||||
{tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
|
||||
{tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
|
||||
{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
|
||||
{tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
|
||||
{tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
|
||||
{tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
|
||||
|
||||
// go1.14
|
||||
// {tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", supportedOnlyTLS12, false},
|
||||
// {tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", supportedOnlyTLS12, false},
|
||||
}
|
||||
}
|
||||
|
||||
// InsecureCipherSuites returns a list of cipher suites currently implemented by
|
||||
// this package and which have security issues.
|
||||
//
|
||||
// Most applications should not use the cipher suites in this list, and should
|
||||
// only use those returned by CipherSuites.
|
||||
func InsecureCipherSuites() []*CipherSuite {
|
||||
// RC4 suites are broken because RC4 is.
|
||||
// CBC-SHA256 suites have no Lucky13 countermeasures.
|
||||
return []*CipherSuite{
|
||||
{tls.TLS_RSA_WITH_RC4_128_SHA, "TLS_RSA_WITH_RC4_128_SHA", supportedUpToTLS12, true},
|
||||
{tls.TLS_RSA_WITH_AES_128_CBC_SHA256, "TLS_RSA_WITH_AES_128_CBC_SHA256", supportedOnlyTLS12, true},
|
||||
{tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", supportedUpToTLS12, true},
|
||||
{tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, "TLS_ECDHE_RSA_WITH_RC4_128_SHA", supportedUpToTLS12, true},
|
||||
{tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", supportedOnlyTLS12, true},
|
||||
{tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", supportedOnlyTLS12, true},
|
||||
}
|
||||
}
|
||||
|
||||
// CipherSuiteName returns the standard name for the passed cipher suite ID
|
||||
// (e.g. "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"), or a fallback representation
|
||||
// of the ID value if the cipher suite is not implemented by this package.
|
||||
func CipherSuiteName(id uint16) string {
|
||||
for _, c := range CipherSuites() {
|
||||
if c.ID == id {
|
||||
return c.Name
|
||||
}
|
||||
}
|
||||
for _, c := range InsecureCipherSuites() {
|
||||
if c.ID == id {
|
||||
return c.Name
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("0x%04X", id)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# Hook definition
|
||||
Hooks are defined as JSON objects. Please note that in order to be considered valid, a hook object must contain the `id` and `execute-command` properties. All other properties are considered optional.
|
||||
|
||||
## Properties (keys)
|
||||
|
||||
* `id` - specifies the ID of your hook. This value is used to create the HTTP endpoint (http://yourserver:port/hooks/your-hook-id)
|
||||
* `execute-command` - specifies the command that should be executed when the hook is triggered
|
||||
* `command-working-directory` - specifies the working directory that will be used for the script when it's executed
|
||||
* `response-message` - specifies the string that will be returned to the hook initiator
|
||||
* `response-headers` - specifies the list of headers in format `{"name": "X-Example-Header", "value": "it works"}` that will be returned in HTTP response for the hook
|
||||
* `success-http-response-code` - specifies the HTTP status code to be returned upon success
|
||||
* `incoming-payload-content-type` - sets the `Content-Type` of the incoming HTTP request (ie. `application/json`); useful when the request lacks a `Content-Type` or sends an erroneous value
|
||||
* `include-command-output-in-response` - boolean whether webhook should wait for the command to finish and return the raw output as a response to the hook initiator. If the command fails to execute or encounters any errors while executing the response will result in 500 Internal Server Error HTTP status code, otherwise the 200 OK status code will be returned.
|
||||
* `include-command-output-in-response-on-error` - boolean whether webhook should include command stdout & stderror as a response in failed executions. It only works if `include-command-output-in-response` is set to `true`.
|
||||
* `parse-parameters-as-json` - specifies the list of arguments that contain JSON strings. These parameters will be decoded by webhook and you can access them like regular objects in rules and `pass-arguments-to-command`.
|
||||
* `pass-arguments-to-command` - specifies the list of arguments that will be passed to the command. Check [Referencing request values page](Referencing-Request-Values.md) to see how to reference the values from the request. If you want to pass a static string value to your command you can specify it as
|
||||
`{ "source": "string", "name": "argumentvalue" }`
|
||||
* `pass-environment-to-command` - specifies the list of arguments that will be passed to the command as environment variables. If you do not specify the `"envname"` field in the referenced value, the hook will be in format "HOOK_argumentname", otherwise "envname" field will be used as it's name. Check [Referencing request values page](Referencing-Request-Values.md) to see how to reference the values from the request. If you want to pass a static string value to your command you can specify it as
|
||||
`{ "source": "string", "envname": "SOMETHING", "name": "argumentvalue" }`
|
||||
* `pass-file-to-command` - specifies a list of entries that will be serialized as a file. Incoming [data](Referencing-Request-Values.md) will be serialized in a request-temporary-file (otherwise parallel calls of the hook would lead to concurrent overwritings of the file). The filename to be addressed within the subsequent script is provided via an environment variable. Use `envname` to specify the name of the environment variable. If `envname` is not provided `HOOK_` and the name used to reference the request value are used. Defining `command-working-directory` will store the file relative to this location, if not provided, the systems temporary file directory will be used. If `base64decode` is true, the incoming binary data will be base 64 decoded prior to storing it into the file. By default the corresponding file will be removed after the webhook exited.
|
||||
* `trigger-rule` - specifies the rule that will be evaluated in order to determine should the hook be triggered. Check [Hook rules page](Hook-Rules.md) to see the list of valid rules and their usage
|
||||
* `trigger-rule-mismatch-http-response-code` - specifies the HTTP status code to be returned when the trigger rule is not satisfied
|
||||
|
||||
## Examples
|
||||
Check out [Hook examples page](Hook-Examples.md) for more complex examples of hooks.
|
||||
@@ -0,0 +1,426 @@
|
||||
# Hook examples
|
||||
This page is still work in progress. Feel free to contribute!
|
||||
|
||||
## Incoming Github webhook
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.name"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha1",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Incoming Bitbucket webhook
|
||||
|
||||
Bitbucket does not pass any secrets back to the webhook. [Per their documentation](https://confluence.atlassian.com/bitbucket/manage-webhooks-735643732.html#Managewebhooks-trigger_webhookTriggeringwebhooks), in order to verify that the webhook came from Bitbucket you must whitelist the IP range `104.192.143.0/24`:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "actor.username"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "ip-whitelist",
|
||||
"ip-range": "104.192.143.0/24"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Incoming Gitlab Webhook
|
||||
Gitlab provides webhooks for many kinds of events.
|
||||
Refer to this URL for example request body content: [gitlab-ce/integrations/webhooks](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/project/integrations/webhooks.md)
|
||||
Values in the request body can be accessed in the command or to the match rule by referencing 'payload' as the source:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "redeploy-webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "user_name"
|
||||
}
|
||||
],
|
||||
"response-message": "Executing redeploy script",
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "<YOUR-GENERATED-TOKEN>",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Gitlab-Token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Incoming Gogs webhook
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.name"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha256",
|
||||
"secret": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Gogs-Signature"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
## Incoming Gitea webhook
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.name"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "mysecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "secret"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Slack slash command
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "redeploy-webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"response-message": "Executing redeploy script",
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "<YOUR-GENERATED-TOKEN>",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## A simple webhook with a secret key in GET query
|
||||
|
||||
__Not recommended in production due to low security__
|
||||
|
||||
`example.com:9000/hooks/simple-one` - won't work
|
||||
`example.com:9000/hooks/simple-one?token=42` - will work
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "simple-one",
|
||||
"execute-command": "/path/to/command.sh",
|
||||
"response-message": "Executing simple webhook...",
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "42",
|
||||
"parameter":
|
||||
{
|
||||
"source": "url",
|
||||
"name": "token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
# JIRA Webhooks
|
||||
[Guide by @perfecto25](https://sites.google.com/site/mrxpalmeiras/notes/jira-webhooks)
|
||||
|
||||
# Pass File-to-command sample
|
||||
|
||||
## Webhook configuration
|
||||
|
||||
<pre>
|
||||
[
|
||||
{
|
||||
"id": "test-file-webhook",
|
||||
"execute-command": "/bin/ls",
|
||||
"command-working-directory": "/tmp",
|
||||
"pass-file-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "binary",
|
||||
"envname": "ENV_VARIABLE", // to use $ENV_VARIABLE in execute-command
|
||||
// if not defined, $HOOK_BINARY will be provided
|
||||
"base64decode": true, // defaults to false
|
||||
}
|
||||
],
|
||||
"include-command-output-in-response": true
|
||||
}
|
||||
]
|
||||
</pre>
|
||||
|
||||
## Sample client usage
|
||||
|
||||
Store the following file as `testRequest.json`.
|
||||
|
||||
<pre>
|
||||
{"binary":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2lpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wUmlnaHRzPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvcmlnaHRzLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcFJpZ2h0czpNYXJrZWQ9IkZhbHNlIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjEzMTA4RDI0QzMxQjExRTBCMzYzRjY1QUQ1Njc4QzFBIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjEzMTA4RDIzQzMxQjExRTBCMzYzRjY1QUQ1Njc4QzFBIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzMgV2luZG93cyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ1dWlkOkFDMUYyRTgzMzI0QURGMTFBQUI4QzUzOTBEODVCNUIzIiBzdFJlZjpkb2N1bWVudElEPSJ1dWlkOkM5RDM0OTY2NEEzQ0REMTFCMDhBQkJCQ0ZGMTcyMTU2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+IBFgEwAAAmJJREFUeNqkk89rE1EQx2d/NNq0xcYYayPYJDWC9ODBsKIgAREjBmvEg2cvHnr05KHQ9iB49SL+/BMEfxBQKHgwCEbTNNIYaqgaoanFJi+rcXezye4689jYkIMIDnx47837zrx583YFx3Hgf0xA6/dJyAkkgUy4vgryAnmNWH9L4EVmotFoKplMHgoGg6PkrFarjXQ6/bFcLj/G5W1E+3NaX4KZeDx+dX5+7kg4HBlmrC6JoiDFYrGhROLM/mp1Y6JSqdCd3/SW0GUqEAjkl5ZyHTSHKBQKnO6a9khD2m5cr91IJBJ1VVWdiM/n6LruNJtNDs3JR3ukIW03SHTHi8iVsbG9I51OG1bW16HVasHQZopDc/JZVgdIQ1o3BmTkEnJXURS/KIpgGAYPkCQJPi0u8uzDKQN0XQPbtgE1MmrHs9nsfSqAEjxCNtHxZHLy4G4smUQgyzL4LzOegDGGp1ucVqsNqKVrpJCM7F4hg6iaZvhqtZrg8XjA4xnAU3XeKLqWaRImoIZeQXVjQO5pYp4xNVirsR1erxer2O4yfa227WCwhtWoJmn7m0h270NxmemFW4706zMm8GCgxBGEASCfhnukIW03iFdQnOPz0LNKp3362JqQzSw4u2LXBe+Bs3xD+/oc1NxN55RiC9fOme0LEQiRf2rBzaKEeJJ37ZWTVunBeGN2WmQjg/DeLTVP89nzAive2dMwlo9bpFVC2xWMZr+A720FVn88fAUb3wDMOjyN7YNc6TvUSHQ4AH6TOUdLL7em68UtWPsJqxgTpgeiLu1EBt1R+Me/mF7CQPTfAgwAGxY2vOTrR3oAAAAASUVORK5CYII="}
|
||||
</pre>
|
||||
|
||||
use then the curl tool to execute a request to the webhook.
|
||||
|
||||
<pre>
|
||||
#!/bin/bash
|
||||
curl -H "Content-Type:application/json" -X POST -d @testRequest.json \
|
||||
http://localhost:9000/hooks/test-file-webhook
|
||||
</pre>
|
||||
|
||||
or in a single line, using https://github.com/jpmens/jo to generate the JSON code
|
||||
<pre>
|
||||
jo binary=%filename.zip | curl -H "Content-Type:application/json" -X POST -d @- \
|
||||
http://localhost:9000/hooks/test-file-webhook
|
||||
</pre>
|
||||
|
||||
|
||||
## Incoming Scalr Webhook
|
||||
[Guide by @hassanbabaie]
|
||||
Scalr makes webhook calls based on an event to a configured webhook endpoint (for example Host Down, Host Up). Webhook endpoints are URLs where Scalr will deliver Webhook notifications.
|
||||
Scalr assigns a unique signing key for every configured webhook endpoint.
|
||||
Refer to this URL for information on how to setup the webhook call on the Scalr side: [Scalr Wiki Webhooks](https://scalr-wiki.atlassian.net/wiki/spaces/docs/pages/6193173/Webhooks)
|
||||
In order to leverage the Signing Key for addtional authentication/security you must configure the trigger rule with a match type of "scalr-signature".
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "redeploy-webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"include-command-output-in-response": true,
|
||||
"trigger-rule":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "scalr-signature",
|
||||
"secret": "Scalr-provided signing key"
|
||||
}
|
||||
},
|
||||
"pass-environment-to-command":
|
||||
[
|
||||
{
|
||||
"envname": "EVENT_NAME",
|
||||
"source": "payload",
|
||||
"name": "eventName"
|
||||
},
|
||||
{
|
||||
"envname": "SERVER_HOSTNAME",
|
||||
"source": "payload",
|
||||
"name": "data.SCALR_SERVER_HOSTNAME"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
## Travis CI webhook
|
||||
Travis sends webhooks as `payload=<JSON_STRING>`, so the payload needs to be parsed as JSON. Here is an example to run on successful builds of the master branch.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "deploy",
|
||||
"execute-command": "/root/my-server/deployment.sh",
|
||||
"command-working-directory": "/root/my-server",
|
||||
"parse-parameters-as-json": [
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "payload"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "passed",
|
||||
"parameter": {
|
||||
"name": "payload.state",
|
||||
"source": "payload"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "master",
|
||||
"parameter": {
|
||||
"name": "payload.branch",
|
||||
"source": "payload"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -0,0 +1,249 @@
|
||||
# Hook rules
|
||||
|
||||
## And
|
||||
*And rule* will evaluate to _true_, if and only if all of the sub rules evaluate to _true_.
|
||||
```json
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "regex",
|
||||
"regex": ".*",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "repository.owner.name"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
## Or
|
||||
*Or rule* will evaluate to _true_, if any of the sub rules evaluate to _true_.
|
||||
```json
|
||||
{
|
||||
"or":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/development",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
## Not
|
||||
*Not rule* will evaluate to _true_, if and only if the sub rule evaluates to _false_.
|
||||
```json
|
||||
{
|
||||
"not":
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/development",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
## Multi-level
|
||||
```json
|
||||
{
|
||||
"and": [
|
||||
{
|
||||
"match": {
|
||||
"parameter": {
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
},
|
||||
"type": "payload-hash-sha1",
|
||||
"secret": "mysecret"
|
||||
}
|
||||
},
|
||||
{
|
||||
"or": [
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
},
|
||||
"type": "value",
|
||||
"value": "refs/heads/master"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-GitHub-Event"
|
||||
},
|
||||
"type": "value",
|
||||
"value": "ping"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
## Match
|
||||
*Match rule* will evaluate to _true_, if and only if the referenced value in the `parameter` field satisfies the `type`-specific rule.
|
||||
|
||||
*Please note:* Due to technical reasons, _number_ and _boolean_ values in the _match rule_ must be wrapped around with a pair of quotes.
|
||||
|
||||
There are three different match rules:
|
||||
|
||||
### 1. Match value
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/development",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Match regex
|
||||
For the regex syntax, check out <http://golang.org/pkg/regexp/syntax/>
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "regex",
|
||||
"regex": ".*",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Match payload-hash-sha1
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha1",
|
||||
"secret": "yoursecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Match payload-hash-sha256
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha256",
|
||||
"secret": "yoursecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Match payload-hash-sha512
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha512",
|
||||
"secret": "yoursecret",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Match Whitelisted IP range
|
||||
|
||||
The IP can be IPv4- or IPv6-formatted, using [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_blocks). To match a single IP address only, use `/32`.
|
||||
|
||||
```json
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "ip-whitelist",
|
||||
"ip-range": "192.168.0.1/24"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. 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"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,90 @@
|
||||
# Referencing request values
|
||||
There are three types of request values:
|
||||
|
||||
1. HTTP Request Header values
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "header",
|
||||
"name": "Header-Name"
|
||||
}
|
||||
```
|
||||
|
||||
2. HTTP Query parameters
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "url",
|
||||
"name": "parameter-name"
|
||||
}
|
||||
```
|
||||
|
||||
3. Payload (JSON or form-value encoded)
|
||||
```json
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "parameter-name"
|
||||
}
|
||||
```
|
||||
|
||||
*Note:* For JSON encoded payload, you can reference nested values using the dot-notation.
|
||||
For example, if you have following JSON payload
|
||||
|
||||
```json
|
||||
{
|
||||
"commits": [
|
||||
{
|
||||
"commit": {
|
||||
"id": 1
|
||||
}
|
||||
}, {
|
||||
"commit": {
|
||||
"id": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You can reference the first commit id as
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "commits.0.commit.id"
|
||||
}
|
||||
```
|
||||
|
||||
If the payload contains a key with the specified name "commits.0.commit.id", then the value of that key has priority over the dot-notation referencing.
|
||||
|
||||
If you are referencing values for environment, you can use `envname` property to set the name of the environment variable like so
|
||||
```json
|
||||
{
|
||||
"source": "url",
|
||||
"name": "q",
|
||||
"envname": "QUERY"
|
||||
}
|
||||
```
|
||||
to get the QUERY environment variable set to the `q` parameter passed in the query string.
|
||||
|
||||
# Special cases
|
||||
If you want to pass the entire payload as JSON string to your command you can use
|
||||
```json
|
||||
{
|
||||
"source": "entire-payload"
|
||||
}
|
||||
```
|
||||
|
||||
for headers you can use
|
||||
```json
|
||||
{
|
||||
"source": "entire-headers"
|
||||
}
|
||||
```
|
||||
|
||||
and for query variables you can use
|
||||
```json
|
||||
{
|
||||
"source": "entire-query"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
# Templates in Webhook
|
||||
|
||||
[`webhook`][w] can parse the `hooks.json` input file as a Go template when given the `-template` [CLI parameter](Webhook-Parameters.md).
|
||||
|
||||
In additional to the [built-in Go template functions and features][tt], `webhook` provides a `getenv` template function for inserting environment variables into a `hooks.json` file.
|
||||
|
||||
## Example Usage
|
||||
|
||||
In the example `hooks.json` file below, the `payload-hash-sha1` matching rule looks up the secret hash from the environment using the `getenv` template function.
|
||||
Additionally, the result is piped through the built-in Go template function `js` to ensure that the result is a well-formed Javascript/JSON string.
|
||||
|
||||
```
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"response-message": "I got the payload!",
|
||||
"response-headers":
|
||||
[
|
||||
{
|
||||
"name": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
],
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.name"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha1",
|
||||
"secret": "{{ getenv "XXXTEST_SECRET" | js }}",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
[w]: https://github.com/adnanh/webhook
|
||||
[tt]: https://golang.org/pkg/text/template/
|
||||
@@ -0,0 +1,44 @@
|
||||
# Webhook parameters
|
||||
```
|
||||
Usage of webhook:
|
||||
-cert string
|
||||
path to the HTTPS certificate pem file (default "cert.pem")
|
||||
-cipher-suites string
|
||||
comma-separated list of supported TLS cipher suites
|
||||
-header value
|
||||
response header to return, specified in format name=value, use multiple times to set multiple headers
|
||||
-hooks value
|
||||
path to the json file containing defined hooks the webhook should serve, use multiple times to load from different files
|
||||
-hotreload
|
||||
watch hooks file for changes and reload them automatically
|
||||
-ip string
|
||||
ip the webhook should serve hooks on (default "0.0.0.0")
|
||||
-key string
|
||||
path to the HTTPS certificate private key pem file (default "key.pem")
|
||||
-list-cipher-suites
|
||||
list available TLS cipher suites
|
||||
-nopanic
|
||||
do not panic if hooks cannot be loaded when webhook is not running in verbose mode
|
||||
-port int
|
||||
port the webhook should serve hooks on (default 9000)
|
||||
-secure
|
||||
use HTTPS instead of HTTP
|
||||
-template
|
||||
parse hooks file as a Go template
|
||||
-tls-min-version string
|
||||
minimum TLS version (1.0, 1.1, 1.2, 1.3) (default "1.2")
|
||||
-urlprefix string
|
||||
url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id) (default "hooks")
|
||||
-verbose
|
||||
show verbose output
|
||||
-version
|
||||
display webhook version and quit
|
||||
```
|
||||
|
||||
Use any of the above specified flags to override their default behavior.
|
||||
|
||||
# Live reloading hooks
|
||||
If you are running an OS that supports USR1 signal, you can use it to trigger hooks reload from hooks file, without restarting the webhook instance.
|
||||
```bash
|
||||
kill -USR1 webhookpid
|
||||
```
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 912 B |
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
+218
-57
@@ -1,20 +1,29 @@
|
||||
package hook
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
)
|
||||
@@ -87,10 +96,12 @@ func (e *ParseError) Error() string {
|
||||
|
||||
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
|
||||
func CheckPayloadSignature(payload []byte, secret string, signature string) (string, error) {
|
||||
if strings.HasPrefix(signature, "sha1=") {
|
||||
signature = signature[5:]
|
||||
if secret == "" {
|
||||
return "", errors.New("signature validation secret can not be empty")
|
||||
}
|
||||
|
||||
signature = strings.TrimPrefix(signature, "sha1=")
|
||||
|
||||
mac := hmac.New(sha1.New, []byte(secret))
|
||||
_, err := mac.Write(payload)
|
||||
if err != nil {
|
||||
@@ -106,10 +117,12 @@ func CheckPayloadSignature(payload []byte, secret string, signature string) (str
|
||||
|
||||
// CheckPayloadSignature256 calculates and verifies SHA256 signature of the given payload
|
||||
func CheckPayloadSignature256(payload []byte, secret string, signature string) (string, error) {
|
||||
if strings.HasPrefix(signature, "sha256=") {
|
||||
signature = signature[7:]
|
||||
if secret == "" {
|
||||
return "", errors.New("signature validation secret can not be empty")
|
||||
}
|
||||
|
||||
signature = strings.TrimPrefix(signature, "sha256=")
|
||||
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
_, err := mac.Write(payload)
|
||||
if err != nil {
|
||||
@@ -123,46 +136,103 @@ func CheckPayloadSignature256(payload []byte, secret string, signature string) (
|
||||
return expectedMAC, err
|
||||
}
|
||||
|
||||
// CheckPayloadSignature512 calculates and verifies SHA512 signature of the given payload
|
||||
func CheckPayloadSignature512(payload []byte, secret string, signature string) (string, error) {
|
||||
if secret == "" {
|
||||
return "", errors.New("signature validation secret can not be empty")
|
||||
}
|
||||
|
||||
signature = strings.TrimPrefix(signature, "sha512=")
|
||||
|
||||
mac := hmac.New(sha512.New, []byte(secret))
|
||||
_, err := mac.Write(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
|
||||
return expectedMAC, &SignatureError{signature}
|
||||
}
|
||||
return expectedMAC, err
|
||||
}
|
||||
|
||||
func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey string, checkDate bool) (bool, error) {
|
||||
// Check for the signature and date headers
|
||||
if _, ok := headers["X-Signature"]; !ok {
|
||||
return false, nil
|
||||
}
|
||||
if _, ok := headers["Date"]; !ok {
|
||||
return false, nil
|
||||
}
|
||||
if signingKey == "" {
|
||||
return false, errors.New("signature validation signing key can not be empty")
|
||||
}
|
||||
|
||||
providedSignature := headers["X-Signature"].(string)
|
||||
dateHeader := headers["Date"].(string)
|
||||
mac := hmac.New(sha1.New, []byte(signingKey))
|
||||
mac.Write(body)
|
||||
mac.Write([]byte(dateHeader))
|
||||
expectedSignature := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
if !hmac.Equal([]byte(providedSignature), []byte(expectedSignature)) {
|
||||
return false, &SignatureError{providedSignature}
|
||||
}
|
||||
|
||||
if !checkDate {
|
||||
return true, nil
|
||||
}
|
||||
// Example format: Fri 08 Sep 2017 11:24:32 UTC
|
||||
date, err := time.Parse("Mon 02 Jan 2006 15:04:05 MST", dateHeader)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
now := time.Now()
|
||||
delta := math.Abs(now.Sub(date).Seconds())
|
||||
|
||||
if delta > 300 {
|
||||
return false, &SignatureError{"outdated"}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CheckIPWhitelist makes sure the provided remote address (of the form IP:port) falls within the provided IP range
|
||||
// (in CIDR form or a single IP address).
|
||||
func CheckIPWhitelist(remoteAddr string, ipRange string) (bool, error) {
|
||||
// Extract IP address from remote address.
|
||||
|
||||
ip := remoteAddr
|
||||
// IPv6 addresses will likely be surrounded by [].
|
||||
ip := strings.Trim(remoteAddr, " []")
|
||||
|
||||
if strings.LastIndex(remoteAddr, ":") != -1 {
|
||||
ip = remoteAddr[0:strings.LastIndex(remoteAddr, ":")]
|
||||
if i := strings.LastIndex(ip, ":"); i != -1 {
|
||||
ip = ip[:i]
|
||||
ip = strings.Trim(ip, " []")
|
||||
}
|
||||
|
||||
ip = strings.TrimSpace(ip)
|
||||
|
||||
// IPv6 addresses will likely be surrounded by [], so don't forget to remove those.
|
||||
|
||||
if strings.HasPrefix(ip, "[") && strings.HasSuffix(ip, "]") {
|
||||
ip = ip[1 : len(ip)-1]
|
||||
}
|
||||
|
||||
parsedIP := net.ParseIP(strings.TrimSpace(ip))
|
||||
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
return false, fmt.Errorf("invalid IP address found in remote address '%s'", remoteAddr)
|
||||
}
|
||||
|
||||
// Extract IP range in CIDR form. If a single IP address is provided, turn it into CIDR form.
|
||||
for _, r := range strings.Fields(ipRange) {
|
||||
// Extract IP range in CIDR form. If a single IP address is provided, turn it into CIDR form.
|
||||
|
||||
ipRange = strings.TrimSpace(ipRange)
|
||||
if !strings.Contains(r, "/") {
|
||||
r = r + "/32"
|
||||
}
|
||||
|
||||
if strings.Index(ipRange, "/") == -1 {
|
||||
ipRange = ipRange + "/32"
|
||||
_, cidr, err := net.ParseCIDR(r)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if cidr.Contains(parsedIP) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
_, cidr, err := net.ParseCIDR(ipRange)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return cidr.Contains(parsedIP), nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// ReplaceParameter replaces parameter value with the passed value in the passed map
|
||||
@@ -175,7 +245,6 @@ func ReplaceParameter(s string, params interface{}, value interface{}) bool {
|
||||
|
||||
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
|
||||
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
|
||||
|
||||
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||
index, err := strconv.ParseUint(p[0], 10, 64)
|
||||
|
||||
@@ -210,8 +279,12 @@ func GetParameter(s string, params interface{}) (interface{}, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
|
||||
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
|
||||
paramsValue := reflect.ValueOf(params)
|
||||
|
||||
switch paramsValue.Kind() {
|
||||
case reflect.Slice:
|
||||
paramsValueSliceLength := paramsValue.Len()
|
||||
if paramsValueSliceLength > 0 {
|
||||
|
||||
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||
index, err := strconv.ParseUint(p[0], 10, 64)
|
||||
@@ -233,18 +306,20 @@ func GetParameter(s string, params interface{}) (interface{}, bool) {
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Map {
|
||||
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||
case reflect.Map:
|
||||
// Check for raw key
|
||||
if v, ok := params.(map[string]interface{})[s]; ok {
|
||||
return v, true
|
||||
}
|
||||
|
||||
// Checked for dotted references
|
||||
p := strings.SplitN(s, ".", 2)
|
||||
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||
if len(p) > 1 {
|
||||
return GetParameter(p[1], pValue)
|
||||
}
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
} else {
|
||||
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||
|
||||
return pValue, true
|
||||
}
|
||||
}
|
||||
@@ -263,9 +338,10 @@ func ExtractParameterAsString(s string, params interface{}) (string, bool) {
|
||||
// Argument type specifies the parameter key name and the source it should
|
||||
// be extracted from
|
||||
type Argument struct {
|
||||
Source string `json:"source,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
EnvName string `json:"envname,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
EnvName string `json:"envname,omitempty"`
|
||||
Base64Decode bool `json:"base64decode,omitempty"`
|
||||
}
|
||||
|
||||
// Get Argument method returns the value for the Argument's key name
|
||||
@@ -286,7 +362,6 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
|
||||
return ha.Name, true
|
||||
case SourceEntirePayload:
|
||||
r, err := json.Marshal(payload)
|
||||
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
@@ -294,7 +369,6 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
|
||||
return string(r), true
|
||||
case SourceEntireHeaders:
|
||||
r, err := json.Marshal(headers)
|
||||
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
@@ -302,7 +376,6 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
|
||||
return string(r), true
|
||||
case SourceEntireQuery:
|
||||
r, err := json.Marshal(query)
|
||||
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
@@ -378,17 +451,21 @@ type Hook struct {
|
||||
ResponseMessage string `json:"response-message,omitempty"`
|
||||
ResponseHeaders ResponseHeaders `json:"response-headers,omitempty"`
|
||||
CaptureCommandOutput bool `json:"include-command-output-in-response,omitempty"`
|
||||
CaptureCommandOutputOnError bool `json:"include-command-output-in-response-on-error,omitempty"`
|
||||
PassEnvironmentToCommand []Argument `json:"pass-environment-to-command,omitempty"`
|
||||
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command,omitempty"`
|
||||
PassFileToCommand []Argument `json:"pass-file-to-command,omitempty"`
|
||||
JSONStringParameters []Argument `json:"parse-parameters-as-json,omitempty"`
|
||||
TriggerRule *Rules `json:"trigger-rule,omitempty"`
|
||||
TriggerRuleMismatchHttpResponseCode int `json:"trigger-rule-mismatch-http-response-code,omitempty"`
|
||||
IncomingPayloadContentType string `json:"incoming-payload-content-type,omitempty"`
|
||||
SuccessHttpResponseCode int `json:"success-http-response-code,omitempty"`
|
||||
}
|
||||
|
||||
// ParseJSONParameters decodes specified arguments to JSON objects and replaces the
|
||||
// string with the newly created object
|
||||
func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface{}) []error {
|
||||
var errors = make([]error, 0)
|
||||
errors := make([]error, 0)
|
||||
|
||||
for i := range h.JSONStringParameters {
|
||||
if arg, ok := h.JSONStringParameters[i].Get(headers, query, payload); ok {
|
||||
@@ -398,7 +475,6 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
|
||||
decoder.UseNumber()
|
||||
|
||||
err := decoder.Decode(&newArg)
|
||||
|
||||
if err != nil {
|
||||
errors = append(errors, &ParseError{err})
|
||||
continue
|
||||
@@ -441,8 +517,8 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
|
||||
// ExtractCommandArguments creates a list of arguments, based on the
|
||||
// PassArgumentsToCommand property that is ready to be used with exec.Command()
|
||||
func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) ([]string, []error) {
|
||||
var args = make([]string, 0)
|
||||
var errors = make([]error, 0)
|
||||
args := make([]string, 0)
|
||||
errors := make([]error, 0)
|
||||
|
||||
args = append(args, h.ExecuteCommand)
|
||||
|
||||
@@ -466,8 +542,8 @@ func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]inter
|
||||
// format, based on the PassEnvironmentToCommand property that is ready to be used
|
||||
// with exec.Command().
|
||||
func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string]interface{}) ([]string, []error) {
|
||||
var args = make([]string, 0)
|
||||
var errors = make([]error, 0)
|
||||
args := make([]string, 0)
|
||||
errors := make([]error, 0)
|
||||
for i := range h.PassEnvironmentToCommand {
|
||||
if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok {
|
||||
if h.PassEnvironmentToCommand[i].EnvName != "" {
|
||||
@@ -489,11 +565,60 @@ func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// FileParameter describes a pass-file-to-command instance to be stored as file
|
||||
type FileParameter struct {
|
||||
File *os.File
|
||||
EnvName string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// ExtractCommandArgumentsForFile creates a list of arguments in key=value
|
||||
// format, based on the PassFileToCommand property that is ready to be used
|
||||
// with exec.Command().
|
||||
func (h *Hook) ExtractCommandArgumentsForFile(headers, query, payload *map[string]interface{}) ([]FileParameter, []error) {
|
||||
args := make([]FileParameter, 0)
|
||||
errors := make([]error, 0)
|
||||
for i := range h.PassFileToCommand {
|
||||
if arg, ok := h.PassFileToCommand[i].Get(headers, query, payload); ok {
|
||||
|
||||
if h.PassFileToCommand[i].EnvName == "" {
|
||||
// if no environment-variable name is set, fall-back on the name
|
||||
log.Printf("no ENVVAR name specified, falling back to [%s]", EnvNamespace+strings.ToUpper(h.PassFileToCommand[i].Name))
|
||||
h.PassFileToCommand[i].EnvName = EnvNamespace + strings.ToUpper(h.PassFileToCommand[i].Name)
|
||||
}
|
||||
|
||||
var fileContent []byte
|
||||
if h.PassFileToCommand[i].Base64Decode {
|
||||
dec, err := base64.StdEncoding.DecodeString(arg)
|
||||
if err != nil {
|
||||
log.Printf("error decoding string [%s]", err)
|
||||
}
|
||||
fileContent = []byte(dec)
|
||||
} else {
|
||||
fileContent = []byte(arg)
|
||||
}
|
||||
|
||||
args = append(args, FileParameter{EnvName: h.PassFileToCommand[i].EnvName, Data: fileContent})
|
||||
|
||||
} else {
|
||||
errors = append(errors, &ArgumentError{h.PassFileToCommand[i]})
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return args, errors
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// Hooks is an array of Hook objects
|
||||
type Hooks []Hook
|
||||
|
||||
// LoadFromFile attempts to load hooks from specified JSON file
|
||||
func (h *Hooks) LoadFromFile(path string) error {
|
||||
// LoadFromFile attempts to load hooks from the specified file, which
|
||||
// can be either JSON or YAML. The asTemplate parameter causes the file
|
||||
// contents to be parsed as a Go text/template prior to unmarshalling.
|
||||
func (h *Hooks) LoadFromFile(path string, asTemplate bool) error {
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -505,6 +630,24 @@ func (h *Hooks) LoadFromFile(path string) error {
|
||||
return e
|
||||
}
|
||||
|
||||
if asTemplate {
|
||||
funcMap := template.FuncMap{"getenv": getenv}
|
||||
|
||||
tmpl, err := template.New("hooks").Funcs(funcMap).Parse(string(file))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
err = tmpl.Execute(&buf, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file = buf.Bytes()
|
||||
}
|
||||
|
||||
e = yaml.Unmarshal(file, h)
|
||||
return e
|
||||
}
|
||||
@@ -573,7 +716,7 @@ func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, body
|
||||
}
|
||||
|
||||
res = res && rv
|
||||
if res == false {
|
||||
if !res {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
@@ -595,7 +738,7 @@ func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, body *
|
||||
}
|
||||
|
||||
res = res || rv
|
||||
if res == true {
|
||||
if res {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
@@ -628,7 +771,9 @@ const (
|
||||
MatchRegex string = "regex"
|
||||
MatchHashSHA1 string = "payload-hash-sha1"
|
||||
MatchHashSHA256 string = "payload-hash-sha256"
|
||||
MatchHashSHA512 string = "payload-hash-sha512"
|
||||
IPWhitelist string = "ip-whitelist"
|
||||
ScalrSignature string = "scalr-signature"
|
||||
)
|
||||
|
||||
// Evaluate MatchRule will return based on the type
|
||||
@@ -636,11 +781,14 @@ 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 {
|
||||
case MatchValue:
|
||||
return arg == r.Value, nil
|
||||
return compare(arg, r.Value), nil
|
||||
case MatchRegex:
|
||||
return regexp.MatchString(r.Regex, arg)
|
||||
case MatchHashSHA1:
|
||||
@@ -649,7 +797,20 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod
|
||||
case MatchHashSHA256:
|
||||
_, err := CheckPayloadSignature256(*body, r.Secret, arg)
|
||||
return err == nil, err
|
||||
case MatchHashSHA512:
|
||||
_, err := CheckPayloadSignature512(*body, r.Secret, arg)
|
||||
return err == nil, err
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// compare is a helper function for constant time string comparisons.
|
||||
func compare(a, b string) bool {
|
||||
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||
}
|
||||
|
||||
// getenv provides a template function to retrieve OS environment variables.
|
||||
func getenv(s string) string {
|
||||
return os.Getenv(s)
|
||||
}
|
||||
|
||||
+236
-53
@@ -1,11 +1,44 @@
|
||||
package hook
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetParameter(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
key string
|
||||
val interface{}
|
||||
expect interface{}
|
||||
ok bool
|
||||
}{
|
||||
// True
|
||||
{"a", map[string]interface{}{"a": "1"}, "1", true},
|
||||
{"a.b", map[string]interface{}{"a.b": "1"}, "1", true},
|
||||
{"a.c", map[string]interface{}{"a": map[string]interface{}{"c": 2}}, 2, true},
|
||||
{"a.1", map[string]interface{}{"a": map[string]interface{}{"1": 3}}, 3, true},
|
||||
{"a.1", map[string]interface{}{"a": []interface{}{"a", "b"}}, "b", true},
|
||||
{"0", []interface{}{"a", "b"}, "a", true},
|
||||
|
||||
// False
|
||||
{"z", map[string]interface{}{"a": "1"}, nil, false},
|
||||
{"a.z", map[string]interface{}{"a": map[string]interface{}{"b": 2}}, nil, false},
|
||||
{"z.b", map[string]interface{}{"a": map[string]interface{}{"z": 2}}, nil, false},
|
||||
{"a.2", map[string]interface{}{"a": []interface{}{"a", "b"}}, nil, false},
|
||||
} {
|
||||
res, ok := GetParameter(test.key, test.val)
|
||||
if ok != test.ok {
|
||||
t.Errorf("unexpected result given {%q, %q}: %t\n", test.key, test.val, ok)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(res, test.expect) {
|
||||
t.Errorf("failed given {%q, %q}:\nexpected {%#v}\ngot {%#v}\n", test.key, test.val, test.expect, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkPayloadSignatureTests = []struct {
|
||||
payload []byte
|
||||
secret string
|
||||
@@ -18,6 +51,7 @@ var checkPayloadSignatureTests = []struct {
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
|
||||
{[]byte(`{"a": "z"}`), "secreX", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "900225703e9342328db7307692736e2f7cc7b36e", false},
|
||||
{[]byte(`{"a": "z"}`), "", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "", false},
|
||||
}
|
||||
|
||||
func TestCheckPayloadSignature(t *testing.T) {
|
||||
@@ -27,7 +61,7 @@ func TestCheckPayloadSignature(t *testing.T) {
|
||||
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
|
||||
}
|
||||
|
||||
if err != nil && strings.Contains(err.Error(), tt.mac) {
|
||||
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
|
||||
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -44,6 +78,7 @@ var checkPayloadSignature256Tests = []struct {
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
|
||||
{[]byte(`{"a": "z"}`), "", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "", false},
|
||||
}
|
||||
|
||||
func TestCheckPayloadSignature256(t *testing.T) {
|
||||
@@ -53,12 +88,117 @@ 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 checkPayloadSignature512Tests = []struct {
|
||||
payload []byte
|
||||
secret string
|
||||
signature string
|
||||
mac string
|
||||
ok bool
|
||||
}{
|
||||
{[]byte(`{"a": "z"}`), "secret", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true},
|
||||
{[]byte(`{"a": "z"}`), "secret", "sha512=4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true},
|
||||
// failures
|
||||
{[]byte(`{"a": "z"}`), "secret", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", false},
|
||||
{[]byte(`{"a": "z"}`), "", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "", false},
|
||||
}
|
||||
|
||||
func TestCheckPayloadSignature512(t *testing.T) {
|
||||
for _, tt := range checkPayloadSignature512Tests {
|
||||
mac, err := CheckPayloadSignature512(tt.payload, tt.secret, tt.signature)
|
||||
if (err == nil) != tt.ok || mac != tt.mac {
|
||||
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 && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
|
||||
t.Errorf("error message should not disclose expected mac: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkScalrSignatureTests = []struct {
|
||||
description string
|
||||
headers map[string]interface{}
|
||||
payload []byte
|
||||
secret string
|
||||
expectedSignature string
|
||||
ok bool
|
||||
}{
|
||||
{
|
||||
"Valid signature",
|
||||
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "48e395e38ac48988929167df531eb2da00063a7d"},
|
||||
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", true,
|
||||
},
|
||||
{
|
||||
"Wrong signature",
|
||||
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "999395e38ac48988929167df531eb2da00063a7d"},
|
||||
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
||||
},
|
||||
{
|
||||
"Missing Date header",
|
||||
map[string]interface{}{"X-Signature": "999395e38ac48988929167df531eb2da00063a7d"},
|
||||
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
||||
},
|
||||
{
|
||||
"Missing X-Signature header",
|
||||
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC"},
|
||||
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
||||
},
|
||||
{
|
||||
"Missing signing key",
|
||||
map[string]interface{}{"Date": "Thu 07 Sep 2017 06:30:04 UTC", "X-Signature": "48e395e38ac48988929167df531eb2da00063a7d"},
|
||||
[]byte(`{"a": "b"}`), "",
|
||||
"48e395e38ac48988929167df531eb2da00063a7d", false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestCheckScalrSignature(t *testing.T) {
|
||||
for _, testCase := range checkScalrSignatureTests {
|
||||
valid, err := CheckScalrSignature(testCase.headers, testCase.payload, testCase.secret, false)
|
||||
if valid != testCase.ok {
|
||||
t.Errorf("failed to check scalr signature fot test case: %s\nexpected ok:%#v, got ok:%#v}",
|
||||
testCase.description, testCase.ok, valid)
|
||||
}
|
||||
|
||||
if err != nil && testCase.secret != "" && strings.Contains(err.Error(), testCase.expectedSignature) {
|
||||
t.Errorf("error message should not disclose expected mac: %s on test case %s", err, testCase.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkIPWhitelistTests = []struct {
|
||||
addr string
|
||||
ipRange string
|
||||
expect bool
|
||||
ok bool
|
||||
}{
|
||||
{"[ 10.0.0.1:1234 ] ", " 10.0.0.1 ", true, true},
|
||||
{"[ 10.0.0.1:1234 ] ", " 10.0.0.0 ", false, true},
|
||||
{"[ 10.0.0.1:1234 ] ", " 10.0.0.1 10.0.0.1 ", true, true},
|
||||
{"[ 10.0.0.1:1234 ] ", " 10.0.0.0/31 ", true, true},
|
||||
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/48 ", true, true},
|
||||
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/48 2001:db8:1::/64", true, true},
|
||||
{" [2001:db8:1:2::1:1234] ", " 2001:db8:1::/64 ", false, true},
|
||||
}
|
||||
|
||||
func TestCheckIPWhitelist(t *testing.T) {
|
||||
for _, tt := range checkIPWhitelistTests {
|
||||
result, err := CheckIPWhitelist(tt.addr, tt.ipRange)
|
||||
if (err == nil) != tt.ok || result != tt.expect {
|
||||
t.Errorf("ip whitelist test failed {%q, %q}:\nwant {expect:%#v, ok:%#v},\ngot {result:%#v, ok:%#v}", tt.addr, tt.ipRange, tt.expect, tt.ok, result, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var extractParameterTests = []struct {
|
||||
s string
|
||||
params interface{}
|
||||
@@ -80,7 +220,7 @@ var extractParameterTests = []struct {
|
||||
{"a.501.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-existent slice index
|
||||
{"a.502.b", map[string]interface{}{"a": []interface{}{}}, "", false}, // non-existent slice index
|
||||
{"a.b.503", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "", false}, // trailing, non-existent slice index
|
||||
{"a.b", interface{}("a"), "", false}, // non-map, non-slice input
|
||||
{"a.b", interface{}("a"), "", false}, // non-map, non-slice input
|
||||
}
|
||||
|
||||
func TestExtractParameter(t *testing.T) {
|
||||
@@ -111,7 +251,7 @@ var argumentGetTests = []struct {
|
||||
|
||||
func TestArgumentGet(t *testing.T) {
|
||||
for _, tt := range argumentGetTests {
|
||||
a := Argument{tt.source, tt.name, ""}
|
||||
a := Argument{tt.source, tt.name, "", false}
|
||||
value, ok := a.Get(tt.headers, tt.query, tt.payload)
|
||||
if ok != tt.ok || value != tt.value {
|
||||
t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, ok:%#v}", tt.source, tt.name, tt.value, tt.ok, value, ok)
|
||||
@@ -125,14 +265,14 @@ var hookParseJSONParametersTests = []struct {
|
||||
rheaders, rquery, rpayload *map[string]interface{}
|
||||
ok bool
|
||||
}{
|
||||
{[]Argument{Argument{"header", "a", ""}}, &map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, true},
|
||||
{[]Argument{Argument{"url", "a", ""}}, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
|
||||
{[]Argument{Argument{"payload", "a", ""}}, nil, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
|
||||
{[]Argument{Argument{"header", "z", ""}}, &map[string]interface{}{"Z": `{}`}, nil, nil, &map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true},
|
||||
{[]Argument{Argument{"header", "a", "", false}}, &map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, true},
|
||||
{[]Argument{Argument{"url", "a", "", false}}, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
|
||||
{[]Argument{Argument{"payload", "a", "", false}}, nil, nil, &map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, &map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
|
||||
{[]Argument{Argument{"header", "z", "", false}}, &map[string]interface{}{"Z": `{}`}, nil, nil, &map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true},
|
||||
// failures
|
||||
{[]Argument{Argument{"header", "z", ""}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // empty string
|
||||
{[]Argument{Argument{"header", "y", ""}}, &map[string]interface{}{"X": `{}`}, nil, nil, &map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter
|
||||
{[]Argument{Argument{"string", "z", ""}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source
|
||||
{[]Argument{Argument{"header", "z", "", false}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // empty string
|
||||
{[]Argument{Argument{"header", "y", "", false}}, &map[string]interface{}{"X": `{}`}, nil, nil, &map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter
|
||||
{[]Argument{Argument{"string", "z", "", false}}, &map[string]interface{}{"Z": ``}, nil, nil, &map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source
|
||||
}
|
||||
|
||||
func TestHookParseJSONParameters(t *testing.T) {
|
||||
@@ -152,9 +292,9 @@ var hookExtractCommandArgumentsTests = []struct {
|
||||
value []string
|
||||
ok bool
|
||||
}{
|
||||
{"test", []Argument{Argument{"header", "a", ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"test", "z"}, true},
|
||||
{"test", []Argument{Argument{"header", "a", "", false}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"test", "z"}, true},
|
||||
// failures
|
||||
{"fail", []Argument{Argument{"payload", "a", ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"fail", ""}, false},
|
||||
{"fail", []Argument{Argument{"payload", "a", "", false}}, &map[string]interface{}{"A": "z"}, nil, nil, []string{"fail", ""}, false},
|
||||
}
|
||||
|
||||
func TestHookExtractCommandArguments(t *testing.T) {
|
||||
@@ -196,14 +336,14 @@ var hookExtractCommandArgumentsForEnvTests = []struct {
|
||||
// successes
|
||||
{
|
||||
"test",
|
||||
[]Argument{Argument{"header", "a", ""}},
|
||||
[]Argument{Argument{"header", "a", "", false}},
|
||||
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{"HOOK_a=z"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"test",
|
||||
[]Argument{Argument{"header", "a", "MYKEY"}},
|
||||
[]Argument{Argument{"header", "a", "MYKEY", false}},
|
||||
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{"MYKEY=z"},
|
||||
true,
|
||||
@@ -211,7 +351,7 @@ var hookExtractCommandArgumentsForEnvTests = []struct {
|
||||
// failures
|
||||
{
|
||||
"fail",
|
||||
[]Argument{Argument{"payload", "a", ""}},
|
||||
[]Argument{Argument{"payload", "a", "", false}},
|
||||
&map[string]interface{}{"A": "z"}, nil, nil,
|
||||
[]string{},
|
||||
false,
|
||||
@@ -229,26 +369,55 @@ func TestHookExtractCommandArgumentsForEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
var hooksLoadFromFileTests = []struct {
|
||||
path string
|
||||
ok bool
|
||||
path string
|
||||
asTemplate bool
|
||||
ok bool
|
||||
}{
|
||||
{"../hooks.json.example", true},
|
||||
{"../hooks.yaml.example", true},
|
||||
{"", true},
|
||||
{"../hooks.json.example", false, true},
|
||||
{"../hooks.yaml.example", false, true},
|
||||
{"../hooks.json.tmpl.example", true, true},
|
||||
{"../hooks.yaml.tmpl.example", true, true},
|
||||
{"", false, true},
|
||||
// failures
|
||||
{"missing.json", false},
|
||||
{"missing.json", false, false},
|
||||
}
|
||||
|
||||
func TestHooksLoadFromFile(t *testing.T) {
|
||||
secret := `foo"123`
|
||||
os.Setenv("XXXTEST_SECRET", secret)
|
||||
|
||||
for _, tt := range hooksLoadFromFileTests {
|
||||
h := &Hooks{}
|
||||
err := h.LoadFromFile(tt.path)
|
||||
err := h.LoadFromFile(tt.path, tt.asTemplate)
|
||||
if (err == nil) != tt.ok {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHooksTemplateLoadFromFile(t *testing.T) {
|
||||
secret := `foo"123`
|
||||
os.Setenv("XXXTEST_SECRET", secret)
|
||||
|
||||
for _, tt := range hooksLoadFromFileTests {
|
||||
if !tt.asTemplate {
|
||||
continue
|
||||
}
|
||||
|
||||
h := &Hooks{}
|
||||
err := h.LoadFromFile(tt.path, tt.asTemplate)
|
||||
if (err == nil) != tt.ok {
|
||||
t.Errorf(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
s := (*h.Match("webhook").TriggerRule.And)[0].Match.Secret
|
||||
if s != secret {
|
||||
t.Errorf("Expected secret of %q, got %q", secret, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hooksMatchTests = []struct {
|
||||
id string
|
||||
hooks Hooks
|
||||
@@ -276,18 +445,18 @@ var matchRuleTests = []struct {
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{"value", "", "", "z", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
|
||||
{"regex", "^z", "", "z", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
|
||||
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
||||
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
||||
{"value", "", "", "z", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
|
||||
{"regex", "^z", "", "z", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
|
||||
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
||||
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
|
||||
// failures
|
||||
{"value", "", "", "X", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
|
||||
{"regex", "^X", "", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
|
||||
{"value", "", "2", "X", "", Argument{"header", "a", ""}, &map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, false}, // reference invalid header
|
||||
{"value", "", "", "X", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
|
||||
{"regex", "^X", "", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
|
||||
{"value", "", "2", "X", "", Argument{"header", "a", "", false}, &map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, false}, // reference invalid header
|
||||
// errors
|
||||
{"regex", "*", "", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex
|
||||
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
{"regex", "*", "", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex
|
||||
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
|
||||
// IP whitelisting, valid cases
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
|
||||
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
|
||||
@@ -324,8 +493,8 @@ var andRuleTests = []struct {
|
||||
{
|
||||
"(a=z, b=y): a=z && b=y",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"A": "z", "B": "y"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
@@ -333,8 +502,8 @@ var andRuleTests = []struct {
|
||||
{
|
||||
"(a=z, b=Y): a=z && b=y",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"A": "z", "B": "Y"}, nil, nil, []byte{},
|
||||
false, false,
|
||||
@@ -343,22 +512,22 @@ var andRuleTests = []struct {
|
||||
{
|
||||
"(a=z, b=y, c=x, d=w=, e=X, f=X): a=z && (b=y && c=x) && (d=w || e=v) && !f=u",
|
||||
AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{
|
||||
And: &AndRule{
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", ""}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", "", false}, ""}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Or: &OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", ""}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", ""}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", "", false}, ""}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Not: &NotRule{
|
||||
Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", ""}, ""},
|
||||
Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", "", false}, ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -369,7 +538,7 @@ var andRuleTests = []struct {
|
||||
// failures
|
||||
{
|
||||
"invalid rule",
|
||||
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}, ""}}},
|
||||
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}},
|
||||
&map[string]interface{}{"Y": "z"}, nil, nil, nil,
|
||||
false, false,
|
||||
},
|
||||
@@ -395,8 +564,8 @@ var orRuleTests = []struct {
|
||||
{
|
||||
"(a=z, b=X): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"A": "z", "B": "X"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
@@ -404,8 +573,8 @@ var orRuleTests = []struct {
|
||||
{
|
||||
"(a=X, b=y): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"A": "X", "B": "y"}, nil, nil, []byte{},
|
||||
true, false,
|
||||
@@ -413,8 +582,8 @@ var orRuleTests = []struct {
|
||||
{
|
||||
"(a=Z, b=Y): a=z || b=y",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil, []byte{},
|
||||
false, false,
|
||||
@@ -423,7 +592,7 @@ var orRuleTests = []struct {
|
||||
{
|
||||
"invalid rule",
|
||||
OrRule{
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
|
||||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
|
||||
},
|
||||
&map[string]interface{}{"Y": "Z"}, nil, nil, []byte{},
|
||||
false, false,
|
||||
@@ -447,8 +616,8 @@ var notRuleTests = []struct {
|
||||
ok bool
|
||||
err bool
|
||||
}{
|
||||
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}, ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
|
||||
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
|
||||
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
|
||||
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
|
||||
}
|
||||
|
||||
func TestNotRule(t *testing.T) {
|
||||
@@ -459,3 +628,17 @@ func TestNotRule(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompare(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
a, b string
|
||||
ok bool
|
||||
}{
|
||||
{"abcd", "abcd", true},
|
||||
{"zyxw", "abcd", false},
|
||||
} {
|
||||
if ok := compare(tt.a, tt.b); ok != tt.ok {
|
||||
t.Errorf("compare failed for %q and %q: got %v\n", tt.a, tt.b, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
[
|
||||
{
|
||||
"id": "webhook",
|
||||
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||
"command-working-directory": "/home/adnan/go",
|
||||
"response-message": "I got the payload!",
|
||||
"response-headers":
|
||||
[
|
||||
{
|
||||
"name": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
],
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "head_commit.id"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.name"
|
||||
},
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
],
|
||||
"trigger-rule":
|
||||
{
|
||||
"and":
|
||||
[
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "payload-hash-sha1",
|
||||
"secret": "{{ getenv "XXXTEST_SECRET" | js }}",
|
||||
"parameter":
|
||||
{
|
||||
"source": "header",
|
||||
"name": "X-Hub-Signature"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match":
|
||||
{
|
||||
"type": "value",
|
||||
"value": "refs/heads/master",
|
||||
"parameter":
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "ref"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
- id: webhook
|
||||
execute-command: /home/adnan/redeploy-go-webhook.sh
|
||||
command-working-directory: /home/adnan/go
|
||||
response-message: I got the payload!
|
||||
response-headers:
|
||||
- name: Access-Control-Allow-Origin
|
||||
value: '*'
|
||||
pass-arguments-to-command:
|
||||
- source: payload
|
||||
name: head_commit.id
|
||||
- source: payload
|
||||
name: pusher.name
|
||||
- source: payload
|
||||
name: pusher.email
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
type: payload-hash-sha1
|
||||
secret: "{{ getenv "XXXTEST_SECRET" | js }}"
|
||||
parameter:
|
||||
source: header
|
||||
name: X-Hub-Signature
|
||||
- match:
|
||||
type: value
|
||||
value: refs/heads/master
|
||||
parameter:
|
||||
source: payload
|
||||
name: ref
|
||||
@@ -5,6 +5,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -23,4 +24,14 @@ func main() {
|
||||
if len(env) > 0 {
|
||||
fmt.Printf("env: %s\n", strings.Join(env, " "))
|
||||
}
|
||||
|
||||
if (len(os.Args) > 1) && (strings.HasPrefix(os.Args[1], "exit=")) {
|
||||
exit_code_str := os.Args[1][5:]
|
||||
exit_code, err := strconv.Atoi(exit_code_str)
|
||||
if err != nil {
|
||||
fmt.Printf("Exit code %s not an int!", exit_code_str)
|
||||
os.Exit(-1)
|
||||
}
|
||||
os.Exit(exit_code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,5 +136,73 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "capture-command-output-on-success-not-by-default",
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "string",
|
||||
"name": "exit=0"
|
||||
}
|
||||
],
|
||||
"execute-command": "{{ .Hookecho }}"
|
||||
},
|
||||
{
|
||||
"id": "capture-command-output-on-success-yes-with-flag",
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "string",
|
||||
"name": "exit=0"
|
||||
}
|
||||
],
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"include-command-output-in-response": true
|
||||
},
|
||||
{
|
||||
"id": "capture-command-output-on-error-not-by-default",
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "string",
|
||||
"name": "exit=1"
|
||||
}
|
||||
],
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"include-command-output-in-response": true
|
||||
},
|
||||
{
|
||||
"id": "capture-command-output-on-error-yes-with-extra-flag",
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "string",
|
||||
"name": "exit=1"
|
||||
}
|
||||
],
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"include-command-output-in-response": true,
|
||||
"include-command-output-in-response-on-error": true
|
||||
},
|
||||
{
|
||||
"id": "static-params-ok",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"response-message": "success",
|
||||
"include-command-output-in-response": true,
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "string",
|
||||
"name": "passed"
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "warn-on-space",
|
||||
"execute-command": "{{ .Hookecho }} foo",
|
||||
"response-message": "success",
|
||||
"include-command-output-in-response": true,
|
||||
"pass-arguments-to-command": [
|
||||
{
|
||||
"source": "string",
|
||||
"name": "passed"
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
+47
-6
@@ -1,4 +1,5 @@
|
||||
- trigger-rule:
|
||||
- id: github
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
parameter:
|
||||
@@ -23,9 +24,10 @@
|
||||
pass-environment-to-command:
|
||||
- source: payload
|
||||
name: head_commit.timestamp
|
||||
id: github
|
||||
command-working-directory: /
|
||||
- trigger-rule:
|
||||
|
||||
- id: bitbucket
|
||||
trigger-rule:
|
||||
and:
|
||||
- match:
|
||||
parameter:
|
||||
@@ -52,9 +54,10 @@
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
response-message: success
|
||||
include-command-output-in-response: false
|
||||
id: bitbucket
|
||||
command-working-directory: /
|
||||
- trigger-rule:
|
||||
|
||||
- id: gitlab
|
||||
trigger-rule:
|
||||
match:
|
||||
parameter:
|
||||
source: payload
|
||||
@@ -71,5 +74,43 @@
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
response-message: success
|
||||
include-command-output-in-response: true
|
||||
id: gitlab
|
||||
command-working-directory: /
|
||||
|
||||
- id: capture-command-output-on-success-not-by-default
|
||||
pass-arguments-to-command:
|
||||
- source: string
|
||||
name: exit=0
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
|
||||
- id: capture-command-output-on-success-yes-with-flag
|
||||
pass-arguments-to-command:
|
||||
- source: string
|
||||
name: exit=0
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
include-command-output-in-response: true
|
||||
|
||||
- id: capture-command-output-on-error-not-by-default
|
||||
pass-arguments-to-command:
|
||||
- source: string
|
||||
name: exit=1
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
include-command-output-in-response: true
|
||||
|
||||
- id: capture-command-output-on-error-yes-with-extra-flag
|
||||
pass-arguments-to-command:
|
||||
- source: string
|
||||
name: exit=1
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
include-command-output-in-response: true
|
||||
include-command-output-in-response-on-error: true
|
||||
|
||||
- id: static-params-ok
|
||||
execute-command: '{{ .Hookecho }}'
|
||||
include-command-output-in-response: true
|
||||
pass-arguments-to-command:
|
||||
- source: string
|
||||
name: passed
|
||||
|
||||
- id: warn-on-space
|
||||
execute-command: '{{ .Hookecho }} foo'
|
||||
include-command-output-in-response: true
|
||||
@@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func writeTLSSupportedCipherStrings(w io.Writer, min uint16) error {
|
||||
for _, c := range CipherSuites() {
|
||||
var found bool
|
||||
|
||||
for _, v := range c.SupportedVersions {
|
||||
if v >= min {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := w.Write([]byte(c.Name + "\n"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTLSMinVersion converts a version string into a TLS version ID.
|
||||
func getTLSMinVersion(v string) uint16 {
|
||||
switch v {
|
||||
case "1.0":
|
||||
return tls.VersionTLS10
|
||||
case "1.1":
|
||||
return tls.VersionTLS11
|
||||
case "1.2", "":
|
||||
return tls.VersionTLS12
|
||||
case "1.3":
|
||||
return tls.VersionTLS13
|
||||
default:
|
||||
log.Fatalln("error: unknown minimum TLS version:", v)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// getTLSCipherSuites converts a comma separated list of cipher suites into a
|
||||
// slice of TLS cipher suite IDs.
|
||||
func getTLSCipherSuites(v string) []uint16 {
|
||||
supported := CipherSuites()
|
||||
|
||||
if v == "" {
|
||||
suites := make([]uint16, len(supported))
|
||||
|
||||
for _, cs := range supported {
|
||||
suites = append(suites, cs.ID)
|
||||
}
|
||||
|
||||
return suites
|
||||
}
|
||||
|
||||
var found bool
|
||||
txts := strings.Split(v, ",")
|
||||
suites := make([]uint16, len(txts))
|
||||
|
||||
for _, want := range txts {
|
||||
found = false
|
||||
|
||||
for _, cs := range supported {
|
||||
if want == cs.Name {
|
||||
suites = append(suites, cs.ID)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
log.Fatalln("error: unknown TLS cipher suite:", want)
|
||||
}
|
||||
}
|
||||
|
||||
return suites
|
||||
}
|
||||
+130
-40
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -17,13 +19,13 @@ import (
|
||||
|
||||
"github.com/codegangsta/negroni"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/satori/go.uuid"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
|
||||
fsnotify "gopkg.in/fsnotify.v1"
|
||||
)
|
||||
|
||||
const (
|
||||
version = "2.6.6"
|
||||
version = "2.6.11"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -34,9 +36,13 @@ var (
|
||||
hotReload = flag.Bool("hotreload", false, "watch hooks file for changes and reload them automatically")
|
||||
hooksURLPrefix = flag.String("urlprefix", "hooks", "url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id)")
|
||||
secure = flag.Bool("secure", false, "use HTTPS instead of HTTP")
|
||||
asTemplate = flag.Bool("template", false, "parse hooks file as a Go template")
|
||||
cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file")
|
||||
key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file")
|
||||
justDisplayVersion = flag.Bool("version", false, "display webhook version and quit")
|
||||
justListCiphers = flag.Bool("list-cipher-suites", false, "list available TLS cipher suites")
|
||||
tlsMinVersion = flag.String("tls-min-version", "1.2", "minimum TLS version (1.0, 1.1, 1.2, 1.3)")
|
||||
tlsCipherSuites = flag.String("cipher-suites", "", "comma-separated list of supported TLS cipher suites")
|
||||
|
||||
responseHeaders hook.ResponseHeaders
|
||||
hooksFiles hook.HooksFiles
|
||||
@@ -77,6 +83,14 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *justListCiphers {
|
||||
err := writeTLSSupportedCipherStrings(os.Stdout, getTLSMinVersion(*tlsMinVersion))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if len(hooksFiles) == 0 {
|
||||
hooksFiles = append(hooksFiles, "hooks.json")
|
||||
}
|
||||
@@ -99,7 +113,7 @@ func main() {
|
||||
|
||||
newHooks := hook.Hooks{}
|
||||
|
||||
err := newHooks.LoadFromFile(hooksFilePath)
|
||||
err := newHooks.LoadFromFile(hooksFilePath, *asTemplate)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("couldn't load hooks from file! %+v\n", err)
|
||||
@@ -119,7 +133,7 @@ func main() {
|
||||
|
||||
newHooksFiles := hooksFiles[:0]
|
||||
for _, filePath := range hooksFiles {
|
||||
if _, ok := loadedHooksFromFiles[filePath]; ok == true {
|
||||
if _, ok := loadedHooksFromFiles[filePath]; ok {
|
||||
newHooksFiles = append(newHooksFiles, filePath)
|
||||
}
|
||||
}
|
||||
@@ -184,22 +198,36 @@ 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)
|
||||
|
||||
if *secure {
|
||||
log.Printf("serving hooks on https://%s:%d%s", *ip, *port, hooksURL)
|
||||
log.Fatal(http.ListenAndServeTLS(fmt.Sprintf("%s:%d", *ip, *port), *cert, *key, n))
|
||||
} else {
|
||||
if !*secure {
|
||||
log.Printf("serving hooks on http://%s:%d%s", *ip, *port, hooksURL)
|
||||
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *ip, *port), n))
|
||||
}
|
||||
|
||||
svr := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", *ip, *port),
|
||||
Handler: n,
|
||||
TLSConfig: &tls.Config{
|
||||
CipherSuites: getTLSCipherSuites(*tlsCipherSuites),
|
||||
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
|
||||
MinVersion: getTLSMinVersion(*tlsMinVersion),
|
||||
PreferServerCipherSuites: true,
|
||||
},
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0), // disable http/2
|
||||
}
|
||||
|
||||
log.Printf("serving hooks on https://%s:%d%s", *ip, *port, hooksURL)
|
||||
log.Fatal(svr.ListenAndServeTLS(*cert, *key))
|
||||
}
|
||||
|
||||
func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// generate a request id for logging
|
||||
rid := uuid.NewV4().String()[:6]
|
||||
|
||||
@@ -228,31 +256,36 @@ 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") {
|
||||
switch {
|
||||
case strings.Contains(contentType, "json"):
|
||||
decoder := json.NewDecoder(strings.NewReader(string(body)))
|
||||
decoder.UseNumber()
|
||||
|
||||
err := decoder.Decode(&payload)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[%s] error parsing JSON payload %+v\n", rid, err)
|
||||
}
|
||||
} else if strings.Contains(contentType, "form") {
|
||||
case strings.Contains(contentType, "x-www-form-urlencoded"):
|
||||
fd, err := url.ParseQuery(string(body))
|
||||
if err != nil {
|
||||
log.Printf("[%s] error parsing form payload %+v\n", rid, err)
|
||||
} else {
|
||||
payload = valuesToMap(fd)
|
||||
}
|
||||
default:
|
||||
log.Printf("[%s] error parsing body payload due to unsupported content type header: %s\n", rid, contentType)
|
||||
}
|
||||
|
||||
// handle hook
|
||||
if errors := matchedHook.ParseJSONParameters(&headers, &query, &payload); errors != nil {
|
||||
for _, err := range errors {
|
||||
log.Printf("[%s] error parsing JSON parameters: %s\n", rid, err)
|
||||
}
|
||||
errors := matchedHook.ParseJSONParameters(&headers, &query, &payload)
|
||||
for _, err := range errors {
|
||||
log.Printf("[%s] error parsing JSON parameters: %s\n", rid, err)
|
||||
}
|
||||
|
||||
var ok bool
|
||||
@@ -263,9 +296,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.Println(msg)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Error occurred while evaluating hook rules.")
|
||||
fmt.Fprint(w, "Error occurred while evaluating hook rules.")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -281,37 +314,45 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
response, err := handleHook(matchedHook, rid, &headers, &query, &payload, &body)
|
||||
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Error occurred while executing the hook's command. Please check your logs for more details.")
|
||||
if matchedHook.CaptureCommandOutputOnError {
|
||||
fmt.Fprint(w, response)
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
fmt.Fprint(w, "Error occurred while executing the hook's command. Please check your logs for more details.")
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(w, response)
|
||||
// Check if a success return code is configured for the hook
|
||||
if matchedHook.SuccessHttpResponseCode != 0 {
|
||||
writeHttpResponseCode(w, rid, matchedHook.ID, matchedHook.SuccessHttpResponseCode)
|
||||
}
|
||||
fmt.Fprint(w, response)
|
||||
}
|
||||
} else {
|
||||
go handleHook(matchedHook, rid, &headers, &query, &payload, &body)
|
||||
fmt.Fprintf(w, matchedHook.ResponseMessage)
|
||||
|
||||
// Check if a success return code is configured for the hook
|
||||
if matchedHook.SuccessHttpResponseCode != 0 {
|
||||
writeHttpResponseCode(w, rid, matchedHook.ID, matchedHook.SuccessHttpResponseCode)
|
||||
}
|
||||
|
||||
fmt.Fprint(w, matchedHook.ResponseMessage)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if a return code is configured for the hook
|
||||
if matchedHook.TriggerRuleMismatchHttpResponseCode != 0 {
|
||||
// Check if the configured return code is supported by the http package
|
||||
// by testing if there is a StatusText for this code.
|
||||
if len(http.StatusText(matchedHook.TriggerRuleMismatchHttpResponseCode)) > 0 {
|
||||
w.WriteHeader(matchedHook.TriggerRuleMismatchHttpResponseCode)
|
||||
} else {
|
||||
log.Printf("[%s] %s got matched, but the configured return code %d is unknown - defaulting to 200\n", rid, matchedHook.ID, matchedHook.TriggerRuleMismatchHttpResponseCode)
|
||||
}
|
||||
writeHttpResponseCode(w, rid, matchedHook.ID, matchedHook.TriggerRuleMismatchHttpResponseCode)
|
||||
}
|
||||
|
||||
// if none of the hooks got triggered
|
||||
log.Printf("[%s] %s got matched, but didn't get triggered because the trigger rules were not satisfied\n", rid, matchedHook.ID)
|
||||
|
||||
fmt.Fprintf(w, "Hook rules were not satisfied.")
|
||||
fmt.Fprint(w, "Hook rules were not satisfied.")
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintf(w, "Hook not found.")
|
||||
fmt.Fprint(w, "Hook not found.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,6 +361,13 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in
|
||||
|
||||
// check the command exists
|
||||
cmdPath, err := exec.LookPath(h.ExecuteCommand)
|
||||
if err != nil {
|
||||
// give a last chance, maybe is a relative path
|
||||
relativeToCwd := filepath.Join(h.CommandWorkingDirectory, h.ExecuteCommand)
|
||||
// check the command exists
|
||||
cmdPath, err = exec.LookPath(relativeToCwd)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("unable to locate command: '%s'", h.ExecuteCommand)
|
||||
|
||||
@@ -336,19 +384,41 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in
|
||||
cmd.Dir = h.CommandWorkingDirectory
|
||||
|
||||
cmd.Args, errors = h.ExtractCommandArguments(headers, query, payload)
|
||||
if errors != nil {
|
||||
for _, err := range errors {
|
||||
log.Printf("[%s] error extracting command arguments: %s\n", rid, err)
|
||||
}
|
||||
for _, err := range errors {
|
||||
log.Printf("[%s] error extracting command arguments: %s\n", rid, err)
|
||||
}
|
||||
|
||||
var envs []string
|
||||
envs, errors = h.ExtractCommandArgumentsForEnv(headers, query, payload)
|
||||
|
||||
if errors != nil {
|
||||
for _, err := range errors {
|
||||
log.Printf("[%s] error extracting command arguments for environment: %s\n", rid, err)
|
||||
for _, err := range errors {
|
||||
log.Printf("[%s] error extracting command arguments for environment: %s\n", rid, err)
|
||||
}
|
||||
|
||||
files, errors := h.ExtractCommandArgumentsForFile(headers, query, payload)
|
||||
|
||||
for _, err := range errors {
|
||||
log.Printf("[%s] error extracting command arguments for file: %s\n", rid, err)
|
||||
}
|
||||
|
||||
for i := range files {
|
||||
tmpfile, err := ioutil.TempFile(h.CommandWorkingDirectory, files[i].EnvName)
|
||||
if err != nil {
|
||||
log.Printf("[%s] error creating temp file [%s]", rid, err)
|
||||
continue
|
||||
}
|
||||
log.Printf("[%s] writing env %s file %s", rid, files[i].EnvName, tmpfile.Name())
|
||||
if _, err := tmpfile.Write(files[i].Data); err != nil {
|
||||
log.Printf("[%s] error writing file %s [%s]", rid, tmpfile.Name(), err)
|
||||
continue
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
log.Printf("[%s] error closing file %s [%s]", rid, tmpfile.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
files[i].File = tmpfile
|
||||
envs = append(envs, files[i].EnvName+"="+tmpfile.Name())
|
||||
}
|
||||
|
||||
cmd.Env = append(os.Environ(), envs...)
|
||||
@@ -363,18 +433,38 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in
|
||||
log.Printf("[%s] error occurred: %+v\n", rid, err)
|
||||
}
|
||||
|
||||
for i := range files {
|
||||
if files[i].File != nil {
|
||||
log.Printf("[%s] removing file %s\n", rid, files[i].File.Name())
|
||||
err := os.Remove(files[i].File.Name())
|
||||
if err != nil {
|
||||
log.Printf("[%s] error removing file %s [%s]", rid, files[i].File.Name(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[%s] finished handling %s\n", rid, h.ID)
|
||||
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
func writeHttpResponseCode(w http.ResponseWriter, rid string, hookId string, responseCode int) {
|
||||
// Check if the given return code is supported by the http package
|
||||
// by testing if there is a StatusText for this code.
|
||||
if len(http.StatusText(responseCode)) > 0 {
|
||||
w.WriteHeader(responseCode)
|
||||
} else {
|
||||
log.Printf("[%s] %s got matched, but the configured return code %d is unknown - defaulting to 200\n", rid, hookId, responseCode)
|
||||
}
|
||||
}
|
||||
|
||||
func reloadHooks(hooksFilePath string) {
|
||||
hooksInFile := hook.Hooks{}
|
||||
|
||||
// parse and swap
|
||||
log.Printf("attempting to reload hooks from %s\n", hooksFilePath)
|
||||
|
||||
err := hooksInFile.LoadFromFile(hooksFilePath)
|
||||
err := hooksInFile.LoadFromFile(hooksFilePath, *asTemplate)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("couldn't load hooks from file! %+v\n", err)
|
||||
@@ -393,7 +483,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
|
||||
|
||||
+159
-124
@@ -13,6 +13,7 @@ import (
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
@@ -21,15 +22,26 @@ import (
|
||||
)
|
||||
|
||||
func TestStaticParams(t *testing.T) {
|
||||
// FIXME(moorereason): incorporate this test into TestWebhook.
|
||||
// Need to be able to execute a binary with a space in the filename.
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Skipping on Windows")
|
||||
}
|
||||
|
||||
spHeaders := make(map[string]interface{})
|
||||
spHeaders["User-Agent"] = "curl/7.54.0"
|
||||
spHeaders["Accept"] = "*/*"
|
||||
|
||||
// case 1: correct entry
|
||||
// case 2: binary with spaces in its name
|
||||
err := os.Symlink("/bin/echo", "/tmp/with space")
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
defer os.Remove("/tmp/with space")
|
||||
|
||||
spHook := &hook.Hook{
|
||||
ID: "static-params-ok",
|
||||
ExecuteCommand: "/bin/echo",
|
||||
ID: "static-params-name-space",
|
||||
ExecuteCommand: "/tmp/with space",
|
||||
CommandWorkingDirectory: "/tmp",
|
||||
ResponseMessage: "success",
|
||||
CaptureCommandOutput: true,
|
||||
@@ -41,159 +53,125 @@ func TestStaticParams(t *testing.T) {
|
||||
b := &bytes.Buffer{}
|
||||
log.SetOutput(b)
|
||||
|
||||
s, err := handleHook(spHook, &spHeaders, &map[string]interface{}{}, &map[string]interface{}{}, &[]byte{})
|
||||
_, err = handleHook(spHook, "test", &spHeaders, &map[string]interface{}{}, &map[string]interface{}{}, &[]byte{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v\n", err)
|
||||
}
|
||||
matched, _ := regexp.MatchString("(?s).*command output: passed.*static-params-ok", b.String())
|
||||
if !matched {
|
||||
t.Fatalf("Unexpected log output:\n%s", b)
|
||||
}
|
||||
|
||||
// case 2: binary with spaces in its name
|
||||
err = os.Symlink("/bin/true", "/tmp/with space")
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
defer os.Remove("/tmp/with space")
|
||||
|
||||
spHook = &hook.Hook{
|
||||
ID: "static-params-name-space",
|
||||
ExecuteCommand: "/tmp/with space",
|
||||
CommandWorkingDirectory: "/tmp",
|
||||
ResponseMessage: "success",
|
||||
CaptureCommandOutput: true,
|
||||
PassArgumentsToCommand: []hook.Argument{
|
||||
hook.Argument{Source: "string", Name: "passed"},
|
||||
},
|
||||
}
|
||||
|
||||
b = &bytes.Buffer{}
|
||||
log.SetOutput(b)
|
||||
|
||||
s, err = handleHook(spHook, &spHeaders, &map[string]interface{}{}, &map[string]interface{}{}, &[]byte{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v\n", err)
|
||||
}
|
||||
matched, _ = regexp.MatchString("(?s)command output: .*static-params-name-space", b.String())
|
||||
matched, _ := regexp.MatchString("(?s)command output: .*static-params-name-space", b.String())
|
||||
if !matched {
|
||||
t.Fatalf("Unexpected log output:\n%sn", b)
|
||||
}
|
||||
|
||||
// case 3: parameters specified in execute-command
|
||||
spHook = &hook.Hook{
|
||||
ID: "static-params-bad",
|
||||
ExecuteCommand: "/bin/echo success",
|
||||
CommandWorkingDirectory: "/tmp",
|
||||
ResponseMessage: "success",
|
||||
CaptureCommandOutput: true,
|
||||
PassArgumentsToCommand: []hook.Argument{
|
||||
hook.Argument{Source: "string", Name: "failed"},
|
||||
},
|
||||
}
|
||||
|
||||
b = &bytes.Buffer{}
|
||||
log.SetOutput(b)
|
||||
|
||||
s, err = handleHook(spHook, &spHeaders, &map[string]interface{}{}, &map[string]interface{}{}, &[]byte{})
|
||||
if err == nil {
|
||||
t.Fatalf("Error expected, but none returned: %s\n", s)
|
||||
}
|
||||
matched, _ = regexp.MatchString("(?s)unable to locate command: ..bin.echo success.*use 'pass-arguments-to-command'", b.String())
|
||||
if !matched {
|
||||
t.Fatalf("Unexpected log output:\n%s\n", b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhook(t *testing.T) {
|
||||
hookecho, cleanupHookecho := buildHookecho(t)
|
||||
defer cleanupHookecho()
|
||||
|
||||
webhook, cleanupWebhookFn := buildWebhook(t)
|
||||
defer cleanupWebhookFn()
|
||||
|
||||
for _, hookTmpl := range []string{"test/hooks.json.tmpl", "test/hooks.yaml.tmpl"} {
|
||||
config, cleanupConfig := genConfig(t, hookecho, hookTmpl)
|
||||
defer cleanupConfig()
|
||||
|
||||
webhook, cleanupWebhook := buildWebhook(t)
|
||||
defer cleanupWebhook()
|
||||
|
||||
ip, port := serverAddress(t)
|
||||
args := []string{fmt.Sprintf("-hooks=%s", config), fmt.Sprintf("-ip=%s", ip), fmt.Sprintf("-port=%s", port), "-verbose"}
|
||||
|
||||
cmd := exec.Command(webhook, args...)
|
||||
//cmd.Stderr = os.Stderr // uncomment to see verbose output
|
||||
cmd.Env = webhookEnv()
|
||||
cmd.Args[0] = "webhook"
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("failed to start webhook: %s", err)
|
||||
}
|
||||
defer killAndWait(cmd)
|
||||
|
||||
waitForServerReady(t, ip, port)
|
||||
configPath, cleanupConfigFn := genConfig(t, hookecho, hookTmpl)
|
||||
defer cleanupConfigFn()
|
||||
|
||||
for _, tt := range hookHandlerTests {
|
||||
url := fmt.Sprintf("http://%s:%s/hooks/%s", ip, port, tt.id)
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
ip, port := serverAddress(t)
|
||||
args := []string{fmt.Sprintf("-hooks=%s", configPath), fmt.Sprintf("-ip=%s", ip), fmt.Sprintf("-port=%s", port), "-verbose"}
|
||||
|
||||
req, err := http.NewRequest("POST", url, ioutil.NopCloser(strings.NewReader(tt.body)))
|
||||
if err != nil {
|
||||
t.Errorf("New request failed: %s", err)
|
||||
}
|
||||
// Setup a buffer for capturing webhook logs for later evaluation
|
||||
b := &buffer{}
|
||||
|
||||
for k, v := range tt.headers {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
cmd := exec.Command(webhook, args...)
|
||||
cmd.Stderr = b
|
||||
cmd.Env = webhookEnv()
|
||||
cmd.Args[0] = "webhook"
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("failed to start webhook: %s", err)
|
||||
}
|
||||
defer killAndWait(cmd)
|
||||
|
||||
var res *http.Response
|
||||
waitForServerReady(t, ip, port)
|
||||
|
||||
if tt.urlencoded == true {
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
} else {
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
}
|
||||
url := fmt.Sprintf("http://%s:%s/hooks/%s", ip, port, tt.id)
|
||||
|
||||
client := &http.Client{}
|
||||
res, err = client.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("client.Do failed: %s", err)
|
||||
}
|
||||
req, err := http.NewRequest("POST", url, ioutil.NopCloser(strings.NewReader(tt.body)))
|
||||
if err != nil {
|
||||
t.Errorf("New request failed: %s", err)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
t.Errorf("POST %q: failed to ready body: %s", tt.desc, err)
|
||||
}
|
||||
for k, v := range tt.headers {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
|
||||
if res.StatusCode != tt.respStatus || string(body) != tt.respBody {
|
||||
t.Errorf("failed %q (id: %s):\nexpected status: %#v, response: %s\ngot status: %#v, response: %s", tt.desc, tt.id, tt.respStatus, tt.respBody, res.StatusCode, body)
|
||||
}
|
||||
var res *http.Response
|
||||
|
||||
if tt.urlencoded == true {
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
} else {
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
res, err = client.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("client.Do failed: %s", err)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
t.Errorf("POST %q: failed to ready body: %s", tt.desc, err)
|
||||
}
|
||||
|
||||
if res.StatusCode != tt.respStatus || string(body) != tt.respBody {
|
||||
t.Errorf("failed %q (id: %s):\nexpected status: %#v, response: %s\ngot status: %#v, response: %s", tt.desc, tt.id, tt.respStatus, tt.respBody, res.StatusCode, body)
|
||||
}
|
||||
|
||||
if tt.logMatch == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// There's the potential for a race condition below where we
|
||||
// try to read the logs buffer b before the logs have been
|
||||
// flushed by the webhook process. Kill the process to flush
|
||||
// the logs.
|
||||
killAndWait(cmd)
|
||||
|
||||
matched, _ := regexp.MatchString(tt.logMatch, b.String())
|
||||
if !matched {
|
||||
t.Errorf("failed log match for %q (id: %s):\nmatch pattern: %q\ngot:\n%s", tt.desc, tt.id, tt.logMatch, b)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildHookecho(t *testing.T) (bin string, cleanup func()) {
|
||||
func buildHookecho(t *testing.T) (binPath string, cleanupFn func()) {
|
||||
tmp, err := ioutil.TempDir("", "hookecho-test-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if cleanup == nil {
|
||||
if cleanupFn == nil {
|
||||
os.RemoveAll(tmp)
|
||||
}
|
||||
}()
|
||||
|
||||
bin = filepath.Join(tmp, "hookecho")
|
||||
binPath = filepath.Join(tmp, "hookecho")
|
||||
if runtime.GOOS == "windows" {
|
||||
bin += ".exe"
|
||||
binPath += ".exe"
|
||||
}
|
||||
|
||||
cmd := exec.Command("go", "build", "-o", bin, "test/hookecho.go")
|
||||
cmd := exec.Command("go", "build", "-o", binPath, "test/hookecho.go")
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("Building hookecho: %v", err)
|
||||
}
|
||||
|
||||
return bin, func() { os.RemoveAll(tmp) }
|
||||
return binPath, func() { os.RemoveAll(tmp) }
|
||||
}
|
||||
|
||||
func genConfig(t *testing.T, bin string, hookTemplate string) (config string, cleanup func()) {
|
||||
func genConfig(t *testing.T, bin string, hookTemplate string) (configPath string, cleanupFn func()) {
|
||||
tmpl := template.Must(template.ParseFiles(hookTemplate))
|
||||
|
||||
tmp, err := ioutil.TempDir("", "webhook-config-")
|
||||
@@ -201,7 +179,7 @@ func genConfig(t *testing.T, bin string, hookTemplate string) (config string, cl
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if cleanup == nil {
|
||||
if cleanupFn == nil {
|
||||
os.RemoveAll(tmp)
|
||||
}
|
||||
}()
|
||||
@@ -215,7 +193,11 @@ func genConfig(t *testing.T, bin string, hookTemplate string) (config string, cl
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
data := struct{ Hookecho string }{filepath.ToSlash(bin)}
|
||||
data := struct{ Hookecho string }{filepath.FromSlash(bin)}
|
||||
if runtime.GOOS == "windows" {
|
||||
// Simulate escaped backslashes on Windows.
|
||||
data.Hookecho = strings.Replace(data.Hookecho, `\`, `\\`, -1)
|
||||
}
|
||||
if err := tmpl.Execute(file, data); err != nil {
|
||||
t.Fatalf("Executing template: %v", err)
|
||||
}
|
||||
@@ -223,28 +205,28 @@ func genConfig(t *testing.T, bin string, hookTemplate string) (config string, cl
|
||||
return path, func() { os.RemoveAll(tmp) }
|
||||
}
|
||||
|
||||
func buildWebhook(t *testing.T) (bin string, cleanup func()) {
|
||||
func buildWebhook(t *testing.T) (binPath string, cleanupFn func()) {
|
||||
tmp, err := ioutil.TempDir("", "webhook-test-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if cleanup == nil {
|
||||
if cleanupFn == nil {
|
||||
os.RemoveAll(tmp)
|
||||
}
|
||||
}()
|
||||
|
||||
bin = filepath.Join(tmp, "webhook")
|
||||
binPath = filepath.Join(tmp, "webhook")
|
||||
if runtime.GOOS == "windows" {
|
||||
bin += ".exe"
|
||||
binPath += ".exe"
|
||||
}
|
||||
|
||||
cmd := exec.Command("go", "build", "-o", bin)
|
||||
cmd := exec.Command("go", "build", "-o", binPath)
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("Building webhook: %v", err)
|
||||
}
|
||||
|
||||
return bin, func() { os.RemoveAll(tmp) }
|
||||
return binPath, func() { os.RemoveAll(tmp) }
|
||||
}
|
||||
|
||||
func serverAddress(t *testing.T) (string, string) {
|
||||
@@ -266,7 +248,7 @@ func serverAddress(t *testing.T) (string, string) {
|
||||
func waitForServerReady(t *testing.T, ip, port string) {
|
||||
waitForServer(t,
|
||||
fmt.Sprintf("http://%v:%v/", ip, port),
|
||||
http.StatusNotFound,
|
||||
http.StatusOK,
|
||||
5*time.Second)
|
||||
}
|
||||
|
||||
@@ -288,6 +270,10 @@ func waitForServer(t *testing.T, url string, status int, timeout time.Duration)
|
||||
}
|
||||
|
||||
func killAndWait(cmd *exec.Cmd) {
|
||||
if cmd == nil || cmd.ProcessState != nil && cmd.ProcessState.Exited() {
|
||||
return
|
||||
}
|
||||
|
||||
cmd.Process.Kill()
|
||||
cmd.Wait()
|
||||
}
|
||||
@@ -313,6 +299,7 @@ var hookHandlerTests = []struct {
|
||||
|
||||
respStatus int
|
||||
respBody string
|
||||
logMatch string
|
||||
}{
|
||||
{
|
||||
"github",
|
||||
@@ -467,6 +454,7 @@ var hookHandlerTests = []struct {
|
||||
`arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
|
||||
env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
|
||||
`,
|
||||
``,
|
||||
},
|
||||
{
|
||||
"bitbucket", // bitbucket sends their payload using uriencoded params.
|
||||
@@ -476,6 +464,7 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
|
||||
true,
|
||||
http.StatusOK,
|
||||
`success`,
|
||||
``,
|
||||
},
|
||||
{
|
||||
"gitlab",
|
||||
@@ -527,6 +516,7 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
|
||||
http.StatusOK,
|
||||
`arg: b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327 John Smith john@example.com
|
||||
`,
|
||||
``,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -567,6 +557,7 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
|
||||
`arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
|
||||
env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
|
||||
`,
|
||||
``,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -605,12 +596,56 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
|
||||
http.StatusOK,
|
||||
`arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
|
||||
`,
|
||||
``,
|
||||
},
|
||||
|
||||
// test with custom return code
|
||||
{"empty payload", "github", nil, `{}`, false, http.StatusBadRequest, `Hook rules were not satisfied.`},
|
||||
{"empty payload", "github", nil, `{}`, false, http.StatusBadRequest, `Hook rules were not satisfied.`, ``},
|
||||
// test with custom invalid http code, should default to 200 OK
|
||||
{"empty payload", "bitbucket", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`},
|
||||
{"empty payload", "bitbucket", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`, ``},
|
||||
// test with no configured http return code, should default to 200 OK
|
||||
{"empty payload", "gitlab", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`},
|
||||
{"empty payload", "gitlab", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`, ``},
|
||||
|
||||
// test capturing command output
|
||||
{"don't capture output on success by default", "capture-command-output-on-success-not-by-default", nil, `{}`, false, http.StatusOK, ``, ``},
|
||||
{"capture output on success with flag set", "capture-command-output-on-success-yes-with-flag", nil, `{}`, false, http.StatusOK, `arg: exit=0
|
||||
`, ``},
|
||||
{"don't capture output on error by default", "capture-command-output-on-error-not-by-default", nil, `{}`, false, http.StatusInternalServerError, `Error occurred while executing the hook's command. Please check your logs for more details.`, ``},
|
||||
{"capture output on error with extra flag set", "capture-command-output-on-error-yes-with-extra-flag", nil, `{}`, false, http.StatusInternalServerError, `arg: exit=1
|
||||
`, ``},
|
||||
|
||||
// Check logs
|
||||
{"static params should pass", "static-params-ok", nil, `{}`, false, http.StatusOK, "arg: passed\n", `(?s)command output: arg: passed`},
|
||||
{"command with space logs warning", "warn-on-space", nil, `{}`, false, http.StatusInternalServerError, "Error occurred while executing the hook's command. Please check your logs for more details.", `(?s)unable to locate command.*use 'pass[-]arguments[-]to[-]command' to specify args`},
|
||||
{"unsupported content type error", "github", map[string]string{"Content-Type": "nonexistent/format"}, `{}`, false, http.StatusBadRequest, `Hook rules were not satisfied.`, `(?s)error parsing body payload due to unsupported content type header:`},
|
||||
}
|
||||
|
||||
// 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