1
2
3
4
5
6
7
8
9 package github
10
11 import (
12 "crypto/hmac"
13 "crypto/sha1"
14 "crypto/sha256"
15 "crypto/sha512"
16 "encoding/hex"
17 "encoding/json"
18 "errors"
19 "fmt"
20 "hash"
21 "io"
22 "mime"
23 "net/http"
24 "net/url"
25 "reflect"
26 "sort"
27 "strings"
28 )
29
30 const (
31
32 sha1Prefix = "sha1"
33
34 sha256Prefix = "sha256"
35 sha512Prefix = "sha512"
36
37 SHA1SignatureHeader = "X-Hub-Signature"
38
39 SHA256SignatureHeader = "X-Hub-Signature-256"
40
41 EventTypeHeader = "X-Github-Event"
42
43 DeliveryIDHeader = "X-Github-Delivery"
44 )
45
46 var (
47
48 eventTypeMapping = map[string]interface{}{
49 "branch_protection_rule": &BranchProtectionRuleEvent{},
50 "check_run": &CheckRunEvent{},
51 "check_suite": &CheckSuiteEvent{},
52 "code_scanning_alert": &CodeScanningAlertEvent{},
53 "commit_comment": &CommitCommentEvent{},
54 "content_reference": &ContentReferenceEvent{},
55 "create": &CreateEvent{},
56 "delete": &DeleteEvent{},
57 "dependabot_alert": &DependabotAlertEvent{},
58 "deploy_key": &DeployKeyEvent{},
59 "deployment": &DeploymentEvent{},
60 "deployment_status": &DeploymentStatusEvent{},
61 "deployment_protection_rule": &DeploymentProtectionRuleEvent{},
62 "discussion": &DiscussionEvent{},
63 "discussion_comment": &DiscussionCommentEvent{},
64 "fork": &ForkEvent{},
65 "github_app_authorization": &GitHubAppAuthorizationEvent{},
66 "gollum": &GollumEvent{},
67 "installation": &InstallationEvent{},
68 "installation_repositories": &InstallationRepositoriesEvent{},
69 "installation_target": &InstallationTargetEvent{},
70 "issue_comment": &IssueCommentEvent{},
71 "issues": &IssuesEvent{},
72 "label": &LabelEvent{},
73 "marketplace_purchase": &MarketplacePurchaseEvent{},
74 "member": &MemberEvent{},
75 "membership": &MembershipEvent{},
76 "merge_group": &MergeGroupEvent{},
77 "meta": &MetaEvent{},
78 "milestone": &MilestoneEvent{},
79 "organization": &OrganizationEvent{},
80 "org_block": &OrgBlockEvent{},
81 "package": &PackageEvent{},
82 "page_build": &PageBuildEvent{},
83 "personal_access_token_request": &PersonalAccessTokenRequestEvent{},
84 "ping": &PingEvent{},
85 "project": &ProjectEvent{},
86 "project_card": &ProjectCardEvent{},
87 "project_column": &ProjectColumnEvent{},
88 "projects_v2": &ProjectV2Event{},
89 "projects_v2_item": &ProjectV2ItemEvent{},
90 "public": &PublicEvent{},
91 "pull_request": &PullRequestEvent{},
92 "pull_request_review": &PullRequestReviewEvent{},
93 "pull_request_review_comment": &PullRequestReviewCommentEvent{},
94 "pull_request_review_thread": &PullRequestReviewThreadEvent{},
95 "pull_request_target": &PullRequestTargetEvent{},
96 "push": &PushEvent{},
97 "repository": &RepositoryEvent{},
98 "repository_dispatch": &RepositoryDispatchEvent{},
99 "repository_import": &RepositoryImportEvent{},
100 "repository_vulnerability_alert": &RepositoryVulnerabilityAlertEvent{},
101 "release": &ReleaseEvent{},
102 "secret_scanning_alert": &SecretScanningAlertEvent{},
103 "security_advisory": &SecurityAdvisoryEvent{},
104 "security_and_analysis": &SecurityAndAnalysisEvent{},
105 "star": &StarEvent{},
106 "status": &StatusEvent{},
107 "team": &TeamEvent{},
108 "team_add": &TeamAddEvent{},
109 "user": &UserEvent{},
110 "watch": &WatchEvent{},
111 "workflow_dispatch": &WorkflowDispatchEvent{},
112 "workflow_job": &WorkflowJobEvent{},
113 "workflow_run": &WorkflowRunEvent{},
114 }
115
116 messageToTypeName = make(map[string]string, len(eventTypeMapping))
117
118 typeToMessageMapping = make(map[string]string, len(eventTypeMapping))
119 )
120
121 func init() {
122 for k, v := range eventTypeMapping {
123 typename := reflect.TypeOf(v).Elem().Name()
124 messageToTypeName[k] = typename
125 typeToMessageMapping[typename] = k
126 }
127 }
128
129
130
131 func genMAC(message, key []byte, hashFunc func() hash.Hash) []byte {
132 mac := hmac.New(hashFunc, key)
133 mac.Write(message)
134 return mac.Sum(nil)
135 }
136
137
138 func checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool {
139 expectedMAC := genMAC(message, key, hashFunc)
140 return hmac.Equal(messageMAC, expectedMAC)
141 }
142
143
144
145 func messageMAC(signature string) ([]byte, func() hash.Hash, error) {
146 if signature == "" {
147 return nil, nil, errors.New("missing signature")
148 }
149 sigParts := strings.SplitN(signature, "=", 2)
150 if len(sigParts) != 2 {
151 return nil, nil, fmt.Errorf("error parsing signature %q", signature)
152 }
153
154 var hashFunc func() hash.Hash
155 switch sigParts[0] {
156 case sha1Prefix:
157 hashFunc = sha1.New
158 case sha256Prefix:
159 hashFunc = sha256.New
160 case sha512Prefix:
161 hashFunc = sha512.New
162 default:
163 return nil, nil, fmt.Errorf("unknown hash type prefix: %q", sigParts[0])
164 }
165
166 buf, err := hex.DecodeString(sigParts[1])
167 if err != nil {
168 return nil, nil, fmt.Errorf("error decoding signature %q: %v", signature, err)
169 }
170 return buf, hashFunc, nil
171 }
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190 func ValidatePayloadFromBody(contentType string, readable io.Reader, signature string, secretToken []byte) (payload []byte, err error) {
191 var body []byte
192
193 switch contentType {
194 case "application/json":
195 var err error
196 if body, err = io.ReadAll(readable); err != nil {
197 return nil, err
198 }
199
200
201
202 payload = body
203
204 case "application/x-www-form-urlencoded":
205
206
207 const payloadFormParam = "payload"
208
209 var err error
210 if body, err = io.ReadAll(readable); err != nil {
211 return nil, err
212 }
213
214
215
216 form, err := url.ParseQuery(string(body))
217 if err != nil {
218 return nil, err
219 }
220 payload = []byte(form.Get(payloadFormParam))
221
222 default:
223 return nil, fmt.Errorf("webhook request has unsupported Content-Type %q", contentType)
224 }
225
226
227 if len(secretToken) > 0 || len(signature) > 0 {
228 if err := ValidateSignature(signature, body, secretToken); err != nil {
229 return nil, err
230 }
231 }
232
233 return payload, nil
234 }
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251 func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err error) {
252 signature := r.Header.Get(SHA256SignatureHeader)
253 if signature == "" {
254 signature = r.Header.Get(SHA1SignatureHeader)
255 }
256
257 contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
258 if err != nil {
259 return nil, err
260 }
261
262 return ValidatePayloadFromBody(contentType, r.Body, signature, secretToken)
263 }
264
265
266
267
268
269
270
271 func ValidateSignature(signature string, payload, secretToken []byte) error {
272 messageMAC, hashFunc, err := messageMAC(signature)
273 if err != nil {
274 return err
275 }
276 if !checkMAC(payload, messageMAC, secretToken, hashFunc) {
277 return errors.New("payload signature check failed")
278 }
279 return nil
280 }
281
282
283
284
285 func WebHookType(r *http.Request) string {
286 return r.Header.Get(EventTypeHeader)
287 }
288
289
290
291
292 func DeliveryID(r *http.Request) string {
293 return r.Header.Get(DeliveryIDHeader)
294 }
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316 func ParseWebHook(messageType string, payload []byte) (interface{}, error) {
317 eventType, ok := messageToTypeName[messageType]
318 if !ok {
319 return nil, fmt.Errorf("unknown X-Github-Event in message: %v", messageType)
320 }
321
322 event := Event{
323 Type: &eventType,
324 RawPayload: (*json.RawMessage)(&payload),
325 }
326 return event.ParsePayload()
327 }
328
329
330
331 func MessageTypes() []string {
332 types := make([]string, 0, len(eventTypeMapping))
333 for t := range eventTypeMapping {
334 types = append(types, t)
335 }
336 sort.Strings(types)
337 return types
338 }
339
340
341
342 func EventForType(messageType string) interface{} {
343 prototype := eventTypeMapping[messageType]
344 if prototype == nil {
345 return nil
346 }
347
348
349
350
351 return reflect.New(reflect.TypeOf(prototype).Elem()).Interface()
352 }
353
View as plain text