Skip to content
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
3 changes: 3 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1957,6 +1957,8 @@ settings.hook_type = Hook Type
settings.slack_token = Token
settings.slack_domain = Domain
settings.slack_channel = Channel
settings.custom_host_url = Host URL
settings.custom_auth_token = Custom Auth Token
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 +1973,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
1 change: 1 addition & 0 deletions public/img/gitea_custom.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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 2022 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 2022 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