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