github.com/google/go-github/v42@v42.0.0/github/messages.go (about) 1 // Copyright 2016 The go-github AUTHORS. All rights reserved. 2 // 3 // Use of this source code is governed by a BSD-style 4 // license that can be found in the LICENSE file. 5 6 // This file provides functions for validating payloads from GitHub Webhooks. 7 // GitHub API docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github 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 // sha1Prefix is the prefix used by GitHub before the HMAC hexdigest. 31 sha1Prefix = "sha1" 32 // sha256Prefix and sha512Prefix are provided for future compatibility. 33 sha256Prefix = "sha256" 34 sha512Prefix = "sha512" 35 // SHA1SignatureHeader is the GitHub header key used to pass the HMAC-SHA1 hexdigest. 36 SHA1SignatureHeader = "X-Hub-Signature" 37 // SHA256SignatureHeader is the GitHub header key used to pass the HMAC-SHA256 hexdigest. 38 SHA256SignatureHeader = "X-Hub-Signature-256" 39 // EventTypeHeader is the GitHub header key used to pass the event type. 40 EventTypeHeader = "X-Github-Event" 41 // DeliveryIDHeader is the GitHub header key used to pass the unique ID for the webhook event. 42 DeliveryIDHeader = "X-Github-Delivery" 43 ) 44 45 var ( 46 // eventTypeMapping maps webhooks types to their corresponding go-github struct types. 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_target": "PullRequestTargetEvent", 85 "push": "PushEvent", 86 "repository": "RepositoryEvent", 87 "repository_dispatch": "RepositoryDispatchEvent", 88 "repository_vulnerability_alert": "RepositoryVulnerabilityAlertEvent", 89 "release": "ReleaseEvent", 90 "star": "StarEvent", 91 "status": "StatusEvent", 92 "team": "TeamEvent", 93 "team_add": "TeamAddEvent", 94 "user": "UserEvent", 95 "watch": "WatchEvent", 96 "workflow_dispatch": "WorkflowDispatchEvent", 97 "workflow_job": "WorkflowJobEvent", 98 "workflow_run": "WorkflowRunEvent", 99 } 100 ) 101 102 // genMAC generates the HMAC signature for a message provided the secret key 103 // and hashFunc. 104 func genMAC(message, key []byte, hashFunc func() hash.Hash) []byte { 105 mac := hmac.New(hashFunc, key) 106 mac.Write(message) 107 return mac.Sum(nil) 108 } 109 110 // checkMAC reports whether messageMAC is a valid HMAC tag for message. 111 func checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool { 112 expectedMAC := genMAC(message, key, hashFunc) 113 return hmac.Equal(messageMAC, expectedMAC) 114 } 115 116 // messageMAC returns the hex-decoded HMAC tag from the signature and its 117 // corresponding hash function. 118 func messageMAC(signature string) ([]byte, func() hash.Hash, error) { 119 if signature == "" { 120 return nil, nil, errors.New("missing signature") 121 } 122 sigParts := strings.SplitN(signature, "=", 2) 123 if len(sigParts) != 2 { 124 return nil, nil, fmt.Errorf("error parsing signature %q", signature) 125 } 126 127 var hashFunc func() hash.Hash 128 switch sigParts[0] { 129 case sha1Prefix: 130 hashFunc = sha1.New 131 case sha256Prefix: 132 hashFunc = sha256.New 133 case sha512Prefix: 134 hashFunc = sha512.New 135 default: 136 return nil, nil, fmt.Errorf("unknown hash type prefix: %q", sigParts[0]) 137 } 138 139 buf, err := hex.DecodeString(sigParts[1]) 140 if err != nil { 141 return nil, nil, fmt.Errorf("error decoding signature %q: %v", signature, err) 142 } 143 return buf, hashFunc, nil 144 } 145 146 // ValidatePayload validates an incoming GitHub Webhook event request body 147 // and returns the (JSON) payload. 148 // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded". 149 // If the Content-Type is neither then an error is returned. 150 // secretToken is the GitHub Webhook secret token. 151 // If your webhook does not contain a secret token, you can pass nil or an empty slice. 152 // This is intended for local development purposes only and all webhooks should ideally set up a secret token. 153 // 154 // Example usage: 155 // 156 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 157 // // read signature from request 158 // signature := "" 159 // payload, err := github.ValidatePayloadFromBody(r.Header.Get("Content-Type"), r.Body, signature, s.webhookSecretKey) 160 // if err != nil { ... } 161 // // Process payload... 162 // } 163 func ValidatePayloadFromBody(contentType string, readable io.Reader, signature string, secretToken []byte) (payload []byte, err error) { 164 var body []byte // Raw body that GitHub uses to calculate the signature. 165 166 switch contentType { 167 case "application/json": 168 var err error 169 if body, err = ioutil.ReadAll(readable); err != nil { 170 return nil, err 171 } 172 173 // If the content type is application/json, 174 // the JSON payload is just the original body. 175 payload = body 176 177 case "application/x-www-form-urlencoded": 178 // payloadFormParam is the name of the form parameter that the JSON payload 179 // will be in if a webhook has its content type set to application/x-www-form-urlencoded. 180 const payloadFormParam = "payload" 181 182 var err error 183 if body, err = ioutil.ReadAll(readable); err != nil { 184 return nil, err 185 } 186 187 // If the content type is application/x-www-form-urlencoded, 188 // the JSON payload will be under the "payload" form param. 189 form, err := url.ParseQuery(string(body)) 190 if err != nil { 191 return nil, err 192 } 193 payload = []byte(form.Get(payloadFormParam)) 194 195 default: 196 return nil, fmt.Errorf("webhook request has unsupported Content-Type %q", contentType) 197 } 198 199 // Only validate the signature if a secret token exists. This is intended for 200 // local development only and all webhooks should ideally set up a secret token. 201 if len(secretToken) > 0 { 202 if err := ValidateSignature(signature, body, secretToken); err != nil { 203 return nil, err 204 } 205 } 206 207 return payload, nil 208 } 209 210 // ValidatePayload validates an incoming GitHub Webhook event request 211 // and returns the (JSON) payload. 212 // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded". 213 // If the Content-Type is neither then an error is returned. 214 // secretToken is the GitHub Webhook secret token. 215 // If your webhook does not contain a secret token, you can pass nil or an empty slice. 216 // This is intended for local development purposes only and all webhooks should ideally set up a secret token. 217 // 218 // Example usage: 219 // 220 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 221 // payload, err := github.ValidatePayload(r, s.webhookSecretKey) 222 // if err != nil { ... } 223 // // Process payload... 224 // } 225 // 226 func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err error) { 227 signature := r.Header.Get(SHA256SignatureHeader) 228 if signature == "" { 229 signature = r.Header.Get(SHA1SignatureHeader) 230 } 231 232 contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) 233 if err != nil { 234 return nil, err 235 } 236 237 return ValidatePayloadFromBody(contentType, r.Body, signature, secretToken) 238 } 239 240 // ValidateSignature validates the signature for the given payload. 241 // signature is the GitHub hash signature delivered in the X-Hub-Signature header. 242 // payload is the JSON payload sent by GitHub Webhooks. 243 // secretToken is the GitHub Webhook secret token. 244 // 245 // GitHub API docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github 246 func ValidateSignature(signature string, payload, secretToken []byte) error { 247 messageMAC, hashFunc, err := messageMAC(signature) 248 if err != nil { 249 return err 250 } 251 if !checkMAC(payload, messageMAC, secretToken, hashFunc) { 252 return errors.New("payload signature check failed") 253 } 254 return nil 255 } 256 257 // WebHookType returns the event type of webhook request r. 258 // 259 // GitHub API docs: https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types 260 func WebHookType(r *http.Request) string { 261 return r.Header.Get(EventTypeHeader) 262 } 263 264 // DeliveryID returns the unique delivery ID of webhook request r. 265 // 266 // GitHub API docs: https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types 267 func DeliveryID(r *http.Request) string { 268 return r.Header.Get(DeliveryIDHeader) 269 } 270 271 // ParseWebHook parses the event payload. For recognized event types, a 272 // value of the corresponding struct type will be returned (as returned 273 // by Event.ParsePayload()). An error will be returned for unrecognized event 274 // types. 275 // 276 // Example usage: 277 // 278 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 279 // payload, err := github.ValidatePayload(r, s.webhookSecretKey) 280 // if err != nil { ... } 281 // event, err := github.ParseWebHook(github.WebHookType(r), payload) 282 // if err != nil { ... } 283 // switch event := event.(type) { 284 // case *github.CommitCommentEvent: 285 // processCommitCommentEvent(event) 286 // case *github.CreateEvent: 287 // processCreateEvent(event) 288 // ... 289 // } 290 // } 291 // 292 func ParseWebHook(messageType string, payload []byte) (interface{}, error) { 293 eventType, ok := eventTypeMapping[messageType] 294 if !ok { 295 return nil, fmt.Errorf("unknown X-Github-Event in message: %v", messageType) 296 } 297 298 event := Event{ 299 Type: &eventType, 300 RawPayload: (*json.RawMessage)(&payload), 301 } 302 return event.ParsePayload() 303 }