messages.go 8.3 KB

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