github.com/google/go-github/v50@v50.2.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 "discussion_comment": "DiscussionCommentEvent", 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 "merge_group": "MergeGroupEvent", 72 "meta": "MetaEvent", 73 "milestone": "MilestoneEvent", 74 "organization": "OrganizationEvent", 75 "org_block": "OrgBlockEvent", 76 "package": "PackageEvent", 77 "page_build": "PageBuildEvent", 78 "ping": "PingEvent", 79 "project": "ProjectEvent", 80 "project_card": "ProjectCardEvent", 81 "project_column": "ProjectColumnEvent", 82 "public": "PublicEvent", 83 "pull_request": "PullRequestEvent", 84 "pull_request_review": "PullRequestReviewEvent", 85 "pull_request_review_comment": "PullRequestReviewCommentEvent", 86 "pull_request_review_thread": "PullRequestReviewThreadEvent", 87 "pull_request_target": "PullRequestTargetEvent", 88 "push": "PushEvent", 89 "repository": "RepositoryEvent", 90 "repository_dispatch": "RepositoryDispatchEvent", 91 "repository_import": "RepositoryImportEvent", 92 "repository_vulnerability_alert": "RepositoryVulnerabilityAlertEvent", 93 "release": "ReleaseEvent", 94 "secret_scanning_alert": "SecretScanningAlertEvent", 95 "star": "StarEvent", 96 "status": "StatusEvent", 97 "team": "TeamEvent", 98 "team_add": "TeamAddEvent", 99 "user": "UserEvent", 100 "watch": "WatchEvent", 101 "workflow_dispatch": "WorkflowDispatchEvent", 102 "workflow_job": "WorkflowJobEvent", 103 "workflow_run": "WorkflowRunEvent", 104 } 105 ) 106 107 // genMAC generates the HMAC signature for a message provided the secret key 108 // and hashFunc. 109 func genMAC(message, key []byte, hashFunc func() hash.Hash) []byte { 110 mac := hmac.New(hashFunc, key) 111 mac.Write(message) 112 return mac.Sum(nil) 113 } 114 115 // checkMAC reports whether messageMAC is a valid HMAC tag for message. 116 func checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool { 117 expectedMAC := genMAC(message, key, hashFunc) 118 return hmac.Equal(messageMAC, expectedMAC) 119 } 120 121 // messageMAC returns the hex-decoded HMAC tag from the signature and its 122 // corresponding hash function. 123 func messageMAC(signature string) ([]byte, func() hash.Hash, error) { 124 if signature == "" { 125 return nil, nil, errors.New("missing signature") 126 } 127 sigParts := strings.SplitN(signature, "=", 2) 128 if len(sigParts) != 2 { 129 return nil, nil, fmt.Errorf("error parsing signature %q", signature) 130 } 131 132 var hashFunc func() hash.Hash 133 switch sigParts[0] { 134 case sha1Prefix: 135 hashFunc = sha1.New 136 case sha256Prefix: 137 hashFunc = sha256.New 138 case sha512Prefix: 139 hashFunc = sha512.New 140 default: 141 return nil, nil, fmt.Errorf("unknown hash type prefix: %q", sigParts[0]) 142 } 143 144 buf, err := hex.DecodeString(sigParts[1]) 145 if err != nil { 146 return nil, nil, fmt.Errorf("error decoding signature %q: %v", signature, err) 147 } 148 return buf, hashFunc, nil 149 } 150 151 // ValidatePayload validates an incoming GitHub Webhook event request body 152 // and returns the (JSON) payload. 153 // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded". 154 // If the Content-Type is neither then an error is returned. 155 // secretToken is the GitHub Webhook secret token. 156 // If your webhook does not contain a secret token, you can pass nil or an empty slice. 157 // This is intended for local development purposes only and all webhooks should ideally set up a secret token. 158 // 159 // Example usage: 160 // 161 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 162 // // read signature from request 163 // signature := "" 164 // payload, err := github.ValidatePayloadFromBody(r.Header.Get("Content-Type"), r.Body, signature, s.webhookSecretKey) 165 // if err != nil { ... } 166 // // Process payload... 167 // } 168 func ValidatePayloadFromBody(contentType string, readable io.Reader, signature string, secretToken []byte) (payload []byte, err error) { 169 var body []byte // Raw body that GitHub uses to calculate the signature. 170 171 switch contentType { 172 case "application/json": 173 var err error 174 if body, err = io.ReadAll(readable); err != nil { 175 return nil, err 176 } 177 178 // If the content type is application/json, 179 // the JSON payload is just the original body. 180 payload = body 181 182 case "application/x-www-form-urlencoded": 183 // payloadFormParam is the name of the form parameter that the JSON payload 184 // will be in if a webhook has its content type set to application/x-www-form-urlencoded. 185 const payloadFormParam = "payload" 186 187 var err error 188 if body, err = io.ReadAll(readable); err != nil { 189 return nil, err 190 } 191 192 // If the content type is application/x-www-form-urlencoded, 193 // the JSON payload will be under the "payload" form param. 194 form, err := url.ParseQuery(string(body)) 195 if err != nil { 196 return nil, err 197 } 198 payload = []byte(form.Get(payloadFormParam)) 199 200 default: 201 return nil, fmt.Errorf("webhook request has unsupported Content-Type %q", contentType) 202 } 203 204 // Only validate the signature if a secret token exists. This is intended for 205 // local development only and all webhooks should ideally set up a secret token. 206 if len(secretToken) > 0 { 207 if err := ValidateSignature(signature, body, secretToken); err != nil { 208 return nil, err 209 } 210 } 211 212 return payload, nil 213 } 214 215 // ValidatePayload validates an incoming GitHub Webhook event request 216 // and returns the (JSON) payload. 217 // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded". 218 // If the Content-Type is neither then an error is returned. 219 // secretToken is the GitHub Webhook secret token. 220 // If your webhook does not contain a secret token, you can pass nil or an empty slice. 221 // This is intended for local development purposes only and all webhooks should ideally set up a secret token. 222 // 223 // Example usage: 224 // 225 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 226 // payload, err := github.ValidatePayload(r, s.webhookSecretKey) 227 // if err != nil { ... } 228 // // Process payload... 229 // } 230 func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err error) { 231 signature := r.Header.Get(SHA256SignatureHeader) 232 if signature == "" { 233 signature = r.Header.Get(SHA1SignatureHeader) 234 } 235 236 contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) 237 if err != nil { 238 return nil, err 239 } 240 241 return ValidatePayloadFromBody(contentType, r.Body, signature, secretToken) 242 } 243 244 // ValidateSignature validates the signature for the given payload. 245 // signature is the GitHub hash signature delivered in the X-Hub-Signature header. 246 // payload is the JSON payload sent by GitHub Webhooks. 247 // secretToken is the GitHub Webhook secret token. 248 // 249 // GitHub API docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github 250 func ValidateSignature(signature string, payload, secretToken []byte) error { 251 messageMAC, hashFunc, err := messageMAC(signature) 252 if err != nil { 253 return err 254 } 255 if !checkMAC(payload, messageMAC, secretToken, hashFunc) { 256 return errors.New("payload signature check failed") 257 } 258 return nil 259 } 260 261 // WebHookType returns the event type of webhook request r. 262 // 263 // GitHub API docs: https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types 264 func WebHookType(r *http.Request) string { 265 return r.Header.Get(EventTypeHeader) 266 } 267 268 // DeliveryID returns the unique delivery ID of webhook request r. 269 // 270 // GitHub API docs: https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types 271 func DeliveryID(r *http.Request) string { 272 return r.Header.Get(DeliveryIDHeader) 273 } 274 275 // ParseWebHook parses the event payload. For recognized event types, a 276 // value of the corresponding struct type will be returned (as returned 277 // by Event.ParsePayload()). An error will be returned for unrecognized event 278 // types. 279 // 280 // Example usage: 281 // 282 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 283 // payload, err := github.ValidatePayload(r, s.webhookSecretKey) 284 // if err != nil { ... } 285 // event, err := github.ParseWebHook(github.WebHookType(r), payload) 286 // if err != nil { ... } 287 // switch event := event.(type) { 288 // case *github.CommitCommentEvent: 289 // processCommitCommentEvent(event) 290 // case *github.CreateEvent: 291 // processCreateEvent(event) 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 }