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