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