mirror of https://github.com/matrix-org/go-neb.git
Browse Source
Merge pull request #2 from matrix-org/kegan/webhooks
Merge pull request #2 from matrix-org/kegan/webhooks
Add a webhook handler and parse incoming Github webhook eventspull/4/head
Kegsay
8 years ago
committed by
GitHub
10 changed files with 1840 additions and 27 deletions
-
26src/github.com/matrix-org/go-neb/api.go
-
9src/github.com/matrix-org/go-neb/clients/clients.go
-
11src/github.com/matrix-org/go-neb/database/db.go
-
15src/github.com/matrix-org/go-neb/database/schema.go
-
1src/github.com/matrix-org/go-neb/goneb.go
-
8src/github.com/matrix-org/go-neb/services/echo/echo.go
-
10src/github.com/matrix-org/go-neb/services/github/github.go
-
259src/github.com/matrix-org/go-neb/services/github/webhook/webhook.go
-
1524src/github.com/matrix-org/go-neb/services/github/webhook/webhook_test.go
-
4src/github.com/matrix-org/go-neb/types/types.go
@ -0,0 +1,259 @@ |
|||||
|
package webhook |
||||
|
|
||||
|
import ( |
||||
|
"crypto/hmac" |
||||
|
"crypto/sha1" |
||||
|
"encoding/hex" |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
log "github.com/Sirupsen/logrus" |
||||
|
"github.com/google/go-github/github" |
||||
|
"html" |
||||
|
"io/ioutil" |
||||
|
"net/http" |
||||
|
"strings" |
||||
|
) |
||||
|
|
||||
|
// OnReceiveRequest processes incoming github webhook requests. The secretToken
|
||||
|
// parameter is optional.
|
||||
|
func OnReceiveRequest(w http.ResponseWriter, r *http.Request, secretToken string) { |
||||
|
// Verify the HMAC signature if NEB was configured with a secret token
|
||||
|
eventType := r.Header.Get("X-GitHub-Event") |
||||
|
signatureSHA1 := r.Header.Get("X-Hub-Signature") |
||||
|
content, err := ioutil.ReadAll(r.Body) |
||||
|
if err != nil { |
||||
|
log.WithError(err).Print("Failed to read Github webhook body") |
||||
|
w.WriteHeader(400) |
||||
|
return |
||||
|
} |
||||
|
// Verify request if a secret token has been supplied.
|
||||
|
if secretToken != "" { |
||||
|
sigHex := strings.Split(signatureSHA1, "=")[1] |
||||
|
var sigBytes []byte |
||||
|
sigBytes, err = hex.DecodeString(sigHex) |
||||
|
if err != nil { |
||||
|
log.WithError(err).WithField("X-Hub-Signature", sigHex).Print( |
||||
|
"Failed to decode signature as hex.") |
||||
|
w.WriteHeader(400) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if !checkMAC([]byte(content), sigBytes, []byte(secretToken)) { |
||||
|
log.WithFields(log.Fields{ |
||||
|
"X-Hub-Signature": signatureSHA1, |
||||
|
}).Print("Received Github event which failed MAC check.") |
||||
|
w.WriteHeader(403) |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
log.WithFields(log.Fields{ |
||||
|
"event_type": eventType, |
||||
|
"signature": signatureSHA1, |
||||
|
}).Print("Received Github event") |
||||
|
|
||||
|
htmlStr, repo, err := parseGithubEvent(eventType, content) |
||||
|
if err != nil { |
||||
|
log.WithError(err).Print("Failed to parse github event") |
||||
|
w.WriteHeader(500) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if err := handleWebhookEvent(eventType, htmlStr, repo); err != nil { |
||||
|
log.WithError(err).Print("Failed to handle Github webhook event") |
||||
|
w.WriteHeader(500) |
||||
|
return |
||||
|
} |
||||
|
w.WriteHeader(200) |
||||
|
} |
||||
|
|
||||
|
// checkMAC reports whether messageMAC is a valid HMAC tag for message.
|
||||
|
func checkMAC(message, messageMAC, key []byte) bool { |
||||
|
mac := hmac.New(sha1.New, key) |
||||
|
mac.Write(message) |
||||
|
expectedMAC := mac.Sum(nil) |
||||
|
return hmac.Equal(messageMAC, expectedMAC) |
||||
|
} |
||||
|
|
||||
|
// parseGithubEvent parses a github event type and JSON data and returns an explanatory
|
||||
|
// HTML string and the github repository this event affects, or an error.
|
||||
|
func parseGithubEvent(eventType string, data []byte) (string, *github.Repository, error) { |
||||
|
if eventType == "pull_request" { |
||||
|
var ev github.PullRequestEvent |
||||
|
if err := json.Unmarshal(data, &ev); err != nil { |
||||
|
return "", nil, err |
||||
|
} |
||||
|
return pullRequestHTMLMessage(ev), ev.Repo, nil |
||||
|
} else if eventType == "issues" { |
||||
|
var ev github.IssuesEvent |
||||
|
if err := json.Unmarshal(data, &ev); err != nil { |
||||
|
return "", nil, err |
||||
|
} |
||||
|
return issueHTMLMessage(ev), ev.Repo, nil |
||||
|
} else if eventType == "push" { |
||||
|
var ev github.PushEvent |
||||
|
if err := json.Unmarshal(data, &ev); err != nil { |
||||
|
return "", nil, err |
||||
|
} |
||||
|
|
||||
|
// The 'push' event repository format is subtly different from normal, so munge the bits we need.
|
||||
|
fullName := *ev.Repo.Owner.Name + "/" + *ev.Repo.Name |
||||
|
repo := github.Repository{ |
||||
|
Owner: &github.User{ |
||||
|
Login: ev.Repo.Owner.Name, |
||||
|
}, |
||||
|
Name: ev.Repo.Name, |
||||
|
FullName: &fullName, |
||||
|
} |
||||
|
return pushHTMLMessage(ev), &repo, nil |
||||
|
} else if eventType == "issue_comment" { |
||||
|
var ev github.IssueCommentEvent |
||||
|
if err := json.Unmarshal(data, &ev); err != nil { |
||||
|
return "", nil, err |
||||
|
} |
||||
|
return issueCommentHTMLMessage(ev), ev.Repo, nil |
||||
|
} else if eventType == "pull_request_review_comment" { |
||||
|
var ev github.PullRequestReviewCommentEvent |
||||
|
if err := json.Unmarshal(data, &ev); err != nil { |
||||
|
return "", nil, err |
||||
|
} |
||||
|
return prReviewCommentHTMLMessage(ev), ev.Repo, nil |
||||
|
} |
||||
|
return "", nil, fmt.Errorf("Unrecognized event type") |
||||
|
} |
||||
|
|
||||
|
func handleWebhookEvent(eventType string, htmlStr string, repo *github.Repository) error { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func pullRequestHTMLMessage(p github.PullRequestEvent) string { |
||||
|
var actionTarget string |
||||
|
if p.PullRequest.Assignee != nil && p.PullRequest.Assignee.Login != nil { |
||||
|
actionTarget = fmt.Sprintf(" to %s", *p.PullRequest.Assignee.Login) |
||||
|
} |
||||
|
return fmt.Sprintf( |
||||
|
"[<u>%s</u>] %s %s <b>pull request #%d</b>: %s [%s]%s - %s", |
||||
|
html.EscapeString(*p.Repo.FullName), |
||||
|
html.EscapeString(*p.Sender.Login), |
||||
|
html.EscapeString(*p.Action), |
||||
|
*p.Number, |
||||
|
html.EscapeString(*p.PullRequest.Title), |
||||
|
html.EscapeString(*p.PullRequest.State), |
||||
|
html.EscapeString(actionTarget), |
||||
|
html.EscapeString(*p.PullRequest.HTMLURL), |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
func issueHTMLMessage(p github.IssuesEvent) string { |
||||
|
var actionTarget string |
||||
|
if p.Issue.Assignee != nil && p.Issue.Assignee.Login != nil { |
||||
|
actionTarget = fmt.Sprintf(" to %s", *p.Issue.Assignee.Login) |
||||
|
} |
||||
|
return fmt.Sprintf( |
||||
|
"[<u>%s</u>] %s %s <b>issue #%d</b>: %s [%s]%s - %s", |
||||
|
html.EscapeString(*p.Repo.FullName), |
||||
|
html.EscapeString(*p.Sender.Login), |
||||
|
html.EscapeString(*p.Action), |
||||
|
*p.Issue.Number, |
||||
|
html.EscapeString(*p.Issue.Title), |
||||
|
html.EscapeString(*p.Issue.State), |
||||
|
html.EscapeString(actionTarget), |
||||
|
html.EscapeString(*p.Issue.HTMLURL), |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
func issueCommentHTMLMessage(p github.IssueCommentEvent) string { |
||||
|
var kind string |
||||
|
if p.Issue.PullRequestLinks == nil { |
||||
|
kind = "issue" |
||||
|
} else { |
||||
|
kind = "pull request" |
||||
|
} |
||||
|
|
||||
|
return fmt.Sprintf( |
||||
|
"[<u>%s</u>] %s commented on %s's <b>%s #%d</b>: %s - %s", |
||||
|
html.EscapeString(*p.Repo.FullName), |
||||
|
html.EscapeString(*p.Comment.User.Login), |
||||
|
html.EscapeString(*p.Issue.User.Login), |
||||
|
kind, |
||||
|
*p.Issue.Number, |
||||
|
html.EscapeString(*p.Issue.Title), |
||||
|
html.EscapeString(*p.Issue.HTMLURL), |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
func prReviewCommentHTMLMessage(p github.PullRequestReviewCommentEvent) string { |
||||
|
assignee := "None" |
||||
|
if p.PullRequest.Assignee != nil { |
||||
|
assignee = html.EscapeString(*p.PullRequest.Assignee.Login) |
||||
|
} |
||||
|
return fmt.Sprintf( |
||||
|
"[<u>%s</u>] %s made a line comment on %s's <b>pull request #%d</b> (assignee: %s): %s - %s", |
||||
|
html.EscapeString(*p.Repo.FullName), |
||||
|
html.EscapeString(*p.Sender.Login), |
||||
|
html.EscapeString(*p.PullRequest.User.Login), |
||||
|
*p.PullRequest.Number, |
||||
|
assignee, |
||||
|
html.EscapeString(*p.PullRequest.Title), |
||||
|
html.EscapeString(*p.Comment.HTMLURL), |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
func pushHTMLMessage(p github.PushEvent) string { |
||||
|
// /refs/heads/alice/branch-name => alice/branch-name
|
||||
|
branch := strings.Replace(*p.Ref, "refs/heads/", "", -1) |
||||
|
|
||||
|
// this branch was deleted, no HeadCommit object and deleted=true
|
||||
|
if p.HeadCommit == nil && p.Deleted != nil && *p.Deleted { |
||||
|
return fmt.Sprintf( |
||||
|
`[<u>%s</u>] %s <font color="red"><b>deleted</font> %s</b>`, |
||||
|
html.EscapeString(*p.Repo.FullName), |
||||
|
html.EscapeString(*p.Pusher.Name), |
||||
|
html.EscapeString(branch), |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
if p.Commits != nil && len(p.Commits) > 1 { |
||||
|
// multi-commit message
|
||||
|
// [<repo>] <username> pushed <num> commits to <branch>: <git.io link>
|
||||
|
// <up to 3 commits>
|
||||
|
var cList []string |
||||
|
for _, c := range p.Commits { |
||||
|
cList = append(cList, fmt.Sprintf( |
||||
|
`%s: %s`, |
||||
|
html.EscapeString(nameForAuthor(c.Author)), |
||||
|
html.EscapeString(*c.Message), |
||||
|
)) |
||||
|
} |
||||
|
return fmt.Sprintf( |
||||
|
`[<u>%s</u>] %s pushed %d commits to <b>%s</b>: %s<br>%s`, |
||||
|
html.EscapeString(*p.Repo.FullName), |
||||
|
html.EscapeString(nameForAuthor(p.HeadCommit.Committer)), |
||||
|
len(p.Commits), |
||||
|
html.EscapeString(branch), |
||||
|
html.EscapeString(*p.HeadCommit.URL), |
||||
|
strings.Join(cList, "<br>"), |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// single commit message
|
||||
|
// [<repo>] <username> pushed to <branch>: <msg> - <git.io link>
|
||||
|
return fmt.Sprintf( |
||||
|
`[<u>%s</u>] %s pushed to <b>%s</b>: %s - %s`, |
||||
|
html.EscapeString(*p.Repo.FullName), |
||||
|
html.EscapeString(nameForAuthor(p.HeadCommit.Committer)), |
||||
|
html.EscapeString(branch), |
||||
|
html.EscapeString(*p.HeadCommit.Message), |
||||
|
html.EscapeString(*p.HeadCommit.URL), |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
func nameForAuthor(a *github.CommitAuthor) string { |
||||
|
if a == nil { |
||||
|
return "" |
||||
|
} |
||||
|
if a.Login != nil { // prefer to use their GH username than the name they commited as
|
||||
|
return *a.Login |
||||
|
} |
||||
|
return *a.Name |
||||
|
} |
1524
src/github.com/matrix-org/go-neb/services/github/webhook/webhook_test.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue