Compare commits

...

39 Commits

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

Updates #290
2019-01-02 16:50:23 -06:00
Cameron Moore 1a17dc83fe Return errors on empty secrets during signature validations
Fixes #207
2019-01-02 16:09:27 -06:00
Adnan Hajdarević 753734428f Merge pull request #283 from adnanh/healthcheck-route
Add `/` route handler to return 200 OK which can be used as a healthcheck endpoint
2018-11-17 19:03:20 +01:00
Adnan Hajdarevic f76426e9b0 add handler for the route to be used as a healtcheck endpoint, fixes #233 2018-11-17 19:01:26 +01:00
Adnan Hajdarević 5803d5e849 Merge pull request #282 from adnanh/master
Backmerge
2018-11-16 18:59:25 +01:00
Adnan Hajdarević 385898b21f Merge pull request #281 from ruliezz/patch-1
Update docs with a Gitea example
2018-11-16 18:58:58 +01:00
ruliezz ce7f8d5d28 Updated with a Gitea example 2018-11-16 15:23:39 +01:00
Adnan Hajdarević b2899d1d3e Update README.md 2018-11-16 10:46:35 +01:00
Adnan Hajdarević e8628cd662 Add more guides to the README 2018-11-16 10:45:37 +01:00
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
10 changed files with 595 additions and 384 deletions
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
open_collective: webhook
+7 -2
View File
@@ -1,18 +1,23 @@
language: go
go:
- 1.8.x
- 1.9.x
- 1.11.x
- 1.12.x
- 1.13.x
- tip
os:
- linux
- osx
- windows
matrix:
fast_finish: true
allow_failures:
- go: tip
exclude:
- os: windows
go: tip
install:
- go get -d -v -t ./...
+16
View File
@@ -33,6 +33,9 @@ $ go get github.com/adnanh/webhook
to get the latest version of the [webhook][w].
### Using package manager
#### Snap store
[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-white.svg)](https://snapcraft.io/webhook)
#### Ubuntu
If you are using Ubuntu linux (17.04 or later), you can install webhook using `sudo apt-get install webhook` which will install community packaged version.
@@ -93,6 +96,19 @@ 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)
- [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.
-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 ./...
+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
* `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`.
+102
View File
@@ -176,6 +176,61 @@ Values in the request body can be accessed in the command or to the match rule b
}
]
```
## 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
@@ -322,3 +377,50 @@ 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"
}
}
}
]
}
}
]
```
+51 -42
View File
@@ -94,6 +94,10 @@ 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 secret == "" {
return "", errors.New("signature validation secret can not be empty")
}
signature = strings.TrimPrefix(signature, "sha1=")
mac := hmac.New(sha1.New, []byte(secret))
@@ -111,6 +115,10 @@ 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 secret == "" {
return "", errors.New("signature validation secret can not be empty")
}
signature = strings.TrimPrefix(signature, "sha256=")
mac := hmac.New(sha256.New, []byte(secret))
@@ -134,6 +142,10 @@ func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey
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))
@@ -150,7 +162,6 @@ func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey
}
// 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(time.RFC1123, dateHeader)
if err != nil {
return false, err
}
@@ -168,41 +179,37 @@ func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey
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)
}
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(ipRange, "/") {
ipRange = ipRange + "/32"
if !strings.Contains(r, "/") {
r = r + "/32"
}
_, cidr, err := net.ParseCIDR(ipRange)
_, cidr, err := net.ParseCIDR(r)
if err != nil {
return false, err
}
return cidr.Contains(parsedIP), nil
if cidr.Contains(parsedIP) {
return true, nil
}
}
return false, nil
}
// ReplaceParameter replaces parameter value with the passed value in the passed map
@@ -215,7 +222,6 @@ func ReplaceParameter(s string, params interface{}, value interface{}) bool {
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
index, err := strconv.ParseUint(p[0], 10, 64)
@@ -250,8 +256,12 @@ func GetParameter(s string, params interface{}) (interface{}, bool) {
return nil, false
}
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 {
paramsValue := reflect.ValueOf(params)
switch paramsValue.Kind() {
case reflect.Slice:
paramsValueSliceLength := paramsValue.Len()
if paramsValueSliceLength > 0 {
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
index, err := strconv.ParseUint(p[0], 10, 64)
@@ -273,18 +283,20 @@ func GetParameter(s string, params interface{}) (interface{}, bool) {
}
return nil, false
case reflect.Map:
// Check for raw key
if v, ok := params.(map[string]interface{})[s]; ok {
return v, true
}
if p := strings.SplitN(s, ".", 2); len(p) > 1 {
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Map {
// 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
}
}
@@ -327,7 +339,6 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
return ha.Name, true
case SourceEntirePayload:
r, err := json.Marshal(payload)
if err != nil {
return "", false
}
@@ -335,7 +346,6 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
return string(r), true
case SourceEntireHeaders:
r, err := json.Marshal(headers)
if err != nil {
return "", false
}
@@ -343,7 +353,6 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
return string(r), true
case SourceEntireQuery:
r, err := json.Marshal(query)
if err != nil {
return "", false
}
@@ -427,12 +436,13 @@ type Hook struct {
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 {
@@ -442,7 +452,6 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
decoder.UseNumber()
err := decoder.Decode(&newArg)
if err != nil {
errors = append(errors, &ParseError{err})
continue
@@ -485,8 +494,8 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
// ExtractCommandArguments creates a list of arguments, based on the
// PassArgumentsToCommand property that is ready to be used with exec.Command()
func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) ([]string, []error) {
var args = make([]string, 0)
var errors = make([]error, 0)
args := make([]string, 0)
errors := make([]error, 0)
args = append(args, h.ExecuteCommand)
@@ -510,8 +519,8 @@ func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]inter
// format, based on the PassEnvironmentToCommand property that is ready to be used
// with exec.Command().
func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string]interface{}) ([]string, []error) {
var args = make([]string, 0)
var errors = make([]error, 0)
args := make([]string, 0)
errors := make([]error, 0)
for i := range h.PassEnvironmentToCommand {
if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok {
if h.PassEnvironmentToCommand[i].EnvName != "" {
@@ -544,8 +553,8 @@ type FileParameter struct {
// format, based on the PassFileToCommand property that is ready to be used
// with exec.Command().
func (h *Hook) ExtractCommandArgumentsForFile(headers, query, payload *map[string]interface{}) ([]FileParameter, []error) {
var args = make([]FileParameter, 0)
var errors = make([]error, 0)
args := make([]FileParameter, 0)
errors := make([]error, 0)
for i := range h.PassFileToCommand {
if arg, ok := h.PassFileToCommand[i].Get(headers, query, payload); ok {
+67 -3
View File
@@ -7,6 +7,38 @@ import (
"testing"
)
func TestGetParameter(t *testing.T) {
for _, test := range []struct {
key string
val interface{}
expect interface{}
ok bool
}{
// True
{"a", map[string]interface{}{"a": "1"}, "1", true},
{"a.b", map[string]interface{}{"a.b": "1"}, "1", true},
{"a.c", map[string]interface{}{"a": map[string]interface{}{"c": 2}}, 2, true},
{"a.1", map[string]interface{}{"a": map[string]interface{}{"1": 3}}, 3, true},
{"a.1", map[string]interface{}{"a": []interface{}{"a", "b"}}, "b", true},
{"0", []interface{}{"a", "b"}, "a", true},
// False
{"z", map[string]interface{}{"a": "1"}, nil, false},
{"a.z", map[string]interface{}{"a": map[string]interface{}{"b": 2}}, nil, false},
{"z.b", map[string]interface{}{"a": map[string]interface{}{"z": 2}}, nil, false},
{"a.2", map[string]interface{}{"a": []interface{}{"a", "b"}}, nil, false},
} {
res, ok := GetParameter(test.key, test.val)
if ok != test.ok {
t.Errorf("unexpected result given {%q, %q}: %t\n", test.key, test.val, ok)
}
if !reflect.DeepEqual(res, test.expect) {
t.Errorf("failed given {%q, %q}:\nexpected {%#v}\ngot {%#v}\n", test.key, test.val, test.expect, res)
}
}
}
var checkPayloadSignatureTests = []struct {
payload []byte
secret string
@@ -19,6 +51,7 @@ var checkPayloadSignatureTests = []struct {
// failures
{[]byte(`{"a": "z"}`), "secret", "XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false},
{[]byte(`{"a": "z"}`), "secreX", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "900225703e9342328db7307692736e2f7cc7b36e", false},
{[]byte(`{"a": "z"}`), "", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "", false},
}
func TestCheckPayloadSignature(t *testing.T) {
@@ -28,7 +61,7 @@ func TestCheckPayloadSignature(t *testing.T) {
t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil))
}
if err != nil && strings.Contains(err.Error(), tt.mac) {
if err != nil && tt.mac != "" && strings.Contains(err.Error(), tt.mac) {
t.Errorf("error message should not disclose expected mac: %s", err)
}
}
@@ -45,6 +78,7 @@ var checkPayloadSignature256Tests = []struct {
{[]byte(`{"a": "z"}`), "secret", "sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true},
// failures
{[]byte(`{"a": "z"}`), "secret", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false},
{[]byte(`{"a": "z"}`), "", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "", false},
}
func TestCheckPayloadSignature256(t *testing.T) {
@@ -54,7 +88,7 @@ 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)
}
}
@@ -92,6 +126,12 @@ var checkScalrSignatureTests = []struct {
[]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) {
@@ -102,12 +142,36 @@ func TestCheckScalrSignature(t *testing.T) {
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)
}
}
}
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{}
+41 -15
View File
@@ -10,6 +10,7 @@ import (
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
@@ -23,7 +24,7 @@ import (
)
const (
version = "2.6.9"
version = "2.6.10"
)
var (
@@ -185,6 +186,10 @@ func main() {
hooksURL = "/" + *hooksURLPrefix + "/{id}"
}
router.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprint(w, "OK")
})
router.HandleFunc(hooksURL, hookHandler)
n.UseHandler(router)
@@ -269,7 +274,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
msg := fmt.Sprintf("[%s] error evaluating hook: %s", rid, err)
log.Print(msg)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Error occurred while evaluating hook rules.")
fmt.Fprint(w, "Error occurred while evaluating hook rules.")
return
}
}
@@ -287,39 +292,43 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
if matchedHook.CaptureCommandOutputOnError {
fmt.Fprintf(w, response)
fmt.Fprint(w, response)
} else {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, "Error occurred while executing the hook's command. Please check your logs for more details.")
fmt.Fprint(w, "Error occurred while executing the hook's command. Please check your logs for more details.")
}
} else {
fmt.Fprintf(w, response)
// Check if a success return code is configured for the hook
if matchedHook.SuccessHttpResponseCode != 0 {
writeHttpResponseCode(w, rid, matchedHook.ID, matchedHook.SuccessHttpResponseCode)
}
fmt.Fprint(w, response)
}
} else {
go handleHook(matchedHook, rid, &headers, &query, &payload, &body)
fmt.Fprintf(w, matchedHook.ResponseMessage)
// Check if a success return code is configured for the hook
if matchedHook.SuccessHttpResponseCode != 0 {
writeHttpResponseCode(w, rid, matchedHook.ID, matchedHook.SuccessHttpResponseCode)
}
fmt.Fprint(w, matchedHook.ResponseMessage)
}
return
}
// Check if a return code is configured for the hook
if matchedHook.TriggerRuleMismatchHttpResponseCode != 0 {
// Check if the configured return code is supported by the http package
// by testing if there is a StatusText for this code.
if len(http.StatusText(matchedHook.TriggerRuleMismatchHttpResponseCode)) > 0 {
w.WriteHeader(matchedHook.TriggerRuleMismatchHttpResponseCode)
} else {
log.Printf("[%s] %s got matched, but the configured return code %d is unknown - defaulting to 200\n", rid, matchedHook.ID, matchedHook.TriggerRuleMismatchHttpResponseCode)
}
writeHttpResponseCode(w, rid, matchedHook.ID, matchedHook.TriggerRuleMismatchHttpResponseCode)
}
// if none of the hooks got triggered
log.Printf("[%s] %s got matched, but didn't get triggered because the trigger rules were not satisfied\n", rid, matchedHook.ID)
fmt.Fprintf(w, "Hook rules were not satisfied.")
fmt.Fprint(w, "Hook rules were not satisfied.")
} else {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Hook not found.")
fmt.Fprint(w, "Hook not found.")
}
}
@@ -328,6 +337,13 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in
// check the command exists
cmdPath, err := exec.LookPath(h.ExecuteCommand)
if err != nil {
// give a last chance, maybe is a relative path
relativeToCwd := filepath.Join(h.CommandWorkingDirectory, h.ExecuteCommand)
// check the command exists
cmdPath, err = exec.LookPath(relativeToCwd)
}
if err != nil {
log.Printf("unable to locate command: '%s'", h.ExecuteCommand)
@@ -408,6 +424,16 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in
return string(out), err
}
func writeHttpResponseCode(w http.ResponseWriter, rid string, hookId string, responseCode int) {
// Check if the given return code is supported by the http package
// by testing if there is a StatusText for this code.
if len(http.StatusText(responseCode)) > 0 {
w.WriteHeader(responseCode)
} else {
log.Printf("[%s] %s got matched, but the configured return code %d is unknown - defaulting to 200\n", rid, hookId, responseCode)
}
}
func reloadHooks(hooksFilePath string) {
hooksInFile := hook.Hooks{}
+1 -2
View File
@@ -76,7 +76,6 @@ func TestWebhook(t *testing.T) {
for _, tt := range hookHandlerTests {
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"}
@@ -249,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)
}