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