diff --git a/internal/hook/hook.go b/internal/hook/hook.go index c446aee..34a429d 100644 --- a/internal/hook/hook.go +++ b/internal/hook/hook.go @@ -17,7 +17,6 @@ import ( "log" "math" "net" - "net/http" "net/textproto" "os" "reflect" @@ -48,30 +47,6 @@ 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 diff --git a/internal/hook/request.go b/internal/hook/request.go new file mode 100644 index 0000000..ef7c949 --- /dev/null +++ b/internal/hook/request.go @@ -0,0 +1,116 @@ +package hook + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" + "unicode" + + "github.com/clbanning/mxj" +) + +// 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 +} + +func (r *Request) ParseJSONPayload() error { + decoder := json.NewDecoder(bytes.NewReader(r.Body)) + decoder.UseNumber() + + var firstChar byte + for i := 0; i < len(r.Body); i++ { + if unicode.IsSpace(rune(r.Body[i])) { + continue + } + firstChar = r.Body[i] + break + } + + if firstChar == byte('[') { + var arrayPayload interface{} + err := decoder.Decode(&arrayPayload) + if err != nil { + return fmt.Errorf("error parsing JSON array payload %+v", err) + } + + r.Payload = make(map[string]interface{}, 1) + r.Payload["root"] = arrayPayload + } else { + err := decoder.Decode(&r.Payload) + if err != nil { + return fmt.Errorf("error parsing JSON payload %+v", err) + } + } + + return nil +} + +func (r *Request) ParseHeaders(headers map[string][]string) { + r.Headers = make(map[string]interface{}, len(headers)) + + for k, v := range headers { + if len(v) > 0 { + r.Headers[k] = v[0] + } + } +} + +func (r *Request) ParseQuery(query map[string][]string) { + r.Query = make(map[string]interface{}, len(query)) + + for k, v := range query { + if len(v) > 0 { + r.Query[k] = v[0] + } + } +} + +func (r *Request) ParseFormPayload() error { + fd, err := url.ParseQuery(string(r.Body)) + if err != nil { + return fmt.Errorf("error parsing form payload %+v", err) + } + + r.Payload = make(map[string]interface{}, len(fd)) + + for k, v := range fd { + if len(v) > 0 { + r.Payload[k] = v[0] + } + } + + return nil +} + +func (r *Request) ParseXMLPayload() error { + var err error + + r.Payload, err = mxj.NewMapXmlReader(bytes.NewReader(r.Body)) + if err != nil { + return fmt.Errorf("error parsing XML payload: %+v", err) + } + + return nil +} diff --git a/webhook.go b/webhook.go index f220d4c..ea6a9f2 100644 --- a/webhook.go +++ b/webhook.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "crypto/tls" "encoding/json" "flag" @@ -10,19 +9,16 @@ import ( "log" "net" "net/http" - "net/url" "os" "os/exec" "path/filepath" "strings" "time" - "unicode" "github.com/adnanh/webhook/internal/hook" "github.com/adnanh/webhook/internal/middleware" "github.com/adnanh/webhook/internal/pidfile" - "github.com/clbanning/mxj" chimiddleware "github.com/go-chi/chi/middleware" "github.com/gorilla/mux" fsnotify "gopkg.in/fsnotify.v1" @@ -373,57 +369,26 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { } } - // parse headers - req.Headers = valuesToMap(r.Header) - - // parse query variables - req.Query = valuesToMap(r.URL.Query()) - - // parse body - // var payload map[string]interface{} + req.ParseHeaders(r.Header) + req.ParseQuery(r.URL.Query()) switch { case strings.Contains(req.ContentType, "json"): - decoder := json.NewDecoder(bytes.NewReader(req.Body)) - decoder.UseNumber() - - var firstChar byte - for i := 0; i < len(req.Body); i++ { - if unicode.IsSpace(rune(req.Body[i])) { - continue - } - firstChar = req.Body[i] - break - } - - if firstChar == byte('[') { - var arrayPayload interface{} - err := decoder.Decode(&arrayPayload) - if err != nil { - log.Printf("[%s] error parsing JSON array payload %+v\n", req.ID, err) - } - - req.Payload = make(map[string]interface{}, 1) - req.Payload["root"] = arrayPayload - } else { - err := decoder.Decode(&req.Payload) - if err != nil { - log.Printf("[%s] error parsing JSON payload %+v\n", req.ID, err) - } + err = req.ParseJSONPayload() + if err != nil { + log.Printf("[%s] %s", req.ID, err) } case strings.Contains(req.ContentType, "x-www-form-urlencoded"): - fd, err := url.ParseQuery(string(req.Body)) + err = req.ParseFormPayload() if err != nil { - log.Printf("[%s] error parsing form payload %+v\n", req.ID, err) - } else { - req.Payload = valuesToMap(fd) + log.Printf("[%s] %s", req.ID, err) } case strings.Contains(req.ContentType, "xml"): - req.Payload, err = mxj.NewMapXmlReader(bytes.NewReader(req.Body)) + err = req.ParseXMLPayload() if err != nil { - log.Printf("[%s] error parsing XML payload: %+v\n", req.ID, err) + log.Printf("[%s] %s", req.ID, err) } case isMultipart: