You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
go-scm/scm/driver/github/webhook.go

587 lines
16 KiB
Go

// Copyright 2017 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package github
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"regexp"
"time"
"git.awesome-for.me/liuzhiguo/go-scm/scm"
"git.awesome-for.me/liuzhiguo/go-scm/scm/driver/internal/hmac"
"git.awesome-for.me/liuzhiguo/go-scm/scm/driver/internal/null"
)
type webhookService struct {
client *wrapper
}
func (s *webhookService) Parse(req *http.Request, fn scm.SecretFunc) (scm.Webhook, error) {
data, err := ioutil.ReadAll(
io.LimitReader(req.Body, 10000000),
)
if err != nil {
return nil, err
}
var hook scm.Webhook
switch req.Header.Get("X-GitHub-Event") {
case "push":
hook, err = s.parsePushHook(data)
case "create":
hook, err = s.parseCreateHook(data)
case "delete":
hook, err = s.parseDeleteHook(data)
case "pull_request":
hook, err = s.parsePullRequestHook(data)
case "deployment":
hook, err = s.parseDeploymentHook(data)
// case "pull_request_review_comment":
// case "issues":
case "issue_comment":
hook, err = s.parseIssueCommentHook(data)
case "release":
hook, err = s.parseReleaseHook(data)
default:
return nil, scm.ErrUnknownEvent
}
if err != nil {
return nil, err
}
// get the gogs signature key to verify the payload
// signature. If no key is provided, no validation
// is performed.
key, err := fn(hook)
if err != nil {
return hook, err
} else if key == "" {
return hook, nil
}
sig := req.Header.Get("X-Hub-Signature-256")
if sig == "" {
sig = req.Header.Get("X-Hub-Signature")
}
if !hmac.ValidatePrefix(data, []byte(key), sig) {
return hook, scm.ErrSignatureInvalid
}
return hook, nil
}
func (s *webhookService) parsePushHook(data []byte) (scm.Webhook, error) {
dst := new(pushHook)
err := json.Unmarshal(data, dst)
return convertPushHook(dst), err
}
func (s *webhookService) parseCreateHook(data []byte) (scm.Webhook, error) {
src := new(createDeleteHook)
err := json.Unmarshal(data, src)
if err != nil {
return nil, err
}
if src.RefType == "branch" {
dst := convertBranchHook(src)
dst.Action = scm.ActionCreate
return dst, nil
}
dst := convertTagHook(src)
dst.Action = scm.ActionCreate
return dst, nil
}
func (s *webhookService) parseDeleteHook(data []byte) (scm.Webhook, error) {
src := new(createDeleteHook)
err := json.Unmarshal(data, src)
if err != nil {
return nil, err
}
if src.RefType == "branch" {
dst := convertBranchHook(src)
dst.Action = scm.ActionDelete
return dst, nil
}
dst := convertTagHook(src)
dst.Action = scm.ActionDelete
return dst, nil
}
func (s *webhookService) parseDeploymentHook(data []byte) (scm.Webhook, error) {
src := new(deploymentHook)
err := json.Unmarshal(data, src)
if err != nil {
return nil, err
}
dst := convertDeploymentHook(src)
return dst, nil
}
func (s *webhookService) parseIssueCommentHook(data []byte) (scm.Webhook, error) {
src := new(issueCommentHook)
err := json.Unmarshal(data, src)
if err != nil {
return nil, err
}
dst := convertIssueCommentHook(src)
switch src.Action {
case "created":
dst.Action = scm.ActionCreate
case "edited":
dst.Action = scm.ActionEdit
case "deleted":
dst.Action = scm.ActionDelete
default:
dst.Action = scm.ActionUnknown
}
return dst, nil
}
func (s *webhookService) parsePullRequestHook(data []byte) (scm.Webhook, error) {
src := new(pullRequestHook)
err := json.Unmarshal(data, src)
if err != nil {
return nil, err
}
dst := convertPullRequestHook(src)
switch src.Action {
case "labeled":
dst.Action = scm.ActionLabel
case "unlabeled":
dst.Action = scm.ActionUnlabel
case "opened":
dst.Action = scm.ActionOpen
case "edited":
dst.Action = scm.ActionUpdate
case "closed":
// TODO(bradrydzewski) github does not provide a merged action,
// but this is provided by gitlab and bitbucket. Is it possible
// to emulate the merge action?
// if merged == true
// dst.Action = scm.ActionMerge
dst.Action = scm.ActionClose
case "reopened":
dst.Action = scm.ActionReopen
case "synchronize":
dst.Action = scm.ActionSync
case "ready_for_review":
dst.Action = scm.ActionReviewReady
case "assigned", "unassigned", "review_requested", "review_request_removed", "locked", "unlocked":
dst.Action = scm.ActionUnknown
default:
dst.Action = scm.ActionUnknown
}
return dst, nil
}
func (s *webhookService) parseReleaseHook(data []byte) (scm.Webhook, error) {
src := new(releaseHook)
err := json.Unmarshal(data, src)
if err != nil {
return nil, err
}
dst := convertReleaseHook(src)
switch src.Action {
case "created":
dst.Action = scm.ActionCreate
case "edited":
dst.Action = scm.ActionEdit
case "deleted":
dst.Action = scm.ActionDelete
case "published":
dst.Action = scm.ActionPublish
case "unpublished":
dst.Action = scm.ActionUnpublish
case "prereleased":
dst.Action = scm.ActionPrerelease
case "released":
dst.Action = scm.ActionRelease
default:
dst.Action = scm.ActionUnknown
}
return dst, nil
}
//
// native data structures
//
type (
// github create webhook payload
createDeleteHook struct {
Ref string `json:"ref"`
RefType string `json:"ref_type"`
Repository repository `json:"repository"`
Sender user `json:"sender"`
}
// github push webhook payload
pushHook struct {
Ref string `json:"ref"`
BaseRef string `json:"base_ref"`
Before string `json:"before"`
After string `json:"after"`
Compare string `json:"compare"`
Head struct {
ID string `json:"id"`
TreeID string `json:"tree_id"`
Distinct bool `json:"distinct"`
Message string `json:"message"`
Timestamp null.Time `json:"timestamp"`
URL string `json:"url"`
Author struct {
Name string `json:"name"`
Email string `json:"email"`
Username string `json:"username"`
} `json:"author"`
Committer struct {
Name string `json:"name"`
Email string `json:"email"`
Username string `json:"username"`
} `json:"committer"`
Added []interface{} `json:"added"`
Removed []interface{} `json:"removed"`
Modified []string `json:"modified"`
} `json:"head_commit"`
Commits []struct {
ID string `json:"id"`
TreeID string `json:"tree_id"`
Distinct bool `json:"distinct"`
Message string `json:"message"`
Timestamp null.Time `json:"timestamp"`
URL string `json:"url"`
Author struct {
Name string `json:"name"`
Email string `json:"email"`
Username string `json:"username"`
} `json:"author"`
Committer struct {
Name string `json:"name"`
Email string `json:"email"`
Username string `json:"username"`
} `json:"committer"`
Added []interface{} `json:"added"`
Removed []interface{} `json:"removed"`
Modified []string `json:"modified"`
} `json:"commits"`
Repository struct {
ID int64 `json:"id"`
Owner struct {
Login string `json:"login"`
AvatarURL string `json:"avatar_url"`
} `json:"owner"`
Name string `json:"name"`
FullName string `json:"full_name"`
Private bool `json:"private"`
Visibility string `json:"visibility"`
Fork bool `json:"fork"`
HTMLURL string `json:"html_url"`
SSHURL string `json:"ssh_url"`
CloneURL string `json:"clone_url"`
DefaultBranch string `json:"default_branch"`
} `json:"repository"`
Pusher user `json:"pusher"`
Sender user `json:"sender"`
}
pullRequestHook struct {
Action string `json:"action"`
Number int `json:"number"`
PullRequest pr `json:"pull_request"`
Repository repository `json:"repository"`
Sender user `json:"sender"`
}
// github deployment webhook payload
deploymentHook struct {
Deployment struct {
ID int64 `json:"id"`
Creator user `json:"creator"`
Description null.String `json:"description"`
Environment null.String `json:"environment"`
EnvironmentURL null.String `json:"environment_url"`
URL null.String `json:"url"`
Sha null.String `json:"sha"`
Ref null.String `json:"ref"`
Task null.String `json:"task"`
Payload interface{} `json:"payload"`
} `json:"deployment"`
Repository repository `json:"repository"`
Sender user `json:"sender"`
}
// github issue_comment webhook payload
issueCommentHook struct {
Action string `json:"action"`
Issue issue `json:"issue"`
Repository repository `json:"repository"`
Sender user `json:"sender"`
Organization user `json:"organization"`
Comment struct {
ID int `json:"id"`
Body string `json:"body"`
User user `json:"user"`
Created time.Time `json:"created_at"`
Updated time.Time `json:"updated_at"`
} `json:"comment"`
}
// github release webhook payload
releaseHook struct {
Action string `json:"action"`
Release struct {
ID int `json:"id"`
Title string `json:"name"`
Description string `json:"body"`
Link string `json:"html_url,omitempty"`
Tag string `json:"tag_name,omitempty"`
Commitish string `json:"target_commitish,omitempty"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
Created time.Time `json:"created_at"`
Published time.Time `json:"published_at"`
} `json:"release"`
Repository repository `json:"repository"`
Sender user `json:"sender"`
}
)
//
// native data structure conversion
//
func convertPushHook(src *pushHook) *scm.PushHook {
var commits []scm.Commit
for _, c := range src.Commits {
commits = append(commits,
scm.Commit{
Sha: c.ID,
Message: c.Message,
Link: c.URL,
Author: scm.Signature{
Login: c.Author.Username,
Email: c.Author.Email,
Name: c.Author.Name,
Date: c.Timestamp.ValueOrZero(),
},
Committer: scm.Signature{
Login: c.Committer.Username,
Email: c.Committer.Email,
Name: c.Committer.Name,
Date: c.Timestamp.ValueOrZero(),
},
})
}
dst := &scm.PushHook{
Ref: src.Ref,
BaseRef: src.BaseRef,
Before: src.Before,
After: src.After,
Commit: scm.Commit{
Sha: src.After,
Message: src.Head.Message,
Link: src.Compare,
Author: scm.Signature{
Login: src.Head.Author.Username,
Email: src.Head.Author.Email,
Name: src.Head.Author.Name,
Date: src.Head.Timestamp.ValueOrZero(),
},
Committer: scm.Signature{
Login: src.Head.Committer.Username,
Email: src.Head.Committer.Email,
Name: src.Head.Committer.Name,
Date: src.Head.Timestamp.ValueOrZero(),
},
},
Repo: scm.Repository{
ID: fmt.Sprint(src.Repository.ID),
Namespace: src.Repository.Owner.Login,
Name: src.Repository.Name,
Branch: src.Repository.DefaultBranch,
Private: src.Repository.Private,
Visibility: convertVisibility(src.Repository.Visibility),
Clone: src.Repository.CloneURL,
CloneSSH: src.Repository.SSHURL,
Link: src.Repository.HTMLURL,
},
Sender: *convertUser(&src.Sender),
Commits: commits,
}
// fix https://git.awesome-for.me/liuzhiguo/go-scm/issues/8
if scm.IsTag(dst.Ref) && src.Head.ID != "" {
dst.Commit.Sha = src.Head.ID
dst.After = src.Head.ID
}
return dst
}
func convertBranchHook(src *createDeleteHook) *scm.BranchHook {
return &scm.BranchHook{
Ref: scm.Reference{
Name: src.Ref,
},
Repo: scm.Repository{
ID: fmt.Sprint(src.Repository.ID),
Namespace: src.Repository.Owner.Login,
Name: src.Repository.Name,
Branch: src.Repository.DefaultBranch,
Private: src.Repository.Private,
Visibility: convertVisibility(src.Repository.Visibility),
Clone: src.Repository.CloneURL,
CloneSSH: src.Repository.SSHURL,
Link: src.Repository.HTMLURL,
},
Sender: *convertUser(&src.Sender),
}
}
func convertTagHook(src *createDeleteHook) *scm.TagHook {
return &scm.TagHook{
Ref: scm.Reference{
Name: src.Ref,
},
Repo: scm.Repository{
ID: fmt.Sprint(src.Repository.ID),
Namespace: src.Repository.Owner.Login,
Name: src.Repository.Name,
Branch: src.Repository.DefaultBranch,
Private: src.Repository.Private,
Visibility: convertVisibility(src.Repository.Visibility),
Clone: src.Repository.CloneURL,
CloneSSH: src.Repository.SSHURL,
Link: src.Repository.HTMLURL,
},
Sender: *convertUser(&src.Sender),
}
}
func convertPullRequestHook(src *pullRequestHook) *scm.PullRequestHook {
return &scm.PullRequestHook{
// Action Action
Repo: scm.Repository{
ID: fmt.Sprint(src.Repository.ID),
Namespace: src.Repository.Owner.Login,
Name: src.Repository.Name,
Branch: src.Repository.DefaultBranch,
Private: src.Repository.Private,
Visibility: convertVisibility(src.Repository.Visibility),
Clone: src.Repository.CloneURL,
CloneSSH: src.Repository.SSHURL,
Link: src.Repository.HTMLURL,
},
PullRequest: *convertPullRequest(&src.PullRequest),
Sender: *convertUser(&src.Sender),
}
}
func convertDeploymentHook(src *deploymentHook) *scm.DeployHook {
dst := &scm.DeployHook{
Number: src.Deployment.ID,
Data: src.Deployment.Payload,
Desc: src.Deployment.Description.String,
Ref: scm.Reference{
Name: src.Deployment.Ref.String,
Path: src.Deployment.Ref.String,
Sha: src.Deployment.Sha.String,
},
Repo: scm.Repository{
ID: fmt.Sprint(src.Repository.ID),
Namespace: src.Repository.Owner.Login,
Name: src.Repository.Name,
Branch: src.Repository.DefaultBranch,
Private: src.Repository.Private,
Visibility: convertVisibility(src.Repository.Visibility),
Clone: src.Repository.CloneURL,
CloneSSH: src.Repository.SSHURL,
Link: src.Repository.HTMLURL,
},
Sender: *convertUser(&src.Sender),
Task: src.Deployment.Task.String,
Target: src.Deployment.Environment.String,
TargetURL: src.Deployment.EnvironmentURL.String,
}
// handle deployment events for commits which
// use a different payload structure and lack
// branch or reference details.
if len(dst.Ref.Name) == 40 && dst.Ref.Name == dst.Ref.Sha {
dst.Ref.Name = ""
dst.Ref.Path = ""
return dst
}
if tagRE.MatchString(dst.Ref.Name) {
dst.Ref.Path = scm.ExpandRef(dst.Ref.Path, "refs/tags/")
} else {
dst.Ref.Path = scm.ExpandRef(dst.Ref.Path, "refs/heads/")
}
return dst
}
func convertIssueCommentHook(src *issueCommentHook) *scm.IssueCommentHook {
dst := &scm.IssueCommentHook{
Repo: scm.Repository{
ID: fmt.Sprint(src.Repository.ID),
Namespace: src.Repository.Owner.Login,
Name: src.Repository.Name,
Branch: src.Repository.DefaultBranch,
Private: src.Repository.Private,
Visibility: convertVisibility(src.Repository.Visibility),
Clone: src.Repository.CloneURL,
CloneSSH: src.Repository.SSHURL,
Link: src.Repository.HTMLURL,
},
Issue: *convertIssue(&src.Issue),
Comment: scm.Comment{
ID: src.Comment.ID,
Body: src.Comment.Body,
Author: *convertUser(&src.Comment.User),
Created: src.Comment.Created,
Updated: src.Comment.Updated,
},
Sender: *convertUser(&src.Sender),
}
return dst
}
func convertReleaseHook(src *releaseHook) *scm.ReleaseHook {
dst := &scm.ReleaseHook{
Release: scm.Release{
ID: src.Release.ID,
Title: src.Release.Title,
Description: src.Release.Description,
Link: src.Release.Link,
Tag: src.Release.Tag,
Commitish: src.Release.Commitish,
Draft: src.Release.Draft,
Prerelease: src.Release.Prerelease,
Created: src.Release.Created,
Published: src.Release.Published,
},
Repo: scm.Repository{
ID: fmt.Sprint(src.Repository.ID),
Namespace: src.Repository.Owner.Login,
Name: src.Repository.Name,
Branch: src.Repository.DefaultBranch,
Private: src.Repository.Private,
Visibility: convertVisibility(src.Repository.Visibility),
Clone: src.Repository.CloneURL,
CloneSSH: src.Repository.SSHURL,
Link: src.Repository.HTMLURL,
},
Sender: *convertUser(&src.Sender),
}
return dst
}
// regexp help determine if the named git object is a tag.
// this is not meant to be 100% accurate.
var tagRE = regexp.MustCompile("^v?(\\d+).(.+)")