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