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