github.com/google/go-github/v74@v74.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 "mime" 23 "net/http" 24 "net/url" 25 "reflect" 26 "sort" 27 "strings" 28 ) 29 30 const ( 31 // sha1Prefix is the prefix used by GitHub before the HMAC hexdigest. 32 sha1Prefix = "sha1" 33 // sha256Prefix and sha512Prefix are provided for future compatibility. 34 sha256Prefix = "sha256" 35 sha512Prefix = "sha512" 36 // SHA1SignatureHeader is the GitHub header key used to pass the HMAC-SHA1 hexdigest. 37 SHA1SignatureHeader = "X-Hub-Signature" 38 // SHA256SignatureHeader is the GitHub header key used to pass the HMAC-SHA256 hexdigest. 39 SHA256SignatureHeader = "X-Hub-Signature-256" 40 // EventTypeHeader is the GitHub header key used to pass the event type. 41 EventTypeHeader = "X-Github-Event" 42 // DeliveryIDHeader is the GitHub header key used to pass the unique ID for the webhook event. 43 DeliveryIDHeader = "X-Github-Delivery" 44 ) 45 46 var ( 47 // eventTypeMapping maps webhooks types to their corresponding go-github struct types. 48 eventTypeMapping = map[string]any{ 49 "branch_protection_configuration": &BranchProtectionConfigurationEvent{}, 50 "branch_protection_rule": &BranchProtectionRuleEvent{}, 51 "check_run": &CheckRunEvent{}, 52 "check_suite": &CheckSuiteEvent{}, 53 "code_scanning_alert": &CodeScanningAlertEvent{}, 54 "commit_comment": &CommitCommentEvent{}, 55 "content_reference": &ContentReferenceEvent{}, 56 "create": &CreateEvent{}, 57 "custom_property": &CustomPropertyEvent{}, 58 "custom_property_values": &CustomPropertyValuesEvent{}, 59 "delete": &DeleteEvent{}, 60 "dependabot_alert": &DependabotAlertEvent{}, 61 "deploy_key": &DeployKeyEvent{}, 62 "deployment": &DeploymentEvent{}, 63 "deployment_review": &DeploymentReviewEvent{}, 64 "deployment_status": &DeploymentStatusEvent{}, 65 "deployment_protection_rule": &DeploymentProtectionRuleEvent{}, 66 "discussion": &DiscussionEvent{}, 67 "discussion_comment": &DiscussionCommentEvent{}, 68 "fork": &ForkEvent{}, 69 "github_app_authorization": &GitHubAppAuthorizationEvent{}, 70 "gollum": &GollumEvent{}, 71 "installation": &InstallationEvent{}, 72 "installation_repositories": &InstallationRepositoriesEvent{}, 73 "installation_target": &InstallationTargetEvent{}, 74 "issue_comment": &IssueCommentEvent{}, 75 "issues": &IssuesEvent{}, 76 "label": &LabelEvent{}, 77 "marketplace_purchase": &MarketplacePurchaseEvent{}, 78 "member": &MemberEvent{}, 79 "membership": &MembershipEvent{}, 80 "merge_group": &MergeGroupEvent{}, 81 "meta": &MetaEvent{}, 82 "milestone": &MilestoneEvent{}, 83 "organization": &OrganizationEvent{}, 84 "org_block": &OrgBlockEvent{}, 85 "package": &PackageEvent{}, 86 "page_build": &PageBuildEvent{}, 87 "personal_access_token_request": &PersonalAccessTokenRequestEvent{}, 88 "ping": &PingEvent{}, 89 "projects_v2": &ProjectV2Event{}, 90 "projects_v2_item": &ProjectV2ItemEvent{}, 91 "public": &PublicEvent{}, 92 "pull_request": &PullRequestEvent{}, 93 "pull_request_review": &PullRequestReviewEvent{}, 94 "pull_request_review_comment": &PullRequestReviewCommentEvent{}, 95 "pull_request_review_thread": &PullRequestReviewThreadEvent{}, 96 "pull_request_target": &PullRequestTargetEvent{}, 97 "push": &PushEvent{}, 98 "registry_package": &RegistryPackageEvent{}, 99 "repository": &RepositoryEvent{}, 100 "repository_dispatch": &RepositoryDispatchEvent{}, 101 "repository_import": &RepositoryImportEvent{}, 102 "repository_ruleset": &RepositoryRulesetEvent{}, 103 "repository_vulnerability_alert": &RepositoryVulnerabilityAlertEvent{}, 104 "release": &ReleaseEvent{}, 105 "secret_scanning_alert": &SecretScanningAlertEvent{}, 106 "secret_scanning_alert_location": &SecretScanningAlertLocationEvent{}, 107 "security_advisory": &SecurityAdvisoryEvent{}, 108 "security_and_analysis": &SecurityAndAnalysisEvent{}, 109 "sponsorship": &SponsorshipEvent{}, 110 "star": &StarEvent{}, 111 "status": &StatusEvent{}, 112 "team": &TeamEvent{}, 113 "team_add": &TeamAddEvent{}, 114 "user": &UserEvent{}, 115 "watch": &WatchEvent{}, 116 "workflow_dispatch": &WorkflowDispatchEvent{}, 117 "workflow_job": &WorkflowJobEvent{}, 118 "workflow_run": &WorkflowRunEvent{}, 119 } 120 // Forward mapping of event types to the string names of the structs. 121 messageToTypeName = make(map[string]string, len(eventTypeMapping)) 122 // Inverse map of the above. 123 typeToMessageMapping = make(map[string]string, len(eventTypeMapping)) 124 ) 125 126 func init() { 127 for k, v := range eventTypeMapping { 128 typename := reflect.TypeOf(v).Elem().Name() 129 messageToTypeName[k] = typename 130 typeToMessageMapping[typename] = k 131 } 132 } 133 134 // genMAC generates the HMAC signature for a message provided the secret key 135 // and hashFunc. 136 func genMAC(message, key []byte, hashFunc func() hash.Hash) []byte { 137 mac := hmac.New(hashFunc, key) 138 mac.Write(message) 139 return mac.Sum(nil) 140 } 141 142 // checkMAC reports whether messageMAC is a valid HMAC tag for message. 143 func checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool { 144 expectedMAC := genMAC(message, key, hashFunc) 145 return hmac.Equal(messageMAC, expectedMAC) 146 } 147 148 // messageMAC returns the hex-decoded HMAC tag from the signature and its 149 // corresponding hash function. 150 func messageMAC(signature string) ([]byte, func() hash.Hash, error) { 151 if signature == "" { 152 return nil, nil, errors.New("missing signature") 153 } 154 sigParts := strings.SplitN(signature, "=", 2) 155 if len(sigParts) != 2 { 156 return nil, nil, fmt.Errorf("error parsing signature %q", signature) 157 } 158 159 var hashFunc func() hash.Hash 160 switch sigParts[0] { 161 case sha1Prefix: 162 hashFunc = sha1.New 163 case sha256Prefix: 164 hashFunc = sha256.New 165 case sha512Prefix: 166 hashFunc = sha512.New 167 default: 168 return nil, nil, fmt.Errorf("unknown hash type prefix: %q", sigParts[0]) 169 } 170 171 buf, err := hex.DecodeString(sigParts[1]) 172 if err != nil { 173 return nil, nil, fmt.Errorf("error decoding signature %q: %v", signature, err) 174 } 175 return buf, hashFunc, nil 176 } 177 178 // ValidatePayloadFromBody validates an incoming GitHub Webhook event request body 179 // and returns the (JSON) payload. 180 // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded". 181 // If the Content-Type is neither then an error is returned. 182 // secretToken is the GitHub Webhook secret token. 183 // If your webhook does not contain a secret token, you can pass an empty secretToken. 184 // Webhooks without a secret token are not secure and should be avoided. 185 // 186 // Example usage: 187 // 188 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 189 // // read signature from request 190 // signature := "" 191 // payload, err := github.ValidatePayloadFromBody(r.Header.Get("Content-Type"), r.Body, signature, s.webhookSecretKey) 192 // if err != nil { ... } 193 // // Process payload... 194 // } 195 func ValidatePayloadFromBody(contentType string, readable io.Reader, signature string, secretToken []byte) (payload []byte, err error) { 196 var body []byte // Raw body that GitHub uses to calculate the signature. 197 198 switch contentType { 199 case "application/json": 200 var err error 201 if body, err = io.ReadAll(readable); err != nil { 202 return nil, err 203 } 204 205 // If the content type is application/json, 206 // the JSON payload is just the original body. 207 payload = body 208 209 case "application/x-www-form-urlencoded": 210 // payloadFormParam is the name of the form parameter that the JSON payload 211 // will be in if a webhook has its content type set to application/x-www-form-urlencoded. 212 const payloadFormParam = "payload" 213 214 var err error 215 if body, err = io.ReadAll(readable); err != nil { 216 return nil, err 217 } 218 219 // If the content type is application/x-www-form-urlencoded, 220 // the JSON payload will be under the "payload" form param. 221 form, err := url.ParseQuery(string(body)) 222 if err != nil { 223 return nil, err 224 } 225 payload = []byte(form.Get(payloadFormParam)) 226 227 default: 228 return nil, fmt.Errorf("webhook request has unsupported Content-Type %q", contentType) 229 } 230 231 // Validate the signature if present or if one is expected (secretToken is non-empty). 232 if len(secretToken) > 0 || len(signature) > 0 { 233 if err := ValidateSignature(signature, body, secretToken); err != nil { 234 return nil, err 235 } 236 } 237 238 return payload, nil 239 } 240 241 // ValidatePayload validates an incoming GitHub Webhook event request 242 // and returns the (JSON) payload. 243 // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded". 244 // If the Content-Type is neither then an error is returned. 245 // secretToken is the GitHub Webhook secret token. 246 // If your webhook does not contain a secret token, you can pass nil or an empty slice. 247 // This is intended for local development purposes only and all webhooks should ideally set up a secret token. 248 // 249 // Example usage: 250 // 251 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 252 // payload, err := github.ValidatePayload(r, s.webhookSecretKey) 253 // if err != nil { ... } 254 // // Process payload... 255 // } 256 func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err error) { 257 signature := r.Header.Get(SHA256SignatureHeader) 258 if signature == "" { 259 signature = r.Header.Get(SHA1SignatureHeader) 260 } 261 262 contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) 263 if err != nil { 264 return nil, err 265 } 266 267 return ValidatePayloadFromBody(contentType, r.Body, signature, secretToken) 268 } 269 270 // ValidateSignature validates the signature for the given payload. 271 // signature is the GitHub hash signature delivered in the X-Hub-Signature header. 272 // payload is the JSON payload sent by GitHub Webhooks. 273 // secretToken is the GitHub Webhook secret token. 274 // 275 // GitHub API docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github 276 func ValidateSignature(signature string, payload, secretToken []byte) error { 277 messageMAC, hashFunc, err := messageMAC(signature) 278 if err != nil { 279 return err 280 } 281 if !checkMAC(payload, messageMAC, secretToken, hashFunc) { 282 return errors.New("payload signature check failed") 283 } 284 return nil 285 } 286 287 // WebHookType returns the event type of webhook request r. 288 // 289 // GitHub API docs: https://docs.github.com/developers/webhooks-and-events/events/github-event-types 290 func WebHookType(r *http.Request) string { 291 return r.Header.Get(EventTypeHeader) 292 } 293 294 // DeliveryID returns the unique delivery ID of webhook request r. 295 // 296 // GitHub API docs: https://docs.github.com/developers/webhooks-and-events/events/github-event-types 297 func DeliveryID(r *http.Request) string { 298 return r.Header.Get(DeliveryIDHeader) 299 } 300 301 // ParseWebHook parses the event payload. For recognized event types, a 302 // value of the corresponding struct type will be returned (as returned 303 // by [Event.ParsePayload]). An error will be returned for unrecognized event 304 // types. 305 // 306 // Example usage: 307 // 308 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 309 // payload, err := github.ValidatePayload(r, s.webhookSecretKey) 310 // if err != nil { ... } 311 // event, err := github.ParseWebHook(github.WebHookType(r), payload) 312 // if err != nil { ... } 313 // switch event := event.(type) { 314 // case *github.CommitCommentEvent: 315 // processCommitCommentEvent(event) 316 // case *github.CreateEvent: 317 // processCreateEvent(event) 318 // ... 319 // } 320 // } 321 func ParseWebHook(messageType string, payload []byte) (any, error) { 322 eventType, ok := messageToTypeName[messageType] 323 if !ok { 324 return nil, fmt.Errorf("unknown X-Github-Event in message: %v", messageType) 325 } 326 327 event := Event{ 328 Type: &eventType, 329 RawPayload: (*json.RawMessage)(&payload), 330 } 331 return event.ParsePayload() 332 } 333 334 // MessageTypes returns a sorted list of all the known GitHub event type strings 335 // supported by go-github. 336 func MessageTypes() []string { 337 types := make([]string, 0, len(eventTypeMapping)) 338 for t := range eventTypeMapping { 339 types = append(types, t) 340 } 341 sort.Strings(types) 342 return types 343 } 344 345 // EventForType returns an empty struct matching the specified GitHub event type. 346 // If messageType does not match any known event types, it returns nil. 347 func EventForType(messageType string) any { 348 prototype := eventTypeMapping[messageType] 349 if prototype == nil { 350 return nil 351 } 352 // return a _copy_ of the pointed-to-object. Unfortunately, for this we 353 // need to use reflection. If we store the actual objects in the map, 354 // we still need to use reflection to convert from `any` to the actual 355 // type, so this was deemed the lesser of two evils. (#2865) 356 return reflect.New(reflect.TypeOf(prototype).Elem()).Interface() 357 }