2014-05-05 20:52:25 -04:00
// Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
2015-02-10 21:06:59 -05:00
"crypto/tls"
2014-05-05 20:52:25 -04:00
"encoding/json"
2014-05-05 21:36:08 -04:00
"errors"
2014-08-24 08:59:47 -04:00
"io/ioutil"
2015-07-25 21:32:04 +08:00
"sync"
2014-06-08 04:45:34 -04:00
"time"
2014-05-05 20:52:25 -04:00
2014-06-08 04:45:34 -04:00
"github.com/gogits/gogs/modules/httplib"
2014-05-05 20:52:25 -04:00
"github.com/gogits/gogs/modules/log"
2014-06-08 04:45:34 -04:00
"github.com/gogits/gogs/modules/setting"
2014-08-09 15:40:10 -07:00
"github.com/gogits/gogs/modules/uuid"
2014-05-05 20:52:25 -04:00
)
2014-05-05 21:36:08 -04:00
var (
ErrWebhookNotExist = errors . New ( "Webhook does not exist" )
)
2014-06-08 04:45:34 -04:00
type HookContentType int
2014-05-05 20:52:25 -04:00
const (
2014-06-08 04:45:34 -04:00
JSON HookContentType = iota + 1
FORM
2014-05-05 20:52:25 -04:00
)
2014-11-13 12:57:00 -05:00
var hookContentTypes = map [ string ] HookContentType {
"json" : JSON ,
"form" : FORM ,
}
// ToHookContentType returns HookContentType by given name.
func ToHookContentType ( name string ) HookContentType {
return hookContentTypes [ name ]
}
2014-11-13 02:32:18 -05:00
func ( t HookContentType ) Name ( ) string {
switch t {
case JSON :
return "json"
case FORM :
return "form"
}
return ""
}
2014-11-13 12:57:00 -05:00
// IsValidHookContentType returns true if given name is a valid hook content type.
func IsValidHookContentType ( name string ) bool {
_ , ok := hookContentTypes [ name ]
return ok
}
2014-06-08 04:45:34 -04:00
// HookEvent represents events that will delivery hook.
2014-05-05 20:52:25 -04:00
type HookEvent struct {
PushOnly bool ` json:"push_only" `
}
2014-06-08 04:45:34 -04:00
// Webhook represents a web hook object.
2014-05-05 20:52:25 -04:00
type Webhook struct {
2014-08-24 08:59:47 -04:00
Id int64
RepoId int64
Url string ` xorm:"TEXT" `
ContentType HookContentType
Secret string ` xorm:"TEXT" `
Events string ` xorm:"TEXT" `
* HookEvent ` xorm:"-" `
IsSsl bool
IsActive bool
HookTaskType HookTaskType
Meta string ` xorm:"TEXT" ` // store hook-specific attributes
2014-09-04 07:17:00 -04:00
OrgId int64
2014-11-14 17:11:30 -05:00
Created time . Time ` xorm:"CREATED" `
Updated time . Time ` xorm:"UPDATED" `
2014-05-05 20:52:25 -04:00
}
2014-06-08 04:54:52 -04:00
// GetEvent handles conversion from Events to HookEvent.
2014-05-05 21:36:08 -04:00
func ( w * Webhook ) GetEvent ( ) {
w . HookEvent = & HookEvent { }
if err := json . Unmarshal ( [ ] byte ( w . Events ) , w . HookEvent ) ; err != nil {
2014-07-26 00:24:27 -04:00
log . Error ( 4 , "webhook.GetEvent(%d): %v" , w . Id , err )
2014-05-05 20:52:25 -04:00
}
}
2014-08-24 08:59:47 -04:00
func ( w * Webhook ) GetSlackHook ( ) * Slack {
s := & Slack { }
if err := json . Unmarshal ( [ ] byte ( w . Meta ) , s ) ; err != nil {
log . Error ( 4 , "webhook.GetSlackHook(%d): %v" , w . Id , err )
}
return s
}
2014-06-08 04:54:52 -04:00
// UpdateEvent handles conversion from HookEvent to Events.
2014-06-08 04:45:34 -04:00
func ( w * Webhook ) UpdateEvent ( ) error {
2014-05-05 21:36:08 -04:00
data , err := json . Marshal ( w . HookEvent )
2014-05-05 20:52:25 -04:00
w . Events = string ( data )
return err
}
2014-12-06 20:22:48 -05:00
// HasPushEvent returns true if hook enabled push event.
2014-05-06 11:50:31 -04:00
func ( w * Webhook ) HasPushEvent ( ) bool {
if w . PushOnly {
return true
}
return false
}
2014-06-08 04:45:34 -04:00
// CreateWebhook creates a new web hook.
2014-05-05 20:52:25 -04:00
func CreateWebhook ( w * Webhook ) error {
2014-06-21 00:51:41 -04:00
_ , err := x . Insert ( w )
2014-05-05 20:52:25 -04:00
return err
}
2014-05-05 21:36:08 -04:00
// GetWebhookById returns webhook by given ID.
func GetWebhookById ( hookId int64 ) ( * Webhook , error ) {
w := & Webhook { Id : hookId }
2014-06-21 00:51:41 -04:00
has , err := x . Get ( w )
2014-05-05 21:36:08 -04:00
if err != nil {
return nil , err
} else if ! has {
return nil , ErrWebhookNotExist
}
return w , nil
}
2014-05-06 11:50:31 -04:00
// GetActiveWebhooksByRepoId returns all active webhooks of repository.
func GetActiveWebhooksByRepoId ( repoId int64 ) ( ws [ ] * Webhook , err error ) {
2014-09-09 10:17:35 -04:00
err = x . Where ( "repo_id=?" , repoId ) . And ( "is_active=?" , true ) . Find ( & ws )
2014-05-06 11:50:31 -04:00
return ws , err
}
2014-05-05 20:52:25 -04:00
// GetWebhooksByRepoId returns all webhooks of repository.
func GetWebhooksByRepoId ( repoId int64 ) ( ws [ ] * Webhook , err error ) {
2014-06-21 00:51:41 -04:00
err = x . Find ( & ws , & Webhook { RepoId : repoId } )
2014-05-05 20:52:25 -04:00
return ws , err
}
2014-05-05 21:36:08 -04:00
2014-06-08 04:45:34 -04:00
// UpdateWebhook updates information of webhook.
func UpdateWebhook ( w * Webhook ) error {
2014-08-31 15:11:55 +08:00
_ , err := x . Id ( w . Id ) . AllCols ( ) . Update ( w )
2014-06-08 04:45:34 -04:00
return err
}
2014-05-05 21:36:08 -04:00
// DeleteWebhook deletes webhook of repository.
func DeleteWebhook ( hookId int64 ) error {
2014-06-21 00:51:41 -04:00
_ , err := x . Delete ( & Webhook { Id : hookId } )
2014-05-05 21:36:08 -04:00
return err
}
2014-06-08 04:45:34 -04:00
2014-09-04 07:17:00 -04:00
// GetWebhooksByOrgId returns all webhooks for an organization.
func GetWebhooksByOrgId ( orgId int64 ) ( ws [ ] * Webhook , err error ) {
err = x . Find ( & ws , & Webhook { OrgId : orgId } )
return ws , err
}
// GetActiveWebhooksByOrgId returns all active webhooks for an organization.
func GetActiveWebhooksByOrgId ( orgId int64 ) ( ws [ ] * Webhook , err error ) {
2014-09-09 10:17:35 -04:00
err = x . Where ( "org_id=?" , orgId ) . And ( "is_active=?" , true ) . Find ( & ws )
2014-09-04 07:17:00 -04:00
return ws , err
}
2014-06-08 04:45:34 -04:00
// ___ ___ __ ___________ __
// / | \ ____ ____ | | _\__ ___/____ _____| | __
// / ~ \/ _ \ / _ \| |/ / | | \__ \ / ___/ |/ /
// \ Y ( <_> | <_> ) < | | / __ \_\___ \| <
// \___|_ / \____/ \____/|__|_ \ |____| (____ /____ >__|_ \
// \/ \/ \/ \/ \/
type HookTaskType int
const (
2014-08-24 08:59:47 -04:00
GOGS HookTaskType = iota + 1
SLACK
2014-06-08 04:45:34 -04:00
)
2014-11-13 12:57:00 -05:00
var hookTaskTypes = map [ string ] HookTaskType {
"gogs" : GOGS ,
"slack" : SLACK ,
}
// ToHookTaskType returns HookTaskType by given name.
func ToHookTaskType ( name string ) HookTaskType {
return hookTaskTypes [ name ]
}
2014-11-13 02:32:18 -05:00
func ( t HookTaskType ) Name ( ) string {
switch t {
case GOGS :
return "gogs"
case SLACK :
return "slack"
}
return ""
}
2014-11-13 12:57:00 -05:00
// IsValidHookTaskType returns true if given name is a valid hook task type.
func IsValidHookTaskType ( name string ) bool {
_ , ok := hookTaskTypes [ name ]
return ok
}
2014-08-09 15:40:10 -07:00
type HookEventType string
const (
2015-03-18 04:51:02 -04:00
HOOK_EVENT_PUSH HookEventType = "push"
2014-08-09 15:40:10 -07:00
)
2014-11-16 20:27:04 -05:00
// FIXME: just use go-gogs-client structs maybe?
2014-06-08 04:45:34 -04:00
type PayloadAuthor struct {
2014-09-17 09:11:51 -04:00
Name string ` json:"name" `
Email string ` json:"email" `
UserName string ` json:"username" `
2014-06-08 04:45:34 -04:00
}
type PayloadCommit struct {
Id string ` json:"id" `
Message string ` json:"message" `
Url string ` json:"url" `
Author * PayloadAuthor ` json:"author" `
}
type PayloadRepo struct {
Id int64 ` json:"id" `
Name string ` json:"name" `
Url string ` json:"url" `
Description string ` json:"description" `
Website string ` json:"website" `
Watchers int ` json:"watchers" `
2014-09-17 09:11:51 -04:00
Owner * PayloadAuthor ` json:"owner" `
2014-06-08 04:45:34 -04:00
Private bool ` json:"private" `
}
2014-08-24 08:59:47 -04:00
type BasePayload interface {
GetJSONPayload ( ) ( [ ] byte , error )
}
2014-06-08 04:54:52 -04:00
// Payload represents a payload information of hook.
2014-06-08 04:45:34 -04:00
type Payload struct {
2014-08-26 08:20:18 -04:00
Secret string ` json:"secret" `
Ref string ` json:"ref" `
Commits [ ] * PayloadCommit ` json:"commits" `
Repo * PayloadRepo ` json:"repository" `
Pusher * PayloadAuthor ` json:"pusher" `
Before string ` json:"before" `
After string ` json:"after" `
CompareUrl string ` json:"compare_url" `
2014-06-08 04:45:34 -04:00
}
2014-08-24 08:59:47 -04:00
func ( p Payload ) GetJSONPayload ( ) ( [ ] byte , error ) {
data , err := json . Marshal ( p )
if err != nil {
return [ ] byte { } , err
}
return data , nil
}
2014-06-08 04:54:52 -04:00
// HookTask represents a hook task.
2014-06-08 04:45:34 -04:00
type HookTask struct {
2015-07-25 21:32:04 +08:00
ID int64 ` xorm:"pk autoincr" `
RepoID int64 ` xorm:"INDEX" `
HookID int64
2014-08-09 15:40:10 -07:00
Uuid string
2014-06-08 04:54:52 -04:00
Type HookTaskType
2014-06-08 04:45:34 -04:00
Url string
2014-08-24 08:59:47 -04:00
BasePayload ` xorm:"-" `
2014-06-08 04:45:34 -04:00
PayloadContent string ` xorm:"TEXT" `
ContentType HookContentType
2014-08-09 15:40:10 -07:00
EventType HookEventType
2014-06-08 04:45:34 -04:00
IsSsl bool
2014-08-24 08:59:47 -04:00
IsDelivered bool
2015-07-25 21:32:04 +08:00
Delivered int64
2014-08-09 15:40:10 -07:00
IsSucceed bool
2014-06-08 04:45:34 -04:00
}
// CreateHookTask creates a new hook task,
// it handles conversion from Payload to PayloadContent.
func CreateHookTask ( t * HookTask ) error {
2014-08-24 08:59:47 -04:00
data , err := t . BasePayload . GetJSONPayload ( )
2014-06-08 04:45:34 -04:00
if err != nil {
return err
}
2014-08-09 15:40:10 -07:00
t . Uuid = uuid . NewV4 ( ) . String ( )
2014-06-08 04:45:34 -04:00
t . PayloadContent = string ( data )
2014-06-21 00:51:41 -04:00
_ , err = x . Insert ( t )
2014-06-08 04:45:34 -04:00
return err
}
// UpdateHookTask updates information of hook task.
func UpdateHookTask ( t * HookTask ) error {
2015-07-25 21:32:04 +08:00
_ , err := x . Id ( t . ID ) . AllCols ( ) . Update ( t )
2014-06-08 04:45:34 -04:00
return err
}
2015-07-25 21:32:04 +08:00
type hookQueue struct {
// Make sure one repository only occur once in the queue.
lock sync . Mutex
repoIDs map [ int64 ] bool
2014-10-09 19:01:22 -04:00
2015-07-25 21:32:04 +08:00
queue chan int64
}
func ( q * hookQueue ) removeRepoID ( id int64 ) {
q . lock . Lock ( )
defer q . lock . Unlock ( )
delete ( q . repoIDs , id )
}
func ( q * hookQueue ) addRepoID ( id int64 ) {
q . lock . Lock ( )
if q . repoIDs [ id ] {
q . lock . Unlock ( )
2014-10-09 19:01:22 -04:00
return
}
2015-07-25 21:32:04 +08:00
q . repoIDs [ id ] = true
q . lock . Unlock ( )
q . queue <- id
}
2014-10-09 19:01:22 -04:00
2015-07-25 21:32:04 +08:00
// AddRepoID adds repository ID to hook delivery queue.
func ( q * hookQueue ) AddRepoID ( id int64 ) {
go q . addRepoID ( id )
}
var HookQueue * hookQueue
func deliverHook ( t * HookTask ) {
2015-02-10 21:06:59 -05:00
timeout := time . Duration ( setting . Webhook . DeliverTimeout ) * time . Second
2015-07-25 21:32:04 +08:00
req := httplib . Post ( t . Url ) . SetTimeout ( timeout , timeout ) .
Header ( "X-Gogs-Delivery" , t . Uuid ) .
Header ( "X-Gogs-Event" , string ( t . EventType ) ) .
SetTLSClientConfig ( & tls . Config { InsecureSkipVerify : setting . Webhook . SkipTLSVerify } )
2014-06-08 04:45:34 -04:00
2015-07-25 21:32:04 +08:00
switch t . ContentType {
case JSON :
req = req . Header ( "Content-Type" , "application/json" ) . Body ( t . PayloadContent )
case FORM :
req . Param ( "payload" , t . PayloadContent )
}
2014-08-09 15:40:10 -07:00
2015-07-25 21:32:04 +08:00
t . IsDelivered = true
// FIXME: record response.
switch t . Type {
case GOGS :
{
if resp , err := req . Response ( ) ; err != nil {
log . Error ( 5 , "Delivery: %v" , err )
} else {
resp . Body . Close ( )
t . IsSucceed = true
}
}
case SLACK :
{
if resp , err := req . Response ( ) ; err != nil {
log . Error ( 5 , "Delivery: %v" , err )
} else {
defer resp . Body . Close ( )
contents , err := ioutil . ReadAll ( resp . Body )
if err != nil {
log . Error ( 5 , "%s" , err )
} else {
if string ( contents ) != "ok" {
log . Error ( 5 , "slack failed with: %s" , string ( contents ) )
2014-08-24 08:59:47 -04:00
} else {
t . IsSucceed = true
}
}
2014-08-09 15:40:10 -07:00
}
2015-07-25 21:32:04 +08:00
}
}
2014-08-09 15:40:10 -07:00
2015-07-25 21:32:04 +08:00
t . Delivered = time . Now ( ) . UTC ( ) . UnixNano ( )
if t . IsSucceed {
log . Trace ( "Hook delivered(%s): %s" , t . Uuid , t . PayloadContent )
}
}
2014-06-08 04:45:34 -04:00
2015-07-25 21:32:04 +08:00
// DeliverHooks checks and delivers undelivered hooks.
func DeliverHooks ( ) {
tasks := make ( [ ] * HookTask , 0 , 10 )
x . Where ( "is_delivered=?" , false ) . Iterate ( new ( HookTask ) ,
func ( idx int , bean interface { } ) error {
t := bean . ( * HookTask )
deliverHook ( t )
tasks = append ( tasks , t )
2014-06-08 04:45:34 -04:00
return nil
} )
2014-09-12 18:58:24 -04:00
// Update hook task status.
for _ , t := range tasks {
if err := UpdateHookTask ( t ) ; err != nil {
2015-07-25 21:32:04 +08:00
log . Error ( 4 , "UpdateHookTask(%d): %v" , t . ID , err )
}
}
HookQueue = & hookQueue {
lock : sync . Mutex { } ,
repoIDs : make ( map [ int64 ] bool ) ,
queue : make ( chan int64 , setting . Webhook . QueueLength ) ,
}
// Start listening on new hook requests.
for repoID := range HookQueue . queue {
HookQueue . removeRepoID ( repoID )
tasks = make ( [ ] * HookTask , 0 , 5 )
if err := x . Where ( "repo_id=? AND is_delivered=?" , repoID , false ) . Find ( & tasks ) ; err != nil {
log . Error ( 4 , "Get repository(%d) hook tasks: %v" , repoID , err )
continue
}
for _ , t := range tasks {
deliverHook ( t )
if err := UpdateHookTask ( t ) ; err != nil {
log . Error ( 4 , "UpdateHookTask(%d): %v" , t . ID , err )
}
2014-09-12 18:58:24 -04:00
}
}
2014-06-08 04:45:34 -04:00
}
2015-07-25 21:32:04 +08:00
func InitDeliverHooks ( ) {
go DeliverHooks ( )
}