golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/pubsubhelper/github.go (about)

     1  // Copyright 2017 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"crypto/hmac"
     9  	"crypto/sha1"
    10  	"crypto/sha256"
    11  	"encoding/hex"
    12  	"encoding/json"
    13  	"fmt"
    14  	"hash"
    15  	"io"
    16  	"log"
    17  	"net/http"
    18  	"strings"
    19  
    20  	"golang.org/x/build/cmd/pubsubhelper/pubsubtypes"
    21  )
    22  
    23  func handleGithubWebhook(w http.ResponseWriter, r *http.Request) {
    24  	if r.TLS == nil {
    25  		http.Error(w, "HTTPS required", http.StatusBadRequest)
    26  		return
    27  	}
    28  	body, err := validateGithubRequest(w, r)
    29  	if err != nil {
    30  		log.Printf("failed to validate github webhook request: %v", err)
    31  		// But send a 200 OK anyway, so they don't queue up on
    32  		// Github's side if they're real.
    33  		return
    34  	}
    35  
    36  	var payload githubWebhookPayload
    37  	if err := json.Unmarshal(body, &payload); err != nil {
    38  		log.Printf("error unmarshalling payload: %v; payload=%s", err, body)
    39  		// But send a 200 OK anyway. Our fault.
    40  		return
    41  	}
    42  	back, _ := json.MarshalIndent(payload, "", "\t")
    43  	log.Printf("github verified webhook: %s", back)
    44  
    45  	if payload.Repository == nil || (payload.Issue == nil && payload.PullRequest == nil) {
    46  		// Ignore.
    47  		return
    48  	}
    49  
    50  	f := strings.Split(payload.Repository.FullName, "/")
    51  	if len(f) != 2 {
    52  		log.Printf("bogus repository name %q", payload.Repository.FullName)
    53  		return
    54  	}
    55  	owner, repo := f[0], f[1]
    56  
    57  	var issueNumber int
    58  	if payload.Issue != nil {
    59  		issueNumber = payload.Issue.Number
    60  	}
    61  	var prNumber int
    62  	if payload.PullRequest != nil {
    63  		prNumber = payload.PullRequest.Number
    64  	}
    65  
    66  	publish(&pubsubtypes.Event{
    67  		GitHub: &pubsubtypes.GitHubEvent{
    68  			Action:            payload.Action,
    69  			RepoOwner:         owner,
    70  			Repo:              repo,
    71  			IssueNumber:       issueNumber,
    72  			PullRequestNumber: prNumber,
    73  		},
    74  	})
    75  }
    76  
    77  // validateGithubRequest compares the signature in the request header with the body.
    78  func validateGithubRequest(w http.ResponseWriter, r *http.Request) (body []byte, err error) {
    79  	// Decode signature header.
    80  	sigHeader := r.Header.Get("X-Hub-Signature")
    81  	sigParts := strings.SplitN(sigHeader, "=", 2)
    82  	if len(sigParts) != 2 {
    83  		return nil, fmt.Errorf("Bad signature header: %q", sigHeader)
    84  	}
    85  	var h func() hash.Hash
    86  	switch alg := sigParts[0]; alg {
    87  	case "sha1":
    88  		h = sha1.New
    89  	case "sha256":
    90  		h = sha256.New
    91  	default:
    92  		return nil, fmt.Errorf("Unsupported hash algorithm: %q", alg)
    93  	}
    94  	gotSig, err := hex.DecodeString(sigParts[1])
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	body, err = io.ReadAll(http.MaxBytesReader(w, r.Body, 5<<20))
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  	// TODO(golang/go#37171): find a cleaner solution than using a global
   104  	mac := hmac.New(h, []byte(*webhookSecret))
   105  	mac.Write(body)
   106  	expectSig := mac.Sum(nil)
   107  
   108  	if !hmac.Equal(gotSig, expectSig) {
   109  		return nil, fmt.Errorf("Invalid signature %X, want %x", gotSig, expectSig)
   110  	}
   111  	return body, nil
   112  }
   113  
   114  type githubWebhookPayload struct {
   115  	Action      string             `json:"action"`
   116  	Repository  *githubRepository  `json:"repository"`
   117  	Issue       *githubIssue       `json:"issue"`
   118  	PullRequest *githubPullRequest `json:"pull_request"`
   119  }
   120  
   121  type githubRepository struct {
   122  	FullName string `json:"full_name"` // "golang/go"
   123  }
   124  
   125  type githubIssue struct {
   126  	URL    string `json:"url"`    // https://api.github.com/repos/baxterthehacker/public-repo/issues/2
   127  	Number int    `json:"number"` // 2
   128  }
   129  
   130  type githubPullRequest struct {
   131  	URL    string `json:"url"`    // https://api.github.com/repos/baxterthehacker/public-repo/pulls/8
   132  	Number int    `json:"number"` // 8
   133  }