Browse Source

Parse github webhook requests into suitable HTML messages

Verifies requests using an optional secret token. This commit doesn't
implement sending of these messages.
kegan/webhooks
Kegan Dougal 9 years ago
parent
commit
665d43f726
  1. 4
      src/github.com/matrix-org/go-neb/services/echo/echo.go
  2. 6
      src/github.com/matrix-org/go-neb/services/github/github.go
  3. 259
      src/github.com/matrix-org/go-neb/services/github/webhook/webhook.go
  4. 4
      src/github.com/matrix-org/go-neb/types/types.go

4
src/github.com/matrix-org/go-neb/services/echo/echo.go

@ -4,6 +4,7 @@ import (
"github.com/matrix-org/go-neb/matrix" "github.com/matrix-org/go-neb/matrix"
"github.com/matrix-org/go-neb/plugin" "github.com/matrix-org/go-neb/plugin"
"github.com/matrix-org/go-neb/types" "github.com/matrix-org/go-neb/types"
"net/http"
"strings" "strings"
) )
@ -29,6 +30,9 @@ func (e *echoService) Plugin(roomID string) plugin.Plugin {
}, },
} }
} }
func (e *echoService) OnReceiveWebhook(w http.ResponseWriter, req http.Request) {
w.WriteHeader(200) // Do nothing
}
func init() { func init() {
types.RegisterService(func(serviceID string) types.Service { types.RegisterService(func(serviceID string) types.Service {

6
src/github.com/matrix-org/go-neb/services/github/github.go

@ -6,8 +6,10 @@ import (
"github.com/google/go-github/github" "github.com/google/go-github/github"
"github.com/matrix-org/go-neb/matrix" "github.com/matrix-org/go-neb/matrix"
"github.com/matrix-org/go-neb/plugin" "github.com/matrix-org/go-neb/plugin"
"github.com/matrix-org/go-neb/services/github/webhook"
"github.com/matrix-org/go-neb/types" "github.com/matrix-org/go-neb/types"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"net/http"
"regexp" "regexp"
"strconv" "strconv"
) )
@ -60,6 +62,10 @@ func (s *githubService) Plugin(roomID string) plugin.Plugin {
}, },
} }
} }
func (s *githubService) OnReceiveWebhook(w http.ResponseWriter, req http.Request) {
// defer entirely to the webhook package
webhook.OnReceiveRequest(w, req, "")
}
// githubClient returns a github Client which can perform Github API operations. // githubClient returns a github Client which can perform Github API operations.
// If `token` is empty, a non-authenticated client will be created. This should be // If `token` is empty, a non-authenticated client will be created. This should be

259
src/github.com/matrix-org/go-neb/services/github/webhook/webhook.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
}

4
src/github.com/matrix-org/go-neb/types/types.go

@ -3,7 +3,7 @@ package types
import ( import (
"errors" "errors"
"github.com/matrix-org/go-neb/plugin" "github.com/matrix-org/go-neb/plugin"
// "net/http"
"net/http"
"net/url" "net/url"
) )
@ -32,7 +32,7 @@ type Service interface {
ServiceType() string ServiceType() string
RoomIDs() []string RoomIDs() []string
Plugin(roomID string) plugin.Plugin Plugin(roomID string) plugin.Plugin
// OnReceiveWebhook(req http.Request)
OnReceiveWebhook(w http.ResponseWriter, req http.Request)
} }
var servicesByType = map[string]func(string) Service{} var servicesByType = map[string]func(string) Service{}

Loading…
Cancel
Save