[REFACTOR] webhook shared code to prevent import cycles
This commit is contained in:
parent
c4adb08d6d
commit
04a398a1af
17 changed files with 232 additions and 211 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
// .___
|
||||
// | | ______ ________ __ ____
|
||||
// | |/ ___// ___/ | \_/ __ \
|
||||
|
|
|
@ -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,8 +56,8 @@ 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,
|
||||
return forms.WebhookForm{
|
||||
WebhookCoreForm: form.WebhookCoreForm,
|
||||
URL: form.PayloadURL,
|
||||
ContentType: contentType,
|
||||
Secret: form.Secret,
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -17,23 +17,24 @@ 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,
|
||||
return forms.WebhookForm{
|
||||
WebhookCoreForm: form.WebhookCoreForm,
|
||||
URL: form.PayloadURL,
|
||||
ContentType: webhook_model.ContentTypeJSON,
|
||||
Secret: "",
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -22,24 +22,25 @@ 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,
|
||||
return forms.WebhookForm{
|
||||
WebhookCoreForm: form.WebhookCoreForm,
|
||||
URL: form.PayloadURL,
|
||||
ContentType: webhook_model.ContentTypeJSON,
|
||||
Secret: "",
|
||||
|
@ -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) {
|
||||
|
|
|
@ -15,22 +15,23 @@ 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,
|
||||
return forms.WebhookForm{
|
||||
WebhookCoreForm: form.WebhookCoreForm,
|
||||
URL: form.PayloadURL,
|
||||
ContentType: webhook_model.ContentTypeJSON,
|
||||
Secret: "",
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(`<img width="` + s + `" height="` + s + `" src="` + src + `">`)
|
||||
}
|
||||
|
|
|
@ -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,8 +31,8 @@ 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,
|
||||
return forms.WebhookForm{
|
||||
WebhookCoreForm: form.WebhookCoreForm,
|
||||
URL: form.PayloadURL,
|
||||
ContentType: contentType,
|
||||
Secret: form.Secret,
|
||||
|
|
|
@ -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,21 +36,21 @@ 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,
|
||||
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: "",
|
||||
|
@ -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
|
||||
|
|
|
@ -17,23 +17,24 @@ 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,
|
||||
return forms.WebhookForm{
|
||||
WebhookCoreForm: form.WebhookCoreForm,
|
||||
URL: form.PayloadURL,
|
||||
ContentType: webhook_model.ContentTypeJSON,
|
||||
Secret: "",
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -15,24 +15,25 @@ 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,
|
||||
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: "",
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
15
services/webhook/shared/img.go
Normal file
15
services/webhook/shared/img.go
Normal file
|
@ -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(`<img width="` + s + `" height="` + s + `" src="` + src + `">`)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,12 +54,12 @@ 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,
|
||||
return forms.WebhookForm{
|
||||
WebhookCoreForm: form.WebhookCoreForm,
|
||||
URL: form.PayloadURL,
|
||||
ContentType: webhook_model.ContentTypeJSON,
|
||||
Secret: "",
|
||||
|
@ -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}$`)
|
||||
|
|
|
@ -18,24 +18,25 @@ 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,
|
||||
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: "",
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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,18 +24,18 @@ 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,
|
||||
return forms.WebhookForm{
|
||||
WebhookCoreForm: form.WebhookCoreForm,
|
||||
URL: form.PayloadURL,
|
||||
ContentType: webhook_model.ContentTypeJSON,
|
||||
Secret: "",
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue