github.com/google/go-github/v49@v49.1.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 "mime" 23 "net/http" 24 "net/url" 25 "strings" 26 ) 27 28 const ( 29 // sha1Prefix is the prefix used by GitHub before the HMAC hexdigest. 30 sha1Prefix = "sha1" 31 // sha256Prefix and sha512Prefix are provided for future compatibility. 32 sha256Prefix = "sha256" 33 sha512Prefix = "sha512" 34 // SHA1SignatureHeader is the GitHub header key used to pass the HMAC-SHA1 hexdigest. 35 SHA1SignatureHeader = "X-Hub-Signature" 36 // SHA256SignatureHeader is the GitHub header key used to pass the HMAC-SHA256 hexdigest. 37 SHA256SignatureHeader = "X-Hub-Signature-256" 38 // EventTypeHeader is the GitHub header key used to pass the event type. 39 EventTypeHeader = "X-Github-Event" 40 // DeliveryIDHeader is the GitHub header key used to pass the unique ID for the webhook event. 41 DeliveryIDHeader = "X-Github-Delivery" 42 ) 43 44 var ( 45 // eventTypeMapping maps webhooks types to their corresponding go-github struct types. 46 eventTypeMapping = map[string]string{ 47 "branch_protection_rule": "BranchProtectionRuleEvent", 48 "check_run": "CheckRunEvent", 49 "check_suite": "CheckSuiteEvent", 50 "code_scanning_alert": "CodeScanningAlertEvent", 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 "merge_group": "MergeGroupEvent", 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 // genMAC generates the HMAC signature for a message provided the secret key 107 // and hashFunc. 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 // checkMAC reports whether messageMAC is a valid HMAC tag for message. 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 // messageMAC returns the hex-decoded HMAC tag from the signature and its 121 // corresponding hash function. 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 // ValidatePayload validates an incoming GitHub Webhook event request body 151 // and returns the (JSON) payload. 152 // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded". 153 // If the Content-Type is neither then an error is returned. 154 // secretToken is the GitHub Webhook secret token. 155 // If your webhook does not contain a secret token, you can pass nil or an empty slice. 156 // This is intended for local development purposes only and all webhooks should ideally set up a secret token. 157 // 158 // Example usage: 159 // 160 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 161 // // read signature from request 162 // signature := "" 163 // payload, err := github.ValidatePayloadFromBody(r.Header.Get("Content-Type"), r.Body, signature, s.webhookSecretKey) 164 // if err != nil { ... } 165 // // Process payload... 166 // } 167 func ValidatePayloadFromBody(contentType string, readable io.Reader, signature string, secretToken []byte) (payload []byte, err error) { 168 var body []byte // Raw body that GitHub uses to calculate the signature. 169 170 switch contentType { 171 case "application/json": 172 var err error 173 if body, err = io.ReadAll(readable); err != nil { 174 return nil, err 175 } 176 177 // If the content type is application/json, 178 // the JSON payload is just the original body. 179 payload = body 180 181 case "application/x-www-form-urlencoded": 182 // payloadFormParam is the name of the form parameter that the JSON payload 183 // will be in if a webhook has its content type set to application/x-www-form-urlencoded. 184 const payloadFormParam = "payload" 185 186 var err error 187 if body, err = io.ReadAll(readable); err != nil { 188 return nil, err 189 } 190 191 // If the content type is application/x-www-form-urlencoded, 192 // the JSON payload will be under the "payload" form param. 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 // Only validate the signature if a secret token exists. This is intended for 204 // local development only and all webhooks should ideally set up a secret token. 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 // ValidatePayload validates an incoming GitHub Webhook event request 215 // and returns the (JSON) payload. 216 // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded". 217 // If the Content-Type is neither then an error is returned. 218 // secretToken is the GitHub Webhook secret token. 219 // If your webhook does not contain a secret token, you can pass nil or an empty slice. 220 // This is intended for local development purposes only and all webhooks should ideally set up a secret token. 221 // 222 // Example usage: 223 // 224 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 225 // payload, err := github.ValidatePayload(r, s.webhookSecretKey) 226 // if err != nil { ... } 227 // // Process payload... 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 // ValidateSignature validates the signature for the given payload. 244 // signature is the GitHub hash signature delivered in the X-Hub-Signature header. 245 // payload is the JSON payload sent by GitHub Webhooks. 246 // secretToken is the GitHub Webhook secret token. 247 // 248 // GitHub API docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github 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 // WebHookType returns the event type of webhook request r. 261 // 262 // GitHub API docs: https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types 263 func WebHookType(r *http.Request) string { 264 return r.Header.Get(EventTypeHeader) 265 } 266 267 // DeliveryID returns the unique delivery ID of webhook request r. 268 // 269 // GitHub API docs: https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types 270 func DeliveryID(r *http.Request) string { 271 return r.Header.Get(DeliveryIDHeader) 272 } 273 274 // ParseWebHook parses the event payload. For recognized event types, a 275 // value of the corresponding struct type will be returned (as returned 276 // by Event.ParsePayload()). An error will be returned for unrecognized event 277 // types. 278 // 279 // Example usage: 280 // 281 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 282 // payload, err := github.ValidatePayload(r, s.webhookSecretKey) 283 // if err != nil { ... } 284 // event, err := github.ParseWebHook(github.WebHookType(r), payload) 285 // if err != nil { ... } 286 // switch event := event.(type) { 287 // case *github.CommitCommentEvent: 288 // processCommitCommentEvent(event) 289 // case *github.CreateEvent: 290 // processCreateEvent(event) 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 }