This commit is contained in:
Adnan Hajdarevic 2020-11-19 21:22:13 +01:00
parent d4dacd6f8e
commit 4f1089495d
7 changed files with 123 additions and 73 deletions

View File

@ -32,7 +32,7 @@ Hooks are defined as objects in the JSON or YAML hooks configuration file. Pleas
* `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 `context` as source when referencing values.
* 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.
## Examples
Check out [Hook examples page](Hook-Examples.md) for more complex examples of hooks.

View File

@ -1,12 +1,12 @@
# Referencing request values
There are four types of request values:
1. Context values
1. Pre-hook values
These are the values provided by the `pre-hook-command` output.
```json
{
"source": "context",
"source": "pre-hook",
"name": "parameter-name"
}
```

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

@ -35,7 +35,7 @@ const (
SourceQuery string = "url"
SourceQueryAlias string = "query"
SourcePayload string = "payload"
SourceContext string = "context"
SourcePreHook string = "pre-hook"
SourceString string = "string"
SourceEntirePayload string = "entire-payload"
SourceEntireQuery string = "entire-query"
@ -443,8 +443,8 @@ func (ha *Argument) Get(r *Request) (string, error) {
source = &r.Query
case SourcePayload:
source = &r.Payload
case SourceContext:
source = &r.Context
case SourcePreHook:
source = &r.PreHook
case SourceString:
return ha.Name, nil
case SourceEntirePayload:
@ -591,8 +591,8 @@ func (h *Hook) ParseJSONParameters(r *Request) []error {
source = &r.Headers
case SourcePayload:
source = &r.Payload
case SourceContext:
source = &r.Context
case SourcePreHook:
source = &r.PreHook
case SourceQuery, SourceQueryAlias:
source = &r.Query
}

View File

@ -254,20 +254,20 @@ func TestExtractParameter(t *testing.T) {
var argumentGetTests = []struct {
source, name string
headers, query, payload, context map[string]interface{}
headers, query, payload, prehook map[string]interface{}
value string
ok bool
}{
{"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},
{"context", "a", nil, nil, nil, map[string]interface{}{"a": "z"}, "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"}, 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
{"context", "a", nil, nil, nil, nil, "", false}, // nil context
{"prehook", "a", nil, nil, nil, nil, "", false}, // nil prehook
{"foo", "a", map[string]interface{}{"A": "z"}, nil, nil, nil, "", false}, // invalid source
}
@ -278,7 +278,7 @@ func TestArgumentGet(t *testing.T) {
Headers: tt.headers,
Query: tt.query,
Payload: tt.payload,
Context: tt.context,
PreHook: tt.prehook,
}
value, err := a.Get(r)
if (err == nil) != tt.ok || value != tt.value {
@ -289,14 +289,14 @@ func TestArgumentGet(t *testing.T) {
var hookParseJSONParametersTests = []struct {
params []Argument
headers, query, payload, context map[string]interface{}
rheaders, rquery, rpayload, rcontext map[string]interface{}
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, 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{"context", "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{"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, nil, map[string]interface{}{"Z": ``}, nil, nil, nil, false}, // empty string
@ -311,7 +311,7 @@ func TestHookParseJSONParameters(t *testing.T) {
Headers: tt.headers,
Query: tt.query,
Payload: tt.payload,
Context: tt.context,
PreHook: tt.prehook,
}
err := h.ParseJSONParameters(r)
if (err == nil) != tt.ok || !reflect.DeepEqual(tt.headers, tt.rheaders) {
@ -323,7 +323,7 @@ func TestHookParseJSONParameters(t *testing.T) {
var hookExtractCommandArgumentsTests = []struct {
exec string
args []Argument
headers, query, payload, context map[string]interface{}
headers, query, payload, prehook map[string]interface{}
value []string
ok bool
}{
@ -339,7 +339,7 @@ func TestHookExtractCommandArguments(t *testing.T) {
Headers: tt.headers,
Query: tt.query,
Payload: tt.payload,
Context: tt.context,
PreHook: tt.prehook,
}
value, err := h.ExtractCommandArguments(r)
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
@ -370,7 +370,7 @@ func TestHookExtractCommandArguments(t *testing.T) {
var hookExtractCommandArgumentsForEnvTests = []struct {
exec string
args []Argument
headers, query, payload, context map[string]interface{}
headers, query, payload, prehook map[string]interface{}
value []string
ok bool
}{
@ -406,7 +406,7 @@ func TestHookExtractCommandArgumentsForEnv(t *testing.T) {
Headers: tt.headers,
Query: tt.query,
Payload: tt.payload,
Context: tt.context,
PreHook: tt.prehook,
}
value, err := h.ExtractCommandArgumentsForEnv(r)
if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) {
@ -486,7 +486,7 @@ func TestHooksMatch(t *testing.T) {
var matchRuleTests = []struct {
typ, regex, secret, value, ipRange string
param Argument
headers, query, payload, context map[string]interface{}
headers, query, payload, prehook map[string]interface{}
body []byte
remoteAddr string
ok bool
@ -533,7 +533,7 @@ func TestMatchRule(t *testing.T) {
Headers: tt.headers,
Query: tt.query,
Payload: tt.payload,
Context: tt.context,
PreHook: tt.prehook,
Body: tt.body,
RawRequest: &http.Request{
RemoteAddr: tt.remoteAddr,
@ -549,7 +549,7 @@ func TestMatchRule(t *testing.T) {
var andRuleTests = []struct {
desc string // description of the test case
rule AndRule
headers, query, payload, context map[string]interface{}
headers, query, payload, prehook map[string]interface{}
body []byte
ok bool
err bool
@ -614,7 +614,7 @@ func TestAndRule(t *testing.T) {
Headers: tt.headers,
Query: tt.query,
Payload: tt.payload,
Context: tt.context,
PreHook: tt.prehook,
Body: tt.body,
}
ok, err := tt.rule.Evaluate(r)
@ -627,7 +627,7 @@ func TestAndRule(t *testing.T) {
var orRuleTests = []struct {
desc string // description of the test case
rule OrRule
headers, query, payload, context map[string]interface{}
headers, query, payload, prehook map[string]interface{}
body []byte
ok bool
err bool
@ -676,7 +676,7 @@ func TestOrRule(t *testing.T) {
Headers: tt.headers,
Query: tt.query,
Payload: tt.payload,
Context: tt.context,
PreHook: tt.prehook,
Body: tt.body,
}
ok, err := tt.rule.Evaluate(r)
@ -689,7 +689,7 @@ func TestOrRule(t *testing.T) {
var notRuleTests = []struct {
desc string // description of the test case
rule NotRule
headers, query, payload, context map[string]interface{}
headers, query, payload, prehook map[string]interface{}
body []byte
ok bool
err bool
@ -704,7 +704,7 @@ func TestNotRule(t *testing.T) {
Headers: tt.headers,
Query: tt.query,
Payload: tt.payload,
Context: tt.context,
PreHook: tt.prehook,
Body: tt.body,
}
ok, err := tt.rule.Evaluate(r)

View File

@ -31,8 +31,8 @@ type Request struct {
// Payload is a map of the parsed payload.
Payload map[string]interface{}
// Context is a map of the parsed pre-hook command result
Context 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

@ -473,14 +473,14 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
if matchedHook.PreHookCommand != "" {
// check the command exists
var lookpath string
var searchPath string
if filepath.IsAbs(matchedHook.PreHookCommand) || matchedHook.CommandWorkingDirectory == "" {
lookpath = matchedHook.PreHookCommand
searchPath = matchedHook.PreHookCommand
} else {
lookpath = filepath.Join(matchedHook.CommandWorkingDirectory, matchedHook.PreHookCommand)
searchPath = filepath.Join(matchedHook.CommandWorkingDirectory, matchedHook.PreHookCommand)
}
preHookCommandPath, err := exec.LookPath(lookpath)
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)
@ -489,7 +489,13 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
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)
}
} else {
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,
@ -501,36 +507,67 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
Query: r.URL.Query(),
}
if preHookCommandStdinJSONString, err := json.Marshal(preHookCommandStdin); err != nil {
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)
} else {
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())
if preHookCommandStdinPipe, err := preHookCommand.StdinPipe(); err != nil {
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)
} else {
_, err := io.WriteString(preHookCommandStdinPipe, string(preHookCommandStdinJSONString))
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)
} else {
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)
if preHookCommandOutput, err := preHookCommand.CombinedOutput(); err != nil {
preHookCommandOutput, err := preHookCommand.CombinedOutput()
if err != nil {
log.Printf("[%s] unable to execute pre-hook command: %+v\n", req.ID, err)
} else {
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.Context); err != nil {
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
}
}