From c6507a6c00711692b778c5f2f3a9cbb4976f3154 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Thu, 29 Oct 2015 16:53:20 -0400 Subject: [PATCH] Add Processing + registration functionality * Also updated/added comments --- github/github.go | 110 ++++++++++++++++++++++++++++++++++------ github/github_test.go | 2 +- github/payload.go | 58 +++++++++++----------- webhooks.go | 113 ++++++++++++++++++++++++++++++++++++------ 4 files changed, 223 insertions(+), 60 deletions(-) diff --git a/github/github.go b/github/github.go index a0595d8..d25743d 100644 --- a/github/github.go +++ b/github/github.go @@ -1,15 +1,26 @@ 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 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 type Config struct { - Provider webhooks.Provider + Secret string } // Event defines a GitHub hook event type @@ -17,7 +28,6 @@ type Event string // GitHub hook types const ( - // AnyEvent Event = "*" CommitCommentEvent Event = "commit_comment" CreateEvent Event = "create" DeleteEvent Event = "delete" @@ -53,19 +63,89 @@ const ( 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 func New(config *Config) *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) +} diff --git a/github/github_test.go b/github/github_test.go index 0ae31ee..2f1b323 100644 --- a/github/github_test.go +++ b/github/github_test.go @@ -177,7 +177,7 @@ func TestCommitCommentHook(t *testing.T) { json.Unmarshal([]byte(body), &cc) - Equal(t, cc.Comment.Line, nil) + Equal(t, cc.Comment.Line, 0) } func TestCreateHook(t *testing.T) { diff --git a/github/payload.go b/github/payload.go index 580e212..fccf0e4 100644 --- a/github/payload.go +++ b/github/payload.go @@ -22,9 +22,9 @@ type StatusPayload struct { ID int `json:"id"` SHA string `json:"sha"` Name string `json:"name"` - TragetURL *string `json:"target_url"` + TragetURL string `json:"target_url"` Context string `json:"context"` - Desctiption *string `json:"description"` + Desctiption string `json:"description"` State string `json:"state"` Commit StatusCommit `json:"commit"` Branches []Branch `json:"branches"` @@ -58,7 +58,7 @@ type PushPayload struct { Created bool `json:"created"` Deleted bool `json:"deleted"` Forced bool `json:"forced"` - BaseRef *string `json:"base_ref"` + BaseRef string `json:"base_ref"` Compare string `json:"compare"` Commits []Commit `json:"commits"` HeadCommit HeadCommit `json:"head_commit"` @@ -159,7 +159,7 @@ type DeploymentPayload struct { 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 { Action string `json:"action"` RefType string `json:"ref_type"` @@ -245,7 +245,7 @@ type Repository struct { Size int `json:"size"` StargazersCount int `json:"stargazers_count"` WatchersCount int `json:"watchers_count"` - Language *string `json:"language"` + Language string `json:"language"` HasIssues bool `json:"has_issues"` HasDownloads bool `json:"has_downloads"` HasWiki bool `json:"has_wiki"` @@ -326,9 +326,9 @@ type Comment struct { HTMLURL string `json:"html_url"` ID int `json:"id"` User User `json:"user"` - Position *int `json:"position"` - Line *int `json:"line"` - Path *string `json:"path"` + Position int `json:"position"` + Line int `json:"line"` + Path string `json:"path"` CommitID string `json:"commit_id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -344,7 +344,7 @@ type Deployment struct { Task string `json:"task"` //paylod Environment string `json:"environment"` - Description *string `json:"description"` + Description string `json:"description"` Creator Creator `json:"creator"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -358,7 +358,7 @@ type DeploymentStatus struct { ID int `json:"id"` State string `json:"state"` Creator Creator `json:"creator"` - Description *string `json:"description"` + Description string `json:"description"` TargetURL string `json:"target_url"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -374,22 +374,22 @@ type Forkee struct { // Page contains GitHub's page information type Page struct { - PageName string `json:"page_name"` - Title string `json:"title"` - Summary *string `json:"summary"` - Action string `json:"action"` - SHA string `json:"sha"` - HTMLURL string `json:"html_url"` + PageName string `json:"page_name"` + Title string `json:"title"` + Summary string `json:"summary"` + Action string `json:"action"` + SHA string `json:"sha"` + HTMLURL string `json:"html_url"` } -// Page contains GitHub's label information +// Label contains GitHub's label information type Label struct { URL string `json:"url"` Name string `json:"name"` Color string `json:"color"` } -// Page contains GitHub's issue information +// Issue contains GitHub's issue information type Issue struct { URL string `json:"url"` LabelsURL string `json:"labels_url"` @@ -403,8 +403,8 @@ type Issue struct { Labels []Label `json:"labels"` State string `json:"state"` Locked bool `json:"locked"` - Assignee *string `json:"assignee"` - Milestone *string `json:"milestone"` + Assignee string `json:"assignee"` + Milestone string `json:"milestone"` Comments int `json:"comments"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -567,9 +567,9 @@ type PullRequest struct { UpdatedAt time.Time `json:"updated_at"` ClosedAt time.Time `json:"closed_at"` MergedAt time.Time `json:"merged_at"` - MergeCommitSHA *string `json:"merge_commit_sha"` - Assignee *string `json:"assignee"` - Milestone *string `json:"milestone"` + MergeCommitSHA string `json:"merge_commit_sha"` + Assignee string `json:"assignee"` + Milestone string `json:"milestone"` CommitsURL string `json:"commits_url"` ReviewCommentsURL string `json:"review_comments_url"` ReviewCommentURL string `json:"review_comment_url"` @@ -579,9 +579,9 @@ type PullRequest struct { Base Base `json:"base"` Links LinksPullRequest `json:"_links"` Merged bool `json:"merged"` - Mergable *bool `json:"mergeable"` + Mergable bool `json:"mergeable"` MergableState string `json:"mergeable_state"` - MergedBy *string `json:"merged_by"` + MergedBy string `json:"merged_by"` Comments int `json:"comments"` ReviewComments int `json:"review_comments"` Commits int `json:"commits"` @@ -633,10 +633,10 @@ type Release struct { AssetsURL string `json:"assets_url"` UploadURL string `json:"upload_url"` HTMLURL string `json:"html_url"` - ID string `json:"id"` + ID int `json:"id"` TagName string `json:"tag_name"` TargetCommitish string `json:"target_commitish"` - Name *string `json:"name"` + Name string `json:"name"` Draft bool `json:"draft"` Author Author `json:"author"` Prelelease bool `json:"prerelease"` @@ -645,7 +645,7 @@ type Release struct { Assets []string `json:"assets"` TarballURL string `json:"tarball_url"` ZipballURL string `json:"zipball_url"` - Body *string `json:"body"` + Body string `json:"body"` } // BranchCommit contains GitHub's branch commit information @@ -695,7 +695,7 @@ type StatusCommit struct { Commit StatusCommitInner `json:"commit"` URL string `json:"url"` HTMLURL string `json:"html_url"` - CommentsURL string `json:"comments_url` + CommentsURL string `json:"comments_url"` Author Author `json:"author"` Committer Commiter `json:"committer"` Parents []string `json:"parents"` diff --git a/webhooks.go b/webhooks.go index 651a190..b96c2be 100644 --- a/webhooks.go +++ b/webhooks.go @@ -1,8 +1,23 @@ package webhooks +import ( + "errors" + "fmt" + "net/http" +) + // Provider defines the type of webhook type Provider int +func (p Provider) String() string { + switch p { + case GitHub: + return "GitHub" + default: + return "Unknown" + } +} + // webhooks available providers const ( GitHub Provider = iota @@ -11,23 +26,91 @@ const ( // Webhook interface defines a webhook to recieve events type Webhook interface { Provider() Provider + ParsePayload(w http.ResponseWriter, r *http.Request) } -// Config interface defines the config to setup a webhook instance -type Config interface { - UnderlyingProvider() Provider +type server struct { + hook Webhook + path string } -// New creates and returns a WebHook instance denoted by the Provider type -// func New(config Config) Webhook { +// ProcessPayloadFunc is a common function for payload return values +type ProcessPayloadFunc func(payload interface{}) -// switch config.UnderlyingProvider() { -// case GitHub: -// c := config.(*GitHubConfig) -// return &GitHubWebhook{ -// provider: c.Provider, -// } -// default: -// panic("Invalid config type") -// } -// } +// Run runs a server +func Run(hook Webhook, addr string, path string) error { + srv := &server{ + hook: hook, + path: path, + } + + 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) +}