Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial stub for custom webhooks #18748

Closed
wants to merge 15 commits into from
3 changes: 3 additions & 0 deletions models/webhook/hooktask.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ type HookTask struct {
RequestInfo *HookRequest `xorm:"-"`
ResponseContent string `xorm:"TEXT"`
ResponseInfo *HookResponse `xorm:"-"`

// Used for Auth Headers.
BearerToken string
}

func init() {
Expand Down
1 change: 1 addition & 0 deletions models/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ const (
MATRIX HookType = "matrix"
WECHATWORK HookType = "wechatwork"
PACKAGIST HookType = "packagist"
CUSTOM HookType = "custom"
)

// HookStatus is the status of a web hook
Expand Down
2 changes: 1 addition & 1 deletion modules/setting/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func newWebhookService() {
Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5)
Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool()
Webhook.AllowedHostList = sec.Key("ALLOWED_HOST_LIST").MustString("")
Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "matrix", "wechatwork", "packagist"}
Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "matrix", "wechatwork", "packagist", "custom"}
Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10)
Webhook.ProxyURL = sec.Key("PROXY_URL").MustString("")
if Webhook.ProxyURL != "" {
Expand Down
2 changes: 2 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1957,6 +1957,7 @@ settings.hook_type = Hook Type
settings.slack_token = Token
settings.slack_domain = Domain
settings.slack_channel = Channel
settings.custom_host_url = Host URL
settings.add_web_hook_desc = Integrate <a target="_blank" rel="noreferrer" href="%s">%s</a> into your repository.
settings.web_hook_name_gitea = Gitea
settings.web_hook_name_gogs = Gogs
Expand All @@ -1971,6 +1972,7 @@ settings.web_hook_name_feishu = Feishu
settings.web_hook_name_larksuite = Lark Suite
settings.web_hook_name_wechatwork = WeCom (Wechat Work)
settings.web_hook_name_packagist = Packagist
settings.web_hook_name_custom = Custom
settings.packagist_username = Packagist username
settings.packagist_api_token = API token
settings.packagist_package_url = Packagist package URL
Expand Down
130 changes: 130 additions & 0 deletions routers/web/repo/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,65 @@ func PackagistHooksNewPost(ctx *context.Context) {
ctx.Redirect(orCtx.Link)
}

// CustomHooksNewPost response for creating Custom hook
func CustomHooksNewPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.NewCustomHookForm)
ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
ctx.Data["PageIsSettingHooks"] = true
ctx.Data["PageIsSettingHooksNew"] = true
ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
ctx.Data["HookType"] = webhook.CUSTOM

orCtx, err := getOrgRepoCtx(ctx)
if err != nil {
ctx.ServerError("getOrgRepoCtx", err)
}
ctx.Data["BaseLink"] = orCtx.Link

if ctx.HasError() {
ctx.HTML(200, orCtx.NewTemplate)
return
}

meta, err := json.Marshal(&webhook_service.CustomMeta{
HostURL: form.HostURL,
AuthToken: form.AuthToken,
})
if err != nil {
ctx.ServerError("Marshal", err)
return
}

payloadURL, err := buildCustomURL(form)
if err != nil {
ctx.ServerError("buildCustomURL", err)
return
}

w := &webhook.Webhook{
RepoID: orCtx.RepoID,
URL: payloadURL,
ContentType: webhook.ContentTypeForm,
HookEvent: ParseHookEvent(form.WebhookForm),
IsActive: form.Active,
Type: webhook.CUSTOM,
HTTPMethod: http.MethodPost,
Meta: string(meta),
OrgID: orCtx.OrgID,
IsSystemWebhook: orCtx.IsSystemWebhook,
}
if err := w.UpdateEvent(); err != nil {
ctx.ServerError("UpdateEvent", err)
return
} else if err := webhook.CreateWebhook(db.DefaultContext, w); err != nil {
ctx.ServerError("CreateWebhook", err)
return
}

ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
ctx.Redirect(orCtx.Link)
}

func checkWebhook(ctx *context.Context) (*orgRepoCtx, *webhook.Webhook) {
ctx.Data["RequireHighlightJS"] = true

Expand Down Expand Up @@ -774,6 +833,8 @@ func checkWebhook(ctx *context.Context) (*orgRepoCtx, *webhook.Webhook) {
ctx.Data["MatrixHook"] = webhook_service.GetMatrixHook(w)
case webhook.PACKAGIST:
ctx.Data["PackagistHook"] = webhook_service.GetPackagistHook(w)
case webhook.CUSTOM:
ctx.Data["CustomHook"] = webhook_service.GetCustomHook(w)
}

ctx.Data["History"], err = w.History(1)
Expand Down Expand Up @@ -1236,6 +1297,75 @@ func PackagistHooksEditPost(ctx *context.Context) {
ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
}

// CustomHooksEditPost response for editing custom hook
func CustomHookEditPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.NewCustomHookForm)
ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingHooks"] = true
ctx.Data["PageIsSettingHooksNew"] = true
ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
ctx.Data["HookType"] = webhook.CUSTOM

orCtx, err := getOrgRepoCtx(ctx)
if err != nil {
ctx.ServerError("getOrgRepoCtx", err)
}
ctx.Data["BaseLink"] = orCtx.Link

if ctx.HasError() {
ctx.HTML(200, orCtx.NewTemplate)
return
}

meta, err := json.Marshal(&webhook_service.CustomMeta{
HostURL: form.HostURL,
AuthToken: form.AuthToken,
})
if err != nil {
ctx.ServerError("Marshal", err)
return
}

payloadURL, err := buildCustomURL(form)
if err != nil {
ctx.ServerError("buildCustomURL", err)
return
}

w := &webhook.Webhook{
RepoID: orCtx.RepoID,
URL: payloadURL,
ContentType: webhook.ContentTypeForm,
HookEvent: ParseHookEvent(form.WebhookForm),
IsActive: form.Active,
Type: webhook.CUSTOM,
HTTPMethod: http.MethodPost,
Meta: string(meta),
OrgID: orCtx.OrgID,
IsSystemWebhook: orCtx.IsSystemWebhook,
}
if err := w.UpdateEvent(); err != nil {
ctx.ServerError("UpdateEvent", err)
return
} else if err := webhook.CreateWebhook(db.DefaultContext, w); err != nil {
ctx.ServerError("CreateWebhook", err)
return
}

ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
ctx.Redirect(orCtx.Link)
}

// buildCustomURL returns the correct REST API url for a Custom POST request.
func buildCustomURL(meta *forms.NewCustomHookForm) (string, error) {
tcURL, err := url.Parse(meta.HostURL)
if err != nil {
return "", err
}

return tcURL.String(), nil
}

// TestWebhook test if web hook is work fine
func TestWebhook(ctx *context.Context) {
hookID := ctx.ParamsInt64(":id")
Expand Down
6 changes: 6 additions & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ func RegisterRoutes(m *web.Route) {
m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
m.Post("/wechatwork/{id}", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost)
m.Post("/packagist/{id}", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost)
m.Post("/custom/{id}", bindIgnErr(forms.NewCustomHookForm{}), repo.CustomHookEditPost)
}, webhooksEnabled)

m.Group("/{configType:default-hooks|system-hooks}", func() {
Expand All @@ -472,6 +473,7 @@ func RegisterRoutes(m *web.Route) {
m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
m.Post("/wechatwork/new", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost)
m.Post("/packagist/new", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost)
m.Post("/custom/{id}", bindIgnErr(forms.NewCustomHookForm{}), repo.CustomHooksNewPost)
})

m.Group("/auths", func() {
Expand Down Expand Up @@ -570,6 +572,7 @@ func RegisterRoutes(m *web.Route) {
m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
m.Post("/wechatwork/new", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost)
m.Post("/custom/new", bindIgnErr(forms.NewCustomHookForm{}), repo.CustomHooksNewPost)
m.Group("/{id}", func() {
m.Get("", repo.WebHooksEdit)
m.Post("/replay/{uuid}", repo.ReplayWebhook)
Expand All @@ -584,6 +587,7 @@ func RegisterRoutes(m *web.Route) {
m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
m.Post("/wechatwork/{id}", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost)
m.Post("/custom/{id}", bindIgnErr(forms.NewCustomHookForm{}), repo.CustomHookEditPost)
}, webhooksEnabled)

m.Group("/labels", func() {
Expand Down Expand Up @@ -668,6 +672,7 @@ func RegisterRoutes(m *web.Route) {
m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
m.Post("/wechatwork/new", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost)
m.Post("/packagist/new", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost)
m.Post("/custom/new", bindIgnErr(forms.NewCustomHookForm{}), repo.CustomHooksNewPost)
m.Group("/{id}", func() {
m.Get("", repo.WebHooksEdit)
m.Post("/test", repo.TestWebhook)
Expand All @@ -684,6 +689,7 @@ func RegisterRoutes(m *web.Route) {
m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
m.Post("/wechatwork/{id}", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost)
m.Post("/packagist/{id}", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost)
m.Post("/custom/{id}", bindIgnErr(forms.NewCustomHookForm{}), repo.CustomHookEditPost)
}, webhooksEnabled)

m.Group("/keys", func() {
Expand Down
6 changes: 6 additions & 0 deletions services/forms/repo_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,12 @@ type NewPackagistHookForm struct {
WebhookForm
}

type NewCustomHookForm struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment is needed.

HostURL string `binding:"Required;ValidUrl"`
AuthToken string
WebhookForm
}

// Validate validates the fields
func (f *NewPackagistHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetContext(req)
Expand Down
35 changes: 35 additions & 0 deletions services/webhook/custom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2019 The Gitea 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 webhook

package webhook

import (
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
)

type (
// Custom contains metadata for the Custom WebHook
CustomMeta struct {
HostURL string `json:"host_url"`
AuthToken string `json:"auth_token,omitempty"`
}
)

// GetCustomPayload returns the payload as-is
func GetCustomPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) {
// TODO: add optional body on POST.
return p, nil
}

// GetCustomHook returns Custom metadata
func GetCustomHook(w *webhook_model.Webhook) *CustomMeta {
s := &CustomMeta{}
if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
log.Error("webhook.GetCustomHook(%d): %v", w.ID, err)
}
return s
}
52 changes: 52 additions & 0 deletions services/webhook/custom_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2019 The Gitea 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 webhook

package webhook

import (
"testing"

webhook_model "code.gitea.io/gitea/models/webhook"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

_ "github.com/mattn/go-sqlite3"
)

func TestGetCustomPayload(t *testing.T) {
t.Run("Payload isn't altered.", func(t *testing.T) {
p := createTestPayload()

pl, err := GetCustomPayload(p, webhook_model.HookEventPush, "")
require.NoError(t, err)
require.Equal(t, p, pl)
})
}

func TestWebhook_GetCustomHook(t *testing.T) {
// Run with bearer token
t.Run("GetCustomHook", func(t *testing.T) {
w := &webhook_model.Webhook{
Meta: `{"host_url": "http://localhost.com", "auth_token": "testToken"}`,
}

customHook := GetCustomHook(w)
assert.Equal(t, *customHook, CustomMeta{
HostURL: "http://localhost.com",
AuthToken: "testToken",
})
})
// Run without bearer token
t.Run("GetCustomHook", func(t *testing.T) {
w := &webhook_model.Webhook{
Meta: `{"host_url": "http://localhost.com"}`,
}

customHook := GetCustomHook(w)
assert.Equal(t, *customHook, CustomMeta{
HostURL: "http://localhost.com",
})
})
}
3 changes: 3 additions & 0 deletions services/webhook/deliver.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ func Deliver(t *webhook_model.HookTask) error {

event := t.EventType.Event()
eventType := string(t.EventType)
if t.BearerToken != "" {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", t.BearerToken))
}
req.Header.Add("X-Gitea-Delivery", t.UUID)
req.Header.Add("X-Gitea-Event", event)
req.Header.Add("X-Gitea-Event-Type", eventType)
Expand Down
20 changes: 16 additions & 4 deletions services/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ var webhooks = map[webhook_model.HookType]*webhook{
name: webhook_model.PACKAGIST,
payloadCreator: GetPackagistPayload,
},
webhook_model.CUSTOM: {
name: webhook_model.CUSTOM,
payloadCreator: GetCustomPayload,
},
}

// RegisterWebhook registers a webhook
Expand Down Expand Up @@ -170,11 +174,19 @@ func prepareWebhook(w *webhook_model.Webhook, repo *repo_model.Repository, event
payloader = p
}

// Load bearer token
var authToken string
switch w.Type {
case webhook_model.CUSTOM:
authToken = GetCustomHook(w).AuthToken
}

if err = webhook_model.CreateHookTask(&webhook_model.HookTask{
RepoID: repo.ID,
HookID: w.ID,
Payloader: payloader,
EventType: event,
RepoID: repo.ID,
HookID: w.ID,
Payloader: payloader,
EventType: event,
BearerToken: authToken,
}); err != nil {
return fmt.Errorf("CreateHookTask: %v", err)
}
Expand Down
Loading