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