From c6603894c1215ded1e717594b05dfb7cdd2dd81b Mon Sep 17 00:00:00 2001 From: Cameron Moore Date: Fri, 25 Sep 2020 19:46:06 -0500 Subject: [PATCH] Add Request object to hook package to simplify API To avoid having to pass around so many parameters to the hook package, create a Request object to store all request-specific data. Update APIs accordingly. --- internal/hook/hook.go | 121 ++++++++++++++++---------- internal/hook/hook_test.go | 172 ++++++++++++++++++++++++------------- webhook.go | 153 +++++++++++++++++---------------- webhook_test.go | 6 +- 4 files changed, 269 insertions(+), 183 deletions(-) diff --git a/internal/hook/hook.go b/internal/hook/hook.go index 2c2d787..e6a9272 100644 --- a/internal/hook/hook.go +++ b/internal/hook/hook.go @@ -17,6 +17,7 @@ import ( "log" "math" "net" + "net/http" "net/textproto" "os" "reflect" @@ -47,6 +48,30 @@ const ( EnvNamespace string = "HOOK_" ) +// Request represents a webhook request. +type Request struct { + // The request ID set by the RequestID middleware. + ID string + + // The Content-Type of the request. + ContentType string + + // The raw request body. + Body []byte + + // Headers is a map of the parsed headers. + Headers map[string]interface{} + + // Query is a map of the parsed URL query values. + Query map[string]interface{} + + // Payload is a map of the parsed payload. + Payload map[string]interface{} + + // The underlying HTTP request. + RawRequest *http.Request +} + // ParameterNodeError describes an error walking a parameter node. type ParameterNodeError struct { key string @@ -222,22 +247,26 @@ func CheckPayloadSignature512(payload []byte, secret, signature string) (string, return ValidateMAC(payload, hmac.New(sha512.New, []byte(secret)), signatures) } -func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey string, checkDate bool) (bool, error) { - // Check for the signature and date headers - if _, ok := headers["X-Signature"]; !ok { +func CheckScalrSignature(r *Request, signingKey string, checkDate bool) (bool, error) { + if r.Headers == nil { return false, nil } - if _, ok := headers["Date"]; !ok { + + // Check for the signature and date headers + if _, ok := r.Headers["X-Signature"]; !ok { + return false, nil + } + if _, ok := r.Headers["Date"]; !ok { return false, nil } if signingKey == "" { return false, errors.New("signature validation signing key can not be empty") } - providedSignature := headers["X-Signature"].(string) - dateHeader := headers["Date"].(string) + providedSignature := r.Headers["X-Signature"].(string) + dateHeader := r.Headers["Date"].(string) mac := hmac.New(sha1.New, []byte(signingKey)) - mac.Write(body) + mac.Write(r.Body) mac.Write([]byte(dateHeader)) expectedSignature := hex.EncodeToString(mac.Sum(nil)) @@ -426,41 +455,41 @@ type Argument struct { // 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, error) { +func (ha *Argument) Get(r *Request) (string, error) { var source *map[string]interface{} key := ha.Name switch ha.Source { case SourceHeader: - source = headers + source = &r.Headers key = textproto.CanonicalMIMEHeaderKey(ha.Name) case SourceQuery, SourceQueryAlias: - source = query + source = &r.Query case SourcePayload: - source = payload + source = &r.Payload case SourceString: return ha.Name, nil case SourceEntirePayload: - r, err := json.Marshal(payload) + res, err := json.Marshal(&r.Payload) if err != nil { return "", err } - return string(r), nil + return string(res), nil case SourceEntireHeaders: - r, err := json.Marshal(headers) + res, err := json.Marshal(&r.Headers) if err != nil { return "", err } - return string(r), nil + return string(res), nil case SourceEntireQuery: - r, err := json.Marshal(query) + res, err := json.Marshal(&r.Query) if err != nil { return "", err } - return string(r), nil + return string(res), nil } if source != nil { @@ -545,11 +574,11 @@ type Hook struct { // ParseJSONParameters decodes specified arguments to JSON objects and replaces the // string with the newly created object -func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface{}) []error { +func (h *Hook) ParseJSONParameters(r *Request) []error { errors := make([]error, 0) for i := range h.JSONStringParameters { - arg, err := h.JSONStringParameters[i].Get(headers, query, payload) + arg, err := h.JSONStringParameters[i].Get(r) if err != nil { errors = append(errors, &ArgumentError{h.JSONStringParameters[i]}) } else { @@ -568,11 +597,11 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface switch h.JSONStringParameters[i].Source { case SourceHeader: - source = headers + source = &r.Headers case SourcePayload: - source = payload + source = &r.Payload case SourceQuery, SourceQueryAlias: - source = query + source = &r.Query } if source != nil { @@ -598,14 +627,14 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface // ExtractCommandArguments creates a list of arguments, based on the // PassArgumentsToCommand property that is ready to be used with exec.Command() -func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) ([]string, []error) { +func (h *Hook) ExtractCommandArguments(r *Request) ([]string, []error) { args := make([]string, 0) errors := make([]error, 0) args = append(args, h.ExecuteCommand) for i := range h.PassArgumentsToCommand { - arg, err := h.PassArgumentsToCommand[i].Get(headers, query, payload) + arg, err := h.PassArgumentsToCommand[i].Get(r) if err != nil { args = append(args, "") errors = append(errors, &ArgumentError{h.PassArgumentsToCommand[i]}) @@ -625,11 +654,11 @@ func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]inter // ExtractCommandArgumentsForEnv creates a list of arguments in key=value // format, based on the PassEnvironmentToCommand property that is ready to be used // with exec.Command(). -func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string]interface{}) ([]string, []error) { +func (h *Hook) ExtractCommandArgumentsForEnv(r *Request) ([]string, []error) { args := make([]string, 0) errors := make([]error, 0) for i := range h.PassEnvironmentToCommand { - arg, err := h.PassEnvironmentToCommand[i].Get(headers, query, payload) + arg, err := h.PassEnvironmentToCommand[i].Get(r) if err != nil { errors = append(errors, &ArgumentError{h.PassEnvironmentToCommand[i]}) continue @@ -661,11 +690,11 @@ type FileParameter struct { // ExtractCommandArgumentsForFile creates a list of arguments in key=value // format, based on the PassFileToCommand property that is ready to be used // with exec.Command(). -func (h *Hook) ExtractCommandArgumentsForFile(headers, query, payload *map[string]interface{}) ([]FileParameter, []error) { +func (h *Hook) ExtractCommandArgumentsForFile(r *Request) ([]FileParameter, []error) { args := make([]FileParameter, 0) errors := make([]error, 0) for i := range h.PassFileToCommand { - arg, err := h.PassFileToCommand[i].Get(headers, query, payload) + arg, err := h.PassFileToCommand[i].Get(r) if err != nil { errors = append(errors, &ArgumentError{h.PassFileToCommand[i]}) continue @@ -772,16 +801,16 @@ type Rules struct { // 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, remoteAddr string) (bool, error) { +func (r Rules) Evaluate(req *Request) (bool, error) { switch { case r.And != nil: - return r.And.Evaluate(headers, query, payload, body, remoteAddr) + return r.And.Evaluate(req) case r.Or != nil: - return r.Or.Evaluate(headers, query, payload, body, remoteAddr) + return r.Or.Evaluate(req) case r.Not != nil: - return r.Not.Evaluate(headers, query, payload, body, remoteAddr) + return r.Not.Evaluate(req) case r.Match != nil: - return r.Match.Evaluate(headers, query, payload, body, remoteAddr) + return r.Match.Evaluate(req) } return false, nil @@ -791,11 +820,11 @@ func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, body *[ 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, remoteAddr string) (bool, error) { +func (r AndRule) Evaluate(req *Request) (bool, error) { res := true for _, v := range r { - rv, err := v.Evaluate(headers, query, payload, body, remoteAddr) + rv, err := v.Evaluate(req) if err != nil { return false, err } @@ -813,11 +842,11 @@ func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, body 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, remoteAddr string) (bool, error) { +func (r OrRule) Evaluate(req *Request) (bool, error) { res := false for _, v := range r { - rv, err := v.Evaluate(headers, query, payload, body, remoteAddr) + rv, err := v.Evaluate(req) if err != nil { return false, err } @@ -835,8 +864,8 @@ func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, body * 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, remoteAddr string) (bool, error) { - rv, err := Rules(r).Evaluate(headers, query, payload, body, remoteAddr) +func (r NotRule) Evaluate(req *Request) (bool, error) { + rv, err := Rules(r).Evaluate(req) return !rv, err } @@ -862,15 +891,15 @@ const ( ) // Evaluate MatchRule will return based on the type -func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) { +func (r MatchRule) Evaluate(req *Request) (bool, error) { if r.Type == IPWhitelist { - return CheckIPWhitelist(remoteAddr, r.IPRange) + return CheckIPWhitelist(req.RawRequest.RemoteAddr, r.IPRange) } if r.Type == ScalrSignature { - return CheckScalrSignature(*headers, *body, r.Secret, true) + return CheckScalrSignature(req, r.Secret, true) } - arg, err := r.Parameter.Get(headers, query, payload) + arg, err := r.Parameter.Get(req) if err == nil { switch r.Type { case MatchValue: @@ -878,13 +907,13 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod case MatchRegex: return regexp.MatchString(r.Regex, arg) case MatchHashSHA1: - _, err := CheckPayloadSignature(*body, r.Secret, arg) + _, err := CheckPayloadSignature(req.Body, r.Secret, arg) return err == nil, err case MatchHashSHA256: - _, err := CheckPayloadSignature256(*body, r.Secret, arg) + _, err := CheckPayloadSignature256(req.Body, r.Secret, arg) return err == nil, err case MatchHashSHA512: - _, err := CheckPayloadSignature512(*body, r.Secret, arg) + _, err := CheckPayloadSignature512(req.Body, r.Secret, arg) return err == nil, err } } diff --git a/internal/hook/hook_test.go b/internal/hook/hook_test.go index 46bd444..c99edfb 100644 --- a/internal/hook/hook_test.go +++ b/internal/hook/hook_test.go @@ -1,6 +1,7 @@ package hook import ( + "net/http" "os" "reflect" "strings" @@ -136,7 +137,7 @@ func TestCheckPayloadSignature512(t *testing.T) { var checkScalrSignatureTests = []struct { description string headers map[string]interface{} - payload []byte + body []byte secret string expectedSignature string ok bool @@ -175,7 +176,11 @@ var checkScalrSignatureTests = []struct { func TestCheckScalrSignature(t *testing.T) { for _, testCase := range checkScalrSignatureTests { - valid, err := CheckScalrSignature(testCase.headers, testCase.payload, testCase.secret, false) + r := &Request{ + Headers: testCase.headers, + Body: testCase.body, + } + valid, err := CheckScalrSignature(r, testCase.secret, false) if valid != testCase.ok { t.Errorf("failed to check scalr signature fot test case: %s\nexpected ok:%#v, got ok:%#v}", testCase.description, testCase.ok, valid) @@ -249,25 +254,30 @@ func TestExtractParameter(t *testing.T) { var argumentGetTests = []struct { source, name string - headers, query, payload *map[string]interface{} + headers, query, payload 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, "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}, // 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"}, "", 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 } func TestArgumentGet(t *testing.T) { for _, tt := range argumentGetTests { a := Argument{tt.source, tt.name, "", false} - value, err := a.Get(tt.headers, tt.query, tt.payload) + r := &Request{ + Headers: tt.headers, + Query: tt.query, + Payload: tt.payload, + } + value, err := a.Get(r) if (err == nil) != tt.ok || value != tt.value { t.Errorf("failed to get {%q, %q}:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, err:%v}", tt.source, tt.name, tt.value, tt.ok, value, err) } @@ -276,26 +286,31 @@ func TestArgumentGet(t *testing.T) { var hookParseJSONParametersTests = []struct { params []Argument - headers, query, payload *map[string]interface{} - rheaders, rquery, rpayload *map[string]interface{} + headers, query, payload map[string]interface{} + rheaders, rquery, rpayload 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, 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}, // 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, 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 } func TestHookParseJSONParameters(t *testing.T) { for _, tt := range hookParseJSONParametersTests { h := &Hook{JSONStringParameters: tt.params} - err := h.ParseJSONParameters(tt.headers, tt.query, tt.payload) + r := &Request{ + Headers: tt.headers, + Query: tt.query, + Payload: tt.payload, + } + err := h.ParseJSONParameters(r) if (err == nil) != tt.ok || !reflect.DeepEqual(tt.headers, tt.rheaders) { - t.Errorf("failed to parse %v:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.params, *tt.rheaders, tt.ok, *tt.headers, (err == nil)) + t.Errorf("failed to parse %v:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.params, tt.rheaders, tt.ok, tt.headers, (err == nil)) } } } @@ -303,19 +318,24 @@ func TestHookParseJSONParameters(t *testing.T) { var hookExtractCommandArgumentsTests = []struct { exec string args []Argument - headers, query, payload *map[string]interface{} + headers, query, payload 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, []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, []string{"fail", ""}, false}, } func TestHookExtractCommandArguments(t *testing.T) { for _, tt := range hookExtractCommandArgumentsTests { h := &Hook{ExecuteCommand: tt.exec, PassArgumentsToCommand: tt.args} - value, err := h.ExtractCommandArguments(tt.headers, tt.query, tt.payload) + r := &Request{ + Headers: tt.headers, + Query: tt.query, + Payload: tt.payload, + } + value, err := h.ExtractCommandArguments(r) if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) { t.Errorf("failed to extract args {cmd=%q, args=%v}:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.exec, tt.args, tt.value, tt.ok, value, (err == nil)) } @@ -344,7 +364,7 @@ func TestHookExtractCommandArguments(t *testing.T) { var hookExtractCommandArgumentsForEnvTests = []struct { exec string args []Argument - headers, query, payload *map[string]interface{} + headers, query, payload map[string]interface{} value []string ok bool }{ @@ -352,14 +372,14 @@ var hookExtractCommandArgumentsForEnvTests = []struct { { "test", []Argument{Argument{"header", "a", "", false}}, - &map[string]interface{}{"A": "z"}, nil, nil, + map[string]interface{}{"A": "z"}, 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, []string{"MYKEY=z"}, true, }, @@ -367,7 +387,7 @@ var hookExtractCommandArgumentsForEnvTests = []struct { { "fail", []Argument{Argument{"payload", "a", "", false}}, - &map[string]interface{}{"A": "z"}, nil, nil, + map[string]interface{}{"A": "z"}, nil, nil, []string{}, false, }, @@ -376,7 +396,12 @@ var hookExtractCommandArgumentsForEnvTests = []struct { func TestHookExtractCommandArgumentsForEnv(t *testing.T) { for _, tt := range hookExtractCommandArgumentsForEnvTests { h := &Hook{ExecuteCommand: tt.exec, PassEnvironmentToCommand: tt.args} - value, err := h.ExtractCommandArgumentsForEnv(tt.headers, tt.query, tt.payload) + r := &Request{ + Headers: tt.headers, + Query: tt.query, + Payload: tt.payload, + } + value, err := h.ExtractCommandArgumentsForEnv(r) if (err == nil) != tt.ok || !reflect.DeepEqual(value, tt.value) { t.Errorf("failed to extract args for env {cmd=%q, args=%v}:\nexpected %#v, ok: %v\ngot %#v, ok: %v", tt.exec, tt.args, tt.value, tt.ok, value, (err == nil)) } @@ -454,24 +479,24 @@ 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 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-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, 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, []byte{}, "", true, false}, + {"regex", "^z", "", "z", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false}, + {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, 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}, // 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, []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 // errors - {"regex", "*", "", "", "", Argument{"header", "a", "", false}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex - {"payload-hash-sha1", "", "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 + {"regex", "*", "", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex + {"payload-hash-sha1", "", "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 // 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 @@ -490,7 +515,16 @@ var matchRuleTests = []struct { func TestMatchRule(t *testing.T) { for i, tt := range matchRuleTests { r := MatchRule{tt.typ, tt.regex, tt.secret, tt.value, tt.param, tt.ipRange} - ok, err := r.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, tt.remoteAddr) + req := &Request{ + Headers: tt.headers, + Query: tt.query, + Payload: tt.payload, + Body: tt.body, + RawRequest: &http.Request{ + RemoteAddr: tt.remoteAddr, + }, + } + ok, err := r.Evaluate(req) if ok != tt.ok || (err != nil) != tt.err { t.Errorf("%d failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", i, r, tt.ok, tt.err, ok, err) } @@ -500,7 +534,7 @@ func TestMatchRule(t *testing.T) { var andRuleTests = []struct { desc string // description of the test case rule AndRule - headers, query, payload *map[string]interface{} + headers, query, payload map[string]interface{} body []byte ok bool err bool @@ -511,7 +545,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, + map[string]interface{}{"A": "z", "B": "y"}, nil, nil, []byte{}, true, false, }, @@ -521,7 +555,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, + map[string]interface{}{"A": "z", "B": "Y"}, nil, nil, []byte{}, false, false, }, @@ -548,7 +582,7 @@ var andRuleTests = []struct { }, }, }, - &map[string]interface{}{"A": "z", "B": "y", "C": "x", "D": "w", "E": "X", "F": "X"}, nil, nil, + map[string]interface{}{"A": "z", "B": "y", "C": "x", "D": "w", "E": "X", "F": "X"}, nil, nil, []byte{}, true, false, }, @@ -557,14 +591,20 @@ var andRuleTests = []struct { { "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, false, true, }, } func TestAndRule(t *testing.T) { for _, tt := range andRuleTests { - ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, "") + r := &Request{ + Headers: tt.headers, + Query: tt.query, + Payload: tt.payload, + Body: tt.body, + } + ok, err := tt.rule.Evaluate(r) if ok != tt.ok || (err != nil) != tt.err { t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.desc, tt.ok, tt.err, ok, err) } @@ -574,7 +614,7 @@ func TestAndRule(t *testing.T) { var orRuleTests = []struct { desc string // description of the test case rule OrRule - headers, query, payload *map[string]interface{} + headers, query, payload map[string]interface{} body []byte ok bool err bool @@ -585,7 +625,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, + map[string]interface{}{"A": "z", "B": "X"}, nil, nil, []byte{}, true, false, }, @@ -595,7 +635,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, + map[string]interface{}{"A": "X", "B": "y"}, nil, nil, []byte{}, true, false, }, @@ -605,7 +645,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, + map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil, []byte{}, false, false, }, @@ -615,7 +655,7 @@ var orRuleTests = []struct { OrRule{ {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, }, - &map[string]interface{}{"Y": "Z"}, nil, nil, + map[string]interface{}{"Y": "Z"}, nil, nil, []byte{}, false, true, }, @@ -623,7 +663,13 @@ var orRuleTests = []struct { func TestOrRule(t *testing.T) { for _, tt := range orRuleTests { - ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, "") + r := &Request{ + Headers: tt.headers, + Query: tt.query, + Payload: tt.payload, + Body: tt.body, + } + ok, err := tt.rule.Evaluate(r) if ok != tt.ok || (err != nil) != tt.err { t.Errorf("%#v:\nexpected ok: %#v, err: %v\ngot ok: %#v err: %v", tt.desc, tt.ok, tt.err, ok, err) } @@ -633,18 +679,24 @@ func TestOrRule(t *testing.T) { var notRuleTests = []struct { desc string // description of the test case rule NotRule - headers, query, payload *map[string]interface{} + headers, query, payload 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, []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}, } func TestNotRule(t *testing.T) { for _, tt := range notRuleTests { - ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, "") + r := &Request{ + Headers: tt.headers, + Query: tt.query, + Payload: tt.payload, + Body: tt.body, + } + ok, err := tt.rule.Evaluate(r) if ok != tt.ok || (err != nil) != tt.err { t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.rule, tt.ok, tt.err, ok, err) } diff --git a/webhook.go b/webhook.go index 26872ed..f220d4c 100644 --- a/webhook.go +++ b/webhook.go @@ -302,10 +302,14 @@ func main() { } func hookHandler(w http.ResponseWriter, r *http.Request) { - rid := middleware.GetReqID(r.Context()) + req := &hook.Request{ + ID: middleware.GetReqID(r.Context()), + RawRequest: r, + } - log.Printf("[%s] incoming HTTP %s request from %s\n", rid, r.Method, r.RemoteAddr) + log.Printf("[%s] incoming HTTP %s request from %s\n", req.ID, r.Method, r.RemoteAddr) + // TODO: rename this to avoid confusion with Request.ID id := mux.Vars(r)["id"] matchedHook := matchLoadedHook(id) @@ -341,57 +345,54 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { if !allowedMethod { w.WriteHeader(http.StatusMethodNotAllowed) - log.Printf("[%s] HTTP %s method not allowed for hook %q", rid, r.Method, id) + log.Printf("[%s] HTTP %s method not allowed for hook %q", req.ID, r.Method, id) return } - log.Printf("[%s] %s got matched\n", rid, id) + log.Printf("[%s] %s got matched\n", req.ID, id) for _, responseHeader := range responseHeaders { w.Header().Set(responseHeader.Name, responseHeader.Value) } - var ( - body []byte - err error - ) + var err error // set contentType to IncomingPayloadContentType or header value - contentType := r.Header.Get("Content-Type") + req.ContentType = r.Header.Get("Content-Type") if len(matchedHook.IncomingPayloadContentType) != 0 { - contentType = matchedHook.IncomingPayloadContentType + req.ContentType = matchedHook.IncomingPayloadContentType } - isMultipart := strings.HasPrefix(contentType, "multipart/form-data;") + isMultipart := strings.HasPrefix(req.ContentType, "multipart/form-data;") if !isMultipart { - body, err = ioutil.ReadAll(r.Body) + req.Body, err = ioutil.ReadAll(r.Body) if err != nil { - log.Printf("[%s] error reading the request body: %+v\n", rid, err) + log.Printf("[%s] error reading the request body: %+v\n", req.ID, err) } } // parse headers - headers := valuesToMap(r.Header) + req.Headers = valuesToMap(r.Header) // parse query variables - query := valuesToMap(r.URL.Query()) + req.Query = valuesToMap(r.URL.Query()) // parse body - var payload map[string]interface{} + // var payload map[string]interface{} switch { - case strings.Contains(contentType, "json"): - decoder := json.NewDecoder(bytes.NewReader(body)) + case strings.Contains(req.ContentType, "json"): + decoder := json.NewDecoder(bytes.NewReader(req.Body)) decoder.UseNumber() var firstChar byte - for i := 0; i < len(body); i++ { - if unicode.IsSpace(rune(body[i])) { + for i := 0; i < len(req.Body); i++ { + if unicode.IsSpace(rune(req.Body[i])) { continue } - firstChar = body[i] + firstChar = req.Body[i] break } @@ -399,36 +400,36 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { var arrayPayload interface{} err := decoder.Decode(&arrayPayload) if err != nil { - log.Printf("[%s] error parsing JSON array payload %+v\n", rid, err) + log.Printf("[%s] error parsing JSON array payload %+v\n", req.ID, err) } - payload = make(map[string]interface{}, 1) - payload["root"] = arrayPayload + req.Payload = make(map[string]interface{}, 1) + req.Payload["root"] = arrayPayload } else { - err := decoder.Decode(&payload) + err := decoder.Decode(&req.Payload) if err != nil { - log.Printf("[%s] error parsing JSON payload %+v\n", rid, err) + log.Printf("[%s] error parsing JSON payload %+v\n", req.ID, err) } } - case strings.Contains(contentType, "x-www-form-urlencoded"): - fd, err := url.ParseQuery(string(body)) + case strings.Contains(req.ContentType, "x-www-form-urlencoded"): + fd, err := url.ParseQuery(string(req.Body)) if err != nil { - log.Printf("[%s] error parsing form payload %+v\n", rid, err) + log.Printf("[%s] error parsing form payload %+v\n", req.ID, err) } else { - payload = valuesToMap(fd) + req.Payload = valuesToMap(fd) } - case strings.Contains(contentType, "xml"): - payload, err = mxj.NewMapXmlReader(bytes.NewReader(body)) + case strings.Contains(req.ContentType, "xml"): + req.Payload, err = mxj.NewMapXmlReader(bytes.NewReader(req.Body)) if err != nil { - log.Printf("[%s] error parsing XML payload: %+v\n", rid, err) + log.Printf("[%s] error parsing XML payload: %+v\n", req.ID, err) } case isMultipart: err = r.ParseMultipartForm(*maxMultipartMem) if err != nil { - msg := fmt.Sprintf("[%s] error parsing multipart form: %+v\n", rid, err) + msg := fmt.Sprintf("[%s] error parsing multipart form: %+v\n", req.ID, err) log.Println(msg) w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, "Error occurred while parsing multipart form.") @@ -436,14 +437,14 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { } for k, v := range r.MultipartForm.Value { - log.Printf("[%s] found multipart form value %q", rid, k) + log.Printf("[%s] found multipart form value %q", req.ID, k) - if payload == nil { - payload = make(map[string]interface{}) + if req.Payload == nil { + req.Payload = make(map[string]interface{}) } // TODO(moorereason): support duplicate, named values - payload[k] = v[0] + req.Payload[k] = v[0] } for k, v := range r.MultipartForm.File { @@ -472,11 +473,11 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { } if parseAsJSON { - log.Printf("[%s] parsing multipart form file %q as JSON\n", rid, k) + log.Printf("[%s] parsing multipart form file %q as JSON\n", req.ID, k) f, err := v[0].Open() if err != nil { - msg := fmt.Sprintf("[%s] error parsing multipart form file: %+v\n", rid, err) + msg := fmt.Sprintf("[%s] error parsing multipart form file: %+v\n", req.ID, err) log.Println(msg) w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, "Error occurred while parsing multipart form file.") @@ -489,24 +490,24 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { var part map[string]interface{} err = decoder.Decode(&part) if err != nil { - log.Printf("[%s] error parsing JSON payload file: %+v\n", rid, err) + log.Printf("[%s] error parsing JSON payload file: %+v\n", req.ID, err) } - if payload == nil { - payload = make(map[string]interface{}) + if req.Payload == nil { + req.Payload = make(map[string]interface{}) } - payload[k] = part + req.Payload[k] = part } } default: - log.Printf("[%s] error parsing body payload due to unsupported content type header: %s\n", rid, contentType) + log.Printf("[%s] error parsing body payload due to unsupported content type header: %s\n", req.ID, req.ContentType) } // handle hook - errors := matchedHook.ParseJSONParameters(&headers, &query, &payload) + errors := matchedHook.ParseJSONParameters(req) for _, err := range errors { - log.Printf("[%s] error parsing JSON parameters: %s\n", rid, err) + log.Printf("[%s] error parsing JSON parameters: %s\n", req.ID, err) } var ok bool @@ -514,29 +515,29 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { if matchedHook.TriggerRule == nil { ok = true } else { - ok, err = matchedHook.TriggerRule.Evaluate(&headers, &query, &payload, &body, r.RemoteAddr) + ok, err = matchedHook.TriggerRule.Evaluate(req) if err != nil { if !hook.IsParameterNodeError(err) { - msg := fmt.Sprintf("[%s] error evaluating hook: %s", rid, err) + msg := fmt.Sprintf("[%s] error evaluating hook: %s", req.ID, err) log.Println(msg) w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, "Error occurred while evaluating hook rules.") return } - log.Printf("[%s] %v", rid, err) + log.Printf("[%s] %v", req.ID, err) } } if ok { - log.Printf("[%s] %s hook triggered successfully\n", rid, matchedHook.ID) + log.Printf("[%s] %s hook triggered successfully\n", req.ID, matchedHook.ID) for _, responseHeader := range matchedHook.ResponseHeaders { w.Header().Set(responseHeader.Name, responseHeader.Value) } if matchedHook.CaptureCommandOutput { - response, err := handleHook(matchedHook, rid, &headers, &query, &payload, &body) + response, err := handleHook(matchedHook, req) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -549,16 +550,16 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { } else { // Check if a success return code is configured for the hook if matchedHook.SuccessHttpResponseCode != 0 { - writeHttpResponseCode(w, rid, matchedHook.ID, matchedHook.SuccessHttpResponseCode) + writeHttpResponseCode(w, req.ID, matchedHook.ID, matchedHook.SuccessHttpResponseCode) } fmt.Fprint(w, response) } } else { - go handleHook(matchedHook, rid, &headers, &query, &payload, &body) + go handleHook(matchedHook, req) // Check if a success return code is configured for the hook if matchedHook.SuccessHttpResponseCode != 0 { - writeHttpResponseCode(w, rid, matchedHook.ID, matchedHook.SuccessHttpResponseCode) + writeHttpResponseCode(w, req.ID, matchedHook.ID, matchedHook.SuccessHttpResponseCode) } fmt.Fprint(w, matchedHook.ResponseMessage) @@ -568,16 +569,16 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { // Check if a return code is configured for the hook if matchedHook.TriggerRuleMismatchHttpResponseCode != 0 { - writeHttpResponseCode(w, rid, matchedHook.ID, matchedHook.TriggerRuleMismatchHttpResponseCode) + writeHttpResponseCode(w, req.ID, matchedHook.ID, matchedHook.TriggerRuleMismatchHttpResponseCode) } // if none of the hooks got triggered - log.Printf("[%s] %s got matched, but didn't get triggered because the trigger rules were not satisfied\n", rid, matchedHook.ID) + log.Printf("[%s] %s got matched, but didn't get triggered because the trigger rules were not satisfied\n", req.ID, matchedHook.ID) fmt.Fprint(w, "Hook rules were not satisfied.") } -func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]interface{}, body *[]byte) (string, error) { +func handleHook(h *hook.Hook, r *hook.Request) (string, error) { var errors []error // check the command exists @@ -590,12 +591,12 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in cmdPath, err := exec.LookPath(lookpath) if err != nil { - log.Printf("[%s] error in %s", rid, err) + log.Printf("[%s] error in %s", r.ID, err) // check if parameters specified in execute-command by mistake if strings.IndexByte(h.ExecuteCommand, ' ') != -1 { s := strings.Fields(h.ExecuteCommand)[0] - log.Printf("[%s] use 'pass-arguments-to-command' to specify args for '%s'", rid, s) + log.Printf("[%s] use 'pass-arguments-to-command' to specify args for '%s'", r.ID, s) } return "", err @@ -604,37 +605,37 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in cmd := exec.Command(cmdPath) cmd.Dir = h.CommandWorkingDirectory - cmd.Args, errors = h.ExtractCommandArguments(headers, query, payload) + cmd.Args, errors = h.ExtractCommandArguments(r) for _, err := range errors { - log.Printf("[%s] error extracting command arguments: %s\n", rid, err) + log.Printf("[%s] error extracting command arguments: %s\n", r.ID, err) } var envs []string - envs, errors = h.ExtractCommandArgumentsForEnv(headers, query, payload) + envs, errors = h.ExtractCommandArgumentsForEnv(r) for _, err := range errors { - log.Printf("[%s] error extracting command arguments for environment: %s\n", rid, err) + log.Printf("[%s] error extracting command arguments for environment: %s\n", r.ID, err) } - files, errors := h.ExtractCommandArgumentsForFile(headers, query, payload) + files, errors := h.ExtractCommandArgumentsForFile(r) for _, err := range errors { - log.Printf("[%s] error extracting command arguments for file: %s\n", rid, err) + log.Printf("[%s] error extracting command arguments for file: %s\n", r.ID, err) } for i := range files { tmpfile, err := ioutil.TempFile(h.CommandWorkingDirectory, files[i].EnvName) if err != nil { - log.Printf("[%s] error creating temp file [%s]", rid, err) + log.Printf("[%s] error creating temp file [%s]", r.ID, err) continue } - log.Printf("[%s] writing env %s file %s", rid, files[i].EnvName, tmpfile.Name()) + log.Printf("[%s] writing env %s file %s", r.ID, files[i].EnvName, tmpfile.Name()) if _, err := tmpfile.Write(files[i].Data); err != nil { - log.Printf("[%s] error writing file %s [%s]", rid, tmpfile.Name(), err) + log.Printf("[%s] error writing file %s [%s]", r.ID, tmpfile.Name(), err) continue } if err := tmpfile.Close(); err != nil { - log.Printf("[%s] error closing file %s [%s]", rid, tmpfile.Name(), err) + log.Printf("[%s] error closing file %s [%s]", r.ID, tmpfile.Name(), err) continue } @@ -644,27 +645,27 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in cmd.Env = append(os.Environ(), envs...) - log.Printf("[%s] executing %s (%s) with arguments %q and environment %s using %s as cwd\n", rid, h.ExecuteCommand, cmd.Path, cmd.Args, envs, cmd.Dir) + log.Printf("[%s] executing %s (%s) with arguments %q and environment %s using %s as cwd\n", r.ID, h.ExecuteCommand, cmd.Path, cmd.Args, envs, cmd.Dir) out, err := cmd.CombinedOutput() - log.Printf("[%s] command output: %s\n", rid, out) + log.Printf("[%s] command output: %s\n", r.ID, out) if err != nil { - log.Printf("[%s] error occurred: %+v\n", rid, err) + log.Printf("[%s] error occurred: %+v\n", r.ID, err) } for i := range files { if files[i].File != nil { - log.Printf("[%s] removing file %s\n", rid, files[i].File.Name()) + log.Printf("[%s] removing file %s\n", r.ID, files[i].File.Name()) err := os.Remove(files[i].File.Name()) if err != nil { - log.Printf("[%s] error removing file %s [%s]", rid, files[i].File.Name(), err) + log.Printf("[%s] error removing file %s [%s]", r.ID, files[i].File.Name(), err) } } } - log.Printf("[%s] finished handling %s\n", rid, h.ID) + log.Printf("[%s] finished handling %s\n", r.ID, h.ID) return string(out), err } diff --git a/webhook_test.go b/webhook_test.go index 353d22d..c8a46cc 100644 --- a/webhook_test.go +++ b/webhook_test.go @@ -53,7 +53,11 @@ func TestStaticParams(t *testing.T) { b := &bytes.Buffer{} log.SetOutput(b) - _, err = handleHook(spHook, "test", &spHeaders, &map[string]interface{}{}, &map[string]interface{}{}, &[]byte{}) + r := &hook.Request{ + ID: "test", + Headers: spHeaders, + } + _, err = handleHook(spHook, r) if err != nil { t.Fatalf("Unexpected error: %v\n", err) }