Compare commits

...

17 Commits

Author SHA1 Message Date
Adnan Hajdarevic
794a16ee13 wip 2020-11-19 22:19:48 +01:00
Adnan Hajdarevic
8431357269 wip 2020-11-19 22:19:10 +01:00
Adnan Hajdarevic
528805584d wip 2020-11-19 22:17:11 +01:00
Adnan Hajdarevic
369cb0d3b6 wip 2020-11-19 21:28:57 +01:00
Adnan Hajdarevic
4f1089495d wip 2020-11-19 21:22:13 +01:00
Adnan Hajdarevic
d4dacd6f8e Merge branch 'development' into feature/context-provider-command 2020-11-19 20:19:37 +01:00
Adnan Hajdarevic
31e317be74 Merge branch 'master' into feature/context-provider-command 2020-11-19 19:50:55 +01:00
Adnan Hajdarevic
c2fd1d82a3 wip 2020-11-19 19:50:16 +01:00
Adnan Hajdarevic
eece0137ef Merge branch 'development' into feature/context-provider-command 2020-10-17 20:21:30 +02:00
Adnan Hajdarević
7467933680
Merge pull request #469 from Maximization/patch-1
Add guide to the README
2020-10-14 15:31:17 +02:00
Maxim Orlov
fd50118712
Add guide to the README 2020-10-13 16:12:01 +02:00
Adnan Hajdarevic
08b351605d Update tests with context source 2020-08-06 20:44:17 +02:00
Adnan Hajdarevic
49b375f625 Merge branch 'development' into feature/context-provider-command 2020-04-28 07:44:55 +02:00
Adnan Hajdarevic
01111b5258 Rename RemoteAddress to RemoteAddr. 2019-12-22 22:37:44 +01:00
Adnan Hajdarevic
64942c9793 Update documentation to include pre-hook-command property. 2019-12-05 22:15:15 +01:00
Adnan Hajdarevic
54cfc6bcbd Rename context-provider-command to pre-hook-command and refactor the code to be more readable. 2019-12-05 22:14:08 +01:00
Adnan Hajdarevic
3ec7da2b15 Add suport for context-provider-command hook option.
The `context-provider-command` allows user to specify a command which will be run whenever the hook gets matched.
Webhook will pass the command a JSON string via the STDIN containing the request context (matched hook id, method used to trigger the hook, remote address, requested host, requested URI, raw body, headers and query values).
The output of the command must be a valid JSON string which will be mapped back into a special source named `context` that can be used with existing rules and directives.
2019-11-22 02:40:59 +01:00
10 changed files with 330 additions and 106 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
coverage
webhook
/test/hookecho
go_build_webhook*

View File

@ -129,6 +129,7 @@ Check out [Hook examples page](docs/Hook-Examples.md) for more complex examples
- [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/)
- [Set up Automated Deployments From Github With Webhook](https://maximorlov.com/automated-deployments-from-github-with-webhook/) by [Maxim Orlov](https://twitter.com/_maximization)
- 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 :-)

View File

@ -22,6 +22,18 @@ Hooks are defined as objects in the JSON or YAML hooks configuration file. Pleas
* `pass-file-to-command` - specifies a list of entries that will be serialized as a file. Incoming [data](Referencing-Request-Values.md) will be serialized in a request-temporary-file (otherwise parallel calls of the hook would lead to concurrent overwritings of the file). The filename to be addressed within the subsequent script is provided via an environment variable. Use `envname` to specify the name of the environment variable. If `envname` is not provided `HOOK_` and the name used to reference the request value are used. Defining `command-working-directory` will store the file relative to this location, if not provided, the systems temporary file directory will be used. If `base64decode` is true, the incoming binary data will be base 64 decoded prior to storing it into the file. By default the corresponding file will be removed after the webhook exited.
* `trigger-rule` - specifies the rule that will be evaluated in order to determine should the hook be triggered. Check [Hook rules page](Hook-Rules.md) to see the list of valid rules and their usage
* `trigger-rule-mismatch-http-response-code` - specifies the HTTP status code to be returned when the trigger rule is not satisfied
* `pre-hook-command` - specifies the command that will be run before the hook gets invoked. Check [Pre-hook command page](PreHook-Command.md) for more details and examples.
* to the STDIN of this command, webhook will pass a JSON string representation of an object with the following properties:
* `hookID` - ID of the hook that got matched
* `method` - HTTP(s) method used by the client (i.e. GET, POST, etc...)
* `URI` - URI which client requested
* `host` - value of the `Host` header sent by the client
* `remoteAddr` - client's IP address and port in the `IP:PORT` format
* `query` - object with query parameters and their respective values
* `headers` - object with headers and their respective values
* `base64EncodedBody` - base64 encoded request body
* Output of this command __MUST__ be valid JSON string which will be parsed by the webhook and accessible using the `pre-hook` as source when referencing values.
* __Important! Any errors encountered while trying to execute the pre-hook command will prevent the hook from triggering!__
## Examples
Check out [Hook examples page](Hook-Examples.md) for more complex examples of hooks.

56
docs/PreHook-Command.md Normal file
View File

@ -0,0 +1,56 @@
# Pre-hook command
To the STDIN of the pre-hook command, webhook will pass a JSON string representation of an object with the following properties:
* `hookID` - ID of the hook that got matched
* `method` - HTTP(s) method used by the client (i.e. GET, POST, etc...)
* `URI` - URI which client requested
* `host` - value of the `Host` header sent by the client
* `remoteAddr` - client's IP address and port in the `IP:PORT` format
* `query` - object with query parameters and their respective values
* `headers` - object with headers and their respective values
* `base64EncodedBody` - base64 encoded request body
_Please note!_ Output of this command __MUST__ be valid JSON string which will be parsed by the webhook and accessible using the `pre-hook` as source when referencing values.
__Important! Any errors encountered while trying to execute the pre-hook command will prevent the hook from triggering!__
# Examples
_Please note:_ Following examples use shell scripts as pre-hook commands, but it is possible to use ruby, python, or anything else you like, as long as it outputs a valid JSON string as the result.
_Make sure you have the `jq` command available, as we're using it to parse the JSON in the pre-hook script._
## Getting the IP address of the requester
<details>
<summary>script.sh</summary>
#!/bin/bash
ip=$1
echo $ip >> ips.txt
</details>
<details>
<summary>prehook.sh</summary>
#!/bin/bash
context=$(cat)
ip=`echo $context | jq -r '.remoteAddr' | cut -d ':' -f 1`
echo "{\"ip\": \"$ip\"}"
</details>
<details>
<summary>hooks.json</summary>
[
{
"id": "log-ip",
"pre-hook-command": "/home/example/prehook.sh",
"execute-command": "/home/example/script.sh",
"pass-arguments-to-command": [
{ "source": "pre-hook", "name": "ip" }
]
}
]
</details>

View File

@ -1,7 +1,17 @@
# Referencing request values
There are three types of request values:
There are four types of request values:
1. HTTP Request Header values
1. Pre-hook values
These are the values provided by the `pre-hook-command` output. More details on the pre-hook command can be found [here](PreHook-Command.md).
```json
{
"source": "pre-hook",
"name": "parameter-name"
}
```
2. HTTP Request Header values
```json
{
@ -10,7 +20,7 @@ There are three types of request values:
}
```
2. HTTP Query parameters
3. HTTP Query parameters
```json
{
@ -19,7 +29,7 @@ There are three types of request values:
}
```
3. Payload (JSON or form-value encoded)
4. Payload (JSON or form-value encoded)
```json
{
"source": "payload",

13
hooks.json Normal file
View File

@ -0,0 +1,13 @@
[
{
"id": "test",
"pre-hook-command": "/home/adnan/test.sh",
"execute-command": "/bin/echo",
"pass-arguments-to-command": [
{
"source": "payload",
"name": "root"
}
]
}
]

View File

@ -17,7 +17,9 @@ import (
"log"
"math"
"net"
"net/http"
"net/textproto"
"net/url"
"os"
"reflect"
"regexp"
@ -35,6 +37,7 @@ const (
SourceQuery string = "url"
SourceQueryAlias string = "query"
SourcePayload string = "payload"
SourcePreHook string = "pre-hook"
SourceString string = "string"
SourceEntirePayload string = "entire-payload"
SourceEntireQuery string = "entire-query"
@ -442,6 +445,8 @@ func (ha *Argument) Get(r *Request) (string, error) {
source = &r.Query
case SourcePayload:
source = &r.Payload
case SourcePreHook:
source = &r.PreHook
case SourceString:
return ha.Name, nil
case SourceEntirePayload:
@ -527,10 +532,23 @@ func (h *HooksFiles) Set(value string) error {
return nil
}
// PreHookContext is a structure consisted of request context data that will be passed to the pre-hook command
type PreHookContext struct {
HookID string `json:"hookID"`
Method string `json:"method"`
Base64EncodedBody string `json:"base64EncodedBody"`
RemoteAddr string `json:"remoteAddr"`
URI string `json:"URI"`
Host string `json:"host"`
Headers http.Header `json:"headers"`
Query url.Values `json:"query"`
}
// Hook type is a structure containing details for a single hook
type Hook struct {
ID string `json:"id,omitempty"`
ExecuteCommand string `json:"execute-command,omitempty"`
PreHookCommand string `json:"pre-hook-command,omitempty"`
CommandWorkingDirectory string `json:"command-working-directory,omitempty"`
ResponseMessage string `json:"response-message,omitempty"`
ResponseHeaders ResponseHeaders `json:"response-headers,omitempty"`
@ -575,6 +593,8 @@ func (h *Hook) ParseJSONParameters(r *Request) []error {
source = &r.Headers
case SourcePayload:
source = &r.Payload
case SourcePreHook:
source = &r.PreHook
case SourceQuery, SourceQueryAlias:
source = &r.Query
}
@ -875,6 +895,7 @@ func (r MatchRule) Evaluate(req *Request) (bool, error) {
if r.Type == IPWhitelist {
return CheckIPWhitelist(req.RawRequest.RemoteAddr, r.IPRange)
}
if r.Type == ScalrSignature {
return CheckScalrSignature(req, r.Secret, true)
}

View File

@ -253,20 +253,22 @@ func TestExtractParameter(t *testing.T) {
}
var argumentGetTests = []struct {
source, name string
headers, query, payload map[string]interface{}
value string
ok bool
source, name string
headers, query, payload, prehook map[string]interface{}
value string
ok bool
}{
{"header", "a", map[string]interface{}{"A": "z"}, nil, nil, "z", true},
{"url", "a", nil, map[string]interface{}{"a": "z"}, nil, "z", true},
{"payload", "a", nil, nil, map[string]interface{}{"a": "z"}, "z", true},
{"string", "a", nil, nil, map[string]interface{}{"a": "z"}, "a", true},
{"header", "a", map[string]interface{}{"A": "z"}, nil, nil, nil, "z", true},
{"url", "a", nil, map[string]interface{}{"a": "z"}, nil, nil, "z", true},
{"payload", "a", nil, nil, map[string]interface{}{"a": "z"}, nil, "z", true},
{"prehook", "a", nil, nil, nil, map[string]interface{}{"a": "z"}, "z", true},
{"string", "a", nil, nil, nil, nil, "a", true},
// failures
{"header", "a", nil, map[string]interface{}{"a": "z"}, map[string]interface{}{"a": "z"}, "", false}, // nil headers
{"url", "a", map[string]interface{}{"A": "z"}, nil, map[string]interface{}{"a": "z"}, "", false}, // nil query
{"payload", "a", map[string]interface{}{"A": "z"}, map[string]interface{}{"a": "z"}, nil, "", false}, // nil payload
{"foo", "a", map[string]interface{}{"A": "z"}, nil, nil, "", false}, // invalid source
{"header", "a", nil, map[string]interface{}{"a": "z"}, map[string]interface{}{"a": "z"}, nil, "", false}, // nil headers
{"url", "a", map[string]interface{}{"A": "z"}, nil, map[string]interface{}{"a": "z"}, nil, "", false}, // nil query
{"payload", "a", map[string]interface{}{"A": "z"}, map[string]interface{}{"a": "z"}, nil, nil, "", false}, // nil payload
{"prehook", "a", nil, nil, nil, nil, "", false}, // nil prehook
{"foo", "a", map[string]interface{}{"A": "z"}, nil, nil, nil, "", false}, // invalid source
}
func TestArgumentGet(t *testing.T) {
@ -276,6 +278,7 @@ func TestArgumentGet(t *testing.T) {
Headers: tt.headers,
Query: tt.query,
Payload: tt.payload,
PreHook: tt.prehook,
}
value, err := a.Get(r)
if (err == nil) != tt.ok || value != tt.value {
@ -285,19 +288,20 @@ func TestArgumentGet(t *testing.T) {
}
var hookParseJSONParametersTests = []struct {
params []Argument
headers, query, payload map[string]interface{}
rheaders, rquery, rpayload map[string]interface{}
ok bool
params []Argument
headers, query, payload, prehook map[string]interface{}
rheaders, rquery, rpayload, rprehook map[string]interface{}
ok bool
}{
{[]Argument{Argument{"header", "a", "", false}}, map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, true},
{[]Argument{Argument{"url", "a", "", false}}, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
{[]Argument{Argument{"payload", "a", "", false}}, nil, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
{[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": `{}`}, nil, nil, map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true},
{[]Argument{Argument{"header", "a", "", false}}, map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, nil, map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, nil, true},
{[]Argument{Argument{"url", "a", "", false}}, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, nil, true},
{[]Argument{Argument{"payload", "a", "", false}}, nil, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true},
{[]Argument{Argument{"prehook", "a", "", false}}, nil, nil, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true},
{[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": `{}`}, nil, nil, nil, map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, nil, true},
// failures
{[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, false}, // empty string
{[]Argument{Argument{"header", "y", "", false}}, map[string]interface{}{"X": `{}`}, nil, nil, map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter
{[]Argument{Argument{"string", "z", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source
{[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, nil, false}, // empty string
{[]Argument{Argument{"header", "y", "", false}}, map[string]interface{}{"X": `{}`}, nil, nil, nil, map[string]interface{}{"X": `{}`}, nil, nil, nil, false}, // missing parameter
{[]Argument{Argument{"string", "z", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, nil, false}, // invalid argument source
}
func TestHookParseJSONParameters(t *testing.T) {
@ -307,6 +311,7 @@ func TestHookParseJSONParameters(t *testing.T) {
Headers: tt.headers,
Query: tt.query,
Payload: tt.payload,
PreHook: tt.prehook,
}
err := h.ParseJSONParameters(r)
if (err == nil) != tt.ok || !reflect.DeepEqual(tt.headers, tt.rheaders) {
@ -316,15 +321,15 @@ func TestHookParseJSONParameters(t *testing.T) {
}
var hookExtractCommandArgumentsTests = []struct {
exec string
args []Argument
headers, query, payload map[string]interface{}
value []string
ok bool
exec string
args []Argument
headers, query, payload, prehook map[string]interface{}
value []string
ok bool
}{
{"test", []Argument{Argument{"header", "a", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"test", "z"}, true},
{"test", []Argument{Argument{"header", "a", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, nil, []string{"test", "z"}, true},
// failures
{"fail", []Argument{Argument{"payload", "a", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"fail", ""}, false},
{"fail", []Argument{Argument{"payload", "a", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, nil, []string{"fail", ""}, false},
}
func TestHookExtractCommandArguments(t *testing.T) {
@ -334,6 +339,7 @@ func TestHookExtractCommandArguments(t *testing.T) {
Headers: tt.headers,
Query: tt.query,
Payload: tt.payload,
PreHook: tt.prehook,
}
value, err := h.ExtractCommandArguments(r)
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
@ -362,24 +368,24 @@ func TestHookExtractCommandArguments(t *testing.T) {
// }
// ]
var hookExtractCommandArgumentsForEnvTests = []struct {
exec string
args []Argument
headers, query, payload map[string]interface{}
value []string
ok bool
exec string
args []Argument
headers, query, payload, prehook map[string]interface{}
value []string
ok bool
}{
// successes
{
"test",
[]Argument{Argument{"header", "a", "", false}},
map[string]interface{}{"A": "z"}, nil, nil,
map[string]interface{}{"A": "z"}, nil, nil, nil,
[]string{"HOOK_a=z"},
true,
},
{
"test",
[]Argument{Argument{"header", "a", "MYKEY", false}},
map[string]interface{}{"A": "z"}, nil, nil,
map[string]interface{}{"A": "z"}, nil, nil, nil,
[]string{"MYKEY=z"},
true,
},
@ -387,7 +393,7 @@ var hookExtractCommandArgumentsForEnvTests = []struct {
{
"fail",
[]Argument{Argument{"payload", "a", "", false}},
map[string]interface{}{"A": "z"}, nil, nil,
map[string]interface{}{"A": "z"}, nil, nil, nil,
[]string{},
false,
},
@ -400,6 +406,7 @@ func TestHookExtractCommandArgumentsForEnv(t *testing.T) {
Headers: tt.headers,
Query: tt.query,
Payload: tt.payload,
PreHook: tt.prehook,
}
value, err := h.ExtractCommandArgumentsForEnv(r)
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
@ -479,43 +486,44 @@ func TestHooksMatch(t *testing.T) {
var matchRuleTests = []struct {
typ, regex, secret, value, ipRange string
param Argument
headers, query, payload map[string]interface{}
headers, query, payload, prehook map[string]interface{}
body []byte
remoteAddr string
ok bool
err bool
}{
{"value", "", "", "z", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
{"regex", "^z", "", "z", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
{"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
{"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
{"value", "", "", "z", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, "", true, false},
{"regex", "^z", "", "z", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, "", true, false},
{"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, nil, []byte(`{"a": "z"}`), "", true, false},
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, nil, []byte(`{"a": "z"}`), "", true, false},
{"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, nil, []byte(`{"a": "z"}`), "", true, false},
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, nil, []byte(`{"a": "z"}`), "", true, false},
// failures
{"value", "", "", "X", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
{"regex", "^X", "", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
{"value", "", "2", "X", "", Argument{"header", "a", "", false}, map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, true}, // reference invalid header
{"value", "", "", "X", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, "", false, false},
{"regex", "^X", "", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, "", false, false},
{"value", "", "2", "X", "", Argument{"header", "a", "", false}, map[string]interface{}{"Y": "z"}, nil, nil, nil, []byte{}, "", false, true}, // reference invalid header
// errors
{"regex", "*", "", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex
{"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"payload-hmac-sha512", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"payload-hash-sha512", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"regex", "*", "", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, "", false, true}, // invalid regex
{"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"payload-hmac-sha512", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, nil, []byte{}, "", false, true}, // invalid hmac
{"payload-hash-sha512", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, nil, []byte{}, "", false, true}, // invalid hmac
// IP whitelisting, valid cases
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
{"ip-whitelist", "", "", "", "192.168.0.1", Argument{}, nil, nil, nil, []byte{}, "192.168.0.1:9000", true, false}, // valid IPv4, no range
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, with range
{"ip-whitelist", "", "", "", "::1", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, no range
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
{"ip-whitelist", "", "", "", "192.168.0.1", Argument{}, nil, nil, nil, nil, []byte{}, "192.168.0.1:9000", true, false}, // valid IPv4, no range
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, with range
{"ip-whitelist", "", "", "", "::1", Argument{}, nil, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, no range
// IP whitelisting, invalid cases
{"ip-whitelist", "", "", "", "192.168.0.1/a", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, with range
{"ip-whitelist", "", "", "", "192.168.0.a", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, no range
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.a:9000", false, true}, // invalid IPv4 address
{"ip-whitelist", "", "", "", "::1/a", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, with range
{"ip-whitelist", "", "", "", "::z", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, no range
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, []byte{}, "[::z]:9000", false, true}, // invalid IPv6 address
{"ip-whitelist", "", "", "", "192.168.0.1/a", Argument{}, nil, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, with range
{"ip-whitelist", "", "", "", "192.168.0.a", Argument{}, nil, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, no range
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, nil, []byte{}, "192.168.0.a:9000", false, true}, // invalid IPv4 address
{"ip-whitelist", "", "", "", "::1/a", Argument{}, nil, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, with range
{"ip-whitelist", "", "", "", "::z", Argument{}, nil, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, no range
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, nil, []byte{}, "[::z]:9000", false, true}, // invalid IPv6 address
}
func TestMatchRule(t *testing.T) {
@ -525,6 +533,7 @@ func TestMatchRule(t *testing.T) {
Headers: tt.headers,
Query: tt.query,
Payload: tt.payload,
PreHook: tt.prehook,
Body: tt.body,
RawRequest: &http.Request{
RemoteAddr: tt.remoteAddr,
@ -538,12 +547,12 @@ func TestMatchRule(t *testing.T) {
}
var andRuleTests = []struct {
desc string // description of the test case
rule AndRule
headers, query, payload map[string]interface{}
body []byte
ok bool
err bool
desc string // description of the test case
rule AndRule
headers, query, payload, prehook map[string]interface{}
body []byte
ok bool
err bool
}{
{
"(a=z, b=y): a=z && b=y",
@ -551,8 +560,7 @@ var andRuleTests = []struct {
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
},
map[string]interface{}{"A": "z", "B": "y"}, nil, nil,
[]byte{},
map[string]interface{}{"A": "z", "B": "y"}, nil, nil, nil, []byte{},
true, false,
},
{
@ -561,8 +569,7 @@ var andRuleTests = []struct {
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
},
map[string]interface{}{"A": "z", "B": "Y"}, nil, nil,
[]byte{},
map[string]interface{}{"A": "z", "B": "Y"}, nil, nil, nil, []byte{},
false, false,
},
// Complex test to cover Rules.Evaluate
@ -588,16 +595,15 @@ var andRuleTests = []struct {
},
},
},
map[string]interface{}{"A": "z", "B": "y", "C": "x", "D": "w", "E": "X", "F": "X"}, nil, nil,
[]byte{},
map[string]interface{}{"A": "z", "B": "y", "C": "x", "D": "w", "E": "X", "F": "X"}, nil, nil, nil, []byte{},
true, false,
},
{"empty rule", AndRule{{}}, nil, nil, nil, nil, false, false},
{"empty rule", AndRule{{}}, nil, nil, nil, nil, nil, false, false},
// failures
{
"invalid rule",
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}},
map[string]interface{}{"Y": "z"}, nil, nil, nil,
map[string]interface{}{"Y": "z"}, nil, nil, nil, nil,
false, true,
},
}
@ -608,6 +614,7 @@ func TestAndRule(t *testing.T) {
Headers: tt.headers,
Query: tt.query,
Payload: tt.payload,
PreHook: tt.prehook,
Body: tt.body,
}
ok, err := tt.rule.Evaluate(r)
@ -618,12 +625,12 @@ func TestAndRule(t *testing.T) {
}
var orRuleTests = []struct {
desc string // description of the test case
rule OrRule
headers, query, payload map[string]interface{}
body []byte
ok bool
err bool
desc string // description of the test case
rule OrRule
headers, query, payload, prehook map[string]interface{}
body []byte
ok bool
err bool
}{
{
"(a=z, b=X): a=z || b=y",
@ -631,8 +638,7 @@ var orRuleTests = []struct {
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
},
map[string]interface{}{"A": "z", "B": "X"}, nil, nil,
[]byte{},
map[string]interface{}{"A": "z", "B": "X"}, nil, nil, nil, []byte{},
true, false,
},
{
@ -641,8 +647,7 @@ var orRuleTests = []struct {
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
},
map[string]interface{}{"A": "X", "B": "y"}, nil, nil,
[]byte{},
map[string]interface{}{"A": "X", "B": "y"}, nil, nil, nil, []byte{},
true, false,
},
{
@ -651,8 +656,7 @@ var orRuleTests = []struct {
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}},
},
map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil,
[]byte{},
map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil, nil, []byte{},
false, false,
},
// failures
@ -661,8 +665,7 @@ var orRuleTests = []struct {
OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}},
},
map[string]interface{}{"Y": "Z"}, nil, nil,
[]byte{},
map[string]interface{}{"Y": "Z"}, nil, nil, nil, []byte{},
false, false,
},
}
@ -673,6 +676,7 @@ func TestOrRule(t *testing.T) {
Headers: tt.headers,
Query: tt.query,
Payload: tt.payload,
PreHook: tt.prehook,
Body: tt.body,
}
ok, err := tt.rule.Evaluate(r)
@ -683,15 +687,15 @@ func TestOrRule(t *testing.T) {
}
var notRuleTests = []struct {
desc string // description of the test case
rule NotRule
headers, query, payload map[string]interface{}
body []byte
ok bool
err bool
desc string // description of the test case
rule NotRule
headers, query, payload, prehook map[string]interface{}
body []byte
ok bool
err bool
}{
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, true, false},
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, nil, []byte{}, false, false},
}
func TestNotRule(t *testing.T) {
@ -700,6 +704,7 @@ func TestNotRule(t *testing.T) {
Headers: tt.headers,
Query: tt.query,
Payload: tt.payload,
PreHook: tt.prehook,
Body: tt.body,
}
ok, err := tt.rule.Evaluate(r)

View File

@ -31,6 +31,9 @@ type Request struct {
// Payload is a map of the parsed payload.
Payload map[string]interface{}
// PreHook is a map of the parsed pre-hook command result
PreHook map[string]interface{}
// The underlying HTTP request.
RawRequest *http.Request
}

View File

@ -2,9 +2,11 @@ package main
import (
"crypto/tls"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
@ -469,6 +471,106 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("[%s] error parsing body payload due to unsupported content type header: %s\n", req.ID, req.ContentType)
}
if matchedHook.PreHookCommand != "" {
// check the command exists
var searchPath string
if filepath.IsAbs(matchedHook.PreHookCommand) || matchedHook.CommandWorkingDirectory == "" {
searchPath = matchedHook.PreHookCommand
} else {
searchPath = filepath.Join(matchedHook.CommandWorkingDirectory, matchedHook.PreHookCommand)
}
preHookCommandPath, err := exec.LookPath(searchPath)
if err != nil {
log.Printf("[%s] unable to locate pre-hook command: '%s', %+v\n", req.ID, matchedHook.PreHookCommand, err)
// check if parameters specified in pre-hook command by mistake
if strings.IndexByte(matchedHook.PreHookCommand, ' ') != -1 {
s := strings.Fields(matchedHook.PreHookCommand)[0]
log.Printf("[%s] please use a wrapper script to provide arguments to pre-hook command for '%s'\n", req.ID, s)
}
writeHttpResponseCode(w, req.ID, matchedHook.ID, http.StatusInternalServerError)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprint(w, "Error occurred while executing the hook's pre-hook command. Please check your logs for more details.")
return
}
preHookCommandStdin := hook.PreHookContext{
HookID: matchedHook.ID,
Method: r.Method,
Base64EncodedBody: base64.StdEncoding.EncodeToString(req.Body),
RemoteAddr: r.RemoteAddr,
URI: r.RequestURI,
Host: r.Host,
Headers: r.Header,
Query: r.URL.Query(),
}
preHookCommandStdinJSONString, err := json.Marshal(preHookCommandStdin)
if err != nil {
log.Printf("[%s] unable to encode pre-hook context as JSON string for the pre-hook command: %+v\n", req.ID, err)
writeHttpResponseCode(w, req.ID, matchedHook.ID, http.StatusInternalServerError)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprint(w, "Error occurred while executing the hook's pre-hook command. Please check your logs for more details.")
return
}
preHookCommand := exec.Command(preHookCommandPath)
preHookCommand.Dir = matchedHook.CommandWorkingDirectory
preHookCommand.Env = append(os.Environ())
preHookCommandStdinPipe, err := preHookCommand.StdinPipe()
if err != nil {
log.Printf("[%s] unable to acquire stdin pipe for the pre-hook command: %+v\n", req.ID, err)
writeHttpResponseCode(w, req.ID, matchedHook.ID, http.StatusInternalServerError)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprint(w, "Error occurred while executing the hook's pre-hook command. Please check your logs for more details.")
return
}
_, err = io.WriteString(preHookCommandStdinPipe, string(preHookCommandStdinJSONString))
preHookCommandStdinPipe.Close()
if err != nil {
log.Printf("[%s] unable to write to pre-hook command stdin: %+v\n", req.ID, err)
writeHttpResponseCode(w, req.ID, matchedHook.ID, http.StatusInternalServerError)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprint(w, "Error occurred while executing the hook's pre-hook command. Please check your logs for more details.")
return
}
log.Printf("[%s] executing pre-hook command %s (%s) using %s as cwd\n", req.ID, matchedHook.PreHookCommand, preHookCommand.Path, preHookCommand.Dir)
preHookCommandOutput, err := preHookCommand.CombinedOutput()
if err != nil {
log.Printf("[%s] unable to execute pre-hook command: %+v\n", req.ID, err)
writeHttpResponseCode(w, req.ID, matchedHook.ID, http.StatusInternalServerError)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprint(w, "Error occurred while executing the hook's pre-hook command. Please check your logs for more details.")
return
}
JSONDecoder := json.NewDecoder(strings.NewReader(string(preHookCommandOutput)))
JSONDecoder.UseNumber()
if err := JSONDecoder.Decode(&req.PreHook); err != nil {
log.Printf("[%s] unable to parse pre-hook command output: %+v\npre-hook command output was: %+v\n", req.ID, err, string(preHookCommandOutput))
writeHttpResponseCode(w, req.ID, matchedHook.ID, http.StatusInternalServerError)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprint(w, "Error occurred while executing the hook's pre-hook command. Please check your logs for more details.")
return
}
}
// handle hook
errors := matchedHook.ParseJSONParameters(req)
for _, err := range errors {