github.com/google/go-github/v33@v33.0.0/github/messages.go (about)

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