forgejo_backup/services/webhook/shared/payloader.go
christopher-besch d17aa98262 Actions Failure, Succes, Recover Webhooks (#7508)
Implement Actions Success, Failure and Recover webhooks for Forgejo, Gitea, Gogs, Slack, Discord, DingTalk, Telegram, Microsoft Teams, Feishu / Lark Suite, Matrix, WeCom (Wechat Work), Packagist. Some of these webhooks have not been manually tested.

Implement settings for these new webhooks.

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests

- I added test coverage for Go changes...
  - [x] in their respective `*_test.go` for unit tests.
  - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I added test coverage for JavaScript changes...
  - [ ] in `web_src/js/*.test.js` if it can be unit tested.
  - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [ ] I do not want this change to show in the release notes.
- [x] I want the title to show in the release notes with a link to this pull request.
- [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title.

<!--start release-notes-assistant-->

## Release notes
<!--URL:https://codeberg.org/forgejo/forgejo-->
- Features
  - [PR](https://codeberg.org/forgejo/forgejo/pulls/7508): <!--number 7508 --><!--line 0 --><!--description QWN0aW9ucyBGYWlsdXJlLCBTdWNjZXMsIFJlY292ZXIgV2ViaG9va3M=-->Actions Failure, Succes, Recover Webhooks<!--description-->
<!--end release-notes-assistant-->

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7508
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: christopher-besch <mail@chris-besch.com>
Co-committed-by: christopher-besch <mail@chris-besch.com>
2025-06-03 14:29:19 +02:00

164 lines
6.6 KiB
Go

// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package shared
import (
"bytes"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
webhook_model "forgejo.org/models/webhook"
"forgejo.org/modules/json"
api "forgejo.org/modules/structs"
webhook_module "forgejo.org/modules/webhook"
)
var ErrPayloadTypeNotSupported = errors.New("unsupported webhook event")
// 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)
Issue(*api.IssuePayload) (T, error)
IssueComment(*api.IssueCommentPayload) (T, error)
Push(*api.PushPayload) (T, error)
PullRequest(*api.PullRequestPayload) (T, error)
Review(*api.PullRequestPayload, webhook_module.HookEventType) (T, error)
Repository(*api.RepositoryPayload) (T, error)
Release(*api.ReleasePayload) (T, error)
Wiki(*api.WikiPayload) (T, error)
Package(*api.PackagePayload) (T, error)
Action(*api.ActionPayload) (T, error)
}
func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (T, error) {
var p P
if err := json.Unmarshal(data, &p); err != nil {
var t T
return t, fmt.Errorf("could not unmarshal payload: %w", err)
}
return convert(p)
}
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)
case webhook_module.HookEventDelete:
return convertUnmarshalledJSON(rc.Delete, data)
case webhook_module.HookEventFork:
return convertUnmarshalledJSON(rc.Fork, data)
case webhook_module.HookEventIssues, webhook_module.HookEventIssueAssign, webhook_module.HookEventIssueLabel, webhook_module.HookEventIssueMilestone:
return convertUnmarshalledJSON(rc.Issue, data)
case webhook_module.HookEventIssueComment, webhook_module.HookEventPullRequestComment:
// previous code sometimes sent s.PullRequest(p.(*api.PullRequestPayload))
// however I couldn't find in notifier.go such a payload with an HookEvent***Comment event
// History (most recent first):
// - refactored in https://github.com/go-gitea/gitea/pull/12310
// - assertion added in https://github.com/go-gitea/gitea/pull/12046
// - issue raised in https://github.com/go-gitea/gitea/issues/11940#issuecomment-645713996
// > That's because for HookEventPullRequestComment event, some places use IssueCommentPayload and others use PullRequestPayload
// In modules/actions/workflows.go:183 the type assertion is always payload.(*api.IssueCommentPayload)
return convertUnmarshalledJSON(rc.IssueComment, data)
case webhook_module.HookEventPush:
return convertUnmarshalledJSON(rc.Push, data)
case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestLabel,
webhook_module.HookEventPullRequestMilestone, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestReviewRequest:
return convertUnmarshalledJSON(rc.PullRequest, data)
case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected, webhook_module.HookEventPullRequestReviewComment:
return convertUnmarshalledJSON(func(p *api.PullRequestPayload) (T, error) {
return rc.Review(p, event)
}, data)
case webhook_module.HookEventRepository:
return convertUnmarshalledJSON(rc.Repository, data)
case webhook_module.HookEventRelease:
return convertUnmarshalledJSON(rc.Release, data)
case webhook_module.HookEventWiki:
return convertUnmarshalledJSON(rc.Wiki, data)
case webhook_module.HookEventPackage:
return convertUnmarshalledJSON(rc.Package, data)
case webhook_module.HookEventActionRunFailure, webhook_module.HookEventActionRunRecover, webhook_module.HookEventActionRunSuccess:
return convertUnmarshalledJSON(rc.Action, data)
}
var t T
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)
if err != nil {
return nil, nil, err
}
return NewJSONRequestWithPayload(payload, w, t, withDefaultHeaders)
}
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
}
method := w.HTTPMethod
if method == "" {
method = http.MethodPost
}
req, err := http.NewRequest(method, w.URL, bytes.NewReader(body))
if err != nil {
return nil, nil, err
}
req.Header.Set("Content-Type", "application/json")
if withDefaultHeaders {
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
}