2024-03-20 17:44:01 +03:00
|
|
|
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
package webhook
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
2024-03-22 18:02:48 +03:00
|
|
|
"html/template"
|
2024-03-20 17:44:01 +03:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
webhook_model "code.gitea.io/gitea/models/webhook"
|
2024-04-30 11:18:02 +03:00
|
|
|
"code.gitea.io/gitea/modules/git"
|
|
|
|
"code.gitea.io/gitea/modules/json"
|
2024-03-20 17:44:01 +03:00
|
|
|
"code.gitea.io/gitea/modules/log"
|
2024-03-22 18:02:48 +03:00
|
|
|
"code.gitea.io/gitea/modules/svg"
|
2024-03-20 17:44:01 +03:00
|
|
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
2024-03-21 15:42:40 +03:00
|
|
|
"code.gitea.io/gitea/services/forms"
|
2024-04-03 15:22:36 +03:00
|
|
|
"code.gitea.io/gitea/services/webhook/shared"
|
2024-03-20 17:44:01 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
var _ Handler = defaultHandler{}
|
|
|
|
|
|
|
|
type defaultHandler struct {
|
|
|
|
forgejo bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dh defaultHandler) Type() webhook_module.HookType {
|
|
|
|
if dh.forgejo {
|
|
|
|
return webhook_module.FORGEJO
|
|
|
|
}
|
|
|
|
return webhook_module.GITEA
|
|
|
|
}
|
|
|
|
|
2024-03-22 18:02:48 +03:00
|
|
|
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
|
2024-04-03 15:22:36 +03:00
|
|
|
return shared.ImgIcon("forgejo.svg", size)
|
2024-03-22 18:02:48 +03:00
|
|
|
}
|
|
|
|
return svg.RenderHTML("gitea-gitea", size, "img")
|
|
|
|
}
|
|
|
|
|
2024-03-20 17:44:01 +03:00
|
|
|
func (defaultHandler) Metadata(*webhook_model.Webhook) any { return nil }
|
|
|
|
|
2024-04-03 15:22:36 +03:00
|
|
|
func (defaultHandler) UnmarshalForm(bind func(any)) forms.WebhookForm {
|
2024-03-21 15:42:40 +03:00
|
|
|
var form struct {
|
2024-04-03 15:22:36 +03:00
|
|
|
forms.WebhookCoreForm
|
2024-03-21 15:42:40 +03:00
|
|
|
PayloadURL string `binding:"Required;ValidUrl"`
|
|
|
|
HTTPMethod string `binding:"Required;In(POST,GET)"`
|
|
|
|
ContentType int `binding:"Required"`
|
|
|
|
Secret string
|
|
|
|
}
|
|
|
|
bind(&form)
|
|
|
|
|
|
|
|
contentType := webhook_model.ContentTypeJSON
|
|
|
|
if webhook_model.HookContentType(form.ContentType) == webhook_model.ContentTypeForm {
|
|
|
|
contentType = webhook_model.ContentTypeForm
|
|
|
|
}
|
2024-04-03 15:22:36 +03:00
|
|
|
return forms.WebhookForm{
|
|
|
|
WebhookCoreForm: form.WebhookCoreForm,
|
|
|
|
URL: form.PayloadURL,
|
|
|
|
ContentType: contentType,
|
|
|
|
Secret: form.Secret,
|
|
|
|
HTTPMethod: form.HTTPMethod,
|
|
|
|
Metadata: nil,
|
2024-03-21 15:42:40 +03:00
|
|
|
}
|
2024-03-21 15:23:27 +03:00
|
|
|
}
|
|
|
|
|
2024-03-20 17:44:01 +03:00
|
|
|
func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) {
|
2024-04-30 11:18:02 +03:00
|
|
|
payloadContent := t.PayloadContent
|
|
|
|
if w.Type == webhook_module.GITEA &&
|
|
|
|
(t.EventType == webhook_module.HookEventCreate || t.EventType == webhook_module.HookEventDelete) {
|
|
|
|
// Woodpecker expects the ref to be short on tag creation only
|
|
|
|
// https://github.com/woodpecker-ci/woodpecker/blob/00ccec078cdced80cf309cd4da460a5041d7991a/server/forge/gitea/helper.go#L134
|
|
|
|
// see https://codeberg.org/codeberg/community/issues/1556
|
|
|
|
payloadContent, err = substituteRefShortName(payloadContent)
|
|
|
|
if err != nil {
|
2024-05-25 10:43:50 +03:00
|
|
|
return nil, nil, fmt.Errorf("could not substitute ref: %w", err)
|
2024-04-30 11:18:02 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-20 17:44:01 +03:00
|
|
|
switch w.HTTPMethod {
|
|
|
|
case "":
|
|
|
|
log.Info("HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST", w.Type, w.URL, w.ID)
|
|
|
|
fallthrough
|
|
|
|
case http.MethodPost:
|
|
|
|
switch w.ContentType {
|
|
|
|
case webhook_model.ContentTypeJSON:
|
2024-04-30 11:18:02 +03:00
|
|
|
req, err = http.NewRequest("POST", w.URL, strings.NewReader(payloadContent))
|
2024-03-20 17:44:01 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
case webhook_model.ContentTypeForm:
|
|
|
|
forms := url.Values{
|
2024-04-30 11:18:02 +03:00
|
|
|
"payload": []string{payloadContent},
|
2024-03-20 17:44:01 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
req, err = http.NewRequest("POST", w.URL, strings.NewReader(forms.Encode()))
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
default:
|
|
|
|
return nil, nil, fmt.Errorf("invalid content type: %v", w.ContentType)
|
|
|
|
}
|
|
|
|
case http.MethodGet:
|
|
|
|
u, err := url.Parse(w.URL)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("invalid URL: %w", err)
|
|
|
|
}
|
|
|
|
vals := u.Query()
|
2024-04-30 11:18:02 +03:00
|
|
|
vals["payload"] = []string{payloadContent}
|
2024-03-20 17:44:01 +03:00
|
|
|
u.RawQuery = vals.Encode()
|
|
|
|
req, err = http.NewRequest("GET", u.String(), nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
case http.MethodPut:
|
|
|
|
switch w.Type {
|
|
|
|
case webhook_module.MATRIX: // used when t.Version == 1
|
2024-04-30 11:18:02 +03:00
|
|
|
txnID, err := getMatrixTxnID([]byte(payloadContent))
|
2024-03-20 17:44:01 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID))
|
2024-04-30 11:18:02 +03:00
|
|
|
req, err = http.NewRequest("PUT", url, strings.NewReader(payloadContent))
|
2024-03-20 17:44:01 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod)
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod)
|
|
|
|
}
|
|
|
|
|
2024-04-30 11:18:02 +03:00
|
|
|
body = []byte(payloadContent)
|
2024-04-03 15:22:36 +03:00
|
|
|
return req, body, shared.AddDefaultHeaders(req, []byte(w.Secret), t, body)
|
2024-03-20 17:44:01 +03:00
|
|
|
}
|
2024-04-30 11:18:02 +03:00
|
|
|
|
|
|
|
func substituteRefShortName(body string) (string, error) {
|
|
|
|
var m map[string]any
|
|
|
|
if err := json.Unmarshal([]byte(body), &m); err != nil {
|
|
|
|
return body, err
|
|
|
|
}
|
|
|
|
ref, ok := m["ref"].(string)
|
|
|
|
if !ok {
|
|
|
|
return body, fmt.Errorf("expected string 'ref', got %T", m["ref"])
|
|
|
|
}
|
|
|
|
|
|
|
|
m["ref"] = git.RefName(ref).ShortName()
|
|
|
|
|
|
|
|
buf, err := json.Marshal(m)
|
|
|
|
return string(buf), err
|
|
|
|
}
|