123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- // Copyright 2016 The go-github AUTHORS. All rights reserved.
- //
- // Use of this source code is governed by a BSD-style
- // license that can be found in the LICENSE file.
- // This file provides functions for validating payloads from GitHub Webhooks.
- // GitHub API docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github
- package github
- import (
- "crypto/hmac"
- "crypto/sha1"
- "crypto/sha256"
- "crypto/sha512"
- "encoding/hex"
- "encoding/json"
- "errors"
- "fmt"
- "hash"
- "io/ioutil"
- "net/http"
- "net/url"
- "strings"
- )
- const (
- // sha1Prefix is the prefix used by GitHub before the HMAC hexdigest.
- sha1Prefix = "sha1"
- // sha256Prefix and sha512Prefix are provided for future compatibility.
- sha256Prefix = "sha256"
- sha512Prefix = "sha512"
- // signatureHeader is the GitHub header key used to pass the HMAC hexdigest.
- signatureHeader = "X-Hub-Signature"
- // eventTypeHeader is the GitHub header key used to pass the event type.
- eventTypeHeader = "X-Github-Event"
- // deliveryIDHeader is the GitHub header key used to pass the unique ID for the webhook event.
- deliveryIDHeader = "X-Github-Delivery"
- )
- var (
- // eventTypeMapping maps webhooks types to their corresponding go-github struct types.
- eventTypeMapping = map[string]string{
- "commit_comment": "CommitCommentEvent",
- "create": "CreateEvent",
- "delete": "DeleteEvent",
- "deployment": "DeploymentEvent",
- "deployment_status": "DeploymentStatusEvent",
- "fork": "ForkEvent",
- "gollum": "GollumEvent",
- "installation": "InstallationEvent",
- "installation_repositories": "InstallationRepositoriesEvent",
- "issue_comment": "IssueCommentEvent",
- "issues": "IssuesEvent",
- "label": "LabelEvent",
- "marketplace_purchase": "MarketplacePurchaseEvent",
- "member": "MemberEvent",
- "membership": "MembershipEvent",
- "milestone": "MilestoneEvent",
- "organization": "OrganizationEvent",
- "org_block": "OrgBlockEvent",
- "page_build": "PageBuildEvent",
- "ping": "PingEvent",
- "project": "ProjectEvent",
- "project_card": "ProjectCardEvent",
- "project_column": "ProjectColumnEvent",
- "public": "PublicEvent",
- "pull_request_review": "PullRequestReviewEvent",
- "pull_request_review_comment": "PullRequestReviewCommentEvent",
- "pull_request": "PullRequestEvent",
- "push": "PushEvent",
- "repository": "RepositoryEvent",
- "release": "ReleaseEvent",
- "status": "StatusEvent",
- "team": "TeamEvent",
- "team_add": "TeamAddEvent",
- "watch": "WatchEvent",
- }
- )
- // genMAC generates the HMAC signature for a message provided the secret key
- // and hashFunc.
- func genMAC(message, key []byte, hashFunc func() hash.Hash) []byte {
- mac := hmac.New(hashFunc, key)
- mac.Write(message)
- return mac.Sum(nil)
- }
- // checkMAC reports whether messageMAC is a valid HMAC tag for message.
- func checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool {
- expectedMAC := genMAC(message, key, hashFunc)
- return hmac.Equal(messageMAC, expectedMAC)
- }
- // messageMAC returns the hex-decoded HMAC tag from the signature and its
- // corresponding hash function.
- func messageMAC(signature string) ([]byte, func() hash.Hash, error) {
- if signature == "" {
- return nil, nil, errors.New("missing signature")
- }
- sigParts := strings.SplitN(signature, "=", 2)
- if len(sigParts) != 2 {
- return nil, nil, fmt.Errorf("error parsing signature %q", signature)
- }
- var hashFunc func() hash.Hash
- switch sigParts[0] {
- case sha1Prefix:
- hashFunc = sha1.New
- case sha256Prefix:
- hashFunc = sha256.New
- case sha512Prefix:
- hashFunc = sha512.New
- default:
- return nil, nil, fmt.Errorf("unknown hash type prefix: %q", sigParts[0])
- }
- buf, err := hex.DecodeString(sigParts[1])
- if err != nil {
- return nil, nil, fmt.Errorf("error decoding signature %q: %v", signature, err)
- }
- return buf, hashFunc, nil
- }
- // ValidatePayload validates an incoming GitHub Webhook event request
- // and returns the (JSON) payload.
- // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded".
- // If the Content-Type is neither then an error is returned.
- // secretKey is the GitHub Webhook secret message.
- //
- // Example usage:
- //
- // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- // payload, err := github.ValidatePayload(r, s.webhookSecretKey)
- // if err != nil { ... }
- // // Process payload...
- // }
- //
- func ValidatePayload(r *http.Request, secretKey []byte) (payload []byte, err error) {
- var body []byte // Raw body that GitHub uses to calculate the signature.
- switch ct := r.Header.Get("Content-Type"); ct {
- case "application/json":
- var err error
- if body, err = ioutil.ReadAll(r.Body); err != nil {
- return nil, err
- }
- // If the content type is application/json,
- // the JSON payload is just the original body.
- payload = body
- case "application/x-www-form-urlencoded":
- // payloadFormParam is the name of the form parameter that the JSON payload
- // will be in if a webhook has its content type set to application/x-www-form-urlencoded.
- const payloadFormParam = "payload"
- var err error
- if body, err = ioutil.ReadAll(r.Body); err != nil {
- return nil, err
- }
- // If the content type is application/x-www-form-urlencoded,
- // the JSON payload will be under the "payload" form param.
- form, err := url.ParseQuery(string(body))
- if err != nil {
- return nil, err
- }
- payload = []byte(form.Get(payloadFormParam))
- default:
- return nil, fmt.Errorf("Webhook request has unsupported Content-Type %q", ct)
- }
- sig := r.Header.Get(signatureHeader)
- if err := validateSignature(sig, body, secretKey); err != nil {
- return nil, err
- }
- return payload, nil
- }
- // validateSignature validates the signature for the given payload.
- // signature is the GitHub hash signature delivered in the X-Hub-Signature header.
- // payload is the JSON payload sent by GitHub Webhooks.
- // secretKey is the GitHub Webhook secret message.
- //
- // GitHub API docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github
- func validateSignature(signature string, payload, secretKey []byte) error {
- messageMAC, hashFunc, err := messageMAC(signature)
- if err != nil {
- return err
- }
- if !checkMAC(payload, messageMAC, secretKey, hashFunc) {
- return errors.New("payload signature check failed")
- }
- return nil
- }
- // WebHookType returns the event type of webhook request r.
- //
- // GitHub API docs: https://developer.github.com/v3/repos/hooks/#webhook-headers
- func WebHookType(r *http.Request) string {
- return r.Header.Get(eventTypeHeader)
- }
- // DeliveryID returns the unique delivery ID of webhook request r.
- //
- // GitHub API docs: https://developer.github.com/v3/repos/hooks/#webhook-headers
- func DeliveryID(r *http.Request) string {
- return r.Header.Get(deliveryIDHeader)
- }
- // ParseWebHook parses the event payload. For recognized event types, a
- // value of the corresponding struct type will be returned (as returned
- // by Event.ParsePayload()). An error will be returned for unrecognized event
- // types.
- //
- // Example usage:
- //
- // func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- // payload, err := github.ValidatePayload(r, s.webhookSecretKey)
- // if err != nil { ... }
- // event, err := github.ParseWebHook(github.WebHookType(r), payload)
- // if err != nil { ... }
- // switch event := event.(type) {
- // case *github.CommitCommentEvent:
- // processCommitCommentEvent(event)
- // case *github.CreateEvent:
- // processCreateEvent(event)
- // ...
- // }
- // }
- //
- func ParseWebHook(messageType string, payload []byte) (interface{}, error) {
- eventType, ok := eventTypeMapping[messageType]
- if !ok {
- return nil, fmt.Errorf("unknown X-Github-Event in message: %v", messageType)
- }
- event := Event{
- Type: &eventType,
- RawPayload: (*json.RawMessage)(&payload),
- }
- return event.ParsePayload()
- }
|