summaryrefslogtreecommitdiffstats
path: root/services/webhook/shared/payloader.go
blob: cf0bfa82cb1178c7e8b94d622b575787b845470a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// 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 "code.gitea.io/gitea/models/webhook"
	"code.gitea.io/gitea/modules/json"
	api "code.gitea.io/gitea/modules/structs"
	webhook_module "code.gitea.io/gitea/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)
}

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)
	}
	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
}