github.com/google/go-github/v68@v68.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]interface{}{ 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 "repository": &RepositoryEvent{}, 99 "repository_dispatch": &RepositoryDispatchEvent{}, 100 "repository_import": &RepositoryImportEvent{}, 101 "repository_ruleset": &RepositoryRulesetEvent{}, 102 "repository_vulnerability_alert": &RepositoryVulnerabilityAlertEvent{}, 103 "release": &ReleaseEvent{}, 104 "secret_scanning_alert": &SecretScanningAlertEvent{}, 105 "secret_scanning_alert_location": &SecretScanningAlertLocationEvent{}, 106 "security_advisory": &SecurityAdvisoryEvent{}, 107 "security_and_analysis": &SecurityAndAnalysisEvent{}, 108 "sponsorship": &SponsorshipEvent{}, 109 "star": &StarEvent{}, 110 "status": &StatusEvent{}, 111 "team": &TeamEvent{}, 112 "team_add": &TeamAddEvent{}, 113 "user": &UserEvent{}, 114 "watch": &WatchEvent{}, 115 "workflow_dispatch": &WorkflowDispatchEvent{}, 116 "workflow_job": &WorkflowJobEvent{}, 117 "workflow_run": &WorkflowRunEvent{}, 118 } 119 // Forward mapping of event types to the string names of the structs. 120 messageToTypeName = make(map[string]string, len(eventTypeMapping)) 121 // Inverse map of the above. 122 typeToMessageMapping = make(map[string]string, len(eventTypeMapping)) 123 ) 124 125 func init() { 126 for k, v := range eventTypeMapping { 127 typename := reflect.TypeOf(v).Elem().Name() 128 messageToTypeName[k] = typename 129 typeToMessageMapping[typename] = k 130 } 131 } 132 133 // genMAC generates the HMAC signature for a message provided the secret key 134 // and hashFunc. 135 func genMAC(message, key []byte, hashFunc func() hash.Hash) []byte { 136 mac := hmac.New(hashFunc, key) 137 mac.Write(message) 138 return mac.Sum(nil) 139 } 140 141 // checkMAC reports whether messageMAC is a valid HMAC tag for message. 142 func checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool { 143 expectedMAC := genMAC(message, key, hashFunc) 144 return hmac.Equal(messageMAC, expectedMAC) 145 } 146 147 // messageMAC returns the hex-decoded HMAC tag from the signature and its 148 // corresponding hash function. 149 func messageMAC(signature string) ([]byte, func() hash.Hash, error) { 150 if signature == "" { 151 return nil, nil, errors.New("missing signature") 152 } 153 sigParts := strings.SplitN(signature, "=", 2) 154 if len(sigParts) != 2 { 155 return nil, nil, fmt.Errorf("error parsing signature %q", signature) 156 } 157 158 var hashFunc func() hash.Hash 159 switch sigParts[0] { 160 case sha1Prefix: 161 hashFunc = sha1.New 162 case sha256Prefix: 163 hashFunc = sha256.New 164 case sha512Prefix: 165 hashFunc = sha512.New 166 default: 167 return nil, nil, fmt.Errorf("unknown hash type prefix: %q", sigParts[0]) 168 } 169 170 buf, err := hex.DecodeString(sigParts[1]) 171 if err != nil { 172 return nil, nil, fmt.Errorf("error decoding signature %q: %v", signature, err) 173 } 174 return buf, hashFunc, nil 175 } 176 177 // ValidatePayloadFromBody validates an incoming GitHub Webhook event request body 178 // and returns the (JSON) payload. 179 // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded". 180 // If the Content-Type is neither then an error is returned. 181 // secretToken is the GitHub Webhook secret token. 182 // If your webhook does not contain a secret token, you can pass an empty secretToken. 183 // Webhooks without a secret token are not secure and should be avoided. 184 // 185 // Example usage: 186 // 187 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 188 // // read signature from request 189 // signature := "" 190 // payload, err := github.ValidatePayloadFromBody(r.Header.Get("Content-Type"), r.Body, signature, s.webhookSecretKey) 191 // if err != nil { ... } 192 // // Process payload... 193 // } 194 func ValidatePayloadFromBody(contentType string, readable io.Reader, signature string, secretToken []byte) (payload []byte, err error) { 195 var body []byte // Raw body that GitHub uses to calculate the signature. 196 197 switch contentType { 198 case "application/json": 199 var err error 200 if body, err = io.ReadAll(readable); err != nil { 201 return nil, err 202 } 203 204 // If the content type is application/json, 205 // the JSON payload is just the original body. 206 payload = body 207 208 case "application/x-www-form-urlencoded": 209 // payloadFormParam is the name of the form parameter that the JSON payload 210 // will be in if a webhook has its content type set to application/x-www-form-urlencoded. 211 const payloadFormParam = "payload" 212 213 var err error 214 if body, err = io.ReadAll(readable); err != nil { 215 return nil, err 216 } 217 218 // If the content type is application/x-www-form-urlencoded, 219 // the JSON payload will be under the "payload" form param. 220 form, err := url.ParseQuery(string(body)) 221 if err != nil { 222 return nil, err 223 } 224 payload = []byte(form.Get(payloadFormParam)) 225 226 default: 227 return nil, fmt.Errorf("webhook request has unsupported Content-Type %q", contentType) 228 } 229 230 // Validate the signature if present or if one is expected (secretToken is non-empty). 231 if len(secretToken) > 0 || len(signature) > 0 { 232 if err := ValidateSignature(signature, body, secretToken); err != nil { 233 return nil, err 234 } 235 } 236 237 return payload, nil 238 } 239 240 // ValidatePayload validates an incoming GitHub Webhook event request 241 // and returns the (JSON) payload. 242 // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded". 243 // If the Content-Type is neither then an error is returned. 244 // secretToken is the GitHub Webhook secret token. 245 // If your webhook does not contain a secret token, you can pass nil or an empty slice. 246 // This is intended for local development purposes only and all webhooks should ideally set up a secret token. 247 // 248 // Example usage: 249 // 250 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 251 // payload, err := github.ValidatePayload(r, s.webhookSecretKey) 252 // if err != nil { ... } 253 // // Process payload... 254 // } 255 func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err error) { 256 signature := r.Header.Get(SHA256SignatureHeader) 257 if signature == "" { 258 signature = r.Header.Get(SHA1SignatureHeader) 259 } 260 261 contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) 262 if err != nil { 263 return nil, err 264 } 265 266 return ValidatePayloadFromBody(contentType, r.Body, signature, secretToken) 267 } 268 269 // ValidateSignature validates the signature for the given payload. 270 // signature is the GitHub hash signature delivered in the X-Hub-Signature header. 271 // payload is the JSON payload sent by GitHub Webhooks. 272 // secretToken is the GitHub Webhook secret token. 273 // 274 // GitHub API docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github 275 func ValidateSignature(signature string, payload, secretToken []byte) error { 276 messageMAC, hashFunc, err := messageMAC(signature) 277 if err != nil { 278 return err 279 } 280 if !checkMAC(payload, messageMAC, secretToken, hashFunc) { 281 return errors.New("payload signature check failed") 282 } 283 return nil 284 } 285 286 // WebHookType returns the event type of webhook request r. 287 // 288 // GitHub API docs: https://docs.github.com/developers/webhooks-and-events/events/github-event-types 289 func WebHookType(r *http.Request) string { 290 return r.Header.Get(EventTypeHeader) 291 } 292 293 // DeliveryID returns the unique delivery ID of webhook request r. 294 // 295 // GitHub API docs: https://docs.github.com/developers/webhooks-and-events/events/github-event-types 296 func DeliveryID(r *http.Request) string { 297 return r.Header.Get(DeliveryIDHeader) 298 } 299 300 // ParseWebHook parses the event payload. For recognized event types, a 301 // value of the corresponding struct type will be returned (as returned 302 // by Event.ParsePayload()). An error will be returned for unrecognized event 303 // types. 304 // 305 // Example usage: 306 // 307 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 308 // payload, err := github.ValidatePayload(r, s.webhookSecretKey) 309 // if err != nil { ... } 310 // event, err := github.ParseWebHook(github.WebHookType(r), payload) 311 // if err != nil { ... } 312 // switch event := event.(type) { 313 // case *github.CommitCommentEvent: 314 // processCommitCommentEvent(event) 315 // case *github.CreateEvent: 316 // processCreateEvent(event) 317 // ... 318 // } 319 // } 320 func ParseWebHook(messageType string, payload []byte) (interface{}, error) { 321 eventType, ok := messageToTypeName[messageType] 322 if !ok { 323 return nil, fmt.Errorf("unknown X-Github-Event in message: %v", messageType) 324 } 325 326 event := Event{ 327 Type: &eventType, 328 RawPayload: (*json.RawMessage)(&payload), 329 } 330 return event.ParsePayload() 331 } 332 333 // MessageTypes returns a sorted list of all the known GitHub event type strings 334 // supported by go-github. 335 func MessageTypes() []string { 336 types := make([]string, 0, len(eventTypeMapping)) 337 for t := range eventTypeMapping { 338 types = append(types, t) 339 } 340 sort.Strings(types) 341 return types 342 } 343 344 // EventForType returns an empty struct matching the specified GitHub event type. 345 // If messageType does not match any known event types, it returns nil. 346 func EventForType(messageType string) interface{} { 347 prototype := eventTypeMapping[messageType] 348 if prototype == nil { 349 return nil 350 } 351 // return a _copy_ of the pointed-to-object. Unfortunately, for this we 352 // need to use reflection. If we store the actual objects in the map, 353 // we still need to use reflection to convert from `any` to the actual 354 // type, so this was deemed the lesser of two evils. (#2865) 355 return reflect.New(reflect.TypeOf(prototype).Elem()).Interface() 356 }