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