Add Processing + registration functionality
* Also updated/added comments
This commit is contained in:
+95
-15
@@ -1,15 +1,26 @@
|
|||||||
package github
|
package github
|
||||||
|
|
||||||
import "github.com/joeybloggs/webhooks"
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/joeybloggs/webhooks"
|
||||||
|
)
|
||||||
|
|
||||||
// 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
|
provider webhooks.Provider
|
||||||
|
secret string
|
||||||
|
eventFuncs map[Event]webhooks.ProcessPayloadFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config defines the configuration to create a new GitHubWebhook instance
|
// Config defines the configuration to create a new GitHubWebhook instance
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Provider webhooks.Provider
|
Secret string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event defines a GitHub hook event type
|
// Event defines a GitHub hook event type
|
||||||
@@ -17,7 +28,6 @@ type Event string
|
|||||||
|
|
||||||
// GitHub hook types
|
// GitHub hook types
|
||||||
const (
|
const (
|
||||||
// AnyEvent Event = "*"
|
|
||||||
CommitCommentEvent Event = "commit_comment"
|
CommitCommentEvent Event = "commit_comment"
|
||||||
CreateEvent Event = "create"
|
CreateEvent Event = "create"
|
||||||
DeleteEvent Event = "delete"
|
DeleteEvent Event = "delete"
|
||||||
@@ -53,19 +63,89 @@ const (
|
|||||||
IssueSubtype EventSubtype = "issues"
|
IssueSubtype EventSubtype = "issues"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Provider returns the Webhook's provider
|
|
||||||
func (w Webhook) Provider() webhooks.Provider {
|
|
||||||
return w.provider
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnderlyingProvider returns the Config's Provider
|
|
||||||
func (c Config) UnderlyingProvider() webhooks.Provider {
|
|
||||||
return c.Provider
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(config *Config) *Webhook {
|
||||||
return &Webhook{
|
return &Webhook{
|
||||||
provider: config.Provider,
|
provider: webhooks.GitHub,
|
||||||
|
secret: config.Secret,
|
||||||
|
eventFuncs: map[Event]webhooks.ProcessPayloadFunc{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provider returns the current hooks provider ID
|
||||||
|
func (hook Webhook) Provider() webhooks.Provider {
|
||||||
|
return hook.provider
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
|
||||||
|
event := r.Header.Get("X-GitHub-Event")
|
||||||
|
if len(event) == 0 {
|
||||||
|
http.Error(w, "400 Bad Request - Missing X-GitHub-Event Header", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gitHubEvent := Event(event)
|
||||||
|
|
||||||
|
fn, ok := hook.eventFuncs[gitHubEvent]
|
||||||
|
// if no event registered
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a Secret set, we should check the MAC
|
||||||
|
if len(hook.secret) > 0 {
|
||||||
|
|
||||||
|
signature := r.Header.Get("X-Hub-Signature")
|
||||||
|
|
||||||
|
if len(signature) == 0 {
|
||||||
|
http.Error(w, "403 Forbidden - Missing X-Hub-Signature required for HMAC verification", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mac := hmac.New(sha1.New, []byte(hook.secret))
|
||||||
|
_, err := mac.Write(payload)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "400 Bad Request - HMAC verification failed with body parsing", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedMAC := hex.EncodeToString(mac.Sum(nil))
|
||||||
|
|
||||||
|
if !hmac.Equal([]byte(signature[5:]), []byte(expectedMAC)) {
|
||||||
|
http.Error(w, "403 Forbidden - HMAC verification failed", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var results interface{}
|
||||||
|
|
||||||
|
switch gitHubEvent {
|
||||||
|
case ReleaseEvent:
|
||||||
|
var release ReleasePayload
|
||||||
|
json.Unmarshal([]byte(payload), &release)
|
||||||
|
results = release
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(fn webhooks.ProcessPayloadFunc, results interface{}) {
|
||||||
|
|
||||||
|
// put in recovery here!
|
||||||
|
|
||||||
|
fn(results)
|
||||||
|
}(fn, results)
|
||||||
|
}
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ func TestCommitCommentHook(t *testing.T) {
|
|||||||
|
|
||||||
json.Unmarshal([]byte(body), &cc)
|
json.Unmarshal([]byte(body), &cc)
|
||||||
|
|
||||||
Equal(t, cc.Comment.Line, nil)
|
Equal(t, cc.Comment.Line, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateHook(t *testing.T) {
|
func TestCreateHook(t *testing.T) {
|
||||||
|
|||||||
+29
-29
@@ -22,9 +22,9 @@ type StatusPayload struct {
|
|||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
SHA string `json:"sha"`
|
SHA string `json:"sha"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
TragetURL *string `json:"target_url"`
|
TragetURL string `json:"target_url"`
|
||||||
Context string `json:"context"`
|
Context string `json:"context"`
|
||||||
Desctiption *string `json:"description"`
|
Desctiption string `json:"description"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
Commit StatusCommit `json:"commit"`
|
Commit StatusCommit `json:"commit"`
|
||||||
Branches []Branch `json:"branches"`
|
Branches []Branch `json:"branches"`
|
||||||
@@ -58,7 +58,7 @@ type PushPayload struct {
|
|||||||
Created bool `json:"created"`
|
Created bool `json:"created"`
|
||||||
Deleted bool `json:"deleted"`
|
Deleted bool `json:"deleted"`
|
||||||
Forced bool `json:"forced"`
|
Forced bool `json:"forced"`
|
||||||
BaseRef *string `json:"base_ref"`
|
BaseRef string `json:"base_ref"`
|
||||||
Compare string `json:"compare"`
|
Compare string `json:"compare"`
|
||||||
Commits []Commit `json:"commits"`
|
Commits []Commit `json:"commits"`
|
||||||
HeadCommit HeadCommit `json:"head_commit"`
|
HeadCommit HeadCommit `json:"head_commit"`
|
||||||
@@ -159,7 +159,7 @@ type DeploymentPayload struct {
|
|||||||
Sender Sender `json:"sender"`
|
Sender Sender `json:"sender"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitComment contains the information for GitHub's commit_comment hook event
|
// CommitCommentPayload contains the information for GitHub's commit_comment hook event
|
||||||
type CommitCommentPayload struct {
|
type CommitCommentPayload struct {
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
RefType string `json:"ref_type"`
|
RefType string `json:"ref_type"`
|
||||||
@@ -245,7 +245,7 @@ type Repository struct {
|
|||||||
Size int `json:"size"`
|
Size int `json:"size"`
|
||||||
StargazersCount int `json:"stargazers_count"`
|
StargazersCount int `json:"stargazers_count"`
|
||||||
WatchersCount int `json:"watchers_count"`
|
WatchersCount int `json:"watchers_count"`
|
||||||
Language *string `json:"language"`
|
Language string `json:"language"`
|
||||||
HasIssues bool `json:"has_issues"`
|
HasIssues bool `json:"has_issues"`
|
||||||
HasDownloads bool `json:"has_downloads"`
|
HasDownloads bool `json:"has_downloads"`
|
||||||
HasWiki bool `json:"has_wiki"`
|
HasWiki bool `json:"has_wiki"`
|
||||||
@@ -326,9 +326,9 @@ type Comment struct {
|
|||||||
HTMLURL string `json:"html_url"`
|
HTMLURL string `json:"html_url"`
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
User User `json:"user"`
|
User User `json:"user"`
|
||||||
Position *int `json:"position"`
|
Position int `json:"position"`
|
||||||
Line *int `json:"line"`
|
Line int `json:"line"`
|
||||||
Path *string `json:"path"`
|
Path string `json:"path"`
|
||||||
CommitID string `json:"commit_id"`
|
CommitID string `json:"commit_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -344,7 +344,7 @@ type Deployment struct {
|
|||||||
Task string `json:"task"`
|
Task string `json:"task"`
|
||||||
//paylod
|
//paylod
|
||||||
Environment string `json:"environment"`
|
Environment string `json:"environment"`
|
||||||
Description *string `json:"description"`
|
Description string `json:"description"`
|
||||||
Creator Creator `json:"creator"`
|
Creator Creator `json:"creator"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -358,7 +358,7 @@ type DeploymentStatus struct {
|
|||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
Creator Creator `json:"creator"`
|
Creator Creator `json:"creator"`
|
||||||
Description *string `json:"description"`
|
Description string `json:"description"`
|
||||||
TargetURL string `json:"target_url"`
|
TargetURL string `json:"target_url"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -374,22 +374,22 @@ type Forkee struct {
|
|||||||
|
|
||||||
// Page contains GitHub's page information
|
// Page contains GitHub's page information
|
||||||
type Page struct {
|
type Page struct {
|
||||||
PageName string `json:"page_name"`
|
PageName string `json:"page_name"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Summary *string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
SHA string `json:"sha"`
|
SHA string `json:"sha"`
|
||||||
HTMLURL string `json:"html_url"`
|
HTMLURL string `json:"html_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page contains GitHub's label information
|
// Label contains GitHub's label information
|
||||||
type Label struct {
|
type Label struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Color string `json:"color"`
|
Color string `json:"color"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page contains GitHub's issue information
|
// Issue contains GitHub's issue information
|
||||||
type Issue struct {
|
type Issue struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
LabelsURL string `json:"labels_url"`
|
LabelsURL string `json:"labels_url"`
|
||||||
@@ -403,8 +403,8 @@ type Issue struct {
|
|||||||
Labels []Label `json:"labels"`
|
Labels []Label `json:"labels"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
Locked bool `json:"locked"`
|
Locked bool `json:"locked"`
|
||||||
Assignee *string `json:"assignee"`
|
Assignee string `json:"assignee"`
|
||||||
Milestone *string `json:"milestone"`
|
Milestone string `json:"milestone"`
|
||||||
Comments int `json:"comments"`
|
Comments int `json:"comments"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -567,9 +567,9 @@ type PullRequest struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
ClosedAt time.Time `json:"closed_at"`
|
ClosedAt time.Time `json:"closed_at"`
|
||||||
MergedAt time.Time `json:"merged_at"`
|
MergedAt time.Time `json:"merged_at"`
|
||||||
MergeCommitSHA *string `json:"merge_commit_sha"`
|
MergeCommitSHA string `json:"merge_commit_sha"`
|
||||||
Assignee *string `json:"assignee"`
|
Assignee string `json:"assignee"`
|
||||||
Milestone *string `json:"milestone"`
|
Milestone string `json:"milestone"`
|
||||||
CommitsURL string `json:"commits_url"`
|
CommitsURL string `json:"commits_url"`
|
||||||
ReviewCommentsURL string `json:"review_comments_url"`
|
ReviewCommentsURL string `json:"review_comments_url"`
|
||||||
ReviewCommentURL string `json:"review_comment_url"`
|
ReviewCommentURL string `json:"review_comment_url"`
|
||||||
@@ -579,9 +579,9 @@ type PullRequest struct {
|
|||||||
Base Base `json:"base"`
|
Base Base `json:"base"`
|
||||||
Links LinksPullRequest `json:"_links"`
|
Links LinksPullRequest `json:"_links"`
|
||||||
Merged bool `json:"merged"`
|
Merged bool `json:"merged"`
|
||||||
Mergable *bool `json:"mergeable"`
|
Mergable bool `json:"mergeable"`
|
||||||
MergableState string `json:"mergeable_state"`
|
MergableState string `json:"mergeable_state"`
|
||||||
MergedBy *string `json:"merged_by"`
|
MergedBy string `json:"merged_by"`
|
||||||
Comments int `json:"comments"`
|
Comments int `json:"comments"`
|
||||||
ReviewComments int `json:"review_comments"`
|
ReviewComments int `json:"review_comments"`
|
||||||
Commits int `json:"commits"`
|
Commits int `json:"commits"`
|
||||||
@@ -633,10 +633,10 @@ type Release struct {
|
|||||||
AssetsURL string `json:"assets_url"`
|
AssetsURL string `json:"assets_url"`
|
||||||
UploadURL string `json:"upload_url"`
|
UploadURL string `json:"upload_url"`
|
||||||
HTMLURL string `json:"html_url"`
|
HTMLURL string `json:"html_url"`
|
||||||
ID string `json:"id"`
|
ID int `json:"id"`
|
||||||
TagName string `json:"tag_name"`
|
TagName string `json:"tag_name"`
|
||||||
TargetCommitish string `json:"target_commitish"`
|
TargetCommitish string `json:"target_commitish"`
|
||||||
Name *string `json:"name"`
|
Name string `json:"name"`
|
||||||
Draft bool `json:"draft"`
|
Draft bool `json:"draft"`
|
||||||
Author Author `json:"author"`
|
Author Author `json:"author"`
|
||||||
Prelelease bool `json:"prerelease"`
|
Prelelease bool `json:"prerelease"`
|
||||||
@@ -645,7 +645,7 @@ type Release struct {
|
|||||||
Assets []string `json:"assets"`
|
Assets []string `json:"assets"`
|
||||||
TarballURL string `json:"tarball_url"`
|
TarballURL string `json:"tarball_url"`
|
||||||
ZipballURL string `json:"zipball_url"`
|
ZipballURL string `json:"zipball_url"`
|
||||||
Body *string `json:"body"`
|
Body string `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BranchCommit contains GitHub's branch commit information
|
// BranchCommit contains GitHub's branch commit information
|
||||||
@@ -695,7 +695,7 @@ type StatusCommit struct {
|
|||||||
Commit StatusCommitInner `json:"commit"`
|
Commit StatusCommitInner `json:"commit"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
HTMLURL string `json:"html_url"`
|
HTMLURL string `json:"html_url"`
|
||||||
CommentsURL string `json:"comments_url`
|
CommentsURL string `json:"comments_url"`
|
||||||
Author Author `json:"author"`
|
Author Author `json:"author"`
|
||||||
Committer Commiter `json:"committer"`
|
Committer Commiter `json:"committer"`
|
||||||
Parents []string `json:"parents"`
|
Parents []string `json:"parents"`
|
||||||
|
|||||||
+98
-15
@@ -1,8 +1,23 @@
|
|||||||
package webhooks
|
package webhooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
// Provider defines the type of webhook
|
// Provider defines the type of webhook
|
||||||
type Provider int
|
type Provider int
|
||||||
|
|
||||||
|
func (p Provider) String() string {
|
||||||
|
switch p {
|
||||||
|
case GitHub:
|
||||||
|
return "GitHub"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// webhooks available providers
|
// webhooks available providers
|
||||||
const (
|
const (
|
||||||
GitHub Provider = iota
|
GitHub Provider = iota
|
||||||
@@ -11,23 +26,91 @@ const (
|
|||||||
// Webhook interface defines a webhook to recieve events
|
// Webhook interface defines a webhook to recieve events
|
||||||
type Webhook interface {
|
type Webhook interface {
|
||||||
Provider() Provider
|
Provider() Provider
|
||||||
|
ParsePayload(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config interface defines the config to setup a webhook instance
|
type server struct {
|
||||||
type Config interface {
|
hook Webhook
|
||||||
UnderlyingProvider() Provider
|
path string
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates and returns a WebHook instance denoted by the Provider type
|
// ProcessPayloadFunc is a common function for payload return values
|
||||||
// func New(config Config) Webhook {
|
type ProcessPayloadFunc func(payload interface{})
|
||||||
|
|
||||||
// switch config.UnderlyingProvider() {
|
// Run runs a server
|
||||||
// case GitHub:
|
func Run(hook Webhook, addr string, path string) error {
|
||||||
// c := config.(*GitHubConfig)
|
srv := &server{
|
||||||
// return &GitHubWebhook{
|
hook: hook,
|
||||||
// provider: c.Provider,
|
path: path,
|
||||||
// }
|
}
|
||||||
// default:
|
|
||||||
// panic("Invalid config type")
|
s := &http.Server{Addr: addr, Handler: srv}
|
||||||
// }
|
|
||||||
// }
|
return run(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunTLS runs a server with TLS configuration.
|
||||||
|
func RunTLS(hook Webhook, addr string, path string, certFile string, keyFile string) error {
|
||||||
|
srv := &server{
|
||||||
|
hook: hook,
|
||||||
|
path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &http.Server{Addr: addr, Handler: srv}
|
||||||
|
|
||||||
|
return run(s, certFile, keyFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunServer runs a custom server.
|
||||||
|
func RunServer(s *http.Server, hook Webhook, addr string, path string) error {
|
||||||
|
|
||||||
|
srv := &server{
|
||||||
|
hook: hook,
|
||||||
|
path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Handler = srv
|
||||||
|
|
||||||
|
return run(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunTLSServer runs a custom server with TLS configuration.
|
||||||
|
// NOTE: http.Server Handler will be overridden by this library, just set it to nil
|
||||||
|
func RunTLSServer(s *http.Server, hook Webhook, addr string, path string, certFile string, keyFile string) error {
|
||||||
|
|
||||||
|
srv := &server{
|
||||||
|
hook: hook,
|
||||||
|
path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Handler = srv
|
||||||
|
|
||||||
|
return run(s, certFile, keyFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(s *http.Server, files ...string) error {
|
||||||
|
if len(files) == 0 {
|
||||||
|
return s.ListenAndServe()
|
||||||
|
} else if len(files) == 2 {
|
||||||
|
return s.ListenAndServeTLS(files[0], files[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("invalid server configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
fmt.Println("GOT HERE!")
|
||||||
|
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "405 Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.URL.Path != s.path {
|
||||||
|
http.Error(w, "404 Not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.hook.ParsePayload(w, r)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user