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.
469 lines
14 KiB
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,
|
|
}
|
|
}
|