mirror of
https://github.com/adnanh/webhook.git
synced 2026-07-27 01:29:16 +08:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f83af97138 | |||
| 5a96a5721a | |||
| 37b310feaa | |||
| ad76b51e6a | |||
| 4304aaa2d5 | |||
| a2fac2373b | |||
| c9adceb5d8 | |||
| 8488d3c432 | |||
| d3f5da5489 | |||
| 59b4954845 | |||
| 36aea82855 | |||
| 652109d46e | |||
| a7bbff0963 | |||
| 0b269e4870 | |||
| 40d01e4d5a | |||
| 90528b2ed9 | |||
| 489750a710 | |||
| b332c9e715 | |||
| bacf2b3666 | |||
| c96b0497e4 | |||
| 56560d3809 | |||
| d858b54309 |
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Adnan Hajdarevic <adnanh@gmail.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
@@ -1,154 +1,87 @@
|
|||||||
# webhook
|
# What is webhook?
|
||||||
|
[webhook](https://github.com/adnanh/webhook/) is a lightweight configurable tool written in Go, that allows you to easily create HTTP endpoints (hooks) on your server, which you can use to execute configured commands. You can also pass data from the HTTP request (such as headers, payload or query variables) to your commands. [webhook](https://github.com/adnanh/webhook/) also allows you to specify rules which have to be satisfied in order for the hook to be triggered.
|
||||||
|
|
||||||
## Installing
|
For example, if you're using Github or Bitbucket, you can use [webhook](https://github.com/adnanh/webhook/) to set up a hook that runs a redeploy script for your project on your staging server, whenever you push changes to the master branch of your project.
|
||||||
*Please note:* Before installing the webhook, make sure you have installed `go` and properly set up your `$GOPATH` environment variable.
|
|
||||||
|
|
||||||
```go
|
If you use Slack, you can set up an "Outgoing webhook integration" to run various commands on your server, which can then report back directly to your Slack channels using the "Incoming webhook integrations".
|
||||||
|
|
||||||
|
[webhook](https://github.com/adnanh/webhook/) aims to do nothing more than it should do, and that is:
|
||||||
|
1. receive the request,
|
||||||
|
2. parse the headers, payload and query variables,
|
||||||
|
3. check if the specified rules for the hook are satisfied,
|
||||||
|
3. and finally, pass the specified arguments to the specified command.
|
||||||
|
|
||||||
|
Everything else is the responsibility of the command's author.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Getting started
|
||||||
|
To get started, first make sure you've properly set up your [Golang](http://golang.org/doc/install) environment and then run the
|
||||||
|
```bash
|
||||||
$ go get github.com/adnanh/webhook
|
$ go get github.com/adnanh/webhook
|
||||||
```
|
```
|
||||||
|
to get the latest version of the [webhook](https://github.com/adnanh/webhook/).
|
||||||
|
|
||||||
## Updating
|
Next step is to define some hooks you want [webhook](https://github.com/adnanh/webhook/) to serve. Begin by creating an empty file named `hooks.json`. This file will contain an array of hooks the [webhook](https://github.com/adnanh/webhook/) will serve. Check [Hook definition page](https://github.com/adnanh/webhook/wiki/Hook-Definition) to see the detailed description of what properties a hook can contain, and how to use them.
|
||||||
```go
|
|
||||||
$ go get -u github.com/adnanh/webhook
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding hooks
|
Let's define a simple hook named `redeploy-webhook` that will run a redeploy script located in `/var/scripts/redeploy.sh`.
|
||||||
Hooks are defined using JSON format. The _hooks file_ must contain an array of JSON formatted hooks. Here is an example of a valid _hooks file_ containing only one hook. The hook will be triggered whenever a push to the master branch occurrs.
|
|
||||||
|
|
||||||
|
Our `hooks.json` file will now look like this:
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "hook-1",
|
"id": "redeploy-webhook",
|
||||||
"command": "OS command to be executed when the hook gets triggered",
|
"execute-command": "/var/scripts/redeploy.sh",
|
||||||
"args": [
|
"command-working-directory": "/var/webhook"
|
||||||
"ref",
|
|
||||||
"repository.owner.name"
|
|
||||||
],
|
|
||||||
"cwd": "current working directory under which the specified command will be executed (optional, defaults to the directory where the binary resides)",
|
|
||||||
"secret": "secret key used to compute the hash of the payload (optional)",
|
|
||||||
"trigger-rule":
|
|
||||||
{
|
|
||||||
"match":
|
|
||||||
{
|
|
||||||
"parameter": "ref",
|
|
||||||
"value": "refs/heads/master"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
## Passing parameters to the command
|
|
||||||
If you wish to pass parameters from the payload to the command that will be executed, you can use the `args` field in the `hook` definition. The parameters will be passed in order they are specified. If the payload does not contain the specified parameter, an empty string will be passed to the command instead.
|
|
||||||
|
|
||||||
## Trigger rules
|
You can now run [webhook](https://github.com/adnanh/webhook/) using
|
||||||
### And
|
|
||||||
*And rule* will evaluate to _true_, if and only if all of the sub rules evaluate to _true_.
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"and":
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"match":
|
|
||||||
{
|
|
||||||
"parameter": "ref",
|
|
||||||
"value": "refs/heads/master"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match":
|
|
||||||
{
|
|
||||||
"parameter": "repository.owner.name",
|
|
||||||
"value": "adnanh"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
### Or
|
|
||||||
*Or rule* will evaluate to _true_, if any of the sub rules evaluate to _true_.
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"or":
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"match":
|
|
||||||
{
|
|
||||||
"parameter": "ref",
|
|
||||||
"value": "refs/heads/master"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match":
|
|
||||||
{
|
|
||||||
"parameter": "ref",
|
|
||||||
"value": "refs/heads/development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
### Not
|
|
||||||
*Not rule* will evaluate to _true_, if and only if the sub rule evaluates to _false_.
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"not":
|
|
||||||
{
|
|
||||||
"match":
|
|
||||||
{
|
|
||||||
"parameter": "ref",
|
|
||||||
"value": "refs/heads/master"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
### Match
|
|
||||||
*Match rule* will evaluate to _true_, if and only if the payload JSON object contains the key specified in the `parameter` field that has the same value as specified in the `value` field.
|
|
||||||
|
|
||||||
*Please note:* Due to technical reasons, _number_ and _boolean_ values in the _hooks file_ must be wrapped around with a pair of quotes.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"match":
|
|
||||||
{
|
|
||||||
"parameter": "repository.id",
|
|
||||||
"value": "123456"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
It is possible to specify the values deeper in the payload JSON object with the dot operator, and if a value of the specified key happens to be an array, it's possible to index the array values by using the number instead of a string as the key, which is shown in the following example:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"match":
|
|
||||||
{
|
|
||||||
"parameter": "commits.0.author.username",
|
|
||||||
"value": "adnanh"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
## Running
|
|
||||||
After installing webhook, in your `$GOPATH/bin` directory you should have `webhook` binary.
|
|
||||||
|
|
||||||
By simply running the binary using the `./webhook` command, the webhook will start with the default options.
|
|
||||||
That means the webhook will listen on _all interfaces_ on port `9000`. It will try to read and parse `hooks.json` file from the same directory where the binary is located, and it will log everything to `stdout` and the file `webhook.log`.
|
|
||||||
|
|
||||||
To override any of these options, you can use the following command line flags:
|
|
||||||
```bash
|
```bash
|
||||||
-hooks="hooks.json": path to the json file containing defined hooks the webhook should serve
|
$ /path/to/webhook -hooks hooks.json -verbose
|
||||||
-ip="": ip the webhook server should listen on
|
|
||||||
-log="webhook.log": path to the log file
|
|
||||||
-port=9000: port the webhook server should listen on
|
|
||||||
```
|
```
|
||||||
|
|
||||||
All hooks are served under the `http://ip:port/hook/:id`, where the `:id` corresponds to the hook *id* specified in _hooks file_.
|
It will start up on default port 9000 and will provide you with one HTTP endpoint
|
||||||
|
```http
|
||||||
|
http://yourserver:9000/hooks/redeploy-webhook
|
||||||
|
```
|
||||||
|
|
||||||
Visiting `http://ip:port` will show version, uptime and number of hooks the webhook is serving.
|
Check [webhook parameters page](https://github.com/adnanh/webhook/wiki/Webhook-Parameters) to see how to override the ip, port and other settings such as hook hotreload, verbose output, etc, when starting the [webhook](https://github.com/adnanh/webhook/).
|
||||||
|
|
||||||
## Todo
|
By performing a simple HTTP GET or POST request to that endpoint, your specified redeploy script would be executed. Neat!
|
||||||
* Architecture overhaul to support vendor specific handlers and hook options
|
|
||||||
* Add support for ip white/black listing
|
However, hook defined like that could pose a security threat to your system, because anyone who knows your endpoint, can send a request and execute your command. To prevent that, you can use the `"trigger-rule"` property for your hook, to specify the exact circumstances under which the hook would be triggered. For example, you can use them to add a secret that you must supply as a parameter in order to successfully trigger the hook. Please check out the [Hook rules page](https://github.com/adnanh/webhook/wiki/Hook-Rules) for detailed list of available rules and their usage.
|
||||||
* Add "match-header" rule
|
|
||||||
* Add "match-regex" rule
|
# Using HTTPS
|
||||||
* Update README file with the description of what the tool is really trying to achieve and provide some real-life examples of usage
|
[webhook](https://github.com/adnanh/webhook/) by default serves hooks using http. If you want [webhook](https://github.com/adnanh/webhook/) to serve secure content using https, you can use the `-secure` flag while starting [webhook](https://github.com/adnanh/webhook/). Files containing a certificate and matching private key for the server must be provided using the `-cert /path/to/cert.pem` and `-key /path/to/key.pem` flags. If the certificate is signed by a certificate authority, the cert file should be the concatenation of the server's certificate followed by the CA's certificate.
|
||||||
* Move the specific details to their respective wiki pages
|
|
||||||
* ???
|
# Examples
|
||||||
|
Check out [Hook examples page](https://github.com/adnanh/webhook/wiki/Hook-Examples) for more complex examples of hooks.
|
||||||
|
|
||||||
|
# Contributing
|
||||||
|
Any form of contribution is welcome and highly appreciated.
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Adnan Hajdarevic <adnanh@gmail.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|||||||
+15
-8
@@ -5,7 +5,6 @@ import (
|
|||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -13,6 +12,10 @@ import (
|
|||||||
|
|
||||||
// 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, bool) {
|
func CheckPayloadSignature(payload []byte, secret string, signature string) (string, bool) {
|
||||||
|
if strings.HasPrefix(signature, "sha1=") {
|
||||||
|
signature = signature[5:]
|
||||||
|
}
|
||||||
|
|
||||||
mac := hmac.New(sha1.New, []byte(secret))
|
mac := hmac.New(sha1.New, []byte(secret))
|
||||||
mac.Write(payload)
|
mac.Write(payload)
|
||||||
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
||||||
@@ -20,11 +23,11 @@ func CheckPayloadSignature(payload []byte, secret string, signature string) (str
|
|||||||
return expectedMAC, hmac.Equal([]byte(signature), []byte(expectedMAC))
|
return expectedMAC, hmac.Equal([]byte(signature), []byte(expectedMAC))
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormValuesToMap converts url.Values to a map[string]interface{} object
|
// ValuesToMap converts map[string][]string to a map[string]string object
|
||||||
func FormValuesToMap(formValues url.Values) map[string]interface{} {
|
func ValuesToMap(values map[string][]string) map[string]interface{} {
|
||||||
ret := make(map[string]interface{})
|
ret := make(map[string]interface{})
|
||||||
|
|
||||||
for key, value := range formValues {
|
for key, value := range values {
|
||||||
if len(value) > 0 {
|
if len(value) > 0 {
|
||||||
ret[key] = value[0]
|
ret[key] = value[0]
|
||||||
}
|
}
|
||||||
@@ -33,8 +36,12 @@ func FormValuesToMap(formValues url.Values) map[string]interface{} {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractJSONParameter extracts value from payload based on the passed string
|
// ExtractParameter extracts value from interface{} based on the passed string
|
||||||
func ExtractJSONParameter(s string, params interface{}) (string, bool) {
|
func ExtractParameter(s string, params interface{}) (string, bool) {
|
||||||
|
if params == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
var p []string
|
var p []string
|
||||||
|
|
||||||
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
|
if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice {
|
||||||
@@ -49,7 +56,7 @@ func ExtractJSONParameter(s string, params interface{}) (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
return ExtractJSONParameter(p[2], params.([]map[string]interface{})[index])
|
return ExtractParameter(p[2], params.([]map[string]interface{})[index])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +65,7 @@ func ExtractJSONParameter(s string, params interface{}) (string, bool) {
|
|||||||
|
|
||||||
if p = strings.SplitN(s, ".", 2); len(p) > 1 {
|
if p = strings.SplitN(s, ".", 2); len(p) > 1 {
|
||||||
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||||
return ExtractJSONParameter(p[1], pValue)
|
return ExtractParameter(p[1], pValue)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
if pValue, ok := params.(map[string]interface{})[p[0]]; ok {
|
||||||
|
|||||||
+215
@@ -0,0 +1,215 @@
|
|||||||
|
package hook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/adnanh/webhook/helpers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants used to specify the parameter source
|
||||||
|
const (
|
||||||
|
SourceHeader string = "header"
|
||||||
|
SourceQuery string = "url"
|
||||||
|
SourcePayload string = "payload"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Argument type specifies the parameter key name and the source it should
|
||||||
|
// be extracted from
|
||||||
|
type Argument struct {
|
||||||
|
Source string `json:"source"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Argument method returns the value for the Argument's key name
|
||||||
|
// based on the Argument's source
|
||||||
|
func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string, bool) {
|
||||||
|
var source *map[string]interface{}
|
||||||
|
|
||||||
|
switch ha.Source {
|
||||||
|
case SourceHeader:
|
||||||
|
source = headers
|
||||||
|
case SourceQuery:
|
||||||
|
source = query
|
||||||
|
case SourcePayload:
|
||||||
|
source = payload
|
||||||
|
}
|
||||||
|
|
||||||
|
if source != nil {
|
||||||
|
return helpers.ExtractParameter(ha.Name, *source)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook type is a structure containing details for a single hook
|
||||||
|
type Hook struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
ExecuteCommand string `json:"execute-command"`
|
||||||
|
CommandWorkingDirectory string `json:"command-working-directory"`
|
||||||
|
ResponseMessage string `json:"response-message"`
|
||||||
|
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command"`
|
||||||
|
TriggerRule *Rules `json:"trigger-rule"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
var args = make([]string, 0)
|
||||||
|
|
||||||
|
args = append(args, h.ExecuteCommand)
|
||||||
|
|
||||||
|
for i := range h.PassArgumentsToCommand {
|
||||||
|
if arg, ok := h.PassArgumentsToCommand[i].Get(headers, query, payload); ok {
|
||||||
|
args = append(args, arg)
|
||||||
|
} else {
|
||||||
|
args = append(args, "")
|
||||||
|
log.Printf("couldn't retrieve argument for %+v\n", h.PassArgumentsToCommand[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hooks is an array of Hook objects
|
||||||
|
type Hooks []Hook
|
||||||
|
|
||||||
|
// LoadFromFile attempts to load hooks from specified JSON file
|
||||||
|
func (h *Hooks) LoadFromFile(path string) error {
|
||||||
|
if path == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse hook file for hooks
|
||||||
|
file, e := ioutil.ReadFile(path)
|
||||||
|
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
e = json.Unmarshal(file, h)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match iterates through Hooks and returns first one that matches the given ID,
|
||||||
|
// if no hook matches the given ID, nil is returned
|
||||||
|
func (h *Hooks) Match(id string) *Hook {
|
||||||
|
for i := range *h {
|
||||||
|
if (*h)[i].ID == id {
|
||||||
|
return &(*h)[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rules is a structure that contains one of the valid rule types
|
||||||
|
type Rules struct {
|
||||||
|
And *AndRule `json:"and"`
|
||||||
|
Or *OrRule `json:"or"`
|
||||||
|
Not *NotRule `json:"not"`
|
||||||
|
Match *MatchRule `json:"match"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate finds the first rule property that is not nil and returns the value
|
||||||
|
// it evaluates to
|
||||||
|
func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) bool {
|
||||||
|
switch {
|
||||||
|
case r.And != nil:
|
||||||
|
return r.And.Evaluate(headers, query, payload, body)
|
||||||
|
case r.Or != nil:
|
||||||
|
return r.Or.Evaluate(headers, query, payload, body)
|
||||||
|
case r.Not != nil:
|
||||||
|
return r.Not.Evaluate(headers, query, payload, body)
|
||||||
|
case r.Match != nil:
|
||||||
|
return r.Match.Evaluate(headers, query, payload, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// AndRule will evaluate to true if and only if all of the ChildRules evaluate to true
|
||||||
|
type AndRule []Rules
|
||||||
|
|
||||||
|
// Evaluate AndRule will return true if and only if all of ChildRules evaluate to true
|
||||||
|
func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) bool {
|
||||||
|
res := true
|
||||||
|
|
||||||
|
for _, v := range r {
|
||||||
|
res = res && v.Evaluate(headers, query, payload, body)
|
||||||
|
if res == false {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrRule will evaluate to true if any of the ChildRules evaluate to true
|
||||||
|
type OrRule []Rules
|
||||||
|
|
||||||
|
// Evaluate OrRule will return true if any of ChildRules evaluate to true
|
||||||
|
func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) bool {
|
||||||
|
res := false
|
||||||
|
|
||||||
|
for _, v := range r {
|
||||||
|
res = res || v.Evaluate(headers, query, payload, body)
|
||||||
|
if res == true {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotRule will evaluate to true if any and only if the ChildRule evaluates to false
|
||||||
|
type NotRule Rules
|
||||||
|
|
||||||
|
// Evaluate NotRule will return true if and only if ChildRule evaluates to false
|
||||||
|
func (r NotRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) bool {
|
||||||
|
return !r.Evaluate(headers, query, payload, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchRule will evaluate to true based on the type
|
||||||
|
type MatchRule struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Regex string `json:"regex"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Parameter Argument `json:"parameter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constants for the MatchRule type
|
||||||
|
const (
|
||||||
|
MatchValue string = "value"
|
||||||
|
MatchRegex string = "regex"
|
||||||
|
MatchHashSHA1 string = "payload-hash-sha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Evaluate MatchRule will return based on the type
|
||||||
|
func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) bool {
|
||||||
|
if arg, ok := r.Parameter.Get(headers, query, payload); ok {
|
||||||
|
switch r.Type {
|
||||||
|
case MatchValue:
|
||||||
|
return arg == r.Value
|
||||||
|
case MatchRegex:
|
||||||
|
ok, err := regexp.MatchString(r.Regex, arg)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error while trying to evaluate regex: %+v", err)
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
case MatchHashSHA1:
|
||||||
|
expected, ok := helpers.CheckPayloadSignature(*body, r.Secret, arg)
|
||||||
|
if !ok {
|
||||||
|
log.Printf("payload signature mismatch, expected %s got %s", expected, arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("couldn't retrieve argument for %+v\n", r.Parameter)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
+40
-6
@@ -1,18 +1,52 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "webhook",
|
"id": "webhook",
|
||||||
"command": "/home/adnan/redeploy-go-webhook.sh",
|
"execute-command": "/home/adnan/redeploy-go-webhook.sh",
|
||||||
"args": [
|
"command-working-directory": "/home/adnan/go",
|
||||||
"head"
|
"pass-arguments-to-command":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"source": "payload",
|
||||||
|
"name": "head_commit.id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "payload",
|
||||||
|
"name": "pusher.name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "payload",
|
||||||
|
"name": "pusher.email"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"cwd": "/home/adnan/go",
|
|
||||||
"trigger-rule":
|
"trigger-rule":
|
||||||
|
{
|
||||||
|
"and":
|
||||||
|
[
|
||||||
{
|
{
|
||||||
"match":
|
"match":
|
||||||
{
|
{
|
||||||
"parameter": "ref",
|
"type": "payload-hash-sha1",
|
||||||
"value": "refs/heads/master"
|
"secret": "mysecret",
|
||||||
|
"parameter":
|
||||||
|
{
|
||||||
|
"source": "header",
|
||||||
|
"name": "X-Hub-Signature"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match":
|
||||||
|
{
|
||||||
|
"type": "value",
|
||||||
|
"value": "refs/heads/master",
|
||||||
|
"parameter":
|
||||||
|
{
|
||||||
|
"source": "payload",
|
||||||
|
"name": "ref"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
-203
@@ -1,203 +0,0 @@
|
|||||||
package hooks
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/adnanh/webhook/helpers"
|
|
||||||
"github.com/adnanh/webhook/rules"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Hook is a structure that contains command to be executed
|
|
||||||
// and the current working directory name where that command should be executed
|
|
||||||
type Hook struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Command string `json:"command"`
|
|
||||||
Cwd string `json:"cwd"`
|
|
||||||
Secret string `json:"secret"`
|
|
||||||
Args []string `json:"args"`
|
|
||||||
Rule rules.Rule `json:"trigger-rule"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hooks represents structure that contains list of Hook objects
|
|
||||||
// and the name of file which is correspondingly mapped to it
|
|
||||||
type Hooks struct {
|
|
||||||
fileName string
|
|
||||||
list []Hook
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseFormArgs gets arguments from the Form payload that should be passed to the command
|
|
||||||
func (h *Hook) ParseFormArgs(form url.Values) []string {
|
|
||||||
var args = make([]string, 0)
|
|
||||||
|
|
||||||
args = append(args, h.Command)
|
|
||||||
|
|
||||||
for i := range h.Args {
|
|
||||||
if arg := form[h.Args[i]]; len(arg) > 0 {
|
|
||||||
args = append(args, arg[0])
|
|
||||||
} else {
|
|
||||||
args = append(args, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseJSONArgs gets arguments from the JSON payload that should be passed to the command
|
|
||||||
func (h *Hook) ParseJSONArgs(payload interface{}) []string {
|
|
||||||
var args = make([]string, 0)
|
|
||||||
|
|
||||||
args = append(args, h.Command)
|
|
||||||
|
|
||||||
for i := range h.Args {
|
|
||||||
if arg, ok := helpers.ExtractJSONParameter(h.Args[i], payload); ok {
|
|
||||||
args = append(args, arg)
|
|
||||||
} else {
|
|
||||||
args = append(args, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON implementation for a single hook
|
|
||||||
func (h *Hook) UnmarshalJSON(j []byte) error {
|
|
||||||
m := make(map[string]interface{})
|
|
||||||
err := json.Unmarshal(j, &m)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := m["id"]; ok {
|
|
||||||
h.ID = v.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := m["command"]; ok {
|
|
||||||
h.Command = v.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := m["cwd"]; ok {
|
|
||||||
h.Cwd = v.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := m["secret"]; ok {
|
|
||||||
h.Secret = v.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := m["args"]; ok {
|
|
||||||
h.Args = make([]string, 0)
|
|
||||||
|
|
||||||
for i := range v.([]interface{}) {
|
|
||||||
h.Args = append(h.Args, v.([]interface{})[i].(string))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := m["trigger-rule"]; ok {
|
|
||||||
rule := v.(map[string]interface{})
|
|
||||||
|
|
||||||
if ruleValue, ok := rule["match"]; ok {
|
|
||||||
ruleString, _ := json.Marshal(ruleValue)
|
|
||||||
rulePtr := new(rules.MatchRule)
|
|
||||||
|
|
||||||
err = json.Unmarshal(ruleString, rulePtr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Rule = *rulePtr
|
|
||||||
|
|
||||||
} else if ruleValue, ok := rule["not"]; ok {
|
|
||||||
ruleString, _ := json.Marshal(ruleValue)
|
|
||||||
rulePtr := new(rules.NotRule)
|
|
||||||
|
|
||||||
err = json.Unmarshal(ruleString, rulePtr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Rule = *rulePtr
|
|
||||||
|
|
||||||
} else if ruleValue, ok := rule["and"]; ok {
|
|
||||||
ruleString, _ := json.Marshal(ruleValue)
|
|
||||||
rulePtr := new(rules.AndRule)
|
|
||||||
|
|
||||||
err = json.Unmarshal(ruleString, rulePtr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Rule = *rulePtr
|
|
||||||
|
|
||||||
} else if ruleValue, ok := rule["or"]; ok {
|
|
||||||
ruleString, _ := json.Marshal(ruleValue)
|
|
||||||
rulePtr := new(rules.OrRule)
|
|
||||||
|
|
||||||
err = json.Unmarshal(ruleString, rulePtr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Rule = *rulePtr
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates an instance of Hooks, tries to unmarshal contents of hookFile
|
|
||||||
// and returns a pointer to the newly created instance
|
|
||||||
func New(hookFile string) (*Hooks, error) {
|
|
||||||
h := &Hooks{fileName: hookFile}
|
|
||||||
|
|
||||||
if hookFile == "" {
|
|
||||||
return h, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse hook file for hooks
|
|
||||||
file, e := ioutil.ReadFile(hookFile)
|
|
||||||
|
|
||||||
if e != nil {
|
|
||||||
return h, e
|
|
||||||
}
|
|
||||||
|
|
||||||
e = json.Unmarshal(file, &(h.list))
|
|
||||||
|
|
||||||
h.SetDefaults()
|
|
||||||
|
|
||||||
return h, e
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match looks for the hook with the given id in the list of hooks
|
|
||||||
// and returns the pointer to the hook if it exists, or nil if it doesn't exist
|
|
||||||
func (h *Hooks) Match(id string, params interface{}) *Hook {
|
|
||||||
for i := range h.list {
|
|
||||||
if h.list[i].ID == id {
|
|
||||||
if h.list[i].Rule == nil || (h.list[i].Rule != nil && h.list[i].Rule.Evaluate(params)) {
|
|
||||||
return &h.list[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count returns number of hooks in the list
|
|
||||||
func (h *Hooks) Count() int {
|
|
||||||
return len(h.list)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDefaults sets default values that were ommited for hooks in JSON file
|
|
||||||
func (h *Hooks) SetDefaults() {
|
|
||||||
for i := range h.list {
|
|
||||||
if h.list[i].Cwd == "" {
|
|
||||||
h.list[i].Cwd = "."
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.list[i].Args == nil {
|
|
||||||
h.list[i].Args = make([]string, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-262
@@ -1,262 +0,0 @@
|
|||||||
package rules
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/adnanh/webhook/helpers"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Rule interface
|
|
||||||
type Rule interface {
|
|
||||||
Evaluate(params interface{}) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// AndRule type is a structure that contains list of rules (SubRules) that will be evaluated,
|
|
||||||
// and the AndRule's Evaluate method will evaluate to true if and only if all
|
|
||||||
// of the SubRules evaluate to true
|
|
||||||
type AndRule struct {
|
|
||||||
SubRules []Rule `json:"and"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OrRule type is a structure that contains list of rules (SubRules) that will be evaluated,
|
|
||||||
// and the OrRule's Evaluate method will evaluate to true if any of the SubRules
|
|
||||||
// evaluate to true
|
|
||||||
type OrRule struct {
|
|
||||||
SubRules []Rule `json:"or"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotRule type is a structure that contains a single rule (SubRule) that will be evaluated,
|
|
||||||
// and the OrRule's Evaluate method will evaluate to true if any and only if
|
|
||||||
// the SubRule evaluates to false
|
|
||||||
type NotRule struct {
|
|
||||||
SubRule Rule `json:"not"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MatchRule type is a structure that contains MatchParameter structure
|
|
||||||
type MatchRule struct {
|
|
||||||
MatchParameter MatchParameter `json:"match"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MatchParameter type is a structure that contains Parameter and Value which are used in
|
|
||||||
// Match
|
|
||||||
type MatchParameter struct {
|
|
||||||
Parameter string `json:"parameter"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate AndRule will return true if and only if all of SubRules evaluate to true
|
|
||||||
func (r AndRule) Evaluate(params interface{}) bool {
|
|
||||||
res := true
|
|
||||||
|
|
||||||
for _, v := range r.SubRules {
|
|
||||||
res = res && v.Evaluate(params)
|
|
||||||
if res == false {
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate OrRule will return true if any of SubRules evaluate to true
|
|
||||||
func (r OrRule) Evaluate(params interface{}) bool {
|
|
||||||
res := false
|
|
||||||
|
|
||||||
for _, v := range r.SubRules {
|
|
||||||
res = res || v.Evaluate(params)
|
|
||||||
if res == true {
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate NotRule will return true if and only if SubRule evaluates to false
|
|
||||||
func (r NotRule) Evaluate(params interface{}) bool {
|
|
||||||
return !r.SubRule.Evaluate(params)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate MatchRule will return true if and only if the MatchParameter.Parameter
|
|
||||||
// named property value in supplied params matches the MatchParameter.Value
|
|
||||||
func (r MatchRule) Evaluate(params interface{}) bool {
|
|
||||||
if v, ok := helpers.ExtractJSONParameter(r.MatchParameter.Parameter, params); ok {
|
|
||||||
return v == r.MatchParameter.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON implementation for the MatchRule type
|
|
||||||
func (r *MatchRule) UnmarshalJSON(j []byte) error {
|
|
||||||
err := json.Unmarshal(j, &r.MatchParameter)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON implementation for the NotRule type
|
|
||||||
func (r *NotRule) UnmarshalJSON(j []byte) error {
|
|
||||||
m := make(map[string]interface{})
|
|
||||||
err := json.Unmarshal(j, &m)
|
|
||||||
|
|
||||||
if ruleValue, ok := m["match"]; ok {
|
|
||||||
ruleString, _ := json.Marshal(ruleValue)
|
|
||||||
rulePtr := new(MatchRule)
|
|
||||||
|
|
||||||
err = json.Unmarshal(ruleString, &rulePtr.MatchParameter)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.SubRule = *rulePtr
|
|
||||||
|
|
||||||
} else if ruleValue, ok := m["not"]; ok {
|
|
||||||
ruleString, _ := json.Marshal(ruleValue)
|
|
||||||
rulePtr := new(NotRule)
|
|
||||||
|
|
||||||
err = json.Unmarshal(ruleString, rulePtr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.SubRule = *rulePtr
|
|
||||||
|
|
||||||
} else if ruleValue, ok := m["and"]; ok {
|
|
||||||
ruleString, _ := json.Marshal(ruleValue)
|
|
||||||
rulePtr := new(AndRule)
|
|
||||||
|
|
||||||
err = json.Unmarshal(ruleString, rulePtr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.SubRule = *rulePtr
|
|
||||||
|
|
||||||
} else if ruleValue, ok := m["or"]; ok {
|
|
||||||
ruleString, _ := json.Marshal(ruleValue)
|
|
||||||
rulePtr := new(OrRule)
|
|
||||||
|
|
||||||
err = json.Unmarshal(ruleString, rulePtr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.SubRule = *rulePtr
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON implementation for the AndRule type
|
|
||||||
func (r *AndRule) UnmarshalJSON(j []byte) error {
|
|
||||||
rules := new([]interface{})
|
|
||||||
err := json.Unmarshal(j, &rules)
|
|
||||||
|
|
||||||
for _, rulesValue := range *rules {
|
|
||||||
m := rulesValue.(map[string]interface{})
|
|
||||||
|
|
||||||
if ruleValue, ok := m["match"]; ok {
|
|
||||||
ruleString, _ := json.Marshal(ruleValue)
|
|
||||||
rulePtr := new(MatchRule)
|
|
||||||
|
|
||||||
err = json.Unmarshal(ruleString, &rulePtr.MatchParameter)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.SubRules = append(r.SubRules, *rulePtr)
|
|
||||||
|
|
||||||
} else if ruleValue, ok := m["not"]; ok {
|
|
||||||
ruleString, _ := json.Marshal(ruleValue)
|
|
||||||
rulePtr := new(NotRule)
|
|
||||||
|
|
||||||
err = json.Unmarshal(ruleString, rulePtr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.SubRules = append(r.SubRules, *rulePtr)
|
|
||||||
|
|
||||||
} else if ruleValue, ok := m["and"]; ok {
|
|
||||||
ruleString, _ := json.Marshal(ruleValue)
|
|
||||||
rulePtr := new(AndRule)
|
|
||||||
|
|
||||||
err = json.Unmarshal(ruleString, rulePtr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.SubRules = append(r.SubRules, *rulePtr)
|
|
||||||
|
|
||||||
} else if ruleValue, ok := m["or"]; ok {
|
|
||||||
ruleString, _ := json.Marshal(ruleValue)
|
|
||||||
rulePtr := new(OrRule)
|
|
||||||
|
|
||||||
err = json.Unmarshal(ruleString, rulePtr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.SubRules = append(r.SubRules, *rulePtr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON implementation for the OrRule type
|
|
||||||
func (r *OrRule) UnmarshalJSON(j []byte) error {
|
|
||||||
rules := new([]interface{})
|
|
||||||
err := json.Unmarshal(j, &rules)
|
|
||||||
|
|
||||||
for _, rulesValue := range *rules {
|
|
||||||
m := rulesValue.(map[string]interface{})
|
|
||||||
|
|
||||||
if ruleValue, ok := m["match"]; ok {
|
|
||||||
ruleString, _ := json.Marshal(ruleValue)
|
|
||||||
rulePtr := new(MatchRule)
|
|
||||||
|
|
||||||
err = json.Unmarshal(ruleString, &rulePtr.MatchParameter)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.SubRules = append(r.SubRules, *rulePtr)
|
|
||||||
|
|
||||||
} else if ruleValue, ok := m["not"]; ok {
|
|
||||||
ruleString, _ := json.Marshal(ruleValue)
|
|
||||||
rulePtr := new(NotRule)
|
|
||||||
|
|
||||||
err = json.Unmarshal(ruleString, rulePtr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.SubRules = append(r.SubRules, *rulePtr)
|
|
||||||
|
|
||||||
} else if ruleValue, ok := m["and"]; ok {
|
|
||||||
ruleString, _ := json.Marshal(ruleValue)
|
|
||||||
rulePtr := new(AndRule)
|
|
||||||
|
|
||||||
err = json.Unmarshal(ruleString, rulePtr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.SubRules = append(r.SubRules, *rulePtr)
|
|
||||||
|
|
||||||
} else if ruleValue, ok := m["or"]; ok {
|
|
||||||
ruleString, _ := json.Marshal(ruleValue)
|
|
||||||
rulePtr := new(OrRule)
|
|
||||||
|
|
||||||
err = json.Unmarshal(ruleString, rulePtr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.SubRules = append(r.SubRules, *rulePtr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
+192
-91
@@ -5,134 +5,235 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/adnanh/webhook/helpers"
|
"github.com/adnanh/webhook/helpers"
|
||||||
"github.com/adnanh/webhook/hooks"
|
"github.com/adnanh/webhook/hook"
|
||||||
|
|
||||||
"github.com/go-martini/martini"
|
"github.com/codegangsta/negroni"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
l4g "code.google.com/p/log4go"
|
fsnotify "gopkg.in/fsnotify.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
version string = "1.0.4"
|
version = "2.2.2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
webhooks *hooks.Hooks
|
ip = flag.String("ip", "", "ip the webhook should serve hooks on")
|
||||||
appStart time.Time
|
port = flag.Int("port", 9000, "port the webhook should serve hooks on")
|
||||||
ip = flag.String("ip", "", "ip the webhook server should listen on")
|
verbose = flag.Bool("verbose", false, "show verbose output")
|
||||||
port = flag.Int("port", 9000, "port the webhook server should listen on")
|
hotReload = flag.Bool("hotreload", false, "watch hooks file for changes and reload them automatically")
|
||||||
hooksFilename = flag.String("hooks", "hooks.json", "path to the json file containing defined hooks the webhook should serve")
|
hooksFilePath = flag.String("hooks", "hooks.json", "path to the json file containing defined hooks the webhook should serve")
|
||||||
logFilename = flag.String("log", "webhook.log", "path to the log file")
|
hooksURLPrefix = flag.String("urlprefix", "hooks", "url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id)")
|
||||||
|
secure = flag.Bool("secure", false, "use HTTPS instead of HTTP")
|
||||||
|
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")
|
||||||
|
|
||||||
|
watcher *fsnotify.Watcher
|
||||||
|
|
||||||
|
hooks hook.Hooks
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
hooks = hook.Hooks{}
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
fileLogWriter := l4g.NewFileLogWriter(*logFilename, false)
|
log.SetPrefix("[webhook] ")
|
||||||
fileLogWriter.SetRotateDaily(false)
|
log.SetFlags(log.Ldate | log.Ltime)
|
||||||
|
|
||||||
martini.Env = "production"
|
if !*verbose {
|
||||||
|
log.SetOutput(ioutil.Discard)
|
||||||
|
}
|
||||||
|
|
||||||
l4g.AddFilter("file", l4g.FINE, fileLogWriter)
|
log.Println("version " + version + " starting")
|
||||||
|
|
||||||
|
// load and parse hooks
|
||||||
|
log.Printf("attempting to load hooks from %s\n", *hooksFilePath)
|
||||||
|
|
||||||
|
err := hooks.LoadFromFile(*hooksFilePath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("couldn't load hooks from file! %+v\n", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("loaded %d hook(s) from file\n", len(hooks))
|
||||||
|
|
||||||
|
for _, hook := range hooks {
|
||||||
|
log.Printf("\t> %s\n", hook.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
appStart = time.Now()
|
if *hotReload {
|
||||||
var e error
|
// set up file watcher
|
||||||
|
log.Printf("setting up file watcher for %s\n", *hooksFilePath)
|
||||||
|
|
||||||
webhooks, e = hooks.New(*hooksFilename)
|
var err error
|
||||||
|
|
||||||
if e != nil {
|
watcher, err = fsnotify.NewWatcher()
|
||||||
l4g.Warn("Error occurred while loading hooks from %s: %s", *hooksFilename, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
web := martini.Classic()
|
|
||||||
|
|
||||||
web.Get("/", rootHandler)
|
|
||||||
web.Get("/hook/:id", hookHandler)
|
|
||||||
web.Post("/hook/:id", hookHandler)
|
|
||||||
|
|
||||||
l4g.Info("Starting webhook %s with %d hook(s) on %s:%d", version, webhooks.Count(), *ip, *port)
|
|
||||||
|
|
||||||
web.RunOnAddr(fmt.Sprintf("%s:%d", *ip, *port))
|
|
||||||
}
|
|
||||||
|
|
||||||
func rootHandler() string {
|
|
||||||
return fmt.Sprintf("webhook %s running for %s serving %d hook(s)\n", version, time.Since(appStart).String(), webhooks.Count())
|
|
||||||
}
|
|
||||||
|
|
||||||
func jsonHandler(id string, body []byte, signature string, payload interface{}) {
|
|
||||||
if hook := webhooks.Match(id, payload); hook != nil {
|
|
||||||
if hook.Secret != "" {
|
|
||||||
if signature == "" {
|
|
||||||
l4g.Error("Hook %s got matched and contains the secret, but the request didn't contain any signature.", hook.ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if expectedMAC, ok := helpers.CheckPayloadSignature(body, hook.Secret, signature); !ok {
|
|
||||||
l4g.Error("Hook %s got matched and contains the secret, but the request contained invalid signature. Expected %s, got %s.", hook.ID, expectedMAC, signature)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(hook.Command)
|
|
||||||
cmd.Args = hook.ParseJSONArgs(payload)
|
|
||||||
cmd.Dir = hook.Cwd
|
|
||||||
out, err := cmd.Output()
|
|
||||||
l4g.Info("Hook %s triggered successfully! Command output:\n%s\n%+v", hook.ID, out, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func formHandler(id string, formValues url.Values) {
|
|
||||||
if hook := webhooks.Match(id, helpers.FormValuesToMap(formValues)); hook != nil {
|
|
||||||
cmd := exec.Command(hook.Command)
|
|
||||||
cmd.Args = hook.ParseFormArgs(formValues)
|
|
||||||
cmd.Dir = hook.Cwd
|
|
||||||
out, err := cmd.Output()
|
|
||||||
l4g.Info("Hook %s triggered successfully! Command output:\n%s\n%+v", hook.ID, out, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func hookHandler(req *http.Request, params martini.Params) string {
|
|
||||||
if req.Header.Get("Content-Type") == "application/json" {
|
|
||||||
defer req.Body.Close()
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(req.Body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l4g.Warn("Error occurred while trying to read the request body: %s", err)
|
log.Fatal("error creating file watcher instance", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
payloadJSON := make(map[string]interface{})
|
defer watcher.Close()
|
||||||
|
|
||||||
|
go watchForFileChange()
|
||||||
|
|
||||||
|
err = watcher.Add(*hooksFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("error adding hooks file to the watcher", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l := log.New(os.Stdout, "[webhook] ", log.Ldate|log.Ltime)
|
||||||
|
|
||||||
|
negroniLogger := &negroni.Logger{l}
|
||||||
|
|
||||||
|
negroniRecovery := &negroni.Recovery{
|
||||||
|
Logger: l,
|
||||||
|
PrintStack: true,
|
||||||
|
StackAll: false,
|
||||||
|
StackSize: 1024 * 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
n := negroni.New(negroniRecovery, negroniLogger)
|
||||||
|
|
||||||
|
router := mux.NewRouter()
|
||||||
|
|
||||||
|
var hooksURL string
|
||||||
|
|
||||||
|
if *hooksURLPrefix == "" {
|
||||||
|
hooksURL = "/{id}"
|
||||||
|
} else {
|
||||||
|
hooksURL = "/" + *hooksURLPrefix + "/{id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
router.HandleFunc(hooksURL, hookHandler)
|
||||||
|
|
||||||
|
n.UseHandler(router)
|
||||||
|
|
||||||
|
if *secure {
|
||||||
|
log.Printf("starting secure (https) webhook on %s:%d", *ip, *port)
|
||||||
|
log.Fatal(http.ListenAndServeTLS(fmt.Sprintf("%s:%d", *ip, *port), *cert, *key, n))
|
||||||
|
} else {
|
||||||
|
log.Printf("starting insecure (http) webhook on %s:%d", *ip, *port)
|
||||||
|
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *ip, *port), n))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := mux.Vars(r)["id"]
|
||||||
|
|
||||||
|
hook := hooks.Match(id)
|
||||||
|
|
||||||
|
if hook != nil {
|
||||||
|
log.Printf("%s got matched\n", id)
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error reading the request body. %+v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse headers
|
||||||
|
headers := helpers.ValuesToMap(r.Header)
|
||||||
|
|
||||||
|
// parse query variables
|
||||||
|
query := helpers.ValuesToMap(r.URL.Query())
|
||||||
|
|
||||||
|
// parse body
|
||||||
|
var payload map[string]interface{}
|
||||||
|
|
||||||
|
contentType := r.Header.Get("Content-Type")
|
||||||
|
|
||||||
|
if contentType == "application/json" {
|
||||||
decoder := json.NewDecoder(strings.NewReader(string(body)))
|
decoder := json.NewDecoder(strings.NewReader(string(body)))
|
||||||
decoder.UseNumber()
|
decoder.UseNumber()
|
||||||
|
|
||||||
err = decoder.Decode(&payloadJSON)
|
err := decoder.Decode(&payload)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l4g.Warn("Error occurred while trying to parse the payload as JSON: %s", err)
|
log.Printf("error parsing JSON payload %+v\n", err)
|
||||||
}
|
}
|
||||||
|
} else if contentType == "application/x-www-form-urlencoded" {
|
||||||
payloadSignature := ""
|
fd, err := url.ParseQuery(string(body))
|
||||||
|
if err != nil {
|
||||||
if strings.Contains(req.Header.Get("User-Agent"), "GitHub-Hookshot") {
|
log.Printf("error parsing form payload %+v\n", err)
|
||||||
if len(req.Header.Get("X-Hub-Signature")) > 5 {
|
|
||||||
payloadSignature = req.Header.Get("X-Hub-Signature")[5:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go jsonHandler(params["id"], body, payloadSignature, payloadJSON)
|
|
||||||
} else {
|
} else {
|
||||||
req.ParseForm()
|
payload = helpers.ValuesToMap(fd)
|
||||||
go formHandler(params["id"], req.Form)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "Got it, thanks. :-)"
|
// handle hook
|
||||||
|
go handleHook(hook, &headers, &query, &payload, &body)
|
||||||
|
|
||||||
|
// send the hook defined response message
|
||||||
|
fmt.Fprintf(w, hook.ResponseMessage)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
fmt.Fprintf(w, "Hook not found.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHook(hook *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) {
|
||||||
|
if hook.TriggerRule == nil || hook.TriggerRule != nil && hook.TriggerRule.Evaluate(headers, query, payload, body) {
|
||||||
|
log.Printf("%s hook triggered successfully\n", hook.ID)
|
||||||
|
|
||||||
|
cmd := exec.Command(hook.ExecuteCommand)
|
||||||
|
cmd.Args = hook.ExtractCommandArguments(headers, query, payload)
|
||||||
|
cmd.Dir = hook.CommandWorkingDirectory
|
||||||
|
|
||||||
|
log.Printf("executing %s (%s) with arguments %s using %s as cwd\n", hook.ExecuteCommand, cmd.Path, cmd.Args, cmd.Dir)
|
||||||
|
|
||||||
|
out, err := cmd.Output()
|
||||||
|
|
||||||
|
log.Printf("stdout: %s\n", out)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("stderr: %+v\n", err)
|
||||||
|
}
|
||||||
|
log.Printf("finished handling %s\n", hook.ID)
|
||||||
|
} else {
|
||||||
|
log.Printf("%s hook did not get triggered\n", hook.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func watchForFileChange() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-(*watcher).Events:
|
||||||
|
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||||
|
log.Println("hooks file modified")
|
||||||
|
|
||||||
|
newHooks := hook.Hooks{}
|
||||||
|
|
||||||
|
// parse and swap
|
||||||
|
log.Printf("attempting to reload hooks from %s\n", *hooksFilePath)
|
||||||
|
|
||||||
|
err := newHooks.LoadFromFile(*hooksFilePath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("couldn't load hooks from file! %+v\n", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("loaded %d hook(s) from file\n", len(hooks))
|
||||||
|
|
||||||
|
for _, hook := range hooks {
|
||||||
|
log.Printf("\t> %s\n", hook.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
hooks = newHooks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case err := <-(*watcher).Errors:
|
||||||
|
log.Println("watcher error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user