Compare commits

..

60 Commits

Author SHA1 Message Date
Adnan Hajdarevic 8fe6c9a05d Update version to 2.6.11 2019-12-15 14:30:54 +01:00
Adnan Hajdarević 7c4e6e94fc Merge pull request #365 from moorereason/feature/364-constant-time
Use constant time string compare for match value
2019-12-11 07:06:12 +01:00
Cameron Moore 31e76bcd00 Use constant time string compare for match value
Fixes #364
2019-12-10 22:22:13 -06:00
Adnan Hajdarević c47c06e822 Merge pull request #363 from moorereason/feature/arm64-travisci
Add arm64 to Travis CI
2019-12-10 00:23:38 +01:00
Cameron Moore bf3d042da6 Use master instead of tip 2019-12-09 16:57:55 -06:00
Cameron Moore d05911cdcb Add arm64 to Travis CI 2019-12-09 16:50:19 -06:00
Adnan Hajdarević 634ca84807 Merge pull request #362 from moorereason/feature/cipher-suites
Feature/cipher suites
2019-12-09 21:52:25 +01:00
Cameron Moore 8c46a8343b Document minimum Go release 2019-12-05 17:01:38 -06:00
Cameron Moore 13d5630e80 Update docs for TLS version and cipher suite options 2019-12-03 21:36:14 -06:00
Cameron Moore f1003560f1 Add list cipher suites support 2019-12-03 21:35:16 -06:00
Cameron Moore 997db04b9f Require Go 1.12 or newer
Go 1.11 does not support TLS 1.3.  To simplify cipher suite selection,
we now require at least Go 1.12.
2019-12-03 21:31:23 -06:00
Cameron Moore 769e743563 Add missing files 2019-12-03 15:21:18 -06:00
Cameron Moore 43f519a712 Add TLS version and cipher suites options
Default to TLS 1.2 and secure cipher suites.

Built for Go 1.13. Code in cipher_suites.go taken from Go tip commit
0ee22d9, which is scheduled for the upcoming Go 1.14 release.  Once Go
1.14 is released, we can remove this file and use the stdlib.

Fixes #244
2019-12-03 15:13:12 -06:00
Adnan Hajdarević a617b1a6ac Merge pull request #361 from adnanh/feature/check-payload-hash-sha512
Add SHA512 payload check rule
2019-12-02 22:34:07 +01:00
Adnan Hajdarević 9117f4f6d6 Merge pull request #360 from adnanh/improvement/content-type-based-payload-parsing
Fix invalid assumption that multipart forms can be parsed in te same way as urlencoded forms.
2019-12-02 22:33:45 +01:00
Adnan Hajdarevic b53996f175 Add tests for SHA512 payload hash check rule. 2019-12-02 19:49:56 +01:00
Adnan Hajdarevic 154177e46a Add documentation for SHA512 payload hash check rule. 2019-12-02 19:49:34 +01:00
Adnan Hajdarevic d4e98281d7 Add SHA512 payload check rule. 2019-12-02 19:48:59 +01:00
Adnan Hajdarevic ce186487f4 Format the file using go fmt. 2019-12-02 19:03:38 +01:00
Adnan Hajdarevic 1110f82443 Add test for unsupported content type error message. 2019-12-02 19:01:20 +01:00
Adnan Hajdarevic a99abd4e6f Fix invalid assumption in code that multipart forms can be parsed in the same way as urlencoded forms.
Refactored code to use switch-case statement over the `Content-Type` header and log unsupported content types instead of silently failing.
Also made the `x-www-form-urlencoded` content type handler more specific (as opposed to the previous code which looked for `form` occurence in the value),
as we need to use different logic for multipart forms, which we'll hopefully implement soon.

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

Updates #290
2019-01-02 16:50:23 -06:00
Cameron Moore 1a17dc83fe Return errors on empty secrets during signature validations
Fixes #207
2019-01-02 16:09:27 -06:00
Adnan Hajdarević 753734428f Merge pull request #283 from adnanh/healthcheck-route
Add `/` route handler to return 200 OK which can be used as a healthcheck endpoint
2018-11-17 19:03:20 +01:00
Adnan Hajdarevic f76426e9b0 add handler for the route to be used as a healtcheck endpoint, fixes #233 2018-11-17 19:01:26 +01:00
Adnan Hajdarević 5803d5e849 Merge pull request #282 from adnanh/master
Backmerge
2018-11-16 18:59:25 +01:00
Adnan Hajdarević 385898b21f Merge pull request #281 from ruliezz/patch-1
Update docs with a Gitea example
2018-11-16 18:58:58 +01:00
ruliezz ce7f8d5d28 Updated with a Gitea example 2018-11-16 15:23:39 +01:00
Adnan Hajdarević b2899d1d3e Update README.md 2018-11-16 10:46:35 +01:00
Adnan Hajdarević e8628cd662 Add more guides to the README 2018-11-16 10:45:37 +01:00
Andreas Lundblad 54a7190113 Forgot a rename in previous refactoring. 2018-09-17 20:41:51 +02:00
Andreas Lundblad b65bdbbb24 Removed trailing tab 2018-09-17 20:35:51 +02:00
Andreas Lundblad ef3f43f89f Added SuccessHttpResponseCode handling for case when capture output is set to true. 2018-09-15 16:06:18 +02:00
Andreas Lundblad 22073d8847 Renamed http-response-code to success-http-response-code 2018-09-15 16:00:42 +02:00
Andreas Lundblad c05ca8c528 Added HttpResponseCode hook setting 2018-09-15 15:55:28 +02:00
14 changed files with 944 additions and 401 deletions
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
open_collective: webhook
+16 -4
View File
@@ -1,18 +1,30 @@
language: go language: go
go: go:
- 1.8.x - 1.12.x
- 1.9.x - 1.13.x
- tip - master
os: os:
- linux - linux
- osx - osx
- windows
arch:
- amd64
- arm64
matrix: matrix:
fast_finish: true fast_finish: true
allow_failures: allow_failures:
- go: tip - go: master
exclude:
- os: windows
go: master
- os: windows
arch: arm64
- os: osx
arch: arm64
install: install:
- go get -d -v -t ./... - go get -d -v -t ./...
+20 -2
View File
@@ -26,13 +26,16 @@ If you don't have time to waste configuring, hosting, debugging and maintaining
# Getting started # Getting started
## Installation ## Installation
### Building from source ### 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 ```bash
$ go get github.com/adnanh/webhook $ go get github.com/adnanh/webhook
``` ```
to get the latest version of the [webhook][w]. to get the latest version of the [webhook][w].
### Using package manager ### Using package manager
#### Snap store
[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-white.svg)](https://snapcraft.io/webhook)
#### Ubuntu #### 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. 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.
@@ -80,6 +83,8 @@ However, hook defined like that could pose a security threat to your system, bec
## Using HTTPS ## 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. [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.
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 ## 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. 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.
@@ -93,7 +98,20 @@ Check out [Hook examples page](docs/Hook-Examples.md) for more complex examples
- [Webhook & JIRA](https://sites.google.com/site/mrxpalmeiras/notes/jira-webhooks) by [@perfecto25](https://github.com/perfecto25) - [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/) - [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) - [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 ## 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. 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.
-15
View File
@@ -1,15 +0,0 @@
clone_folder: C:\gopath\src\github.com\adnanh\webhook
environment:
GOPATH: c:\gopath
install:
- set PATH=%PATH%;C:\MinGW\bin;%GOPATH%\bin
- echo %PATH%
- echo %GOPATH%
- go version
- go env
- go get -d -v -t ./...
build_script:
- go test -v -race ./...
+102
View File
@@ -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)
}
+2
View File
@@ -8,6 +8,8 @@ Hooks are defined as JSON objects. Please note that in order to be considered va
* `command-working-directory` - specifies the working directory that will be used for the script when it's executed * `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-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 * `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` - 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`. * `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`. * `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`.
+398 -296
View File
@@ -1,296 +1,351 @@
# Hook examples # Hook examples
This page is still work in progress. Feel free to contribute! This page is still work in progress. Feel free to contribute!
## Incoming Github webhook ## Incoming Github webhook
```json ```json
[ [
{ {
"id": "webhook", "id": "webhook",
"execute-command": "/home/adnan/redeploy-go-webhook.sh", "execute-command": "/home/adnan/redeploy-go-webhook.sh",
"command-working-directory": "/home/adnan/go", "command-working-directory": "/home/adnan/go",
"pass-arguments-to-command": "pass-arguments-to-command":
[ [
{ {
"source": "payload", "source": "payload",
"name": "head_commit.id" "name": "head_commit.id"
}, },
{ {
"source": "payload", "source": "payload",
"name": "pusher.name" "name": "pusher.name"
}, },
{ {
"source": "payload", "source": "payload",
"name": "pusher.email" "name": "pusher.email"
} }
], ],
"trigger-rule": "trigger-rule":
{ {
"and": "and":
[ [
{ {
"match": "match":
{ {
"type": "payload-hash-sha1", "type": "payload-hash-sha1",
"secret": "mysecret", "secret": "mysecret",
"parameter": "parameter":
{ {
"source": "header", "source": "header",
"name": "X-Hub-Signature" "name": "X-Hub-Signature"
} }
} }
}, },
{ {
"match": "match":
{ {
"type": "value", "type": "value",
"value": "refs/heads/master", "value": "refs/heads/master",
"parameter": "parameter":
{ {
"source": "payload", "source": "payload",
"name": "ref" "name": "ref"
} }
} }
} }
] ]
} }
} }
] ]
``` ```
## Incoming Bitbucket webhook ## 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`: 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 ```json
[ [
{ {
"id": "webhook", "id": "webhook",
"execute-command": "/home/adnan/redeploy-go-webhook.sh", "execute-command": "/home/adnan/redeploy-go-webhook.sh",
"command-working-directory": "/home/adnan/go", "command-working-directory": "/home/adnan/go",
"pass-arguments-to-command": "pass-arguments-to-command":
[ [
{ {
"source": "payload", "source": "payload",
"name": "actor.username" "name": "actor.username"
} }
], ],
"trigger-rule": "trigger-rule":
{ {
"match": "match":
{ {
"type": "ip-whitelist", "type": "ip-whitelist",
"ip-range": "104.192.143.0/24" "ip-range": "104.192.143.0/24"
} }
} }
} }
] ]
``` ```
## Incoming Gitlab Webhook ## Incoming Gitlab Webhook
Gitlab provides webhooks for many kinds of events. 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) 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: Values in the request body can be accessed in the command or to the match rule by referencing 'payload' as the source:
```json ```json
[ [
{ {
"id": "redeploy-webhook", "id": "redeploy-webhook",
"execute-command": "/home/adnan/redeploy-go-webhook.sh", "execute-command": "/home/adnan/redeploy-go-webhook.sh",
"command-working-directory": "/home/adnan/go", "command-working-directory": "/home/adnan/go",
"pass-arguments-to-command": "pass-arguments-to-command":
[ [
{ {
"source": "payload", "source": "payload",
"name": "user_name" "name": "user_name"
} }
], ],
"response-message": "Executing redeploy script", "response-message": "Executing redeploy script",
"trigger-rule": "trigger-rule":
{ {
"match": "match":
{ {
"type": "value", "type": "value",
"value": "<YOUR-GENERATED-TOKEN>", "value": "<YOUR-GENERATED-TOKEN>",
"parameter": "parameter":
{ {
"source": "header", "source": "header",
"name": "X-Gitlab-Token" "name": "X-Gitlab-Token"
} }
} }
} }
} }
] ]
``` ```
## Incoming Gogs webhook ## Incoming Gogs webhook
```json ```json
[ [
{ {
"id": "webhook", "id": "webhook",
"execute-command": "/home/adnan/redeploy-go-webhook.sh", "execute-command": "/home/adnan/redeploy-go-webhook.sh",
"command-working-directory": "/home/adnan/go", "command-working-directory": "/home/adnan/go",
"pass-arguments-to-command": "pass-arguments-to-command":
[ [
{ {
"source": "payload", "source": "payload",
"name": "head_commit.id" "name": "head_commit.id"
}, },
{ {
"source": "payload", "source": "payload",
"name": "pusher.name" "name": "pusher.name"
}, },
{ {
"source": "payload", "source": "payload",
"name": "pusher.email" "name": "pusher.email"
} }
], ],
"trigger-rule": "trigger-rule":
{ {
"and": "and":
[ [
{ {
"match": "match":
{ {
"type": "payload-hash-sha256", "type": "payload-hash-sha256",
"secret": "mysecret", "secret": "mysecret",
"parameter": "parameter":
{ {
"source": "header", "source": "header",
"name": "X-Gogs-Signature" "name": "X-Gogs-Signature"
} }
} }
}, },
{ {
"match": "match":
{ {
"type": "value", "type": "value",
"value": "refs/heads/master", "value": "refs/heads/master",
"parameter": "parameter":
{ {
"source": "payload", "source": "payload",
"name": "ref" "name": "ref"
} }
} }
} }
] ]
} }
} }
] ]
``` ```
## Incoming Gitea webhook
## Slack slash command ```json
```json [
[ {
{ "id": "webhook",
"id": "redeploy-webhook", "execute-command": "/home/adnan/redeploy-go-webhook.sh",
"execute-command": "/home/adnan/redeploy-go-webhook.sh", "command-working-directory": "/home/adnan/go",
"command-working-directory": "/home/adnan/go", "pass-arguments-to-command":
"response-message": "Executing redeploy script", [
"trigger-rule": {
{ "source": "payload",
"match": "name": "head_commit.id"
{ },
"type": "value", {
"value": "<YOUR-GENERATED-TOKEN>", "source": "payload",
"parameter": "name": "pusher.name"
{ },
"source": "payload", {
"name": "token" "source": "payload",
} "name": "pusher.email"
} }
} ],
} "trigger-rule":
] {
``` "and":
[
## A simple webhook with a secret key in GET query {
"match":
__Not recommended in production due to low security__ {
"type": "value",
`example.com:9000/hooks/simple-one` - won't work "value": "mysecret",
`example.com:9000/hooks/simple-one?token=42` - will work "parameter":
{
```json "source": "payload",
[ "name": "secret"
{ }
"id": "simple-one", }
"execute-command": "/path/to/command.sh", },
"response-message": "Executing simple webhook...", {
"trigger-rule": "match":
{ {
"match": "type": "value",
{ "value": "refs/heads/master",
"type": "value", "parameter":
"value": "42", {
"parameter": "source": "payload",
{ "name": "ref"
"source": "url", }
"name": "token" }
} }
} ]
} }
} }
] ]
``` ```
# JIRA Webhooks ## Slack slash command
[Guide by @perfecto25](https://sites.google.com/site/mrxpalmeiras/notes/jira-webhooks) ```json
[
# Pass File-to-command sample {
"id": "redeploy-webhook",
## Webhook configuration "execute-command": "/home/adnan/redeploy-go-webhook.sh",
"command-working-directory": "/home/adnan/go",
<pre> "response-message": "Executing redeploy script",
[ "trigger-rule":
{ {
"id": "test-file-webhook", "match":
"execute-command": "/bin/ls", {
"command-working-directory": "/tmp", "type": "value",
"pass-file-to-command": "value": "<YOUR-GENERATED-TOKEN>",
[ "parameter":
{ {
"source": "payload", "source": "payload",
"name": "binary", "name": "token"
"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 ```
}
] ## A simple webhook with a secret key in GET query
</pre>
__Not recommended in production due to low security__
## Sample client usage
`example.com:9000/hooks/simple-one` - won't work
Store the following file as `testRequest.json`. `example.com:9000/hooks/simple-one?token=42` - will work
<pre> ```json
{"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> {
"id": "simple-one",
use then the curl tool to execute a request to the webhook. "execute-command": "/path/to/command.sh",
"response-message": "Executing simple webhook...",
<pre> "trigger-rule":
#!/bin/bash {
curl -H "Content-Type:application/json" -X POST -d @testRequest.json \ "match":
http://localhost:9000/hooks/test-file-webhook {
</pre> "type": "value",
"value": "42",
or in a single line, using https://github.com/jpmens/jo to generate the JSON code "parameter":
<pre> {
jo binary=%filename.zip | curl -H "Content-Type:application/json" -X POST -d @- \ "source": "url",
http://localhost:9000/hooks/test-file-webhook "name": "token"
</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) # JIRA 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". [Guide by @perfecto25](https://sites.google.com/site/mrxpalmeiras/notes/jira-webhooks)
```json # 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", "id": "redeploy-webhook",
@@ -319,6 +374,53 @@ In order to leverage the Signing Key for addtional authentication/security you m
} }
] ]
} }
] ]
``` ```
## 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"
}
}
}
]
}
}
]
```
+34 -2
View File
@@ -186,7 +186,39 @@ For the regex syntax, check out <http://golang.org/pkg/regexp/syntax/>
} }
``` ```
### 4. Match Whitelisted IP range ### 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`. 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`.
@@ -200,7 +232,7 @@ The IP can be IPv4- or IPv6-formatted, using [CIDR notation](https://en.wikipedi
} }
``` ```
### 5. Match scalr-signature ### 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. 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. A unqiue signing key is generated for each webhook endpoint URL you register in Scalr.
+7 -1
View File
@@ -3,6 +3,8 @@
Usage of webhook: Usage of webhook:
-cert string -cert string
path to the HTTPS certificate pem file (default "cert.pem") path to the HTTPS certificate pem file (default "cert.pem")
-cipher-suites string
comma-separated list of supported TLS cipher suites
-header value -header value
response header to return, specified in format name=value, use multiple times to set multiple headers response header to return, specified in format name=value, use multiple times to set multiple headers
-hooks value -hooks value
@@ -13,6 +15,8 @@ Usage of webhook:
ip the webhook should serve hooks on (default "0.0.0.0") ip the webhook should serve hooks on (default "0.0.0.0")
-key string -key string
path to the HTTPS certificate private key pem file (default "key.pem") path to the HTTPS certificate private key pem file (default "key.pem")
-list-cipher-suites
list available TLS cipher suites
-nopanic -nopanic
do not panic if hooks cannot be loaded when webhook is not running in verbose mode do not panic if hooks cannot be loaded when webhook is not running in verbose mode
-port int -port int
@@ -21,6 +25,8 @@ Usage of webhook:
use HTTPS instead of HTTP use HTTPS instead of HTTP
-template -template
parse hooks file as a Go 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 -urlprefix string
url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id) (default "hooks") url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id) (default "hooks")
-verbose -verbose
@@ -35,4 +41,4 @@ Use any of the above specified flags to override their default behavior.
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. 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 ```bash
kill -USR1 webhookpid kill -USR1 webhookpid
``` ```
+91 -50
View File
@@ -5,6 +5,8 @@ import (
"crypto/hmac" "crypto/hmac"
"crypto/sha1" "crypto/sha1"
"crypto/sha256" "crypto/sha256"
"crypto/sha512"
"crypto/subtle"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
@@ -94,6 +96,10 @@ func (e *ParseError) Error() string {
// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload // CheckPayloadSignature calculates and verifies SHA1 signature of the given payload
func CheckPayloadSignature(payload []byte, secret string, signature string) (string, error) { func CheckPayloadSignature(payload []byte, secret string, signature string) (string, error) {
if secret == "" {
return "", errors.New("signature validation secret can not be empty")
}
signature = strings.TrimPrefix(signature, "sha1=") signature = strings.TrimPrefix(signature, "sha1=")
mac := hmac.New(sha1.New, []byte(secret)) mac := hmac.New(sha1.New, []byte(secret))
@@ -111,6 +117,10 @@ func CheckPayloadSignature(payload []byte, secret string, signature string) (str
// CheckPayloadSignature256 calculates and verifies SHA256 signature of the given payload // CheckPayloadSignature256 calculates and verifies SHA256 signature of the given payload
func CheckPayloadSignature256(payload []byte, secret string, signature string) (string, error) { func CheckPayloadSignature256(payload []byte, secret string, signature string) (string, error) {
if secret == "" {
return "", errors.New("signature validation secret can not be empty")
}
signature = strings.TrimPrefix(signature, "sha256=") signature = strings.TrimPrefix(signature, "sha256=")
mac := hmac.New(sha256.New, []byte(secret)) mac := hmac.New(sha256.New, []byte(secret))
@@ -126,6 +136,27 @@ func CheckPayloadSignature256(payload []byte, secret string, signature string) (
return expectedMAC, err 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) { func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey string, checkDate bool) (bool, error) {
// Check for the signature and date headers // Check for the signature and date headers
if _, ok := headers["X-Signature"]; !ok { if _, ok := headers["X-Signature"]; !ok {
@@ -134,6 +165,10 @@ func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey
if _, ok := headers["Date"]; !ok { if _, ok := headers["Date"]; !ok {
return false, nil return false, nil
} }
if signingKey == "" {
return false, errors.New("signature validation signing key can not be empty")
}
providedSignature := headers["X-Signature"].(string) providedSignature := headers["X-Signature"].(string)
dateHeader := headers["Date"].(string) dateHeader := headers["Date"].(string)
mac := hmac.New(sha1.New, []byte(signingKey)) mac := hmac.New(sha1.New, []byte(signingKey))
@@ -150,7 +185,6 @@ func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey
} }
// Example format: Fri 08 Sep 2017 11:24:32 UTC // Example format: Fri 08 Sep 2017 11:24:32 UTC
date, err := time.Parse("Mon 02 Jan 2006 15:04:05 MST", dateHeader) date, err := time.Parse("Mon 02 Jan 2006 15:04:05 MST", dateHeader)
//date, err := time.Parse(time.RFC1123, dateHeader)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -168,41 +202,37 @@ func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey
func CheckIPWhitelist(remoteAddr string, ipRange string) (bool, error) { func CheckIPWhitelist(remoteAddr string, ipRange string) (bool, error) {
// Extract IP address from remote address. // Extract IP address from remote address.
ip := remoteAddr // IPv6 addresses will likely be surrounded by [].
ip := strings.Trim(remoteAddr, " []")
if strings.LastIndex(remoteAddr, ":") != -1 { if i := strings.LastIndex(ip, ":"); i != -1 {
ip = remoteAddr[0:strings.LastIndex(remoteAddr, ":")] ip = ip[:i]
ip = strings.Trim(ip, " []")
} }
ip = strings.TrimSpace(ip) parsedIP := net.ParseIP(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))
if parsedIP == nil { if parsedIP == nil {
return false, fmt.Errorf("invalid IP address found in remote address '%s'", remoteAddr) 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.Contains(ipRange, "/") { _, cidr, err := net.ParseCIDR(r)
ipRange = ipRange + "/32" if err != nil {
return false, err
}
if cidr.Contains(parsedIP) {
return true, nil
}
} }
_, cidr, err := net.ParseCIDR(ipRange) return false, nil
if err != nil {
return false, err
}
return cidr.Contains(parsedIP), nil
} }
// ReplaceParameter replaces parameter value with the passed value in the passed map // ReplaceParameter replaces parameter value with the passed value in the passed map
@@ -215,7 +245,6 @@ func ReplaceParameter(s string, params interface{}, value interface{}) bool {
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice { if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 { if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
if p := strings.SplitN(s, ".", 2); len(p) > 1 { if p := strings.SplitN(s, ".", 2); len(p) > 1 {
index, err := strconv.ParseUint(p[0], 10, 64) index, err := strconv.ParseUint(p[0], 10, 64)
@@ -250,8 +279,12 @@ func GetParameter(s string, params interface{}) (interface{}, bool) {
return nil, false return nil, false
} }
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice { paramsValue := reflect.ValueOf(params)
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
switch paramsValue.Kind() {
case reflect.Slice:
paramsValueSliceLength := paramsValue.Len()
if paramsValueSliceLength > 0 {
if p := strings.SplitN(s, ".", 2); len(p) > 1 { if p := strings.SplitN(s, ".", 2); len(p) > 1 {
index, err := strconv.ParseUint(p[0], 10, 64) index, err := strconv.ParseUint(p[0], 10, 64)
@@ -273,18 +306,20 @@ func GetParameter(s string, params interface{}) (interface{}, bool) {
} }
return nil, false return nil, false
}
if p := strings.SplitN(s, ".", 2); len(p) > 1 { case reflect.Map:
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Map { // Check for raw key
if pValue, ok := params.(map[string]interface{})[p[0]]; ok { 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) return GetParameter(p[1], pValue)
} }
} else {
return nil, false
}
} else {
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
return pValue, true return pValue, true
} }
} }
@@ -327,7 +362,6 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
return ha.Name, true return ha.Name, true
case SourceEntirePayload: case SourceEntirePayload:
r, err := json.Marshal(payload) r, err := json.Marshal(payload)
if err != nil { if err != nil {
return "", false return "", false
} }
@@ -335,7 +369,6 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
return string(r), true return string(r), true
case SourceEntireHeaders: case SourceEntireHeaders:
r, err := json.Marshal(headers) r, err := json.Marshal(headers)
if err != nil { if err != nil {
return "", false return "", false
} }
@@ -343,7 +376,6 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
return string(r), true return string(r), true
case SourceEntireQuery: case SourceEntireQuery:
r, err := json.Marshal(query) r, err := json.Marshal(query)
if err != nil { if err != nil {
return "", false return "", false
} }
@@ -426,13 +458,14 @@ type Hook struct {
JSONStringParameters []Argument `json:"parse-parameters-as-json,omitempty"` JSONStringParameters []Argument `json:"parse-parameters-as-json,omitempty"`
TriggerRule *Rules `json:"trigger-rule,omitempty"` TriggerRule *Rules `json:"trigger-rule,omitempty"`
TriggerRuleMismatchHttpResponseCode int `json:"trigger-rule-mismatch-http-response-code,omitempty"` TriggerRuleMismatchHttpResponseCode int `json:"trigger-rule-mismatch-http-response-code,omitempty"`
IncomingPayloadContentType string `json:"incoming-payload-content-type,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 // ParseJSONParameters decodes specified arguments to JSON objects and replaces the
// string with the newly created object // string with the newly created object
func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface{}) []error { 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 { for i := range h.JSONStringParameters {
if arg, ok := h.JSONStringParameters[i].Get(headers, query, payload); ok { if arg, ok := h.JSONStringParameters[i].Get(headers, query, payload); ok {
@@ -442,7 +475,6 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
decoder.UseNumber() decoder.UseNumber()
err := decoder.Decode(&newArg) err := decoder.Decode(&newArg)
if err != nil { if err != nil {
errors = append(errors, &ParseError{err}) errors = append(errors, &ParseError{err})
continue continue
@@ -485,8 +517,8 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
// ExtractCommandArguments creates a list of arguments, based on the // ExtractCommandArguments creates a list of arguments, based on the
// PassArgumentsToCommand property that is ready to be used with exec.Command() // PassArgumentsToCommand property that is ready to be used with exec.Command()
func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) ([]string, []error) { func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) ([]string, []error) {
var args = make([]string, 0) args := make([]string, 0)
var errors = make([]error, 0) errors := make([]error, 0)
args = append(args, h.ExecuteCommand) args = append(args, h.ExecuteCommand)
@@ -510,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 // format, based on the PassEnvironmentToCommand property that is ready to be used
// with exec.Command(). // with exec.Command().
func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string]interface{}) ([]string, []error) { func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string]interface{}) ([]string, []error) {
var args = make([]string, 0) args := make([]string, 0)
var errors = make([]error, 0) errors := make([]error, 0)
for i := range h.PassEnvironmentToCommand { for i := range h.PassEnvironmentToCommand {
if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok { if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok {
if h.PassEnvironmentToCommand[i].EnvName != "" { if h.PassEnvironmentToCommand[i].EnvName != "" {
@@ -544,8 +576,8 @@ type FileParameter struct {
// format, based on the PassFileToCommand property that is ready to be used // format, based on the PassFileToCommand property that is ready to be used
// with exec.Command(). // with exec.Command().
func (h *Hook) ExtractCommandArgumentsForFile(headers, query, payload *map[string]interface{}) ([]FileParameter, []error) { func (h *Hook) ExtractCommandArgumentsForFile(headers, query, payload *map[string]interface{}) ([]FileParameter, []error) {
var args = make([]FileParameter, 0) args := make([]FileParameter, 0)
var errors = make([]error, 0) errors := make([]error, 0)
for i := range h.PassFileToCommand { for i := range h.PassFileToCommand {
if arg, ok := h.PassFileToCommand[i].Get(headers, query, payload); ok { if arg, ok := h.PassFileToCommand[i].Get(headers, query, payload); ok {
@@ -739,6 +771,7 @@ const (
MatchRegex string = "regex" MatchRegex string = "regex"
MatchHashSHA1 string = "payload-hash-sha1" MatchHashSHA1 string = "payload-hash-sha1"
MatchHashSHA256 string = "payload-hash-sha256" MatchHashSHA256 string = "payload-hash-sha256"
MatchHashSHA512 string = "payload-hash-sha512"
IPWhitelist string = "ip-whitelist" IPWhitelist string = "ip-whitelist"
ScalrSignature string = "scalr-signature" ScalrSignature string = "scalr-signature"
) )
@@ -755,7 +788,7 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod
if arg, ok := r.Parameter.Get(headers, query, payload); ok { if arg, ok := r.Parameter.Get(headers, query, payload); ok {
switch r.Type { switch r.Type {
case MatchValue: case MatchValue:
return arg == r.Value, nil return compare(arg, r.Value), nil
case MatchRegex: case MatchRegex:
return regexp.MatchString(r.Regex, arg) return regexp.MatchString(r.Regex, arg)
case MatchHashSHA1: case MatchHashSHA1:
@@ -764,11 +797,19 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod
case MatchHashSHA256: case MatchHashSHA256:
_, err := CheckPayloadSignature256(*body, r.Secret, arg) _, err := CheckPayloadSignature256(*body, r.Secret, arg)
return err == nil, err return err == nil, err
case MatchHashSHA512:
_, err := CheckPayloadSignature512(*body, r.Secret, arg)
return err == nil, err
} }
} }
return false, nil 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. // getenv provides a template function to retrieve OS environment variables.
func getenv(s string) string { func getenv(s string) string {
return os.Getenv(s) return os.Getenv(s)
+109 -4
View File
@@ -7,6 +7,38 @@ import (
"testing" "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 { var checkPayloadSignatureTests = []struct {
payload []byte payload []byte
secret string secret string
@@ -19,6 +51,7 @@ var checkPayloadSignatureTests = []struct {
// failures // failures
{[]byte(`{"a": "z"}`), "secret", "XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false}, {[]byte(`{"a": "z"}`), "secret", "XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
{[]byte(`{"a": "z"}`), "secreX", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "900225703e9342328db7307692736e2f7cc7b36e", false}, {[]byte(`{"a": "z"}`), "secreX", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "900225703e9342328db7307692736e2f7cc7b36e", false},
{[]byte(`{"a": "z"}`), "", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "", false},
} }
func TestCheckPayloadSignature(t *testing.T) { func TestCheckPayloadSignature(t *testing.T) {
@@ -28,7 +61,7 @@ func TestCheckPayloadSignature(t *testing.T) {
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil)) 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) t.Errorf("error message should not disclose expected mac: %s", err)
} }
} }
@@ -45,6 +78,7 @@ var checkPayloadSignature256Tests = []struct {
{[]byte(`{"a": "z"}`), "secret", "sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true}, {[]byte(`{"a": "z"}`), "secret", "sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
// failures // failures
{[]byte(`{"a": "z"}`), "secret", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false}, {[]byte(`{"a": "z"}`), "secret", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
{[]byte(`{"a": "z"}`), "", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "", false},
} }
func TestCheckPayloadSignature256(t *testing.T) { func TestCheckPayloadSignature256(t *testing.T) {
@@ -54,7 +88,34 @@ 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)) 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) t.Errorf("error message should not disclose expected mac: %s", err)
} }
} }
@@ -92,6 +153,12 @@ var checkScalrSignatureTests = []struct {
[]byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+", []byte(`{"a": "b"}`), "bilFGi4ZVZUdG+C6r0NIM9tuRq6PaG33R3eBUVhLwMAErGBaazvXe4Gq2DcJs5q+",
"48e395e38ac48988929167df531eb2da00063a7d", false, "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) { func TestCheckScalrSignature(t *testing.T) {
@@ -102,12 +169,36 @@ func TestCheckScalrSignature(t *testing.T) {
testCase.description, testCase.ok, valid) testCase.description, testCase.ok, valid)
} }
if err != nil && strings.Contains(err.Error(), testCase.expectedSignature) { 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) 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 { var extractParameterTests = []struct {
s string s string
params interface{} params interface{}
@@ -129,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.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.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.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) { func TestExtractParameter(t *testing.T) {
@@ -537,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)
}
}
}
+85
View File
@@ -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
}
+75 -25
View File
@@ -1,6 +1,7 @@
package main package main
import ( import (
"crypto/tls"
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
@@ -10,6 +11,7 @@ import (
"net/url" "net/url"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"strings" "strings"
"time" "time"
@@ -17,13 +19,13 @@ import (
"github.com/codegangsta/negroni" "github.com/codegangsta/negroni"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/satori/go.uuid" uuid "github.com/satori/go.uuid"
fsnotify "gopkg.in/fsnotify.v1" fsnotify "gopkg.in/fsnotify.v1"
) )
const ( const (
version = "2.6.9" version = "2.6.11"
) )
var ( var (
@@ -38,6 +40,9 @@ var (
cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file") 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") 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") 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 responseHeaders hook.ResponseHeaders
hooksFiles hook.HooksFiles hooksFiles hook.HooksFiles
@@ -78,6 +83,14 @@ func main() {
os.Exit(0) os.Exit(0)
} }
if *justListCiphers {
err := writeTLSSupportedCipherStrings(os.Stdout, getTLSMinVersion(*tlsMinVersion))
if err != nil {
log.Fatal(err)
}
os.Exit(0)
}
if len(hooksFiles) == 0 { if len(hooksFiles) == 0 {
hooksFiles = append(hooksFiles, "hooks.json") hooksFiles = append(hooksFiles, "hooks.json")
} }
@@ -185,22 +198,36 @@ func main() {
hooksURL = "/" + *hooksURLPrefix + "/{id}" hooksURL = "/" + *hooksURLPrefix + "/{id}"
} }
router.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprint(w, "OK")
})
router.HandleFunc(hooksURL, hookHandler) router.HandleFunc(hooksURL, hookHandler)
n.UseHandler(router) n.UseHandler(router)
if *secure { 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 {
log.Printf("serving hooks on http://%s:%d%s", *ip, *port, hooksURL) log.Printf("serving hooks on http://%s:%d%s", *ip, *port, hooksURL)
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *ip, *port), n)) 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) { func hookHandler(w http.ResponseWriter, r *http.Request) {
// generate a request id for logging // generate a request id for logging
rid := uuid.NewV4().String()[:6] rid := uuid.NewV4().String()[:6]
@@ -235,22 +262,24 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
contentType = matchedHook.IncomingPayloadContentType contentType = matchedHook.IncomingPayloadContentType
} }
if strings.Contains(contentType, "json") { switch {
case strings.Contains(contentType, "json"):
decoder := json.NewDecoder(strings.NewReader(string(body))) decoder := json.NewDecoder(strings.NewReader(string(body)))
decoder.UseNumber() decoder.UseNumber()
err := decoder.Decode(&payload) err := decoder.Decode(&payload)
if err != nil { if err != nil {
log.Printf("[%s] error parsing JSON payload %+v\n", rid, err) 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)) fd, err := url.ParseQuery(string(body))
if err != nil { if err != nil {
log.Printf("[%s] error parsing form payload %+v\n", rid, err) log.Printf("[%s] error parsing form payload %+v\n", rid, err)
} else { } else {
payload = valuesToMap(fd) payload = valuesToMap(fd)
} }
default:
log.Printf("[%s] error parsing body payload due to unsupported content type header: %s\n", rid, contentType)
} }
// handle hook // handle hook
@@ -267,9 +296,9 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
ok, err = matchedHook.TriggerRule.Evaluate(&headers, &query, &payload, &body, r.RemoteAddr) ok, err = matchedHook.TriggerRule.Evaluate(&headers, &query, &payload, &body, r.RemoteAddr)
if err != nil { if err != nil {
msg := fmt.Sprintf("[%s] error evaluating hook: %s", rid, err) msg := fmt.Sprintf("[%s] error evaluating hook: %s", rid, err)
log.Print(msg) log.Println(msg)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Error occurred while evaluating hook rules.") fmt.Fprint(w, "Error occurred while evaluating hook rules.")
return return
} }
} }
@@ -287,39 +316,43 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
if matchedHook.CaptureCommandOutputOnError { if matchedHook.CaptureCommandOutputOnError {
fmt.Fprintf(w, response) fmt.Fprint(w, response)
} else { } else {
w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, "Error occurred while executing the hook's command. Please check your logs for more details.") fmt.Fprint(w, "Error occurred while executing the hook's command. Please check your logs for more details.")
} }
} else { } 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 { } else {
go handleHook(matchedHook, rid, &headers, &query, &payload, &body) 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 return
} }
// Check if a return code is configured for the hook // Check if a return code is configured for the hook
if matchedHook.TriggerRuleMismatchHttpResponseCode != 0 { if matchedHook.TriggerRuleMismatchHttpResponseCode != 0 {
// Check if the configured return code is supported by the http package writeHttpResponseCode(w, rid, matchedHook.ID, matchedHook.TriggerRuleMismatchHttpResponseCode)
// 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)
}
} }
// if none of the hooks got triggered // if none of the hooks got triggered
log.Printf("[%s] %s got matched, but didn't get triggered because the trigger rules were not satisfied\n", rid, matchedHook.ID) log.Printf("[%s] %s got matched, but didn't get triggered because the trigger rules were not satisfied\n", rid, matchedHook.ID)
fmt.Fprintf(w, "Hook rules were not satisfied.") fmt.Fprint(w, "Hook rules were not satisfied.")
} else { } else {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Hook not found.") fmt.Fprint(w, "Hook not found.")
} }
} }
@@ -328,6 +361,13 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in
// check the command exists // check the command exists
cmdPath, err := exec.LookPath(h.ExecuteCommand) 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 { if err != nil {
log.Printf("unable to locate command: '%s'", h.ExecuteCommand) log.Printf("unable to locate command: '%s'", h.ExecuteCommand)
@@ -408,6 +448,16 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in
return string(out), err 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) { func reloadHooks(hooksFilePath string) {
hooksInFile := hook.Hooks{} hooksInFile := hook.Hooks{}
+2 -2
View File
@@ -76,7 +76,6 @@ func TestWebhook(t *testing.T) {
for _, tt := range hookHandlerTests { for _, tt := range hookHandlerTests {
t.Run(tt.desc, func(t *testing.T) { t.Run(tt.desc, func(t *testing.T) {
ip, port := serverAddress(t) ip, port := serverAddress(t)
args := []string{fmt.Sprintf("-hooks=%s", configPath), fmt.Sprintf("-ip=%s", ip), fmt.Sprintf("-port=%s", port), "-verbose"} args := []string{fmt.Sprintf("-hooks=%s", configPath), fmt.Sprintf("-ip=%s", ip), fmt.Sprintf("-port=%s", port), "-verbose"}
@@ -249,7 +248,7 @@ func serverAddress(t *testing.T) (string, string) {
func waitForServerReady(t *testing.T, ip, port string) { func waitForServerReady(t *testing.T, ip, port string) {
waitForServer(t, waitForServer(t,
fmt.Sprintf("http://%v:%v/", ip, port), fmt.Sprintf("http://%v:%v/", ip, port),
http.StatusNotFound, http.StatusOK,
5*time.Second) 5*time.Second)
} }
@@ -618,6 +617,7 @@ env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
// Check logs // Check logs
{"static params should pass", "static-params-ok", nil, `{}`, false, http.StatusOK, "arg: passed\n", `(?s)command output: arg: passed`}, {"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`}, {"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. // buffer provides a concurrency-safe bytes.Buffer to tests above.