From e452811cf1da43e9f8e54d3363600237bcccba13 Mon Sep 17 00:00:00 2001 From: Dean Karn Date: Wed, 25 Jul 2018 22:59:10 -0700 Subject: [PATCH] covert bitbucket, gitlab and gogs to new style --- .travis.yml | 5 +- README.md | 6 +- bitbucket/bitbucket.go | 198 +++++++++++++++++++++-------------------- github/github.go | 6 +- gitlab/gitlab.go | 185 ++++++++++++++++++++------------------ gogs/gogs.go | 170 ++++++++++++++++++----------------- webhooks.go | 8 +- 7 files changed, 298 insertions(+), 280 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5143ab5..ffcf684 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: go go: - - 1.10.2 + - 1.10.3 - tip matrix: allow_failures: @@ -22,6 +22,7 @@ before_install: - ln -s $GOPATH/src/github.com/$TRAVIS_REPO_SLUG $GOPATH/src/gopkg.in/webhooks.v2 - ln -s $GOPATH/src/github.com/$TRAVIS_REPO_SLUG $GOPATH/src/gopkg.in/webhooks.v3 - ln -s $GOPATH/src/github.com/$TRAVIS_REPO_SLUG $GOPATH/src/gopkg.in/webhooks.v4 + - ln -s $GOPATH/src/github.com/$TRAVIS_REPO_SLUG $GOPATH/src/gopkg.in/webhooks.v5 before_script: - go vet ./... @@ -34,6 +35,6 @@ script: - go test -race after_success: | - [ $TRAVIS_GO_VERSION = 1.10.2 ] && + [ $TRAVIS_GO_VERSION = 1.10.3 ] && overalls -project="github.com/go-playground/webhooks" -covermode=count -ignore=.git,examples -debug && goveralls -coverprofile=overalls.coverprofile -service travis-ci -repotoken $COVERALLS_TOKEN diff --git a/README.md b/README.md index ea4d564..870da95 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ Library webhooks ================ -![Project status](https://img.shields.io/badge/version-4.1.1-green.svg) -[![Build Status](https://travis-ci.org/go-playground/webhooks.svg?branch=v4)](https://travis-ci.org/go-playground/webhooks) -[![Coverage Status](https://coveralls.io/repos/go-playground/webhooks/badge.svg?branch=v4&service=github)](https://coveralls.io/github/go-playground/webhooks?branch=v3) +![Project status](https://img.shields.io/badge/version-5.0.0-green.svg) +[![Build Status](https://travis-ci.org/go-playground/webhooks.svg?branch=v5)](https://travis-ci.org/go-playground/webhooks) +[![Coverage Status](https://coveralls.io/repos/go-playground/webhooks/badge.svg?branch=v5&service=github)](https://coveralls.io/github/go-playground/webhooks?branch=v5) [![Go Report Card](https://goreportcard.com/badge/go-playground/webhooks)](https://goreportcard.com/report/go-playground/webhooks) [![GoDoc](https://godoc.org/gopkg.in/go-playground/webhooks.v5?status.svg)](https://godoc.org/gopkg.in/go-playground/webhooks.v5) ![License](https://img.shields.io/dub/l/vibe-d.svg) diff --git a/bitbucket/bitbucket.go b/bitbucket/bitbucket.go index 4b430d7..cd4ff76 100644 --- a/bitbucket/bitbucket.go +++ b/bitbucket/bitbucket.go @@ -2,23 +2,27 @@ package bitbucket import ( "encoding/json" + "errors" "fmt" + "io" "io/ioutil" "net/http" +) - "gopkg.in/go-playground/webhooks.v5" +// parse errros +var ( + ErrEventNotSpecifiedToParse = errors.New("No Event specified to parse") + ErrInvalidHTTPMethod = errors.New("Invalid HTTP Method") + ErrMissingHookUUIDHeader = errors.New("Missing X-Hook-UUID Header") + ErrMissingEventKeyHeader = errors.New("Missing X-Event-Key Header") + ErrEventNotFound = errors.New("Event not defined to be parsed") + ErrParsingPayload = errors.New("Error parsing payload") + ErrUUIDVerificationFailed = errors.New("UUID verification failed") ) // Webhook instance contains all methods needed to process events type Webhook struct { - provider webhooks.Provider - uuid string - eventFuncs map[Event]webhooks.ProcessPayloadFunc -} - -// Config defines the configuration to create a new Bitbucket Webhook instance -type Config struct { - UUID string + uuid string } // Event defines a Bitbucket hook event type @@ -46,154 +50,154 @@ const ( PullRequestCommentDeletedEvent Event = "pullrequest:comment_deleted" ) +// Option is a configuration option for the webhook +type Option func(*Webhook) error + +// Options is a namespace var for configuration options +var Options = WebhookOptions{} + +// WebhookOptions is a namespace for configuration option methods +type WebhookOptions struct{} + +// UUID registers the BitBucket secret +func (WebhookOptions) UUID(uuid string) Option { + return func(hook *Webhook) error { + hook.uuid = uuid + return nil + } +} + // New creates and returns a WebHook instance denoted by the Provider type -func New(config *Config) *Webhook { - return &Webhook{ - provider: webhooks.Bitbucket, - uuid: config.UUID, - eventFuncs: map[Event]webhooks.ProcessPayloadFunc{}, +func New(options ...Option) (*Webhook, error) { + hook := new(Webhook) + for _, opt := range options { + if err := opt(hook); err != nil { + return nil, errors.New("Error applying Option") + } } + return hook, nil } -// Provider returns the current hooks provider ID -func (hook Webhook) Provider() webhooks.Provider { - return hook.provider -} +// Parse verifies and parses the events specified and returns the payload object or an error +func (hook Webhook) Parse(r *http.Request, events ...Event) (interface{}, error) { + defer func() { + _, _ = io.Copy(ioutil.Discard, r.Body) + _ = r.Body.Close() + }() -// RegisterEvents registers the function to call when the specified event(s) are encountered -func (hook Webhook) RegisterEvents(fn webhooks.ProcessPayloadFunc, events ...Event) { - - for _, event := range events { - hook.eventFuncs[event] = fn + if len(events) == 0 { + return nil, ErrEventNotSpecifiedToParse + } + if r.Method != http.MethodPost { + return nil, ErrInvalidHTTPMethod } -} - -// ParsePayload parses and verifies the payload and fires off the mapped function, if it exists. -func (hook Webhook) ParsePayload(w http.ResponseWriter, r *http.Request) { - webhooks.DefaultLog.Info("Parsing Payload...") uuid := r.Header.Get("X-Hook-UUID") if uuid == "" { - webhooks.DefaultLog.Error("Missing X-Hook-UUID Header") - http.Error(w, "400 Bad Request - Missing X-Hook-UUID Header", http.StatusBadRequest) - return + return nil, ErrMissingHookUUIDHeader } - webhooks.DefaultLog.Debug(fmt.Sprintf("X-Hook-UUID:%s", uuid)) - - if len(hook.uuid) > 0 { - if uuid != hook.uuid { - webhooks.DefaultLog.Error(fmt.Sprintf("X-Hook-UUID %s does not match configured uuid of %s", uuid, hook.uuid)) - http.Error(w, "403 Forbidden - X-Hook-UUID does not match", http.StatusForbidden) - return - } - } else { - webhooks.DefaultLog.Debug("hook uuid not defined - recommend setting for improved security") + if len(hook.uuid) > 0 && uuid != hook.uuid { + return nil, ErrUUIDVerificationFailed } event := r.Header.Get("X-Event-Key") if event == "" { - webhooks.DefaultLog.Error("Missing X-Event-Key Header") - http.Error(w, "400 Bad Request - Missing X-Event-Key Header", http.StatusBadRequest) - return + return nil, ErrMissingEventKeyHeader } - webhooks.DefaultLog.Debug(fmt.Sprintf("X-Event-Key:%s", event)) bitbucketEvent := Event(event) - fn, ok := hook.eventFuncs[bitbucketEvent] - // if no event registered - if !ok { - webhooks.DefaultLog.Info(fmt.Sprintf("Webhook Event %s not registered, it is recommended to setup only events in bitbucket that will be registered in the webhook to avoid unnecessary traffic and reduce potential attack vectors.", event)) - return + var found bool + for _, evt := range events { + if evt == bitbucketEvent { + found = true + break + } + } + // event not defined to be parsed + if !found { + return nil, ErrEventNotFound } payload, err := ioutil.ReadAll(r.Body) if err != nil || len(payload) == 0 { - webhooks.DefaultLog.Error("Issue reading Payload") - http.Error(w, "Issue reading Payload", http.StatusInternalServerError) - return + return nil, ErrParsingPayload } - webhooks.DefaultLog.Debug(fmt.Sprintf("Payload:%s", string(payload))) - hd := webhooks.Header(r.Header) switch bitbucketEvent { case RepoPushEvent: var pl RepoPushPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case RepoForkEvent: var pl RepoForkPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case RepoUpdatedEvent: var pl RepoUpdatedPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case RepoCommitCommentCreatedEvent: var pl RepoCommitCommentCreatedPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case RepoCommitStatusCreatedEvent: var pl RepoCommitStatusCreatedPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case RepoCommitStatusUpdatedEvent: var pl RepoCommitStatusUpdatedPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case IssueCreatedEvent: var pl IssueCreatedPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case IssueUpdatedEvent: var pl IssueUpdatedPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case IssueCommentCreatedEvent: var pl IssueCommentCreatedPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case PullRequestCreatedEvent: var pl PullRequestCreatedPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case PullRequestUpdatedEvent: var pl PullRequestUpdatedPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case PullRequestApprovedEvent: var pl PullRequestApprovedPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case PullRequestUnapprovedEvent: var pl PullRequestUnapprovedPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case PullRequestMergedEvent: var pl PullRequestMergedPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case PullRequestDeclinedEvent: var pl PullRequestDeclinedPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case PullRequestCommentCreatedEvent: var pl PullRequestCommentCreatedPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case PullRequestCommentUpdatedEvent: var pl PullRequestCommentUpdatedPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case PullRequestCommentDeletedEvent: var pl PullRequestCommentDeletedPayload - json.Unmarshal([]byte(payload), &pl) - hook.runProcessPayloadFunc(fn, pl, hd) + err = json.Unmarshal([]byte(payload), &pl) + return pl, err + default: + return nil, fmt.Errorf("unknown event %s", bitbucketEvent) } } - -func (hook Webhook) runProcessPayloadFunc(fn webhooks.ProcessPayloadFunc, results interface{}, header webhooks.Header) { - go func(fn webhooks.ProcessPayloadFunc, results interface{}, header webhooks.Header) { - fn(results, header) - }(fn, results, header) -} diff --git a/github/github.go b/github/github.go index 800d2e3..e9733d9 100644 --- a/github/github.go +++ b/github/github.go @@ -17,9 +17,9 @@ var ( ErrEventNotSpecifiedToParse = errors.New("No Event specified to parse") ErrInvalidHTTPMethod = errors.New("Invalid HTTP Method") ErrMissingGithubEventHeader = errors.New("Missing X-GitHub-Event Header") - ErrMissingHubSignatureHeader = errors.New("Missing X-Hub-Signature") + ErrMissingHubSignatureHeader = errors.New("Missing X-Hub-Signature Header") ErrEventNotFound = errors.New("Event not defined to be parsed") - ErrParsingPayload = errors.New("Error Reading Payload") + ErrParsingPayload = errors.New("Error parsing payload") ErrHMACVerificationFailed = errors.New("HMAC verification failed") ) @@ -123,7 +123,7 @@ func (hook Webhook) Parse(r *http.Request, events ...Event) (interface{}, error) } event := r.Header.Get("X-GitHub-Event") - if len(event) == 0 { + if event == "" { return nil, ErrMissingGithubEventHeader } gitHubEvent := Event(event) diff --git a/gitlab/gitlab.go b/gitlab/gitlab.go index 3784e1f..7fa4729 100644 --- a/gitlab/gitlab.go +++ b/gitlab/gitlab.go @@ -2,28 +2,13 @@ package gitlab import ( "encoding/json" + "errors" "fmt" + "io" "io/ioutil" "net/http" - - "gopkg.in/go-playground/webhooks.v5" ) -// Webhook instance contains all methods needed to process events -type Webhook struct { - provider webhooks.Provider - secret string - eventFuncs map[Event]webhooks.ProcessPayloadFunc -} - -// Config defines the configuration to create a new GitHub Webhook instance -type Config struct { - Secret string -} - -// Event defines a GitHub hook event type -type Event string - // GitLab hook types const ( PushEvents Event = "Push Hook" @@ -37,121 +22,145 @@ const ( BuildEvents Event = "Build Hook" ) +// Option is a configuration option for the webhook +type Option func(*Webhook) error + +// Options is a namespace var for configuration options +var Options = WebhookOptions{} + +// WebhookOptions is a namespace for configuration option methods +type WebhookOptions struct{} + +// Secret registers the GitLab secret +func (WebhookOptions) Secret(secret string) Option { + return func(hook *Webhook) error { + hook.secret = secret + return nil + } +} + +// Webhook instance contains all methods needed to process events +type Webhook struct { + secret string +} + +// Event defines a GitHub hook event type +type Event string + // New creates and returns a WebHook instance denoted by the Provider type -func New(config *Config) *Webhook { - return &Webhook{ - provider: webhooks.GitLab, - secret: config.Secret, - eventFuncs: map[Event]webhooks.ProcessPayloadFunc{}, +func New(options ...Option) (*Webhook, error) { + hook := new(Webhook) + for _, opt := range options { + if err := opt(hook); err != nil { + return nil, errors.New("Error applying Option") + } } + return hook, nil } -// Provider returns the current hooks provider ID -func (hook Webhook) Provider() webhooks.Provider { - return hook.provider -} +// parse errros +var ( + ErrEventNotSpecifiedToParse = errors.New("No Event specified to parse") + ErrInvalidHTTPMethod = errors.New("Invalid HTTP Method") + ErrMissingGitLabEventHeader = errors.New("Missing X-Gitlab-Event Header") + ErrMissingGitLabTokenHeader = errors.New("Missing X-Gitlab-Token Header") + ErrEventNotFound = errors.New("Event not defined to be parsed") + ErrParsingPayload = errors.New("Error parsing payload") + // ErrHMACVerificationFailed = errors.New("HMAC verification failed") +) -// RegisterEvents registers the function to call when the specified event(s) are encountered -func (hook Webhook) RegisterEvents(fn webhooks.ProcessPayloadFunc, events ...Event) { +// Parse verifies and parses the events specified and returns the payload object or an error +func (hook Webhook) Parse(r *http.Request, events ...Event) (interface{}, error) { + defer func() { + _, _ = io.Copy(ioutil.Discard, r.Body) + _ = r.Body.Close() + }() - for _, event := range events { - hook.eventFuncs[event] = fn + if len(events) == 0 { + return nil, ErrEventNotSpecifiedToParse + } + if r.Method != http.MethodPost { + return nil, ErrInvalidHTTPMethod } -} - -// ParsePayload parses and verifies the payload and fires off the mapped function, if it exists. -func (hook Webhook) ParsePayload(w http.ResponseWriter, r *http.Request) { - webhooks.DefaultLog.Info("Parsing Payload...") event := r.Header.Get("X-Gitlab-Event") if len(event) == 0 { - webhooks.DefaultLog.Error("Missing X-Gitlab-Event Header") - http.Error(w, "400 Bad Request - Missing X-Gitlab-Event Header", http.StatusBadRequest) - return + return nil, ErrMissingGitLabEventHeader } - webhooks.DefaultLog.Debug(fmt.Sprintf("X-Gitlab-Event:%s", event)) gitLabEvent := Event(event) - fn, ok := hook.eventFuncs[gitLabEvent] - // if no event registered - if !ok { - webhooks.DefaultLog.Info(fmt.Sprintf("Webhook Event %s not registered, it is recommended to setup only events in gitlab that will be registered in the webhook to avoid unnecessary traffic and reduce potential attack vectors.", event)) - return + var found bool + for _, evt := range events { + if evt == gitLabEvent { + found = true + break + } + } + // event not defined to be parsed + if !found { + return nil, ErrEventNotFound } payload, err := ioutil.ReadAll(r.Body) if err != nil || len(payload) == 0 { - webhooks.DefaultLog.Error("Issue reading Payload") - http.Error(w, "Error reading Payload", http.StatusInternalServerError) - return + return nil, ErrParsingPayload } - webhooks.DefaultLog.Debug(fmt.Sprintf("Payload:%s", string(payload))) // If we have a Secret set, we should check the MAC if len(hook.secret) > 0 { - webhooks.DefaultLog.Info("Checking secret") signature := r.Header.Get("X-Gitlab-Token") if signature != hook.secret { - webhooks.DefaultLog.Error(fmt.Sprintf("Invalid X-Gitlab-Token of '%s'", signature)) - http.Error(w, "403 Forbidden - Token missmatch", http.StatusForbidden) - return + return nil, ErrMissingGitLabTokenHeader } } - // Make headers available to ProcessPayloadFunc as a webhooks type - hd := webhooks.Header(r.Header) - switch gitLabEvent { case PushEvents: - var pe PushEventPayload - json.Unmarshal([]byte(payload), &pe) - hook.runProcessPayloadFunc(fn, pe, hd) + var pl PushEventPayload + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case TagEvents: - var te TagEventPayload - json.Unmarshal([]byte(payload), &te) - hook.runProcessPayloadFunc(fn, te, hd) + var pl TagEventPayload + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case ConfidentialIssuesEvents: - var cie ConfidentialIssueEventPayload - json.Unmarshal([]byte(payload), &cie) - hook.runProcessPayloadFunc(fn, cie, hd) + var pl ConfidentialIssueEventPayload + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case IssuesEvents: - var ie IssueEventPayload - json.Unmarshal([]byte(payload), &ie) - hook.runProcessPayloadFunc(fn, ie, hd) + var pl IssueEventPayload + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case CommentEvents: - var ce CommentEventPayload - json.Unmarshal([]byte(payload), &ce) - hook.runProcessPayloadFunc(fn, ce, hd) + var pl CommentEventPayload + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case MergeRequestEvents: - var mre MergeRequestEventPayload - json.Unmarshal([]byte(payload), &mre) - hook.runProcessPayloadFunc(fn, mre, hd) + var pl MergeRequestEventPayload + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case WikiPageEvents: - var wpe WikiPageEventPayload - json.Unmarshal([]byte(payload), &wpe) - hook.runProcessPayloadFunc(fn, wpe, hd) + var pl WikiPageEventPayload + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case PipelineEvents: - var pe PipelineEventPayload - json.Unmarshal([]byte(payload), &pe) - hook.runProcessPayloadFunc(fn, pe, hd) + var pl PipelineEventPayload + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case BuildEvents: - var be BuildEventPayload - json.Unmarshal([]byte(payload), &be) - hook.runProcessPayloadFunc(fn, be, hd) + var pl BuildEventPayload + err = json.Unmarshal([]byte(payload), &pl) + return pl, err + default: + return nil, fmt.Errorf("unknown event %s", gitLabEvent) } } - -func (hook Webhook) runProcessPayloadFunc(fn webhooks.ProcessPayloadFunc, results interface{}, header webhooks.Header) { - go func(fn webhooks.ProcessPayloadFunc, results interface{}, header webhooks.Header) { - fn(results, header) - }(fn, results, header) -} diff --git a/gogs/gogs.go b/gogs/gogs.go index 28b9314..1a2d9b4 100644 --- a/gogs/gogs.go +++ b/gogs/gogs.go @@ -2,7 +2,9 @@ package gogs import ( "encoding/json" + "errors" "fmt" + "io" "io/ioutil" "net/http" @@ -11,19 +13,28 @@ import ( "encoding/hex" client "github.com/gogits/go-gogs-client" - "gopkg.in/go-playground/webhooks.v5" ) +// Option is a configuration option for the webhook +type Option func(*Webhook) error + +// Options is a namespace var for configuration options +var Options = WebhookOptions{} + +// WebhookOptions is a namespace for configuration option methods +type WebhookOptions struct{} + +// Secret registers the GitLab secret +func (WebhookOptions) Secret(secret string) Option { + return func(hook *Webhook) error { + hook.secret = secret + return nil + } +} + // Webhook instance contains all methods needed to process events type Webhook struct { - provider webhooks.Provider - secret string - eventFuncs map[Event]webhooks.ProcessPayloadFunc -} - -// Config defines the configuration to create a new Gogs Webhook instance -type Config struct { - Secret string + secret string } // Event defines a Gogs hook event type @@ -42,66 +53,71 @@ const ( ) // New creates and returns a WebHook instance denoted by the Provider type -func New(config *Config) *Webhook { - return &Webhook{ - provider: webhooks.Gogs, - secret: config.Secret, - eventFuncs: map[Event]webhooks.ProcessPayloadFunc{}, +func New(options ...Option) (*Webhook, error) { + hook := new(Webhook) + for _, opt := range options { + if err := opt(hook); err != nil { + return nil, errors.New("Error applying Option") + } } + return hook, nil } -// Provider returns the current hooks provider ID -func (hook Webhook) Provider() webhooks.Provider { - return hook.provider -} +// parse errros +var ( + ErrEventNotSpecifiedToParse = errors.New("No Event specified to parse") + ErrInvalidHTTPMethod = errors.New("Invalid HTTP Method") + ErrMissingGogsEventHeader = errors.New("Missing X-Gogs-Event Header") + ErrMissingGogsSignatureHeader = errors.New("Missing X-Gogs-Signature Header") + ErrEventNotFound = errors.New("Event not defined to be parsed") + ErrParsingPayload = errors.New("Error parsing payload") + ErrHMACVerificationFailed = errors.New("HMAC verification failed") +) -// RegisterEvents registers the function to call when the specified event(s) are encountered -func (hook Webhook) RegisterEvents(fn webhooks.ProcessPayloadFunc, events ...Event) { +// Parse verifies and parses the events specified and returns the payload object or an error +func (hook Webhook) Parse(r *http.Request, events ...Event) (interface{}, error) { + defer func() { + _, _ = io.Copy(ioutil.Discard, r.Body) + _ = r.Body.Close() + }() - for _, event := range events { - hook.eventFuncs[event] = fn + if len(events) == 0 { + return nil, ErrEventNotSpecifiedToParse + } + if r.Method != http.MethodPost { + return nil, ErrInvalidHTTPMethod } -} - -// ParsePayload parses and verifies the payload and fires off the mapped function, if it exists. -func (hook Webhook) ParsePayload(w http.ResponseWriter, r *http.Request) { - webhooks.DefaultLog.Info("Parsing Payload...") event := r.Header.Get("X-Gogs-Event") if len(event) == 0 { - webhooks.DefaultLog.Error("Missing X-Gogs-Event Header") - http.Error(w, "400 Bad Request - Missing X-Gogs-Event Header", http.StatusBadRequest) - return + return nil, ErrMissingGogsEventHeader } - webhooks.DefaultLog.Debug(fmt.Sprintf("X-Gogs-Event:%s", event)) gogsEvent := Event(event) - fn, ok := hook.eventFuncs[gogsEvent] - // if no event registered - if !ok { - webhooks.DefaultLog.Info(fmt.Sprintf("Webhook Event %s not registered, it is recommended to setup only events in gogs that will be registered in the webhook to avoid unnecessary traffic and reduce potential attack vectors.", event)) - return + var found bool + for _, evt := range events { + if evt == gogsEvent { + found = true + break + } + } + // event not defined to be parsed + if !found { + return nil, ErrEventNotFound } payload, err := ioutil.ReadAll(r.Body) if err != nil || len(payload) == 0 { - webhooks.DefaultLog.Error("Issue reading Payload") - http.Error(w, "Issue reading Payload", http.StatusInternalServerError) - return + return nil, ErrParsingPayload } - webhooks.DefaultLog.Debug(fmt.Sprintf("Payload:%s", string(payload))) // If we have a Secret set, we should check the MAC if len(hook.secret) > 0 { - webhooks.DefaultLog.Info("Checking secret") signature := r.Header.Get("X-Gogs-Signature") if len(signature) == 0 { - webhooks.DefaultLog.Error("Missing X-Gogs-Signature required for HMAC verification") - http.Error(w, "403 Forbidden - Missing X-Gogs-Signature required for HMAC verification", http.StatusForbidden) - return + return nil, ErrMissingGogsSignatureHeader } - webhooks.DefaultLog.Debug(fmt.Sprintf("X-Gogs-Signature:%s", signature)) mac := hmac.New(sha256.New, []byte(hook.secret)) mac.Write(payload) @@ -109,60 +125,52 @@ func (hook Webhook) ParsePayload(w http.ResponseWriter, r *http.Request) { expectedMAC := hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(signature), []byte(expectedMAC)) { - webhooks.DefaultLog.Debug(string(payload)) - http.Error(w, "403 Forbidden - HMAC verification failed", http.StatusForbidden) - return + return nil, ErrHMACVerificationFailed } } - // Make headers available to ProcessPayloadFunc as a webhooks type - hd := webhooks.Header(r.Header) - switch gogsEvent { case CreateEvent: - var pe client.CreatePayload - json.Unmarshal([]byte(payload), &pe) - hook.runProcessPayloadFunc(fn, pe, hd) + var pl client.CreatePayload + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case ReleaseEvent: - var re client.ReleasePayload - json.Unmarshal([]byte(payload), &re) - hook.runProcessPayloadFunc(fn, re, hd) + var pl client.ReleasePayload + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case PushEvent: - var pe client.PushPayload - json.Unmarshal([]byte(payload), &pe) - hook.runProcessPayloadFunc(fn, pe, hd) + var pl client.PushPayload + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case DeleteEvent: - var de client.DeletePayload - json.Unmarshal([]byte(payload), &de) - hook.runProcessPayloadFunc(fn, de, hd) + var pl client.DeletePayload + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case ForkEvent: - var fe client.ForkPayload - json.Unmarshal([]byte(payload), &fe) - hook.runProcessPayloadFunc(fn, fe, hd) + var pl client.ForkPayload + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case IssuesEvent: - var ie client.IssuesPayload - json.Unmarshal([]byte(payload), &ie) - hook.runProcessPayloadFunc(fn, ie, hd) + var pl client.IssuesPayload + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case IssueCommentEvent: - var ice client.IssueCommentPayload - json.Unmarshal([]byte(payload), &ice) - hook.runProcessPayloadFunc(fn, ice, hd) + var pl client.IssueCommentPayload + err = json.Unmarshal([]byte(payload), &pl) + return pl, err case PullRequestEvent: - var pre client.PullRequestPayload - json.Unmarshal([]byte(payload), &pre) - hook.runProcessPayloadFunc(fn, pre, hd) + var pl client.PullRequestPayload + err = json.Unmarshal([]byte(payload), &pl) + return pl, err + + default: + return nil, fmt.Errorf("unknown event %s", gogsEvent) } } - -func (hook Webhook) runProcessPayloadFunc(fn webhooks.ProcessPayloadFunc, results interface{}, header webhooks.Header) { - go func(fn webhooks.ProcessPayloadFunc, results interface{}, header webhooks.Header) { - fn(results, header) - }(fn, results, header) -} diff --git a/webhooks.go b/webhooks.go index 55c85aa..516cdee 100644 --- a/webhooks.go +++ b/webhooks.go @@ -1,11 +1,7 @@ package webhooks -import ( - "net/http" -) - // Header provides http.Header to minimize imports -type Header http.Header +// type Header http.Header // // Provider defines the type of webhook // type Provider int @@ -46,7 +42,7 @@ type Header http.Header // } // ProcessPayloadFunc is a common function for payload return values -type ProcessPayloadFunc func(payload interface{}, header Header) error +// type ProcessPayloadFunc func(payload interface{}, header Header) error // // Handler returns the webhook http.Handler for use in your own Mux implementation // func Handler(hook Webhook) http.Handler {