You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

messages.go 8.5KB

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