diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index 4469eac9e..eee493e2c 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -148,7 +148,7 @@ func WebhookNew(ctx *context.Context) {
}
// ParseHookEvent convert web form content to webhook.HookEvent
-func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent {
+func ParseHookEvent(form forms.WebhookCoreForm) *webhook_module.HookEvent {
return &webhook_module.HookEvent{
PushOnly: form.PushOnly(),
SendEverything: form.SendEverything(),
@@ -188,7 +188,7 @@ func WebhookCreate(ctx *context.Context) {
return
}
- fields := handler.FormFields(func(form any) {
+ fields := handler.UnmarshalForm(func(form any) {
errs := binding.Bind(ctx.Req, form)
middleware.Validate(errs, ctx.Data, form, ctx.Locale) // error checked below in ctx.HasError
})
@@ -215,10 +215,10 @@ func WebhookCreate(ctx *context.Context) {
w.URL = fields.URL
w.ContentType = fields.ContentType
w.Secret = fields.Secret
- w.HookEvent = ParseHookEvent(fields.WebhookForm)
- w.IsActive = fields.WebhookForm.Active
+ w.HookEvent = ParseHookEvent(fields.WebhookCoreForm)
+ w.IsActive = fields.Active
w.HTTPMethod = fields.HTTPMethod
- err := w.SetHeaderAuthorization(fields.WebhookForm.AuthorizationHeader)
+ err := w.SetHeaderAuthorization(fields.AuthorizationHeader)
if err != nil {
ctx.ServerError("SetHeaderAuthorization", err)
return
@@ -245,14 +245,14 @@ func WebhookCreate(ctx *context.Context) {
HTTPMethod: fields.HTTPMethod,
ContentType: fields.ContentType,
Secret: fields.Secret,
- HookEvent: ParseHookEvent(fields.WebhookForm),
- IsActive: fields.WebhookForm.Active,
+ HookEvent: ParseHookEvent(fields.WebhookCoreForm),
+ IsActive: fields.Active,
Type: hookType,
Meta: string(meta),
OwnerID: orCtx.OwnerID,
IsSystemWebhook: orCtx.IsSystemWebhook,
}
- err = w.SetHeaderAuthorization(fields.WebhookForm.AuthorizationHeader)
+ err = w.SetHeaderAuthorization(fields.AuthorizationHeader)
if err != nil {
ctx.ServerError("SetHeaderAuthorization", err)
return
@@ -286,7 +286,7 @@ func WebhookUpdate(ctx *context.Context) {
return
}
- fields := handler.FormFields(func(form any) {
+ fields := handler.UnmarshalForm(func(form any) {
errs := binding.Bind(ctx.Req, form)
middleware.Validate(errs, ctx.Data, form, ctx.Locale) // error checked below in ctx.HasError
})
@@ -295,11 +295,11 @@ func WebhookUpdate(ctx *context.Context) {
w.URL = fields.URL
w.ContentType = fields.ContentType
w.Secret = fields.Secret
- w.HookEvent = ParseHookEvent(fields.WebhookForm)
- w.IsActive = fields.WebhookForm.Active
+ w.HookEvent = ParseHookEvent(fields.WebhookCoreForm)
+ w.IsActive = fields.Active
w.HTTPMethod = fields.HTTPMethod
- err := w.SetHeaderAuthorization(fields.WebhookForm.AuthorizationHeader)
+ err := w.SetHeaderAuthorization(fields.AuthorizationHeader)
if err != nil {
ctx.ServerError("SetHeaderAuthorization", err)
return
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index b5ff031f4..e0540852a 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models"
issues_model "code.gitea.io/gitea/models/issues"
project_model "code.gitea.io/gitea/models/project"
+ webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web/middleware"
@@ -235,8 +236,8 @@ func (f *ProtectBranchForm) Validate(req *http.Request, errs binding.Errors) bin
// \__/\ / \___ >___ /___| /\____/ \____/|__|_ \
// \/ \/ \/ \/ \/
-// WebhookForm form for changing web hook
-type WebhookForm struct {
+// WebhookCoreForm form for changing web hook (common to all webhook types)
+type WebhookCoreForm struct {
Events string
Create bool
Delete bool
@@ -265,20 +266,30 @@ type WebhookForm struct {
}
// PushOnly if the hook will be triggered when push
-func (f WebhookForm) PushOnly() bool {
+func (f WebhookCoreForm) PushOnly() bool {
return f.Events == "push_only"
}
// SendEverything if the hook will be triggered any event
-func (f WebhookForm) SendEverything() bool {
+func (f WebhookCoreForm) SendEverything() bool {
return f.Events == "send_everything"
}
// ChooseEvents if the hook will be triggered choose events
-func (f WebhookForm) ChooseEvents() bool {
+func (f WebhookCoreForm) ChooseEvents() bool {
return f.Events == "choose_events"
}
+// WebhookForm form for changing web hook (specific handling depending on the webhook type)
+type WebhookForm struct {
+ WebhookCoreForm
+ URL string
+ ContentType webhook_model.HookContentType
+ Secret string
+ HTTPMethod string
+ Metadata any
+}
+
// .___
// | | ______ ________ __ ____
// | |/ ___// ___/ | \_/ __ \
diff --git a/services/webhook/default.go b/services/webhook/default.go
index be3b9b3c7..314f53964 100644
--- a/services/webhook/default.go
+++ b/services/webhook/default.go
@@ -5,13 +5,8 @@ package webhook
import (
"context"
- "crypto/hmac"
- "crypto/sha1"
- "crypto/sha256"
- "encoding/hex"
"fmt"
"html/template"
- "io"
"net/http"
"net/url"
"strings"
@@ -21,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/svg"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/webhook/shared"
)
var _ Handler = defaultHandler{}
@@ -39,16 +35,16 @@ func (dh defaultHandler) Type() webhook_module.HookType {
func (dh defaultHandler) Icon(size int) template.HTML {
if dh.forgejo {
// forgejo.svg is not in web_src/svg/, so svg.RenderHTML does not work
- return imgIcon("forgejo.svg", size)
+ return shared.ImgIcon("forgejo.svg", size)
}
return svg.RenderHTML("gitea-gitea", size, "img")
}
func (defaultHandler) Metadata(*webhook_model.Webhook) any { return nil }
-func (defaultHandler) FormFields(bind func(any)) FormFields {
+func (defaultHandler) UnmarshalForm(bind func(any)) forms.WebhookForm {
var form struct {
- forms.WebhookForm
+ forms.WebhookCoreForm
PayloadURL string `binding:"Required;ValidUrl"`
HTTPMethod string `binding:"Required;In(POST,GET)"`
ContentType int `binding:"Required"`
@@ -60,13 +56,13 @@ func (defaultHandler) FormFields(bind func(any)) FormFields {
if webhook_model.HookContentType(form.ContentType) == webhook_model.ContentTypeForm {
contentType = webhook_model.ContentTypeForm
}
- return FormFields{
- WebhookForm: form.WebhookForm,
- URL: form.PayloadURL,
- ContentType: contentType,
- Secret: form.Secret,
- HTTPMethod: form.HTTPMethod,
- Metadata: nil,
+ return forms.WebhookForm{
+ WebhookCoreForm: form.WebhookCoreForm,
+ URL: form.PayloadURL,
+ ContentType: contentType,
+ Secret: form.Secret,
+ HTTPMethod: form.HTTPMethod,
+ Metadata: nil,
}
}
@@ -130,42 +126,5 @@ func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook,
}
body = []byte(t.PayloadContent)
- return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body)
-}
-
-func addDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error {
- var signatureSHA1 string
- var signatureSHA256 string
- if len(secret) > 0 {
- sig1 := hmac.New(sha1.New, secret)
- sig256 := hmac.New(sha256.New, secret)
- _, err := io.MultiWriter(sig1, sig256).Write(payloadContent)
- if err != nil {
- // this error should never happen, since the hashes are writing to []byte and always return a nil error.
- return fmt.Errorf("prepareWebhooks.sigWrite: %w", err)
- }
- signatureSHA1 = hex.EncodeToString(sig1.Sum(nil))
- signatureSHA256 = hex.EncodeToString(sig256.Sum(nil))
- }
-
- event := t.EventType.Event()
- eventType := string(t.EventType)
- req.Header.Add("X-Forgejo-Delivery", t.UUID)
- req.Header.Add("X-Forgejo-Event", event)
- req.Header.Add("X-Forgejo-Event-Type", eventType)
- req.Header.Add("X-Forgejo-Signature", signatureSHA256)
- req.Header.Add("X-Gitea-Delivery", t.UUID)
- req.Header.Add("X-Gitea-Event", event)
- req.Header.Add("X-Gitea-Event-Type", eventType)
- req.Header.Add("X-Gitea-Signature", signatureSHA256)
- req.Header.Add("X-Gogs-Delivery", t.UUID)
- req.Header.Add("X-Gogs-Event", event)
- req.Header.Add("X-Gogs-Event-Type", eventType)
- req.Header.Add("X-Gogs-Signature", signatureSHA256)
- req.Header.Add("X-Hub-Signature", "sha1="+signatureSHA1)
- req.Header.Add("X-Hub-Signature-256", "sha256="+signatureSHA256)
- req.Header["X-GitHub-Delivery"] = []string{t.UUID}
- req.Header["X-GitHub-Event"] = []string{event}
- req.Header["X-GitHub-Event-Type"] = []string{eventType}
- return nil
+ return req, body, shared.AddDefaultHeaders(req, []byte(w.Secret), t, body)
}
diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go
index 0a0160ac4..ea3544243 100644
--- a/services/webhook/dingtalk.go
+++ b/services/webhook/dingtalk.go
@@ -17,28 +17,29 @@ import (
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/webhook/shared"
)
type dingtalkHandler struct{}
func (dingtalkHandler) Type() webhook_module.HookType { return webhook_module.DINGTALK }
func (dingtalkHandler) Metadata(*webhook_model.Webhook) any { return nil }
-func (dingtalkHandler) Icon(size int) template.HTML { return imgIcon("dingtalk.ico", size) }
+func (dingtalkHandler) Icon(size int) template.HTML { return shared.ImgIcon("dingtalk.ico", size) }
-func (dingtalkHandler) FormFields(bind func(any)) FormFields {
+func (dingtalkHandler) UnmarshalForm(bind func(any)) forms.WebhookForm {
var form struct {
- forms.WebhookForm
+ forms.WebhookCoreForm
PayloadURL string `binding:"Required;ValidUrl"`
}
bind(&form)
- return FormFields{
- WebhookForm: form.WebhookForm,
- URL: form.PayloadURL,
- ContentType: webhook_model.ContentTypeJSON,
- Secret: "",
- HTTPMethod: http.MethodPost,
- Metadata: nil,
+ return forms.WebhookForm{
+ WebhookCoreForm: form.WebhookCoreForm,
+ URL: form.PayloadURL,
+ ContentType: webhook_model.ContentTypeJSON,
+ Secret: "",
+ HTTPMethod: http.MethodPost,
+ Metadata: nil,
}
}
@@ -225,8 +226,8 @@ func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkP
type dingtalkConvertor struct{}
-var _ payloadConvertor[DingtalkPayload] = dingtalkConvertor{}
+var _ shared.PayloadConvertor[DingtalkPayload] = dingtalkConvertor{}
func (dingtalkHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
- return newJSONRequest(dingtalkConvertor{}, w, t, true)
+ return shared.NewJSONRequest(dingtalkConvertor{}, w, t, true)
}
diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index 2efb46f5b..cb756688c 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -22,28 +22,29 @@ import (
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/webhook/shared"
)
type discordHandler struct{}
func (discordHandler) Type() webhook_module.HookType { return webhook_module.DISCORD }
-func (discordHandler) Icon(size int) template.HTML { return imgIcon("discord.png", size) }
+func (discordHandler) Icon(size int) template.HTML { return shared.ImgIcon("discord.png", size) }
-func (discordHandler) FormFields(bind func(any)) FormFields {
+func (discordHandler) UnmarshalForm(bind func(any)) forms.WebhookForm {
var form struct {
- forms.WebhookForm
+ forms.WebhookCoreForm
PayloadURL string `binding:"Required;ValidUrl"`
Username string
IconURL string
}
bind(&form)
- return FormFields{
- WebhookForm: form.WebhookForm,
- URL: form.PayloadURL,
- ContentType: webhook_model.ContentTypeJSON,
- Secret: "",
- HTTPMethod: http.MethodPost,
+ return forms.WebhookForm{
+ WebhookCoreForm: form.WebhookCoreForm,
+ URL: form.PayloadURL,
+ ContentType: webhook_model.ContentTypeJSON,
+ Secret: "",
+ HTTPMethod: http.MethodPost,
Metadata: &DiscordMeta{
Username: form.Username,
IconURL: form.IconURL,
@@ -287,7 +288,7 @@ type discordConvertor struct {
AvatarURL string
}
-var _ payloadConvertor[DiscordPayload] = discordConvertor{}
+var _ shared.PayloadConvertor[DiscordPayload] = discordConvertor{}
func (discordHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
meta := &DiscordMeta{}
@@ -298,7 +299,7 @@ func (discordHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook,
Username: meta.Username,
AvatarURL: meta.IconURL,
}
- return newJSONRequest(sc, w, t, true)
+ return shared.NewJSONRequest(sc, w, t, true)
}
func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) {
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index eba54fa09..f77c3bbd6 100644
--- a/services/webhook/feishu.go
+++ b/services/webhook/feishu.go
@@ -15,27 +15,28 @@ import (
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/webhook/shared"
)
type feishuHandler struct{}
func (feishuHandler) Type() webhook_module.HookType { return webhook_module.FEISHU }
-func (feishuHandler) Icon(size int) template.HTML { return imgIcon("feishu.png", size) }
+func (feishuHandler) Icon(size int) template.HTML { return shared.ImgIcon("feishu.png", size) }
-func (feishuHandler) FormFields(bind func(any)) FormFields {
+func (feishuHandler) UnmarshalForm(bind func(any)) forms.WebhookForm {
var form struct {
- forms.WebhookForm
+ forms.WebhookCoreForm
PayloadURL string `binding:"Required;ValidUrl"`
}
bind(&form)
- return FormFields{
- WebhookForm: form.WebhookForm,
- URL: form.PayloadURL,
- ContentType: webhook_model.ContentTypeJSON,
- Secret: "",
- HTTPMethod: http.MethodPost,
- Metadata: nil,
+ return forms.WebhookForm{
+ WebhookCoreForm: form.WebhookCoreForm,
+ URL: form.PayloadURL,
+ ContentType: webhook_model.ContentTypeJSON,
+ Secret: "",
+ HTTPMethod: http.MethodPost,
+ Metadata: nil,
}
}
@@ -192,8 +193,8 @@ func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error)
type feishuConvertor struct{}
-var _ payloadConvertor[FeishuPayload] = feishuConvertor{}
+var _ shared.PayloadConvertor[FeishuPayload] = feishuConvertor{}
func (feishuHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
- return newJSONRequest(feishuConvertor{}, w, t, true)
+ return shared.NewJSONRequest(feishuConvertor{}, w, t, true)
}
diff --git a/services/webhook/general.go b/services/webhook/general.go
index 454efc649..c41f58fe8 100644
--- a/services/webhook/general.go
+++ b/services/webhook/general.go
@@ -6,9 +6,7 @@ package webhook
import (
"fmt"
"html"
- "html/template"
"net/url"
- "strconv"
"strings"
webhook_model "code.gitea.io/gitea/models/webhook"
@@ -354,9 +352,3 @@ func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
Created: w.CreatedUnix.AsTime(),
}, nil
}
-
-func imgIcon(name string, size int) template.HTML {
- s := strconv.Itoa(size)
- src := html.EscapeString(setting.StaticURLPrefix + "/assets/img/" + name)
- return template.HTML(``)
-}
diff --git a/services/webhook/gogs.go b/services/webhook/gogs.go
index f616f5e2f..7dbf64343 100644
--- a/services/webhook/gogs.go
+++ b/services/webhook/gogs.go
@@ -10,16 +10,17 @@ import (
webhook_model "code.gitea.io/gitea/models/webhook"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/webhook/shared"
)
type gogsHandler struct{ defaultHandler }
func (gogsHandler) Type() webhook_module.HookType { return webhook_module.GOGS }
-func (gogsHandler) Icon(size int) template.HTML { return imgIcon("gogs.ico", size) }
+func (gogsHandler) Icon(size int) template.HTML { return shared.ImgIcon("gogs.ico", size) }
-func (gogsHandler) FormFields(bind func(any)) FormFields {
+func (gogsHandler) UnmarshalForm(bind func(any)) forms.WebhookForm {
var form struct {
- forms.WebhookForm
+ forms.WebhookCoreForm
PayloadURL string `binding:"Required;ValidUrl"`
ContentType int `binding:"Required"`
Secret string
@@ -30,12 +31,12 @@ func (gogsHandler) FormFields(bind func(any)) FormFields {
if webhook_model.HookContentType(form.ContentType) == webhook_model.ContentTypeForm {
contentType = webhook_model.ContentTypeForm
}
- return FormFields{
- WebhookForm: form.WebhookForm,
- URL: form.PayloadURL,
- ContentType: contentType,
- Secret: form.Secret,
- HTTPMethod: http.MethodPost,
- Metadata: nil,
+ return forms.WebhookForm{
+ WebhookCoreForm: form.WebhookCoreForm,
+ URL: form.PayloadURL,
+ ContentType: contentType,
+ Secret: form.Secret,
+ HTTPMethod: http.MethodPost,
+ Metadata: nil,
}
}
diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go
index 322b4d666..697e33e94 100644
--- a/services/webhook/matrix.go
+++ b/services/webhook/matrix.go
@@ -25,6 +25,7 @@ import (
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/webhook/shared"
)
type matrixHandler struct{}
@@ -35,25 +36,25 @@ func (matrixHandler) Icon(size int) template.HTML {
return svg.RenderHTML("gitea-matrix", size, "img")
}
-func (matrixHandler) FormFields(bind func(any)) FormFields {
+func (matrixHandler) UnmarshalForm(bind func(any)) forms.WebhookForm {
var form struct {
- forms.WebhookForm
+ forms.WebhookCoreForm
HomeserverURL string `binding:"Required;ValidUrl"`
RoomID string `binding:"Required"`
MessageType int
// enforce requirement of authorization_header
- // (value will still be set in the embedded WebhookForm)
+ // (value will still be set in the embedded WebhookCoreForm)
AuthorizationHeader string `binding:"Required"`
}
bind(&form)
- return FormFields{
- WebhookForm: form.WebhookForm,
- URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)),
- ContentType: webhook_model.ContentTypeJSON,
- Secret: "",
- HTTPMethod: http.MethodPut,
+ return forms.WebhookForm{
+ WebhookCoreForm: form.WebhookCoreForm,
+ URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)),
+ ContentType: webhook_model.ContentTypeJSON,
+ Secret: "",
+ HTTPMethod: http.MethodPut,
Metadata: &MatrixMeta{
HomeserverURL: form.HomeserverURL,
Room: form.RoomID,
@@ -70,7 +71,7 @@ func (matrixHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t
mc := matrixConvertor{
MsgType: messageTypeText[meta.MessageType],
}
- payload, err := newPayload(mc, []byte(t.PayloadContent), t.EventType)
+ payload, err := shared.NewPayload(mc, []byte(t.PayloadContent), t.EventType)
if err != nil {
return nil, nil, err
}
@@ -90,7 +91,7 @@ func (matrixHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t
}
req.Header.Set("Content-Type", "application/json")
- return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) // likely useless, but has always been sent historially
+ return req, body, shared.AddDefaultHeaders(req, []byte(w.Secret), t, body) // likely useless, but has always been sent historially
}
const matrixPayloadSizeLimit = 1024 * 64
@@ -125,7 +126,7 @@ type MatrixPayload struct {
Commits []*api.PayloadCommit `json:"io.gitea.commits,omitempty"`
}
-var _ payloadConvertor[MatrixPayload] = matrixConvertor{}
+var _ shared.PayloadConvertor[MatrixPayload] = matrixConvertor{}
type matrixConvertor struct {
MsgType string
diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go
index 940a6c49a..3e9959146 100644
--- a/services/webhook/msteams.go
+++ b/services/webhook/msteams.go
@@ -17,28 +17,29 @@ import (
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/webhook/shared"
)
type msteamsHandler struct{}
func (msteamsHandler) Type() webhook_module.HookType { return webhook_module.MSTEAMS }
func (msteamsHandler) Metadata(*webhook_model.Webhook) any { return nil }
-func (msteamsHandler) Icon(size int) template.HTML { return imgIcon("msteams.png", size) }
+func (msteamsHandler) Icon(size int) template.HTML { return shared.ImgIcon("msteams.png", size) }
-func (msteamsHandler) FormFields(bind func(any)) FormFields {
+func (msteamsHandler) UnmarshalForm(bind func(any)) forms.WebhookForm {
var form struct {
- forms.WebhookForm
+ forms.WebhookCoreForm
PayloadURL string `binding:"Required;ValidUrl"`
}
bind(&form)
- return FormFields{
- WebhookForm: form.WebhookForm,
- URL: form.PayloadURL,
- ContentType: webhook_model.ContentTypeJSON,
- Secret: "",
- HTTPMethod: http.MethodPost,
- Metadata: nil,
+ return forms.WebhookForm{
+ WebhookCoreForm: form.WebhookCoreForm,
+ URL: form.PayloadURL,
+ ContentType: webhook_model.ContentTypeJSON,
+ Secret: "",
+ HTTPMethod: http.MethodPost,
+ Metadata: nil,
}
}
@@ -370,8 +371,8 @@ func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTar
type msteamsConvertor struct{}
-var _ payloadConvertor[MSTeamsPayload] = msteamsConvertor{}
+var _ shared.PayloadConvertor[MSTeamsPayload] = msteamsConvertor{}
func (msteamsHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
- return newJSONRequest(msteamsConvertor{}, w, t, true)
+ return shared.NewJSONRequest(msteamsConvertor{}, w, t, true)
}
diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go
index f1f330610..9831a4e00 100644
--- a/services/webhook/packagist.go
+++ b/services/webhook/packagist.go
@@ -15,28 +15,29 @@ import (
"code.gitea.io/gitea/modules/log"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/webhook/shared"
)
type packagistHandler struct{}
func (packagistHandler) Type() webhook_module.HookType { return webhook_module.PACKAGIST }
-func (packagistHandler) Icon(size int) template.HTML { return imgIcon("packagist.png", size) }
+func (packagistHandler) Icon(size int) template.HTML { return shared.ImgIcon("packagist.png", size) }
-func (packagistHandler) FormFields(bind func(any)) FormFields {
+func (packagistHandler) UnmarshalForm(bind func(any)) forms.WebhookForm {
var form struct {
- forms.WebhookForm
+ forms.WebhookCoreForm
Username string `binding:"Required"`
APIToken string `binding:"Required"`
PackageURL string `binding:"Required;ValidUrl"`
}
bind(&form)
- return FormFields{
- WebhookForm: form.WebhookForm,
- URL: fmt.Sprintf("https://packagist.org/api/update-package?username=%s&apiToken=%s", url.QueryEscape(form.Username), url.QueryEscape(form.APIToken)),
- ContentType: webhook_model.ContentTypeJSON,
- Secret: "",
- HTTPMethod: http.MethodPost,
+ return forms.WebhookForm{
+ WebhookCoreForm: form.WebhookCoreForm,
+ URL: fmt.Sprintf("https://packagist.org/api/update-package?username=%s&apiToken=%s", url.QueryEscape(form.Username), url.QueryEscape(form.APIToken)),
+ ContentType: webhook_model.ContentTypeJSON,
+ Secret: "",
+ HTTPMethod: http.MethodPost,
Metadata: &PackagistMeta{
Username: form.Username,
APIToken: form.APIToken,
@@ -85,5 +86,5 @@ func (packagistHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook
URL: meta.PackageURL,
},
}
- return newJSONRequestWithPayload(payload, w, t, false)
+ return shared.NewJSONRequestWithPayload(payload, w, t, false)
}
diff --git a/services/webhook/shared/img.go b/services/webhook/shared/img.go
new file mode 100644
index 000000000..2d65ba4e0
--- /dev/null
+++ b/services/webhook/shared/img.go
@@ -0,0 +1,15 @@
+package shared
+
+import (
+ "html"
+ "html/template"
+ "strconv"
+
+ "code.gitea.io/gitea/modules/setting"
+)
+
+func ImgIcon(name string, size int) template.HTML {
+ s := strconv.Itoa(size)
+ src := html.EscapeString(setting.StaticURLPrefix + "/assets/img/" + name)
+ return template.HTML(``)
+}
diff --git a/services/webhook/payloader.go b/services/webhook/shared/payloader.go
similarity index 65%
rename from services/webhook/payloader.go
rename to services/webhook/shared/payloader.go
index f87e6e4ee..da7424dc2 100644
--- a/services/webhook/payloader.go
+++ b/services/webhook/shared/payloader.go
@@ -1,11 +1,16 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
-package webhook
+package shared
import (
"bytes"
+ "crypto/hmac"
+ "crypto/sha1"
+ "crypto/sha256"
+ "encoding/hex"
"fmt"
+ "io"
"net/http"
webhook_model "code.gitea.io/gitea/models/webhook"
@@ -14,8 +19,8 @@ import (
webhook_module "code.gitea.io/gitea/modules/webhook"
)
-// payloadConvertor defines the interface to convert system payload to webhook payload
-type payloadConvertor[T any] interface {
+// PayloadConvertor defines the interface to convert system payload to webhook payload
+type PayloadConvertor[T any] interface {
Create(*api.CreatePayload) (T, error)
Delete(*api.DeletePayload) (T, error)
Fork(*api.ForkPayload) (T, error)
@@ -39,7 +44,7 @@ func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte)
return convert(p)
}
-func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module.HookEventType) (T, error) {
+func NewPayload[T any](rc PayloadConvertor[T], data []byte, event webhook_module.HookEventType) (T, error) {
switch event {
case webhook_module.HookEventCreate:
return convertUnmarshalledJSON(rc.Create, data)
@@ -83,15 +88,15 @@ func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module
return t, fmt.Errorf("newPayload unsupported event: %s", event)
}
-func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) {
- payload, err := newPayload(pc, []byte(t.PayloadContent), t.EventType)
+func NewJSONRequest[T any](pc PayloadConvertor[T], w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) {
+ payload, err := NewPayload(pc, []byte(t.PayloadContent), t.EventType)
if err != nil {
return nil, nil, err
}
- return newJSONRequestWithPayload(payload, w, t, withDefaultHeaders)
+ return NewJSONRequestWithPayload(payload, w, t, withDefaultHeaders)
}
-func newJSONRequestWithPayload(payload any, w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) {
+func NewJSONRequestWithPayload(payload any, w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) {
body, err := json.MarshalIndent(payload, "", " ")
if err != nil {
return nil, nil, err
@@ -109,7 +114,45 @@ func newJSONRequestWithPayload(payload any, w *webhook_model.Webhook, t *webhook
req.Header.Set("Content-Type", "application/json")
if withDefaultHeaders {
- return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body)
+ return req, body, AddDefaultHeaders(req, []byte(w.Secret), t, body)
}
return req, body, nil
}
+
+// AddDefaultHeaders adds the X-Forgejo, X-Gitea, X-Gogs, X-Hub, X-GitHub headers to the given request
+func AddDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error {
+ var signatureSHA1 string
+ var signatureSHA256 string
+ if len(secret) > 0 {
+ sig1 := hmac.New(sha1.New, secret)
+ sig256 := hmac.New(sha256.New, secret)
+ _, err := io.MultiWriter(sig1, sig256).Write(payloadContent)
+ if err != nil {
+ // this error should never happen, since the hashes are writing to []byte and always return a nil error.
+ return fmt.Errorf("prepareWebhooks.sigWrite: %w", err)
+ }
+ signatureSHA1 = hex.EncodeToString(sig1.Sum(nil))
+ signatureSHA256 = hex.EncodeToString(sig256.Sum(nil))
+ }
+
+ event := t.EventType.Event()
+ eventType := string(t.EventType)
+ req.Header.Add("X-Forgejo-Delivery", t.UUID)
+ req.Header.Add("X-Forgejo-Event", event)
+ req.Header.Add("X-Forgejo-Event-Type", eventType)
+ req.Header.Add("X-Forgejo-Signature", signatureSHA256)
+ req.Header.Add("X-Gitea-Delivery", t.UUID)
+ req.Header.Add("X-Gitea-Event", event)
+ req.Header.Add("X-Gitea-Event-Type", eventType)
+ req.Header.Add("X-Gitea-Signature", signatureSHA256)
+ req.Header.Add("X-Gogs-Delivery", t.UUID)
+ req.Header.Add("X-Gogs-Event", event)
+ req.Header.Add("X-Gogs-Event-Type", eventType)
+ req.Header.Add("X-Gogs-Signature", signatureSHA256)
+ req.Header.Add("X-Hub-Signature", "sha1="+signatureSHA1)
+ req.Header.Add("X-Hub-Signature-256", "sha256="+signatureSHA256)
+ req.Header["X-GitHub-Delivery"] = []string{t.UUID}
+ req.Header["X-GitHub-Event"] = []string{event}
+ req.Header["X-GitHub-Event-Type"] = []string{eventType}
+ return nil
+}
diff --git a/services/webhook/slack.go b/services/webhook/slack.go
index 0b4c4b664..c835d5998 100644
--- a/services/webhook/slack.go
+++ b/services/webhook/slack.go
@@ -20,6 +20,7 @@ import (
webhook_module "code.gitea.io/gitea/modules/webhook"
gitea_context "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/webhook/shared"
"gitea.com/go-chi/binding"
)
@@ -27,10 +28,10 @@ import (
type slackHandler struct{}
func (slackHandler) Type() webhook_module.HookType { return webhook_module.SLACK }
-func (slackHandler) Icon(size int) template.HTML { return imgIcon("slack.png", size) }
+func (slackHandler) Icon(size int) template.HTML { return shared.ImgIcon("slack.png", size) }
type slackForm struct {
- forms.WebhookForm
+ forms.WebhookCoreForm
PayloadURL string `binding:"Required;ValidUrl"`
Channel string `binding:"Required"`
Username string
@@ -53,16 +54,16 @@ func (s *slackForm) Validate(req *http.Request, errs binding.Errors) binding.Err
return errs
}
-func (slackHandler) FormFields(bind func(any)) FormFields {
+func (slackHandler) UnmarshalForm(bind func(any)) forms.WebhookForm {
var form slackForm
bind(&form)
- return FormFields{
- WebhookForm: form.WebhookForm,
- URL: form.PayloadURL,
- ContentType: webhook_model.ContentTypeJSON,
- Secret: "",
- HTTPMethod: http.MethodPost,
+ return forms.WebhookForm{
+ WebhookCoreForm: form.WebhookCoreForm,
+ URL: form.PayloadURL,
+ ContentType: webhook_model.ContentTypeJSON,
+ Secret: "",
+ HTTPMethod: http.MethodPost,
Metadata: &SlackMeta{
Channel: strings.TrimSpace(form.Channel),
Username: form.Username,
@@ -334,7 +335,7 @@ type slackConvertor struct {
Color string
}
-var _ payloadConvertor[SlackPayload] = slackConvertor{}
+var _ shared.PayloadConvertor[SlackPayload] = slackConvertor{}
func (slackHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
meta := &SlackMeta{}
@@ -347,7 +348,7 @@ func (slackHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t
IconURL: meta.IconURL,
Color: meta.Color,
}
- return newJSONRequest(sc, w, t, true)
+ return shared.NewJSONRequest(sc, w, t, true)
}
var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`)
diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index daa986baf..724c41012 100644
--- a/services/webhook/telegram.go
+++ b/services/webhook/telegram.go
@@ -18,28 +18,29 @@ import (
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/webhook/shared"
)
type telegramHandler struct{}
func (telegramHandler) Type() webhook_module.HookType { return webhook_module.TELEGRAM }
-func (telegramHandler) Icon(size int) template.HTML { return imgIcon("telegram.png", size) }
+func (telegramHandler) Icon(size int) template.HTML { return shared.ImgIcon("telegram.png", size) }
-func (telegramHandler) FormFields(bind func(any)) FormFields {
+func (telegramHandler) UnmarshalForm(bind func(any)) forms.WebhookForm {
var form struct {
- forms.WebhookForm
+ forms.WebhookCoreForm
BotToken string `binding:"Required"`
ChatID string `binding:"Required"`
ThreadID string
}
bind(&form)
- return FormFields{
- WebhookForm: form.WebhookForm,
- URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&message_thread_id=%s", url.PathEscape(form.BotToken), url.QueryEscape(form.ChatID), url.QueryEscape(form.ThreadID)),
- ContentType: webhook_model.ContentTypeJSON,
- Secret: "",
- HTTPMethod: http.MethodPost,
+ return forms.WebhookForm{
+ WebhookCoreForm: form.WebhookCoreForm,
+ URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&message_thread_id=%s", url.PathEscape(form.BotToken), url.QueryEscape(form.ChatID), url.QueryEscape(form.ThreadID)),
+ ContentType: webhook_model.ContentTypeJSON,
+ Secret: "",
+ HTTPMethod: http.MethodPost,
Metadata: &TelegramMeta{
BotToken: form.BotToken,
ChatID: form.ChatID,
@@ -220,8 +221,8 @@ func createTelegramPayload(message string) TelegramPayload {
type telegramConvertor struct{}
-var _ payloadConvertor[TelegramPayload] = telegramConvertor{}
+var _ shared.PayloadConvertor[TelegramPayload] = telegramConvertor{}
func (telegramHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
- return newJSONRequest(telegramConvertor{}, w, t, true)
+ return shared.NewJSONRequest(telegramConvertor{}, w, t, true)
}
diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go
index f27bffc29..75962db60 100644
--- a/services/webhook/webhook.go
+++ b/services/webhook/webhook.go
@@ -32,22 +32,13 @@ import (
type Handler interface {
Type() webhook_module.HookType
Metadata(*webhook_model.Webhook) any
- // FormFields provides a function to bind the request to the form.
+ // UnmarshalForm provides a function to bind the request to the form.
// If form implements the [binding.Validator] interface, the Validate method will be called
- FormFields(bind func(form any)) FormFields
+ UnmarshalForm(bind func(form any)) forms.WebhookForm
NewRequest(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error)
Icon(size int) template.HTML
}
-type FormFields struct {
- forms.WebhookForm
- URL string
- ContentType webhook_model.HookContentType
- Secret string
- HTTPMethod string
- Metadata any
-}
-
var webhookHandlers = []Handler{
defaultHandler{true},
defaultHandler{false},
diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go
index eff5b9b52..0329cff12 100644
--- a/services/webhook/wechatwork.go
+++ b/services/webhook/wechatwork.go
@@ -15,6 +15,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/webhook/shared"
)
type wechatworkHandler struct{}
@@ -23,23 +24,23 @@ func (wechatworkHandler) Type() webhook_module.HookType { return webhook_m
func (wechatworkHandler) Metadata(*webhook_model.Webhook) any { return nil }
func (wechatworkHandler) Icon(size int) template.HTML {
- return imgIcon("wechatwork.png", size)
+ return shared.ImgIcon("wechatwork.png", size)
}
-func (wechatworkHandler) FormFields(bind func(any)) FormFields {
+func (wechatworkHandler) UnmarshalForm(bind func(any)) forms.WebhookForm {
var form struct {
- forms.WebhookForm
+ forms.WebhookCoreForm
PayloadURL string `binding:"Required;ValidUrl"`
}
bind(&form)
- return FormFields{
- WebhookForm: form.WebhookForm,
- URL: form.PayloadURL,
- ContentType: webhook_model.ContentTypeJSON,
- Secret: "",
- HTTPMethod: http.MethodPost,
- Metadata: nil,
+ return forms.WebhookForm{
+ WebhookCoreForm: form.WebhookCoreForm,
+ URL: form.PayloadURL,
+ ContentType: webhook_model.ContentTypeJSON,
+ Secret: "",
+ HTTPMethod: http.MethodPost,
+ Metadata: nil,
}
}
@@ -203,8 +204,8 @@ func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload,
type wechatworkConvertor struct{}
-var _ payloadConvertor[WechatworkPayload] = wechatworkConvertor{}
+var _ shared.PayloadConvertor[WechatworkPayload] = wechatworkConvertor{}
func (wechatworkHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
- return newJSONRequest(wechatworkConvertor{}, w, t, true)
+ return shared.NewJSONRequest(wechatworkConvertor{}, w, t, true)
}