diff --git a/models/webhook/hooktask.go b/models/webhook/hooktask.go index 1d19ebd24e76b..8a9a3ffd1d6aa 100644 --- a/models/webhook/hooktask.go +++ b/models/webhook/hooktask.go @@ -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() { diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index ffc9b72b64d88..fba4a566e8510 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -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 diff --git a/modules/setting/webhook.go b/modules/setting/webhook.go index 0bfd7dcb4dd3f..8974f92e3876d 100644 --- a/modules/setting/webhook.go +++ b/modules/setting/webhook.go @@ -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 != "" { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index f52fef3c0590b..ed49cdf522c41 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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 %s into your repository. settings.web_hook_name_gitea = Gitea settings.web_hook_name_gogs = Gogs @@ -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 diff --git a/public/img/gitea_custom.svg b/public/img/gitea_custom.svg new file mode 100644 index 0000000000000..d027534502820 --- /dev/null +++ b/public/img/gitea_custom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go index fb984de7f5857..a2e0649103224 100644 --- a/routers/web/repo/webhook.go +++ b/routers/web/repo/webhook.go @@ -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 @@ -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) @@ -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") diff --git a/routers/web/web.go b/routers/web/web.go index d8c197fb967e2..909fe23f66df5 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -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() { @@ -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() { @@ -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) @@ -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() { @@ -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) @@ -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() { diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index da709ef800240..9739f31925a73 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -404,6 +404,12 @@ type NewPackagistHookForm struct { WebhookForm } +type NewCustomHookForm struct { + 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) diff --git a/services/webhook/custom.go b/services/webhook/custom.go new file mode 100644 index 0000000000000..9d525bf1898d2 --- /dev/null +++ b/services/webhook/custom.go @@ -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 +} diff --git a/services/webhook/custom_test.go b/services/webhook/custom_test.go new file mode 100644 index 0000000000000..6af6b5d7edbf5 --- /dev/null +++ b/services/webhook/custom_test.go @@ -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", + }) + }) +} diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go index 88b709cb41e74..4f95105468cb5 100644 --- a/services/webhook/deliver.go +++ b/services/webhook/deliver.go @@ -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) diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index 607fac963452f..bcd13d3a911cc 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -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 @@ -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) } diff --git a/templates/admin/hook_new.tmpl b/templates/admin/hook_new.tmpl index 049e54ef833cf..c47c326b353da 100644 --- a/templates/admin/hook_new.tmpl +++ b/templates/admin/hook_new.tmpl @@ -36,6 +36,8 @@ {{else if eq .HookType "packagist"}} + {{else if eq .HookType "custom"}} + {{end}} @@ -51,6 +53,7 @@ {{template "repo/settings/webhook/matrix" .}} {{template "repo/settings/webhook/wechatwork" .}} {{template "repo/settings/webhook/packagist" .}} + {{template "repo/settings/webhook/custom" .}} {{template "repo/settings/webhook/history" .}} diff --git a/templates/org/settings/hook_new.tmpl b/templates/org/settings/hook_new.tmpl index 5e8ebb51e9427..d0e7484b7e214 100644 --- a/templates/org/settings/hook_new.tmpl +++ b/templates/org/settings/hook_new.tmpl @@ -31,6 +31,8 @@ {{else if eq .HookType "packagist"}} + {{else if eq .HookType "custom"}} + {{end}} @@ -46,6 +48,7 @@ {{template "repo/settings/webhook/matrix" .}} {{template "repo/settings/webhook/wechatwork" .}} {{template "repo/settings/webhook/packagist" .}} + {{template "repo/settings/webhook/custom" .}} {{template "repo/settings/webhook/history" .}} diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl index b1a3771bdba11..22b870b15bb04 100644 --- a/templates/repo/settings/webhook/base_list.tmpl +++ b/templates/repo/settings/webhook/base_list.tmpl @@ -37,6 +37,9 @@ {{.i18n.Tr "repo.settings.web_hook_name_packagist"}} + + {{.i18n.Tr "repo.settings.web_hook_name_custom"}} + diff --git a/templates/repo/settings/webhook/custom.tmpl b/templates/repo/settings/webhook/custom.tmpl new file mode 100644 index 0000000000000..bbc5580ada6b7 --- /dev/null +++ b/templates/repo/settings/webhook/custom.tmpl @@ -0,0 +1,16 @@ +{{if eq .HookType "custom"}} +

{{.i18n.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.io/en-us/webhooks/" (.i18n.Tr "repo.settings.web_hook_name_custom") | Str2html}}

+
+ {{.CsrfTokenHtml}} +
+ + +
+ +
+ + +
+ {{template "repo/settings/webhook/settings" .}} +
+{{end}} diff --git a/templates/repo/settings/webhook/new.tmpl b/templates/repo/settings/webhook/new.tmpl index a438a4c71a3d7..7245827d6dad4 100644 --- a/templates/repo/settings/webhook/new.tmpl +++ b/templates/repo/settings/webhook/new.tmpl @@ -29,6 +29,8 @@ {{else if eq .HookType "packagist"}} + {{else if eq .HookType "custom"}} + {{end}} @@ -44,6 +46,7 @@ {{template "repo/settings/webhook/matrix" .}} {{template "repo/settings/webhook/wechatwork" .}} {{template "repo/settings/webhook/packagist" .}} + {{template "repo/settings/webhook/custom" .}} {{template "repo/settings/webhook/history" .}}