covert bitbucket, gitlab and gogs to new style

This commit is contained in:
Dean Karn
2018-07-25 22:59:10 -07:00
parent 077706a514
commit e452811cf1
7 changed files with 298 additions and 280 deletions
+3 -2
View File
@@ -1,6 +1,6 @@
language: go language: go
go: go:
- 1.10.2 - 1.10.3
- tip - tip
matrix: matrix:
allow_failures: 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.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.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.v4
- ln -s $GOPATH/src/github.com/$TRAVIS_REPO_SLUG $GOPATH/src/gopkg.in/webhooks.v5
before_script: before_script:
- go vet ./... - go vet ./...
@@ -34,6 +35,6 @@ script:
- go test -race - go test -race
after_success: | 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 && overalls -project="github.com/go-playground/webhooks" -covermode=count -ignore=.git,examples -debug &&
goveralls -coverprofile=overalls.coverprofile -service travis-ci -repotoken $COVERALLS_TOKEN goveralls -coverprofile=overalls.coverprofile -service travis-ci -repotoken $COVERALLS_TOKEN
+3 -3
View File
@@ -1,8 +1,8 @@
Library webhooks Library webhooks
================ ================
<img align="right" src="https://raw.githubusercontent.com/go-playground/webhooks/v3/logo.png">![Project status](https://img.shields.io/badge/version-4.1.1-green.svg) <img align="right" src="https://raw.githubusercontent.com/go-playground/webhooks/v5/logo.png">![Project status](https://img.shields.io/badge/version-5.0.0-green.svg)
[![Build Status](https://travis-ci.org/go-playground/webhooks.svg?branch=v4)](https://travis-ci.org/go-playground/webhooks) [![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=v4&service=github)](https://coveralls.io/github/go-playground/webhooks?branch=v3) [![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) [![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) [![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) ![License](https://img.shields.io/dub/l/vibe-d.svg)
+101 -97
View File
@@ -2,23 +2,27 @@ package bitbucket
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net/http" "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 // Webhook instance contains all methods needed to process events
type Webhook struct { type Webhook struct {
provider webhooks.Provider uuid string
uuid string
eventFuncs map[Event]webhooks.ProcessPayloadFunc
}
// Config defines the configuration to create a new Bitbucket Webhook instance
type Config struct {
UUID string
} }
// Event defines a Bitbucket hook event type // Event defines a Bitbucket hook event type
@@ -46,154 +50,154 @@ const (
PullRequestCommentDeletedEvent Event = "pullrequest:comment_deleted" 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 // New creates and returns a WebHook instance denoted by the Provider type
func New(config *Config) *Webhook { func New(options ...Option) (*Webhook, error) {
return &Webhook{ hook := new(Webhook)
provider: webhooks.Bitbucket, for _, opt := range options {
uuid: config.UUID, if err := opt(hook); err != nil {
eventFuncs: map[Event]webhooks.ProcessPayloadFunc{}, return nil, errors.New("Error applying Option")
}
} }
return hook, nil
} }
// Provider returns the current hooks provider ID // Parse verifies and parses the events specified and returns the payload object or an error
func (hook Webhook) Provider() webhooks.Provider { func (hook Webhook) Parse(r *http.Request, events ...Event) (interface{}, error) {
return hook.provider defer func() {
} _, _ = io.Copy(ioutil.Discard, r.Body)
_ = r.Body.Close()
}()
// RegisterEvents registers the function to call when the specified event(s) are encountered if len(events) == 0 {
func (hook Webhook) RegisterEvents(fn webhooks.ProcessPayloadFunc, events ...Event) { return nil, ErrEventNotSpecifiedToParse
}
for _, event := range events { if r.Method != http.MethodPost {
hook.eventFuncs[event] = fn 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") uuid := r.Header.Get("X-Hook-UUID")
if uuid == "" { if uuid == "" {
webhooks.DefaultLog.Error("Missing X-Hook-UUID Header") return nil, ErrMissingHookUUIDHeader
http.Error(w, "400 Bad Request - Missing X-Hook-UUID Header", http.StatusBadRequest)
return
} }
webhooks.DefaultLog.Debug(fmt.Sprintf("X-Hook-UUID:%s", uuid)) if len(hook.uuid) > 0 && uuid != hook.uuid {
return nil, ErrUUIDVerificationFailed
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")
} }
event := r.Header.Get("X-Event-Key") event := r.Header.Get("X-Event-Key")
if event == "" { if event == "" {
webhooks.DefaultLog.Error("Missing X-Event-Key Header") return nil, ErrMissingEventKeyHeader
http.Error(w, "400 Bad Request - Missing X-Event-Key Header", http.StatusBadRequest)
return
} }
webhooks.DefaultLog.Debug(fmt.Sprintf("X-Event-Key:%s", event))
bitbucketEvent := Event(event) bitbucketEvent := Event(event)
fn, ok := hook.eventFuncs[bitbucketEvent] var found bool
// if no event registered for _, evt := range events {
if !ok { if evt == bitbucketEvent {
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)) found = true
return break
}
}
// event not defined to be parsed
if !found {
return nil, ErrEventNotFound
} }
payload, err := ioutil.ReadAll(r.Body) payload, err := ioutil.ReadAll(r.Body)
if err != nil || len(payload) == 0 { if err != nil || len(payload) == 0 {
webhooks.DefaultLog.Error("Issue reading Payload") return nil, ErrParsingPayload
http.Error(w, "Issue reading Payload", http.StatusInternalServerError)
return
} }
webhooks.DefaultLog.Debug(fmt.Sprintf("Payload:%s", string(payload)))
hd := webhooks.Header(r.Header)
switch bitbucketEvent { switch bitbucketEvent {
case RepoPushEvent: case RepoPushEvent:
var pl RepoPushPayload var pl RepoPushPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) return pl, err
case RepoForkEvent: case RepoForkEvent:
var pl RepoForkPayload var pl RepoForkPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) return pl, err
case RepoUpdatedEvent: case RepoUpdatedEvent:
var pl RepoUpdatedPayload var pl RepoUpdatedPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) return pl, err
case RepoCommitCommentCreatedEvent: case RepoCommitCommentCreatedEvent:
var pl RepoCommitCommentCreatedPayload var pl RepoCommitCommentCreatedPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) return pl, err
case RepoCommitStatusCreatedEvent: case RepoCommitStatusCreatedEvent:
var pl RepoCommitStatusCreatedPayload var pl RepoCommitStatusCreatedPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) return pl, err
case RepoCommitStatusUpdatedEvent: case RepoCommitStatusUpdatedEvent:
var pl RepoCommitStatusUpdatedPayload var pl RepoCommitStatusUpdatedPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) return pl, err
case IssueCreatedEvent: case IssueCreatedEvent:
var pl IssueCreatedPayload var pl IssueCreatedPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) return pl, err
case IssueUpdatedEvent: case IssueUpdatedEvent:
var pl IssueUpdatedPayload var pl IssueUpdatedPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) return pl, err
case IssueCommentCreatedEvent: case IssueCommentCreatedEvent:
var pl IssueCommentCreatedPayload var pl IssueCommentCreatedPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) return pl, err
case PullRequestCreatedEvent: case PullRequestCreatedEvent:
var pl PullRequestCreatedPayload var pl PullRequestCreatedPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) return pl, err
case PullRequestUpdatedEvent: case PullRequestUpdatedEvent:
var pl PullRequestUpdatedPayload var pl PullRequestUpdatedPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) return pl, err
case PullRequestApprovedEvent: case PullRequestApprovedEvent:
var pl PullRequestApprovedPayload var pl PullRequestApprovedPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) return pl, err
case PullRequestUnapprovedEvent: case PullRequestUnapprovedEvent:
var pl PullRequestUnapprovedPayload var pl PullRequestUnapprovedPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) return pl, err
case PullRequestMergedEvent: case PullRequestMergedEvent:
var pl PullRequestMergedPayload var pl PullRequestMergedPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) return pl, err
case PullRequestDeclinedEvent: case PullRequestDeclinedEvent:
var pl PullRequestDeclinedPayload var pl PullRequestDeclinedPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) return pl, err
case PullRequestCommentCreatedEvent: case PullRequestCommentCreatedEvent:
var pl PullRequestCommentCreatedPayload var pl PullRequestCommentCreatedPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) return pl, err
case PullRequestCommentUpdatedEvent: case PullRequestCommentUpdatedEvent:
var pl PullRequestCommentUpdatedPayload var pl PullRequestCommentUpdatedPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) return pl, err
case PullRequestCommentDeletedEvent: case PullRequestCommentDeletedEvent:
var pl PullRequestCommentDeletedPayload var pl PullRequestCommentDeletedPayload
json.Unmarshal([]byte(payload), &pl) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pl, hd) 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)
}
+3 -3
View File
@@ -17,9 +17,9 @@ var (
ErrEventNotSpecifiedToParse = errors.New("No Event specified to parse") ErrEventNotSpecifiedToParse = errors.New("No Event specified to parse")
ErrInvalidHTTPMethod = errors.New("Invalid HTTP Method") ErrInvalidHTTPMethod = errors.New("Invalid HTTP Method")
ErrMissingGithubEventHeader = errors.New("Missing X-GitHub-Event Header") 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") 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") 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") event := r.Header.Get("X-GitHub-Event")
if len(event) == 0 { if event == "" {
return nil, ErrMissingGithubEventHeader return nil, ErrMissingGithubEventHeader
} }
gitHubEvent := Event(event) gitHubEvent := Event(event)
+97 -88
View File
@@ -2,28 +2,13 @@ package gitlab
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net/http" "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 // GitLab hook types
const ( const (
PushEvents Event = "Push Hook" PushEvents Event = "Push Hook"
@@ -37,121 +22,145 @@ const (
BuildEvents Event = "Build Hook" 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 // New creates and returns a WebHook instance denoted by the Provider type
func New(config *Config) *Webhook { func New(options ...Option) (*Webhook, error) {
return &Webhook{ hook := new(Webhook)
provider: webhooks.GitLab, for _, opt := range options {
secret: config.Secret, if err := opt(hook); err != nil {
eventFuncs: map[Event]webhooks.ProcessPayloadFunc{}, return nil, errors.New("Error applying Option")
}
} }
return hook, nil
} }
// Provider returns the current hooks provider ID // parse errros
func (hook Webhook) Provider() webhooks.Provider { var (
return hook.provider 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 // Parse verifies and parses the events specified and returns the payload object or an error
func (hook Webhook) RegisterEvents(fn webhooks.ProcessPayloadFunc, events ...Event) { 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 { if len(events) == 0 {
hook.eventFuncs[event] = fn 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") event := r.Header.Get("X-Gitlab-Event")
if len(event) == 0 { if len(event) == 0 {
webhooks.DefaultLog.Error("Missing X-Gitlab-Event Header") return nil, ErrMissingGitLabEventHeader
http.Error(w, "400 Bad Request - Missing X-Gitlab-Event Header", http.StatusBadRequest)
return
} }
webhooks.DefaultLog.Debug(fmt.Sprintf("X-Gitlab-Event:%s", event))
gitLabEvent := Event(event) gitLabEvent := Event(event)
fn, ok := hook.eventFuncs[gitLabEvent] var found bool
// if no event registered for _, evt := range events {
if !ok { if evt == gitLabEvent {
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)) found = true
return break
}
}
// event not defined to be parsed
if !found {
return nil, ErrEventNotFound
} }
payload, err := ioutil.ReadAll(r.Body) payload, err := ioutil.ReadAll(r.Body)
if err != nil || len(payload) == 0 { if err != nil || len(payload) == 0 {
webhooks.DefaultLog.Error("Issue reading Payload") return nil, ErrParsingPayload
http.Error(w, "Error reading Payload", http.StatusInternalServerError)
return
} }
webhooks.DefaultLog.Debug(fmt.Sprintf("Payload:%s", string(payload)))
// If we have a Secret set, we should check the MAC // If we have a Secret set, we should check the MAC
if len(hook.secret) > 0 { if len(hook.secret) > 0 {
webhooks.DefaultLog.Info("Checking secret")
signature := r.Header.Get("X-Gitlab-Token") signature := r.Header.Get("X-Gitlab-Token")
if signature != hook.secret { if signature != hook.secret {
webhooks.DefaultLog.Error(fmt.Sprintf("Invalid X-Gitlab-Token of '%s'", signature)) return nil, ErrMissingGitLabTokenHeader
http.Error(w, "403 Forbidden - Token missmatch", http.StatusForbidden)
return
} }
} }
// Make headers available to ProcessPayloadFunc as a webhooks type
hd := webhooks.Header(r.Header)
switch gitLabEvent { switch gitLabEvent {
case PushEvents: case PushEvents:
var pe PushEventPayload var pl PushEventPayload
json.Unmarshal([]byte(payload), &pe) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pe, hd) return pl, err
case TagEvents: case TagEvents:
var te TagEventPayload var pl TagEventPayload
json.Unmarshal([]byte(payload), &te) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, te, hd) return pl, err
case ConfidentialIssuesEvents: case ConfidentialIssuesEvents:
var cie ConfidentialIssueEventPayload var pl ConfidentialIssueEventPayload
json.Unmarshal([]byte(payload), &cie) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, cie, hd) return pl, err
case IssuesEvents: case IssuesEvents:
var ie IssueEventPayload var pl IssueEventPayload
json.Unmarshal([]byte(payload), &ie) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, ie, hd) return pl, err
case CommentEvents: case CommentEvents:
var ce CommentEventPayload var pl CommentEventPayload
json.Unmarshal([]byte(payload), &ce) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, ce, hd) return pl, err
case MergeRequestEvents: case MergeRequestEvents:
var mre MergeRequestEventPayload var pl MergeRequestEventPayload
json.Unmarshal([]byte(payload), &mre) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, mre, hd) return pl, err
case WikiPageEvents: case WikiPageEvents:
var wpe WikiPageEventPayload var pl WikiPageEventPayload
json.Unmarshal([]byte(payload), &wpe) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, wpe, hd) return pl, err
case PipelineEvents: case PipelineEvents:
var pe PipelineEventPayload var pl PipelineEventPayload
json.Unmarshal([]byte(payload), &pe) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pe, hd) return pl, err
case BuildEvents: case BuildEvents:
var be BuildEventPayload var pl BuildEventPayload
json.Unmarshal([]byte(payload), &be) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, be, hd) 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)
}
+89 -81
View File
@@ -2,7 +2,9 @@ package gogs
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@@ -11,19 +13,28 @@ import (
"encoding/hex" "encoding/hex"
client "github.com/gogits/go-gogs-client" 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 // Webhook instance contains all methods needed to process events
type Webhook struct { type Webhook struct {
provider webhooks.Provider secret string
secret string
eventFuncs map[Event]webhooks.ProcessPayloadFunc
}
// Config defines the configuration to create a new Gogs Webhook instance
type Config struct {
Secret string
} }
// Event defines a Gogs hook event type // Event defines a Gogs hook event type
@@ -42,66 +53,71 @@ const (
) )
// New creates and returns a WebHook instance denoted by the Provider type // New creates and returns a WebHook instance denoted by the Provider type
func New(config *Config) *Webhook { func New(options ...Option) (*Webhook, error) {
return &Webhook{ hook := new(Webhook)
provider: webhooks.Gogs, for _, opt := range options {
secret: config.Secret, if err := opt(hook); err != nil {
eventFuncs: map[Event]webhooks.ProcessPayloadFunc{}, return nil, errors.New("Error applying Option")
}
} }
return hook, nil
} }
// Provider returns the current hooks provider ID // parse errros
func (hook Webhook) Provider() webhooks.Provider { var (
return hook.provider 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 // Parse verifies and parses the events specified and returns the payload object or an error
func (hook Webhook) RegisterEvents(fn webhooks.ProcessPayloadFunc, events ...Event) { 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 { if len(events) == 0 {
hook.eventFuncs[event] = fn 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") event := r.Header.Get("X-Gogs-Event")
if len(event) == 0 { if len(event) == 0 {
webhooks.DefaultLog.Error("Missing X-Gogs-Event Header") return nil, ErrMissingGogsEventHeader
http.Error(w, "400 Bad Request - Missing X-Gogs-Event Header", http.StatusBadRequest)
return
} }
webhooks.DefaultLog.Debug(fmt.Sprintf("X-Gogs-Event:%s", event))
gogsEvent := Event(event) gogsEvent := Event(event)
fn, ok := hook.eventFuncs[gogsEvent] var found bool
// if no event registered for _, evt := range events {
if !ok { if evt == gogsEvent {
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)) found = true
return break
}
}
// event not defined to be parsed
if !found {
return nil, ErrEventNotFound
} }
payload, err := ioutil.ReadAll(r.Body) payload, err := ioutil.ReadAll(r.Body)
if err != nil || len(payload) == 0 { if err != nil || len(payload) == 0 {
webhooks.DefaultLog.Error("Issue reading Payload") return nil, ErrParsingPayload
http.Error(w, "Issue reading Payload", http.StatusInternalServerError)
return
} }
webhooks.DefaultLog.Debug(fmt.Sprintf("Payload:%s", string(payload)))
// If we have a Secret set, we should check the MAC // If we have a Secret set, we should check the MAC
if len(hook.secret) > 0 { if len(hook.secret) > 0 {
webhooks.DefaultLog.Info("Checking secret")
signature := r.Header.Get("X-Gogs-Signature") signature := r.Header.Get("X-Gogs-Signature")
if len(signature) == 0 { if len(signature) == 0 {
webhooks.DefaultLog.Error("Missing X-Gogs-Signature required for HMAC verification") return nil, ErrMissingGogsSignatureHeader
http.Error(w, "403 Forbidden - Missing X-Gogs-Signature required for HMAC verification", http.StatusForbidden)
return
} }
webhooks.DefaultLog.Debug(fmt.Sprintf("X-Gogs-Signature:%s", signature))
mac := hmac.New(sha256.New, []byte(hook.secret)) mac := hmac.New(sha256.New, []byte(hook.secret))
mac.Write(payload) mac.Write(payload)
@@ -109,60 +125,52 @@ func (hook Webhook) ParsePayload(w http.ResponseWriter, r *http.Request) {
expectedMAC := hex.EncodeToString(mac.Sum(nil)) expectedMAC := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(signature), []byte(expectedMAC)) { if !hmac.Equal([]byte(signature), []byte(expectedMAC)) {
webhooks.DefaultLog.Debug(string(payload)) return nil, ErrHMACVerificationFailed
http.Error(w, "403 Forbidden - HMAC verification failed", http.StatusForbidden)
return
} }
} }
// Make headers available to ProcessPayloadFunc as a webhooks type
hd := webhooks.Header(r.Header)
switch gogsEvent { switch gogsEvent {
case CreateEvent: case CreateEvent:
var pe client.CreatePayload var pl client.CreatePayload
json.Unmarshal([]byte(payload), &pe) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pe, hd) return pl, err
case ReleaseEvent: case ReleaseEvent:
var re client.ReleasePayload var pl client.ReleasePayload
json.Unmarshal([]byte(payload), &re) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, re, hd) return pl, err
case PushEvent: case PushEvent:
var pe client.PushPayload var pl client.PushPayload
json.Unmarshal([]byte(payload), &pe) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pe, hd) return pl, err
case DeleteEvent: case DeleteEvent:
var de client.DeletePayload var pl client.DeletePayload
json.Unmarshal([]byte(payload), &de) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, de, hd) return pl, err
case ForkEvent: case ForkEvent:
var fe client.ForkPayload var pl client.ForkPayload
json.Unmarshal([]byte(payload), &fe) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, fe, hd) return pl, err
case IssuesEvent: case IssuesEvent:
var ie client.IssuesPayload var pl client.IssuesPayload
json.Unmarshal([]byte(payload), &ie) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, ie, hd) return pl, err
case IssueCommentEvent: case IssueCommentEvent:
var ice client.IssueCommentPayload var pl client.IssueCommentPayload
json.Unmarshal([]byte(payload), &ice) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, ice, hd) return pl, err
case PullRequestEvent: case PullRequestEvent:
var pre client.PullRequestPayload var pl client.PullRequestPayload
json.Unmarshal([]byte(payload), &pre) err = json.Unmarshal([]byte(payload), &pl)
hook.runProcessPayloadFunc(fn, pre, hd) 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)
}
+2 -6
View File
@@ -1,11 +1,7 @@
package webhooks package webhooks
import (
"net/http"
)
// Header provides http.Header to minimize imports // Header provides http.Header to minimize imports
type Header http.Header // type Header http.Header
// // Provider defines the type of webhook // // Provider defines the type of webhook
// type Provider int // type Provider int
@@ -46,7 +42,7 @@ type Header http.Header
// } // }
// ProcessPayloadFunc is a common function for payload return values // 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 // // Handler returns the webhook http.Handler for use in your own Mux implementation
// func Handler(hook Webhook) http.Handler { // func Handler(hook Webhook) http.Handler {