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 }