diff --git a/Makefile b/Makefile index e9f04d6..b341c1f 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ -OS = darwin freebsd linux openbsd windows +OS = darwin freebsd linux openbsd ARCHS = 386 arm amd64 arm64 -all: build release +all: build release release-windows build: deps go build @@ -18,6 +18,15 @@ release: clean deps done \ done +release-windows: clean deps + @for arch in $(ARCHS);\ + do \ + echo "Building windows-$$arch"; \ + mkdir -p build/webhook-windows-$$arch/; \ + GOOS=windows GOARCH=$$arch go build -o build/webhook-windows-$$arch/webhook.exe; \ + tar cz -C build -f build/webhook-windows-$$arch.tar.gz webhook-windows-$$arch; \ + done + test: deps go test ./... diff --git a/README.md b/README.md index 9875828..a2259d4 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,21 @@ Check out [Hook examples page](docs/Hook-Examples.md) for more complex examples ### Guides featuring webhook - [Webhook & JIRA](https://sites.google.com/site/mrxpalmeiras/notes/jira-webhooks) by [@perfecto25](https://github.com/perfecto25) - [Trigger Ansible AWX job runs on SCM (e.g. git) commit](http://jpmens.net/2017/10/23/trigger-awx-job-runs-on-scm-commit/) by [@jpmens](http://mens.de/) - + - [Deploy using GitHub webhooks](https://davidauthier.wearemd.com/blog/deploy-using-github-webhooks.html) by [@awea](https://davidauthier.wearemd.com) + - [Setting up Automatic Deployment and Builds Using Webhooks](https://willbrowning.me/setting-up-automatic-deployment-and-builds-using-webhooks/) by [Will Browning](https://willbrowning.me/about/) + - [Auto deploy your Node.js app on push to GitHub in 3 simple steps](https://webhookrelay.com/blog/2018/07/17/auto-deploy-on-git-push/) by Karolis Rusenas + - [Automate Static Site Deployments with Salt, Git, and Webhooks](https://www.linode.com/docs/applications/configuration-management/automate-a-static-site-deployment-with-salt/) by [Linode](https://www.linode.com) + - [Using Prometheus to Automatically Scale WebLogic Clusters on Kubernetes](https://blogs.oracle.com/weblogicserver/using-prometheus-to-automatically-scale-weblogic-clusters-on-kubernetes-v5) by [Marina Kogan](https://blogs.oracle.com/author/9a4fe754-1cc2-4c64-95fc-360642b62927) + - [Github Pages and Jekyll - A New Platform for LACNIC Labs](https://labs.lacnic.net/a-new-platform-for-lacniclabs/) by [Carlos Martínez Cagnazzo](https://twitter.com/carlosm3011) + - [How to Deploy React Apps Using Webhooks and Integrating Slack on Ubuntu](https://www.alibabacloud.com/blog/how-to-deploy-react-apps-using-webhooks-and-integrating-slack-on-ubuntu_594116) by Arslan Ud Din Shafiq + - [Private webhooks](https://ihateithe.re/2018/01/private-webhooks/) by [Thomas](https://ihateithe.re/colophon/) + - [Adventures in webhooks](https://medium.com/@draketech/adventures-in-webhooks-2d6584501c62) by [Drake](https://medium.com/@draketech) + - [GitHub pro tips](http://notes.spencerlyon.com/2016/01/04/github-pro-tips/) by [Spencer Lyon](http://notes.spencerlyon.com/) + - [XiaoMi Vacuum + Amazon Button = Dash Cleaning](https://www.instructables.com/id/XiaoMi-Vacuum-Amazon-Button-Dash-Cleaning/) by [c0mmensal](https://www.instructables.com/member/c0mmensal/) + - VIDEO: [Gitlab CI/CD configuration using Docker and adnanh/webhook to deploy on VPS - Tutorial #1](https://www.youtube.com/watch?v=Qhn-lXjyrZA&feature=youtu.be) by [Yes! Let's Learn Software Engineering](https://www.youtube.com/channel/UCH4XJf2BZ_52fbf8fOBMF3w) + - ... + - Want to add your own? Open an Issue or create a PR :-) + ## Community Contributions See the [webhook-contrib][wc] repository for a collections of tools and helpers related to [webhook][w] that have been contributed by the [webhook][w] community. @@ -102,7 +116,7 @@ Check out [existing issues](https://github.com/adnanh/webhook/issues) to see if # Support active development ## Sponsors -## DigitalOcean +## DigitalOcean [DigitalOcean](https://www.digitalocean.com/?ref=webhook) is a simple and robust cloud computing platform, designed for developers. diff --git a/docs/Hook-Examples.md b/docs/Hook-Examples.md index aedba52..b18abf7 100644 --- a/docs/Hook-Examples.md +++ b/docs/Hook-Examples.md @@ -1,296 +1,351 @@ -# Hook examples -This page is still work in progress. Feel free to contribute! - -## Incoming Github webhook -```json -[ - { - "id": "webhook", - "execute-command": "/home/adnan/redeploy-go-webhook.sh", - "command-working-directory": "/home/adnan/go", - "pass-arguments-to-command": - [ - { - "source": "payload", - "name": "head_commit.id" - }, - { - "source": "payload", - "name": "pusher.name" - }, - { - "source": "payload", - "name": "pusher.email" - } - ], - "trigger-rule": - { - "and": - [ - { - "match": - { - "type": "payload-hash-sha1", - "secret": "mysecret", - "parameter": - { - "source": "header", - "name": "X-Hub-Signature" - } - } - }, - { - "match": - { - "type": "value", - "value": "refs/heads/master", - "parameter": - { - "source": "payload", - "name": "ref" - } - } - } - ] - } - } -] -``` - -## Incoming Bitbucket webhook - -Bitbucket does not pass any secrets back to the webhook. [Per their documentation](https://confluence.atlassian.com/bitbucket/manage-webhooks-735643732.html#Managewebhooks-trigger_webhookTriggeringwebhooks), in order to verify that the webhook came from Bitbucket you must whitelist the IP range `104.192.143.0/24`: - -```json -[ - { - "id": "webhook", - "execute-command": "/home/adnan/redeploy-go-webhook.sh", - "command-working-directory": "/home/adnan/go", - "pass-arguments-to-command": - [ - { - "source": "payload", - "name": "actor.username" - } - ], - "trigger-rule": - { - "match": - { - "type": "ip-whitelist", - "ip-range": "104.192.143.0/24" - } - } - } -] -``` - -## Incoming Gitlab Webhook -Gitlab provides webhooks for many kinds of events. -Refer to this URL for example request body content: [gitlab-ce/integrations/webhooks](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/project/integrations/webhooks.md) -Values in the request body can be accessed in the command or to the match rule by referencing 'payload' as the source: -```json -[ - { - "id": "redeploy-webhook", - "execute-command": "/home/adnan/redeploy-go-webhook.sh", - "command-working-directory": "/home/adnan/go", - "pass-arguments-to-command": - [ - { - "source": "payload", - "name": "user_name" - } - ], - "response-message": "Executing redeploy script", - "trigger-rule": - { - "match": - { - "type": "value", - "value": "", - "parameter": - { - "source": "header", - "name": "X-Gitlab-Token" - } - } - } - } -] -``` - -## Incoming Gogs webhook -```json -[ - { - "id": "webhook", - "execute-command": "/home/adnan/redeploy-go-webhook.sh", - "command-working-directory": "/home/adnan/go", - "pass-arguments-to-command": - [ - { - "source": "payload", - "name": "head_commit.id" - }, - { - "source": "payload", - "name": "pusher.name" - }, - { - "source": "payload", - "name": "pusher.email" - } - ], - "trigger-rule": - { - "and": - [ - { - "match": - { - "type": "payload-hash-sha256", - "secret": "mysecret", - "parameter": - { - "source": "header", - "name": "X-Gogs-Signature" - } - } - }, - { - "match": - { - "type": "value", - "value": "refs/heads/master", - "parameter": - { - "source": "payload", - "name": "ref" - } - } - } - ] - } - } -] -``` - -## Slack slash command -```json -[ - { - "id": "redeploy-webhook", - "execute-command": "/home/adnan/redeploy-go-webhook.sh", - "command-working-directory": "/home/adnan/go", - "response-message": "Executing redeploy script", - "trigger-rule": - { - "match": - { - "type": "value", - "value": "", - "parameter": - { - "source": "payload", - "name": "token" - } - } - } - } -] -``` - -## A simple webhook with a secret key in GET query - -__Not recommended in production due to low security__ - -`example.com:9000/hooks/simple-one` - won't work -`example.com:9000/hooks/simple-one?token=42` - will work - -```json -[ - { - "id": "simple-one", - "execute-command": "/path/to/command.sh", - "response-message": "Executing simple webhook...", - "trigger-rule": - { - "match": - { - "type": "value", - "value": "42", - "parameter": - { - "source": "url", - "name": "token" - } - } - } - } -] -``` - -# JIRA Webhooks -[Guide by @perfecto25](https://sites.google.com/site/mrxpalmeiras/notes/jira-webhooks) - -# Pass File-to-command sample - -## Webhook configuration - -
-[
-  {
-    "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
-  }
-]
-
- -## Sample client usage - -Store the following file as `testRequest.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="}
-
- -use then the curl tool to execute a request to the webhook. - -
-#!/bin/bash
-curl -H "Content-Type:application/json" -X POST -d @testRequest.json \
-http://localhost:9000/hooks/test-file-webhook
-
- -or in a single line, using https://github.com/jpmens/jo to generate the JSON code -
-jo binary=%filename.zip | curl -H "Content-Type:application/json" -X POST -d @- \
-http://localhost:9000/hooks/test-file-webhook
-
- - -## 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 +# Hook examples +This page is still work in progress. Feel free to contribute! + +## Incoming Github webhook +```json +[ + { + "id": "webhook", + "execute-command": "/home/adnan/redeploy-go-webhook.sh", + "command-working-directory": "/home/adnan/go", + "pass-arguments-to-command": + [ + { + "source": "payload", + "name": "head_commit.id" + }, + { + "source": "payload", + "name": "pusher.name" + }, + { + "source": "payload", + "name": "pusher.email" + } + ], + "trigger-rule": + { + "and": + [ + { + "match": + { + "type": "payload-hash-sha1", + "secret": "mysecret", + "parameter": + { + "source": "header", + "name": "X-Hub-Signature" + } + } + }, + { + "match": + { + "type": "value", + "value": "refs/heads/master", + "parameter": + { + "source": "payload", + "name": "ref" + } + } + } + ] + } + } +] +``` + +## Incoming Bitbucket webhook + +Bitbucket does not pass any secrets back to the webhook. [Per their documentation](https://confluence.atlassian.com/bitbucket/manage-webhooks-735643732.html#Managewebhooks-trigger_webhookTriggeringwebhooks), in order to verify that the webhook came from Bitbucket you must whitelist the IP range `104.192.143.0/24`: + +```json +[ + { + "id": "webhook", + "execute-command": "/home/adnan/redeploy-go-webhook.sh", + "command-working-directory": "/home/adnan/go", + "pass-arguments-to-command": + [ + { + "source": "payload", + "name": "actor.username" + } + ], + "trigger-rule": + { + "match": + { + "type": "ip-whitelist", + "ip-range": "104.192.143.0/24" + } + } + } +] +``` + +## Incoming Gitlab Webhook +Gitlab provides webhooks for many kinds of events. +Refer to this URL for example request body content: [gitlab-ce/integrations/webhooks](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/project/integrations/webhooks.md) +Values in the request body can be accessed in the command or to the match rule by referencing 'payload' as the source: +```json +[ + { + "id": "redeploy-webhook", + "execute-command": "/home/adnan/redeploy-go-webhook.sh", + "command-working-directory": "/home/adnan/go", + "pass-arguments-to-command": + [ + { + "source": "payload", + "name": "user_name" + } + ], + "response-message": "Executing redeploy script", + "trigger-rule": + { + "match": + { + "type": "value", + "value": "", + "parameter": + { + "source": "header", + "name": "X-Gitlab-Token" + } + } + } + } +] +``` + +## Incoming Gogs webhook +```json +[ + { + "id": "webhook", + "execute-command": "/home/adnan/redeploy-go-webhook.sh", + "command-working-directory": "/home/adnan/go", + "pass-arguments-to-command": + [ + { + "source": "payload", + "name": "head_commit.id" + }, + { + "source": "payload", + "name": "pusher.name" + }, + { + "source": "payload", + "name": "pusher.email" + } + ], + "trigger-rule": + { + "and": + [ + { + "match": + { + "type": "payload-hash-sha256", + "secret": "mysecret", + "parameter": + { + "source": "header", + "name": "X-Gogs-Signature" + } + } + }, + { + "match": + { + "type": "value", + "value": "refs/heads/master", + "parameter": + { + "source": "payload", + "name": "ref" + } + } + } + ] + } + } +] +``` +## Incoming Gitea webhook +```json +[ + { + "id": "webhook", + "execute-command": "/home/adnan/redeploy-go-webhook.sh", + "command-working-directory": "/home/adnan/go", + "pass-arguments-to-command": + [ + { + "source": "payload", + "name": "head_commit.id" + }, + { + "source": "payload", + "name": "pusher.name" + }, + { + "source": "payload", + "name": "pusher.email" + } + ], + "trigger-rule": + { + "and": + [ + { + "match": + { + "type": "value", + "value": "mysecret", + "parameter": + { + "source": "payload", + "name": "secret" + } + } + }, + { + "match": + { + "type": "value", + "value": "refs/heads/master", + "parameter": + { + "source": "payload", + "name": "ref" + } + } + } + ] + } + } +] +``` + +## Slack slash command +```json +[ + { + "id": "redeploy-webhook", + "execute-command": "/home/adnan/redeploy-go-webhook.sh", + "command-working-directory": "/home/adnan/go", + "response-message": "Executing redeploy script", + "trigger-rule": + { + "match": + { + "type": "value", + "value": "", + "parameter": + { + "source": "payload", + "name": "token" + } + } + } + } +] +``` + +## A simple webhook with a secret key in GET query + +__Not recommended in production due to low security__ + +`example.com:9000/hooks/simple-one` - won't work +`example.com:9000/hooks/simple-one?token=42` - will work + +```json +[ + { + "id": "simple-one", + "execute-command": "/path/to/command.sh", + "response-message": "Executing simple webhook...", + "trigger-rule": + { + "match": + { + "type": "value", + "value": "42", + "parameter": + { + "source": "url", + "name": "token" + } + } + } + } +] +``` + +# JIRA Webhooks +[Guide by @perfecto25](https://sites.google.com/site/mrxpalmeiras/notes/jira-webhooks) + +# Pass File-to-command sample + +## Webhook configuration + +
+[
+  {
+    "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
+  }
+]
+
+ +## Sample client usage + +Store the following file as `testRequest.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="}
+
+ +use then the curl tool to execute a request to the webhook. + +
+#!/bin/bash
+curl -H "Content-Type:application/json" -X POST -d @testRequest.json \
+http://localhost:9000/hooks/test-file-webhook
+
+ +or in a single line, using https://github.com/jpmens/jo to generate the JSON code +
+jo binary=%filename.zip | curl -H "Content-Type:application/json" -X POST -d @- \
+http://localhost:9000/hooks/test-file-webhook
+
+ + +## 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", @@ -319,6 +374,6 @@ In order to leverage the Signing Key for addtional authentication/security you m } ] } -] - -``` \ No newline at end of file +] + +``` diff --git a/hook/hook.go b/hook/hook.go index 43bc225..2d2c70a 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -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)) @@ -168,41 +180,36 @@ 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.TrimSpace(ip) - - // IPv6 addresses will likely be surrounded by [], so don't forget to remove those. - - if strings.HasPrefix(ip, "[") && strings.HasSuffix(ip, "]") { - ip = ip[1 : len(ip)-1] - } - - parsedIP := net.ParseIP(strings.TrimSpace(ip)) - + parsedIP := net.ParseIP(ip) if parsedIP == nil { return false, fmt.Errorf("invalid IP address found in remote address '%s'", remoteAddr) } - // Extract IP range in CIDR form. If a single IP address is provided, turn it into CIDR form. + for _, r := range strings.Fields(ipRange) { + // Extract IP range in CIDR form. If a single IP address is provided, turn it into CIDR form. - ipRange = strings.TrimSpace(ipRange) + if !strings.Contains(r, "/") { + r = r + "/32" + } - if !strings.Contains(ipRange, "/") { - ipRange = ipRange + "/32" + _, cidr, err := net.ParseCIDR(r) + if err != nil { + return false, err + } + + if cidr.Contains(parsedIP) { + return true, nil + } } - _, cidr, err := net.ParseCIDR(ipRange) - - if err != nil { - return false, err - } - - return cidr.Contains(parsedIP), nil + return false, nil } // ReplaceParameter replaces parameter value with the passed value in the passed map diff --git a/hook/hook_test.go b/hook/hook_test.go index 1794a7c..e6bfce2 100644 --- a/hook/hook_test.go +++ b/hook/hook_test.go @@ -19,6 +19,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 +29,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 +46,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 +56,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 +94,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 +110,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{} @@ -129,7 +161,7 @@ var extractParameterTests = []struct { {"a.501.b", map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": "y"}, map[string]interface{}{"b": "z"}}}, "", false}, // non-existent slice index {"a.502.b", map[string]interface{}{"a": []interface{}{}}, "", false}, // non-existent slice index {"a.b.503", map[string]interface{}{"a": map[string]interface{}{"b": []interface{}{"x", "y", "z"}}}, "", false}, // trailing, non-existent slice index - {"a.b", interface{}("a"), "", false}, // non-map, non-slice input + {"a.b", interface{}("a"), "", false}, // non-map, non-slice input } func TestExtractParameter(t *testing.T) { diff --git a/webhook.go b/webhook.go index cb44032..36eea4c 100644 --- a/webhook.go +++ b/webhook.go @@ -23,7 +23,7 @@ import ( ) const ( - version = "2.6.8" + version = "2.6.9" ) var ( @@ -185,6 +185,10 @@ func main() { hooksURL = "/" + *hooksURLPrefix + "/{id}" } + router.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "OK") + }) + router.HandleFunc(hooksURL, hookHandler) n.UseHandler(router)