blob: 66c467a1afdef5e5408f8add8bdbbe1971512720 [file] [log] [blame]
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"hash"
"io"
"log"
"net/http"
"strings"
"golang.org/x/build/cmd/pubsubhelper/pubsubtypes"
)
func handleGithubWebhook(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil {
http.Error(w, "HTTPS required", http.StatusBadRequest)
return
}
body, err := validateGithubRequest(w, r)
if err != nil {
log.Printf("failed to validate github webhook request: %v", err)
// But send a 200 OK anyway, so they don't queue up on
// Github's side if they're real.
return
}
var payload githubWebhookPayload
if err := json.Unmarshal(body, &payload); err != nil {
log.Printf("error unmarshalling payload: %v; payload=%s", err, body)
// But send a 200 OK anyway. Our fault.
return
}
back, _ := json.MarshalIndent(payload, "", "\t")
log.Printf("github verified webhook: %s", back)
if payload.Repository == nil || (payload.Issue == nil && payload.PullRequest == nil) {
// Ignore.
return
}
f := strings.Split(payload.Repository.FullName, "/")
if len(f) != 2 {
log.Printf("bogus repository name %q", payload.Repository.FullName)
return
}
owner, repo := f[0], f[1]
var issueNumber int
if payload.Issue != nil {
issueNumber = payload.Issue.Number
}
var prNumber int
if payload.PullRequest != nil {
prNumber = payload.PullRequest.Number
}
publish(&pubsubtypes.Event{
GitHub: &pubsubtypes.GitHubEvent{
Action: payload.Action,
RepoOwner: owner,
Repo: repo,
IssueNumber: issueNumber,
PullRequestNumber: prNumber,
},
})
}
// validateGithubRequest compares the signature in the request header with the body.
func validateGithubRequest(w http.ResponseWriter, r *http.Request) (body []byte, err error) {
// Decode signature header.
sigHeader := r.Header.Get("X-Hub-Signature")
sigParts := strings.SplitN(sigHeader, "=", 2)
if len(sigParts) != 2 {
return nil, fmt.Errorf("Bad signature header: %q", sigHeader)
}
var h func() hash.Hash
switch alg := sigParts[0]; alg {
case "sha1":
h = sha1.New
case "sha256":
h = sha256.New
default:
return nil, fmt.Errorf("Unsupported hash algorithm: %q", alg)
}
gotSig, err := hex.DecodeString(sigParts[1])
if err != nil {
return nil, err
}
body, err = io.ReadAll(http.MaxBytesReader(w, r.Body, 5<<20))
if err != nil {
return nil, err
}
// TODO(golang/go#37171): find a cleaner solution than using a global
mac := hmac.New(h, []byte(*webhookSecret))
mac.Write(body)
expectSig := mac.Sum(nil)
if !hmac.Equal(gotSig, expectSig) {
return nil, fmt.Errorf("Invalid signature %X, want %x", gotSig, expectSig)
}
return body, nil
}
type githubWebhookPayload struct {
Action string `json:"action"`
Repository *githubRepository `json:"repository"`
Issue *githubIssue `json:"issue"`
PullRequest *githubPullRequest `json:"pull_request"`
}
type githubRepository struct {
FullName string `json:"full_name"` // "golang/go"
}
type githubIssue struct {
URL string `json:"url"` // https://api.github.com/repos/baxterthehacker/public-repo/issues/2
Number int `json:"number"` // 2
}
type githubPullRequest struct {
URL string `json:"url"` // https://api.github.com/repos/baxterthehacker/public-repo/pulls/8
Number int `json:"number"` // 8
}