github.com/google/go-github/v33@v33.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/ioutil" 22 "net/http" 23 "net/url" 24 "strings" 25 ) 26 27 const ( 28 // sha1Prefix is the prefix used by GitHub before the HMAC hexdigest. 29 sha1Prefix = "sha1" 30 // sha256Prefix and sha512Prefix are provided for future compatibility. 31 sha256Prefix = "sha256" 32 sha512Prefix = "sha512" 33 // signatureHeader is the GitHub header key used to pass the HMAC hexdigest. 34 signatureHeader = "X-Hub-Signature" 35 // eventTypeHeader is the GitHub header key used to pass the event type. 36 eventTypeHeader = "X-Github-Event" 37 // deliveryIDHeader is the GitHub header key used to pass the unique ID for the webhook event. 38 deliveryIDHeader = "X-Github-Delivery" 39 ) 40 41 var ( 42 // eventTypeMapping maps webhooks types to their corresponding go-github struct types. 43 eventTypeMapping = map[string]string{ 44 "check_run": "CheckRunEvent", 45 "check_suite": "CheckSuiteEvent", 46 "commit_comment": "CommitCommentEvent", 47 "content_reference": "ContentReferenceEvent", 48 "create": "CreateEvent", 49 "delete": "DeleteEvent", 50 "deploy_key": "DeployKeyEvent", 51 "deployment": "DeploymentEvent", 52 "deployment_status": "DeploymentStatusEvent", 53 "fork": "ForkEvent", 54 "github_app_authorization": "GitHubAppAuthorizationEvent", 55 "gollum": "GollumEvent", 56 "installation": "InstallationEvent", 57 "installation_repositories": "InstallationRepositoriesEvent", 58 "issue_comment": "IssueCommentEvent", 59 "issues": "IssuesEvent", 60 "label": "LabelEvent", 61 "marketplace_purchase": "MarketplacePurchaseEvent", 62 "member": "MemberEvent", 63 "membership": "MembershipEvent", 64 "meta": "MetaEvent", 65 "milestone": "MilestoneEvent", 66 "organization": "OrganizationEvent", 67 "org_block": "OrgBlockEvent", 68 "package": "PackageEvent", 69 "page_build": "PageBuildEvent", 70 "ping": "PingEvent", 71 "project": "ProjectEvent", 72 "project_card": "ProjectCardEvent", 73 "project_column": "ProjectColumnEvent", 74 "public": "PublicEvent", 75 "pull_request_review": "PullRequestReviewEvent", 76 "pull_request_review_comment": "PullRequestReviewCommentEvent", 77 "pull_request": "PullRequestEvent", 78 "push": "PushEvent", 79 "repository": "RepositoryEvent", 80 "repository_dispatch": "RepositoryDispatchEvent", 81 "repository_vulnerability_alert": "RepositoryVulnerabilityAlertEvent", 82 "release": "ReleaseEvent", 83 "star": "StarEvent", 84 "status": "StatusEvent", 85 "team": "TeamEvent", 86 "team_add": "TeamAddEvent", 87 "user": "UserEvent", 88 "watch": "WatchEvent", 89 "workflow_dispatch": "WorkflowDispatchEvent", 90 "workflow_run": "WorkflowRunEvent", 91 } 92 ) 93 94 // genMAC generates the HMAC signature for a message provided the secret key 95 // and hashFunc. 96 func genMAC(message, key []byte, hashFunc func() hash.Hash) []byte { 97 mac := hmac.New(hashFunc, key) 98 mac.Write(message) 99 return mac.Sum(nil) 100 } 101 102 // checkMAC reports whether messageMAC is a valid HMAC tag for message. 103 func checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool { 104 expectedMAC := genMAC(message, key, hashFunc) 105 return hmac.Equal(messageMAC, expectedMAC) 106 } 107 108 // messageMAC returns the hex-decoded HMAC tag from the signature and its 109 // corresponding hash function. 110 func messageMAC(signature string) ([]byte, func() hash.Hash, error) { 111 if signature == "" { 112 return nil, nil, errors.New("missing signature") 113 } 114 sigParts := strings.SplitN(signature, "=", 2) 115 if len(sigParts) != 2 { 116 return nil, nil, fmt.Errorf("error parsing signature %q", signature) 117 } 118 119 var hashFunc func() hash.Hash 120 switch sigParts[0] { 121 case sha1Prefix: 122 hashFunc = sha1.New 123 case sha256Prefix: 124 hashFunc = sha256.New 125 case sha512Prefix: 126 hashFunc = sha512.New 127 default: 128 return nil, nil, fmt.Errorf("unknown hash type prefix: %q", sigParts[0]) 129 } 130 131 buf, err := hex.DecodeString(sigParts[1]) 132 if err != nil { 133 return nil, nil, fmt.Errorf("error decoding signature %q: %v", signature, err) 134 } 135 return buf, hashFunc, nil 136 } 137 138 // ValidatePayload validates an incoming GitHub Webhook event request 139 // and returns the (JSON) payload. 140 // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded". 141 // If the Content-Type is neither then an error is returned. 142 // secretToken is the GitHub Webhook secret token. 143 // If your webhook does not contain a secret token, you can pass nil or an empty slice. 144 // This is intended for local development purposes only and all webhooks should ideally set up a secret token. 145 // 146 // Example usage: 147 // 148 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 149 // payload, err := github.ValidatePayload(r, s.webhookSecretKey) 150 // if err != nil { ... } 151 // // Process payload... 152 // } 153 // 154 func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err error) { 155 var body []byte // Raw body that GitHub uses to calculate the signature. 156 157 switch ct := r.Header.Get("Content-Type"); ct { 158 case "application/json": 159 var err error 160 if body, err = ioutil.ReadAll(r.Body); err != nil { 161 return nil, err 162 } 163 164 // If the content type is application/json, 165 // the JSON payload is just the original body. 166 payload = body 167 168 case "application/x-www-form-urlencoded": 169 // payloadFormParam is the name of the form parameter that the JSON payload 170 // will be in if a webhook has its content type set to application/x-www-form-urlencoded. 171 const payloadFormParam = "payload" 172 173 var err error 174 if body, err = ioutil.ReadAll(r.Body); err != nil { 175 return nil, err 176 } 177 178 // If the content type is application/x-www-form-urlencoded, 179 // the JSON payload will be under the "payload" form param. 180 form, err := url.ParseQuery(string(body)) 181 if err != nil { 182 return nil, err 183 } 184 payload = []byte(form.Get(payloadFormParam)) 185 186 default: 187 return nil, fmt.Errorf("Webhook request has unsupported Content-Type %q", ct) 188 } 189 190 // Only validate the signature if a secret token exists. This is intended for 191 // local development only and all webhooks should ideally set up a secret token. 192 if len(secretToken) > 0 { 193 sig := r.Header.Get(signatureHeader) 194 if err := ValidateSignature(sig, body, secretToken); err != nil { 195 return nil, err 196 } 197 } 198 199 return payload, nil 200 } 201 202 // ValidateSignature validates the signature for the given payload. 203 // signature is the GitHub hash signature delivered in the X-Hub-Signature header. 204 // payload is the JSON payload sent by GitHub Webhooks. 205 // secretToken is the GitHub Webhook secret token. 206 // 207 // GitHub API docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github 208 func ValidateSignature(signature string, payload, secretToken []byte) error { 209 messageMAC, hashFunc, err := messageMAC(signature) 210 if err != nil { 211 return err 212 } 213 if !checkMAC(payload, messageMAC, secretToken, hashFunc) { 214 return errors.New("payload signature check failed") 215 } 216 return nil 217 } 218 219 // WebHookType returns the event type of webhook request r. 220 // 221 // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/hooks/#webhook-headers 222 func WebHookType(r *http.Request) string { 223 return r.Header.Get(eventTypeHeader) 224 } 225 226 // DeliveryID returns the unique delivery ID of webhook request r. 227 // 228 // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/hooks/#webhook-headers 229 func DeliveryID(r *http.Request) string { 230 return r.Header.Get(deliveryIDHeader) 231 } 232 233 // ParseWebHook parses the event payload. For recognized event types, a 234 // value of the corresponding struct type will be returned (as returned 235 // by Event.ParsePayload()). An error will be returned for unrecognized event 236 // types. 237 // 238 // Example usage: 239 // 240 // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 241 // payload, err := github.ValidatePayload(r, s.webhookSecretKey) 242 // if err != nil { ... } 243 // event, err := github.ParseWebHook(github.WebHookType(r), payload) 244 // if err != nil { ... } 245 // switch event := event.(type) { 246 // case *github.CommitCommentEvent: 247 // processCommitCommentEvent(event) 248 // case *github.CreateEvent: 249 // processCreateEvent(event) 250 // ... 251 // } 252 // } 253 // 254 func ParseWebHook(messageType string, payload []byte) (interface{}, error) { 255 eventType, ok := eventTypeMapping[messageType] 256 if !ok { 257 return nil, fmt.Errorf("unknown X-Github-Event in message: %v", messageType) 258 } 259 260 event := Event{ 261 Type: &eventType, 262 RawPayload: (*json.RawMessage)(&payload), 263 } 264 return event.ParsePayload() 265 }