github.com/google/go-github/v53@v53.2.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"
    22  	"mime"
    23  	"net/http"
    24  	"net/url"
    25  	"strings"
    26  )
    27  
    28  const (
    29  	// sha1Prefix is the prefix used by GitHub before the HMAC hexdigest.
    30  	sha1Prefix = "sha1"
    31  	// sha256Prefix and sha512Prefix are provided for future compatibility.
    32  	sha256Prefix = "sha256"
    33  	sha512Prefix = "sha512"
    34  	// SHA1SignatureHeader is the GitHub header key used to pass the HMAC-SHA1 hexdigest.
    35  	SHA1SignatureHeader = "X-Hub-Signature"
    36  	// SHA256SignatureHeader is the GitHub header key used to pass the HMAC-SHA256 hexdigest.
    37  	SHA256SignatureHeader = "X-Hub-Signature-256"
    38  	// EventTypeHeader is the GitHub header key used to pass the event type.
    39  	EventTypeHeader = "X-Github-Event"
    40  	// DeliveryIDHeader is the GitHub header key used to pass the unique ID for the webhook event.
    41  	DeliveryIDHeader = "X-Github-Delivery"
    42  )
    43  
    44  var (
    45  	// eventTypeMapping maps webhooks types to their corresponding go-github struct types.
    46  	eventTypeMapping = map[string]string{
    47  		"branch_protection_rule":         "BranchProtectionRuleEvent",
    48  		"check_run":                      "CheckRunEvent",
    49  		"check_suite":                    "CheckSuiteEvent",
    50  		"code_scanning_alert":            "CodeScanningAlertEvent",
    51  		"commit_comment":                 "CommitCommentEvent",
    52  		"content_reference":              "ContentReferenceEvent",
    53  		"create":                         "CreateEvent",
    54  		"delete":                         "DeleteEvent",
    55  		"deploy_key":                     "DeployKeyEvent",
    56  		"deployment":                     "DeploymentEvent",
    57  		"deployment_status":              "DeploymentStatusEvent",
    58  		"deployment_protection_rule":     "DeploymentProtectionRuleEvent",
    59  		"discussion":                     "DiscussionEvent",
    60  		"discussion_comment":             "DiscussionCommentEvent",
    61  		"fork":                           "ForkEvent",
    62  		"github_app_authorization":       "GitHubAppAuthorizationEvent",
    63  		"gollum":                         "GollumEvent",
    64  		"installation":                   "InstallationEvent",
    65  		"installation_repositories":      "InstallationRepositoriesEvent",
    66  		"issue_comment":                  "IssueCommentEvent",
    67  		"issues":                         "IssuesEvent",
    68  		"label":                          "LabelEvent",
    69  		"marketplace_purchase":           "MarketplacePurchaseEvent",
    70  		"member":                         "MemberEvent",
    71  		"membership":                     "MembershipEvent",
    72  		"merge_group":                    "MergeGroupEvent",
    73  		"meta":                           "MetaEvent",
    74  		"milestone":                      "MilestoneEvent",
    75  		"organization":                   "OrganizationEvent",
    76  		"org_block":                      "OrgBlockEvent",
    77  		"package":                        "PackageEvent",
    78  		"page_build":                     "PageBuildEvent",
    79  		"ping":                           "PingEvent",
    80  		"project":                        "ProjectEvent",
    81  		"project_card":                   "ProjectCardEvent",
    82  		"project_column":                 "ProjectColumnEvent",
    83  		"public":                         "PublicEvent",
    84  		"pull_request":                   "PullRequestEvent",
    85  		"pull_request_review":            "PullRequestReviewEvent",
    86  		"pull_request_review_comment":    "PullRequestReviewCommentEvent",
    87  		"pull_request_review_thread":     "PullRequestReviewThreadEvent",
    88  		"pull_request_target":            "PullRequestTargetEvent",
    89  		"push":                           "PushEvent",
    90  		"repository":                     "RepositoryEvent",
    91  		"repository_dispatch":            "RepositoryDispatchEvent",
    92  		"repository_import":              "RepositoryImportEvent",
    93  		"repository_vulnerability_alert": "RepositoryVulnerabilityAlertEvent",
    94  		"release":                        "ReleaseEvent",
    95  		"secret_scanning_alert":          "SecretScanningAlertEvent",
    96  		"security_advisory":              "SecurityAdvisoryEvent",
    97  		"star":                           "StarEvent",
    98  		"status":                         "StatusEvent",
    99  		"team":                           "TeamEvent",
   100  		"team_add":                       "TeamAddEvent",
   101  		"user":                           "UserEvent",
   102  		"watch":                          "WatchEvent",
   103  		"workflow_dispatch":              "WorkflowDispatchEvent",
   104  		"workflow_job":                   "WorkflowJobEvent",
   105  		"workflow_run":                   "WorkflowRunEvent",
   106  	}
   107  )
   108  
   109  // genMAC generates the HMAC signature for a message provided the secret key
   110  // and hashFunc.
   111  func genMAC(message, key []byte, hashFunc func() hash.Hash) []byte {
   112  	mac := hmac.New(hashFunc, key)
   113  	mac.Write(message)
   114  	return mac.Sum(nil)
   115  }
   116  
   117  // checkMAC reports whether messageMAC is a valid HMAC tag for message.
   118  func checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool {
   119  	expectedMAC := genMAC(message, key, hashFunc)
   120  	return hmac.Equal(messageMAC, expectedMAC)
   121  }
   122  
   123  // messageMAC returns the hex-decoded HMAC tag from the signature and its
   124  // corresponding hash function.
   125  func messageMAC(signature string) ([]byte, func() hash.Hash, error) {
   126  	if signature == "" {
   127  		return nil, nil, errors.New("missing signature")
   128  	}
   129  	sigParts := strings.SplitN(signature, "=", 2)
   130  	if len(sigParts) != 2 {
   131  		return nil, nil, fmt.Errorf("error parsing signature %q", signature)
   132  	}
   133  
   134  	var hashFunc func() hash.Hash
   135  	switch sigParts[0] {
   136  	case sha1Prefix:
   137  		hashFunc = sha1.New
   138  	case sha256Prefix:
   139  		hashFunc = sha256.New
   140  	case sha512Prefix:
   141  		hashFunc = sha512.New
   142  	default:
   143  		return nil, nil, fmt.Errorf("unknown hash type prefix: %q", sigParts[0])
   144  	}
   145  
   146  	buf, err := hex.DecodeString(sigParts[1])
   147  	if err != nil {
   148  		return nil, nil, fmt.Errorf("error decoding signature %q: %v", signature, err)
   149  	}
   150  	return buf, hashFunc, nil
   151  }
   152  
   153  // ValidatePayloadFromBody validates an incoming GitHub Webhook event request body
   154  // and returns the (JSON) payload.
   155  // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded".
   156  // If the Content-Type is neither then an error is returned.
   157  // secretToken is the GitHub Webhook secret token.
   158  // If your webhook does not contain a secret token, you can pass an empty secretToken.
   159  // Webhooks without a secret token are not secure and should be avoided.
   160  //
   161  // Example usage:
   162  //
   163  //	func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   164  //	  // read signature from request
   165  //	  signature := ""
   166  //	  payload, err := github.ValidatePayloadFromBody(r.Header.Get("Content-Type"), r.Body, signature, s.webhookSecretKey)
   167  //	  if err != nil { ... }
   168  //	  // Process payload...
   169  //	}
   170  func ValidatePayloadFromBody(contentType string, readable io.Reader, signature string, secretToken []byte) (payload []byte, err error) {
   171  	var body []byte // Raw body that GitHub uses to calculate the signature.
   172  
   173  	switch contentType {
   174  	case "application/json":
   175  		var err error
   176  		if body, err = io.ReadAll(readable); err != nil {
   177  			return nil, err
   178  		}
   179  
   180  		// If the content type is application/json,
   181  		// the JSON payload is just the original body.
   182  		payload = body
   183  
   184  	case "application/x-www-form-urlencoded":
   185  		// payloadFormParam is the name of the form parameter that the JSON payload
   186  		// will be in if a webhook has its content type set to application/x-www-form-urlencoded.
   187  		const payloadFormParam = "payload"
   188  
   189  		var err error
   190  		if body, err = io.ReadAll(readable); err != nil {
   191  			return nil, err
   192  		}
   193  
   194  		// If the content type is application/x-www-form-urlencoded,
   195  		// the JSON payload will be under the "payload" form param.
   196  		form, err := url.ParseQuery(string(body))
   197  		if err != nil {
   198  			return nil, err
   199  		}
   200  		payload = []byte(form.Get(payloadFormParam))
   201  
   202  	default:
   203  		return nil, fmt.Errorf("webhook request has unsupported Content-Type %q", contentType)
   204  	}
   205  
   206  	// Validate the signature if present or if one is expected (secretToken is non-empty).
   207  	if len(secretToken) > 0 || len(signature) > 0 {
   208  		if err := ValidateSignature(signature, body, secretToken); err != nil {
   209  			return nil, err
   210  		}
   211  	}
   212  
   213  	return payload, nil
   214  }
   215  
   216  // ValidatePayload validates an incoming GitHub Webhook event request
   217  // and returns the (JSON) payload.
   218  // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded".
   219  // If the Content-Type is neither then an error is returned.
   220  // secretToken is the GitHub Webhook secret token.
   221  // If your webhook does not contain a secret token, you can pass nil or an empty slice.
   222  // This is intended for local development purposes only and all webhooks should ideally set up a secret token.
   223  //
   224  // Example usage:
   225  //
   226  //	func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   227  //	  payload, err := github.ValidatePayload(r, s.webhookSecretKey)
   228  //	  if err != nil { ... }
   229  //	  // Process payload...
   230  //	}
   231  func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err error) {
   232  	signature := r.Header.Get(SHA256SignatureHeader)
   233  	if signature == "" {
   234  		signature = r.Header.Get(SHA1SignatureHeader)
   235  	}
   236  
   237  	contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  
   242  	return ValidatePayloadFromBody(contentType, r.Body, signature, secretToken)
   243  }
   244  
   245  // ValidateSignature validates the signature for the given payload.
   246  // signature is the GitHub hash signature delivered in the X-Hub-Signature header.
   247  // payload is the JSON payload sent by GitHub Webhooks.
   248  // secretToken is the GitHub Webhook secret token.
   249  //
   250  // GitHub API docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github
   251  func ValidateSignature(signature string, payload, secretToken []byte) error {
   252  	messageMAC, hashFunc, err := messageMAC(signature)
   253  	if err != nil {
   254  		return err
   255  	}
   256  	if !checkMAC(payload, messageMAC, secretToken, hashFunc) {
   257  		return errors.New("payload signature check failed")
   258  	}
   259  	return nil
   260  }
   261  
   262  // WebHookType returns the event type of webhook request r.
   263  //
   264  // GitHub API docs: https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types
   265  func WebHookType(r *http.Request) string {
   266  	return r.Header.Get(EventTypeHeader)
   267  }
   268  
   269  // DeliveryID returns the unique delivery ID of webhook request r.
   270  //
   271  // GitHub API docs: https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types
   272  func DeliveryID(r *http.Request) string {
   273  	return r.Header.Get(DeliveryIDHeader)
   274  }
   275  
   276  // ParseWebHook parses the event payload. For recognized event types, a
   277  // value of the corresponding struct type will be returned (as returned
   278  // by Event.ParsePayload()). An error will be returned for unrecognized event
   279  // types.
   280  //
   281  // Example usage:
   282  //
   283  //	func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   284  //	  payload, err := github.ValidatePayload(r, s.webhookSecretKey)
   285  //	  if err != nil { ... }
   286  //	  event, err := github.ParseWebHook(github.WebHookType(r), payload)
   287  //	  if err != nil { ... }
   288  //	  switch event := event.(type) {
   289  //	  case *github.CommitCommentEvent:
   290  //	      processCommitCommentEvent(event)
   291  //	  case *github.CreateEvent:
   292  //	      processCreateEvent(event)
   293  //	  ...
   294  //	  }
   295  //	}
   296  func ParseWebHook(messageType string, payload []byte) (interface{}, error) {
   297  	eventType, ok := eventTypeMapping[messageType]
   298  	if !ok {
   299  		return nil, fmt.Errorf("unknown X-Github-Event in message: %v", messageType)
   300  	}
   301  
   302  	event := Event{
   303  		Type:       &eventType,
   304  		RawPayload: (*json.RawMessage)(&payload),
   305  	}
   306  	return event.ParsePayload()
   307  }