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/gitee/webhook.go

469 lines
14 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 gitee
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"time"
"git.awesome-for.me/liuzhiguo/go-scm/scm"
)
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-Gitee-Event") {
case "Push Hook":
hook, err = s.parsePushHook(data)
case "Merge Request Hook":
hook, err = s.parseMergeRequestHook(data)
case "Issue Hook":
hook, err = s.parseIssueHook(data)
case "Note Hook":
hook, err = s.parseNoteHook(data)
case "Tag Push Hook":
hook, err = s.parseTagPushHook(data)
default:
return nil, scm.ErrUnknownEvent
}
if err != nil {
return nil, err
}
key, err := fn(hook)
if err != nil {
return hook, err
} else if key == "" {
return hook, nil
}
agent := req.Header.Get("User-Agent")
if agent != "git-oschina-hook" {
return hook, &Error{
Message: "hook's user-agent is not git-oschina-hook",
}
}
timestamp := req.Header.Get("X-Gitee-Timestamp")
signature := req.Header.Get("X-Gitee-Token")
if !validateSignature(signature, key, timestamp) {
return hook, scm.ErrSignatureInvalid
}
return hook, nil
}
func (s *webhookService) parsePushHook(data []byte) (scm.Webhook, error) {
dst := new(pushOrTagPushHook)
err := json.Unmarshal(data, dst)
return convertPushHook(dst), err
}
func (s *webhookService) parseMergeRequestHook(data []byte) (scm.Webhook, error) {
dst := new(mergeRequestHook)
err := json.Unmarshal(data, dst)
return convertPullRequestHook(dst), err
}
func (s *webhookService) parseIssueHook(data []byte) (scm.Webhook, error) {
dst := new(issueHook)
err := json.Unmarshal(data, dst)
return convertIssueHook(dst), err
}
func (s *webhookService) parseTagPushHook(data []byte) (scm.Webhook, error) {
dst := new(pushOrTagPushHook)
err := json.Unmarshal(data, dst)
return convertTagPushHook(dst), err
}
func (s *webhookService) parseNoteHook(data []byte) (scm.Webhook, error) {
dst := new(noteHook)
err := json.Unmarshal(data, dst)
return convertNoteHook(dst), err
}
// validateSignature
// see https://gitee.com/help/articles/4290#article-header3
func validateSignature(signature, key, timestamp string) bool {
stringToSign := timestamp + "\n" + key
h := hmac.New(sha256.New, []byte(key))
h.Write([]byte(stringToSign))
computedSignature := base64.StdEncoding.EncodeToString(h.Sum(nil))
return computedSignature == signature
}
type (
pushOrTagPushHook struct {
Action string `json:"action"`
HookName string `json:"hook_name"`
Password string `json:"password"`
HookID int `json:"hook_id"`
HookURL string `json:"hook_url"`
Timestamp string `json:"timestamp"`
Sign string `json:"sign"`
Ref string `json:"ref"`
Before string `json:"before"`
After string `json:"after"`
Created bool `json:"created"`
Deleted bool `json:"deleted"`
Compare string `json:"compare"`
Commits []hookCommit `json:"commits"`
HeadCommit hookCommit `json:"head_commit"`
TotalCommitsCount int `json:"total_commits_count"`
CommitsMoreThanTen bool `json:"commits_more_than_ten"`
Repository hookRepository `json:"repository"`
Sender user `json:"sender"`
Enterprise enterprise `json:"enterprise"`
}
mergeRequestHook struct {
Action string `json:"action"`
Number int `json:"number"`
Title string `json:"title"`
Project hookRepository `json:"project"`
PullRequest pr `json:"pull_request"`
Iid int `json:"iid"`
ActionDesc string `json:"action_desc"`
Author user `json:"author"`
Body string `json:"body"`
Enterprise enterprise `json:"enterprise"`
Languages []string `json:"languages"`
MergeCommitSha string `json:"merge_commit_sha"`
MergeStatus string `json:"merge_status"`
Password string `json:"password"`
Repository hookRepository `json:"repository"`
Sender user `json:"sender"`
SourceBranch string `json:"source_branch"`
SourceRepo struct {
Project hookRepository `json:"project"`
Repository hookRepository `json:"repository"`
} `json:"source_repo"`
TargetRepo struct {
Project hookRepository `json:"project"`
Repository hookRepository `json:"repository"`
} `json:"target_repo"`
State string `json:"state"`
TargetBranch string `json:"target_branch"`
TargetUser user `json:"target_user"`
Timestamp string `json:"timestamp"`
UpdatedBy user `json:"updated_by"`
URL string `json:"url"`
}
noteHook struct {
Action string `json:"action"`
HookName string `json:"hook_name"`
Password string `json:"password"`
HookID int `json:"hook_id"`
HookURL string `json:"hook_url"`
Timestamp string `json:"timestamp"`
Sign string `json:"sign"`
Comment hookComment `json:"comment"`
NoteableType string `json:"noteable_type"`
Issue issue `json:"issue"`
PullRequest pr `json:"pull_request"`
Repository hookRepository `json:"repository"`
Sender user `json:"sender"`
Enterprise enterprise `json:"enterprise"`
}
issueHook struct {
Action string `json:"action"`
HookName string `json:"hook_name"`
Password string `json:"password"`
HookID int `json:"hook_id"`
HookURL string `json:"hook_url"`
Timestamp string `json:"timestamp"`
Sign string `json:"sign"`
Issue issue `json:"issue"`
Repository hookRepository `json:"repository"`
Sender user `json:"sender"`
Enterprise enterprise `json:"enterprise"`
}
hookAuthorOrCommitter struct {
Time time.Time `json:"time"`
Name string `json:"name"`
Email string `json:"email"`
Username string `json:"username"`
UserName string `json:"user_name"`
URL string `json:"url"`
}
hookCommit struct {
ID string `json:"id"`
TreeID string `json:"tree_id"`
Distinct bool `json:"distinct"`
Message string `json:"message"`
Timestamp time.Time `json:"timestamp"`
URL string `json:"url"`
Author hookAuthorOrCommitter `json:"author"`
Committer hookAuthorOrCommitter `json:"committer"`
Added interface{} `json:"added"`
Removed interface{} `json:"removed"`
Modified []string `json:"modified"`
}
hookComment struct {
HtmlURL string `json:"html_url"`
ID int `json:"id"`
Body string `json:"body"`
User user `json:"user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
hookRepository struct {
CloneURL string `json:"clone_url"`
CreatedAt time.Time `json:"created_at"`
DefaultBranch string `json:"default_branch"`
Description string `json:"description"`
Fork bool `json:"fork"`
ForksCount int `json:"forks_count"`
FullName string `json:"full_name"`
GitHttpURL string `json:"git_http_url"`
GitSshURL string `json:"git_ssh_url"`
GitSvnURL string `json:"git_svn_url"`
GitURL string `json:"git_url"`
HasIssues bool `json:"has_issues"`
HasPages bool `json:"has_pages"`
HasWiki bool `json:"has_wiki"`
Homepage string `json:"homepage"`
HtmlURL string `json:"html_url"`
ID int `json:"id"`
Language interface{} `json:"language"`
License interface{} `json:"license"`
Name string `json:"name"`
NameWithNamespace string `json:"name_with_namespace"`
Namespace string `json:"namespace"`
OpenIssuesCount int `json:"open_issues_count"`
Owner user `json:"owner"`
Path string `json:"path"`
PathWithNamespace string `json:"path_with_namespace"`
Private bool `json:"private"`
PushedAt time.Time `json:"pushed_at"`
SSHURL string `json:"ssh_url"`
StargazersCount int `json:"stargazers_count"`
SvnURL string `json:"svn_url"`
UpdatedAt time.Time `json:"updated_at"`
URL string `json:"url"`
WatchersCount int `json:"watchers_count"`
}
enterprise struct {
Name string `json:"name"`
URL string `json:"url"`
}
)
func convertPushHook(src *pushOrTagPushHook) *scm.PushHook {
var commits []scm.Commit
if &src.Commits != nil {
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,
},
Committer: scm.Signature{
Login: c.Committer.Username,
Email: c.Committer.Email,
Name: c.Committer.Name,
Date: c.Timestamp,
},
})
}
}
dst := &scm.PushHook{
Ref: src.Ref,
Repo: *convertHookRepository(&src.Repository),
Before: src.Before,
After: src.After,
Commit: scm.Commit{
Sha: src.After,
Link: src.Compare,
},
Sender: *convertUser(&src.Sender),
Commits: commits,
}
if &src.HeadCommit != nil {
dst.Commit.Message = src.HeadCommit.Message
dst.Commit.Author = scm.Signature{
Login: src.HeadCommit.Author.Username,
Email: src.HeadCommit.Author.Email,
Name: src.HeadCommit.Author.Name,
Date: src.HeadCommit.Timestamp,
}
dst.Commit.Committer = scm.Signature{
Login: src.HeadCommit.Committer.Username,
Email: src.HeadCommit.Committer.Email,
Name: src.HeadCommit.Committer.Name,
Date: src.HeadCommit.Timestamp,
}
}
return dst
}
func convertPullRequestHook(src *mergeRequestHook) *scm.PullRequestHook {
dst := &scm.PullRequestHook{
Repo: *convertHookRepository(&src.Repository),
PullRequest: *convertPullRequest(&src.PullRequest),
Sender: *convertUser(&src.Sender),
}
switch src.Action {
case "update":
if src.ActionDesc == "update_label" {
if len(src.PullRequest.Labels) == 0 {
dst.Action = scm.ActionUnlabel
} else {
dst.Action = scm.ActionLabel
}
} else if src.ActionDesc == "source_branch_changed" {
// Gitee does not provide a synchronize action.
// But when action_desc is 'source_branch_changed',
// what happens is the same as GitHub's synchronize
dst.Action = scm.ActionSync
}
case "open":
dst.Action = scm.ActionOpen
case "close":
dst.Action = scm.ActionClose
case "merge":
dst.Action = scm.ActionMerge
case "test", "tested", "assign", "approved":
dst.Action = scm.ActionUnknown
default:
dst.Action = scm.ActionUnknown
}
return dst
}
func convertIssueHook(src *issueHook) *scm.IssueHook {
dst := &scm.IssueHook{
Repo: *convertHookRepository(&src.Repository),
Issue: *convertIssue(&src.Issue),
Sender: *convertUser(&src.Sender),
}
switch src.Action {
case "open":
dst.Action = scm.ActionOpen
case "delete":
dst.Action = scm.ActionClose
case "state_change":
switch src.Issue.State {
case "open", "progressing":
dst.Action = scm.ActionOpen
case "close", "rejected":
dst.Action = scm.ActionClose
}
case "assign":
dst.Action = scm.ActionUpdate
default:
dst.Action = scm.ActionUnknown
}
return dst
}
func convertTagPushHook(src *pushOrTagPushHook) scm.Webhook {
dst := &scm.TagHook{
Ref: scm.Reference{
Name: scm.TrimRef(src.Ref),
Sha: src.HeadCommit.ID,
},
Repo: *convertHookRepository(&src.Repository),
Sender: *convertUser(&src.Sender),
}
if src.Created {
dst.Action = scm.ActionCreate
} else if src.Deleted {
dst.Action = scm.ActionDelete
dst.Ref.Sha = ""
} else {
dst.Action = scm.ActionUnknown
}
return dst
}
func convertNoteHook(src *noteHook) scm.Webhook {
convertHookComment := func(comment *hookComment) *scm.Comment {
return &scm.Comment{
ID: comment.ID,
Body: comment.Body,
Author: *convertUser(&comment.User),
Created: comment.CreatedAt,
Updated: comment.UpdatedAt,
}
}
convertCommentAction := func(src string) (action scm.Action) {
switch src {
case "comment":
return scm.ActionCreate
case "edited":
return scm.ActionEdit
case "deleted":
return scm.ActionDelete
default:
return scm.ActionUnknown
}
}
if src.NoteableType == "Issue" {
return &scm.IssueCommentHook{
Action: convertCommentAction(src.Action),
Repo: *convertHookRepository(&src.Repository),
Issue: *convertIssue(&src.Issue),
Comment: *convertHookComment(&src.Comment),
Sender: *convertUser(&src.Sender),
}
}
if src.NoteableType == "PullRequest" {
// not support review comment
return &scm.PullRequestCommentHook{
Action: convertCommentAction(src.Action),
Repo: *convertHookRepository(&src.Repository),
PullRequest: *convertPullRequest(&src.PullRequest),
Comment: *convertHookComment(&src.Comment),
Sender: *convertUser(&src.Sender),
}
}
return nil
}
func convertHookRepository(from *hookRepository) *scm.Repository {
return &scm.Repository{
ID: fmt.Sprint(from.ID),
Namespace: from.Namespace,
Name: from.Name,
Branch: from.DefaultBranch,
Private: from.Private,
Clone: from.CloneURL,
CloneSSH: from.GitSshURL,
Link: from.HtmlURL,
Created: from.CreatedAt,
Updated: from.UpdatedAt,
}
}