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 "commit_comment": "CommitCommentEvent",
52 "content_reference": "ContentReferenceEvent",
53 "create": "CreateEvent",
54 "delete": "DeleteEvent",
55 "deploy_key": "DeployKeyEvent",
56 "deployment": "DeploymentEvent",
57 "deployment_status": "DeploymentStatusEvent",
58 "discussion": "DiscussionEvent",
59 "fork": "ForkEvent",
60 "github_app_authorization": "GitHubAppAuthorizationEvent",
61 "gollum": "GollumEvent",
62 "installation": "InstallationEvent",
63 "installation_repositories": "InstallationRepositoriesEvent",
64 "issue_comment": "IssueCommentEvent",
65 "issues": "IssuesEvent",
66 "label": "LabelEvent",
67 "marketplace_purchase": "MarketplacePurchaseEvent",
68 "member": "MemberEvent",
69 "membership": "MembershipEvent",
70 "meta": "MetaEvent",
71 "milestone": "MilestoneEvent",
72 "organization": "OrganizationEvent",
73 "org_block": "OrgBlockEvent",
74 "package": "PackageEvent",
75 "page_build": "PageBuildEvent",
76 "ping": "PingEvent",
77 "project": "ProjectEvent",
78 "project_card": "ProjectCardEvent",
79 "project_column": "ProjectColumnEvent",
80 "public": "PublicEvent",
81 "pull_request": "PullRequestEvent",
82 "pull_request_review": "PullRequestReviewEvent",
83 "pull_request_review_comment": "PullRequestReviewCommentEvent",
84 "pull_request_review_thread": "PullRequestReviewThreadEvent",
85 "pull_request_target": "PullRequestTargetEvent",
86 "push": "PushEvent",
87 "repository": "RepositoryEvent",
88 "repository_dispatch": "RepositoryDispatchEvent",
89 "repository_import": "RepositoryImportEvent",
90 "repository_vulnerability_alert": "RepositoryVulnerabilityAlertEvent",
91 "release": "ReleaseEvent",
92 "secret_scanning_alert": "SecretScanningAlertEvent",
93 "star": "StarEvent",
94 "status": "StatusEvent",
95 "team": "TeamEvent",
96 "team_add": "TeamAddEvent",
97 "user": "UserEvent",
98 "watch": "WatchEvent",
99 "workflow_dispatch": "WorkflowDispatchEvent",
100 "workflow_job": "WorkflowJobEvent",
101 "workflow_run": "WorkflowRunEvent",
102 }
103 )
104
105
106
107 func genMAC(message, key []byte, hashFunc func() hash.Hash) []byte {
108 mac := hmac.New(hashFunc, key)
109 mac.Write(message)
110 return mac.Sum(nil)
111 }
112
113
114 func checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool {
115 expectedMAC := genMAC(message, key, hashFunc)
116 return hmac.Equal(messageMAC, expectedMAC)
117 }
118
119
120
121 func messageMAC(signature string) ([]byte, func() hash.Hash, error) {
122 if signature == "" {
123 return nil, nil, errors.New("missing signature")
124 }
125 sigParts := strings.SplitN(signature, "=", 2)
126 if len(sigParts) != 2 {
127 return nil, nil, fmt.Errorf("error parsing signature %q", signature)
128 }
129
130 var hashFunc func() hash.Hash
131 switch sigParts[0] {
132 case sha1Prefix:
133 hashFunc = sha1.New
134 case sha256Prefix:
135 hashFunc = sha256.New
136 case sha512Prefix:
137 hashFunc = sha512.New
138 default:
139 return nil, nil, fmt.Errorf("unknown hash type prefix: %q", sigParts[0])
140 }
141
142 buf, err := hex.DecodeString(sigParts[1])
143 if err != nil {
144 return nil, nil, fmt.Errorf("error decoding signature %q: %v", signature, err)
145 }
146 return buf, hashFunc, nil
147 }
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166 func ValidatePayloadFromBody(contentType string, readable io.Reader, signature string, secretToken []byte) (payload []byte, err error) {
167 var body []byte
168
169 switch contentType {
170 case "application/json":
171 var err error
172 if body, err = ioutil.ReadAll(readable); err != nil {
173 return nil, err
174 }
175
176
177
178 payload = body
179
180 case "application/x-www-form-urlencoded":
181
182
183 const payloadFormParam = "payload"
184
185 var err error
186 if body, err = ioutil.ReadAll(readable); err != nil {
187 return nil, err
188 }
189
190
191
192 form, err := url.ParseQuery(string(body))
193 if err != nil {
194 return nil, err
195 }
196 payload = []byte(form.Get(payloadFormParam))
197
198 default:
199 return nil, fmt.Errorf("webhook request has unsupported Content-Type %q", contentType)
200 }
201
202
203
204 if len(secretToken) > 0 {
205 if err := ValidateSignature(signature, body, secretToken); err != nil {
206 return nil, err
207 }
208 }
209
210 return payload, nil
211 }
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
295 func ParseWebHook(messageType string, payload []byte) (interface{}, error) {
296 eventType, ok := eventTypeMapping[messageType]
297 if !ok {
298 return nil, fmt.Errorf("unknown X-Github-Event in message: %v", messageType)
299 }
300
301 event := Event{
302 Type: &eventType,
303 RawPayload: (*json.RawMessage)(&payload),
304 }
305 return event.ParsePayload()
306 }
307
View as plain text